From 97129c4fa863de43abf2d637017d2f5b1416b301 Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 13:10:55 +0000 Subject: [PATCH 01/16] fix: typo in NAT_OUTBOUND_REQUIRED_IF --- plugins/module_utils/nat_outbound.py | 2 +- plugins/modules/pfsense_aggregate.py | 4 ++-- plugins/modules/pfsense_nat_outbound.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/nat_outbound.py b/plugins/module_utils/nat_outbound.py index ca5662d8..bb843e6e 100644 --- a/plugins/module_utils/nat_outbound.py +++ b/plugins/module_utils/nat_outbound.py @@ -33,7 +33,7 @@ before=dict(required=False, type='str'), ) -NAT_OUTBOUD_REQUIRED_IF = [ +NAT_OUTBOUND_REQUIRED_IF = [ ["state", "present", ["interface", "source", "destination"]] ] diff --git a/plugins/modules/pfsense_aggregate.py b/plugins/modules/pfsense_aggregate.py index 354fe561..1b8325f1 100644 --- a/plugins/modules/pfsense_aggregate.py +++ b/plugins/modules/pfsense_aggregate.py @@ -595,7 +595,7 @@ INTERFACE_REQUIRED_IF, INTERFACE_MUTUALLY_EXCLUSIVE, ) -from ansible_collections.pfsensible.core.plugins.module_utils.nat_outbound import PFSenseNatOutboundModule, NAT_OUTBOUND_ARGUMENT_SPEC, NAT_OUTBOUD_REQUIRED_IF +from ansible_collections.pfsensible.core.plugins.module_utils.nat_outbound import PFSenseNatOutboundModule, NAT_OUTBOUND_ARGUMENT_SPEC, NAT_OUTBOUND_REQUIRED_IF from ansible_collections.pfsensible.core.plugins.module_utils.nat_port_forward import ( PFSenseNatPortForwardModule, NAT_PORT_FORWARD_ARGUMENT_SPEC, @@ -1058,7 +1058,7 @@ def main(): type='list', elements='dict', options=INTERFACE_ARGUMENT_SPEC, required_if=INTERFACE_REQUIRED_IF, mutually_exclusive=INTERFACE_MUTUALLY_EXCLUSIVE), aggregated_rules=dict(type='list', elements='dict', options=RULE_ARGUMENT_SPEC, required_if=RULE_REQUIRED_IF), - aggregated_nat_outbounds=dict(type='list', elements='dict', options=NAT_OUTBOUND_ARGUMENT_SPEC, required_if=NAT_OUTBOUD_REQUIRED_IF), + aggregated_nat_outbounds=dict(type='list', elements='dict', options=NAT_OUTBOUND_ARGUMENT_SPEC, required_if=NAT_OUTBOUND_REQUIRED_IF), aggregated_nat_port_forwards=dict(type='list', elements='dict', options=NAT_PORT_FORWARD_ARGUMENT_SPEC, required_if=NAT_PORT_FORWARD_REQUIRED_IF), aggregated_rule_separators=dict( type='list', elements='dict', diff --git a/plugins/modules/pfsense_nat_outbound.py b/plugins/modules/pfsense_nat_outbound.py index 19b8471e..72232c0e 100644 --- a/plugins/modules/pfsense_nat_outbound.py +++ b/plugins/modules/pfsense_nat_outbound.py @@ -123,13 +123,13 @@ """ from ansible.module_utils.basic import AnsibleModule -from ansible_collections.pfsensible.core.plugins.module_utils.nat_outbound import PFSenseNatOutboundModule, NAT_OUTBOUND_ARGUMENT_SPEC, NAT_OUTBOUD_REQUIRED_IF +from ansible_collections.pfsensible.core.plugins.module_utils.nat_outbound import PFSenseNatOutboundModule, NAT_OUTBOUND_ARGUMENT_SPEC, NAT_OUTBOUND_REQUIRED_IF def main(): module = AnsibleModule( argument_spec=NAT_OUTBOUND_ARGUMENT_SPEC, - required_if=NAT_OUTBOUD_REQUIRED_IF, + required_if=NAT_OUTBOUND_REQUIRED_IF, supports_check_mode=True) pfmodule = PFSenseNatOutboundModule(module) From a7657ea6f80078d56a95028573ad39329a1fdee9 Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 13:55:42 +0000 Subject: [PATCH 02/16] fix: kill dhclient process when interface was dhcp --- plugins/module_utils/interface.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/module_utils/interface.py b/plugins/module_utils/interface.py index 08db349e..a20646b3 100644 --- a/plugins/module_utils/interface.py +++ b/plugins/module_utils/interface.py @@ -220,9 +220,17 @@ def _copy_and_update_target(self): if changed: if self.params['enable']: self.setup_interface_cmds += "interface_bring_down('{0}', false);\n".format(self.target_elt.tag) + + # possibly kill remaining dhclient process + if 'ipaddr' in before and before['ipaddr'] == 'dhcp': + self.setup_interface_cmds += "kill_dhclient_process(get_real_interface({0}));\n".format(self.target_elt.tag) + self.setup_interface_cmds += "interface_configure('{0}', true);\n".format(self.target_elt.tag) else: self.setup_interface_cmds += "interface_bring_down('{0}', true);\n".format(self.target_elt.tag) + # possibly kill remaining dhclient process + if 'ipaddr' in before and before['ipaddr'] == 'dhcp': + self.setup_interface_cmds += "kill_dhclient_process(get_real_interface({0}));\n".format(self.target_elt.tag) return (before, changed) From 013e91bac4bff51797d28f94478e6449d348b873 Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 13:36:38 +0000 Subject: [PATCH 03/16] feat: add interface_facts gather module --- plugins/modules/pfsense_interface_facts.py | 143 +++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 plugins/modules/pfsense_interface_facts.py diff --git a/plugins/modules/pfsense_interface_facts.py b/plugins/modules/pfsense_interface_facts.py new file mode 100644 index 00000000..5f547640 --- /dev/null +++ b/plugins/modules/pfsense_interface_facts.py @@ -0,0 +1,143 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_interface_facts +version_added: 0.5.2 +author: Jan Wenzel (@coffeelover) +short_description: Gather pfsense interfaces +description: + - Gather pfSense interfaces. +options: +notes: +""" + +EXAMPLES = """ +- name: Gather interface facts + pfsense_interface_facts: +""" + +RETURN = """ +ansible_facts: + description: Facts to add to ansible_facts. + returned: always + type: complex + contains: + interface_facts: + description: + - Maps the interfaces to a list of dicts with additional interface information + returned: always + type: dict + contains: + dmesg: + description: The interface information from dmesg + returned: always + type: str + friendly: + description: The interface friendly name used in the config + returned: always + type: str + interface_name: + description: The interface name + returned: always + type: str + ipaddr: + description: The ipv4 address + returned: always + type: str + macaddr: + description: The MAC address + returned: always + type: str + up: + description: The up status + returned: always + type: bool + sample: |- + { + "interface_facts": [ + { + "dmesg": "Intel(R) PRO/1000 Network Connection", + "friendly": "wan", + "interface_name": "em0", + "ipaddr": "192.168.178.190", + "mac": "08:00:27:86:e7:0f", + "up": true + }, + { + "dmesg": "Intel(R) PRO/1000 Network Connection", + "friendly": "lan", + "interface_name": "em1", + "ipaddr": "192.168.61.10", + "mac": "08:00:27:a1:4c:70", + "up": true + } + ], + } +""" + +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase +from ansible_collections.pfsensible.core.plugins.module_utils.interface import PFSenseInterfaceModule +from ansible.module_utils.basic import AnsibleModule + +INTERFACE_FACTS_ARGUMENT_SPEC = dict() +INTERFACE_FACTS_REQUIRED_IF = [] +INTERFACE_FACTS_MUTUALLY_EXCLUSIVE = [] + + +class PFSenseInterfaceFactsModule(PFSenseModuleBase): + """ module managing pfsense interfaces """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return INTERFACE_FACTS_ARGUMENT_SPEC + + def __init__(self, module, pfsense=None): + super(PFSenseInterfaceFactsModule, self).__init__(module, pfsense) + self.name = "pfsense_interface_facts" + self.apply = False + self.obj = dict() + self.root_elt = self.pfsense.interfaces + self.interface_module = PFSenseInterfaceModule(module) + + def run(self, params): + """ process input params to add/update/delete """ + from pprint import pformat + interfaces = self.interface_module._get_interface_list() + results = {'ansible_facts': { + 'interface_facts': [] + }} + for interface in interfaces: + interface_facts = {'interface_name': interface} + interface_facts.update(interfaces[interface]) + if 'friendly' not in interface_facts: + interface_facts['friendly'] = None + results['ansible_facts']['interface_facts'].append(interface_facts) + self.module.exit_json(**results) + + +def main(): + module = AnsibleModule( + argument_spec=INTERFACE_FACTS_ARGUMENT_SPEC, + required_if=INTERFACE_FACTS_REQUIRED_IF, + mutually_exclusive=INTERFACE_FACTS_MUTUALLY_EXCLUSIVE, + supports_check_mode=True) + + pfmodule = PFSenseInterfaceFactsModule(module) + pfmodule.run(module.params) + + +if __name__ == '__main__': + main() From 1b4fe8fb12e2fa52514915f498335c03b917d81d Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 13:40:37 +0000 Subject: [PATCH 04/16] feat: add squid module --- plugins/modules/pfsense_squid.py | 2043 ++++++++++++++++++++++++++++++ 1 file changed, 2043 insertions(+) create mode 100644 plugins/modules/pfsense_squid.py diff --git a/plugins/modules/pfsense_squid.py b/plugins/modules/pfsense_squid.py new file mode 100644 index 00000000..5015734c --- /dev/null +++ b/plugins/modules/pfsense_squid.py @@ -0,0 +1,2043 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# Copyright: (c) 2022, Geno +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_squid +version_added: "0.4.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage squid http proxy settings +description: + - Manage pfSense squid http proxy settings +notes: +options: + auth: + description: Squid Authentication Config + type: dict + suboptions: + auth_method: + description: Authentication Method + type: str + choices: ["none", "local", "ldap", "radius", "cp"] + default: none + auth_server: + description: Authentication Server + type: str + auth_server_port: + description: Authentication Server Port + type: int + auth_prompt: + description: Authentication Prompt + type: str + default: "Please enter your credentials to access the proxy" + auth_processes: + description: Authentication Processes + type: int + default: 5 + auth_ttl: + description: Authentication Time-To-Live + type: int + default: 5 + max_user_ip: + description: Authentication Max User IP Addresses + type: int + unrestricted_auth: + description: Required Authentication for Unrestricted IPs + type: bool + no_auth_hosts: + description: Subnets That Don't Need Authentication + type: list + elements: str + ldap_version: + description: LDAP Version + type: int + choices: [2, 3] + default: 2 + ldap_urltype: + description: LDAP Transport + type: str + choices: ["standard", "starttls", "ssl"] + default: standard + ldap_user: + description: LDAP Server User DN + type: str + ldap_pass: + description: LDAP Password + type: str + ldap_basedomain: + description: LDAP Base Domain + type: str + ldap_userattribute: + description: LDAP Username DN Attribute + type: str + default: uid + ldap_filter: + description: LDAP Search Filter + type: str + default: (&(objectClass=person)(uid=%s)) + ldap_noreferrals: + description: LDAP not follow referrals + type: bool + antivirus: + description: Squid Antivirus Config + type: dict + suboptions: + enable: + description: Enable Antivirus + type: bool + client_info: + description: Client Forward Options + type: str + choices: ["both", "username", "ip", "none"] + default: both + enable_advanced: + description: Enable Manual Configuration + type: bool + default: False + clamav_url: + description: Redirect URL + type: str + default: emtpy for Squid/pfSense WebGUI URL + clamav_scan_type: + description: Scan Type + type: str + choices: ["all", "web", "app"] + default: all + clamav_disable_stream_scanning: + description: Exclude Audio/Video Streams + type: bool + clamav_block_pua: + description: Block Potentially Unwanted Applications + type: bool + clamav_update: + description: ClamAV Database Update (hours) + type: int + choices: [0, 1, 2, 3, 4, 6, 8, 12, 24] + default: 0 + clamav_dbregion: + description: Regional ClamAV Database Update Mirror + type: str + choices: ["", "au", "europe", "ca", "cn", "id", "jp", "kr", "ml", "ru", "sa", "tw", "uk", "us"] + default: "" + clamav_dbservers: + description: Optional ClamAV Database Update Servers + type: list + elements: str + urlhaus_sig: + description: Enables URLhaus active malware distribution sites DB support. + type: bool + interserver_sig: + description: Enables InterServer.net malware DB support + type: bool + securiteinfo_sig: + description: Enables SecuriteInfo.com malware DB support + type: bool + securiteinfo_premium: + description: Enables SecuriteInfo.com 0-day malware DB support + type: bool + securiteinfo_id: + description: SecuriteInfo ID + type: str + raw_squidclamav_conf: + description: Contents of squidclamav.conf + type: str + raw_cicap_conf: + description: Contents of c-icap.conf + type: str + raw_cicap_magic: + description: Contents of c-icap.magic + type: str + raw_freshclam_conf: + description: Contents of freshclam.conf + type: str + raw_clamd_conf: + description: Contents of clamd.conf + type: str + cache: + description: Squid Local Cache Config + type: dict + suboptions: + nocache: + description: Disable caching completely + type: bool + cache_replacement_policy: + description: Cache Replacement Policy + type: str + choices: ["heap LFUDA", "heap GDSF", "heap LRU", "LRU"] + default: "heap LFUDA" + cache_swap_low: + description: Low-Water Mark in % + type: int + default: 90 + cache_swap_high: + description: High-Water Mark in % + type: int + default: 95 + donotcache: + description: Domain(s) and/or IP address(es) that should never be cached + type: list + elements: str + enable_offline: + description: Enable Offline Mode + type: bool + ext_cachemanager: + description: IPs of external cache managers + type: list + elements: str + harddisk_cache_size: + description: Hard Disk Cache Size (MB) + type: int + default: 100 + harddisk_cache_system: + description: Kind of storage system to use + type: str + choices: ["aufs", "diskd", "null", "ufs"] + default: ufs + level1_subdirs: + description: Level 1 Directories + type: int + choices: [4, 8, 16, 32, 64, 128, 256] + default: 16 + harddisk_cache_location: + description: Hard Disk Cache Location + type: path + default: /var/squid/cache + minimum_object_size: + description: Minimum Object Size (KB) + type: int + default: 0 + maximum_object_size: + description: Maximum Object Size (MB) + type: int + default: 4 + memory_cache_size: + description: Memory Cache Size (MB) + type: int + default: 64 + maximum_objsize_in_mem: + description: Maximum Object Size in RAM (KB) + type: int + default: 256 + memory_replacement_policy: + description: Memory Replacement Policy + type: str + choices: ["heap GDSF", "heap LFUDA", "heap LRU", "LRU"] + default: "heap GDFS" + cache_dynamic_content: + description: Cache Dynamic Content + type: bool + custom_refresh_patterns: + description: Custom Refresh Patterns + type: list + elements: str + general: + description: General Squid Config + type: dict + suboptions: + enable_squid: + description: Enable Squid HTTP Proxy + type: bool + keep_squid_data: + description: If enabled, the settings, logs, cache, AV defs and other data will be preserved across package reinstalls. + type: bool + listenproto: + description: Listen IP Version + type: str + choices: [ "inet", "inet6", "any"] + default: inet + carpstatusvid: + description: CARP Status VIP + type: str + active_interface: + description: Proxy Interface(s) + type: list + elements: str + default: ["lan"] + outgoing_interface: + description: Outgoing Network Interface + type: str + default: lan + proxy_port: + description: Proxy Port + type: int + default: 3128 + icp_port: + description: ICP Port + type: int + allow_interface: + description: Allow Users on Interface + type: bool + default: True + dns_v4_first: + description: Resolve DNS IPv4 First + type: bool + disable_pinger: + description: Disable ICMP + type: bool + dns_nameservers: + description: Use Alternate DNS Servers for the Proxy Server + type: list + elements: str + extraca: + description: Extra Trusted CA + type: str + transparent_proxy: + description: Enable Transparent HTTP Proxy + type: bool + transparent_active_interface: + description: Transparent Proxy Interface(s) + type: list + elements: str + private_subnet_proxy_off: + description: Bypass Proxy for Private Address Destination + type: bool + defined_ip_proxy_off: + description: Bypass Proxy for These Source IPs + type: list + elements: str + defined_ip_proxy_off_dest: + description: Bypass Proxy for These Destination IPs + type: list + elements: str + ssl_proxy: + description: Enable HTTPS/SSL Interception + type: bool + sslproxy_mitm_mode: + description: SSL/MITM Mode + type: str + choices: ["splicewhitelist", "spliceall", "custom"] + default: splicewhitelist + ssl_active_interface: + description: SSL Intercept Interface(s) + type: list + elements: str + ssl_proxy_port: + description: SSL Proxy Port while using transparent mode + type: int + default: 3129 + sslproxy_compatibility_mode: + description: SSL Proxy Compatibility Mode + type: str + choices: ["modern", "intermediate"] + default: modern + dhparams_size: + description: DHParams Key Size + type: int + choices: [1024, 2048, 4096] + default: 2048 + dca: + description: Certificate Authority + type: str + sslcrtd_children: + description: SSL Certificate Daemon Children + type: int + default: 5 + interception_checks: + description: Remote Cert Checks + type: list + elements: str + choices: ["sslproxy_cert_error", "sslproxy_flags"] + interception_adapt: + description: Certificate Adapt + type: list + elements: str + choices: ["setValidAfter", "setValidBefore", "setCommonName"] + log_enabled: + description: Enable Access Logging + type: bool + log_dir: + description: Log Store Directory + type: path + default: /var/squid/logs + log_rotate: + description: Rotate Logs (days) + type: int + log_sqd: + description: Log Pages Denied by SquidGuard + type: bool + visible_hostname: + description: Visible Hostname + type: str + default: localhost + admin_email: + description: Administrator'r email + type: str + default: admin@localhost + error_language: + description: Error Language + type: str + choices: ["af", "ar", "az", "bg", "ca", "cs", "da", "de", + "el", "en", "es", "et", "fa", "fi", "fr", "he", + "hu", "hy", "id", "it", "ja", "ko", "lt", "lv", + "ms", "nl", "oc", "pl", "pt", "pt-br", "ro", + "ru", "sk", "sl", "sr-cyrl", "sr-latn", "sv", + "th", "tr", "uk", "uz", "vi", "zh-cn", "zh-tw"] + default: en + xforward_mode: + description: X-Forwarded Header Mode + type: str + choices: ["on", "off", "transparent", "delete", "truncate"] + default: on + disable_via: + description: Disable VIA Header + type: bool + uri_whitespace: + description: URI Whitespace Characters Handling + type: str + choices: ["allow", "chop", "deny", "encode", "strip"] + default: strip + disable_squidversion: + description: Suppress Squid Version + type: bool + custom_options: + description: Integrations + type: str + custom_options_squid3: + description: Custome Options (Before Auth) + type: str + custom_options2_squid3: + description: Custome Options (After Auth) + type: str + custom_options3_squid3: + description: Custome Options (SSL/MITM) + type: str + nac: + description: Network Access Control Squid Config + type: dict + suboptions: + allowed_subnets: + description: Allowed Subnets + type: list + elements: str + unrestricted_hosts: + description: Unrestricted IPs + type: list + elements: str + banned_hosts: + description: Banned Hosts Addresses + type: list + elements: str + whitelist: + description: Whitelist + type: list + elements: str + blacklist: + description: Blacklist + type: list + elements: str + block_user_agent: + description: Block User Agents + type: list + elements: str + block_reply_mime_type: + description: Block MIME Types (Reply Only) + type: list + elements: str + addtl_ports: + description: ACL SafePorts (in addition to 21,70,80,210,280,443,488,563,591,631,777,901,1025-65535) + type: list + elements: int + addtl_sslports: + description: ACL SSLPorts (in addition to 443,563) + type: list + elements: int + google_accounts: + description: Google Accounts Domains + type: list + elements: str + youtube_restrict: + description: Youtube Restrictions + type: str + choices: ["none", "moderate", "strict"] + remote: + description: Remote Cache Squid Settings + type: list + elements: dict + suboptions: + state: + description: Should the remote be defined + type: str + choices: ["present", "absent"] + default: present + enable: + description: Enable Remote + type: bool + proxyaddr: + description: Hostname + type: str + proxyname: + description: Unique Remote Identifier + type: str + proxyport: + description: TCP Port + type: int + default: 3128 + allowmiss: + description: General Options + type: list + elements: str + choices: ["allow-miss", "no-tproxy", "proxy-only"] + default: ["allow-miss"] + hierarchy: + description: Hierarchy + type: str + choices: ["sibling", "parent", "multicast"] + default: parent + peermethod: + description: Peer Method + type: str + choices: ["round-robin", "default", "weighted-round-robin", "carp", "userhash", "sourcehash", "multicast-sibling"] + default: round-robin + weight: + description: Weight + type: int + default: 1 + basetime: + description: Basetime + type: int + default: 1 + ttl: + description: TTL + type: int + default: 1 + nodelay: + description: No Delay + type: bool + default: False + icpport: + description: Remote ICP Port (7 means disabled) + type: int + default: 7 + icpoptions: + description: ICP Options + type: str + choices: ["no-query", "multicast-responder", "closest-only", "background-ping"] + default: no-query + username: + description: Upstream Username + type: str + password: + description: Upstream Password + type: str + authoption: + description: Authentication Options + type: str + choices: ["login=*:password", "login=user:password", "login=PASSTHRU", "login=PASS", "login=NEGOTIATE", "login=NEGOTIATE:principal_name", "connection-auth=on", "connection-auth=off"] + default: login=*:password + sync: + description: Squid XMLRPC Sync Settings + type: dict + suboptions: + synconchanges: + description: Enable Sync + type: str + choices: ["auto", "disabled", "manual"] + default: disabled + synctimeout: + description: Sync Timeout (s) + type: int + choices: [30, 60, 90, 120, 250] + default: 250 + synctargets: + description: Replication Targets (manual mode) + type: list + elements: dict + suboptions: + syncdestinable: + description: Enable Sync Target + type: bool + syncprotocol: + description: XMLRPC Sync Protocol + type: str + choices: ["http", "https"] + default: http + ipaddress: + description: IP Address / Hostname + type: str + required: true + syncport: + description: HTTP/HTTPS Port + type: int + username: + description: Remote Username + type: str + required: true + password: + description: Remote Password + type: str + required: true + traffic: + description: Traffic Management Administration + type: dict + suboptions: + max_download_size: + description: Maximum Download Size in kilobytes (0 = disabled) + type: int + default: 0 + max_upload_size: + description: Maximum Upload Size in kilobytes(0 = disabled) + type: int + default: 0 + overall_throttling: + description: Overall Bandwidth Throttling in kilobytes (0 = disabled) + type: int + default: 0 + perhost_throttling: + description: Download Throttling per host in kilobytes (0 = disabled) + type: int + default: 0 + unrestricted_throttling: + description: Throttle Unrestricted IPs + type: bool + default: false + throttle_specific: + description: Throttle Only Specific Extensions + type: bool + default: false + throttle_binaries: + description: Throttle Binary Files + type: bool + default: false + throttle_cdimages: + description: Throttle DC/DVD Image Files + type: bool + default: false + throttle_multimedia: + description: Throttle Multimedia Files + type: bool + default: false + throttle_others: + description: Throttle Other Extensions + type: list + elements: str + quick_abort_min: + description: Finish transfer if less than x KB remaining + type: int + default: 0 + quick_abort_max: + description: Finish transfer if more than x KB remaining + type: int + default: 0 + quick_abort_pct: + description: Finish transfer if more than x % finished + type: int + default: 0 + users: + description: Local Squid User Administration + type: list + elements: dict + suboptions: + state: + description: Should the user be present or absent + type: str + choices: ["present", "absent"] + default: present + username: + description: Username + type: str + required: True + password: + description: Password + type: str + required: True + description: + description: Description of User + type: str +""" + +EXAMPLES = """ +- name: setup dns resolver to use forwarders + pfsense_dns_resolver: + enable: true + forwarding: true +""" + +RETURN = """ +commands: + description: the set of commands that would be pushed to the remote device (if pfSense had a CLI) + returned: always + type: list + sample: ["update dns_resolver set enable='true', forwarding='true'"] +""" + +import base64 +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.pfsense import PFSenseModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +base64_params = [ + 'allowed_subnets', + 'banned_hosts', + 'blacklist', + 'block_user_agent', + 'block_reply_mime_type', + 'custom_options_squid3', + 'custom_options2_squid3', + 'custom_options3_squid3', + 'custom_refresh_patterns', + 'donotcache', + 'no_auth_hosts', + 'raw_squidclamav_conf', + 'raw_cicap_conf', + 'raw_cicap_magic', + 'raw_freshclam_conf', + 'raw_clamd_conf', + 'unrestricted_hosts', + 'whitelist', +] + +cert_params = [ + 'dca', + 'extraca', +] + +SQUID_CONFIG_AUTH_ARGUMENT_SPEC = dict( + auth_method=dict(required=False, type='str', choices=[ + 'none', + 'local', + 'ldap', + 'radius', + 'cp', + ], default='none'), + auth_server=dict(required=False, type='str'), + auth_server_port=dict(required=False, type='int'), + auth_prompt=dict(required=False, type='str', default='Please enter your credentials to access the proxy'), + auth_processes=dict(required=False, type='int', default=5), + auth_ttl=dict(required=False, type='int', default=5), + max_user_ip=dict(required=False, type='int'), + unrestricted_auth=dict(required=False, type='bool'), + no_auth_hosts=dict(required=False, type='list', elements='str'), + ldap_version=dict(required=False, type='int', choices=[2, 3], default=2), + ldap_urltype=dict(required=False, type='str', choices=[ + 'standard', + 'starttls', + 'ssl', + ], default='standard'), + ldap_user=dict(required=False, type='str'), + ldap_pass=dict(required=False, type='str', no_log=True), + ldap_basedomain=dict(required=False, type='str'), + ldap_userattribute=dict(required=False, type='str', default='uid'), + ldap_filter=dict(required=False, type='str', default='(&(objectClass=person)(uid=%s))'), + ldap_noreferrals=dict(required=False, type='str'), + radius_secret=dict(required=False, type='str', no_log=True), +) + +SQUID_CONFIG_ANTIVIRUS_ARGUMENT_SPEC = dict( + enable=dict(required=False, type='bool'), + client_info=dict(required=False, type='str', choices=[ + 'both', + 'username', + 'ip', + 'none', + ], default='both'), + enable_advanced=dict(required=False, type='bool', default=False), + clamav_url=dict(required=False, type='str'), + clamav_scan_type=dict(required=False, type='str', choices=[ + 'all', + 'app', + 'web', + ], default='all'), + clamav_disable_stream_scanning=dict(required=False, type='bool'), + clamav_block_pua=dict(required=False, type='bool'), + clamav_update=dict(required=False, type='int', choices=[ + 0, 1, 2, 3, 4, 6, 8, 12, 24 + ], default=0), + clamav_dbregion=dict(required=False, type='str', choices=[ + '', 'au', 'europe', 'ca', 'cn', 'id', 'jp', + 'kr', 'ml', 'ru', 'sa', 'tw', 'uk', 'us', + ]), + clamav_dbservers=dict(required=False, type='list', elements='str'), + urlhaus_sig=dict(required=False, type='bool'), + interserver_sig=dict(required=False, type='bool'), + securiteinfo_sig=dict(required=False, type='bool'), + securiteinfo_premium=dict(required=False, type='bool'), + securiteinfo_id=dict(required=False, type='str'), + raw_squidclamav_conf=dict(required=False, type='str'), + raw_cicap_conf=dict(required=False, type='str'), + raw_cicap_magic=dict(required=False, type='str'), + raw_freshclam_conf=dict(required=False, type='str'), + raw_clamd_conf=dict(required=False, type='str'), +) + +SQUID_CONFIG_CACHE_ARGUMENT_SPEC = dict( + nocache=dict(required=False, type='bool'), + cache_replacement_policy=dict(required=False, type='str', choices=[ + 'heap LFUDA', + 'heap GDSF', + 'heap LRU', + 'LRU', + ], default='heap LFUDA'), + cache_swap_low=dict(required=False, type='int', default=90), + cache_swap_high=dict(required=False, type='int', default=95), + donotcache=dict(required=False, type='list', elements='str'), + enable_offline=dict(required=False, type='bool'), + ext_cachemanager=dict(required=False, type='list', elements='str'), + harddisk_cache_size=dict(required=False, type='int', default=100), + harddisk_cache_system=dict(required=False, type='str', choices=[ + 'aufs', + 'diskd', + 'null', + 'ufs', + ], default='ufs'), + level1_subdirs=dict(required=False, type='int', choices=[ + 4, 8, 16, 32, 64, 128, 256, + ], default=16), + harddisk_cache_location=dict(required=False, type='path', default='/var/squid/cache'), + minimum_object_size=dict(required=False, type='int', default=0), + maximum_object_size=dict(required=False, type='int', default=4), + memory_cache_size=dict(required=False, type='int', default=64), + maximum_objsize_in_mem=dict(required=False, type='int', default=256), + memory_replacement_policy=dict(required=False, type='str', choices=[ + 'heap GDSF', + 'heap LFUDA', + 'heap LRU', + 'LRU', + ], default='heap GDSF'), + cache_dynamic_content=dict(required=False, type='bool'), + custom_refresh_patterns=dict(required=False, type='list', elements='str'), +) + +SQUID_CONFIG_GENERAL_ARGUMENT_SPEC = dict( + enable_squid=dict(required=False, type='bool'), + keep_squid_data=dict(required=False, type='bool', default=True), + listenproto=dict(required=False, type='str', choices=[ + 'inet', + 'inet6', + 'any'], + default='inet'), + carpstatusvid=dict(required=False, type='str'), + active_interface=dict(required=False, type='list', elements='str', default=['lan']), + outgoing_interface=dict(required=False, type='str', default='lan'), + proxy_port=dict(required=False, type='int', default=3128), + icp_port=dict(required=False, type='int'), + allow_interface=dict(required=False, type='bool', default=True), + dns_v4_first=dict(required=False, type='bool'), + disable_pinger=dict(required=False, type='bool'), + dns_nameservers=dict(required=False, type='list', elements='str'), + extraca=dict(required=False, type='str'), + transparent_proxy=dict(required=False, type='bool'), + transparent_active_interface=dict(required=False, type='list', elements='str'), + private_subnet_proxy_off=dict(required=False, type='bool'), + defined_ip_proxy_off=dict(required=False, type='list', elements='str'), + defined_ip_proxy_off_dest=dict(required=False, type='list', elements='str'), + ssl_proxy=dict(required=False, type='bool'), + sslproxy_mitm_mode=dict(required=False, type='str', choices=[ + 'splicewhitelist', + 'spliceall', + 'custom', + ], default='splicewhitelist'), + ssl_active_interface=dict(required=False, type='list', elements='str', default=['lan']), + ssl_proxy_port=dict(required=False, type='int', default=3129), + sslproxy_compatibility_mode=dict(required=False, type='str', choices=[ + 'modern', + 'intermediate', + ], default='modern'), + dhparams_size=dict(required=False, type='int', choices=[ + 1024, + 2048, + 4096, + ], default=2048), + dca=dict(required=False, type='str'), + sslcrtd_children=dict(required=False, type='int', default=5), + interception_checks=dict(required=False, type='list', elements='str', choices=[ + 'sslproxy_cert_error', + 'sslproxy_flags', + ]), + interception_adapt=dict(required=False, type='list', elements='str', choices=[ + 'setValidAfter', + 'setValidBefore', + 'setCommonName', + ]), + log_enabled=dict(required=False, type='bool'), + log_dir=dict(required=False, type='path', default='/var/squid/logs'), + log_rotate=dict(required=False, type='int'), + log_sqd=dict(required=False, type='bool'), + visible_hostname=dict(required=False, type='str', default='localhost'), + admin_email=dict(required=False, type='str', default='admin@localhost'), + error_language=dict(required=False, type='str', choices=[ + 'af', 'ar', 'az', 'bg', 'ca', 'cs', 'da', 'de', + 'el', 'en', 'es', 'et', 'fa', 'fi', 'fr', 'he', + 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'lt', 'lv', + 'ms', 'nl', 'oc', 'pl', 'pt', 'pt-br', 'ro', + 'ru', 'sk', 'sl', 'sr-cyrl', 'sr-latn', 'sv', + 'th', 'tr', 'uk', 'uz', 'vi', 'zh-cn', 'zh-tw', + ], default='en'), + xforward_mode=dict(required=False, type='str', choices=[ + 'on', + 'off', + 'transparent', + 'delete', + 'truncate', + ], default='on'), + disable_via=dict(required=False, type='bool'), + uri_whitespace=dict(required=False, type='str', choices=[ + 'allow', + 'chop', + 'deny', + 'encode', + 'strip', + ], default='strip'), + disable_squidversion=dict(required=False, type='bool'), + custom_options=dict(required=False, type='str'), + custom_options_squid3=dict(required=False, type='str'), + custom_options2_squid3=dict(required=False, type='str'), + custom_options3_squid3=dict(required=False, type='str'), +) + +SQUID_CONFIG_NAC_ARGUMENT_SPEC = dict( + allowed_subnets=dict(type='list', elements='str'), + unrestricted_hosts=dict(type='list', elements='str'), + banned_hosts=dict(type='list', elements='str'), + whitelist=dict(type='list', elements='str'), + blacklist=dict(type='list', elements='str'), + block_user_agent=dict(type='list', elements='str'), + block_reply_mime_type=dict(type='list', elements='str'), + addtl_ports=dict(type='list', elements='int'), + addtl_sslports=dict(type='list', elements='int'), + google_accounts=dict(type='list', elements='str'), + youtube_restrict=dict(type='str', choices=[ + 'none', + 'moderate', + 'strict', + ]), +) + +SQUID_CONFIG_REMOTE_ARGUMENT_SPEC = dict( + state=dict(required=False, type='str', default='present'), + enable=dict(required=False, type='bool', default=False), + proxyaddr=dict(required=False, type='str'), + proxyname=dict(required=False, type='str'), + proxyport=dict(required=False, type='int', default=3128), + allowmiss=dict(required=False, type='list', elements='str', choices=[ + 'allow-miss', + 'no-tproxy', + 'proxy-only', + ], default=['allow-miss']), + hierarchy=dict(required=False, type='str', choices=[ + 'sibling', + 'parent', + 'multicast', + ], default='parent'), + peermethod=dict(required=False, type='str', choices=[ + 'round-robin', + 'default', + 'weighted-round-robin', + 'carp', + 'userhash', + 'sourcehash', + 'multicast-sibling', + ], default='round-robin'), + weight=dict(required=False, type='int', default=1), + basetime=dict(required=False, type='int', default=1), + ttl=dict(required=False, type='int', default=1), + nodelay=dict(required=False, type='bool', default=False), + icpport=dict(required=False, type='int', default=7), + icpoptions=dict(required=False, type='str', choices=[ + 'no-query', + 'multicast-responder', + 'closest-only', + 'background-ping', + ], default='no-query'), + username=dict(required=False, type='str'), + password=dict(required=False, type='str', no_log=True), + authoption=dict(required=False, type='str', choices=[ + 'login=*:password', + 'login=user:password', + 'login=PASSTHRU', + 'login=PASS', + 'login=NEGOTIATE', + 'login=NEGOTIATE:principal_name', + 'connection-auth=on', + 'connection-auth=off', + ], default='login=*:password') +) + +SQUID_CONFIG_SYNC_TARGET_ARGUMENT_SPEC = dict( + syncdestinenable=dict(type='bool'), + syncprotocol=dict(type='str', choices=[ + 'http', + 'https' + ], default='http'), + ipaddress=dict(required=True, type='str'), + syncport=dict(required=False, type='int'), + username=dict(required=True, type='str'), + password=dict(required=True, type='str', no_log=True), +) + +SQUID_CONFIG_SYNC_ARGUMENT_SPEC = dict( + synconchanges=dict(type='str', choices=[ + 'auto', + 'disabled', + 'manual', + ], default='disabled'), + synctimeout=dict(type='int', choices=[ + 30, 60, 90, 120, 250, + ], default=250), + synctargets=dict(type='list', elements='dict', + options=SQUID_CONFIG_SYNC_TARGET_ARGUMENT_SPEC, + ), +) + +SQUID_CONFIG_TRAFFIC_ARGUMENT_SPEC = dict( + max_download_size=dict(type='int', default=0), + max_upload_size=dict(type='int', default=0), + overall_throttling=dict(type='int', default=0), + perhost_throttling=dict(type='int', default=0), + unrestricted_throttling=dict(type='bool', default=False), + throttle_specific=dict(type='bool', default=False), + throttle_binaries=dict(type='bool', default=False), + throttle_cdimages=dict(type='bool', default=False), + throttle_multimedia=dict(type='bool', default=False), + throttle_others=dict(type='list', elements='str'), + quick_abort_min=dict(type='int', default=0), + quick_abort_max=dict(type='int', default=0), + quick_abort_pct=dict(type='int', default=0), +) + +SQUID_CONFIG_USER_ARGUMENT_SPEC = dict( + state=dict(required=False, type='str', default='present'), + username=dict(required=True, type='str'), + password=dict(required=True, type='str', no_log=True), + description=dict(required=False, type='str'), +) + +class PFSenseSquidConfigAuthModule(PFSenseModuleBase): + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return SQUID_CONFIG_AUTH_ARGUMENT_SPEC(PFSenseModuleBase) + + def __init__(self, module, pfsense=None): + super(PFSenseSquidConfigAuthModule, self).__init__(module, pfsense) + self.name = "pfsense_squid_config" + self.obj = dict() + self.before = None + self.before_elt = None + pkgs_elt = self.pfsense.get_element('installedpackages') + squidauth_elt = self.pfsense.get_element('squidauth', pkgs_elt, create_node=True) + self.root_elt = self.pfsense.get_element('config', squidauth_elt, create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + if param in base64_params: + target[param] = base64.b64encode(params[param]) + else: + target[param] = params[param] + else: + if param in base64_params: + target[param] = base64.b64encode('\n'.join(params[param]).encode()).decode() + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and (param not in target or target[param] != 'on'): + target[param] = 'on' + elif value is False and (param not in target or target[param] != ''): + target[param] = '' + + for param in SQUID_CONFIG_AUTH_ARGUMENT_SPEC: + if SQUID_CONFIG_AUTH_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + else: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def run(self, params): + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + @staticmethod + def _get_obj_name(): + return "auth" + + @staticmethod + def fvalue_bool(value): + """ boolean value formatting function """ + if value is None or value is False or value == 'none' or value != 'on': + return 'False' + + return 'True' + + def _log_fields(self, before=None): + values = '' + + if before is None: + for param in SQUID_CONFIG_AUTH_ARGUMENT_SPEC: + if SQUID_CONFIG_AUTH_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool) + else: + values += self.format_cli_field(self.obj, param) + else: + for param in SQUID_CONFIG_AUTH_ARGUMENT_SPEC: + if SQUID_CONFIG_AUTH_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, self.before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + return values + +class PFSenseSquidConfigAntivirusModule(PFSenseModuleBase): + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return SQUID_CONFIG_ANTIVIRUS_ARGUMENT_SPEC(PFSenseModuleBase) + + def __init__(self, module, pfsense=None): + super(PFSenseSquidConfigAntivirusModule, self).__init__(module, pfsense) + self.name = "pfsense_squid_config" + self.obj = dict() + self.before = None + self.before_elt = None + pkgs_elt = self.pfsense.get_element('installedpackages') + squidantivirus_elt = self.pfsense.get_element('squidantivirus', pkgs_elt, create_node=True) + self.root_elt = self.pfsense.get_element('config', squidantivirus_elt, create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + if param in base64_params: + target[param] = base64.b64encode(params[param]) + else: + target[param] = params[param] + else: + if param in base64_params: + target[param] = base64.b64encode('\n'.join(params[param]).encode()).decode() + elif param == 'clamav_dbservers': + target[param] = ';'.join(params[param]) + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if param == 'enable_advanced': + pass + elif value is True and (param not in target or target[param] != 'on'): + target[param] = 'on' + elif value is False and (param not in target or target[param] != ''): + target[param] = '' + + for param in SQUID_CONFIG_ANTIVIRUS_ARGUMENT_SPEC: + if SQUID_CONFIG_ANTIVIRUS_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + else: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def run(self, params): + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + @staticmethod + def _get_obj_name(): + return "antivirus" + + @staticmethod + def fvalue_bool(value): + """ boolean value formatting function """ + if value is None or value is False or value == 'none' or value != 'on': + return 'False' + + return 'True' + + def _log_fields(self, before=None): + values = '' + + if before is None: + for param in SQUID_CONFIG_ANTIVIRUS_ARGUMENT_SPEC: + if SQUID_CONFIG_ANTIVIRUS_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool) + else: + values += self.format_cli_field(self.obj, param) + else: + for param in SQUID_CONFIG_ANTIVIRUS_ARGUMENT_SPEC: + if SQUID_CONFIG_ANTIVIRUS_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values), log_none=False) + + return values + +class PFSenseSquidConfigCacheModule(PFSenseModuleBase): + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return SQUID_CONFIG_CACHE_ARGUMENT_SPEC(PFSenseModuleBase) + + def __init__(self, module, pfsense=None): + super(PFSenseSquidConfigCacheModule, self).__init__(module, pfsense) + self.name = "pfsense_squid_config" + self.obj = dict() + self.before = None + self.before_elt = None + pkgs_elt = self.pfsense.get_element('installedpackages') + squidcache_elt = self.pfsense.get_element('squidcache', pkgs_elt, create_node=True) + self.root_elt = self.pfsense.get_element('config', squidcache_elt, create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + def _set_param_list(target, param): + if params.get(param) is not None: + if param in base64_params: + target[param] = base64.b64encode('\n'.join(params[param]).encode()).decode() + elif param == 'ext_cachemanager': + target[param] = ';'.join(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and (param not in target or target[param] != 'on'): + target[param] = 'on' + elif value is False and (param not in target or target[param] != ''): + target[param] = '' + + for param in SQUID_CONFIG_CACHE_ARGUMENT_SPEC: + if SQUID_CONFIG_CACHE_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + elif SQUID_CONFIG_CACHE_ARGUMENT_SPEC[param]['type'] == 'list': + _set_param_list(obj, param) + else: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def run(self, params): + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + @staticmethod + def _get_obj_name(): + return "cache" + + @staticmethod + def fvalue_bool(value): + """ boolean value formatting function """ + if value is None or value is False or value == 'none' or value != 'on': + return 'False' + + return 'True' + + def _log_fields(self, before=None): + values = '' + + if before is None: + for param in SQUID_CONFIG_CACHE_ARGUMENT_SPEC: + if SQUID_CONFIG_CACHE_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool) + else: + values += self.format_cli_field(self.obj, param) + else: + for param in SQUID_CONFIG_CACHE_ARGUMENT_SPEC: + if SQUID_CONFIG_CACHE_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values), log_none=False) + + return values + +class PFSenseSquidConfigGeneralModule(PFSenseModuleBase): + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return SQUID_CONFIG_GENERAL_ARGUMENT_SPEC(PFSenseModuleBase) + + def __init__(self, module, pfsense=None): + super(PFSenseSquidConfigGeneralModule, self).__init__(module, pfsense) + self.name = "pfsense_squid_config" + self.obj = dict() + self.before = None + self.before_elt = None + pkgs_elt = self.pfsense.get_element('installedpackages') + squid_elt = self.pfsense.get_element('squid', pkgs_elt, create_node=True) + self.root_elt = self.pfsense.get_element('config', squid_elt, create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + if param in base64_params: + target[param] = base64.b64encode(params[param]) + elif param in cert_params: + ca_elt = self.pfsense.find_ca_elt(params[param]) + target[param] = ca_elt.find('refid').text + else: + target[param] = params[param] + else: + if param in base64_params: + target[param] = base64.b64encode('\n'.join(params[param]).encode()).decode() + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and (param not in target or target[param] != 'on'): + target[param] = 'on' + elif value is False and (param not in target or target[param] != ''): + target[param] = '' + + def _set_param_list(target, param): + if params.get(param) is not None: + if param in base64_params: + target[param] = base64.b64encode('\n'.join(params[param]).encode()).decode() + elif param in ['active_interface', 'transparent_active_interface', 'ssl_active_interface', 'interception_adapt', 'interception_checks', 'google_accounts']: + target[param] = ','.join(params[param]) + elif param in ['addtl_ports', 'addtl_sslports']: + target[param] = ' '.join(params[param]) + elif param in ['dns_nameservers', 'defined_ip_proxy_off', 'defined_ip_proxy_off_dest']: + target[param] = ';'.join(params[param]) + + for param in SQUID_CONFIG_GENERAL_ARGUMENT_SPEC: + if SQUID_CONFIG_GENERAL_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + elif SQUID_CONFIG_GENERAL_ARGUMENT_SPEC[param]['type'] == 'list': + _set_param_list(obj, param) + else: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def run(self, params): + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + @staticmethod + def _get_obj_name(): + return "general" + + @staticmethod + def fvalue_bool(value): + """ boolean value formatting function """ + if value is None or value is False or value == 'none' or value != 'on': + return 'False' + + return 'True' + + def _log_fields(self, before=None): + values = '' + + if before is None: + for param in SQUID_CONFIG_GENERAL_ARGUMENT_SPEC: + if SQUID_CONFIG_GENERAL_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool) + else: + values += self.format_cli_field(self.obj, param) + else: + for param in SQUID_CONFIG_GENERAL_ARGUMENT_SPEC: + if SQUID_CONFIG_GENERAL_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, self.before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + return values + +class PFSenseSquidConfigNacModule(PFSenseModuleBase): + @staticmethod + def _get_obj_name(): + return "nac" + + def __init__(self, module, pfsense=None): + super(PFSenseSquidConfigNacModule, self).__init__(module, pfsense) + self.name = "pfsense_squid_config" + self.obj = dict() + self.before = None + self.before_elt = None + pkgs_elt = self.pfsense.get_element('installedpackages') + squidnac_elt = self.pfsense.get_element('squidnac', pkgs_elt, create_node=True) + self.root_elt = self.pfsense.get_element('config', squidnac_elt, create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + if param in base64_params: + target[param] = base64.b64encode(params[param]) + else: + target[param] = params[param] + else: + if param in base64_params: + target[param] = base64.b64encode('\n'.join(params[param]).encode()).decode() + else: + target[param] = str(params[param]) + + for param in SQUID_CONFIG_NAC_ARGUMENT_SPEC: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def run(self, params): + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + def _log_fields(self, before=None): + values = '' + + if before is None: + for param in SQUID_CONFIG_NAC_ARGUMENT_SPEC: + values += self.format_cli_field(self.obj, param) + else: + for param in SQUID_CONFIG_NAC_ARGUMENT_SPEC: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + return values + +class PFSenseSquidConfigRemoteModule(PFSenseModuleBase): + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return SQUID_CONFIG_REMOTE_ARGUMENT_SPEC(PFSenseModuleBase) + + def __init__(self, module, pfsense=None): + super(PFSenseSquidConfigRemoteModule, self).__init__(module, pfsense) + self.name = "pfsense_squid_config" + self.obj = dict() + self.before = None + self.before_elt = None + pkgs_elt = self.pfsense.get_element('installedpackages', create_node=True) + self.root_elt = self.pfsense.get_element('squidremote', pkgs_elt, create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + obj = dict() + obj['proxyname'] = params['proxyname'] + obj['proxyaddr'] = params['proxyaddr'] + if params['state'] == 'present': + if params['enable']: + obj['enable'] = 'on' + obj['proxyport'] = str(params['proxyport']) + obj['allowmiss'] = ','.join(params['allowmiss']) + obj['hierarchy'] = params['hierarchy'] + obj['peermethod'] = params['peermethod'] + obj['weight'] = str(params['weight']) + obj['basetime'] = str(params['basetime']) + obj['ttl'] = str(params['ttl']) + if params['nodelay']: + obj['nodelay'] = 'on' + obj['icpport'] = str(params['icpport']) + obj['icpoptions'] = params['icpoptions'] + obj['username'] = params['username'] + obj['password'] = params['password'] + obj['authoption'] = params['authoption'] + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def _create_target(self): + """ create the XML target_elt """ + config_elt = self.pfsense.new_element('config') + return config_elt + + def _find_target(self): + """ find the XML target_elt """ + for config_elt in self.root_elt: + if config_elt.tag != 'config': + continue + proxyname_elt = config_elt.find('proxyname') + proxyaddr_elt = config_elt.find('proxyaddr') + if (proxyname_elt is not None and proxyname_elt.text == self.obj['proxyname']) and (proxyaddr_elt is not None and proxyaddr_elt.text == self.obj['proxyaddr']): + return config_elt + + return None + + @staticmethod + def _get_obj_name(): + return "remote" + + @staticmethod + def fvalue_bool(value): + """ boolean value formatting function """ + if value is None or value is False or value == 'none' or value != 'on': + return 'False' + + return 'True' + + def _log_fields(self, before=None): + values = '' + + if before is None: + for param in SQUID_CONFIG_REMOTE_ARGUMENT_SPEC: + if SQUID_CONFIG_REMOTE_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool) + else: + values += self.format_cli_field(self.obj, param) + else: + for param in SQUID_CONFIG_REMOTE_ARGUMENT_SPEC: + if SQUID_CONFIG_REMOTE_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values), log_none=False) + + return values + +class PFSenseSquidConfigSyncModule(PFSenseModuleBase): + @staticmethod + def _get_obj_name(): + return "sync" + + def __init__(self, module, pfsense=None): + super(PFSenseSquidConfigSyncModule, self).__init__(module, pfsense) + self.name = "pfsense_squid_config" + self.obj = dict() + self.before = None + self.before_elt = None + + pkgs_elt = self.pfsense.get_element('installedpackages', create_node=True) + squidsync_elt = self.pfsense.get_element('squidsync', pkgs_elt, create_node=True) + self.root_elt = self.pfsense.get_element('config', squidsync_elt, create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param_list(target, params, param): + if params.get(param) is not None: + if param == 'synctargets': + synctargets = [] + for entry in params.get(param): + synctarget = dict() + for subparam in SQUID_CONFIG_SYNC_TARGET_ARGUMENT_SPEC: + if entry.get(subparam) is not None: + if SQUID_CONFIG_SYNC_TARGET_ARGUMENT_SPEC[subparam]['type'] == 'bool': + _set_param_bool(synctarget, entry, subparam) + else: + _set_param(synctarget, entry, subparam) + synctargets.append(synctarget) + target['row'] = synctargets + + def _set_param_bool(target, params, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and param not in target: + target[param] = 'ON' + elif value is False and param in target: + del target[param] + + def _set_param(target, params, param): + if params.get(param) is not None: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + for param in SQUID_CONFIG_SYNC_ARGUMENT_SPEC: + if SQUID_CONFIG_SYNC_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, self.params, param) + elif SQUID_CONFIG_SYNC_ARGUMENT_SPEC[param]['type'] == 'list': + _set_param_list(obj, self.params, param) + else: + _set_param(obj, self.params, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def run(self, params): + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + @staticmethod + def fvalue_bool(value): + """ boolean value formatting function """ + if value is None or value is False or value == 'none' or value != 'ON': + return 'False' + + return 'True' + + def _log_fields(self, before=None): + values = '' + + if before is None: + for param in SQUID_CONFIG_SYNC_ARGUMENT_SPEC: + values += self.format_cli_field(self.obj, param) + else: + for param in SQUID_CONFIG_SYNC_ARGUMENT_SPEC: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + return values + +class PFSenseSquidConfigTrafficModule(PFSenseModuleBase): + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return SQUID_CONFIG_TRAFFIC_ARGUMENT_SPEC(PFSenseModuleBase) + + def __init__(self, module, pfsense=None): + super(PFSenseSquidConfigTrafficModule, self).__init__(module, pfsense) + self.name = "pfsense_squid_config" + self.obj = dict() + self.before = None + self.before_elt = None + + pkgs_elt = self.pfsense.get_element('installedpackages', create_node=True) + squidtraffic_elt = self.pfsense.get_element('squidtraffic', pkgs_elt, create_node=True) + self.root_elt = self.pfsense.get_element('config', squidtraffic_elt, create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + def _set_param_list(target, param): + if params.get(param) is not None: + if param in base64_params: + target[param] = base64.b64encode('\n'.join(params[param]).encode()).decode() + elif param == 'ext_cachemanager': + target[param] = ';'.join(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and (param not in target or target[param] != 'on'): + target[param] = 'on' + elif value is False and (param not in target or target[param] != ''): + target[param] = '' + + for param in SQUID_CONFIG_TRAFFIC_ARGUMENT_SPEC: + if SQUID_CONFIG_TRAFFIC_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + elif SQUID_CONFIG_TRAFFIC_ARGUMENT_SPEC[param]['type'] == 'list': + _set_param_list(obj, param) + else: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def run(self, params): + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + @staticmethod + def _get_obj_name(): + return "traffic" + + @staticmethod + def fvalue_bool(value): + """ boolean value formatting function """ + if value is None or value is False or value == 'none' or value != 'on': + return 'False' + + return 'True' + + def _log_fields(self, before=None): + values = '' + + if before is None: + for param in SQUID_CONFIG_TRAFFIC_ARGUMENT_SPEC: + if SQUID_CONFIG_TRAFFIC_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool) + else: + values += self.format_cli_field(self.obj, param) + else: + for param in SQUID_CONFIG_TRAFFIC_ARGUMENT_SPEC: + if SQUID_CONFIG_TRAFFIC_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values), log_none=False) + + return values +class PFSenseSquidConfigUsersModule(PFSenseModuleBase): + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return SQUID_CONFIG_USER_ARGUMENT_SPEC(PFSenseModuleBase) + + def __init__(self, module, pfsense=None): + super(PFSenseSquidConfigUsersModule, self).__init__(module, pfsense) + self.name = "pfsense_squid_config" + self.obj = dict() + pkgs_elt = self.pfsense.get_element('installedpackages', create_node=True) + self.root_elt = self.pfsense.get_element('squidusers', pkgs_elt, create_node=True) + + self.users = None + + def _params_to_obj(self): + """ return a dict from module params """ + obj = dict() + obj['username'] = self.params['username'] + if self.params['state'] == 'present': + obj['password'] = self.params['password'] + obj['description'] = self.params['description'] + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def _create_target(self): + """ create the XML target_elt """ + config_elt = self.pfsense.new_element('config') + return config_elt + + def _find_target(self): + """ find the XML target_elt """ + for config_elt in self.root_elt: + if config_elt.tag != 'config': + continue + username_elt = config_elt.find('username') + if username_elt is not None and username_elt.text == self.obj['username']: + return config_elt + + return None + + @staticmethod + def _get_obj_name(): + return "users" + + def _log_fields(self, before=None): + values = '' + + if before is None: + for param in SQUID_CONFIG_USER_ARGUMENT_SPEC: + values += self.format_cli_field(self.obj, param) + else: + for param in SQUID_CONFIG_USER_ARGUMENT_SPEC: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + return values + +class PFSenseSquidModule(object): + """ module managing pfsense squid http proxy settings """ + + def __init__(self, module): + self.module = module + self.pfsense = PFSenseModule(module) + self.pfsense_squid_auth = PFSenseSquidConfigAuthModule(module, self.pfsense) + self.pfsense_squid_antivirus = PFSenseSquidConfigAntivirusModule(module, self.pfsense) + self.pfsense_squid_cache = PFSenseSquidConfigCacheModule(module, self.pfsense) + self.pfsense_squid_general = PFSenseSquidConfigGeneralModule(module, self.pfsense) + self.pfsense_squid_nac = PFSenseSquidConfigNacModule(module, self.pfsense) + self.pfsense_squid_remotes = PFSenseSquidConfigRemoteModule(module, self.pfsense) + self.pfsense_squid_sync = PFSenseSquidConfigSyncModule(module, self.pfsense) + self.pfsense_squid_traffic = PFSenseSquidConfigTrafficModule(module, self.pfsense) + self.pfsense_squid_users = PFSenseSquidConfigUsersModule(module, self.pfsense) + + def _update(self): + run = False + + cmd = '''require_once("filter.inc"); + require_once("squid.inc"); + squid_resync(); + ''' + + if self.pfsense_squid_auth.result['changed']: + run = True + elif self.pfsense_squid_antivirus.result['changed']: + run = True + elif self.pfsense_squid_cache.result['changed']: + run = True + elif self.pfsense_squid_general.result['changed']: + run = True + elif self.pfsense_squid_nac.result['changed']: + run = True + elif self.pfsense_squid_remotes.result['changed']: + run = True + elif self.pfsense_squid_sync.result['changed']: + run = True + elif self.pfsense_squid_traffic.result['changed']: + run = True + elif self.pfsense_squid_users.result['changed']: + run = True + + if run: + return self.pfsense.phpshell(cmd) + + return ('', '', '') + + def run_auth(self): + want = self.module.params['auth'] + if want is not None: + self.pfsense_squid_auth.run(want) + + def run_antivirus(self): + want = self.module.params['antivirus'] + if want is not None: + self.pfsense_squid_antivirus.run(want) + + def run_cache(self): + want = self.module.params['cache'] + if want is not None: + self.pfsense_squid_cache.run(want) + + def run_general(self): + want = self.module.params['general'] + if want is not None: + self.pfsense_squid_general.run(want) + + def run_nac(self): + want = self.module.params['nac'] + if want is not None: + self.pfsense_squid_nac.run(want) + + @staticmethod + def want_remote(config_elt, remotes): + """ return True if we want to keep config_elt """ + proxyname = config_elt.find('proxyname').text + proxyaddr = config_elt.find('proxyaddr').text + + for remote in remotes: + if remote['state'] == 'absent': + continue + if remote['proxyname'] == proxyname and remote['proxyaddr'] == proxyaddr: + return True + return False + + def run_remotes(self): + want = self.module.params['remotes'] + if want is None: + return + + for param in want: + self.pfsense_squid_remotes.run(param) + + if self.module.params['purge_remotes']: + todel = [] + for config_elt in self.pfsense_squid_remotes.root_elt: + if not self.want_remote(config_elt, want): + params = {} + params['state'] = 'absent' + params['proxyaddr'] = config_elt.find('proxyaddr').text + params['proxyname'] = config_elt.find('proxyname').text + todel.append(params) + + for params in todel: + self.pfsense_squid_remotes.run(params) + + def run_sync(self): + want = self.module.params['sync'] + if want is not None: + self.pfsense_squid_sync.run(want) + + def run_traffic(self): + want = self.module.params['traffic'] + if want is not None: + self.pfsense_squid_traffic.run(want) + + @staticmethod + def want_user(config_elt, users): + """ return True if we want to keep config_elt """ + username = config_elt.find('username').text + + for user in users: + if user['state'] == 'absent': + continue + if user['username'] == username: + return True + return False + + def run_users(self): + want = self.module.params['users'] + if want is None: + return + + for param in want: + self.pfsense_squid_users.run(param) + + if self.module.params['purge_users']: + todel = [] + for config_elt in self.pfsense_squid_users.root_elt: + if not self.want_user(config_elt, want): + params = {} + params['state'] = 'absent' + params['username'] = config_elt.find('username').text + todel.append(params) + + for params in todel: + self.pfsense_squid_users.run(params) + + def commit_changes(self): + stdout = '' + stderr = '' + changed = ( + self.pfsense_squid_auth.result['changed'] or + self.pfsense_squid_antivirus.result['changed'] or + self.pfsense_squid_cache.result['changed'] or + self.pfsense_squid_general.result['changed'] or + self.pfsense_squid_nac.result['changed'] or + self.pfsense_squid_remotes.result['changed'] or + self.pfsense_squid_sync.result['changed'] or + self.pfsense_squid_traffic.result['changed'] or + self.pfsense_squid_users.result['changed'] + ) + + if changed and not self.module.check_mode: + self.pfsense.write_config(descr='squid config') + (dummy, stdout, stderr) = self._update() + + result = {} + result['result_auth'] = self.pfsense_squid_auth.result['commands'] + result['result_antivirus'] = self.pfsense_squid_antivirus.result['commands'] + result['result_cache'] = self.pfsense_squid_cache.result['commands'] + result['result_general'] = self.pfsense_squid_general.result['commands'] + result['result_nac'] = self.pfsense_squid_nac.result['commands'] + result['result_remotes'] = self.pfsense_squid_remotes.result['commands'] + result['result_sync'] = self.pfsense_squid_sync.result['commands'] + result['result_traffic'] = self.pfsense_squid_traffic.result['commands'] + result['result_users'] = self.pfsense_squid_users.result['commands'] + + result['diff_auth'] = self.pfsense_squid_auth.diff + result['diff_antivirus'] = self.pfsense_squid_antivirus.diff + result['diff_cache'] = self.pfsense_squid_cache.diff + result['diff_general'] = self.pfsense_squid_general.diff + result['diff_nac'] = self.pfsense_squid_nac.diff + result['diff_remotes'] = self.pfsense_squid_remotes.diff + result['diff_sync'] = self.pfsense_squid_sync.diff + result['diff_traffic'] = self.pfsense_squid_traffic.diff + result['diff_users'] = self.pfsense_squid_users.diff + + result['changed'] = changed + result['stdout'] = stdout + result['stderr'] = stderr + self.module.exit_json(**result) + +def main(): + module = AnsibleModule( + argument_spec=dict( + auth=dict(type='dict', options=SQUID_CONFIG_AUTH_ARGUMENT_SPEC), + antivirus=dict(type='dict', options=SQUID_CONFIG_ANTIVIRUS_ARGUMENT_SPEC), + cache=dict(type='dict', options=SQUID_CONFIG_CACHE_ARGUMENT_SPEC), + general=dict(type='dict', options=SQUID_CONFIG_GENERAL_ARGUMENT_SPEC), + nac=dict(type='dict', options=SQUID_CONFIG_NAC_ARGUMENT_SPEC), + remotes=dict(type='list', elements='dict', options=SQUID_CONFIG_REMOTE_ARGUMENT_SPEC), + sync=dict(type='dict', options=SQUID_CONFIG_SYNC_ARGUMENT_SPEC), + traffic=dict(type='list', elements='dict', options=SQUID_CONFIG_TRAFFIC_ARGUMENT_SPEC), + users=dict(type='list', elements='dict', options=SQUID_CONFIG_USER_ARGUMENT_SPEC), + purge_remotes=dict(type='bool', default=False), + purge_users=dict(type='bool', default=False), + ), + supports_check_mode=True) + + pfmodule = PFSenseSquidModule(module) + pfmodule.run_auth() + pfmodule.run_antivirus() + pfmodule.run_cache() + pfmodule.run_general() + pfmodule.run_nac() + pfmodule.run_remotes() + pfmodule.run_sync() + pfmodule.run_traffic() + pfmodule.run_users() + + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From b78736850e5d76e8101069f719a99a9222d661b7 Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 14:46:44 +0000 Subject: [PATCH 05/16] feat: add default-gateway module --- plugins/modules/pfsense_default_gateway.py | 146 +++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 plugins/modules/pfsense_default_gateway.py diff --git a/plugins/modules/pfsense_default_gateway.py b/plugins/modules/pfsense_default_gateway.py new file mode 100644 index 00000000..9c15bfdc --- /dev/null +++ b/plugins/modules/pfsense_default_gateway.py @@ -0,0 +1,146 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_default_gateway +version_added: "0.4.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage pfSense default gateways +description: + - Manage pfSense default gateways for IPv4/IPv6 +notes: +options: + defaultgw4: + description: Default Gateway (IPv4) (name of existing gateway, auto or none) + required: false + type: str + defaultgw6: + description: Default Gateway (IPv6) (name of existing gateway, auto or none) + required: false + type: str +""" + +EXAMPLES = """ +pfsensible.core.pfsense_default_gateway: + defaultgw4: "LANGW" +""" + +RETURN = """ +""" + +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + + +DEFAULT_GATEWAY_ARGUMENT_SPEC = dict( + defaultgw4=dict(required=False, type='str'), + defaultgw6=dict(required=False, type='str'), +) + +# map field names between ansible and pfsense +params_map = {} + +# fields with inverted logic +inverted_list = [] + +# fields that are not written to pfsense +skip_list = ['state'] + +class PFSenseDefaultGatewayModule(PFSenseModuleBase): + """ module managing pfsense default gateway settings """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return DEFAULT_GATEWAY_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseDefaultGatewayModule, self).__init__(module, pfsense) + self.name = "default_gateway" + self.root_elt = self.pfsense.get_element('gateways', create_node=True) + self.obj = dict() + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = dict() + self.obj = obj + + def _set_param(target, param): + if params.get(param) is not None: + if params[param].lower() == 'auto': + target[param] = '' + elif params[param].lower() == 'none': + target[param] = '-' + else: + target[param] = params[param] + + for param in DEFAULT_GATEWAY_ARGUMENT_SPEC: + _set_param(obj, param) + + return obj + + + def _validate_params(self): + """ do some extra checks on input parameters """ + return + + def run(self, params): + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "default_gateway" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + if before is None: + for param in DEFAULT_GATEWAY_ARGUMENT_SPEC: + values += self.format_cli_field(self.obj, param) + else: + for param in DEFAULT_GATEWAY_ARGUMENT_SPEC: + values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values), log_none=False) + + return values + + +def main(): + module = AnsibleModule( + argument_spec=DEFAULT_GATEWAY_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseDefaultGatewayModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From a7801f5b464737d902a897513f0f6090688b7e60 Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 14:47:40 +0000 Subject: [PATCH 06/16] feat: add dhcp-relay module --- plugins/modules/pfsense_dhcp_relay.py | 217 ++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 plugins/modules/pfsense_dhcp_relay.py diff --git a/plugins/modules/pfsense_dhcp_relay.py b/plugins/modules/pfsense_dhcp_relay.py new file mode 100644 index 00000000..5f0dd9b8 --- /dev/null +++ b/plugins/modules/pfsense_dhcp_relay.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_dhcp_relay +version_added: "0.4.7" +author: Jan Wenzel (@coffeelover) +short_description: Manage pfSense dhcp relay settings +description: + - Manage pfSense dhcp relay settings +notes: +options: + enable: + description: Enable DHCP Relay + required: false + type: bool + interface: + description: comma separated list of listening interfaces + required: false + type: str + agentoption: + description: Append circuit ID and agent ID to requests + required: false + type: bool + server: + description: comma separated list of destination servers + required: false + type: str +""" + +EXAMPLES = """ +- name: setup dhcp relay + pfsense_dhcp_relay: + enable: true + server: dhcp1.example.com,dhcp2.example.com + agentoption: true + interface: wan,lan +""" + +RETURN = """ +""" + +import re +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +DHCP_RELAY_ARGUMENT_SPEC = dict( + enable=dict(required=False, type='bool'), + interface=dict(required=False, type='str'), + server=dict(required=False, type='str'), + agentoption=dict(required=False, type='bool'), +) + +# rename the reserved words with log prefix +params_map = { +} + +# fields with inverted logic +inverted_list = [] + + +class PFSenseDHCPRelayModule(PFSenseModuleBase): + """ module managing pfsense dhcp_relay settings """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return LOG_SETTINGS_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseDHCPRelayModule, self).__init__(module, pfsense) + self.name = "dhcp_relay" + self.root_elt = self.pfsense.get_element('dhcrelay') + self.target_elt = self.root_elt + self.params = dict() + self.obj = dict() + self.before = None + self.before_elt = None + self.route_cmds = list() + self.params_to_delete = list() + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param(target, param): + # get possibly mapped settings name + _param = params_map.get(param, param) + if params.get(param) is not None: + if param == 'sourceip': + target[param] = self._get_source_ip_interface(params[param]) + else: + if isinstance(params[param], str): + target[_param] = params[param] + else: + target[_param] = str(params[param]) + + def _set_param_bool(target, param): + # get possibly mapped settings name + _param = params_map.get(param, param) + if params.get(param) is not None: + value = not params.get(param) if param in inverted_list else params.get(param) + if value is True and _param not in target: + target[_param] = '' + elif value is False and _param in target: + del target[_param] + + for param in DHCP_RELAY_ARGUMENT_SPEC: + if DHCP_RELAY_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + else: + _set_param(obj, param) + + return obj + + + def _validate_params(self): + """ do some extra checks on input parameters """ + params = self.params + + + ############################## + # XML processing + # + def _remove_deleted_params(self): + """ Remove from target_elt a few deleted params """ + changed = False + for param in DHCP_RELAY_ARGUMENT_SPEC: + if DHCP_RELAY_ARGUMENT_SPEC[param]['type'] == 'bool': + _param = params_map.get(param, param) + if self.pfsense.remove_deleted_param_from_elt(self.target_elt, _param, self.obj): + changed = True + + return changed + + ############################## + # run + # + def run(self, params): + """ process input params to add/update/delete """ + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + def _update(self): + """ make the target pfsense reload """ + for cmd in self.route_cmds: + self.module.run_command(cmd) + + cmd = ''' +require_once("filter.inc"); +$retval = 0; +$retval |= services_dhcrelay_configure(); +$retval |= filter_configure();''' + + return self.pfsense.phpshell(cmd) + + ############################## + # Logging + # + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "dhcrelay" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + for param in DHCP_RELAY_ARGUMENT_SPEC: + _param = params_map.get(param, param) + if DHCP_RELAY_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, self.before, _param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, self.before, _param, add_comma=(values), log_none=False) + + return values + + +def main(): + module = AnsibleModule( + argument_spec=DHCP_RELAY_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseDHCPRelayModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From 3ed4917c9789c064cc7a71dab99c1fe5774a3b4e Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 14:49:38 +0000 Subject: [PATCH 07/16] feat: add widgets module --- plugins/modules/pfsense_widgets.py | 140 +++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 plugins/modules/pfsense_widgets.py diff --git a/plugins/modules/pfsense_widgets.py b/plugins/modules/pfsense_widgets.py new file mode 100644 index 00000000..661a7a5f --- /dev/null +++ b/plugins/modules/pfsense_widgets.py @@ -0,0 +1,140 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018-2020, Orion Poplawski +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_widgets +version_added: 0.1.0 +short_description: Manage pfSense dashboard widgets +description: + > + Manage pfSense widgets +author: Orion Poplawski (@opoplawski) +notes: +options: + sequence: + description: The ordered list of the widgets + required: true + type: list + elements: dict + period: + description: + elements: str +""" + +EXAMPLES = """ +- name: Configure dashboard widgets + pfsense_widgets: + sequence: + - name: "system_information" + column: "col1" + state: "open" + - name: "interfaces" + column: "col2" + state: "open" + - name: "carp_status" + column: "col2" + state: "open" +""" + +RETURN = """ + +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + + +class PFSenseWidgetsModule(PFSenseModuleBase): + """ module managing pfsense dashboard widgets """ + + def __init__(self, module, pfsense=None): + super(PFSenseWidgetsModule, self).__init__(module, pfsense) + self.name = "pfsense_widgets" + self.root_elt = self.pfsense.get_element('widgets') + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = dict() + self.obj = obj + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + + ############################## + # XML processing + # + def _create_target(self): + """ create the XML target_elt """ + return self.pfsense.new_element('widgets') + + def _find_target(self): + return self.pfsense.find_elt('widgets') + + ############################## + # Logging + # + def _get_obj_name(self): + """ return obj's name """ + return self.obj['name'] + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + return values + + +def main(): + module = AnsibleModule( + argument_spec={ + 'name': { + 'required': True, + 'type': 'str', + 'choices': [ + + ], + }, + 'column': { + 'required': True, + 'type': 'int', + 'choices': [ + 1, + 2, + 3, + 4, + 5, + 6, + ], + }, + 'open': { + 'required': False, + 'type': 'bool', + 'default': True, + }, + }, + supports_check_mode=True) + + pfmodule = PFSenseWidgetsModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From 252885773c3a54bb45161ccfd0c1930ada67af2b Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 14:51:36 +0000 Subject: [PATCH 08/16] feat: add virtual-ip module --- plugins/modules/pfsense_virtual_ip.py | 309 ++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 plugins/modules/pfsense_virtual_ip.py diff --git a/plugins/modules/pfsense_virtual_ip.py b/plugins/modules/pfsense_virtual_ip.py new file mode 100644 index 00000000..08c37f2d --- /dev/null +++ b/plugins/modules/pfsense_virtual_ip.py @@ -0,0 +1,309 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_virtual_ip +version_added: "0.4.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage pfSense virtual ip settings +description: + - Manage pfSense virtual ip settings +notes: +options: + mode: + description: Type + required: true + type: str + choices: ['proxyarp', 'carp', 'ipalias', 'other'] + noexpand: + description: Disable expansion of this entry into IPs on NAT lists (e.g. 192.168.1.0/24 expands to 256 entries.) + required: false + type: bool + descr: + description: Description + required: false + type: str + interface: + description: Interface + required: true + type: str + vhid: + description: VHID Group + required: false + type: int + advbase: + description: Advertising Frequency Base + required: false + type: int + advskew: + description: Advertising Frequency Skew + required: false + type: int + password: + description: Virtual IP Password + required: false + type: str + uniqid: + description: Unique ID of Virtual IP in configuration + required: false + type: str + type: + description: Address Type + required: false + type: str + choices: ['single'] + default: single + subnet_bits + description: Network's subnet mask + required: false + type: int + default: 32 + subnet + description: Network subnet + required: false + type: str + state: + description: State in which to leave the Virtual IP + choices: [ "present", "absent" ] + default: present + type: str +""" + +EXAMPLES = """ +""" + +RETURN = """ +""" + +import re +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + + +VIRTUALIP_ARGUMENT_SPEC = dict( + mode=dict(required=True, type='str'), + interface=dict(required=True, type='str'), + vhid=dict(required=False, type='int'), + advskew=dict(required=False, type='int'), + advbase=dict(required=False, type='int'), + password=dict(required=False, type='str'), + uniqid=dict(required=False, type='str'), + descr=dict(required=False, type='str'), + type=dict(required=False, type='str', default='single'), + subnet_bits=dict(required=False, type='int', default=32), + subnet=dict(required=False, type='str'), + state=dict(default='present', choices=['present', 'absent'], type='str'), +) + +VIRTUALIP_REQUIRED_IF = [ + ["mode", "carp", ["uniqid", "password", "vhid", "advbase"]], + ["mode", "ipalias", ["uniqid"]], +] + +# map field names between ansible and pfsense +params_map = {} + +# fields with inverted logic +inverted_list = [] + +# fields that are not written to pfsense +skip_list = ['state'] + +class PFSenseVirtualIPModule(PFSenseModuleBase): + """ module managing pfsense virtual ip settings """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return VIRTUALIP_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseVirtualIPModule, self).__init__(module, pfsense) + self.name = "virtual_ip" + self.root_elt = self.pfsense.get_element('virtualip') + self.obj = dict() + + if self.root_elt is None: + self.root_elt = self.pfsense.new_element('virtualip') + self.pfsense.root.append(self.root_elt) + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = dict() + self.obj = obj + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and param not in target: + target[param] = '' + elif value is False and param in target: + del target[param] + + for param in VIRTUALIP_ARGUMENT_SPEC: + if param not in skip_list: + if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + else: + _set_param(obj, param) + + return obj + + + def _validate_params(self): + """ do some extra checks on input parameters """ + params = self.params + return + + ############################## + # XML processing + # + def _create_target(self): + """ create the XML target_elt """ + return self.pfsense.new_element('vip') + + def _find_target(self): + """ find the XML target elt """ + for vip_elt in self.root_elt: + if self.params['mode'] in ['ipalias', 'carp']: + if vip_elt.find('uniqid') is not None and vip_elt.find('uniqid').text == self.params['uniqid']: + return vip_elt + else: + if vip_elt.find('descr') is not None and vip_elt.find('descr').text == self.params['descr']: + return vip_elt + return None + + def _remove_deleted_params(self): + """ Remove from target_elt a few deleted params """ + changed = False + for param in VIRTUALIP_ARGUMENT_SPEC: + if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool': + if self.pfsense.remove_deleted_param_from_elt(self.target_elt, param, self.obj): + changed = True + + return changed + + def _update(self): + """ make the target pfsense reload """ + cmd = ''' +require_once("globals.inc"); +require_once("functions.inc"); +require_once("filter.inc"); +require_once("shaper.inc"); +require_once("interfaces.inc"); +require_once("util.inc"); +$check_carp = false; +$retval = 0; +''' + + if self.params.get('mode') in ['carp', 'ipalias']: + cmd += '$uniqid = "' + self.params.get('uniqid') +'";\n' + cmd += '$subnet = "' + self.params.get('subnet') +'";\n' + cmd += '$interface = "' + self.params.get('interface') +'";\n' + cmd += '$vipif = get_real_interface($interface);\n' + + if self.params.get('state') == 'present': + if self.params.get('mode') in ['carp', 'ipalias']: + cmd += '$check_carp = true;\n' + cmd += 'foreach ($config["virtualip"]["vip"] as $vip) {\n' + cmd += 'if ($vip["uniqid"] == $uniqid) {\n' + cmd += 'interface_' + self.params.get('mode') + '_configure($vip);\n' + cmd += '}\n}\n' + else: + if self.params.get('mode') == 'carp': + cmd += 'if (does_interface_exist($vipif)) {\n' + cmd += 'if (is_ipaddrv6($subnet)) {\n' + cmd += 'mwexec("/sbin/ifconfig " . escapeshellarg($vipif) . " inet6 " . escapeshellarg($subnet) . " delete");\n' + cmd += '} else {\n' + cmd += 'pfSense_interface_deladdress($vipif, $subnet);\n' + cmd += '}\n}\n' + elif self.params.get('mode') == 'ipalias': + cmd += 'if (does_interface_exist($vipif)) {\n' + cmd += 'if (is_ipaddrv6($subnet)) {\n' + cmd += 'mwexec("/sbin/ifconfig " . escapeshellarg($vipif) . " inet6 " . escapeshellarg($subnet) . " -alias");\n' + cmd += '} else {\n' + cmd += 'pfSense_interface_deladdress($vipif, $subnet);\n' + cmd += '}\n}\n' + + cmd += ''' +if ($check_carp === true && !get_carp_status()) { + set_single_sysctl("net.inet.carp.allow", "1"); +} +$retval |= filter_configure(); +$retval |= mwexec("/etc/rc.filter_synchronize"); +clear_subsystem_dirty('vip');''' + + return self.pfsense.phpshell(cmd) + + ############################## + # Logging + # + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "vip" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + if before is None: + for param in VIRTUALIP_ARGUMENT_SPEC: + if param not in skip_list: + if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool) + else: + values += self.format_cli_field(self.obj, param) + else: + for param in VIRTUALIP_ARGUMENT_SPEC: + if param not in skip_list: + if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values), log_none=False) + + return values + + +def main(): + module = AnsibleModule( + argument_spec=VIRTUALIP_ARGUMENT_SPEC, + required_if=VIRTUALIP_REQUIRED_IF, + supports_check_mode=True) + + pfmodule = PFSenseVirtualIPModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From 6c6b565526def431e1353b1449959a7e44ea069e Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 14:54:06 +0000 Subject: [PATCH 09/16] feat: add telegraf module --- plugins/modules/pfsense_telegraf.py | 294 ++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 plugins/modules/pfsense_telegraf.py diff --git a/plugins/modules/pfsense_telegraf.py b/plugins/modules/pfsense_telegraf.py new file mode 100644 index 00000000..d5102c41 --- /dev/null +++ b/plugins/modules/pfsense_telegraf.py @@ -0,0 +1,294 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_telegraf +version_added: "0.4.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage pfSense telegraf settings +description: + - Manage pfSense telegraf settings +notes: +options: + enable: + description: Enable DNS resolver + required: false + type: bool + interval: + description: Update Interval + required: false + type: int + default: 10 + telegraf_output: + description: List of outputs to use + required: false + type: list + choices: ['influxdb', 'elasticsearch', 'graphite'] + influx_server: + description: Full HTTP or UDP endpoint URL for InfluxDB instance + required: false + type: str + influx_db: + description: Target database for metrics (created if does not exist) + required: false + type: str + influx_user: + description: Database user name if required by InfluxDB config + required: false + type: str + influx_pass: + description: Database password if required by InfluxDB config + required: false + type: str + insecure_skip_verify: + description: Use SSL but skip chain and host verification + required: false + type: bool + shortname: + description: Use short hostname instead of FQDN + required: false + type: bool + elasticsearch_server: + description: Full HTTP endpoint URL for ElasticSearch instance + required: false + type: str + graphite_server: + description: Graphite Endpoint + required: false + type: str + graphite_prefix: + description: Prefix to be used when submitting data to Graphite + required: false + type: str + graphite_timeout: + description: Timeout when submitting data to Graphite + required: false + type: str + haproxy_enable: + description: Database user name if required by InfluxDB config + required: false + type: bool + haproxy_port: + description: Port number where HAProxy status is available (default: 2200) + required: false + type: int + netstat_enable: + description: Enable Netstat Monitor + required: false + type: bool + ping_enable: + description: Enable Ping Monitor (up to 4 hosts (IPs)) + required: false + type: bool + ping_host_1: + description: Ping Host 1 + required: false + type: str + ping_host_2: + description: Ping Host 2 + required: false + type: str + ping_host_3: + description: Ping Host 3 + required: false + type: str + ping_host_4: + description: Ping Host 4 + required: false + type: str + telegraf_raw_config: + description: Additional configuration for Telegraf + required: false + type: str +""" + +EXAMPLES = """ +- name: setup telegraf + pfsense_telegraf: + enable: true + telegraf_raw_config: | + [[outputs.prometheus_client]] + listen = ":9273" + path = "/" + metric_version = 2 +""" + +RETURN = """ +commands: + description: the set of commands that would be pushed to the remote device (if pfSense had a CLI) + returned: always + type: list + sample: ["update telegraf set enable=True"] +""" + +import base64 +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +base64_params = [ + 'telegraf_raw_config' +] + +TELEGRAF_ARGUMENT_SPEC = dict( + enable=dict(required=False, type='bool'), + interval=dict(required=False, type='int', default=10), + telegraf_output=dict(required=False, type='list', elements='str', + choices=['influxdb', 'elasticsearch', 'graphite'], default=[]), + influx_server=dict(required=False, type='str'), + influx_db=dict(required=False, type='str'), + influx_user=dict(required=False, type='str'), + influx_pass=dict(required=False, type='str', no_log=True), + insecure_skip_verify=dict(required=False, type='bool', default=False), + shortname=dict(required=False, type='bool', default=False), + elasticsearch_server=dict(required=False, type='str'), + graphite_server=dict(required=False, type='str'), + graphite_prefix=dict(required=False, type='str'), + graphite_timeout=dict(required=False, type='int'), + haproxy_enable=dict(required=False, type='bool', default=False), + haproxy_port=dict(required=False, type=int, default='2200'), + netstat_enable=dict(required=False, type='bool', default=False), + ping_enable=dict(required=False, type='bool', default=False), + ping_host_1=dict(required=False, type='str'), + ping_host_2=dict(required=False, type='str'), + ping_host_3=dict(required=False, type='str'), + ping_host_4=dict(required=False, type='str'), + telegraf_raw_config=dict(required=False, type='str'), +) + + +class PFSenseTelegrafModule(PFSenseModuleBase): + """ module managing pfsense telegraf settings """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return TELEGRAF_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseTelegrafModule, self).__init__(module, pfsense) + self.name = "telegraf" + pkgs_elt = self.pfsense.get_element('installedpackages', create_node=True) + telegraf_elt = self.pfsense.get_element('telegraf', pkgs_elt, create_node=True) + self.root_elt = self.pfsense.get_element('config', telegraf_elt, create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + from pprint import pformat + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + if param in base64_params: + target[param] = base64.b64encode(params[param].encode()).decode() + else: + target[param] = params[param] + else: + if param in base64_params: + target[param] = base64.b64encode('\n'.join(params[param]).encode()).decode() + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and (param not in target or target[param] != 'on'): + target[param] = 'on' + elif value is False and (param not in target or target[param] != ''): + target[param] = '' + + def _set_param_list(target, param): + if param == 'telegraf_output': + target[param] = ','.join(params.get(param, [])) + + for param in TELEGRAF_ARGUMENT_SPEC: + if TELEGRAF_ARGUMENT_SPEC[param]['type'] == 'list': + _set_param_list(obj, param) + elif TELEGRAF_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + else: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def run(self, params): + """ process input params to add/update/delete """ + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + def _update(self): + """ make the target pfsense reload """ + cmd = ''' +require_once("filter.inc"); +require_once("telegraf.inc"); +$retval = 0; +$retval |= telegraf_resync_config(); +''' + return self.pfsense.phpshell(cmd) + + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "telegraf" + + @staticmethod + def fvalue_bool(value): + """ boolean value formatting function """ + if value is None or value is False or value == 'none' or value != 'on': + return 'False' + + return 'True' + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + for param in TELEGRAF_ARGUMENT_SPEC: + if TELEGRAF_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, self.before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + return values + + +def main(): + module = AnsibleModule( + argument_spec=TELEGRAF_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseTelegrafModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From 830a2e80dea379fae6916839e4d5993dd57ea0e1 Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 14:56:00 +0000 Subject: [PATCH 10/16] feat: add sudo module --- plugins/modules/pfsense_sudo.py | 243 ++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 plugins/modules/pfsense_sudo.py diff --git a/plugins/modules/pfsense_sudo.py b/plugins/modules/pfsense_sudo.py new file mode 100644 index 00000000..003be0ad --- /dev/null +++ b/plugins/modules/pfsense_sudo.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# 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 + +from pprint import pformat + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_sudo +version_added: "0.4.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage sudo settings +description: + - Manage pfSense sudo settings +notes: +options: + config: + description: Setup each sudo rule + required: true + type: list + elements: dict + suboptions: + username: + description: User or group name (prefix user with user: and group with group:) + required: True + type: str + runas: + description: Run As + required: False + type: str + default: user:root + nopasswd: + description: Require password + required: False + type: bool + default: False + cmdlist: + description: List of allowed commands (full paths required) + required: False + type: list + default: ['ALL'] + elements: str + add_includedir: + description: Include additional custom configuration files from /usr/local/etc/sudoers.d + required: False + type: str + choices: none, include_start, include_end +""" + +EXAMPLES = """ +""" + +RETURN = """ +""" + +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +SUDO_CONFIG_ARGUMENT_SPEC = dict( + username=dict(required=True, type='str'), + runas=dict(required=False, type='str', default='user:root'), + nopasswd=dict(required=False, type='bool', default=False), + cmdlist=dict(required=False, type='list', elements='str', default=['ALL']), +) + +SUDO_ARGUMENT_SPEC = dict( + row=dict(required=False, type='list', elements='dict', options=SUDO_CONFIG_ARGUMENT_SPEC), + add_includedir=dict(required=False, type='str', choices=[ + 'none', + 'include_start', + 'include_end', + ], default='none'), +) + +class PFSenseSudoModule(PFSenseModuleBase): + """ module managing pfsense sudo settings """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return SUDO_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseSudoModule, self).__init__(module, pfsense) + self.name = "sudo" + pkgs_elt = self.pfsense.get_element('installedpackages') + sudo_elt = self.pfsense.get_element('sudo', pkgs_elt, create_node=True) + self.root_elt = self.pfsense.get_element('config', sudo_elt, create_node=True) + self.target_elt = self.root_elt + self.params = dict() + self.obj = dict() + self.before = None + self.before_elt = None + self.params_to_delete = list() + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param_list(target, param): + if params.get(param) is not None: + if param == 'row': + rows = [] + for entry in params.get(param): + row = dict() + for subparam in SUDO_CONFIG_ARGUMENT_SPEC: + if entry.get(subparam) is not None: + value = entry.get(subparam) + if SUDO_CONFIG_ARGUMENT_SPEC[subparam]['type'] == 'bool': + if value is True: + row[subparam] = 'ON' + elif SUDO_CONFIG_ARGUMENT_SPEC[subparam]['type'] == 'list': + if subparam == 'cmdlist': + row[subparam] = ','.join(value) + else: + if isinstance(value, str): + row[subparam] = value + else: + row[subparam] = str(value) + rows.append(row) + + target[param] = rows + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and param not in target: + target[param] = '' + elif value is False and param in target: + del target[param] + + for param in SUDO_ARGUMENT_SPEC: + if SUDO_ARGUMENT_SPEC[param]['type'] == 'list': + _set_param_list(obj, param) + elif SUDO_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + else: + _set_param(obj, param) + + # self.module.fail_json(isinstance(obj['row'], list)) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + ############################## + # XML processing + # + def _remove_deleted_params(self): + """ Remove from target_elt a few deleted params """ + changed = False + for param in SUDO_ARGUMENT_SPEC: + if SUDO_ARGUMENT_SPEC[param]['type'] == 'bool': + if self.pfsense.remove_deleted_param_from_elt(self.target_elt, param, self.obj): + changed = True + + return changed + + ############################## + # run + # + def run(self, params): + """ process input params to add/update/delete """ + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + def _update(self): + """ make the target pfsense reload """ + cmd = ''' +require_once("sudo.inc"); +$retval = 0; +$retval |= sudo_write_config(); +''' + return self.pfsense.phpshell(cmd) + + ############################## + # Logging + # + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "sudo" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + for param in SUDO_ARGUMENT_SPEC: + if SUDO_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, self.before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + elif SUDO_ARGUMENT_SPEC[param]['type'] == 'list': + pass + else: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + return values + + +def main(): + module = AnsibleModule( + argument_spec=SUDO_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseSudoModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From f72fa3e70016c37b67e07345761547df9df2d197 Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 15:02:16 +0000 Subject: [PATCH 11/16] feat: add dns-resolver module --- plugins/modules/pfsense_dns_resolver.py | 429 ++++++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 plugins/modules/pfsense_dns_resolver.py diff --git a/plugins/modules/pfsense_dns_resolver.py b/plugins/modules/pfsense_dns_resolver.py new file mode 100644 index 00000000..6f603c2b --- /dev/null +++ b/plugins/modules/pfsense_dns_resolver.py @@ -0,0 +1,429 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_dns_resolver +version_added: "0.4.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage pfSense dns resolver settings +description: + - Manage pfSense dns resolver settings +notes: +options: + enable: + description: Enable DNS resolver + required: false + type: bool + port: + description: Listen Port + required: false + type: int + enablessl: + description: Enable SSL/TLS Service + required: false + type: bool + sslcertref: + description: Enable SSL/TLS Service + required: false + type: str + tlsport: + description: SSL/TLS Listen Port + required: false + type: int + active_interface: + description: Network Interfaces + required: false + type: list + outgoing_interface: + description: Outgoing Network Interfaces + required: false + type: list + system_domain_local_zone_type: + description: System Domain Local Zone Type + required: false + type: str + choices: ['deny', 'refuse', 'static', 'transparent', 'type transparent', 'redirect', 'inform', 'inform deny', 'no default'] + dnssec: + description: Enable DNSSEC Support + required: false + type: bool + python: + description: Enable Python Module + required: false + type: bool + python_order: + description: Python Module Order + required: false + type: str + choices: ['pre_validator', 'post_validator'] + python_script: + required: false + type: str + forwarding: + description: Enable Forwarding Mode + required: false + type: bool + forward_tls_upstream: + description: Use SSL/TLS for outgoing DNS Queries to Forwarding Servers + required: false + type: bool + regdhcp: + description: Register DHCP leases in the DNS Resolver + required: false + type: bool + regdhcpstatic: + description: Register DHCP static mappings in the DNS Resolver + required: false + type: bool + regovpnclients: + description: Register connected OpenVPN clients in the DNS Resolver + required: false + type: bool + hosts: + description: Dict of host overrides + type: list + elements: dict + suboptions: + host: + description: Hostname + required: true + type: str + domain: + description: Domain Name + required: true + type: str + ip: + description: IP Address + required: true + type: str + descr: + description: Description of Host + required: false + type: str + aliases: + description: List of dicts of additional hostnames + required: false + type: list + elements: dict + suboptions: + host: + description: Hostname + required: true + type: str + domain: + description: Domain Name + required: true + type: str + descr: + description: Description of Host + required: false + type: str + domainoverrides: + description: Dict of domain overrides + type: list + elements: dict + suboptions: + domain: + description: Domain Name + required: true + type: str + ip: + description: IP Address of Nameserver + required: true + type: str + forward_tls_upstream: + description: Use SSL/TLS for DNS Queries forwarded to this server + required: false + type: str + tls_hostname: + description: An optional TLS hostname used to verify the server certificate when performing TLS Queries. + required: false + type: str + descr: + description: Description of Domain + required: false + type: str +""" + +EXAMPLES = """ +- name: setup dns resolver to use forwarders + pfsense_dns_resolver: + enable: true + forwarding: true +""" + +RETURN = """ +commands: + description: the set of commands that would be pushed to the remote device (if pfSense had a CLI) + returned: always + type: list + sample: ["update dns_resolver set enable='true', forwarding='true'"] +""" + +import re +import sys +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +DNS_RESOLVER_HOSTS_ALIASES_ARGUMENT_SPEC = dict( + host=dict(required=True, type='str'), + domain=dict(required=True, type='str'), + descr=dict(required=False, type='str'), +) + +DNS_RESOLVER_HOSTS_ARGUMENT_SPEC = dict( + host=dict(required=True, type='str'), + domain=dict(required=True, type='str'), + ip=dict(required=True, type='str'), + descr=dict(required=False, type='str'), + aliases=dict(required=False, type='list', elements='dict', + options=DNS_RESOLVER_HOSTS_ALIASES_ARGUMENT_SPEC), +) + +DNS_RESOLVER_DOMAINOVERRIDES_ARGUMENT_SPEC = dict( + domain=dict(required=True, type='str'), + ip=dict(required=True, type='str'), + descr=dict(required=False, type='str'), + tls_hostname=dict(required=False, type='str'), + forward_tls_upstream=dict(required=False, type='bool'), +) + +DNS_RESOLVER_ARGUMENT_SPEC = dict( + enable=dict(required=False, type='bool'), + port=dict(required=False, type='int'), + enablessl=dict(required=False, type='bool'), + sslcertref=dict(required=False, type='str'), + tlsport=dict(required=False, type='int'), + active_interface=dict(required=False, type='int'), + outgoing_interface=dict(required=False, type='int'), + system_domain_local_zone_type=dict(required=False, type='str', + choices=['deny', + 'refuse', + 'static', + 'transparent', + 'type transparent', + 'redirect', + 'inform', + 'inform deny', + 'no default']), + dnssec=dict(required=False, type='bool'), + python=dict(required=False, type='bool'), + python_order=dict(required=False, type='str', choices=['pre_validator', 'post_validator']), + python_script=dict(required=False, type='str'), + forwarding=dict(required=False, type='bool'), + forward_tls_upstream=dict(required=False, type='bool'), + regdhcp=dict(required=False, type='bool'), + regdhcpstatic=dict(required=False, type='bool'), + regovpnclients=dict(required=False, type='bool'), + custom_options=dict(required=False, type='str'), + hosts=dict( + required=False, type='list', elements='dict', + options=DNS_RESOLVER_HOSTS_ARGUMENT_SPEC, + ), + domainoverrides=dict( + required=False, type='list', elements='dict', + options=DNS_RESOLVER_DOMAINOVERRIDES_ARGUMENT_SPEC, + ) +) + + +class PFSenseDNSResolverModule(PFSenseModuleBase): + """ module managing pfsense dns resolver settings """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return DNS_RESOLVER_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseDNSResolverModule, self).__init__(module, pfsense) + self.name = "dns_resolver" + self.root_elt = self.pfsense.get_element('unbound') + self.target_elt = self.root_elt + self.params = dict() + self.obj = dict() + self.before = None + self.before_elt = None + self.route_cmds = list() + self.params_to_delete = list() + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and param not in target: + target[param] = '' + elif value is False and param in target: + del target[param] + + def _set_param_list(target, param): + if params.get(param) is not None: + if param == 'domainoverrides': + domainoverrides = [] + for entry in params.get(param): + domainoverride = dict() + for subparam in DNS_RESOLVER_DOMAINOVERRIDES_ARGUMENT_SPEC: + if entry.get(subparam) is not None: + if DNS_RESOLVER_DOMAINOVERRIDES_ARGUMENT_SPEC[subparam]['type'] == 'bool': + value = entry.get(subparam) + if value is True: + domainoverride[subparam] = '' + else: + if isinstance(entry[subparam], str): + domainoverride[subparam] = entry[subparam] + else: + domainoverride[subparam] = str(entry[subparam]) + domainoverrides.append(domainoverride) + target[param] = domainoverrides + elif param == 'hosts': + hosts = [] + for entry in params.get(param): + host = dict() + for subparam in DNS_RESOLVER_HOSTS_ARGUMENT_SPEC: + if entry.get(subparam) is not None: + host[subparam] = {} + if DNS_RESOLVER_HOSTS_ARGUMENT_SPEC[subparam]['type'] == 'list': + # this will break the config + aliases = [] + for subentry in entry.get(subparam): + alias = dict() + for subsubparam in DNS_RESOLVER_HOSTS_ALIASES_ARGUMENT_SPEC: + if isinstance(subentry[subsubparam], str): + alias[subsubparam] = subentry[subsubparam] + else: + alias[subsubparam] = str(subentry[subsubparam]) + aliases.append(alias) + # dict_to_element will generate multiple elements, but pfsense wants with multiple -Elements + host[subparam]['item'] = aliases + else: + if isinstance(entry[subparam], str): + host[subparam] = entry[subparam] + else: + host[subparam] = str(entry[subparam]) + hosts.append(host) + target[param] = hosts + + for param in DNS_RESOLVER_ARGUMENT_SPEC: + if DNS_RESOLVER_ARGUMENT_SPEC[param]['type'] == 'list': + _set_param_list(obj, param) + elif DNS_RESOLVER_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + else: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + ############################## + # XML processing + # + def _remove_deleted_params(self): + """ Remove from target_elt a few deleted params """ + changed = False + for param in DNS_RESOLVER_ARGUMENT_SPEC: + if DNS_RESOLVER_ARGUMENT_SPEC[param]['type'] == 'bool': + if self.pfsense.remove_deleted_param_from_elt(self.target_elt, param, self.obj): + changed = True + + return changed + + ############################## + # run + # + def run(self, params): + """ process input params to add/update/delete """ + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + def _update(self): + """ make the target pfsense reload """ + for cmd in self.route_cmds: + self.module.run_command(cmd) + + cmd = ''' +require_once("filter.inc"); +$retval = 0; +$retval |= services_unbound_configure(); +if ($retval == 0) { + clear_subsystem_dirty('unbound'); +} +/* Update resolv.conf in case the interface bindings exclude localhost. */ +system_resolvconf_generate(); +/* Start or restart dhcpleases when it's necessary */ +system_dhcpleases_configure(); +''' + return self.pfsense.phpshell(cmd) + + ############################## + # Logging + # + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "unbound" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + for param in DNS_RESOLVER_ARGUMENT_SPEC: + if DNS_RESOLVER_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, self.before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + return values + + +def main(): + module = AnsibleModule( + argument_spec=DNS_RESOLVER_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseDNSResolverModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From 7739940a996bf1160383e18e7d221f5b22bcb715 Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 15:05:50 +0000 Subject: [PATCH 12/16] feat: add hasync module --- plugins/modules/pfsense_hasync.py | 310 ++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 plugins/modules/pfsense_hasync.py diff --git a/plugins/modules/pfsense_hasync.py b/plugins/modules/pfsense_hasync.py new file mode 100644 index 00000000..f4809df5 --- /dev/null +++ b/plugins/modules/pfsense_hasync.py @@ -0,0 +1,310 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_hasync +version_added: "0.4.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage pfSense hasync settings +description: + - Manage pfSense hasync settings +notes: +options: + pfsyncenabled: + description: Transfer state insertion, update, and deletion messages between firewalls. + required: false + type: bool + pfsyncinterface: + description: If pfsyncenabled is true this interface will be used for communication. + required: false + type: str + pfsyncpeerip: + description: Setting this option will force pfsync to synchronize its state table to this IP address. The default is directed multicast. + required: false + type: str + synchronizetoip: + description: The IP address of the firewall to which the selected configuration sections should be synchronized. + required: false + type: str + username: + description: The username of the synchronizetoip system for synchronizing the configuration. + required: false + type: str + password: + description: The password of the synchronizetoip system for synchronizing the configuration. + required: false + type: str + adminsync: + description: Synchronize admin accounts and autoupdate sync password. + required: false + type: bool + synchronizeusers: + description: Sync User manager users and groups + required: false + type: bool + synchronizeauthservers: + description: Sync Authentication servers (e.g. LDAP, RADIUS) + required: false + type: bool + synchronizecerts: + description: Sync Certificate Authorities, Certificates, and Certificate Revocation Lists + required: false + type: bool + synchronizerules: + description: Sync Firewall rules + required: false + type: bool + synchronizeschedules: + description: Sync Firewall schedules + required: false + type: bool + synchronizealiases: + description: Sync Firewall aliases + required: false + type: bool + synchronizenat: + description: Sync NAT configuration + required: false + type: bool + synchronizeipsec: + description: Sync IPsec configuration + required: false + type: bool + synchronizeopenvpn: + description: Sync OpenVPN configuration (Implies CA/Cert/CRL Sync) + required: false + type: bool + synchronizedhcpd: + description: Sync DHCP Server settings + required: false + type: bool + synchronizewol: + description: Sync WoL Server settings + required: false + type: bool + synchronizestaticroutes: + description: Sync Static Route configuration + required: false + type: bool + synchronizevirtualip: + description: Sync Virtual IPs + required: false + type: bool + synchronizetrafficshaper: + description: Sync Traffic Shaper configuration + required: false + type: bool + synchronizetrafficshaperlimiter: + description: Sync Traffic Shaper Limiters configuration + required: false + type: bool + synchronizednsforwarder: + description: Sync DNS Forwarder and DNS Resolver configurations + required: false + type: bool + synchronizecaptiveportal: + description: Sync Captive Portal + required: false + type: bool +""" + +EXAMPLES = """ + pfsensible.core.pfsense_hasync: + pfsyncenabled: true + pfsyncpeerip: "192.168.61.12" + pfsyncinterface: "lan" + synchronizetoip: "192.168.61.11" + username: "admin" + password: "pfsense" + synchronizerules: true + synchronizedhcpd: true +""" + +RETURN = """ + description: the set of commands that would be pushed to the remote device (if pfSense had a CLI) + returned: always + type: list + sample: [ + "update hasync hasync set pfsyncinterface='lan', pfsyncpeerip='192.168.61.12'" + ] +""" + +import re +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + + +HASYNC_ARGUMENT_SPEC = dict( + pfsyncenabled=dict(required=False, type='bool'), + pfsyncinterface=dict(required=False, type='str'), + pfsyncpeerip=dict(required=False, type='str'), + synchronizetoip=dict(required=False, type='str'), + username=dict(required=False, type='str'), + password=dict(required=False, type='str', no_log=True), + adminsync=dict(required=False, type='bool'), + synchronizeusers=dict(required=False, type='bool'), + synchronizeauthservers=dict(required=False, type='bool'), + synchronizecerts=dict(required=False, type='bool'), + synchronizerules=dict(required=False, type='bool'), + synchronizeschedules=dict(required=False, type='bool'), + synchronizealiases=dict(required=False, type='bool'), + synchronizenat=dict(required=False, type='bool'), + synchronizeipsec=dict(required=False, type='bool'), + synchronizeopenvpn=dict(required=False, type='bool'), + synchronizedhcpd=dict(required=False, type='bool'), + synchronizewol=dict(required=False, type='bool'), + synchronizestaticroutes=dict(required=False, type='bool'), + synchronizevirtualip=dict(required=False, type='bool'), + synchronizetrafficshaper=dict(required=False, type='bool'), + synchronizetrafficshaperlimiter=dict(required=False, type='bool'), + synchronizednsforwarder=dict(required=False, type='bool'), + synchronizecaptiveportal=dict(required=False, type='bool'), +) + +class PFSenseHASyncModule(PFSenseModuleBase): + """ module managing pfsense hasync settings """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return HASYNC_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseHASyncModule, self).__init__(module, pfsense) + self.name = "hasync" + self.root_elt = self.pfsense.get_element('hasync') + self.target_elt = self.root_elt + self.params = dict() + self.obj = dict() + self.before = None + self.before_elt = None + self.route_cmds = list() + self.params_to_delete = list() + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param(target, param): + # get possibly mapped settings name + if params.get(param) is not None: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + # get possibly mapped settings name + if params.get(param) is not None: + value = params.get(param) + if value is True and param not in target: + target[param] = 'on' + elif value is False and param in target: + del target[param] + + for param in HASYNC_ARGUMENT_SPEC: + if HASYNC_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + else: + _set_param(obj, param) + + return obj + + + def _validate_params(self): + """ do some extra checks on input parameters """ + params = self.params + return + + ############################## + # XML processing + # + def _remove_deleted_params(self): + """ Remove from target_elt a few deleted params """ + changed = False + for param in HASYNC_ARGUMENT_SPEC: + if HASYNC_ARGUMENT_SPEC[param]['type'] == 'bool': + if self.pfsense.remove_deleted_param_from_elt(self.target_elt, param, self.obj): + changed = True + + return changed + + ############################## + # run + # + def run(self, params): + """ process input params to add/update/delete """ + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + def _update(self): + """ make the target pfsense reload """ + for cmd in self.route_cmds: + self.module.run_command(cmd) + cmd = ''' +require_once("interfaces.inc"); +$retval = 0; +$retval |= interfaces_sync_setup();''' + + return self.pfsense.phpshell(cmd) + + ############################## + # Logging + # + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "hasync_settings" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + for param in HASYNC_ARGUMENT_SPEC: + if HASYNC_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, self.before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + return values + + +def main(): + module = AnsibleModule( + argument_spec=HASYNC_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseHASyncModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From 907908256ef45b0063e8a9dfef2bfbad6499a98b Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 15:11:47 +0000 Subject: [PATCH 13/16] feat: add nat-outbound-mode module --- plugins/modules/pfsense_nat_outbound_mode.py | 115 +++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 plugins/modules/pfsense_nat_outbound_mode.py diff --git a/plugins/modules/pfsense_nat_outbound_mode.py b/plugins/modules/pfsense_nat_outbound_mode.py new file mode 100644 index 00000000..6a8f582f --- /dev/null +++ b/plugins/modules/pfsense_nat_outbound_mode.py @@ -0,0 +1,115 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Frederic Bor +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_nat_outbound_mode +version_added: "0.4.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage pfSense Outbound NAT Mode +description: + - Manage pfSense Outbound NAT Mode +notes: +options: + mode: + description: The outbound nat mode + required: true + default: null + type: str + choices: ['automatic', 'hybrid', 'advanced', 'disabled'] +""" + +EXAMPLES = """ +- name: "Set NAT outbound mode to hybrid" + pfsense_nat_outbound_mode: + mode: 'hybrid' +""" + +RETURN = """ +commands: + description: the set of commands that would be pushed to the remote device (if pfSense had a CLI) + returned: always + type: list + sample: ["update nat_outbound_mode"] +""" + +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +NAT_OUTBOUND_MODE_ARGUMENT_SPEC = dict( + mode=dict(type='str', required=True, choices=['automatic', 'hybrid', 'advanced', 'disabled']) +) + + +class PFSenseNatOutboundModeModule(PFSenseModuleBase): + """ module managing pfsense outbound nat mode """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return NAT_OUTBOUND_MODE_ARGUMENT_SPEC + + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "mode" + + def __init__(self, module, pfsense=None): + super(PFSenseNatOutboundModeModule, self).__init__(module, pfsense) + self.name = "nat_outbound" + self.obj = dict() + self.before = None + self.before_elt = None + nat_elt = self.pfsense.get_element('nat', create_node=True) + self.root_elt = self.pfsense.get_element('outbound', nat_elt, create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + + params = self.params + + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + obj = dict() + obj['mode'] = params['mode'] + return obj + + def run(self, params): + self.params = params + self.target_elt = self.root_elt + self.obj = self._params_to_obj() + self._add() + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + return self.format_updated_cli_field(self.obj, self.before, 'mode') + + def _update(self): + return self.pfsense.phpshell('''require_once("filter.inc"); +if (filter_configure() == 0) { clear_subsystem_dirty('natconf'); clear_subsystem_dirty('filter'); }''') + +def main(): + module = AnsibleModule( + argument_spec=NAT_OUTBOUND_MODE_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseNatOutboundModeModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From 06a2fc4c802433436b6010dee84433bc10349d87 Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 15:14:16 +0000 Subject: [PATCH 14/16] feat: add ntpd module --- plugins/modules/pfsense_ntpd.py | 262 ++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 plugins/modules/pfsense_ntpd.py diff --git a/plugins/modules/pfsense_ntpd.py b/plugins/modules/pfsense_ntpd.py new file mode 100644 index 00000000..ccff63e3 --- /dev/null +++ b/plugins/modules/pfsense_ntpd.py @@ -0,0 +1,262 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_ntpd +version_added: "0.4.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage ntp service settings +description: + - Manage pfSense ntp service settings. +notes: NTP Peers without options must be set via pfsense_setup +options: + enable: + description: Enable NTP Service + required: false + type: bool + orphan: + description: Stratum of local clock when in orphan mode + required: false + type: int + default: 12 + interface: + description: Interfaces to listen on + required: false + type: list + timeservers: + description: NTP Peers + required: false + type: list + elements: dict + suboptions: + server: + description: NTP Host + required: true + type: str + prefer: + description: Prefer flag + required: false + type: bool + noselect: + description: Noselect flag + required: false + type: bool + ispool: + description: server is pool + required: false + type: bool + dnsresolv: + description: DNS Resolution Behaviour + required: false + type: str + choices: ['auto', 'inet', 'inet6'] + ntpminpoll: + description: Minimum Poll Interval (pfsense-CE >=2.5.0, pfsense-PLUS >=21.2) + required: false + type: raw + choices: ['', 'omit', 5-17] + ntpmaxpoll: + description: Minimum Poll Interval (pfsense-CE >=2.5.0, pfsense-PLUS >=21.2) + required: false + type: raw + choices: ['', 'omit', 5-17] + statsgraph: + description: Enable RRD graphs of NTP statistics + required: false + type: bool + logpeer: + description: Log peer messages + required: false + type: bool + logsys: + description: Log system messages + required: false + type: bool +""" + +EXAMPLES = """ +""" + +RETURN = """ +""" + +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.pfsense import PFSenseModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +NTP_TIMESERVER_ARGUMENT_SPEC = dict( + server=dict(required=True, type='str'), + prefer=dict(required=False, type='bool'), + noselect=dict(required=False, type='bool'), + ispool=dict(required=False, type='bool'), +) + +NTP_SERVICE_ARGUMENT_SPEC = dict( + enable=dict(required=False, type='bool', default=True), + orphan=dict(required=False, type='int', default=12), + interface=dict(required=False, type='list', elements='str'), + timeservers=dict(required=False, type='list', elements='dict', + options=NTP_TIMESERVER_ARGUMENT_SPEC), + dnsresolv=dict(required=False, type='str', + choices=['auto', 'inet', 'inet6'], default='auto'), + ntpminpoll=dict(required=False, type='raw', + choices=['', 'omit', 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17], default=''), + ntpmaxpoll=dict(required=False, type='raw', + choices=['', 'omit', 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17], default=''), + statsgraph=dict(required=False, type='bool'), + logpeer=dict(required=False, type='bool'), + logsys=dict(required=False, type='bool'), +) + + +class PFSenseNtpServiceModule(PFSenseModuleBase): + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return NTP_SERVICE_ARGUMENT_SPEC(PFSenseModuleBase) + + def __init__(self, module, pfsense=None): + super(PFSenseNtpServiceModule, self).__init__(module, pfsense) + self.name = "pfsense_ntpd_config" + self.obj = dict() + self.before = None + self.before_elt = None + self.root_elt = self.pfsense.get_element('ntpd', create_node=True) + self.system_elt = self.pfsense.get_element('system') + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + + def _set_param(target, param): + if params.get(param) is not None: + if param in ['ntpminpoll', 'ntpmaxpoll']: + if self.pfsense.is_at_least_2_5_0(): + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + else: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if param == 'enable': + if value is True and (param not in target or target[param] != 'enabled'): + target[param] = 'enabled' + elif value is False and (param not in target or target[param] != ''): + target[param] = '' + else: + if value is True and (param not in target or target[param] != 'yes'): + target[param] = 'yes' + elif value is False and (param not in target or target[param] != ''): + target[param] = '' + + def _set_param_list(target, param): + if params.get(param) is not None: + if param == 'timeservers': + noselect = [] + ispool = [] + prefer = [] + for timeserver in params.get(param): + if timeserver.get('ispool', False) is True: + ispool.append(timeserver.get('server')) + if timeserver.get('noselect', False) is True: + noselect.append(timeserver.get('server')) + if timeserver.get('prefer', False) is True: + prefer.append(timeserver.get('server')) + if noselect: + target['noselect'] = ' '.join(noselect) + if ispool: + target['ispool'] = ' '.join(ispool) + if prefer: + target['prefer'] = ' '.join(prefer) + if param == 'interface': + target[param] = ','.join(params.get(param)) + + for param in NTP_SERVICE_ARGUMENT_SPEC: + if NTP_SERVICE_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + elif NTP_SERVICE_ARGUMENT_SPEC[param]['type'] == 'list': + _set_param_list(obj, param) + else: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def run(self, params): + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + def _update(self): + """ make the target pfsense reload """ + cmd = ''' +require_once("auth.inc"); +require_once("filter.inc"); +$retval = 0; +$retval |= system_ntp_configure();''' + return self.pfsense.phpshell(cmd) + + @staticmethod + def _get_obj_name(): + return "ntpd" + + def _log_fields(self, before=None): + values = '' + + if before is None: + for param in NTP_SERVICE_ARGUMENT_SPEC: + if NTP_SERVICE_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool) + else: + values += self.format_cli_field(self.obj, param) + else: + for param in NTP_SERVICE_ARGUMENT_SPEC: + if NTP_SERVICE_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, self.before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + return values + +def main(): + module = AnsibleModule( + argument_spec=NTP_SERVICE_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseNtpServiceModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From baf5807d0d5a0e422b16b8d67262626de99e2d4b Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 15:19:02 +0000 Subject: [PATCH 15/16] feat: add service module --- plugins/modules/pfsense_service.py | 184 +++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 plugins/modules/pfsense_service.py diff --git a/plugins/modules/pfsense_service.py b/plugins/modules/pfsense_service.py new file mode 100644 index 00000000..31222fc0 --- /dev/null +++ b/plugins/modules/pfsense_service.py @@ -0,0 +1,184 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Peter B. Dick +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_service +version_added: "0.4.3" +author: Peter B. Dick +short_description: Adds a service to PFSense config settings +description: + - Adds a service to PFSense config settings +notes: +options: + name: + description: The name of the service + required: true + type: str + rcfile: + description: The name of the rc file + required: true + type: str + executable: + description: The name of the executable + required: true + type: str + description: + description: This parameter is not yet supported! + required: false + type: str +""" + +EXAMPLES = """ +- name: add a service + pfsense_service: + name: filebeat + rcfile: filebeat + executable: filebeat +""" + +RETURN = """ +commands: + description: the set of commands that would be pushed to the remote device (if pfSense had a CLI) + returned: always + type: list + sample: ["services add filebeat"] +""" + +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +SERVICE_ARGUMENT_SPEC = dict( + name=dict(required=True, type='str'), + rcfile=dict(required=True, type='str'), + executable=dict(required=True, type='str'), + description=dict(default='', required=False, type='str'), +) + + +class PFSenseServiceModule(PFSenseModuleBase): + """ module managing pfsense Service settings """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return SERVICE_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseServiceModule, self).__init__(module, pfsense) + self.serviceElement = None + self.name = "pfsense_service" + self.installedPackagesElement = self.pfsense.get_element('installedpackages', create_node=True) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + obj = self.pfsense.element_to_dict(self.serviceElement) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.serviceElement) + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + for param in SERVICE_ARGUMENT_SPEC: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + def run(self, params): + """ process input params to add/update/delete """ + self.params = params + + # find given name for service in parameter name + serviceName = params.get('name') + + # find service for service name + for service in self.installedPackagesElement.findall('service'): + if service is not None: + name = service.find('name').text + if serviceName == name: + self.serviceElement = service + + # add a new new service if none exist with the given parameter name + if self.serviceElement is None: + self.serviceElement = self.pfsense.new_element('service') + self.installedPackagesElement.append(self.serviceElement) + + self.target_elt = self.serviceElement + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + def _update(self): + """ make the target pfsense reload """ + cmd = ''' +require_once("filter.inc"); +require_once("Service.inc"); +$retval = 0; +$retval |= Service_resync_config(); +''' + return self.pfsense.phpshell(cmd) + + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "Service" + + @staticmethod + def fvalue_bool(value): + """ boolean value formatting function """ + if value is None or value is False or value == 'none' or value != 'on': + return 'False' + + return 'True' + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + for param in SERVICE_ARGUMENT_SPEC: + if SERVICE_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, self.before, param, fvalue=self.fvalue_bool, + add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), + log_none=False) + + return values + + +def main(): + module = AnsibleModule( + argument_spec=SERVICE_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseServiceModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From 4cc1e6040ebd5c59a361fb9b0955186a8f51613e Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 15:21:43 +0000 Subject: [PATCH 16/16] feat: add snmp module --- plugins/modules/pfsense_snmp.py | 299 ++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 plugins/modules/pfsense_snmp.py diff --git a/plugins/modules/pfsense_snmp.py b/plugins/modules/pfsense_snmp.py new file mode 100644 index 00000000..e3b382b9 --- /dev/null +++ b/plugins/modules/pfsense_snmp.py @@ -0,0 +1,299 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_snmp +version_added: "0.4.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage pfSense snmp settings +description: + - Manage pfSense snmp settings +notes: +options: + enable: + description: Enable SNMP Service + required: false + type: bool + pollport: + description: SNMP Service Port + required: false + type: int + default: 161 + syslocation: + description: System Location + required: false + type: str + syscontact: + description: System Contact + required: false + type: str + rocommunity: + description: Read Community String + required: false + type: str + default: public + trapenable: + description: Enable SNMP Trap + required: false + type: bool + trapserver: + description: SNMP Trap Target Server + required: false + type: str + trapserverport: + description: SNMP Trap Target Port + required: false + type: int + trapstring: + description: SNMP Trap String + required: false + type: str + mibii: + description: Enable SNMP MibII Module + required: false + type: bool + netgraph: + description: Enable SNMP Netgraph Module + required: false + type: bool + pf: + description: Enable SNMP PF Module + required: false + type: bool + hostres: + description: Enable SNMP Host Resources Module + required: false + type: bool + ucd: + description: Enable SNMP UCD Module + required: false + type: bool + regex: + description: Enable SNMP Regex Module + required: false + type: bool + bindip: + description: Bind Interfaces + required: false + type: list + elements: str +""" + +EXAMPLES = """ +pfsensible.core.pfsense_snmp: + enable: true + syslocation: "Some Datacenter" + bindip: + - "lan" + - "lo0" +""" + +RETURN = """ + description: the set of commands that would be pushed to the remote device (if pfSense had a CLI) + returned: always + type: list + sample: [ + "update snmp snmp set enable=True, syslocation='Some Datacenter', bindip='lan,lo0'" + ] +""" + +import re +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +SNMP_ARGUMENT_SPEC = dict( + enable=dict(required=False, type='bool'), + pollport=dict(required=False, type='int'), + syslocation=dict(required=False, type='str'), + syscontact=dict(required=False, type='str'), + rocommunity=dict(required=False, type='str', default='public'), + trapenable=dict(required=False, type='bool'), + trapserver=dict(required=False, type='str'), + trapserverport=dict(required=False, type='int', default=162), + trapstring=dict(required=False, type='str'), + mibii=dict(required=False, type='bool', default=True), + netgraph=dict(required=False, type='bool', default=True), + pf=dict(required=False, type='bool', default=True), + hostres=dict(required=False, type='bool', default=True), + ucd=dict(required=False, type='bool', default=True), + regex=dict(required=False, type='bool', default=True), + bindip=dict(required=False, type='list', elements='str', default=['all']), +) + +modules = ['mibii', 'netgraph', 'pf', 'hostres', 'ucd', 'regex'] + + +class PFSenseSnmpModule(PFSenseModuleBase): + """ module managing pfsense snmp settings """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return SNMP_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseSnmpModule, self).__init__(module, pfsense) + self.name = "snmp" + self.root_elt = self.pfsense.get_element('snmpd') + self.target_elt = self.root_elt + self.params = dict() + self.obj = dict() + self.before = None + self.before_elt = None + self.route_cmds = list() + self.params_to_delete = list() + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = self.pfsense.element_to_dict(self.root_elt) + self.before = deepcopy(obj) + self.before_elt = deepcopy(self.root_elt) + modules = obj['modules'] + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and param not in target: + target[param] = '' + elif value is False and param in target: + del target[param] + + def _set_param_list(target, param): + if params.get(param) is not None: + target[param] = ','.join(params[param]) + + _set_param_bool(obj, 'enable') + _set_param(obj, 'pollport') + _set_param(obj, 'syslocation') + _set_param(obj, 'syscontact') + _set_param(obj, 'rocommunity') + _set_param_bool(obj, 'trapenable') + _set_param(obj, 'trapserver') + _set_param(obj, 'trapserverport') + _set_param(obj, 'trapstring') + _set_param_bool(modules, 'mibii') + _set_param_bool(modules, 'netgraph') + _set_param_bool(modules, 'pf') + _set_param_bool(modules, 'hostres') + _set_param_bool(modules, 'ucd') + _set_param_bool(modules, 'regex') + _set_param_list(obj, 'bindip') + + return obj + + + def _validate_params(self): + """ do some extra checks on input parameters """ + pass + + ############################## + # XML processing + # + def _remove_deleted_params(self): + """ Remove from target_elt a few deleted params """ + changed = False + for param in SNMP_ARGUMENT_SPEC: + if SNMP_ARGUMENT_SPEC[param]['type'] == 'bool': + if param in modules: + continue + if self.pfsense.remove_deleted_param_from_elt(self.target_elt, param, self.obj): + changed = True + + modules_elt = self.target_elt.find('modules') + _modules = self.obj['modules'] + for param in modules: + if self.pfsense.remove_deleted_param_from_elt(modules_elt, param, _modules): + changed = True + + return changed + + ############################## + # run + # + def run(self, params): + """ process input params to add/update/delete """ + self.params = params + self.target_elt = self.root_elt + self._validate_params() + self.obj = self._params_to_obj() + self._add() + + def _update(self): + """ make the target pfsense reload """ + for cmd in self.route_cmds: + self.module.run_command(cmd) + + cmd = ''' +require_once("functions.inc"); +$retval = 0; +$retval |= services_snmpd_configure();''' + + return self.pfsense.phpshell(cmd) + + ############################## + # Logging + # + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "snmp" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + for param in SNMP_ARGUMENT_SPEC: + if param in modules: + continue + if SNMP_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, self.before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, self.before, param, add_comma=(values), log_none=False) + + for param in modules: + values += self.format_updated_cli_field(self.obj['modules'], self.before['modules'], param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + + return values + + +def main(): + module = AnsibleModule( + argument_spec=SNMP_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseSnmpModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main()