diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 050210a2..aca58d21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,8 +18,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.9'] - ansible-version: ['2.12', '2.13', '2.14'] + python-version: ['3.10'] + ansible-version: ['2.14', '2.15', '2.16'] # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 00000000..d01e4f6e --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,53 @@ +============================= +pfSensible.Core Release Notes +============================= + +.. contents:: Topics + + +v0.6.0 +====== + +Major Changes +------------- + +- pfsense_default_gateway - Add module for setting the default gateways (https://github.com/pfsensible/core/pull/99) +- pfsense_dns_resolver - Add module for DNS resolver (unbound) settings (https://github.com/pfsensible/core/pull/76) + +Minor Changes +------------- + +- ipaddress support for pfSense 2.4.4 +- pfsense_cert - Support EC certs (https://github.com/pfsensible/core/pull/98) +- pfsense_interface - Always return `ifname` - even on interface creation +- pfsense_interface - Prevent removal if interface is part of an interface group +- pfsense_nat_outbound - Allow for NET:INTERFACE addresses +- pfsense_nat_port_forward - 2.4.5 compatibility +- pfsense_openvpn_server - Do not allow removal of an instance with an interface assignment +- pfsense_rule - Add option to ignore an inexistent queue +- pfsense_rule - Add support for floating 'any' interface rule (https://github.com/pfsensible/core/pull/90) +- plugins/lookup/pfsense - Optimization and ignore queue setting +- tests/plays - Add plays for testing with a live pfSense instance + +Bugfixes +-------- + +- pfsense_aggregate - Fix where a rule with a duplicated name would not be deleted if required +- pfsense_dhcp_static - Allow removing entry with just name (https://github.com/pfsensible/core/issues/69) +- pfsense_dhcp_static - Allow use of display name for netif. Error in case a interface group name is specified (https://github.com/pfsensible/core/issues/79) +- pfsense_interface - Properly shut dwon interface and kill dhclient process when removing interface (https://github.com/pfsensible/core/pull/67) +- pfsense_interface_group - Check that members list is unique +- pfsense_interface_group - Fix creation (https://github.com/pfsensible/core/issues/74) +- pfsense_interface_group - `members` is only required for creation +- pfsense_nat_outbound - Fix boolean values, invert (https://github.com/pfsensible/core/issues/92) +- pfsense_openvpn_client - Fix strictuserdn -> strictusercn option (https://github.com/pfsensible/core/pull/93) +- pfsense_openvpn_client/override/server - Allow network alias and non-strict network address for `tunnel_network`/`tunnel_network6` (https://github.com/pfsensible/core/issues/77) +- pfsense_openvpn_server - Fix use of `generate` with `shared_key` and `tls` (https://github.com/pfsensible/core/issues/81) +- pfsense_setup - No default values - leads to unexpected changes (https://github.com/pfsensible/core/issues/91) +- pfsense_user - Fix setting system group membership (https://github.com/pfsensible/core/issues/70) + +New Modules +----------- + +- pfsensible.core.pfsense_default_gateway - Manage pfSense default gateway +- pfsensible.core.pfsense_dns_resolver - Manage pfSense DNS resolver (unbound) settings diff --git a/README.md b/README.md index 4963ec71..7c3a1bcf 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,9 @@ The following modules are currently available: * [pfsense_authserver_radius](https://github.com/pfsensible/core/wiki/pfsense_authserver_radius) for RADIUS authentication servers * [pfsense_ca](https://github.com/pfsensible/core/wiki/pfsense_ca) for Certificate Authorities * [pfsense_cert](https://github.com/pfsensible/core/wiki/pfsense_cert) for Certificates +* [pfsense_default_gateway](https://github.com/pfsensible/core/wiki/pfsense_default_gateway) for setting the default gateways * [pfsense_dhcp_static](https://github.com/pfsensible/core/wiki/pfsense_dhcp_static) for static DHCP entries +* [pfsense_dns_resolver](https://github.com/pfsensible/core/wiki/pfsense_dns_resolver) for DNS resolver (unbound) settings * [pfsense_gateway](https://github.com/pfsensible/core/wiki/pfsense_gateway) for routing gateways * [pfsense_group](https://github.com/pfsensible/core/wiki/pfsense_group) for user groups * [pfsense_interface](https://github.com/pfsensible/core/wiki/pfsense_interface) for interfaces @@ -99,6 +101,8 @@ These modules allow you to manage installed packages: * [pfsense_haproxy_backend](https://github.com/pfsensible/core/wiki/pfsense_haproxy_backend) for HAProxy backends * [pfsense_haproxy_backend_server](https://github.com/pfsensible/core/wiki/pfsense_haproxy_backend_server) for HAProxy backends servers +## [Change Log](https://github.com/pfsensible/core/blob/master/CHANGELOG.rst) + ## Operation Modules in the collection work by editing `/cf/conf/config.xml` using xml.etree.ElementTree, then diff --git a/changelogs/.plugin-cache.yaml b/changelogs/.plugin-cache.yaml new file mode 100644 index 00000000..58001b45 --- /dev/null +++ b/changelogs/.plugin-cache.yaml @@ -0,0 +1,184 @@ +objects: + role: {} +plugins: + become: {} + cache: {} + callback: {} + cliconf: {} + connection: {} + filter: {} + httpapi: {} + inventory: {} + lookup: + pfsense: + description: Generate pfSense aliases, rules and rule_separators + name: pfsense + version_added: 0.1.0 + module: + pfsense_aggregate: + description: Manage multiple pfSense firewall aliases, rules, and rule separators, + plus interfaces and VLANs + name: pfsense_aggregate + namespace: '' + version_added: 0.1.0 + pfsense_alias: + description: Manage pfSense aliases + name: pfsense_alias + namespace: '' + version_added: 0.1.0 + pfsense_authserver_ldap: + description: Manage pfSense LDAP authentication servers + name: pfsense_authserver_ldap + namespace: '' + version_added: 0.1.0 + pfsense_authserver_radius: + description: Manage pfSense RADIUS authentication servers + name: pfsense_authserver_radius + namespace: '' + version_added: 0.5.0 + pfsense_ca: + description: Manage pfSense Certificate Authorities + name: pfsense_ca + namespace: '' + version_added: 0.1.0 + pfsense_cert: + description: Manage pfSense certificates + name: pfsense_cert + namespace: '' + version_added: 0.5.0 + pfsense_default_gateway: + description: Manage pfSense default gateway + name: pfsense_default_gateway + namespace: '' + version_added: 0.6.0 + pfsense_dhcp_static: + description: Manage pfSense DHCP static mapping + name: pfsense_dhcp_static + namespace: '' + version_added: 0.5.0 + pfsense_dns_resolver: + description: Manage pfSense DNS resolver (unbound) settings + name: pfsense_dns_resolver + namespace: '' + version_added: 0.6.0 + pfsense_gateway: + description: Manage pfSense gateways + name: pfsense_gateway + namespace: '' + version_added: 0.1.0 + pfsense_group: + description: Manage pfSense user groups + name: pfsense_group + namespace: '' + version_added: 0.1.0 + pfsense_haproxy_backend: + description: Manage pfSense HAProxy backends + name: pfsense_haproxy_backend + namespace: '' + version_added: 0.1.0 + pfsense_haproxy_backend_server: + description: Manage pfSense haproxy backend servers + name: pfsense_haproxy_backend_server + namespace: '' + version_added: 0.1.0 + pfsense_interface: + description: Manage pfSense interfaces + name: pfsense_interface + namespace: '' + version_added: 0.1.0 + pfsense_interface_group: + description: Manage pfSense interface groups + name: pfsense_interface_group + namespace: '' + version_added: 0.5.0 + pfsense_ipsec: + description: Manage pfSense IPsec tunnels and phase 1 options + name: pfsense_ipsec + namespace: '' + version_added: 0.1.0 + pfsense_ipsec_aggregate: + description: Manage multiple pfSense IPsec tunnels, phases 1, phases 2 and proposals + name: pfsense_ipsec_aggregate + namespace: '' + version_added: 0.1.0 + pfsense_ipsec_p2: + description: Manage pfSense IPsec tunnels phase 2 options + name: pfsense_ipsec_p2 + namespace: '' + version_added: 0.1.0 + pfsense_ipsec_proposal: + description: Manage pfSense IPsec proposals + name: pfsense_ipsec_proposal + namespace: '' + version_added: 0.1.0 + pfsense_log_settings: + description: Manage pfSense syslog settings + name: pfsense_log_settings + namespace: '' + version_added: 0.4.2 + pfsense_nat_outbound: + description: Manage pfSense Outbound NAT (SNAT) rules + name: pfsense_nat_outbound + namespace: '' + version_added: 0.1.0 + pfsense_nat_port_forward: + description: Manage pfSense port forwarding NAT (DNAT) rules + name: pfsense_nat_port_forward + namespace: '' + version_added: 0.1.0 + pfsense_openvpn_client: + description: Manage pfSense OpenVPN configuration + name: pfsense_openvpn_client + namespace: '' + version_added: 0.5.0 + pfsense_openvpn_override: + description: Manage pfSense OpenVPN Client Specific Overrides + name: pfsense_openvpn_override + namespace: '' + version_added: 0.5.0 + pfsense_openvpn_server: + description: Manage pfSense OpenVPN server configuration + name: pfsense_openvpn_server + namespace: '' + version_added: 0.5.0 + pfsense_rewrite_config: + description: Rewrite pfSense config.xml + name: pfsense_rewrite_config + namespace: '' + version_added: 0.5.3 + pfsense_route: + description: Manage pfSense routes + name: pfsense_route + namespace: '' + version_added: 0.1.0 + pfsense_rule: + description: Manage pfSense firewall rules + name: pfsense_rule + namespace: '' + version_added: 0.1.0 + pfsense_rule_separator: + description: Manage pfSense firewall rule separators + name: pfsense_rule_separator + namespace: '' + version_added: 0.1.0 + pfsense_setup: + description: Manage pfSense general setup + name: pfsense_setup + namespace: '' + version_added: 0.1.0 + pfsense_user: + description: Manage pfSense users + name: pfsense_user + namespace: '' + version_added: 0.1.0 + pfsense_vlan: + description: Manage pfSense VLANs + name: pfsense_vlan + namespace: '' + version_added: 0.1.0 + netconf: {} + shell: {} + strategy: {} + test: {} + vars: {} +version: 0.6.0 diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml new file mode 100644 index 00000000..9b17d290 --- /dev/null +++ b/changelogs/changelog.yaml @@ -0,0 +1,49 @@ +ancestor: null +releases: + 0.6.0: + changes: + bugfixes: + - pfsense_aggregate - Fix where a rule with a duplicated name would not be deleted + if required + - pfsense_dhcp_static - Allow removing entry with just name (https://github.com/pfsensible/core/issues/69) + - pfsense_dhcp_static - Allow use of display name for netif. Error in case a + interface group name is specified (https://github.com/pfsensible/core/issues/79) + - pfsense_interface - Properly shut dwon interface and kill dhclient process + when removing interface (https://github.com/pfsensible/core/pull/67) + - pfsense_interface_group - Check that members list is unique + - pfsense_interface_group - Fix creation (https://github.com/pfsensible/core/issues/74) + - pfsense_interface_group - `members` is only required for creation + - pfsense_nat_outbound - Fix boolean values, invert (https://github.com/pfsensible/core/issues/92) + - pfsense_openvpn_client - Fix strictuserdn -> strictusercn option (https://github.com/pfsensible/core/pull/93) + - pfsense_openvpn_client/override/server - Allow network alias and non-strict + network address for `tunnel_network`/`tunnel_network6` (https://github.com/pfsensible/core/issues/77) + - pfsense_openvpn_server - Fix use of `generate` with `shared_key` and `tls` + (https://github.com/pfsensible/core/issues/81) + - pfsense_setup - No default values - leads to unexpected changes (https://github.com/pfsensible/core/issues/91) + - pfsense_user - Fix setting system group membership (https://github.com/pfsensible/core/issues/70) + major_changes: + - pfsense_default_gateway - Add module for setting the default gateways + - pfsense_dns_resolver - Add module for DNS resolver (unbound) settings + minor_changes: + - ipaddress support for pfSense 2.4.4 + - pfsense_cert - Support EC certs (https://github.com/pfsensible/core/pull/98) + - pfsense_interface - Always return `ifname` - even on interface creation + - pfsense_interface - Prevent removal if interface is part of an interface group + - pfsense_nat_outbound - Allow for NET:INTERFACE addresses + - pfsense_nat_port_forward - 2.4.5 compatibility + - pfsense_openvpn_server - Do not allow removal of an instance with an interface + assignment + - pfsense_rule - Add option to ignore an inexistent queue + - pfsense_rule - Add support for floating 'any' interface rule (https://github.com/pfsensible/core/pull/90) + - plugins/lookup/pfsense - Optimization and ignore queue setting + - tests/plays - Add plays for testing with a live pfSense instance + fragments: + - 0.6.0-changes.yaml + modules: + - description: Manage pfSense default gateway + name: pfsense_default_gateway + namespace: '' + - description: Manage pfSense DNS resolver (unbound) settings + name: pfsense_dns_resolver + namespace: '' + release_date: '2024-01-06' diff --git a/changelogs/config.yaml b/changelogs/config.yaml new file mode 100644 index 00000000..9d0f579d --- /dev/null +++ b/changelogs/config.yaml @@ -0,0 +1,32 @@ +changelog_filename_template: ../CHANGELOG.rst +changelog_filename_version_depth: 0 +changes_file: changelog.yaml +changes_format: combined +ignore_other_fragment_extensions: true +keep_fragments: false +mention_ancestor: true +new_plugins_after_name: removed_features +notesdir: fragments +prelude_section_name: release_summary +prelude_section_title: Release Summary +sanitize_changelog: true +sections: +- - major_changes + - Major Changes +- - minor_changes + - Minor Changes +- - breaking_changes + - Breaking Changes / Porting Guide +- - deprecated_features + - Deprecated Features +- - removed_features + - Removed Features (previously deprecated) +- - security_fixes + - Security Fixes +- - bugfixes + - Bugfixes +- - known_issues + - Known Issues +title: pfSensible.Core +trivial_section_name: trivial +use_fqcn: true diff --git a/changelogs/fragments/111-Add-arp_table-static_entry.yml b/changelogs/fragments/111-Add-arp_table-static_entry.yml new file mode 100644 index 00000000..93d1db2d --- /dev/null +++ b/changelogs/fragments/111-Add-arp_table-static_entry.yml @@ -0,0 +1,3 @@ +minor_changes: + - pfsense_dhcp_static - Add arp_table_static_entry argument + (https://github.com/https://github.com/pfsensible/core/issues/109). diff --git a/changelogs/fragments/pfsense_ca-allow-disabling.yml b/changelogs/fragments/pfsense_ca-allow-disabling.yml new file mode 100644 index 00000000..57892baa --- /dev/null +++ b/changelogs/fragments/pfsense_ca-allow-disabling.yml @@ -0,0 +1,2 @@ +minor_changes: + - pfsense_ca - allow for disabling `randomserial` and `trust` parameters. diff --git a/examples/ipsec/filter_plugins/pfsense.py b/examples/ipsec/filter_plugins/pfsense.py index e067f5a6..4106e6de 100644 --- a/examples/ipsec/filter_plugins/pfsense.py +++ b/examples/ipsec/filter_plugins/pfsense.py @@ -7,14 +7,6 @@ __metaclass__ = type from ansible.errors import AnsibleFilterError -from ipaddress import ip_network -import re - -try: - from __main__ import display -except ImportError: - from ansible.utils.display import Display - display = Display() def format_ipsec_aggregate_ipsecs(all_tunnels, pfname): diff --git a/galaxy.yml b/galaxy.yml index 6327db0f..9cbf8151 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -9,7 +9,7 @@ namespace: pfsensible name: core # The version of the collection. Must be compatible with semantic versioning -version: 0.5.3 +version: 0.6.0 # The path to the Markdown (.md) readme file. This path is relative to the root of the collection readme: README.md @@ -63,6 +63,7 @@ build_ignore: - .gitignore - .travis.yml - '*.tar.gz' + - changelogs - examples - misc - setup.cfg diff --git a/plugins/module_utils/__impl/checks.py b/plugins/module_utils/__impl/checks.py index b8525526..f6beebac 100644 --- a/plugins/module_utils/__impl/checks.py +++ b/plugins/module_utils/__impl/checks.py @@ -58,6 +58,25 @@ def check_ip_address(self, address, ipprotocol, objtype, allow_networks=False, f self.module.fail_json(msg='IPv4 and IPv6 addresses can not be used in objects that apply to both IPv4 and IPv6 (except within an alias).') +def validate_openvpn_tunnel_network(self, network, ipproto): + """ check openvpn tunnel network validity - based on pfSense's openvpn_validate_tunnel_network() """ + if network is not None and network != '': + alias_elt = self.find_alias(network, aliastype='network') + if alias_elt is not None: + networks = alias_elt.find('address').text.split() + if len(networks) > 1: + self.module.fail_json("The alias {0} contains more than one network".format(network)) + network = networks[0] + + if not self.is_ipv4_network(network, strict=False) and ipproto == 'ipv4': + self.module.fail_json("{0} is not a valid IPv4 network".format(network)) + if not self.is_ipv6_network(network, strict=False) and ipproto == 'ipv6': + self.module.fail_json("{0} is not a valid IPv6 network".format(network)) + return True + + return True + + def validate_string(self, name, objtype): """ check string validity - similar to pfSense's do_input_validate() """ diff --git a/plugins/module_utils/default_gateway.py b/plugins/module_utils/default_gateway.py new file mode 100644 index 00000000..eeb7e58f --- /dev/null +++ b/plugins/module_utils/default_gateway.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2023, Nicolas Zagulajew +# +# 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 ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + + +DEFAULT_GATEWAY_ARGUMENT_SPEC = dict( + gateway=dict(type='str'), + ipprotocol=dict(default='inet', choices=['inet', 'inet6']), +) + + +class PFSenseDefaultGatewayModule(PFSenseModuleBase): + """ module managing pfsense default gateways """ + + @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 = "pfsense_default_gateway" + self.root_elt = self.pfsense.get_element('gateways') + self.target_elt = self.root_elt + self.obj = dict() + self.interface_elt = None + self.read_only = False + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a dict from module params + gateway required, str + ipprotocol default : inet, choice inet/inet6 + """ + params = self.params + + obj = dict() + + # Modification + if params["gateway"]: + my_defaultgw = self._gw2machine(params['gateway']) + if params['ipprotocol'] == "inet": + obj['defaultgw4'] = my_defaultgw + self.result["defaultgw4"] = params["gateway"] + elif params['ipprotocol'] == "inet6": + obj['defaultgw6'] = my_defaultgw + self.result["defaultgw6"] = params["gateway"] + else: + self.module.fail_json(msg='Please specify a valid ipprotocol (inet/inet6)') + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters + gateway required, str + ipprotocol default : inet, choice inet/inet6 + """ + params = self.params + gateway_list = ["none", "automatic"] + [gw["Name"] for gw in self.pfsense.find_active_gateways()] + + # get list of current default gateways and append gateway_groups to list + for elt in self.root_elt: + if elt.tag in ["gateway_group"]: + gateway_list.append(elt.find("name").text) + elif elt.tag == "defaultgw4": + self.result["defaultgw4"] = self._gw2human(elt.text) + elif elt.tag == "defaultgw6": + self.result["defaultgw6"] = self._gw2human(elt.text) + + if params["gateway"]: + if str(params["gateway"]) not in gateway_list: + self.module.fail_json(msg="Unknown gateway %s : %s" % (params["gateway"], gateway_list)) + + ############################## + # XML processing + # + def _create_target(self): + """ create the XML target_elt """ + if self.params["ipprotocol"] == "inet": + return self.pfsense.new_element('defaultgw4') + elif self.params["ipprotocol"] == "inet6": + return self.pfsense.new_element('defaultgw6') + + ############################## + # Utilities + # + + @staticmethod + def _gw2machine(gateway): + """ + Translates special gateway to machine-readable + "-" means none + "" means automatic + """ + if gateway is not None: + if gateway.lower() == "automatic": + return "" + elif gateway.lower() == "none": + return "-" + return gateway + + @staticmethod + def _gw2human(gateway): + """ + Translates special gateway as human-readable + "-" means none + "" means automatic + """ + if gateway is None: + return "automatic" + elif gateway == "-": + return "none" + else: + return gateway + + @staticmethod + def _get_params_to_remove(): + """ returns the list of params to remove if they are not set """ + return [] + + ############################## + def run(self, params): + """ process input params to add/update/delete """ + self.params = params + self._check_deprecated_params() + self._check_onward_params() + self._validate_params() + + self.obj = self._params_to_obj() + + if params["gateway"]: + self._add() + + def _update(self): + """ make the target pfsense reload """ + return self.pfsense.phpshell(''' +require_once("filter.inc"); +$retval = 0; + +$retval |= system_routing_configure(); +$retval |= system_resolvconf_generate(); +$retval |= filter_configure(); +/* reconfigure our gateway monitor */ +setup_gateways_monitor(); +/* Dynamic DNS on gw groups may have changed */ +send_event("service reload dyndnsall"); + +if ($retval == 0) clear_subsystem_dirty('staticroutes'); +''') + + ############################## + # Logging + # + def _get_obj_name(self): + """ return obj's name """ + return "" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + if self.params["ipprotocol"] == "inet": + values += self.format_updated_cli_field(self.obj, before, 'defaultgw4', add_comma=values) + elif self.params["ipprotocol"] == "inet6": + values += self.format_updated_cli_field(self.obj, before, 'defaultgw6', add_comma=values) + + return values diff --git a/plugins/module_utils/interface.py b/plugins/module_utils/interface.py index d7fa97a2..b2942acd 100644 --- a/plugins/module_utils/interface.py +++ b/plugins/module_utils/interface.py @@ -64,6 +64,7 @@ def __init__(self, module, pfsense=None): self.root_elt = self.pfsense.interfaces self.setup_interface_cmds = "" + self.setup_interface_pre_cmds = "" ############################## # params processing @@ -146,9 +147,6 @@ def _params_to_obj(self): else: self.target_elt = self._get_interface_elt_by_display_name(self.obj['descr']) - if self.target_elt is not None: - self.result['ifname'] = self.target_elt.tag - return obj def _validate_params(self): @@ -213,6 +211,7 @@ def _copy_and_add_target(self): """ create the XML target_elt """ self.pfsense.copy_dict_to_element(self.obj, self.target_elt) self.setup_interface_cmds += "interface_configure('{0}', true);\n".format(self.target_elt.tag) + self.result['ifname'] = self.target_elt.tag def _copy_and_update_target(self): """ update the XML target_elt """ @@ -228,6 +227,7 @@ def _copy_and_update_target(self): else: self.setup_interface_cmds += "interface_bring_down('{0}', true);\n".format(self.target_elt.tag) + self.result['ifname'] = self.target_elt.tag return (before, changed) def _create_target(self): @@ -321,10 +321,19 @@ def _pre_remove_target_elt(self): """ processing before removing elt """ self.obj['if'] = self.target_elt.find('if').text - self._remove_all_separators(self.target_elt.tag) - self._remove_all_rules(self.target_elt.tag) + ifname = self.target_elt.tag + if self.pfsense.ifgroups is not None: + for ifgroup_elt in self.pfsense.ifgroups.findall("ifgroupentry"): + members = ifgroup_elt.find('members').text.split() + if ifname in members: + self.module.fail_json(msg='The interface is part of the group {0}. Please remove it from the group first.'.format( + ifgroup_elt.find('ifname').text)) + + self._remove_all_separators(ifname) + self._remove_all_rules(ifname) - self.setup_interface_cmds += "interface_bring_down('{0}');\n".format(self.target_elt.tag) + self.setup_interface_pre_cmds += "interface_bring_down('{0}');\n".format(ifname) + self.result['ifname'] = ifname def _remove_all_rules(self, interface): """ delete all interface rules """ @@ -462,6 +471,16 @@ def _get_media_mode(self, interface): '}\n' 'echo json_encode($mediaopts_list);') + def get_pre_update_cmds(self): + """ build and return php commands to setup interfaces before changing config """ + cmd = 'require_once("filter.inc");\n' + cmd += 'require_once("interfaces.inc");\n' + + if self.setup_interface_pre_cmds != "": + cmd += self.setup_interface_pre_cmds + + return cmd + def get_update_cmds(self): """ build and return php commands to setup interfaces """ cmd = 'require_once("filter.inc");\n' @@ -482,6 +501,10 @@ def get_update_cmds(self): cmd += "if (is_subsystem_dirty('staticroutes') && (system_routing_configure() == 0)) clear_subsystem_dirty('staticroutes');" return cmd + def _pre_update(self): + """ tasks to run before making config changes """ + return self.pfsense.phpshell(self.get_pre_update_cmds()) + def _update(self): """ make the target pfsense reload interfaces """ return self.pfsense.phpshell(self.get_update_cmds()) diff --git a/plugins/module_utils/interface_group.py b/plugins/module_utils/interface_group.py index 2021488e..76457e24 100644 --- a/plugins/module_utils/interface_group.py +++ b/plugins/module_utils/interface_group.py @@ -13,9 +13,13 @@ state=dict(default='present', choices=['present', 'absent']), name=dict(required=True, type='str'), descr=dict(type='str'), - members=dict(required=True, type='list', elements='str'), + members=dict(type='list', elements='str'), ) +INTERFACE_GROUP_REQUIRED_IF = [ + ['state', 'present', ['members']], +] + INTERFACE_GROUP_PHP_COMMAND = ''' require_once("interfaces.inc"); {0} @@ -77,31 +81,15 @@ def _validate_params(self): self.module.fail_json(msg='Group name cannot have more than 15 characters.') if re.match('[0-9]$', params['name']) is not None: self.module.fail_json(msg='Group name cannot end with a digit.') + # Make sure list of interfaces is a unique set + if params['state'] == 'present': + if len(params['members']) > len(set(params['members'])): + self.module.fail_json(msg='List of members is not unique.') # TODO - check that name isn't in use by any interfaces ############################## # XML processing # - def _copy_and_add_target(self): - """ create the XML target_elt """ - self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - - def _copy_and_update_target(self): - """ update the XML target_elt """ - before = self.pfsense.element_to_dict(self.target_elt) - changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - if self._remove_deleted_params(): - changed = True - - self.diff['before'] = before - if changed: - self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) - self.result['changed'] = True - else: - self.diff['after'] = self.obj - - return (before, changed) - def _create_target(self): """ create the XML target_elt """ self.diff['before'] = '' @@ -110,8 +98,13 @@ def _create_target(self): def _find_target(self): """ find the XML target_elt """ - target_elt = self.root_elt.findall("ifgroupentry[ifname='{0}']".format(self.obj['ifname']))[0] - return target_elt + result = self.root_elt.findall("ifgroupentry[ifname='{0}']".format(self.obj['ifname'])) + if len(result) == 1: + return result[0] + elif len(result) > 1: + self.module.fail_json(msg='Found multiple interface groups for name {0}.'.format(self.obj['ifname'])) + else: + return None def _pre_remove_target_elt(self): """ processing before removing elt """ @@ -195,12 +188,6 @@ def _log_fields(self, before=None): values += self.format_cli_field(self.obj, 'descr') values += self.format_cli_field(self.obj, 'members') else: - values += self.format_updated_cli_field(self.obj, before, 'descr', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'descr', add_comma=(values), log_none=False) values += self.format_updated_cli_field(self.obj, before, 'members', add_comma=(values)) return values - - def _log_update(self, before): - """ generate pseudo-CLI command to update an interface """ - log = "update {0}".format(self._get_module_name(True)) - values = self._log_fields(before) - self.result['commands'].append(log + ' set ' + values) diff --git a/plugins/module_utils/module_base.py b/plugins/module_utils/module_base.py index e127d36e..996ec9ae 100644 --- a/plugins/module_utils/module_base.py +++ b/plugins/module_utils/module_base.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2019, Frederic Bor +# Copyright: (c) 2024, 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) @@ -68,8 +69,10 @@ def _get_ansible_param_bool(self, obj, name, fname=None, force=False, value='yes if self.params.get(name) is not None: if self.params.get(name): obj[fname] = value - elif force: + elif value_false is not None: obj[fname] = value_false + elif force: + obj[fname] = None elif force: obj[fname] = value_false @@ -185,10 +188,15 @@ def commit_changes(self): self.result['stdout'] = '' self.result['stderr'] = '' if self.result['changed'] and not self.module.check_mode: + if self.apply: + (dummy, self.result['stdout'], self.result['stderr']) = self._pre_update() + self.pfsense.write_config(descr=self.change_descr) if self.apply: - (dummy, self.result['stdout'], self.result['stderr']) = self._update() + (dummy, stdout, stderr) = self._update() + self.result['stdout'] += stdout + self.result['stderr'] += stderr self.module.exit_json(**self.result) @@ -209,6 +217,11 @@ def _remove(self): self._post_remove_target_elt() self.change_descr = 'ansible {0} removed {1}'.format(self._get_module_name(), self._get_obj_name()) + @staticmethod + def _pre_update(): + """ tasks to run before making config changes """ + return ('', '', '') + @staticmethod def _update(): """ make the target pfsense reload """ diff --git a/plugins/module_utils/nat_outbound.py b/plugins/module_utils/nat_outbound.py index bb843e6e..cd40353b 100644 --- a/plugins/module_utils/nat_outbound.py +++ b/plugins/module_utils/nat_outbound.py @@ -76,9 +76,10 @@ def _params_to_obj(self): obj = dict() obj['descr'] = self.params['descr'] - if self.params['state'] == 'present': + params = self.params + if params['state'] == 'present': obj['sourceport'] = '' - obj['interface'] = self.pfsense.parse_interface(self.params['interface']) + obj['interface'] = self.pfsense.parse_interface(params['interface']) self._get_ansible_param(obj, 'ipprotocol') if obj['ipprotocol'] == 'inet46': del obj['ipprotocol'] @@ -88,20 +89,21 @@ def _params_to_obj(self): self._get_ansible_param(obj, 'poolopts') self._get_ansible_param(obj, 'source_hash_key') self._get_ansible_param(obj, 'natport') - self._get_ansible_param_bool(obj, 'disabled') - self._get_ansible_param_bool(obj, 'nonat') - self._get_ansible_param_bool(obj, 'invert') - self._get_ansible_param_bool(obj, 'staticnatport') - self._get_ansible_param_bool(obj, 'nosync') + self._get_ansible_param_bool(obj, 'disabled', value='') + self._get_ansible_param_bool(obj, 'nonat', value='') + self._get_ansible_param_bool(obj, 'staticnatport', value='') + self._get_ansible_param_bool(obj, 'nosync', value='') - if 'after' in self.params and self.params['after'] is not None: - self.after = self.params['after'] + if 'after' in params and params['after'] is not None: + self.after = params['after'] - if 'before' in self.params and self.params['before'] is not None: - self.before = self.params['before'] + if 'before' in params and params['before'] is not None: + self.before = params['before'] self._parse_address(obj, 'source', 'sourceport', True, 'network') - self._parse_address(obj, 'destination', 'dstport', False, 'address') + self._parse_address(obj, 'destination', 'dstport', False, 'network') + if params['invert']: + obj['destination']['not'] = None self._parse_translated_address(obj) if obj['source_hash_key'] != '' and not obj['source_hash_key'].startswith('0x'): @@ -119,32 +121,49 @@ def _parse_address(self, obj, field, field_port, allow_self, target): param = self.params[field] addr = param.split(':') - if len(addr) > 2: + if len(addr) > 3: self.module.fail_json(msg='Cannot parse address %s' % (param)) address = addr[0] ret = dict() - ports = addr[1] if len(addr) > 1 else None - if address == 'any': - if field == 'source': - ret[target] = 'any' - else: - ret['any'] = '' - # rule with this firewall - elif allow_self and address == '(self)': - ret[target] = '(self)' - elif self.pfsense.is_ipv4_address(address): - ret[target] = address + '/32' - elif self.pfsense.is_ipv4_network(address, False): - (addr, bits) = self.pfsense.parse_ip_network(address, False, False) - ret[target] = addr + '/' + str(bits) - elif self.pfsense.find_alias(address, 'host') is not None or self.pfsense.find_alias(address, 'network') is not None: - ret[target] = address + + if address == 'NET': + interface = addr[1] if len(addr) > 1 else None + ports = addr[2] if len(addr) > 2 else None + if interface is None or interface == '': + self.module.fail_json(msg='Cannot parse address %s' % (param)) + + ret['network'] = self.pfsense.parse_interface(interface) else: - self.module.fail_json(msg='Cannot parse address %s, not IP or alias' % (address)) + ports = addr[1] if len(addr) > 1 else None + if address == 'any': + if field == 'source': + ret[target] = 'any' + else: + ret['any'] = '' + # rule with this firewall + elif allow_self and address == '(self)': + ret[target] = '(self)' + elif self.params['ipprotocol'] != 'inet6' and self.pfsense.is_ipv4_address(address): + ret[target] = address + '/32' + self.module.warn('Specifying an address without a CIDR prefix is depracated. Please add /32 if you want a single host address') + elif self.params['ipprotocol'] != 'inet4' and self.pfsense.is_ipv6_address(address): + ret[target] = address + '/128' + self.module.warn('Specifying an address without a CIDR prefix is depracated. Please add /128 if you want a single host address') + elif self.params['ipprotocol'] != 'inet6' and self.pfsense.is_ipv4_network(address, False): + (addr, bits) = self.pfsense.parse_ip_network(address, False, False) + ret[target] = addr + '/' + str(bits) + elif self.params['ipprotocol'] != 'inet4' and self.pfsense.is_ipv6_network(address, False): + (addr, bits) = self.pfsense.parse_ip_network(address, False, False) + ret[target] = addr + '/' + str(bits) + elif self.pfsense.find_alias(address, 'host') is not None or self.pfsense.find_alias(address, 'network') is not None: + ret[target] = address + else: + self.module.fail_json(msg='Cannot parse address %s, not %s network or alias' % (address, self.params['ipprotocol'])) - self._parse_ports(obj, ports, field_port, param) + if ports is not None: + self._parse_ports(obj, ports, field_port, param) obj[field] = ret @@ -426,13 +445,14 @@ def _log_fields(self, before=None): return values - @staticmethod - def _obj_address_to_log_field(rule, addr, target, port): + def _obj_address_to_log_field(self, rule, addr, target, port): """ return formated address from dict """ field = '' if addr in rule: if target in rule[addr]: - field = rule[addr][target] + if self.pfsense.interfaces.find(rule[addr][target]): + field = 'NET:' + field += rule[addr][target] elif addr == 'destination' and 'any' in rule[addr]: field = 'any' @@ -446,7 +466,7 @@ def _obj_to_log_fields(self, rule): """ return formated source and destination from dict """ res = {} res['source'] = self._obj_address_to_log_field(rule, 'source', 'network', 'sourceport') - res['destination'] = self._obj_address_to_log_field(rule, 'destination', 'address', 'dstport') + res['destination'] = self._obj_address_to_log_field(rule, 'destination', 'network', 'dstport') res['interface'] = self.pfsense.get_interface_display_name(rule['interface']) if rule['target'] == 'other-subnet': diff --git a/plugins/module_utils/openvpn_client.py b/plugins/module_utils/openvpn_client.py index 0ea1fd12..6bfba7d0 100644 --- a/plugins/module_utils/openvpn_client.py +++ b/plugins/module_utils/openvpn_client.py @@ -94,7 +94,7 @@ class PFSenseOpenVPNClientModule(PFSenseModuleBase): def __init__(self, module, pfsense=None): super(PFSenseOpenVPNClientModule, self).__init__(module, pfsense) self.name = "pfsense_openvpn" - self.root_elt = self.pfsense.get_element('openvpn') + self.root_elt = self.pfsense.get_element('openvpn', create_node=True) self.obj = dict() ############################## @@ -172,6 +172,13 @@ def _validate_params(self): # check name self.pfsense.validate_string(params['name'], 'openvpn') + if params['state'] == 'absent': + return True + + # check tunnel_networks - can be network alias or non-strict IP CIDR network + self.pfsense.validate_openvpn_tunnel_network(params.get('tunnel_network'), 'ipv4') + self.pfsense.validate_openvpn_tunnel_network(params.get('tunnel_network6'), 'ipv6') + # Check auth clients if len(params['authmode']) > 0: system = self.pfsense.get_element('system') @@ -180,13 +187,19 @@ def _validate_params(self): self.module.fail_json(msg='Cannot find authentication client {0}.'.format(authsrv)) # validate key - if params['shared_key'] is not None: - key = params['shared_key'] - lines = key.splitlines() - if lines[0] == '-----BEGIN OpenVPN Static key V1-----' and lines[-1] == '-----END OpenVPN Static key V1-----': - params['shared_key'] = base64.b64encode(key.encode()).decode() - elif not re.match('LS0tLS1CRUdJTiBPcGVuVlBOIFN0YXRpYyBrZXkgVjEtLS0tLQ', key): - self.module.fail_json(msg='Could not recognize key format: %s' % (key)) + for param in ['shared_key', 'tls']: + if params[param] is not None: + key = params[param] + if key == 'generate': + # generate during params_to_obj + pass + elif re.search('^-----BEGIN OpenVPN Static key V1-----.*-----END OpenVPN Static key V1-----$', key, flags=re.MULTILINE | re.DOTALL): + params[param] = base64.b64encode(key.encode()).decode() + else: + key_decoded = base64.b64decode(key.encode()).decode() + if not re.search('^-----BEGIN OpenVPN Static key V1-----.*-----END OpenVPN Static key V1-----$', + key_decoded, flags=re.MULTILINE | re.DOTALL): + self.module.fail_json(msg='Could not recognize {0} key format: {1}'.format(param, key_decoded)) def _nextvpnid(self): """ find next available vpnid """ @@ -216,16 +229,9 @@ def _find_last_openvpn_idx(self): def _copy_and_update_target(self): """ update the XML target_elt """ - before = self.pfsense.element_to_dict(self.target_elt) - changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - if self._remove_deleted_params(): - changed = True - - self.diff['before'] = before - if changed: - self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) - self.result['changed'] = True - else: + (before, changed) = super(PFSenseOpenVPNClientModule, self)._copy_and_update_target() + + if not changed: self.diff['after'] = self.obj return (before, changed) @@ -243,6 +249,17 @@ def _create_target(self): def _find_target(self): """ find the XML target_elt """ (target_elt, self.idx) = self._find_openvpn_client(self.obj['description']) + for param in ['shared_key', 'tls']: + current_elt = self.pfsense.get_element(param, target_elt) + if self.params[param] == 'generate': + if current_elt is None: + (dummy, key, stderr) = self.module.run_command('/usr/local/sbin/openvpn --genkey secret /dev/stdout') + if stderr != "": + self.module.fail_json(msg='generate for "{0}" secret key: {1}'.format(param, stderr)) + self.obj[param] = base64.b64encode(key.encode()).decode() + self.result[param] = self.obj[param] + else: + self.obj[param] = current_elt.text return target_elt def _remove_target_elt(self): diff --git a/plugins/module_utils/openvpn_override.py b/plugins/module_utils/openvpn_override.py index e4f0eaa0..2cd96bd8 100644 --- a/plugins/module_utils/openvpn_override.py +++ b/plugins/module_utils/openvpn_override.py @@ -54,6 +54,8 @@ class PFSenseOpenVPNOverrideModule(PFSenseModuleBase): """ module managing pfSense OpenVPN Client Specific Overrides """ + from ansible_collections.pfsensible.core.plugins.module_utils.__impl.checks import validate_openvpn_tunnel_network + @staticmethod def get_argument_spec(): """ return argument spec """ @@ -121,10 +123,13 @@ def _validate_params(self): # check name self.pfsense.validate_string(params['name'], 'openvpn_override') - if params.get('tunnel_network') and not self.pfsense.is_ipv4_network(params['tunnel_network']): - self.module.fail_json(msg='A valid IPv4 network must be specified for tunnel_network.') - if params.get('tunnel_network6') and not self.pfsense.is_ipv6_network(params['tunnel_networkv6']): - self.module.fail_json(msg='A valid IPv6 network must be specified for tunnel_network6.') + if params['state'] == 'absent': + return True + + # check tunnel_networks - can be network alias or non-strict IP CIDR network + self.pfsense.validate_openvpn_tunnel_network(params.get('tunnel_network'), 'ipv4') + self.pfsense.validate_openvpn_tunnel_network(params.get('tunnel_network6'), 'ipv6') + if params.get('local_network') and not self.pfsense.is_ipv4_network(params['local_network']): self.module.fail_json(msg='A valid IPv4 network must be specified for local_network.') if params.get('local_network6') and not self.pfsense.is_ipv6_network(params['local_networkv6']): diff --git a/plugins/module_utils/openvpn_server.py b/plugins/module_utils/openvpn_server.py index a5c06d35..403369e2 100644 --- a/plugins/module_utils/openvpn_server.py +++ b/plugins/module_utils/openvpn_server.py @@ -106,7 +106,7 @@ def get_argument_spec(): def __init__(self, module, pfsense=None): super(PFSenseOpenVPNServerModule, self).__init__(module, pfsense) self.name = "pfsense_openvpn_server" - self.root_elt = self.pfsense.get_element('openvpn') + self.root_elt = self.pfsense.get_element('openvpn', create_node=True) self.obj = dict() ############################## @@ -202,6 +202,13 @@ def _validate_params(self): # check name self.pfsense.validate_string(params['name'], 'openvpn') + if params['state'] == 'absent': + return True + + # check tunnel_networks - can be network alias or non-strict IP CIDR network + self.pfsense.validate_openvpn_tunnel_network(params.get('tunnel_network'), 'ipv4') + self.pfsense.validate_openvpn_tunnel_network(params.get('tunnel_network6'), 'ipv6') + # Check auth servers if len(params['authmode']) > 0: system = self.pfsense.get_element('system') @@ -213,10 +220,13 @@ def _validate_params(self): for param in ['shared_key', 'tls']: if params[param] is not None: key = params[param] - if re.search('^-----BEGIN OpenVPN Static key V1-----.*-----END OpenVPN Static key V1-----$', key, flags=re.MULTILINE | re.DOTALL): + if key == 'generate': + # generate during _find_target (after _params_to_obj) - for just generate if not exists + pass + elif re.search('^-----BEGIN OpenVPN Static key V1-----.*-----END OpenVPN Static key V1-----$', key, flags=re.MULTILINE | re.DOTALL): params[param] = base64.b64encode(key.encode()).decode() else: - key_decoded = base64.b64decode(params[param].encode()).decode() + key_decoded = base64.b64decode(key.encode()).decode() if not re.search('^-----BEGIN OpenVPN Static key V1-----.*-----END OpenVPN Static key V1-----$', key_decoded, flags=re.MULTILINE | re.DOTALL): self.module.fail_json(msg='Could not recognize {0} key format: {1}'.format(param, key_decoded)) @@ -283,21 +293,15 @@ def _get_params_to_remove(self): def _copy_and_update_target(self): """ update the XML target_elt """ - before = self.pfsense.element_to_dict(self.target_elt) + (before, changed) = super(PFSenseOpenVPNServerModule, self)._copy_and_update_target() + # Check if local port is used self._openvpn_port_used(self.params['protocol'], self.params['interface'], self.params['local_port'], before['vpnid']) - changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - if self._remove_deleted_params(): - changed = True - - self.diff['before'] = before - if changed: - self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) - self.result['changed'] = True - else: + + if not changed: self.diff['after'] = self.obj - self.result['vpnid'] = int(self.diff['before']['vpnid']) + self.result['vpnid'] = int(before['vpnid']) return (before, changed) def _create_target(self): @@ -316,6 +320,17 @@ def _create_target(self): def _find_target(self): """ find the XML target_elt """ (target_elt, self.idx) = self._find_openvpn_server(self.obj['description']) + for param in ['shared_key', 'tls']: + current_elt = self.pfsense.get_element(param, target_elt) + if self.params[param] == 'generate': + if current_elt is None: + (dummy, key, stderr) = self.module.run_command('/usr/local/sbin/openvpn --genkey secret /dev/stdout') + if stderr != "": + self.module.fail_json(msg='generate for "{0}" secret key: {1}'.format(param, stderr)) + self.obj[param] = base64.b64encode(key.encode()).decode() + self.result[param] = self.obj[param] + else: + self.obj[param] = current_elt.text return target_elt ############################## @@ -324,6 +339,11 @@ def _find_target(self): def _pre_remove_target_elt(self): """ processing before removing elt """ self.diff['before'] = self.pfsense.element_to_dict(self.target_elt) + + if len(self.pfsense.interfaces.findall("*[if='ovpns{0}']".format(self.diff['before']['vpnid']))) > 0: + self.module.fail_json(msg='Cannot delete the OpenVPN instance while the interface ovpns{0} is assigned. Remove the interface assignment first.' + .format(self.diff['before']['vpnid'])) + self.result['vpnid'] = int(self.diff['before']['vpnid']) self.command_output = self.pfsense.phpshell(OPENVPN_SERVER_PHP_COMMAND_DEL.format(idx=self.idx)) diff --git a/plugins/module_utils/pfsense.py b/plugins/module_utils/pfsense.py index 6309280d..7a970ede 100644 --- a/plugins/module_utils/pfsense.py +++ b/plugins/module_utils/pfsense.py @@ -20,6 +20,7 @@ from tempfile import mkstemp +# Return an element in node, but return an empty element instead of None if not found def xml_find(node, elt): res = node.find(elt) if res is None: @@ -55,7 +56,12 @@ class PFSenseModule(object): parse_ip_network, parse_port, ) - from ansible_collections.pfsensible.core.plugins.module_utils.__impl.checks import check_name, check_ip_address, validate_string + from ansible_collections.pfsensible.core.plugins.module_utils.__impl.checks import ( + check_name, + check_ip_address, + validate_string, + validate_openvpn_tunnel_network, + ) def __init__(self, module, config='/cf/conf/config.xml'): self.module = module @@ -153,7 +159,7 @@ def find_elt_xpath(self, search_xpath, root_elt=None, multiple_ok=False): if multiple_ok: return result else: - self.module.fail_json(msg='Found multiple groups for name {0}.'.format(self.obj['name'])) + self.module.fail_json(msg='Found multiple elements for name {0}.'.format(self.obj['name'])) return None @staticmethod @@ -254,7 +260,10 @@ def copy_dict_to_element(self, src, top_elt, sub=0): elif isinstance(value, list): for item in value: new_elt = self.new_element(key) - new_elt.text = item + if isinstance(item, dict): + self.copy_dict_to_element(item, new_elt, sub=sub + 1) + else: + new_elt.text = item top_elt.append(new_elt) else: # Create a new element @@ -339,17 +348,29 @@ def element_to_dict(src_elt): res[elt.tag] = value return res + def get_refid(self, node, name): + """ get refid of name in specific nodes """ + elt = self.find_elt(node, name) + if elt is not None: + return xml_find(elt, 'refid').text + else: + return None + def get_caref(self, name): """ get CA refid for name """ # global is a special case if name == 'global': return 'global' - # Otherwise search for added CAs - cas = self.get_elements('ca') - for elt in cas: - if xml_find(elt, 'descr').text == name: - return xml_find(elt, 'refid').text - return None + # Otherwise search the ca elements + return self.get_refid('ca', name) + + def get_certref(self, name): + """ get Cert refid for name """ + return self.get_refid('cert', name) + + def get_crlref(self, name): + """ get CRL refid for name """ + return self.get_refid('crl', name) @staticmethod def get_username(): @@ -566,6 +587,32 @@ def find_gateway_group_elt(self, name, protocol='inet'): return None + def find_active_gateways(self): + """ returns list of active gateways """ + (retcode, raw_output, error) = self.phpshell("playback gatewaystatus") + + write = False + output = [] + lines = raw_output.split("\n") + for line in lines: + if write and line != "" and "shell:" not in line: + output.append(line) + if "started" in line: + write = True + + head = output[0].split() + data = [] + + for line in output[1:]: + c = 0 + dline = {} + for item in line.split(): + dline[head[c]] = item + c += 1 + if dline is not {}: + data.append(dline) + return data + def find_ca_elt(self, ca, search_field='descr'): """ return certificate authority elt if found """ return self.find_elt('ca', ca, search_field) diff --git a/plugins/module_utils/rule.py b/plugins/module_utils/rule.py index 54d5a6d0..25ace9ea 100644 --- a/plugins/module_utils/rule.py +++ b/plugins/module_utils/rule.py @@ -154,7 +154,10 @@ def _parse_floating_interfaces(self, interfaces): """ validate param interface field when floating is true """ res = [] for interface in interfaces.split(','): - res.append(self.pfsense.parse_interface(interface)) + if interface == 'any': + res.append(interface) + else: + res.append(self.pfsense.parse_interface(interface)) self._floating_interfaces = interfaces return ','.join(res) diff --git a/plugins/modules/pfsense_aggregate.py b/plugins/modules/pfsense_aggregate.py index d45e37b7..a7fa77b6 100644 --- a/plugins/modules/pfsense_aggregate.py +++ b/plugins/modules/pfsense_aggregate.py @@ -536,11 +536,13 @@ ignored_aliases: description: aliases that will be ignored (won't be auto deleted) required: False + default: [] type: list elements: str ignored_rules: description: rules that will be ignored (won't be auto deleted) required: False + default: [] type: list elements: str """ diff --git a/plugins/modules/pfsense_authserver_ldap.py b/plugins/modules/pfsense_authserver_ldap.py index 91d31f04..067abcf0 100644 --- a/plugins/modules/pfsense_authserver_ldap.py +++ b/plugins/modules/pfsense_authserver_ldap.py @@ -298,24 +298,9 @@ def _create_target(self): """ create the XML target_elt """ elt = self.pfsense.new_element('authserver') elt.append(self.pfsense.new_element('ldap_allow_unauthenticated', text=None)) + elt.append(self.pfsense.new_element('refid', text=self.pfsense.uniqid())) return elt - def _copy_and_add_target(self): - """ populate the XML target_elt """ - obj = self.obj - obj['refid'] = self.pfsense.uniqid() - self.pfsense.copy_dict_to_element(obj, self.target_elt) - self.diff['after'] = obj - self.root_elt.insert(self._find_last_index(), self.target_elt) - - def _copy_and_update_target(self): - """ update the XML target_elt """ - before = self.pfsense.element_to_dict(self.target_elt) - self.diff['before'] = before - changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) - return (before, changed) - ############################## # Logging # diff --git a/plugins/modules/pfsense_authserver_radius.py b/plugins/modules/pfsense_authserver_radius.py index 6cdeb367..6a3957ea 100644 --- a/plugins/modules/pfsense_authserver_radius.py +++ b/plugins/modules/pfsense_authserver_radius.py @@ -163,28 +163,19 @@ def _find_this_index(self): def _create_target(self): """ create the XML target_elt """ - return self.pfsense.new_element('authserver') + elt = self.pfsense.new_element('authserver') + elt.append(self.pfsense.new_element('refid', text=self.pfsense.uniqid())) + return elt def _copy_and_add_target(self): """ populate the XML target_elt """ - obj = self.obj - - obj['refid'] = self.pfsense.uniqid() - self.pfsense.copy_dict_to_element(obj, self.target_elt) - self.diff['after'] = obj + self.pfsense.copy_dict_to_element(self.obj, self.target_elt) + self.diff['after'] = self.obj if len(self.authservers) > 0: self.root_elt.insert(list(self.root_elt).index(self.authservers[len(self.authservers) - 1]), self.target_elt) else: self.root_elt.append(self.target_elt) - def _copy_and_update_target(self): - """ update the XML target_elt """ - before = self.pfsense.element_to_dict(self.target_elt) - self.diff['before'] = before - changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) - return (before, changed) - ############################## # Logging # diff --git a/plugins/modules/pfsense_ca.py b/plugins/modules/pfsense_ca.py index 84babdd1..9cdd1611 100644 --- a/plugins/modules/pfsense_ca.py +++ b/plugins/modules/pfsense_ca.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright: (c) 2018-2021, Orion Poplawski +# Copyright: (c) 2018-2024, 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 @@ -32,11 +32,11 @@ choices: [ "present", "absent" ] type: str trust: - description: Add this Certificate Authority to the Operating System Trust Store. + description: Add this Certificate Authority to the Operating System Trust Store. Defaults to false. type: bool version_added: 0.5.0 randomserial: - description: Use random serial numbers when signing certifices. + description: Use random serial numbers when signing certifices. Defaults to false. type: bool version_added: 0.5.0 certificate: @@ -234,21 +234,23 @@ def _find_crl_by_refid(self, crlrefid): def _create_target(self): """ create the XML target_elt """ elt = self.pfsense.new_element('ca') - obj = dict(trust='disabled', randomserial='disabled', serial='0') - self.pfsense.copy_dict_to_element(obj, elt) + # We need this later in _copy_and_add_target() + self.obj['refid'] = self.pfsense.uniqid() + elt.append(self.pfsense.new_element('refid', text=self.obj['refid'])) + # These are default but not enforced values + elt.append(self.pfsense.new_element('randomserial', text='disabled')) + elt.append(self.pfsense.new_element('serial', text='0')) + elt.append(self.pfsense.new_element('trust', text='disabled')) return elt def _copy_and_add_target(self): """ populate the XML target_elt """ - obj = self.obj - - obj['refid'] = self.pfsense.uniqid() - self.pfsense.copy_dict_to_element(obj, self.target_elt) + self.pfsense.copy_dict_to_element(self.obj, self.target_elt) self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) self.root_elt.insert(self._find_last_ca_index(), self.target_elt) if self.crl is not None: crl_elt = self.pfsense.new_element('crl') - self.crl['caref'] = obj['refid'] + self.crl['caref'] = self.obj['refid'] if 'refid' not in self.crl: self.crl['refid'] = self.pfsense.uniqid() self.pfsense.copy_dict_to_element(self.crl, crl_elt) @@ -258,12 +260,7 @@ def _copy_and_add_target(self): def _copy_and_update_target(self): """ update the XML target_elt """ - obj = self.obj - before = self.pfsense.element_to_dict(self.target_elt) - self.diff['before'] = before - - changed = self.pfsense.copy_dict_to_element(obj, self.target_elt) - self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) + (before, changed) = super(PFSenseCAModule, self)._copy_and_update_target() if self.crl is not None: crl_elt = None diff --git a/plugins/modules/pfsense_default_gateway.py b/plugins/modules/pfsense_default_gateway.py new file mode 100644 index 00000000..370733ec --- /dev/null +++ b/plugins/modules/pfsense_default_gateway.py @@ -0,0 +1,88 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Orion Poplawski +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2023, Nicolas Zagulajew + +# 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.6.0 +author: "Nicolas Zagulajew (@freeeflyer)" +short_description: Manage pfSense default gateway +description: Check and update pfSense default gateway +notes: +options: + gateway: + description: Default gateway name + required: false + type: str + ipprotocol: + description: Choose the Internet Protocol Version for this gateway. + required: false + choices: [ "inet", "inet6" ] + default: inet + type: str +""" + +EXAMPLES = """ +- name: Sets default gateway to automatic + pfsense_default_gateway: + gateway: automatic + ipprotocol: inet + +- name: Remove gateway (ie setting it to None) + pfsense_default_gateway: + gateway: none + ipprotocol: inet + +- name: return gateways + pfsense_default_gateway: + +""" + +RETURN = """ +defaultgw4: + description: default gateway for ipv4 + returned: always + type: str + sample: INTERNET_GW4 +defaultgw6: + description: default gateway for ipv6 + returned: always + type: str + sample: INTERNET_GW4 +commands: + description: the set of commands that would be pushed to the remote device (if pfSense had a CLI). If state=read, also returns defaultgw4 and defaultgw6. + returned: always + type: list + sample: [update default_gateway name='my_gw', protocol='inet6' ] +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.default_gateway import PFSenseDefaultGatewayModule, \ + DEFAULT_GATEWAY_ARGUMENT_SPEC + + +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() diff --git a/plugins/modules/pfsense_dhcp_static.py b/plugins/modules/pfsense_dhcp_static.py index a83b5377..3b0af8cb 100644 --- a/plugins/modules/pfsense_dhcp_static.py +++ b/plugins/modules/pfsense_dhcp_static.py @@ -122,6 +122,11 @@ numberoptions: description: The number options type: str + arp_table_static_entry: + description: Create an ARP Table Static Entry for this MAC & IP Address pair + type: bool + required: false + default: false state: description: State in which to leave the configuration default: present @@ -189,9 +194,14 @@ filename64arm=dict(type='str'), uefihttpboot=dict(type='str'), numberoptions=dict(type='str'), + arp_table_static_entry=dict(default=False, type='bool'), state=dict(type='str', default='present', choices=['present', 'absent']), ) +DHCP_STATIC_REQUIRED_IF = [ + ['arp_table_static_entry', True, ['ipaddr']], +] + DHCP_STATIC_REQUIRED_ONE_OF = [ ('name', 'macaddr'), ] @@ -268,6 +278,7 @@ def _params_to_obj(self): self._get_ansible_param(obj, option) # Defaulted options self._get_ansible_param(obj, 'ddnsdomainkeyalgorithm', force_value='hmac-md5', force=True) + self._get_ansible_param_bool(obj, "arp_table_static_entry", value="") return obj @@ -329,24 +340,14 @@ def _create_target(self): def _copy_and_add_target(self): """ populate the XML target_elt """ - obj = self.obj - - self.diff['after'] = obj - self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - self.root_elt.append(self.target_elt) + super(PFSenseDHCPStaticModule, self)._copy_and_add_target() # Reset static map list self.staticmaps = self.root_elt.findall('staticmap') - def _copy_and_update_target(self): - """ update the XML target_elt """ - - before = self.pfsense.element_to_dict(self.target_elt) - self.diff['before'] = before - - changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) - - return (before, changed) + @staticmethod + def _get_params_to_remove(): + """ returns the list of params to remove if they are not set """ + return ['arp_table_static_entry'] ############################## # Logging @@ -361,9 +362,11 @@ def _log_fields(self, before=None): if before is None: values += self.format_cli_field(self.params, 'macaddr') values += self.format_cli_field(self.params, 'ipaddr') + values += self.format_cli_field(self.params, 'arp_table_static_entry', fvalue=self.fvalue_bool, default=False) else: values += self.format_updated_cli_field(self.obj, before, 'macaddr', add_comma=(values)) values += self.format_updated_cli_field(self.obj, before, 'ipaddr', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'arp_table_static_entry', fvalue=self.fvalue_bool, add_comma=(values)) return values ############################## @@ -392,6 +395,7 @@ def _pre_remove_target_elt(self): def main(): module = AnsibleModule( argument_spec=DHCP_STATIC_ARGUMENT_SPEC, + required_if=DHCP_STATIC_REQUIRED_IF, required_one_of=DHCP_STATIC_REQUIRED_ONE_OF, supports_check_mode=True) diff --git a/plugins/modules/pfsense_dns_resolver.py b/plugins/modules/pfsense_dns_resolver.py new file mode 100644 index 00000000..ba218925 --- /dev/null +++ b/plugins/modules/pfsense_dns_resolver.py @@ -0,0 +1,577 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Chris Liu +# 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.6.0 +author: Chris liu (@chris-cyliu), Daniel Huss (@danhuss) +short_description: Manage pfSense DNS resolver (unbound) settings +description: + - Manage pfSense DNS resolver (unbound) settings +notes: +options: + state: + description: Enable/Disable DNS Resolver + default: present + choices: [ "present", "absent" ] + type: str + port: + description: Listen Port + required: false + default: null + type: int + enablessl: + description: Enable SSL/TLS Service + required: false + default: false + type: bool + sslcert: + description: Description of the server certificate to use for SSL/TLS service. + required: false + default: "" + type: str + tlsport: + description: SSL/TLS Listen Port + required: false + default: null + type: int + active_interface: + description: Interface IPs used by the DNS Resolver for responding to queries from clients. + required: false + default: [ "all" ] + type: list + elements: str + outgoing_interface: + description: Utilize different network interface(s) that the DNS Resolver will use to send queries to authoritative servers and receive their replies. + required: false + default: [ "all" ] + type: list + elements: str + system_domain_local_zone_type: + description: The local-zone type used for the pfSense system domain. + required: false + default: "transparent" + type: str + choices: [ "deny", "refuse", "static", "transparent", "typetransparent", "redirect", "inform", "inform_deny", "nodefault" ] + dnssec: + description: Enable DNSSEC Support + required: false + default: false + type: bool + forwarding: + description: DNS Query Forwarding. + required: false + default: false + type: bool + forward_tls_upstream: + description: Use SSL/TLS for DNS Query Forwarding. + required: false + default: false + type: bool + regdhcp: + description: Register DHCP leases in the DNS Resolver + required: false + default: false + type: bool + regdhcpstatic: + description: Register DHCP static mappings in the DNS Resolver + required: false + default: false + type: bool + regovpnclients: + description: Register OpenVPN clients in the DNS Resolver + required: false + default: false + type: bool + custom_options: + description: additional configuration parameters + required: false + default: "" + type: str + hosts: + description: Individual hosts for which the resolver's standard DNS lookup should be overridden. + required: false + default: [] + type: list + elements: dict + suboptions: + host: + description: Name of the host, without the domain part. + required: true + type: str + domain: + description: Parent domain of the host. + required: true + type: str + ip: + description: IPv4 or IPv6 comma-separated addresses to be returned for the host + required: true + type: str + descr: + description: A description may be entered here for administrative reference. + required: false + default: "" + type: str + aliases: + description: Additional names for this host. + required: false + default: [] + type: list + elements: dict + suboptions: + host: + description: Name of the host, without the domain part. + required: true + type: str + domain: + description: Parent domain of the host. + required: true + type: str + description: + description: A description may be entered here for administrative reference. + required: true + type: str + domainoverrides: + description: Domains for which the resolver's standard DNS lookup should be overridden. + required: false + type: list + elements: dict + suboptions: + domain: + description: Domain whose lookups will be directed to a user-specified DNS lookup server. + required: true + type: str + ip: + description: IPv4 or IPv6 address of the authoritative DNS server for this domain. + required: true + type: str + forward_tls_upstream: + description: Use SSL/TLS for DNS Queries forwarded to this server + required: false + default: '' + type: str + tls_hostname: + description: An optional TLS hostname used to verify the server certificate when performing TLS Queries. + required: false + default: '' + type: str + descr: + description: A description may be entered here for administrative reference. + required: false + type: str + hideidentity: + description: id.server and hostname.bind queries are refused. + required: false + default: true + type: bool + hideversion: + description: version.server and version.bind queries are refused. + required: false + default: true + type: bool + prefetch: + description: Message cache elements are prefetched before they expire to help keep the cache up to date. + required: false + default: false + type: bool + prefetchkey: + description: DNSKEYs are fetched earlier in the validation process when a Delegation signer is encountered. + required: false + default: false + type: bool + dnssecstripped: + description: If enabled, DNSSEC data is required for trust-anchored zones. + required: false + default: true + type: bool + msgcachesize: + description: Message cache size in MB + required: false + default: 4 + choices: [ 4, 10, 20, 50, 100, 250, 512 ] + type: int + outgoing_num_tcp: + description: Number of outgoing TCP buffers to allocate per thread. + required: false + default: 10 + choices: [ 0, 10, 20, 30, 50 ] + type: int + incoming_num_tcp: + description: Number of incoming TCP buffers to allocate per thread. + required: false + default: 10 + choices: [ 0, 10, 20, 30, 50 ] + type: int + edns_buffer_size: + description: Number of bytes to advertise as the EDNS reassembly buffer size. + required: false + default: "auto" + choices: [ "auto", "512", "1220", "1232", "1432", "1480", "4096" ] + type: str + num_queries_per_thread: + description: Number of queries that every thread will service simultaneously. + required: false + default: 512 + choices: [ 512, 1024, 2048 ] + type: int + jostle_timeout: + description: This timeout (in milliseconds) is used for when the server is very busy. + required: false + default: 200 + choices: [ 100, 200, 500, 1000 ] + type: int + cache_max_ttl: + description: The Maximum Time to Live (in seconds) for RRsets and messages in the cache. + required: false + default: 86400 + type: int + cache_min_ttl: + description: The Minimum Time to Live (in seconds) for RRsets and messages in the cache. + required: false + default: 0 + type: int + infra_host_ttl: + description: Time to Live, in seconds, for entries in the infrastructure host cache. + required: false + default: 900 + choices: [ 60, 120, 300, 600, 900 ] + type: int + infra_cache_numhosts: + description: Number of infrastructure hosts for which information is cached. + required: false + default: 10000 + choices: [ 1000, 5000, 10000, 20000, 50000, 100000, 200000 ] + type: int + unwanted_reply_threshold: + description: If enabled, a total number of unwanted replies is kept track of in every thread. + required: false + default: "disabled" + choices: [ "disabled", "5000000", "10000000", "20000000", "40000000", "50000000" ] + type: str + log_verbosity: + description: The level of detail to be logged. + required: false + default: 1 + choices: [ 0, 1, 2, 3, 4, 5 ] + type: int +""" + +EXAMPLES = """ +- name: Enable DNS Resolver + pfsense_dns_resolver: + state: present + +- name: Enable DNS Resolver with some options + pfsense_dns_resolver: + state: present + enablessl: true + sslcert: "webConfigurator default" + dnssec: true + regdhcp: true + regdhcpstatic: true + hosts: + - { host: test, domain: home.local, ip: 192.168.1.100, descr: "Example host override", + aliases: [{ host: test-admin, domain: home.local, description: "Example aliases" }] } + +- name: Disable DNS Resolver + pfsense_dns_resolver: + state: absent +""" + +RETURN = """ + +""" + +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase +from ansible.module_utils.basic import AnsibleModule +import base64 + +# TODO: access control is not done here +# TODO: alias for DNS record + +DNS_RESOLVER_DOMAIN_OVERRIDE_SPEC = dict( + domain=dict(required=True, type='str'), + ip=dict(required=True, type='str'), + descr=dict(type='str'), + tls_hostname=dict(default='', type='str'), + forward_tls_upstream=dict(default='', type='str'), +) + +DNS_RESOLVER_HOST_ALIAS_SPEC = dict( + host=dict(required=True, type='str'), + domain=dict(required=True, type='str'), + description=dict(required=True, type='str'), +) + +DNS_RESOLVER_HOST_SPEC = dict( + host=dict(required=True, type='str'), + domain=dict(required=True, type='str'), + ip=dict(required=True, type='str'), + descr=dict(default="", type='str'), + aliases=dict(default=[], type='list', elements='dict', options=DNS_RESOLVER_HOST_ALIAS_SPEC), +) + +DNS_RESOLVER_ARGUMENT_SPEC = dict( + state=dict(default='present', choices=['present', 'absent']), + + # General Settings + port=dict(default=None, type='int'), + enablessl=dict(default=False, type='bool'), + sslcert=dict(default="", type='str'), # need transform + tlsport=dict(default=None, type='int'), + active_interface=dict(default=["all"], type='list', elements='str'), + outgoing_interface=dict(default=["all"], type='list', elements='str'), + # TODO: Strict Outgoing Network interface Binding: check box option + system_domain_local_zone_type=dict(default='transparent', choices=['deny', 'refuse', 'static', 'transparent', 'typetransparent', 'redirect', 'inform', + 'inform_deny', 'nodefault']), + dnssec=dict(default=False, type='bool'), + # TODO: Python Module: Enable the Python Module. These 3 options omited when disabled + # python=dict(default=False, type='bool'), + # python_order=dict(default="pre_validator", type='str', choices=["pre_validator", "post_validator"]), + # python_script=dict(default="", type='str'), #Not sure what this is or how to handle it. + forwarding=dict(default=False, type='bool'), + forward_tls_upstream=dict(default=False, type='bool'), + regdhcp=dict(default=False, type='bool'), + regdhcpstatic=dict(default=False, type='bool'), + regovpnclients=dict(default=False, type='bool'), + custom_options=dict(default="", type='str'), + hosts=dict(default=[], type='list', elements='dict', options=DNS_RESOLVER_HOST_SPEC), + domainoverrides=dict(type='list', elements='dict', options=DNS_RESOLVER_DOMAIN_OVERRIDE_SPEC), + # Advanced Settings + hideidentity=dict(default=True, type='bool'), + hideversion=dict(default=True, type='bool'), + # TODO: Query Name Minimization + # TODO: Strict Query Name Minimization + prefetch=dict(default=False, type='bool'), + prefetchkey=dict(default=False, type='bool'), + dnssecstripped=dict(default=True, type='bool'), + # TODO: Serve Expired + # TODO: Aggressive NSEC + msgcachesize=dict(default=4, type='int', choices=[4, 10, 20, 50, 100, 250, 512]), + outgoing_num_tcp=dict(default=10, type='int', choices=[0, 10, 20, 30, 50]), + incoming_num_tcp=dict(default=10, type='int', choices=[0, 10, 20, 30, 50]), + edns_buffer_size=dict(default="auto", type='str', choices=["auto", "512", "1220", "1232", "1432", "1480", "4096"]), + num_queries_per_thread=dict(default=512, type='int', choices=[512, 1024, 2048]), + jostle_timeout=dict(default=200, type='int', choices=[100, 200, 500, 1000]), + cache_max_ttl=dict(default=86400, type='int'), + cache_min_ttl=dict(default=0, type='int'), + infra_host_ttl=dict(default=900, type='int', choices=[60, 120, 300, 600, 900]), + infra_cache_numhosts=dict(default=10000, type='int', choices=[1000, 5000, 10000, 20000, 50000, 100000, 200000]), + unwanted_reply_threshold=dict(default="disabled", type='str', choices=["disabled", "5000000", "10000000", "20000000", "40000000", "50000000"]), + log_verbosity=dict(default=1, type='int', choices=[0, 1, 2, 3, 4, 5]) + # TODO: Disable Auto-added Access Control + # TODO: Disable Auto-added Host Entries + # TODO: Experimental Bit 0x20 Support + # TODO: DNS64 Support +) + +DNS_RESOLVER_REQUIRED_IF = [] + + +class PFSenseDNSResolverModule(PFSenseModuleBase): + """ module managing pfsense dns resolver (unbound) """ + + @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 = "pfsense_dns_resolver" + self.root_elt = self.pfsense.get_element('unbound') + self.obj = dict() + self.interface_elt = None + self.dynamic = False + + if self.root_elt is None: + self.root_elt = self.pfsense.new_element('unbound') + self.pfsense.root.append(self.root_elt) + + def get_interface_by_display_name(self, if_descr: str): + if if_descr.lower() == "all": + return "all" + else: + return self.pfsense.get_interface_by_display_name(if_descr) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = dict() + + if params["state"] == "present": + + obj["enable"] = "" + obj["active_interface"] = ",".join(self.get_interface_by_display_name(x) for x in params["active_interface"]) + obj["outgoing_interface"] = ",".join(self.get_interface_by_display_name(x) for x in params["outgoing_interface"]) + obj["custom_options"] = base64.b64encode(bytes(params['custom_options'], 'utf-8')).decode() + self._get_ansible_param_bool(obj, "hideidentity", value="") + self._get_ansible_param_bool(obj, "hideversion", value="") + self._get_ansible_param_bool(obj, "dnssecstripped", value="") + self._get_ansible_param(obj, "port") + self._get_ansible_param(obj, "tlsport") + if params["sslcert"]: + obj["sslcertref"] = self.pfsense.find_cert_elt(params["sslcert"]).find("refid").text + self._get_ansible_param_bool(obj, "forwarding", value="") + self._get_ansible_param(obj, "system_domain_local_zone_type") + self._get_ansible_param_bool(obj, "regdhcp", value="") + self._get_ansible_param_bool(obj, "regdhcpstatic", value="") + self._get_ansible_param_bool(obj, "regovpnclients", value="") + self._get_ansible_param_bool(obj, "enablessl", value="") + self._get_ansible_param_bool(obj, "dnssec", value="") + self._get_ansible_param_bool(obj, "forward_tls_upstream", value="") + self._get_ansible_param_bool(obj, "prefetch", value="") + self._get_ansible_param_bool(obj, "prefetchkey", value="") + self._get_ansible_param(obj, "msgcachesize") + self._get_ansible_param(obj, "outgoing_num_tcp") + self._get_ansible_param(obj, "incoming_num_tcp") + self._get_ansible_param(obj, "edns_buffer_size") + self._get_ansible_param(obj, "num_queries_per_thread") + self._get_ansible_param(obj, "jostle_timeout") + self._get_ansible_param(obj, "cache_max_ttl") + self._get_ansible_param(obj, "cache_min_ttl") + self._get_ansible_param(obj, "infra_host_ttl") + self._get_ansible_param(obj, "infra_cache_numhosts") + self._get_ansible_param(obj, "unwanted_reply_threshold") + self._get_ansible_param(obj, "log_verbosity") + self._get_ansible_param(obj, "hosts") + self._get_ansible_param(obj, "domainoverrides") + + if obj["active_interface"] != "all": + obj["active_interface"] += ",lo0" + + # wrap to all hosts.alias + for host in obj["hosts"]: + if host["aliases"]: + tmp_aliases = host["aliases"] + host["aliases"] = { + "item": tmp_aliases + } + else: + # Default is an empty element + host["aliases"] = "\n\t\t\t" + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + params = self.params + + if params["sslcert"] and not self.pfsense.find_cert_elt(params["sslcert"]): + self.module.fail_json(msg=f'sslcert, {params["sslcert"]} is not a valid description of cert') + + for host in params["hosts"]: + if not self.pfsense.is_ipv4_address(host["ip"]): + self.module.fail_json(msg=f'ip, {host["ip"]} is not a ipv4 address') + + for if_descr in params["active_interface"] + params["outgoing_interface"]: + if not self.pfsense.is_interface_display_name(if_descr) and if_descr.lower() != "all": + self.module.fail_json(msg=f'if_descr, {if_descr}, is not exist') + + ############################## + # XML processing + # + def _create_target(self): + """ create the XML target_elt """ + return self.root_elt + + def _find_target(self): + """ find the XML target_elt """ + return self.root_elt + + def _get_params_to_remove(self): + """ returns the list of params to remove if they are not set """ + if self.params["state"] == "absent": + return ["enable"] + else: + return [] + + ############################## + # run + # + def _update(self): + """ make the target pfsense reload """ + return self.pfsense.phpshell(''' +require_once("unbound.inc"); +require_once("pfsense-utils.inc"); +require_once("system.inc"); + +services_unbound_configure(); +system_resolvconf_generate(); +system_dhcpleases_configure(); +clear_subsystem_dirty("unbound"); +''') + + ############################## + # Logging + # + def _get_obj_name(self): + """ return obj's name """ + return self.name + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + values += self.format_updated_cli_field(self.obj, before, 'enable', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'active_interface', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'outgoing_interface', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'custom_options', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'hideidentity', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'hideversion', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'dnssecstripped', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'port', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'tlsport', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'sslcertref', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'forwarding', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'system_domain_local_zone_type', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'regdhcp', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'regdhcpstatic', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'prefetch', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'prefetchkey', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'msgcachesize', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'outgoing_num_tcp', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'incoming_num_tcp', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'edns_buffer_size', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'num_queries_per_thread', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'jostle_timeout', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'cache_max_ttl', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'cache_min_ttl', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'infra_host_ttl', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'infra_cache_numhosts', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'unwanted_reply_threshold', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'log_verbosity', add_comma=(values), log_none=False) + + # todo: hosts and domainoverrides is not logged + return values + + +def main(): + module = AnsibleModule( + argument_spec=DNS_RESOLVER_ARGUMENT_SPEC, + required_if=DNS_RESOLVER_REQUIRED_IF, + supports_check_mode=True) + + pfmodule = PFSenseDNSResolverModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/pfsense_interface_group.py b/plugins/modules/pfsense_interface_group.py index 4a82ad73..9bfcccb7 100644 --- a/plugins/modules/pfsense_interface_group.py +++ b/plugins/modules/pfsense_interface_group.py @@ -37,7 +37,6 @@ members: description: The members of the interface group. type: list - required: yes elements: str """ @@ -76,12 +75,17 @@ """ from ansible.module_utils.basic import AnsibleModule -from ansible_collections.pfsensible.core.plugins.module_utils.interface_group import PFSenseInterfaceGroupModule, INTERFACE_GROUP_ARGUMENT_SPEC +from ansible_collections.pfsensible.core.plugins.module_utils.interface_group import ( + PFSenseInterfaceGroupModule, + INTERFACE_GROUP_ARGUMENT_SPEC, + INTERFACE_GROUP_REQUIRED_IF +) def main(): module = AnsibleModule( argument_spec=INTERFACE_GROUP_ARGUMENT_SPEC, + required_if=INTERFACE_GROUP_REQUIRED_IF, supports_check_mode=True) pfmodule = PFSenseInterfaceGroupModule(module) diff --git a/plugins/modules/pfsense_nat_outbound.py b/plugins/modules/pfsense_nat_outbound.py index 72232c0e..9586de96 100644 --- a/plugins/modules/pfsense_nat_outbound.py +++ b/plugins/modules/pfsense_nat_outbound.py @@ -49,12 +49,12 @@ choices: [ "any", "tcp", "udp", "tcp/udp", "icmp", "esp", "ah", "gre", "ipv6", "igmp", "carp", "pfsync" ] type: str source: - description: The matching source address, in {any,(self),ALIAS,NETWORK}[:port] format. + description: The matching source address, in {any,(self),ALIAS,NETWORK,NET:INTERFACE}[:port] format. required: false default: null type: str destination: - description: The matching destination address, in {any,ALIAS,NETWORK}[:port] format. + description: The matching destination address, in {any,ALIAS,NETWORK,NET:INTERFACE}[:port] format. required: false default: null type: str diff --git a/plugins/modules/pfsense_openvpn_client.py b/plugins/modules/pfsense_openvpn_client.py index 9b0fa12f..360775cc 100644 --- a/plugins/modules/pfsense_openvpn_client.py +++ b/plugins/modules/pfsense_openvpn_client.py @@ -87,7 +87,7 @@ default: false type: bool shared_key: - description: Pre-shared key for shared key modes. + description: Pre-shared key for shared key modes. If set to 'generate' it will create a key if one does not already exist. type: str dh_length: description: DH parameter length. @@ -220,6 +220,28 @@ ''' RETURN = r''' +shared_key: + description: The generated shared key, base64 encoded + returned: when `generate` is passed as the shared_key argument and a key is generated. + type: str + sample: |- + IwojIDIwNDggYml0IE9wZW5WUE4gc3RhdGljIGtleQojCi0tLS0tQkVHSU4gT3BlblZQTiBTdGF0aWMga2V5IFYxLS0tLS0KNjFiY2E4MDk0ZmM4YjA3ZTZlMjE3NzRmNTI0YTIyOWYKNGMzZGZhMDVjZ + Tc2ODVlN2NkNDc1N2I0OGM3ZmMzZDcKYzQzMjhjYzBmMWQ4Yjc2OTk2MjVjNzAwYmVkNzNhNWYKY2RjMjYzMTY2YThlMzVmYTk4NGU0OWVkZDg5MDNkZmMKMDc1ZTQyY2ZlOTM5NzUwYzhmMjc1YTY3MT + kzMGRmMzEKMDY2Mzk1MjM2ZWRkYWQ3NDc3YmVjZjJmNDgyNzBlMjUKODM1N2JlMGE1MGUzY2Y0ZjllZTEyZTdkMmM4YTY2YzEKODUwNjBlODM5ZWUyMzdjNTZkZmUzNjA4NjU0NDhhYzgKNjhmM2JhYWQ + 4ODNjNDU3NTdlZTVjMWQ4ZDk5ZjM4ZjcKZGNiZDAwZmI3Nzc2ZWFlYjQ1ZmQwOTBjNGNlYTNmMGMKMzgzNDE0ZTJlYmU4MWNiZGIxZmNlN2M2YmFhMDlkMWYKMTU4OGUzNGRkYzUxY2NjOTE5NDNjNTFh + OTI2OTE3NWQKNzZiZjdhOWI1ZmM3NDAyNmE3MTVkNGVmODVkYzY2Y2UKMWE5MWQwNjNhODIwZDY4MTc0ODlmYjJkZjNmYzY2MmMKMmU2OWZiMzNiMzM5MjdjYjUyNThkZDQ4M2NkNDE0Y2QKMDJhZWE3Z + jA3MmNhZmEwOTY5Yjg5NWVjYzNiYmExNGQKLS0tLS1FTkQgT3BlblZQTiBTdGF0aWMga2V5IFYxLS0tLS0K +tls: + description: The generated tls key, base64 encoded + returned: when `generate` is passed as the tls argument and a key is generated. + type: str + sample: |- + IwojIDIwNDggYml0IE9wZW5WUE4gc3RhdGljIGtleQojCi0tLS0tQkVHSU4gT3BlblZQTiBTdGF0aWMga2V5IFYxLS0tLS0KNjFiY2E4MDk0ZmM4YjA3ZTZlMjE3NzRmNTI0YTIyOWYKNGMzZGZhMDVjZ + Tc2ODVlN2NkNDc1N2I0OGM3ZmMzZDcKYzQzMjhjYzBmMWQ4Yjc2OTk2MjVjNzAwYmVkNzNhNWYKY2RjMjYzMTY2YThlMzVmYTk4NGU0OWVkZDg5MDNkZmMKMDc1ZTQyY2ZlOTM5NzUwYzhmMjc1YTY3MT + kzMGRmMzEKMDY2Mzk1MjM2ZWRkYWQ3NDc3YmVjZjJmNDgyNzBlMjUKODM1N2JlMGE1MGUzY2Y0ZjllZTEyZTdkMmM4YTY2YzEKODUwNjBlODM5ZWUyMzdjNTZkZmUzNjA4NjU0NDhhYzgKNjhmM2JhYWQ + 4ODNjNDU3NTdlZTVjMWQ4ZDk5ZjM4ZjcKZGNiZDAwZmI3Nzc2ZWFlYjQ1ZmQwOTBjNGNlYTNmMGMKMzgzNDE0ZTJlYmU4MWNiZGIxZmNlN2M2YmFhMDlkMWYKMTU4OGUzNGRkYzUxY2NjOTE5NDNjNTFh + OTI2OTE3NWQKNzZiZjdhOWI1ZmM3NDAyNmE3MTVkNGVmODVkYzY2Y2UKMWE5MWQwNjNhODIwZDY4MTc0ODlmYjJkZjNmYzY2MmMKMmU2OWZiMzNiMzM5MjdjYjUyNThkZDQ4M2NkNDE0Y2QKMDJhZWE3Z + jA3MmNhZmEwOTY5Yjg5NWVjYzNiYmExNGQKLS0tLS1FTkQgT3BlblZQTiBTdGF0aWMga2V5IFYxLS0tLS0K ''' from ansible.module_utils.basic import AnsibleModule diff --git a/plugins/modules/pfsense_openvpn_server.py b/plugins/modules/pfsense_openvpn_server.py index 384126d3..91047b59 100644 --- a/plugins/modules/pfsense_openvpn_server.py +++ b/plugins/modules/pfsense_openvpn_server.py @@ -90,7 +90,7 @@ default: false type: bool shared_key: - description: Pre-shared key for shared key modes. + description: Pre-shared key for shared key modes. If set to 'generate' it will create a key if one does not already exist. type: str dh_length: description: DH parameter length. @@ -258,6 +258,28 @@ """ RETURN = r''' +shared_key: + description: The generated shared key, base64 encoded + returned: when `generate` is passed as the shared_key argument and a key is generated. + type: str + sample: |- + IwojIDIwNDggYml0IE9wZW5WUE4gc3RhdGljIGtleQojCi0tLS0tQkVHSU4gT3BlblZQTiBTdGF0aWMga2V5IFYxLS0tLS0KNjFiY2E4MDk0ZmM4YjA3ZTZlMjE3NzRmNTI0YTIyOWYKNGMzZGZhMDVjZ + Tc2ODVlN2NkNDc1N2I0OGM3ZmMzZDcKYzQzMjhjYzBmMWQ4Yjc2OTk2MjVjNzAwYmVkNzNhNWYKY2RjMjYzMTY2YThlMzVmYTk4NGU0OWVkZDg5MDNkZmMKMDc1ZTQyY2ZlOTM5NzUwYzhmMjc1YTY3MT + kzMGRmMzEKMDY2Mzk1MjM2ZWRkYWQ3NDc3YmVjZjJmNDgyNzBlMjUKODM1N2JlMGE1MGUzY2Y0ZjllZTEyZTdkMmM4YTY2YzEKODUwNjBlODM5ZWUyMzdjNTZkZmUzNjA4NjU0NDhhYzgKNjhmM2JhYWQ + 4ODNjNDU3NTdlZTVjMWQ4ZDk5ZjM4ZjcKZGNiZDAwZmI3Nzc2ZWFlYjQ1ZmQwOTBjNGNlYTNmMGMKMzgzNDE0ZTJlYmU4MWNiZGIxZmNlN2M2YmFhMDlkMWYKMTU4OGUzNGRkYzUxY2NjOTE5NDNjNTFh + OTI2OTE3NWQKNzZiZjdhOWI1ZmM3NDAyNmE3MTVkNGVmODVkYzY2Y2UKMWE5MWQwNjNhODIwZDY4MTc0ODlmYjJkZjNmYzY2MmMKMmU2OWZiMzNiMzM5MjdjYjUyNThkZDQ4M2NkNDE0Y2QKMDJhZWE3Z + jA3MmNhZmEwOTY5Yjg5NWVjYzNiYmExNGQKLS0tLS1FTkQgT3BlblZQTiBTdGF0aWMga2V5IFYxLS0tLS0K +tls: + description: The generated tls key, base64 encoded + returned: when `generate` is passed as the tls argument and a key is generated. + type: str + sample: |- + IwojIDIwNDggYml0IE9wZW5WUE4gc3RhdGljIGtleQojCi0tLS0tQkVHSU4gT3BlblZQTiBTdGF0aWMga2V5IFYxLS0tLS0KNjFiY2E4MDk0ZmM4YjA3ZTZlMjE3NzRmNTI0YTIyOWYKNGMzZGZhMDVjZ + Tc2ODVlN2NkNDc1N2I0OGM3ZmMzZDcKYzQzMjhjYzBmMWQ4Yjc2OTk2MjVjNzAwYmVkNzNhNWYKY2RjMjYzMTY2YThlMzVmYTk4NGU0OWVkZDg5MDNkZmMKMDc1ZTQyY2ZlOTM5NzUwYzhmMjc1YTY3MT + kzMGRmMzEKMDY2Mzk1MjM2ZWRkYWQ3NDc3YmVjZjJmNDgyNzBlMjUKODM1N2JlMGE1MGUzY2Y0ZjllZTEyZTdkMmM4YTY2YzEKODUwNjBlODM5ZWUyMzdjNTZkZmUzNjA4NjU0NDhhYzgKNjhmM2JhYWQ + 4ODNjNDU3NTdlZTVjMWQ4ZDk5ZjM4ZjcKZGNiZDAwZmI3Nzc2ZWFlYjQ1ZmQwOTBjNGNlYTNmMGMKMzgzNDE0ZTJlYmU4MWNiZGIxZmNlN2M2YmFhMDlkMWYKMTU4OGUzNGRkYzUxY2NjOTE5NDNjNTFh + OTI2OTE3NWQKNzZiZjdhOWI1ZmM3NDAyNmE3MTVkNGVmODVkYzY2Y2UKMWE5MWQwNjNhODIwZDY4MTc0ODlmYjJkZjNmYzY2MmMKMmU2OWZiMzNiMzM5MjdjYjUyNThkZDQ4M2NkNDE0Y2QKMDJhZWE3Z + jA3MmNhZmEwOTY5Yjg5NWVjYzNiYmExNGQKLS0tLS1FTkQgT3BlblZQTiBTdGF0aWMga2V5IFYxLS0tLS0K vpnid: description: The vpnid number of the OpenVPN server instance. returned: always diff --git a/plugins/modules/pfsense_rule.py b/plugins/modules/pfsense_rule.py index 749358ef..0c22db1e 100644 --- a/plugins/modules/pfsense_rule.py +++ b/plugins/modules/pfsense_rule.py @@ -42,7 +42,7 @@ default: false type: bool interface: - description: The interface for the rule + description: The interface for the rule. Use 'any' to apply to all interface (for floating rules only). required: true type: str floating: diff --git a/plugins/modules/pfsense_setup.py b/plugins/modules/pfsense_setup.py index c64b98d3..cea65766 100644 --- a/plugins/modules/pfsense_setup.py +++ b/plugins/modules/pfsense_setup.py @@ -50,14 +50,13 @@ required: false description: > Do not use the DNS Forwarder/DNS Resolver as a DNS server for the firewall. - "" Use local DNS, fall back to remote DNS server - "local" Use local DNS, ignore remote DNS server + "" Use local DNS (127.0.0.1), fall back to remote DNS servers (Default) + "local" Use local DNS (127.0.0), ignore remote DNS servers "remote" Use remote DNS server, ignore local DNS true will be mapped to "remote" false will be mapped to "" type: str choices: ["", "local", "remote", "true", "false"] - default: "" timezone: description: Select a geographic region name (Continent/Location) to determine the timezone for the firewall. required: false @@ -85,18 +84,17 @@ choices: ['nohost', 'hostonly', 'fqdn'] type: str session_timeout: - description: Time in minutes to expire idle management sessions (0 means no expiration) + description: > + Time in minutes to expire idle management sessions. The default is 4 hours (240 minutes). + Use 0 to never expire sessions. NOTE: This is a security risk! required: false - default: 240 type: int authmode: - description: Authentication Server ('Local Database' means local), use name of configured ldap or radius server + description: Authentication Server ('Local Database' means local (Default)), use name of configured ldap or radius server required: false - default: 'Local Database' type: str shellauth: - description: Use Authentication Server for Shell Authentication (pfsense-CE >=2.5.0, pfsense-PLUS >=21.2) - default: false + description: Use Authentication Server for Shell Authentication (pfsense-CE >=2.5.0, pfsense-PLUS >=21.2). Default is false. type: bool dashboardcolumns: description: Dashboard columns @@ -201,9 +199,9 @@ type='str', choices=['bs', 'de_DE', 'en_US', 'es', 'es_AR', 'fr', 'ko', 'nb', 'nl', 'pl', 'pt_PT', 'pt_BR', 'ru', 'zh_CN', 'zh_Hans_CN', 'zh_HK', 'zh_TW'] ), - session_timeout=dict(required=False, type='int', default=240), - authmode=dict(required=False, type='str', default='Local Database'), - shellauth=dict(required=False, type='bool', default=False), + session_timeout=dict(required=False, type='int'), + authmode=dict(required=False, type='str'), + shellauth=dict(required=False, type='bool'), webguicss=dict(required=False, type='str'), webguifixedmenu=dict(required=False, type='bool'), webguihostnamemenu=dict(required=False, type='str', choices=['nohost', 'hostonly', 'fqdn']), diff --git a/tests/plays/README.md b/tests/plays/README.md new file mode 100644 index 00000000..44d2fe36 --- /dev/null +++ b/tests/plays/README.md @@ -0,0 +1,11 @@ +# Testing pfsensible/core with plays + +You must checkout this repository into a path of the form ../ansible_collections/pfsensible/core/. + +The following collection dependencies are needed: + * ansible.utils + +You will need a fresh pfSense install available as `pfsense-test` or adjust the `hosts` file as needed. +You need to be able to ssh to it as `root` without a password or use `--ask-pass`. + +Update `host_vars/pfsense-test.yml` with IP addresses of your test pfSense install. diff --git a/tests/plays/ansible.cfg b/tests/plays/ansible.cfg new file mode 100644 index 00000000..6fa7b3b0 --- /dev/null +++ b/tests/plays/ansible.cfg @@ -0,0 +1,7 @@ +# config file for ansible -- https://ansible.com/ +# =============================================== + +[defaults] +inventory = hosts +collections_paths = ../../../.. +remote_user = root diff --git a/tests/plays/host_vars/pfsense-test.yml b/tests/plays/host_vars/pfsense-test.yml new file mode 100644 index 00000000..e915689f --- /dev/null +++ b/tests/plays/host_vars/pfsense-test.yml @@ -0,0 +1,4 @@ +--- +# IP address of the interfaces +interface_ips: + wan: 192.168.122.228 diff --git a/tests/plays/hosts b/tests/plays/hosts new file mode 100644 index 00000000..8c2c912e --- /dev/null +++ b/tests/plays/hosts @@ -0,0 +1,2 @@ +[pfsense] +pfsense-test diff --git a/openvpn.yml b/tests/plays/openvpn.yml similarity index 86% rename from openvpn.yml rename to tests/plays/openvpn.yml index 4ac1bc87..fbc61489 100644 --- a/openvpn.yml +++ b/tests/plays/openvpn.yml @@ -195,6 +195,39 @@ - openvpn - openvpn_psk + - name: Create OpenVPN Server generate + import_tasks: tasks/test_openvpn_server_create.yml + vars: + openvpn_server_args: + name: OpenVPN Server generate + mode: server_tls_user + authmode: + - RADIUS + interface: wan + local_port: 1197 + tls: generate + tls_type: auth + ca: OpenVPN CA + cert: pfsense-test + data_ciphers: + - AES-256-GCM + - AES-128-GCM + - AES-256-CBC + tunnel_network: 10.100.1.0/24 + compression: "" + gwredir: yes + passtos: yes + dns_domain: example.com + dns_server1: 10.10.10.10 + dns_server2: 10.10.10.11 + custom_options: |- + tls-version-min 1.2; + username_as_common_name: no + openvpn_server_vpnid: 4 + tags: + - openvpn + - openvpn_generate + - name: Create OpenVPN override vpnuser import_tasks: tasks/test_openvpn_override_create.yml vars: @@ -282,6 +315,34 @@ - openvpn - openvpn_override + - name: Delete VPN1 interfce (fails) + pfsensible.core.pfsense_interface: + descr: VPN1 + state: absent + register: interface + failed_when: interface.msg != "The interface is part of the group VPN. Please remove it from the group first." + tags: + - openvpn + - openvpn_interface_delete + + - name: Delete OpenVPN Server 1 (fails) + pfsensible.core.pfsense_openvpn_server: + name: OpenVPN Server 1 + state: absent + tags: + - openvpn + - openvpn_delete + register: openvpn_server + failed_when: openvpn_server.msg != "Cannot delete the OpenVPN instance while the interface ovpns1 is assigned. Remove the interface assignment first." + + - name: Delete VPN interface_group + pfsensible.core.pfsense_interface_group: + name: VPN + state: absent + tags: + - openvpn + - openvpn_interface_delete + - name: Delete OpenVPN Server 1 import_tasks: tasks/test_openvpn_server_delete.yml vars: @@ -311,3 +372,13 @@ tags: - openvpn - openvpn_delete + + - name: Delete OpenVPN Server generate + import_tasks: tasks/test_openvpn_server_delete.yml + vars: + openvpn_server_args: + name: OpenVPN Server generate + openvpn_server_vpnid: 4 + tags: + - openvpn + - openvpn_delete diff --git a/tasks/test_interface_create.yml b/tests/plays/tasks/test_interface_create.yml similarity index 72% rename from tasks/test_interface_create.yml rename to tests/plays/tasks/test_interface_create.yml index 8acfcbbe..91039114 100644 --- a/tasks/test_interface_create.yml +++ b/tests/plays/tasks/test_interface_create.yml @@ -1,5 +1,5 @@ --- - - name: "Define {{ interface_args.name }}" + - name: "Define {{ interface_args.descr }}" pfsensible.core.pfsense_interface: "{{ interface_args }}" register: interface @@ -7,11 +7,13 @@ msg: Interface ifname {{ interface.ifname }} does not match expected value {{ interface_ifname }} when: interface.ifname != interface_ifname - - command: /sbin/ifconfig {{ interface_args.interface }} + - name: Get interface configuration for {{ interface_args.interface }} + command: /sbin/ifconfig {{ interface_args.interface }} changed_when: no register: ifconfig - - set_fact: + - name: Get interface description + set_fact: if_description: "{{ ifconfig.stdout_lines | select('search', 'description:') | map('regex_replace', '^\\s*description:\\s*', '') | first }}" - fail: diff --git a/tasks/test_interface_group_create.yml b/tests/plays/tasks/test_interface_group_create.yml similarity index 100% rename from tasks/test_interface_group_create.yml rename to tests/plays/tasks/test_interface_group_create.yml diff --git a/tasks/test_interface_group_ifconfig_groups.yml b/tests/plays/tasks/test_interface_group_ifconfig_groups.yml similarity index 100% rename from tasks/test_interface_group_ifconfig_groups.yml rename to tests/plays/tasks/test_interface_group_ifconfig_groups.yml diff --git a/tasks/test_openvpn_override_create.yml b/tests/plays/tasks/test_openvpn_override_create.yml similarity index 100% rename from tasks/test_openvpn_override_create.yml rename to tests/plays/tasks/test_openvpn_override_create.yml diff --git a/tasks/test_openvpn_override_delete.yml b/tests/plays/tasks/test_openvpn_override_delete.yml similarity index 100% rename from tasks/test_openvpn_override_delete.yml rename to tests/plays/tasks/test_openvpn_override_delete.yml diff --git a/tasks/test_openvpn_override_file_exists.yml b/tests/plays/tasks/test_openvpn_override_file_exists.yml similarity index 100% rename from tasks/test_openvpn_override_file_exists.yml rename to tests/plays/tasks/test_openvpn_override_file_exists.yml diff --git a/tasks/test_openvpn_server_create.yml b/tests/plays/tasks/test_openvpn_server_create.yml similarity index 77% rename from tasks/test_openvpn_server_create.yml rename to tests/plays/tasks/test_openvpn_server_create.yml index 7b25e3b7..8a7e4d46 100644 --- a/tasks/test_openvpn_server_create.yml +++ b/tests/plays/tasks/test_openvpn_server_create.yml @@ -11,19 +11,23 @@ - wait_for: path: "/var/etc/openvpn/server{{ openvpn_server.vpnid }}/config.ovpn" - - slurp: + - name: Retrieve config.ovpn + slurp: src: "/var/etc/openvpn/server{{ openvpn_server.vpnid }}/config.ovpn" register: openvpn_config_file - - debug: msg="{{ openvpn_config_file['content'] | b64decode }}" + - name: Contents of config.ovpn + debug: msg="{{ openvpn_config_file['content'] | b64decode }}" - - template: + - name: Check if config.ovpn matches expected content + template: src: openvpn-server-config.ovpn.j2 dest: /var/etc/openvpn/server{{ openvpn_server.vpnid }}/config.ovpn owner: root group: wheel mode: 0600 - check_mode: yes + check_mode: true + diff: true register: config - fail: @@ -31,10 +35,11 @@ when: config.changed # TODO - Use community.general.pids with pattern (need version 3.0.0) - - shell: "ps xo command | grep '/openvpn --config /var/etc/openvpn/server{{ openvpn_server.vpnid }}/config.ovpn' | grep -v grep" + - name: Check if openvpn server is running + shell: "ps xo command | grep '/openvpn --config /var/etc/openvpn/server{{ openvpn_server.vpnid }}/config.ovpn' | grep -v grep" register: openvpn_server_process - ignore_errors: yes - changed_when: no + ignore_errors: true + changed_when: false - fail: msg: OpenVPN server process is not running diff --git a/tasks/test_openvpn_server_delete.yml b/tests/plays/tasks/test_openvpn_server_delete.yml similarity index 83% rename from tasks/test_openvpn_server_delete.yml rename to tests/plays/tasks/test_openvpn_server_delete.yml index e3f4c8a1..96f318f0 100644 --- a/tasks/test_openvpn_server_delete.yml +++ b/tests/plays/tasks/test_openvpn_server_delete.yml @@ -9,12 +9,14 @@ msg: OpenVPN server vpnid {{ openvpn_server.vpnid }} does not match expected value {{ openvpn_server_vpnid }} when: openvpn_server.vpnid != openvpn_server_vpnid - - wait_for: + - name: Wait for config.ovpn to be removed + wait_for: path: "/var/etc/openvpn/server{{ openvpn_server.vpnid }}/config.ovpn" state: absent # TODO - Use community.general.pids with pattern (need version 3.0.0) - - shell: "ps xo command | grep '/openvpn --config /var/etc/openvpn/server{{ openvpn_server.vpnid }}/config.ovpn' | grep -v grep" + - name: Check for running openvpn server + shell: "ps xo command | grep '/openvpn --config /var/etc/openvpn/server{{ openvpn_server.vpnid }}/config.ovpn' | grep -v grep" ignore_errors: yes register: openvpn_server_process changed_when: no diff --git a/templates/openvpn-override.j2 b/tests/plays/templates/openvpn-override.j2 similarity index 56% rename from templates/openvpn-override.j2 rename to tests/plays/templates/openvpn-override.j2 index 43031314..be24e0a9 100644 --- a/templates/openvpn-override.j2 +++ b/tests/plays/templates/openvpn-override.j2 @@ -1,8 +1,8 @@ {% if openvpn_override_args.tunnel_network is defined %} -ifconfig {{ openvpn_override_args.tunnel_network | nthhost(1) }} {{ openvpn_override_args.tunnel_network | nthhost(2) }} +ifconfig {{ openvpn_override_args.tunnel_network | ansible.utils.nthhost(1) }} {{ openvpn_override_args.tunnel_network | ansible.utils.nthhost(2) }} {% endif %} {% if openvpn_override_args.remote_network is defined %} -route {{ openvpn_override_args.remote_network | ipaddr('network') }} {{ openvpn_override_args.remote_network | ipaddr('netmask') }} +route {{ openvpn_override_args.remote_network | ansible.utils.ipaddr('network') }} {{ openvpn_override_args.remote_network | ansible.utils.ipaddr('netmask') }} {% endif %} {% if openvpn_override_args.gwredir is defined and openvpn_override_args.gwredir %} push "redirect-gateway def1" diff --git a/templates/openvpn-server-config.ovpn.j2 b/tests/plays/templates/openvpn-server-config.ovpn.j2 similarity index 81% rename from templates/openvpn-server-config.ovpn.j2 rename to tests/plays/templates/openvpn-server-config.ovpn.j2 index d5456bb8..65272fdd 100644 --- a/templates/openvpn-server-config.ovpn.j2 +++ b/tests/plays/templates/openvpn-server-config.ovpn.j2 @@ -1,5 +1,4 @@ dev ovpns{{ openvpn_server.vpnid }} -disable-dco verb {{ openvpn_server_args.verbosity_level if openvpn_server_args.verbosity_level is defined else '1' }} dev-type tun dev-node /dev/tun{{ openvpn_server.vpnid }} @@ -23,15 +22,15 @@ client-disconnect /usr/local/sbin/openvpn.attributes.sh {% if openvpn_server_args.interface == 'any' %} multihome {% else %} -local 192.168.122.227 +local {{ interface_ips[openvpn_server_args.interface] }} {% endif %} {% if 'tls' in openvpn_server_args.mode %} tls-server {% endif %} {% if 'p2p' in openvpn_server_args.mode %} -ifconfig {{ openvpn_server_args.tunnel_network | nthhost(1) }} {{ openvpn_server_args.tunnel_network | nthhost(2) }} +ifconfig {{ openvpn_server_args.tunnel_network | ansible.utils.nthhost(1) }} {{ openvpn_server_args.tunnel_network | ansible.utils.nthhost(2) }} {% else %} -server 10.100.0.0 255.255.255.0 +server {{ openvpn_server_args.tunnel_network | ansible.utils.ipaddr('network') }} {{ openvpn_server_args.tunnel_network | ansible.utils.ipaddr('netmask') }} {% endif %} {% if 'user' in openvpn_server_args.mode %} client-config-dir /var/etc/openvpn/server{{ openvpn_server.vpnid }}/csc @@ -48,12 +47,12 @@ tls-verify "/usr/local/sbin/ovpn_auth_verify tls 'pfsense-test' 1" lport {{ openvpn_server_args.local_port }} management /var/etc/openvpn/server{{ openvpn_server.vpnid }}/sock unix {% if 'user' in openvpn_server_args.mode %} -push "dhcp-option DOMAIN example.com" -push "dhcp-option DNS 10.10.10.10" -push "dhcp-option DNS 10.10.10.11" +push "dhcp-option DOMAIN {{ openvpn_server_args.dns_domain }}" +push "dhcp-option DNS {{ openvpn_server_args.dns_server1 }}" +push "dhcp-option DNS {{ openvpn_server_args.dns_server2 }}" {% endif %} {% if openvpn_server_args.remote_network is defined %} -route {{ openvpn_server_args.remote_network | ipaddr('network') }} {{ openvpn_server_args.remote_network | ipaddr('netmask') }} +route {{ openvpn_server_args.remote_network | ansible.utils.ipaddr('network') }} {{ openvpn_server_args.remote_network | ansible.utils.ipaddr('netmask') }} {% endif %} {% if 'shared_key' in openvpn_server_args.mode %} secret /var/etc/openvpn/server{{ openvpn_server.vpnid }}/secret diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt new file mode 100644 index 00000000..b7549f46 --- /dev/null +++ b/tests/sanity/ignore-2.16.txt @@ -0,0 +1,5 @@ +plugins/modules/pfsense_cert.py validate-modules:no-log-needed # Argument 'keylen' is not sensitive +plugins/modules/pfsense_dhcp_static.py validate-modules:no-log-needed # Arguments 'ddnsdomainkeyname' and 'ddnsdomainkeyalgorithm' are not sensitive +plugins/modules/pfsense_ipsec.py validate-modules:no-log-needed # Argument 'rekey_time' is not sensitive +plugins/modules/pfsense_ipsec_aggregate.py validate-modules:no-log-needed # Argument 'rekey_time' is not sensitive +plugins/modules/pfsense_user.py validate-modules:no-log-needed # Argument 'authorizedkeys' is not sensitive diff --git a/tests/unit/plugins/lookup/test_pfsense.py b/tests/unit/plugins/lookup/test_pfsense.py index a5d55eaf..b908d5da 100644 --- a/tests/unit/plugins/lookup/test_pfsense.py +++ b/tests/unit/plugins/lookup/test_pfsense.py @@ -4,7 +4,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import os from collections import OrderedDict import yaml from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch @@ -43,7 +42,6 @@ def setUp(self): self.build_definitions() - # self.fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'pfsense.yaml') self.mock_get_hostname = patch('ansible_collections.pfsensible.core.plugins.lookup.pfsense.LookupModule.get_hostname') get_hostname = self.mock_get_hostname.start() get_hostname.return_value = ('pf_test1') diff --git a/tests/unit/plugins/modules/fixtures/pfsense_interface_config.xml b/tests/unit/plugins/modules/fixtures/pfsense_interface_config.xml index 9c736a31..5bc323e1 100644 --- a/tests/unit/plugins/modules/fixtures/pfsense_interface_config.xml +++ b/tests/unit/plugins/modules/fixtures/pfsense_interface_config.xml @@ -1606,6 +1606,13 @@ acme.com + + + IFGROUP1 + + opt1 opt3 + + vmx0 diff --git a/tests/unit/plugins/modules/pfsense_module.py b/tests/unit/plugins/modules/pfsense_module.py index 03ea2e43..4305e430 100644 --- a/tests/unit/plugins/modules/pfsense_module.py +++ b/tests/unit/plugins/modules/pfsense_module.py @@ -1,6 +1,6 @@ # Copyright: (c) 2018 Red Hat Inc. # Copyright: (c) 2018, Frederic Bor -# Copyright: (c) 2022, Orion Poplawski +# Copyright: (c) 2024, 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) @@ -408,9 +408,9 @@ def check_param_equal(self, params, target_elt, param, default=None, xml_field=N else: self.assert_xml_elt_is_none_or_empty(target_elt, xml_field) - def check_param_bool(self, params, target_elt, param, default=False, value_true=None, xml_field=None): + def check_param_bool(self, params, target_elt, param, default=False, value_true=None, value_false=None, xml_field=None): """ if param is defined, check the elt exist and text equals value_true, otherwise that it does not exist in XML or - is empty if value_true is not None """ + is empty if value_true is not None or equals value_false if set """ if xml_field is None: xml_field = param @@ -423,7 +423,10 @@ def check_param_bool(self, params, target_elt, param, default=False, value_true= if value_true is None: self.assert_not_find_xml_elt(target_elt, xml_field) else: - self.assert_xml_elt_is_none_or_empty(target_elt, xml_field) + if value_false is not None: + self.assert_xml_elt_equal(target_elt, xml_field, value_false) + else: + self.assert_xml_elt_is_none_or_empty(target_elt, xml_field) def check_value_equal(self, target_elt, xml_field, value, empty=True): """ if value is defined, check if target_elt has the right value, otherwise that it does not exist in XML """ diff --git a/tests/unit/plugins/modules/test_pfsense_alias_null.py b/tests/unit/plugins/modules/test_pfsense_alias_null.py index ac1591bc..5be91a7b 100644 --- a/tests/unit/plugins/modules/test_pfsense_alias_null.py +++ b/tests/unit/plugins/modules/test_pfsense_alias_null.py @@ -4,7 +4,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from copy import copy import pytest import sys diff --git a/tests/unit/plugins/modules/test_pfsense_authserver_ldap.py b/tests/unit/plugins/modules/test_pfsense_authserver_ldap.py index d8c968f6..da286ac6 100644 --- a/tests/unit/plugins/modules/test_pfsense_authserver_ldap.py +++ b/tests/unit/plugins/modules/test_pfsense_authserver_ldap.py @@ -10,10 +10,8 @@ if sys.version_info < (2, 7): pytestmark = pytest.mark.skip("pfSense Ansible modules require Python >= 2.7") -from xml.etree.ElementTree import fromstring, ElementTree -from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch from ansible_collections.pfsensible.core.plugins.modules import pfsense_authserver_ldap -from .pfsense_module import TestPFSenseModule, load_fixture +from .pfsense_module import TestPFSenseModule class TestPFSenseAuthserverLDAPModule(TestPFSenseModule): diff --git a/tests/unit/plugins/modules/test_pfsense_authserver_radius.py b/tests/unit/plugins/modules/test_pfsense_authserver_radius.py index 1c5c2d51..cc4d879f 100644 --- a/tests/unit/plugins/modules/test_pfsense_authserver_radius.py +++ b/tests/unit/plugins/modules/test_pfsense_authserver_radius.py @@ -10,10 +10,8 @@ if sys.version_info < (2, 7): pytestmark = pytest.mark.skip("pfSense Ansible modules require Python >= 2.7") -from xml.etree.ElementTree import fromstring, ElementTree -from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch from ansible_collections.pfsensible.core.plugins.modules import pfsense_authserver_radius -from .pfsense_module import TestPFSenseModule, load_fixture +from .pfsense_module import TestPFSenseModule class TestPFSenseAuthserverRADIUSModule(TestPFSenseModule): diff --git a/tests/unit/plugins/modules/test_pfsense_ca.py b/tests/unit/plugins/modules/test_pfsense_ca.py index c1dd03ed..a65047ae 100644 --- a/tests/unit/plugins/modules/test_pfsense_ca.py +++ b/tests/unit/plugins/modules/test_pfsense_ca.py @@ -10,10 +10,8 @@ if sys.version_info < (2, 7): pytestmark = pytest.mark.skip("pfSense Ansible modules require Python >= 2.7") -from xml.etree.ElementTree import fromstring, ElementTree -from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch from ansible_collections.pfsensible.core.plugins.modules import pfsense_ca -from .pfsense_module import TestPFSenseModule, load_fixture +from .pfsense_module import TestPFSenseModule CERTIFICATE = ( "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlFQ0RDQ0F2Q2dBd0lCQWdJSUZqRk9oczFuTXpRd0RRWUpLb1pJaHZjTkFRRUxCUUF3WERFVE1CRUdBMVVFDQpBeE1LYjNCbGJuWndiaTFqWVRF" @@ -86,8 +84,10 @@ def check_target_elt(self, obj, target_elt): """ check XML definition of target elt """ self.check_param_equal(obj, target_elt, 'name', xml_field='descr') - self.check_param_equal_or_present(obj, target_elt, 'trust') - self.check_param_equal_or_present(obj, target_elt, 'randomserial') + if 'trust' in obj: + self.check_param_bool(obj, target_elt, 'trust', value_true='enabled', value_false='disabled') + if 'randomserial' in obj: + self.check_param_bool(obj, target_elt, 'randomserial', value_true='enabled', value_false='disabled') self.check_param_equal_or_present(obj, target_elt, 'serial') self.check_param_equal(obj, target_elt, 'certificate', xml_field='crt') @@ -124,6 +124,11 @@ def test_ca_update_serial(self): obj = dict(name='testdel', certificate=CERTIFICATE, serial=10) self.do_module_test(obj, command='update ca testdel set ') + def test_ca_update_trust(self): + """ test updating trust of a ca """ + obj = dict(name='testdel', certificate=CERTIFICATE, trust=False) + self.do_module_test(obj, command='update ca testdel set ') + ############## # misc # diff --git a/tests/unit/plugins/modules/test_pfsense_dhcp_static.py b/tests/unit/plugins/modules/test_pfsense_dhcp_static.py index 54cf3e74..d433b7a3 100644 --- a/tests/unit/plugins/modules/test_pfsense_dhcp_static.py +++ b/tests/unit/plugins/modules/test_pfsense_dhcp_static.py @@ -97,6 +97,14 @@ def test_dhcp_static_create_display(self): ) self.do_module_test(obj, command=command) + def test_dhcp_static_create_arp_table_static_entry(self): + """ test create with arp_table_static_entry """ + obj = dict(name='test_entry', macaddr='ab:ab:ab:ab:ab:ab', ipaddr='10.0.0.101', netif='opt1', arp_table_static_entry=True) + command = ( + "create dhcp_static 'test_entry', macaddr='ab:ab:ab:ab:ab:ab', ipaddr='10.0.0.101', arp_table_static_entry=True" + ) + self.do_module_test(obj, command=command) + def test_dhcp_static_create_wrong_subnet(self): """ test create with IP address in the wrong subnet """ obj = dict(name='test_entry', macaddr='ab:ab:ab:ab:ab:ab', ipaddr='1.2.3.4', netif='opt1') diff --git a/tests/unit/plugins/modules/test_pfsense_interface.py b/tests/unit/plugins/modules/test_pfsense_interface.py index c34e3cf1..a922ca17 100644 --- a/tests/unit/plugins/modules/test_pfsense_interface.py +++ b/tests/unit/plugins/modules/test_pfsense_interface.py @@ -165,13 +165,13 @@ def test_interface_create_none_mac_mtu_mss(self): def test_interface_delete(self): """ test deletion of an interface """ - interface = dict(descr='vt1', state='absent') + interface = dict(descr='vt1') command = "delete interface 'vt1'" self.do_module_test(interface, delete=True, command=command) def test_interface_delete_lan(self): """ test deletion of an interface """ - interface = dict(descr='lan', state='absent') + interface = dict(descr='lan') commands = [ "delete rule_separator 'test_separator', interface='lan'", "update rule 'floating_rule_2' on 'floating(lan,wan,lan_1100)' set interface='wan,lan_1100'", @@ -183,6 +183,12 @@ def test_interface_delete_lan(self): ] self.do_module_test(interface, delete=True, command=commands) + def test_interface_delete_fails(self): + """ test deletion of an interface that is part of a group """ + interface = dict(descr='lan_1100') + msg = "The interface is part of the group IFGROUP1. Please remove it from the group first." + self.do_module_test(interface, delete=True, failed=True, msg=msg) + def test_interface_update_noop(self): """ test not updating a interface """ interface = dict(descr='lan_1100', interface='vmx1.1100', enable=True, ipv4_type='static', ipv4_address='172.16.151.210', ipv4_prefixlen=24) @@ -278,7 +284,7 @@ def test_interface_error_inet6_overlaps2(self): def test_interface_delete_sub(self): """ test delete sub interface """ - interface = dict(descr='lan_1200', interface='vmx1.1200', state='absent') + interface = dict(descr='lan_1200', interface='vmx1.1200') command = "delete interface 'lan_1200'" self.do_module_test(interface, delete=True, command=command) diff --git a/tests/unit/plugins/modules/test_pfsense_interface_group.py b/tests/unit/plugins/modules/test_pfsense_interface_group.py new file mode 100644 index 00000000..4a57c440 --- /dev/null +++ b/tests/unit/plugins/modules/test_pfsense_interface_group.py @@ -0,0 +1,132 @@ +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2024, Orioni 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 + +import pytest +import sys + +if sys.version_info < (2, 7): + pytestmark = pytest.mark.skip("pfSense Ansible modules require Python >= 2.7") + +from ansible_collections.pfsensible.core.plugins.modules import pfsense_interface_group +from ansible_collections.pfsensible.core.plugins.module_utils.interface_group import PFSenseInterfaceGroupModule +from .pfsense_module import TestPFSenseModule + + +class TestPFSenseInterfaceGroupModule(TestPFSenseModule): + + module = pfsense_interface_group + + def __init__(self, *args, **kwargs): + super(TestPFSenseInterfaceGroupModule, self).__init__(*args, **kwargs) + self.config_file = 'pfsense_interface_config.xml' + self.pfmodule = PFSenseInterfaceGroupModule + + def setUp(self): + """ mocking up """ + + def php_mock(command): + if 'get_interface_list' in command: + interfaces = dict() + interfaces['vmx0'] = dict() + interfaces['vmx1'] = dict(descr='notuniq') + interfaces['vmx2'] = dict(descr='notuniq') + interfaces['vmx3'] = dict() + interfaces['vmx0.100'] = dict(descr='uniq') + interfaces['vmx1.1100'] = dict() + return interfaces + return ['autoselect'] + + super(TestPFSenseInterfaceGroupModule, self).setUp() + + self.php.return_value = None + self.php.side_effect = php_mock + + def tearDown(self): + """ mocking down """ + super(TestPFSenseInterfaceGroupModule, self).tearDown() + + self.php.stop() + + ############## + # tests utils + # + def get_target_elt(self, obj, absent=False, module_result=None): + """ get the generated interface group xml definition """ + elt_filter = {} + elt_filter['ifname'] = obj['name'] + + return self.assert_has_xml_tag('ifgroups', elt_filter, absent=absent) + + def check_target_elt(self, obj, target_elt): + """ test the xml definition of interface group """ + + # descr, members + if obj.get('descr'): + self.assert_xml_elt_equal(target_elt, 'descr', obj['descr']) + else: + self.assert_xml_elt_is_none_or_empty(target_elt, 'descr') + + if obj.get('members'): + self.assert_xml_elt_equal(target_elt, 'members', ' '.join(obj['members'])) + else: + self.assert_not_find_xml_elt(target_elt, 'members') + + ############## + # tests + # + def test_interface_group_create(self): + """ test creation of a new interface group """ + interface_group = dict(name='IFGROUP2', members=['wan', 'lan']) + command = "create interface_group 'IFGROUP2', members='wan lan'" + self.do_module_test(interface_group, command=command) + + def test_interface_group_create_with_descr(self): + """ test creation of a new interface group with a description """ + interface_group = dict(name='IFGROUP2', members=['wan', 'lan'], descr='Primary interfaces') + command = "create interface_group 'IFGROUP2', descr='Primary interfaces', members='wan lan'" + self.do_module_test(interface_group, command=command) + + def test_interface_group_delete(self): + """ test deletion of an interface group """ + interface_group = dict(name='IFGROUP1', state='absent') + command = "delete interface_group 'IFGROUP1'" + self.do_module_test(interface_group, delete=True, command=command) + + def test_interface_group_update_noop(self): + """ test not updating a interface group """ + interface_group = dict(name='IFGROUP1', members=['opt1', 'opt3']) + self.do_module_test(interface_group, changed=False) + + def test_interface_group_update_descr(self): + """ test updating interface group description """ + interface_group = dict(name='IFGROUP1', members=['opt1', 'opt3'], descr='Opt Interfaces') + command = "update interface_group 'IFGROUP1' set descr='Opt Interfaces'" + self.do_module_test(interface_group, changed=True, command=command) + + def test_interface_group_update_members(self): + """ test updating interface group members """ + interface_group = dict(name='IFGROUP1', members=['opt1', 'opt2']) + command = "update interface_group 'IFGROUP1' set members='opt1 opt2'" + self.do_module_test(interface_group, changed=True, command=command) + + def test_interface_group_error_no_members(self): + """ test error no members specified """ + interface_group = dict(name='IFGROUP2', descr='Primary interfaces') + msg = "state is present but all of the following are missing: members" + self.do_module_test(interface_group, failed=True, msg=msg) + + def test_interface_group_error_member_does_not_exist(self): + """ test error member does not exist """ + interface_group = dict(name='IFGROUP2', members=['blah'], descr='Primary interfaces') + msg = 'Unknown interface name "blah".' + self.do_module_test(interface_group, failed=True, msg=msg) + + def test_interface_group_error_members_not_uniq(self): + """ test error member does not exist """ + interface_group = dict(name='IFGROUP2', members=['opt1', 'opt1'], descr='Primary interfaces') + msg = 'List of members is not unique.' + self.do_module_test(interface_group, failed=True, msg=msg) diff --git a/tests/unit/plugins/modules/test_pfsense_ipsec.py b/tests/unit/plugins/modules/test_pfsense_ipsec.py index fa7319e7..d4dacd65 100644 --- a/tests/unit/plugins/modules/test_pfsense_ipsec.py +++ b/tests/unit/plugins/modules/test_pfsense_ipsec.py @@ -13,7 +13,6 @@ from ansible_collections.pfsensible.core.plugins.modules import pfsense_ipsec from ansible_collections.pfsensible.core.plugins.module_utils.ipsec import PFSenseIpsecModule from .pfsense_module import TestPFSenseModule -from parameterized import parameterized class TestPFSenseIpsecModule(TestPFSenseModule): diff --git a/tests/unit/plugins/modules/test_pfsense_log_settings.py b/tests/unit/plugins/modules/test_pfsense_log_settings.py index 3e3397fc..bfc11823 100644 --- a/tests/unit/plugins/modules/test_pfsense_log_settings.py +++ b/tests/unit/plugins/modules/test_pfsense_log_settings.py @@ -4,14 +4,12 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from copy import copy import pytest import sys if sys.version_info < (2, 7): pytestmark = pytest.mark.skip("pfSense Ansible modules require Python >= 2.7") -from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import set_module_args from ansible_collections.pfsensible.core.plugins.modules import pfsense_log_settings from .pfsense_module import TestPFSenseModule diff --git a/tests/unit/plugins/modules/test_pfsense_nat_outbound.py b/tests/unit/plugins/modules/test_pfsense_nat_outbound.py index 3af48110..7ac92dc8 100644 --- a/tests/unit/plugins/modules/test_pfsense_nat_outbound.py +++ b/tests/unit/plugins/modules/test_pfsense_nat_outbound.py @@ -35,28 +35,35 @@ def is_ipv4_address(address): pass return False - def parse_address(self, addr, field): + def parse_address(self, name, addr, field, invert=False): """ return address parsed in dict """ parts = addr.split(':') res = {} port = None - if parts[0] == 'any': - if field == 'network': - res[field] = 'any' - else: - res['any'] = None - elif parts[0] == '(self)': - res[field] = '(self)' - elif parts[0] in ['lan', 'vpn', 'vt1', 'lan_100']: - res[field] = self.unalias_interface(parts[0]) + if parts[0] == 'NET': + res[field] = parts[1] + if len(parts) > 2: + port = parts[2].replace('-', ':') else: - res[field] = parts[0] + if parts[0] == 'any': + if name == 'source': + res[field] = 'any' + else: + res['any'] = None + elif parts[0] == '(self)': + res[field] = '(self)' + elif parts[0] in ['lan', 'vpn', 'vt1', 'lan_100']: + res[field] = self.unalias_interface(parts[0]) + else: + res[field] = parts[0] - if field in res and self.is_ipv4_address(res[field]) and res[field].find('/') == -1: - res[field] += '/32' + if field in res and self.is_ipv4_address(res[field]) and res[field].find('/') == -1: + res[field] += '/32' - if len(parts) > 1: - port = parts[1].replace('-', ':') + if len(parts) > 1: + port = parts[1].replace('-', ':') + if invert: + res['not'] = None return (res, port) @@ -68,9 +75,9 @@ def reparse_network(value): return '2.3.4.0/24' return value - def check_addr(self, params, target_elt, addr, field, port): + def check_addr(self, params, target_elt, addr, field, port, invert=False): """ test the addresses definition """ - (addr_dict, port_value) = self.parse_address(params[addr], field) + (addr_dict, port_value) = self.parse_address(addr, params[addr], field, invert=invert) addr_elt = self.assert_find_xml_elt(target_elt, addr) for key, value in addr_dict.items(): self.check_value_equal(addr_elt, key, self.reparse_network(value)) @@ -107,12 +114,11 @@ def md5(value): def check_target_elt(self, obj, target_elt, target_idx=-1): """ test the xml definition """ self.check_addr(obj, target_elt, 'source', 'network', 'sourceport') - self.check_addr(obj, target_elt, 'destination', 'address', 'dstport') + self.check_addr(obj, target_elt, 'destination', 'network', 'dstport', invert=obj.get('invert')) self.check_target_addr(obj, target_elt) self.check_param_equal_or_not_find(obj, target_elt, 'disabled') self.check_param_equal_or_not_find(obj, target_elt, 'nonat') - self.check_param_equal_or_not_find(obj, target_elt, 'invert') self.check_param_equal_or_not_find(obj, target_elt, 'staticnatport') self.check_param_equal_or_not_find(obj, target_elt, 'nosync') self.check_param_equal_or_not_find(obj, target_elt, 'nonat') @@ -191,6 +197,24 @@ def test_nat_outbound_create_networks(self): command = "create nat_outbound 'https-source-rewriting', interface='lan', source='1.2.3.4/24', destination='2.3.4.5/24:443'" self.do_module_test(obj, command=command, target_idx=3) + def test_nat_outbound_create_networks_invert(self): + """ test """ + obj = dict(descr='https-source-rewriting', interface='lan', source='1.2.3.4/24', destination='2.3.4.5/24:443', invert=True) + command = "create nat_outbound 'https-source-rewriting', interface='lan', source='1.2.3.4/24', destination='2.3.4.5/24:443', invert=True" + self.do_module_test(obj, command=command, target_idx=3) + + def test_nat_outbound_create_interface_destination_network(self): + """ test """ + obj = dict(descr='https-source-rewriting', interface='lan', source='1.2.3.4/24', destination='NET:lan:443') + command = "create nat_outbound 'https-source-rewriting', interface='lan', source='1.2.3.4/24', destination='NET:lan:443'" + self.do_module_test(obj, command=command, target_idx=3) + + def test_nat_outbound_create_interface_source_network(self): + """ test """ + obj = dict(descr='https-source-rewriting', interface='lan', source='NET:lan', destination='2.3.4.5/24:443') + command = "create nat_outbound 'https-source-rewriting', interface='lan', source='NET:lan', destination='2.3.4.5/24:443'" + self.do_module_test(obj, command=command, target_idx=3) + def test_nat_outbound_create_top(self): """ test """ obj = dict(descr='https-source-rewriting', interface='lan', source='any', destination='1.2.3.4:443', after='top') diff --git a/tests/unit/plugins/modules/test_pfsense_openvpn_override.py b/tests/unit/plugins/modules/test_pfsense_openvpn_override.py index a0001868..09665992 100644 --- a/tests/unit/plugins/modules/test_pfsense_openvpn_override.py +++ b/tests/unit/plugins/modules/test_pfsense_openvpn_override.py @@ -10,10 +10,8 @@ if sys.version_info < (2, 7): pytestmark = pytest.mark.skip("pfSense Ansible modules require Python >= 2.7") -from xml.etree.ElementTree import fromstring, ElementTree -from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch from ansible_collections.pfsensible.core.plugins.modules import pfsense_openvpn_override -from .pfsense_module import TestPFSenseModule, load_fixture +from .pfsense_module import TestPFSenseModule class TestPFSenseOpenVPNOverrideModule(TestPFSenseModule): @@ -76,7 +74,7 @@ def test_openvpn_override_update_noop(self): def test_openvpn_override_update_network(self): """ test updating network of a OpenVPN override """ - obj = dict(name='delvpnuser', gwredir=True, server_list=1, custom_options='ifconfig-push 10.8.0.1 255.255.255.0', tunnel_network='10.10.10.0/24') + obj = dict(name='delvpnuser', gwredir=True, server_list=1, custom_options='ifconfig-push 10.8.0.1 255.255.255.0', tunnel_network='10.10.10.10/24') self.do_module_test(obj, command="update openvpn_override 'delvpnuser' set ") ############## diff --git a/tests/unit/plugins/modules/test_pfsense_openvpn_server.py b/tests/unit/plugins/modules/test_pfsense_openvpn_server.py index 7974fd1f..4c92060e 100644 --- a/tests/unit/plugins/modules/test_pfsense_openvpn_server.py +++ b/tests/unit/plugins/modules/test_pfsense_openvpn_server.py @@ -4,16 +4,16 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +import base64 import pytest import sys if sys.version_info < (2, 7): pytestmark = pytest.mark.skip("pfSense Ansible modules require Python >= 2.7") -from xml.etree.ElementTree import fromstring, ElementTree -from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch from ansible_collections.pfsensible.core.plugins.modules import pfsense_openvpn_server -from .pfsense_module import TestPFSenseModule, load_fixture +from .pfsense_module import TestPFSenseModule +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch CERTIFICATE = ( "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlFQ0RDQ0F2Q2dBd0lCQWdJSUZqRk9oczFuTXpRd0RRWUpLb1pJaHZjTkFRRUxCUUF3WERFVE1CRUdBMVVFDQpBeE1LYjNCbGJuWndiaTFqWVRF" @@ -49,6 +49,21 @@ def __init__(self, *args, **kwargs): self.config_file = 'pfsense_openvpn_config.xml' self.pfmodule = pfsense_openvpn_server.PFSenseOpenVPNServerModule + def setUp(self): + """ mocking up """ + + super(TestPFSenseOpenVPNServerModule, self).setUp() + + self.mock_run_command = patch('ansible.module_utils.basic.AnsibleModule.run_command') + self.run_command = self.mock_run_command.start() + self.run_command.return_value = (0, base64.b64decode(TLSKEY.encode()).decode(), '') + + def tearDown(self): + """ mocking down """ + super(TestPFSenseOpenVPNServerModule, self).tearDown() + + self.run_command.stop() + @staticmethod def runTest(): """ dummy function needed to instantiate this test module from another in python 2.7 """ @@ -89,6 +104,12 @@ def certref(descr): def check_target_elt(self, obj, target_elt): """ check XML definition of target elt """ + # Use "generated" key + if 'shared_key' in obj and obj['shared_key'] == 'generate': + obj['shared_key'] = TLSKEY + if 'tls' in obj and obj['tls'] == 'generate': + obj['tls'] = TLSKEY + self.check_param_equal(obj, target_elt, 'name', xml_field='description') self.check_param_equal(obj, target_elt, 'custom_options') self.check_param_equal(obj, target_elt, 'mode', default='ptp_tls') @@ -102,6 +123,7 @@ def check_target_elt(self, obj, target_elt): self.check_param_equal(obj, target_elt, 'local_port', default=1194) self.check_param_equal(obj, target_elt, 'protocol', default='UDP4') if 'tls' in obj['mode']: + self.check_param_equal(obj, target_elt, 'tls') self.check_param_equal(obj, target_elt, 'tls') self.check_param_equal(obj, target_elt, 'tls_type') self.assert_xml_elt_equal(target_elt, 'caref', self.caref(obj['ca'])) @@ -143,6 +165,11 @@ def test_openvpn_server_create(self): obj = dict(name='ovpns3', mode='p2p_tls', ca='OpenVPN CA', local_port=1196) self.do_module_test(obj, command="create openvpn_server 'ovpns3', description='ovpns3'") + def test_openvpn_server_create_generate(self): + """ test creation of a new OpenVPN server """ + obj = dict(name='ovpns3', mode='p2p_tls', ca='OpenVPN CA', local_port=1196, tls='generate') + self.do_module_test(obj, command="create openvpn_server 'ovpns3', description='ovpns3'") + def test_openvpn_server_delete(self): """ test deletion of a OpenVPN server """ obj = dict(name='ovpns2') diff --git a/tests/unit/plugins/modules/test_pfsense_rule.py b/tests/unit/plugins/modules/test_pfsense_rule.py index cdf12614..90bfbbe8 100644 --- a/tests/unit/plugins/modules/test_pfsense_rule.py +++ b/tests/unit/plugins/modules/test_pfsense_rule.py @@ -10,11 +10,9 @@ if sys.version_info < (2, 7): pytestmark = pytest.mark.skip("pfSense Ansible modules require Python >= 2.7") -from xml.etree.ElementTree import fromstring, ElementTree -from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch from ansible_collections.pfsensible.core.plugins.modules import pfsense_rule from ansible_collections.pfsensible.core.plugins.module_utils.rule import PFSenseRuleModule -from .pfsense_module import TestPFSenseModule, load_fixture +from .pfsense_module import TestPFSenseModule class TestPFSenseRuleModule(TestPFSenseModule): diff --git a/tests/unit/plugins/modules/test_pfsense_rule_create.py b/tests/unit/plugins/modules/test_pfsense_rule_create.py index 3855a37a..fd2fa61d 100644 --- a/tests/unit/plugins/modules/test_pfsense_rule_create.py +++ b/tests/unit/plugins/modules/test_pfsense_rule_create.py @@ -66,6 +66,17 @@ def test_rule_create_floating(self): command = "create rule 'one_rule' on 'floating(lan)', source='any', destination='any', direction='any'" self.do_module_test(obj, command=command) + def test_rule_create_floating_any(self): + """ test creation of a new floating rule with any interface """ + obj = dict(name='one_rule', source='any', destination='any', interface='any', floating='yes', direction='any') + command = "create rule 'one_rule' on 'floating(any)', source='any', destination='any', direction='any'" + + def test_rule_create_non_floating_any(self): + """ test creation of a new rule with any interface """ + obj = dict(name='one_rule', source='any', destination='any', interface='any', floating='no', direction='any') + msg = "any is not a valid interface" + self.do_module_test(obj, failed=True, msg=msg) + def test_rule_create_floating_quick(self): """ test creation of a new floating rule with quick match """ obj = dict(name='one_rule', source='any', destination='any', interface='lan', floating='yes', direction='any', quick='yes') diff --git a/tests/unit/plugins/modules/test_pfsense_user.py b/tests/unit/plugins/modules/test_pfsense_user.py index a71cf469..535df690 100644 --- a/tests/unit/plugins/modules/test_pfsense_user.py +++ b/tests/unit/plugins/modules/test_pfsense_user.py @@ -10,10 +10,8 @@ if sys.version_info < (2, 7): pytestmark = pytest.mark.skip("pfSense Ansible modules require Python >= 2.7") -from xml.etree.ElementTree import fromstring, ElementTree -from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch from ansible_collections.pfsensible.core.plugins.modules import pfsense_user -from .pfsense_module import TestPFSenseModule, load_fixture +from .pfsense_module import TestPFSenseModule class TestPFSenseUserModule(TestPFSenseModule):