diff --git a/galaxy.yml b/galaxy.yml index 9a965f83..d902b95a 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 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/interface.py b/plugins/module_utils/interface.py index f2bfb61a..b2942acd 100644 --- a/plugins/module_utils/interface.py +++ b/plugins/module_utils/interface.py @@ -147,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): @@ -214,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 """ @@ -229,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): @@ -322,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_pre_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 """ diff --git a/plugins/module_utils/interface_group.py b/plugins/module_utils/interface_group.py index bffd4b07..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} @@ -78,8 +82,9 @@ def _validate_params(self): 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 len(params['members']) > len(set(params['members'])): - self.module.fail_json(msg='List of members is not unique.') + 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 ############################## @@ -183,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/openvpn_client.py b/plugins/module_utils/openvpn_client.py index 855c6211..6bfba7d0 100644 --- a/plugins/module_utils/openvpn_client.py +++ b/plugins/module_utils/openvpn_client.py @@ -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') @@ -250,6 +257,7 @@ def _find_target(self): 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 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 073d7dc1..403369e2 100644 --- a/plugins/module_utils/openvpn_server.py +++ b/plugins/module_utils/openvpn_server.py @@ -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') @@ -321,6 +328,7 @@ def _find_target(self): 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 @@ -331,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 366011e3..b8c2225d 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 @@ -342,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(): 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_openvpn_client.py b/plugins/modules/pfsense_openvpn_client.py index 790d967c..360775cc 100644 --- a/plugins/modules/pfsense_openvpn_client.py +++ b/plugins/modules/pfsense_openvpn_client.py @@ -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 de14159a..91047b59 100644 --- a/plugins/modules/pfsense_openvpn_server.py +++ b/plugins/modules/pfsense_openvpn_server.py @@ -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/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/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/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_openvpn_override.py b/tests/unit/plugins/modules/test_pfsense_openvpn_override.py index 530d1f2f..09665992 100644 --- a/tests/unit/plugins/modules/test_pfsense_openvpn_override.py +++ b/tests/unit/plugins/modules/test_pfsense_openvpn_override.py @@ -74,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 ") ##############