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 ")
##############