From c6933dda4d138e9f9e2c2decd7ef7576e82d0479 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Wed, 10 Jan 2024 22:17:12 -0700 Subject: [PATCH 01/62] [module_base] Refactor - Allow for managing root_elt, elements - Add _find_this_element_index - Handle no existing elements - Implement _create_target and _find_target for node --- plugins/module_utils/module_base.py | 45 ++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/module_base.py b/plugins/module_utils/module_base.py index 996ec9ae..5e903ea9 100644 --- a/plugins/module_utils/module_base.py +++ b/plugins/module_utils/module_base.py @@ -21,7 +21,7 @@ def get_argument_spec(): ############################## # init # - def __init__(self, module, pfsense=None): + def __init__(self, module, pfsense=None, root=None, node=None, create_node=False): if pfsense is None: pfsense = PFSenseModule(module) self.module = module # ansible module @@ -31,9 +31,25 @@ def __init__(self, module, pfsense=None): self.pfsense = pfsense # helper module self.apply = True # apply configuration at the end + # xml parent of target_elt, node named by root + if root is not None: + if root == 'pfsense': + self.root_elt = self.pfsense.root + else: + self.root_elt = self.pfsense.get_element(root, create_node=create_node) + else: + self.root_elt = None + self.root = root + + # List of elements named node + if node is not None: + self.elements = self.pfsense.get_elements(node) + else: + self.elememts = None + self.node = node + self.obj = None # dict holding target pfsense parameters self.target_elt = None # xml object holding target pfsense parameters - self.root_elt = None # xml parent of target_elt self.change_descr = '' @@ -136,11 +152,32 @@ def _copy_and_update_target(self): def _create_target(self): """ create the XML target_elt """ - raise NotImplementedError() + if self.node is not None: + return self.pfsense.new_element(self.node) + else: + raise NotImplementedError() + + def _find_this_element_index(self): + return self.elements.index(self.target_elt) + + def _find_last_element_index(self): + if len(self.elements): + return list(self.root_elt).index(self.elements[len(self.elements) - 1]) + else: + return len(list(self.root_elt)) def _find_target(self): """ find the XML target_elt """ - raise NotImplementedError() + if self.node is not None: + result = self.root_elt.findall("{node}[descr='{descr}']".format(node=self.node, descr=self.obj['descr'])) + if len(result) == 1: + return result[0] + elif len(result) > 1: + self.module.fail_json(msg='Found multiple {node}s for descr {descr}.'.format(node=self.node, descr=self.obj['descr'])) + else: + return None + else: + raise NotImplementedError() @staticmethod def _get_params_to_remove(): From 5cf4c74b4aee2ed8d716129093e507a3e3f9b3a1 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Wed, 10 Jan 2024 22:17:50 -0700 Subject: [PATCH 02/62] [pfsense_cert] Use expanded PFSenseModuleBase --- plugins/modules/pfsense_cert.py | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/plugins/modules/pfsense_cert.py b/plugins/modules/pfsense_cert.py index 0dca97cc..bd37c837 100644 --- a/plugins/modules/pfsense_cert.py +++ b/plugins/modules/pfsense_cert.py @@ -207,10 +207,8 @@ def get_argument_spec(): # init # def __init__(self, module, pfsense=None): - super(PFSenseCertModule, self).__init__(module, pfsense) + super(PFSenseCertModule, self).__init__(module, pfsense, root='pfsense', node='cert') self.name = "pfsense_cert" - self.root_elt = self.pfsense.root - self.certs = self.pfsense.get_elements('cert') ############################## # params processing @@ -275,21 +273,6 @@ def _params_to_obj(self): ############################## # XML processing # - def _find_target(self): - result = self.root_elt.findall("cert[descr='{0}']".format(self.obj['descr'])) - if len(result) == 1: - return result[0] - elif len(result) > 1: - self.module.fail_json(msg='Found multiple certificates for descr {0}.'.format(self.obj['descr'])) - else: - return None - - def _find_this_cert_index(self): - return self.certs.index(self.target_elt) - - def _find_last_cert_index(self): - return list(self.root_elt).index(self.certs[len(self.certs) - 1]) - def _find_ca(self, caref): result = self.root_elt.findall("ca[descr='{0}']".format(caref)) if len(result) == 1: @@ -305,10 +288,6 @@ def _find_ca(self, caref): else: return None - def _create_target(self): - """ create the XML target_elt """ - return self.pfsense.new_element('cert') - def _copy_and_add_target(self): """ populate the XML target_elt """ obj = self.obj @@ -316,9 +295,7 @@ def _copy_and_add_target(self): obj['refid'] = self.pfsense.uniqid() self.diff['after'] = obj self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - self.root_elt.insert(self._find_last_cert_index(), self.target_elt) - # Reset certs list - self.certs = self.root_elt.findall('cert') + self.root_elt.insert(self._find_last_element_index(), self.target_elt) def _copy_and_update_target(self): """ update the XML target_elt """ @@ -439,7 +416,7 @@ def _pre_remove_target_elt(self): self.diff['after'] = {} if self.target_elt is not None: self.diff['before'] = self.pfsense.element_to_dict(self.target_elt) - self.certs.remove(self.target_elt) + self.elements.remove(self.target_elt) else: self.diff['before'] = {} From 4a67cda9bb830f84ca933cc4b05652641e2efda6 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Thu, 11 Jan 2024 11:13:30 -0700 Subject: [PATCH 03/62] [pfsense_ca] Use expanded PFSenseModuleBase --- plugins/modules/pfsense_ca.py | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/plugins/modules/pfsense_ca.py b/plugins/modules/pfsense_ca.py index 9cdd1611..6795593f 100644 --- a/plugins/modules/pfsense_ca.py +++ b/plugins/modules/pfsense_ca.py @@ -122,10 +122,8 @@ def get_argument_spec(): return PFSENSE_CA_ARGUMENT_SPEC def __init__(self, module, pfsense=None): - super(PFSenseCAModule, self).__init__(module, pfsense) + super(PFSenseCAModule, self).__init__(module, pfsense, root='pfsense', node='ca') self.name = "pfsense_ca" - self.root_elt = self.pfsense.root - self.cas = self.pfsense.get_elements('ca') self.refresh_crls = False self.crl = None @@ -186,24 +184,6 @@ def _params_to_obj(self): ############################## # XML processing # - def _find_target(self): - result = self.root_elt.findall("ca[descr='{0}']".format(self.obj['descr'])) - if len(result) == 1: - return result[0] - elif len(result) > 1: - self.module.fail_json(msg='Found multiple certificate authorities for name {0}.'.format(self.obj['descr'])) - else: - return None - - def _find_this_ca_index(self): - return self.cas.index(self.target_elt) - - def _find_last_ca_index(self): - if len(self.cas): - return list(self.root_elt).index(self.cas[len(self.cas) - 1]) - else: - return len(list(self.root_elt)) - def _find_crl_for_ca(self, caref): result = self.root_elt.findall("crl[caref='{0}']".format(caref)) if len(result) == 1: @@ -247,7 +227,7 @@ def _copy_and_add_target(self): """ populate the XML 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) + self.root_elt.insert(self._find_last_element_index(), self.target_elt) if self.crl is not None: crl_elt = self.pfsense.new_element('crl') self.crl['caref'] = self.obj['refid'] @@ -283,7 +263,7 @@ def _copy_and_update_target(self): self.crl['refid'] = self.pfsense.uniqid() self.pfsense.copy_dict_to_element(self.crl, crl_elt) # Add after the existing ca entry - self.pfsense.root.insert(self._find_this_ca_index() + 1, crl_elt) + self.pfsense.root.insert(self._find_this_element_index() + 1, crl_elt) self.refresh_crls = True else: before['crl'] = crl_elt.find('text').text @@ -355,7 +335,7 @@ def _pre_remove_target_elt(self): if self.target_elt is not None: self.diff['before'] = self.pfsense.element_to_dict(self.target_elt) crl_elt = self._find_crl_for_ca(self.target_elt.find('refid').text) - self.cas.remove(self.target_elt) + self.elements.remove(self.target_elt) if crl_elt is not None: self.diff['before']['crl'] = crl_elt.find('text').text self.root_elt.remove(crl_elt) From d7b929c3983402b02409962c78ca9c7b1375dffe Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Thu, 11 Jan 2024 12:17:42 -0700 Subject: [PATCH 04/62] [module_base] Allow setting root_is_exclusive; self.obj default to {}; _find_target only for root_is_exclusive == False --- plugins/module_utils/module_base.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/module_base.py b/plugins/module_utils/module_base.py index 5e903ea9..6b2f35ea 100644 --- a/plugins/module_utils/module_base.py +++ b/plugins/module_utils/module_base.py @@ -21,7 +21,7 @@ def get_argument_spec(): ############################## # init # - def __init__(self, module, pfsense=None, root=None, node=None, create_node=False): + def __init__(self, module, pfsense=None, root=None, root_is_exclusive=True, create_root=False, node=None): if pfsense is None: pfsense = PFSenseModule(module) self.module = module # ansible module @@ -35,10 +35,16 @@ def __init__(self, module, pfsense=None, root=None, node=None, create_node=False if root is not None: if root == 'pfsense': self.root_elt = self.pfsense.root + self.root_is_exclusive = False else: - self.root_elt = self.pfsense.get_element(root, create_node=create_node) + self.root_elt = self.pfsense.get_element(root, create_node=create_root) + if root in ['system']: + self.root_is_exclusive = False + else: + self.root_is_exclusive = root_is_exclusive else: self.root_elt = None + self.root_is_exclusive = None self.root = root # List of elements named node @@ -48,7 +54,7 @@ def __init__(self, module, pfsense=None, root=None, node=None, create_node=False self.elememts = None self.node = node - self.obj = None # dict holding target pfsense parameters + self.obj = dict() # dict holding target pfsense parameters self.target_elt = None # xml object holding target pfsense parameters self.change_descr = '' @@ -168,7 +174,7 @@ def _find_last_element_index(self): def _find_target(self): """ find the XML target_elt """ - if self.node is not None: + if self.root_is_exclusive is False and self.node is not None: result = self.root_elt.findall("{node}[descr='{descr}']".format(node=self.node, descr=self.obj['descr'])) if len(result) == 1: return result[0] From fe33ad8e22beee99ca85d6ba07ab9cd648cdef05 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Thu, 11 Jan 2024 12:19:13 -0700 Subject: [PATCH 05/62] [pfsense_alias] Use expanded PFSenseModuleBase --- plugins/module_utils/alias.py | 36 ++----------------- .../plugins/modules/test_pfsense_alias.py | 6 ++-- .../modules/test_pfsense_alias_null.py | 2 +- 3 files changed, 6 insertions(+), 38 deletions(-) diff --git a/plugins/module_utils/alias.py b/plugins/module_utils/alias.py index d9d2f66d..960904d8 100644 --- a/plugins/module_utils/alias.py +++ b/plugins/module_utils/alias.py @@ -37,10 +37,8 @@ def get_argument_spec(): # init # def __init__(self, module, pfsense=None): - super(PFSenseAliasModule, self).__init__(module, pfsense) + super(PFSenseAliasModule, self).__init__(module, pfsense, root='aliases', node='alias') self.name = "pfsense_alias" - self.root_elt = self.pfsense.get_element('aliases') - self.obj = dict() ############################## # params processing @@ -103,43 +101,13 @@ def _validate_params(self): ############################## # XML processing # - 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 """ - target_elt = self.pfsense.new_element('alias') - self.diff['before'] = '' - self.diff['after'] = self.obj - self.result['changed'] = True - return target_elt - def _find_target(self): """ find the XML target_elt """ - return self.pfsense.find_alias(self.obj['name']) + return self.pfsense.find_alias(self.obj['name'], aliastype=self.obj.get('type')) ############################## # run # - def _remove(self): - """ delete obj """ - self.diff['after'] = '' - self.diff['before'] = '' - super(PFSenseAliasModule, self)._remove() - def _update(self): """ make the target pfsense reload """ return self.pfsense.phpshell('''require_once("filter.inc"); diff --git a/tests/unit/plugins/modules/test_pfsense_alias.py b/tests/unit/plugins/modules/test_pfsense_alias.py index fe1e2b28..ed8b88a6 100644 --- a/tests/unit/plugins/modules/test_pfsense_alias.py +++ b/tests/unit/plugins/modules/test_pfsense_alias.py @@ -38,7 +38,7 @@ def do_alias_creation_test(self, alias, failed=False, msg='', command=None): result = self.execute_module(changed=True, failed=failed, msg=msg) if not failed: - diff = dict(before='', after=alias) + diff = dict(before={}, after=alias) self.assertEqual(result['diff'], diff) self.assert_xml_elt_dict('aliases', dict(name=alias['name'], type=alias['type']), diff['after']) self.assertEqual(result['commands'], [command]) @@ -50,7 +50,7 @@ def do_alias_deletion_test(self, alias, command=None): set_module_args(self.args_from_var(alias, 'absent')) result = self.execute_module(changed=True) - diff = dict(before=alias, after='') + diff = dict(before=alias, after={}) self.assertEqual(result['diff'], diff) self.assert_has_xml_tag('aliases', dict(name=alias['name'], type=alias['type']), absent=True) self.assertEqual(result['commands'], [command]) @@ -277,7 +277,7 @@ def test_delete_inexistent_alias(self): set_module_args(self.args_from_var(alias, 'absent')) result = self.execute_module(changed=False) - diff = dict(before='', after='') + diff = dict(before={}, after={}) self.assertEqual(result['diff'], diff) self.assertEqual(result['commands'], []) diff --git a/tests/unit/plugins/modules/test_pfsense_alias_null.py b/tests/unit/plugins/modules/test_pfsense_alias_null.py index 5be91a7b..ad956b79 100644 --- a/tests/unit/plugins/modules/test_pfsense_alias_null.py +++ b/tests/unit/plugins/modules/test_pfsense_alias_null.py @@ -38,7 +38,7 @@ def do_alias_creation_test(self, alias, failed=False, msg='', command=None): result = self.execute_module(changed=True, failed=failed, msg=msg) if not failed: - diff = dict(before='', after=alias) + diff = dict(before={}, after=alias) self.assertEqual(result['diff'], diff) self.assert_xml_elt_dict('aliases', dict(name=alias['name'], type=alias['type']), diff['after']) self.assertEqual(result['commands'], [command]) From dbc449d90b0df73a48d6181c1cc3e083a3c5c752 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Thu, 11 Jan 2024 12:45:29 -0700 Subject: [PATCH 06/62] [module_base] Add key, root_is_exclusive defaults to True; root_is_exclusive affects _copy_and_add_target(); _get_obj_name() uses key --- plugins/module_utils/module_base.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/module_base.py b/plugins/module_utils/module_base.py index 6b2f35ea..da91eea2 100644 --- a/plugins/module_utils/module_base.py +++ b/plugins/module_utils/module_base.py @@ -21,7 +21,7 @@ def get_argument_spec(): ############################## # init # - def __init__(self, module, pfsense=None, root=None, root_is_exclusive=True, create_root=False, node=None): + def __init__(self, module, pfsense=None, root=None, root_is_exclusive=True, create_root=False, node=None, key='descr'): if pfsense is None: pfsense = PFSenseModule(module) self.module = module # ansible module @@ -44,16 +44,17 @@ def __init__(self, module, pfsense=None, root=None, root_is_exclusive=True, crea self.root_is_exclusive = root_is_exclusive else: self.root_elt = None - self.root_is_exclusive = None + self.root_is_exclusive = root_is_exclusive self.root = root # List of elements named node if node is not None: self.elements = self.pfsense.get_elements(node) else: - self.elememts = None + self.elements = None self.node = node + self.key = key # item that identifies a target element self.obj = dict() # dict holding target pfsense parameters self.target_elt = None # xml object holding target pfsense parameters @@ -143,7 +144,10 @@ def _copy_and_add_target(self): """ create the XML target_elt """ self.pfsense.copy_dict_to_element(self.obj, self.target_elt) self.diff['after'] = self.obj - self.root_elt.append(self.target_elt) + if self.root_is_exclusive: + self.root_elt.append(self.target_elt) + else: + self.root_elt.insert(self._find_last_element_index(), self.target_elt) def _copy_and_update_target(self): """ update the XML target_elt """ @@ -174,12 +178,12 @@ def _find_last_element_index(self): def _find_target(self): """ find the XML target_elt """ - if self.root_is_exclusive is False and self.node is not None: - result = self.root_elt.findall("{node}[descr='{descr}']".format(node=self.node, descr=self.obj['descr'])) + if self.node is not None: + result = self.root_elt.findall("{node}[{key}='{value}']".format(node=self.node, key=self.key, value=self.obj[self.key])) if len(result) == 1: return result[0] elif len(result) > 1: - self.module.fail_json(msg='Found multiple {node}s for descr {descr}.'.format(node=self.node, descr=self.obj['descr'])) + self.module.fail_json(msg='Found multiple {node}s for {key} {value}.'.format(node=self.node, key=self.key, value=self.obj[self.key])) else: return None else: @@ -319,7 +323,7 @@ def _log_update(self, before): def _get_obj_name(self): """ return obj's name """ - raise NotImplementedError() + return "'{0}'".format(self.obj[self.key]) def _get_module_name(self, strip=False): """ return ansible module's name """ From d7ca82f879fbd40781d32f3eeaa4a019158dfaea Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Thu, 11 Jan 2024 12:46:20 -0700 Subject: [PATCH 07/62] [pfsense_interface_group] Use expanded PFSenseModuleBase --- plugins/module_utils/interface_group.py | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/plugins/module_utils/interface_group.py b/plugins/module_utils/interface_group.py index 76457e24..9cf9f255 100644 --- a/plugins/module_utils/interface_group.py +++ b/plugins/module_utils/interface_group.py @@ -38,11 +38,8 @@ def get_argument_spec(): # init # def __init__(self, module, pfsense=None): - super(PFSenseInterfaceGroupModule, self).__init__(module, pfsense) + super(PFSenseInterfaceGroupModule, self).__init__(module, pfsense, root='ifgroups', node='ifgroupentry', key='ifname') self.name = "pfsense_interface_group" - self.obj = dict() - - self.root_elt = self.pfsense.get_element('ifgroups') ############################## # params processing @@ -90,26 +87,6 @@ def _validate_params(self): ############################## # XML processing # - def _create_target(self): - """ create the XML target_elt """ - self.diff['before'] = '' - self.diff['after'] = self.obj - return self.pfsense.new_element('ifgroupentry') - - def _find_target(self): - """ find the XML 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 """ - self.diff['before'] = self.pfsense.element_to_dict(self.target_elt) - def _remove_all_rules(self, interface): """ delete all interface rules """ From f9185d9792c3a7b18371bd6e07a3acd186540964 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Thu, 11 Jan 2024 18:54:09 -0700 Subject: [PATCH 08/62] [pfsense_default_gateway] Use PFSenseModuleBase root --- plugins/module_utils/default_gateway.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/module_utils/default_gateway.py b/plugins/module_utils/default_gateway.py index eeb7e58f..4fa4d3cf 100644 --- a/plugins/module_utils/default_gateway.py +++ b/plugins/module_utils/default_gateway.py @@ -29,11 +29,9 @@ def get_argument_spec(): # init # def __init__(self, module, pfsense=None): - super(PFSenseDefaultGatewayModule, self).__init__(module, pfsense) + super(PFSenseDefaultGatewayModule, self).__init__(module, pfsense, root='gateways') 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 From bb235e163cae39a210b35b44c245d4691800dc2d Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Thu, 11 Jan 2024 19:07:12 -0700 Subject: [PATCH 09/62] [pfsense_gateway] Use expanded PFSenseModuleBase --- plugins/module_utils/gateway.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/plugins/module_utils/gateway.py b/plugins/module_utils/gateway.py index 922ac6a8..0761970c 100644 --- a/plugins/module_utils/gateway.py +++ b/plugins/module_utils/gateway.py @@ -41,17 +41,11 @@ def get_argument_spec(): # init # def __init__(self, module, pfsense=None): - super(PFSenseGatewayModule, self).__init__(module, pfsense) + super(PFSenseGatewayModule, self).__init__(module, pfsense, root='gateways', create_root=True, node='gateway_item', key='name') self.name = "pfsense_gateway" - self.root_elt = self.pfsense.get_element('gateways') - self.obj = dict() self.interface_elt = None self.dynamic = False - if self.root_elt is None: - self.root_elt = self.pfsense.new_element('gateways') - self.pfsense.root.append(self.root_elt) - ############################## # params processing # @@ -187,14 +181,6 @@ def _validate_params(self): ############################## # XML processing # - def _create_target(self): - """ create the XML target_elt """ - return self.pfsense.new_element('gateway_item') - - def _find_target(self): - """ find the XML target_elt """ - return self.pfsense.find_gateway_elt(self.obj['name']) - @staticmethod def _get_params_to_remove(): """ returns the list of params to remove if they are not set """ From 22e40a8a830aa318dd62ad111e7cf9ac46f56358 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Thu, 11 Jan 2024 19:13:02 -0700 Subject: [PATCH 10/62] [pfsense_interface_group] Drop _get_obj_name() --- plugins/module_utils/interface_group.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/module_utils/interface_group.py b/plugins/module_utils/interface_group.py index 9cf9f255..43d296ed 100644 --- a/plugins/module_utils/interface_group.py +++ b/plugins/module_utils/interface_group.py @@ -154,10 +154,6 @@ def _update(self): ############################## # Logging # - def _get_obj_name(self): - """ return obj's name """ - return "'{0}'".format(self.obj['ifname']) - def _log_fields(self, before=None): """ generate pseudo-CLI command fields parameters to create an obj """ values = '' From af6a85bec966a145619d7f1d236da9d838c7b9f3 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Thu, 11 Jan 2024 20:01:57 -0700 Subject: [PATCH 11/62] [module_base] set name; allow update_php code --- plugins/module_utils/module_base.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/module_base.py b/plugins/module_utils/module_base.py index da91eea2..a2981095 100644 --- a/plugins/module_utils/module_base.py +++ b/plugins/module_utils/module_base.py @@ -21,11 +21,16 @@ def get_argument_spec(): ############################## # init # - def __init__(self, module, pfsense=None, root=None, root_is_exclusive=True, create_root=False, node=None, key='descr'): + def __init__(self, module, pfsense=None, name=None, root=None, root_is_exclusive=True, create_root=False, node=None, key='descr', update_php=None): if pfsense is None: pfsense = PFSenseModule(module) self.module = module # ansible module - self.name = None # ansible module name + if name is not None: # ansible module name + self.name = name + elif node is not None: + self.name = 'pfsense_' + node + else: + self.name = None self.params = None # ansible input parameters self.pfsense = pfsense # helper module @@ -58,6 +63,8 @@ def __init__(self, module, pfsense=None, root=None, root_is_exclusive=True, crea self.obj = dict() # dict holding target pfsense parameters self.target_elt = None # xml object holding target pfsense parameters + self.update_php = update_php # php code to update configuration + self.change_descr = '' self.result = {} @@ -269,10 +276,12 @@ def _pre_update(): """ tasks to run before making config changes """ return ('', '', '') - @staticmethod - def _update(): + def _update(self): """ make the target pfsense reload """ - return ('', '', '') + if self.update_php is not None: + return self.pfsense.phpshell(self.update_php) + else: + return ('', '', '') def run(self, params): """ process input params to add/update/delete """ From 2397e58922db752739ec4b39811093aa17b3e26c Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Thu, 11 Jan 2024 20:02:49 -0700 Subject: [PATCH 12/62] [pfsense_alias] auto set name; drop _find_target, _update --- plugins/module_utils/alias.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/plugins/module_utils/alias.py b/plugins/module_utils/alias.py index 960904d8..a215e1bf 100644 --- a/plugins/module_utils/alias.py +++ b/plugins/module_utils/alias.py @@ -24,6 +24,11 @@ ["type", "urltable_ports", ["updatefreq"]], ] +ALIAS_PHP_COMMAND_SET = """ +require_once("filter.inc"); +if (filter_configure() == 0) { clear_subsystem_dirty('aliases'); } +""" + class PFSenseAliasModule(PFSenseModuleBase): """ module managing pfsense aliases """ @@ -37,8 +42,7 @@ def get_argument_spec(): # init # def __init__(self, module, pfsense=None): - super(PFSenseAliasModule, self).__init__(module, pfsense, root='aliases', node='alias') - self.name = "pfsense_alias" + super(PFSenseAliasModule, self).__init__(module, pfsense, root='aliases', node='alias', key='name', update_php=ALIAS_PHP_COMMAND_SET) ############################## # params processing @@ -98,21 +102,6 @@ def _validate_params(self): if detail.startswith('|') or detail.endswith('|'): self.module.fail_json(msg='Vertical bars (|) at start or end of descriptions not allowed') - ############################## - # XML processing - # - def _find_target(self): - """ find the XML target_elt """ - return self.pfsense.find_alias(self.obj['name'], aliastype=self.obj.get('type')) - - ############################## - # run - # - def _update(self): - """ make the target pfsense reload """ - return self.pfsense.phpshell('''require_once("filter.inc"); -if (filter_configure() == 0) { clear_subsystem_dirty('aliases'); }''') - ############################## # Logging # From e9370d40108dde3d808926a44495f4a9685b8c94 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Fri, 12 Jan 2024 08:43:19 -0700 Subject: [PATCH 13/62] [module_base] Implement basic _params_to_obj() --- plugins/module_utils/module_base.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/module_base.py b/plugins/module_utils/module_base.py index a2981095..2218cbf7 100644 --- a/plugins/module_utils/module_base.py +++ b/plugins/module_utils/module_base.py @@ -21,10 +21,13 @@ def get_argument_spec(): ############################## # init # - def __init__(self, module, pfsense=None, name=None, root=None, root_is_exclusive=True, create_root=False, node=None, key='descr', update_php=None): + def __init__(self, module, pfsense=None, name=None, root=None, root_is_exclusive=True, create_root=False, node=None, key='descr', update_php=None, + map_param_if=None, param_force=None): if pfsense is None: pfsense = PFSenseModule(module) self.module = module # ansible module + self.argument_spec = module.argument_spec # Allow for being overrriden for use with aggregate + if name is not None: # ansible module name self.name = name elif node is not None: @@ -61,6 +64,8 @@ def __init__(self, module, pfsense=None, name=None, root=None, root_is_exclusive self.key = key # item that identifies a target element self.obj = dict() # dict holding target pfsense parameters + self.map_param_if = map_param_if # rules for mapping parameters + self.param_force = param_force # parameters that are forced to be present self.target_elt = None # xml object holding target pfsense parameters self.update_php = update_php # php code to update configuration @@ -106,10 +111,24 @@ def _get_ansible_param_bool(self, obj, name, fname=None, force=False, value='yes elif force: obj[fname] = value_false - @staticmethod - def _params_to_obj(): + def _params_to_obj(self): """ return a dict from module params """ - raise NotImplementedError() + obj = dict() + obj[self.key] = self.params[self.key] + if self.params.get('state') == 'present': + for param in [n for n in self.argument_spec.keys() if n != 'state']: + force = False + if param in self.param_force: + force = True + self._get_ansible_param(obj, param, force=force) + + for map_param, map_value, map_tuple in self.map_param_if: + if self.params.get(map_param) == map_value and map_tuple[0] in obj: + if map_tuple[1] not in obj: + obj[map_tuple[1]] = obj[map_tuple[0]] + del obj[map_tuple[0]] + + return obj @staticmethod def _validate_params(): From fd5818977003d72ce5a62c9ac3df52b7ef67dce1 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Fri, 12 Jan 2024 13:02:14 -0700 Subject: [PATCH 14/62] [checks] Add name to check_name output for clarity with aggregate --- plugins/module_utils/__impl/checks.py | 3 ++- tests/unit/plugins/modules/test_pfsense_gateway.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/__impl/checks.py b/plugins/module_utils/__impl/checks.py index f6beebac..5088f1fe 100644 --- a/plugins/module_utils/__impl/checks.py +++ b/plugins/module_utils/__impl/checks.py @@ -15,7 +15,8 @@ def check_name(self, name, objtype): msg = None if len(name) >= 32 or len(re.findall(r'(^_*$|^\d*$|[^a-zA-Z0-9_])', name)) > 0: - msg = "The {0} name must be less than 32 characters long, may not consist of only numbers, may not consist of only underscores, ".format(objtype) + msg = "The {0} name '{1}' must be less than 32 characters long, may not consist of only numbers, may not consist of only underscores, ".format( + objtype, name) msg += "and may only contain the following characters: a-z, A-Z, 0-9, _" elif name in ["port", "pass"]: msg = "The {0} name must not be either of the reserved words 'port' or 'pass'".format(objtype) diff --git a/tests/unit/plugins/modules/test_pfsense_gateway.py b/tests/unit/plugins/modules/test_pfsense_gateway.py index 6e8ef439..916077a4 100644 --- a/tests/unit/plugins/modules/test_pfsense_gateway.py +++ b/tests/unit/plugins/modules/test_pfsense_gateway.py @@ -82,8 +82,8 @@ def test_gateway_create_in_vip(self): def test_gateway_create_invalid_name(self): """ test """ obj = dict(name='___', interface='lan', gateway='192.168.1.1') - msg = 'The gateway name must be less than 32 characters long, may not consist of only numbers, ' - msg += 'may not consist of only underscores, and may only contain the following characters: a-z, A-Z, 0-9, _' + msg = "The gateway name '___' must be less than 32 characters long, may not consist of only numbers, " + msg += "may not consist of only underscores, and may only contain the following characters: a-z, A-Z, 0-9, _" self.do_module_test(obj, msg=msg, failed=True) def test_gateway_create_invalid_interface(self): From 2d1dd71ba9cd77da465234d854279f50111d91f0 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Fri, 12 Jan 2024 13:20:03 -0700 Subject: [PATCH 15/62] [pfsense_alias] Refactor _params_to_obj() and add url - Add url parameter, deprecate address for urltable/urltable_ports - Use map_param_if and param_force --- .../fragments/pfsense_alias-add-url.yml | 2 + plugins/module_utils/alias.py | 55 ++++++++++++------- plugins/modules/pfsense_aggregate.py | 11 +++- plugins/modules/pfsense_alias.py | 9 ++- .../modules/fixtures/pfsense_alias_config.xml | 1 - .../pfsense_ipsec_aggregate_config.xml | 1 - .../plugins/modules/test_pfsense_aggregate.py | 18 +++--- .../plugins/modules/test_pfsense_alias.py | 52 ++++++++++++------ 8 files changed, 96 insertions(+), 53 deletions(-) create mode 100644 changelogs/fragments/pfsense_alias-add-url.yml diff --git a/changelogs/fragments/pfsense_alias-add-url.yml b/changelogs/fragments/pfsense_alias-add-url.yml new file mode 100644 index 00000000..c285d0a7 --- /dev/null +++ b/changelogs/fragments/pfsense_alias-add-url.yml @@ -0,0 +1,2 @@ +minor_changes: + - pfsense_alias - Add `url` parameter and deprecate using `address` for `urltable` and `urltable_ports` types. diff --git a/plugins/module_utils/alias.py b/plugins/module_utils/alias.py index a215e1bf..fe35427c 100644 --- a/plugins/module_utils/alias.py +++ b/plugins/module_utils/alias.py @@ -13,15 +13,33 @@ state=dict(default='present', choices=['present', 'absent']), type=dict(default=None, required=False, choices=['host', 'network', 'port', 'urltable', 'urltable_ports']), address=dict(default=None, required=False, type='str'), + url=dict(default=None, required=False, type='str'), descr=dict(default=None, required=False, type='str'), detail=dict(default=None, required=False, type='str'), updatefreq=dict(default=None, required=False, type='int'), ) +ALIAS_PARAM_FORCE = ['descr', 'detail'] + +ALIAS_MUTUALLY_EXCLUSIVE = [ + ('address', 'url'), +] + ALIAS_REQUIRED_IF = [ - ["state", "present", ["type", "address"]], + ["state", "present", ["type"]], + ["type", "host", ["address"]], + ["type", "network", ["address"]], + ["type", "port", ["address"]], ["type", "urltable", ["updatefreq"]], ["type", "urltable_ports", ["updatefreq"]], + # When "address" deprecation period is over + # ["type", "urltable", ["updatefreq", "url"]], + # ["type", "urltable_ports", ["updatefreq", "url"]], +] + +ALIAS_MAP_PARAM_IF = [ + ["type", "urltable", ("address", "url")], + ["type", "urltable_ports", ("address", "url")], ] ALIAS_PHP_COMMAND_SET = """ @@ -33,6 +51,10 @@ class PFSenseAliasModule(PFSenseModuleBase): """ module managing pfsense aliases """ + ############################## + # unit tests + # + # Must be class method for unit test usage @staticmethod def get_argument_spec(): """ return argument spec """ @@ -42,26 +64,14 @@ def get_argument_spec(): # init # def __init__(self, module, pfsense=None): - super(PFSenseAliasModule, self).__init__(module, pfsense, root='aliases', node='alias', key='name', update_php=ALIAS_PHP_COMMAND_SET) + super(PFSenseAliasModule, self).__init__(module, pfsense, root='aliases', node='alias', key='name', update_php=ALIAS_PHP_COMMAND_SET, + map_param_if=ALIAS_MAP_PARAM_IF, param_force=ALIAS_PARAM_FORCE) + # Override for use with aggregate + self.argument_spec = ALIAS_ARGUMENT_SPEC ############################## # params processing # - def _params_to_obj(self): - """ return dict from module params """ - obj = dict() - obj['name'] = self.params['name'] - if self.params['state'] == 'present': - obj['type'] = self.params['type'] - obj['address'] = self.params['address'] - obj['descr'] = self.params['descr'] - obj['detail'] = self.params['detail'] - if obj['type'] == 'urltable' or obj['type'] == 'urltable_ports': - obj['url'] = self.params['address'] - obj['updatefreq'] = str(self.params['updatefreq']) - - return obj - def _validate_params(self): """ do some extra checks on input parameters """ params = self.params @@ -93,9 +103,12 @@ def _validate_params(self): # check details count details = params['detail'].split('||') if params['detail'] is not None else [] - addresses = params['address'].split(' ') - if len(details) > len(addresses): - self.module.fail_json(msg='Too many details in relation to addresses') + if params['address'] is not None: + addresses = params['address'].split(' ') + if len(details) > len(addresses): + self.module.fail_json(msg='Too many details in relation to addresses') + if params['type'] in ['urltable', 'urltable_ports']: + self.module.warn('Use of "address" with {type} is depracated, please use "url" instead'.format(type=params['type'])) # pfSense GUI rule for detail in details: @@ -115,12 +128,14 @@ def _log_fields(self, before=None): if before is None: values += self.format_cli_field(self.obj, 'type') values += self.format_cli_field(self.obj, 'address') + values += self.format_cli_field(self.obj, 'url') values += self.format_cli_field(self.obj, 'updatefreq') values += self.format_cli_field(self.obj, 'descr') values += self.format_cli_field(self.obj, 'detail') else: values += self.format_updated_cli_field(self.obj, before, 'type', add_comma=(values)) values += self.format_updated_cli_field(self.obj, before, 'address', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'url', add_comma=(values)) values += self.format_updated_cli_field(self.obj, before, 'updatefreq', add_comma=(values)) values += self.format_updated_cli_field(self.obj, before, 'descr', add_comma=(values)) values += self.format_updated_cli_field(self.obj, before, 'detail', add_comma=(values)) diff --git a/plugins/modules/pfsense_aggregate.py b/plugins/modules/pfsense_aggregate.py index 33547211..3ef4c343 100644 --- a/plugins/modules/pfsense_aggregate.py +++ b/plugins/modules/pfsense_aggregate.py @@ -43,7 +43,11 @@ default: null type: str address: - description: The address of the alias. Use a space separator for multiple values + description: The address of the alias for `host`, `network` or `port` types. Use a space separator for multiple values + default: null + type: str + url: + description: The URL of the alias for `urltable` or `urltable_ports` types. Use a space separator for multiple values default: null type: str descr: @@ -609,7 +613,7 @@ """ from ansible_collections.pfsensible.core.plugins.module_utils.pfsense import PFSenseModule -from ansible_collections.pfsensible.core.plugins.module_utils.alias import PFSenseAliasModule, ALIAS_ARGUMENT_SPEC, ALIAS_REQUIRED_IF +from ansible_collections.pfsensible.core.plugins.module_utils.alias import PFSenseAliasModule, ALIAS_ARGUMENT_SPEC, ALIAS_MUTUALLY_EXCLUSIVE, ALIAS_REQUIRED_IF from ansible_collections.pfsensible.core.plugins.module_utils.interface import ( PFSenseInterfaceModule, INTERFACE_ARGUMENT_SPEC, @@ -1091,7 +1095,8 @@ def commit_changes(self): def main(): argument_spec = dict( - aggregated_aliases=dict(type='list', elements='dict', options=ALIAS_ARGUMENT_SPEC, required_if=ALIAS_REQUIRED_IF), + aggregated_aliases=dict( + type='list', elements='dict', options=ALIAS_ARGUMENT_SPEC, mutually_exclusive=ALIAS_MUTUALLY_EXCLUSIVE, required_if=ALIAS_REQUIRED_IF), aggregated_interfaces=dict( type='list', elements='dict', options=INTERFACE_ARGUMENT_SPEC, required_if=INTERFACE_REQUIRED_IF, mutually_exclusive=INTERFACE_MUTUALLY_EXCLUSIVE), diff --git a/plugins/modules/pfsense_alias.py b/plugins/modules/pfsense_alias.py index c472f69b..2a486ad0 100644 --- a/plugins/modules/pfsense_alias.py +++ b/plugins/modules/pfsense_alias.py @@ -37,7 +37,11 @@ default: null type: str address: - description: The address of the alias. Use a space separator for multiple values + description: The address of the alias for `host`, `network` or `port` types. Use a space separator for multiple values + default: null + type: str + url: + description: The URL of the alias for `urltable` or `urltable_ports` types. Use a space separator for multiple values default: null type: str descr: @@ -81,12 +85,13 @@ """ from ansible.module_utils.basic import AnsibleModule -from ansible_collections.pfsensible.core.plugins.module_utils.alias import PFSenseAliasModule, ALIAS_ARGUMENT_SPEC, ALIAS_REQUIRED_IF +from ansible_collections.pfsensible.core.plugins.module_utils.alias import PFSenseAliasModule, ALIAS_ARGUMENT_SPEC, ALIAS_MUTUALLY_EXCLUSIVE, ALIAS_REQUIRED_IF def main(): module = AnsibleModule( argument_spec=ALIAS_ARGUMENT_SPEC, + mutually_exclusive=ALIAS_MUTUALLY_EXCLUSIVE, required_if=ALIAS_REQUIRED_IF, supports_check_mode=True) diff --git a/tests/unit/plugins/modules/fixtures/pfsense_alias_config.xml b/tests/unit/plugins/modules/fixtures/pfsense_alias_config.xml index 94e4eff1..b8ef3e9e 100644 --- a/tests/unit/plugins/modules/fixtures/pfsense_alias_config.xml +++ b/tests/unit/plugins/modules/fixtures/pfsense_alias_config.xml @@ -1376,7 +1376,6 @@
172.16.1.3
-
http://www.acme-corp.com
urltable 10 http://www.acme-corp.com diff --git a/tests/unit/plugins/modules/fixtures/pfsense_ipsec_aggregate_config.xml b/tests/unit/plugins/modules/fixtures/pfsense_ipsec_aggregate_config.xml index b8d226e5..aff3649d 100644 --- a/tests/unit/plugins/modules/fixtures/pfsense_ipsec_aggregate_config.xml +++ b/tests/unit/plugins/modules/fixtures/pfsense_ipsec_aggregate_config.xml @@ -1634,7 +1634,6 @@
172.16.1.3
-
http://www.acme-corp.com
urltable 10 http://www.acme-corp.com diff --git a/tests/unit/plugins/modules/test_pfsense_aggregate.py b/tests/unit/plugins/modules/test_pfsense_aggregate.py index 63732525..943b2c38 100644 --- a/tests/unit/plugins/modules/test_pfsense_aggregate.py +++ b/tests/unit/plugins/modules/test_pfsense_aggregate.py @@ -154,9 +154,9 @@ def test_aggregate_aliases(self): set_module_args(args) result = self.execute_module(changed=True) result_aliases = [] - result_aliases.append("create alias 'one_host', type='host', address='10.9.8.7'") - result_aliases.append("create alias 'another_host', type='host', address='10.9.8.6'") - result_aliases.append("update alias 'port_ssh' set address='2222', descr=none, detail=none") + result_aliases.append("create alias 'one_host', type='host', address='10.9.8.7', descr='', detail=''") + result_aliases.append("create alias 'another_host', type='host', address='10.9.8.6', descr='', detail=''") + result_aliases.append("update alias 'port_ssh' set address='2222'") result_aliases.append("delete alias 'port_http'") self.assertEqual(result['result_aliases'], result_aliases) @@ -183,9 +183,9 @@ def test_aggregate_aliases_checkmode(self): set_module_args(args) result = self.execute_module(changed=True) result_aliases = [] - result_aliases.append("create alias 'one_host', type='host', address='10.9.8.7'") - result_aliases.append("create alias 'another_host', type='host', address='10.9.8.6'") - result_aliases.append("update alias 'port_ssh' set address='2222', descr=none, detail=none") + result_aliases.append("create alias 'one_host', type='host', address='10.9.8.7', descr='', detail=''") + result_aliases.append("create alias 'another_host', type='host', address='10.9.8.6', descr='', detail=''") + result_aliases.append("update alias 'port_ssh' set address='2222'") result_aliases.append("delete alias 'port_http'") self.assertEqual(result['result_aliases'], result_aliases) @@ -207,9 +207,9 @@ def test_aggregate_aliases_purge(self): set_module_args(args) result = self.execute_module(changed=True) result_aliases = [] - result_aliases.append("create alias 'one_host', type='host', address='10.9.8.7'") - result_aliases.append("create alias 'another_host', type='host', address='10.9.8.6'") - result_aliases.append("update alias 'port_ssh' set address='2222', descr=none, detail=none") + result_aliases.append("create alias 'one_host', type='host', address='10.9.8.7', descr='', detail=''") + result_aliases.append("create alias 'another_host', type='host', address='10.9.8.6', descr='', detail=''") + result_aliases.append("update alias 'port_ssh' set address='2222'") result_aliases.append("delete alias 'port_http'") result_aliases.append("delete alias 'port_dns'") diff --git a/tests/unit/plugins/modules/test_pfsense_alias.py b/tests/unit/plugins/modules/test_pfsense_alias.py index ed8b88a6..5fbeac6e 100644 --- a/tests/unit/plugins/modules/test_pfsense_alias.py +++ b/tests/unit/plugins/modules/test_pfsense_alias.py @@ -32,13 +32,18 @@ def __init__(self, *args, **kwargs): # First we run the module # Then, we check return values # Finally, we check the xml - def do_alias_creation_test(self, alias, failed=False, msg='', command=None): + def do_alias_creation_test(self, alias, set_after=None, unset_after=None, failed=False, msg='', command=None): """ test creation of a new alias """ set_module_args(self.args_from_var(alias)) result = self.execute_module(changed=True, failed=failed, msg=msg) if not failed: diff = dict(before={}, after=alias) + if set_after is not None: + diff['after'].update(set_after) + if unset_after is not None: + for n in unset_after: + del diff['after'][n] self.assertEqual(result['diff'], diff) self.assert_xml_elt_dict('aliases', dict(name=alias['name'], type=alias['type']), diff['after']) self.assertEqual(result['commands'], [command]) @@ -76,7 +81,10 @@ def do_alias_update_field(self, alias, set_after=None, command=None, **kwargs): if set_after is not None: diff['after'].update(set_after) self.assertEqual(result['diff'], diff) - self.assert_xml_elt_value('aliases', dict(name=alias['name'], type=alias['type']), 'address', diff['after']['address']) + if alias['type'] in ['host', 'port', 'network']: + self.assert_xml_elt_value('aliases', dict(name=alias['name'], type=alias['type']), 'address', diff['after']['address']) + else: + self.assert_xml_elt_value('aliases', dict(name=alias['name'], type=alias['type']), 'url', diff['after']['url']) self.assertEqual(result['commands'], [command]) ############## @@ -181,49 +189,59 @@ def test_network_update_descr(self): def test_urltable_create(self): """ test creation of a new urltable alias """ alias = dict(name='acme_table', address='http://www.acme.com', descr='', type='urltable', updatefreq='10', detail='') - alias['url'] = alias['address'] - command = "create alias 'acme_table', type='urltable', address='http://www.acme.com', updatefreq='10', descr='', detail=''" + command = "create alias 'acme_table', type='urltable', url='http://www.acme.com', updatefreq='10', descr='', detail=''" + self.do_alias_creation_test(alias, command=command, set_after=dict(url='http://www.acme.com'), unset_after=['address']) + + def test_urltable_create_url(self): + """ test creation of a new urltable alias """ + alias = dict(name='acme_table', url='http://www.acme.com', descr='', type='urltable', updatefreq='10', detail='') + command = "create alias 'acme_table', type='urltable', url='http://www.acme.com', updatefreq='10', descr='', detail=''" self.do_alias_creation_test(alias, command=command) + def test_urltable_create_exclusive(self): + """ test creattion of a urltable alias with both address and url - fails """ + alias = dict( + name='acme_corp', address='http://www.acme-corp.com', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') + self.do_alias_creation_test(alias, failed=True, msg='parameters are mutually exclusive: address|url') + def test_urltable_delete(self): """ test deletion of a urltable alias """ alias = dict( - name='acme_corp', address='http://www.acme-corp.com', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') + name='acme_corp', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') command = "delete alias 'acme_corp'" self.do_alias_deletion_test(alias, command=command) def test_urltable_update_noop(self): """ test not updating a urltable alias """ alias = dict( - name='acme_corp', address='http://www.acme-corp.com', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') + name='acme_corp', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') self.do_alias_update_noop_test(alias) def test_urltable_update_url(self): - """ test updating address of a urltable alias """ + """ test updating url of a urltable alias """ alias = dict( - name='acme_corp', address='http://www.acme-corp.com', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') - command = "update alias 'acme_corp' set address='http://www.new-acme-corp.com'" - self.do_alias_update_field(alias, address='http://www.new-acme-corp.com', set_after=dict(url='http://www.new-acme-corp.com'), command=command) + name='acme_corp', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') + command = "update alias 'acme_corp' set url='http://www.new-acme-corp.com'" + self.do_alias_update_field(alias, url='http://www.new-acme-corp.com', set_after=dict(url='http://www.new-acme-corp.com'), command=command) def test_urltable_update_descr(self): """ test updating descr of a urltable alias """ alias = dict( - name='acme_corp', address='http://www.acme-corp.com', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') + name='acme_corp', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') command = "update alias 'acme_corp' set descr='acme corp urls'" self.do_alias_update_field(alias, descr='acme corp urls', command=command) def test_urltable_update_freq(self): """ test updating updatefreq of a urltable alias """ alias = dict( - name='acme_corp', address='http://www.acme-corp.com', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') + name='acme_corp', url='http://www.acme-corp.com', descr='', type='urltable', updatefreq='10', detail='') command = "update alias 'acme_corp' set updatefreq='20'" self.do_alias_update_field(alias, updatefreq='20', command=command) def test_urltable_ports_create(self): """ test creation of a new urltable_ports alias """ - alias = dict(name='acme_table', address='http://www.acme.com', descr='', type='urltable_ports', updatefreq='10', detail='') - alias['url'] = alias['address'] - command = "create alias 'acme_table', type='urltable_ports', address='http://www.acme.com', updatefreq='10', descr='', detail=''" + alias = dict(name='acme_table', url='http://www.acme.com', descr='', type='urltable_ports', updatefreq='10', detail='') + command = "create alias 'acme_table', type='urltable_ports', url='http://www.acme.com', updatefreq='10', descr='', detail=''" self.do_alias_creation_test(alias, command=command) ############## @@ -237,7 +255,7 @@ def test_create_alias_duplicate(self): def test_create_alias_invalid_name(self): """ test creation of a new alias with invalid name """ alias = dict(name='ads-ervers', address='10.0.0.1 10.0.0.2', type='host') - msg = "The alias name must be less than 32 characters long, may not consist of only numbers, may not consist of only underscores, " + msg = "The alias name 'ads-ervers' must be less than 32 characters long, may not consist of only numbers, may not consist of only underscores, " msg += "and may only contain the following characters: a-z, A-Z, 0-9, _" self.do_alias_creation_test(alias, failed=True, msg=msg) @@ -259,7 +277,7 @@ def test_create_alias_without_type(self): def test_create_alias_without_address(self): """ test creation of a new host alias without address """ alias = dict(name='adservers', type='host') - self.do_alias_creation_test(alias, failed=True, msg='state is present but all of the following are missing: address') + self.do_alias_creation_test(alias, failed=True, msg='type is host but all of the following are missing: address') def test_create_alias_invalid_details(self): """ test creation of a new host alias with invalid details """ From db0f3527885fe2427f9d64d0386aefe7debdc06d Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Fri, 12 Jan 2024 18:02:38 -0700 Subject: [PATCH 16/62] [module_base] Basic _log_fields() implementation --- plugins/module_utils/module_base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/module_base.py b/plugins/module_utils/module_base.py index 2218cbf7..fa5ce377 100644 --- a/plugins/module_utils/module_base.py +++ b/plugins/module_utils/module_base.py @@ -336,7 +336,14 @@ def _log_delete(self): def _log_fields(self, before=None): """ generate pseudo-CLI command fields parameters to create an obj """ - raise NotImplementedError() + values = '' + if before is None: + for param in [n for n in self.argument_spec.keys() if n != 'state' and n != self.key]: + values += self.format_cli_field(self.obj, param) + else: + for param in [n for n in self.argument_spec.keys() if n != 'state' and n != self.key]: + values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values)) + return values @staticmethod def _log_fields_delete(): From 254360b990b2ebf16f6db76b36e77d290845ec6e Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Fri, 12 Jan 2024 18:04:00 -0700 Subject: [PATCH 17/62] [pfsense_alias] Use PFSenseModuleBase _get_obj_name and _log_fields Remove unneeded test for type presence, don't mark it default --- plugins/module_utils/alias.py | 42 +++---------------- plugins/modules/pfsense_aggregate.py | 1 - plugins/modules/pfsense_alias.py | 1 - .../plugins/modules/test_pfsense_alias.py | 6 +-- 4 files changed, 9 insertions(+), 41 deletions(-) diff --git a/plugins/module_utils/alias.py b/plugins/module_utils/alias.py index fe35427c..28dcc7d4 100644 --- a/plugins/module_utils/alias.py +++ b/plugins/module_utils/alias.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright: (c) 2018, Orion Poplawski +# Copyright: (c) 2018-2024, Orion Poplawski # Copyright: (c) 2018, Frederic Bor # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -11,7 +11,7 @@ ALIAS_ARGUMENT_SPEC = dict( name=dict(required=True, type='str'), state=dict(default='present', choices=['present', 'absent']), - type=dict(default=None, required=False, choices=['host', 'network', 'port', 'urltable', 'urltable_ports']), + type=dict(required=False, choices=['host', 'network', 'port', 'urltable', 'urltable_ports']), address=dict(default=None, required=False, type='str'), url=dict(default=None, required=False, type='str'), descr=dict(default=None, required=False, type='str'), @@ -87,26 +87,22 @@ def _validate_params(self): if params['type'] != alias_elt.find('type').text: self.module.fail_json(msg='An alias with this name and a different type already exists: \'{0}\''.format(params['name'])) + # Aliases cannot have the same name as an interface description if self.pfsense.get_interface_by_display_name(params['name']) is not None: self.module.fail_json(msg='An interface description with this name already exists: \'{0}\''.format(params['name'])) - missings = ['type'] - for param, value in params.items(): - if param in missings and value is not None and value != '': - missings.remove(param) - if missings: - self.module.fail_json(msg='state is present but all of the following are missing: ' + ','.join(missings)) - # updatefreq is for urltable only if params['updatefreq'] is not None and params['type'] != 'urltable' and params['type'] != 'urltable_ports': self.module.fail_json(msg='updatefreq is only valid with type urltable or urltable_ports') - # check details count details = params['detail'].split('||') if params['detail'] is not None else [] if params['address'] is not None: + # check details count addresses = params['address'].split(' ') if len(details) > len(addresses): self.module.fail_json(msg='Too many details in relation to addresses') + + # warn if address is used with urltable to urltable_ports if params['type'] in ['urltable', 'urltable_ports']: self.module.warn('Use of "address" with {type} is depracated, please use "url" instead'.format(type=params['type'])) @@ -114,29 +110,3 @@ def _validate_params(self): for detail in details: if detail.startswith('|') or detail.endswith('|'): self.module.fail_json(msg='Vertical bars (|) at start or end of descriptions not allowed') - - ############################## - # Logging - # - def _get_obj_name(self): - """ return obj's name """ - return "'" + self.obj['name'] + "'" - - def _log_fields(self, before=None): - """ generate pseudo-CLI command fields parameters to create an obj """ - values = '' - if before is None: - values += self.format_cli_field(self.obj, 'type') - values += self.format_cli_field(self.obj, 'address') - values += self.format_cli_field(self.obj, 'url') - values += self.format_cli_field(self.obj, 'updatefreq') - values += self.format_cli_field(self.obj, 'descr') - values += self.format_cli_field(self.obj, 'detail') - else: - values += self.format_updated_cli_field(self.obj, before, 'type', add_comma=(values)) - values += self.format_updated_cli_field(self.obj, before, 'address', add_comma=(values)) - values += self.format_updated_cli_field(self.obj, before, 'url', add_comma=(values)) - values += self.format_updated_cli_field(self.obj, before, 'updatefreq', add_comma=(values)) - values += self.format_updated_cli_field(self.obj, before, 'descr', add_comma=(values)) - values += self.format_updated_cli_field(self.obj, before, 'detail', add_comma=(values)) - return values diff --git a/plugins/modules/pfsense_aggregate.py b/plugins/modules/pfsense_aggregate.py index 3ef4c343..f6bd936a 100644 --- a/plugins/modules/pfsense_aggregate.py +++ b/plugins/modules/pfsense_aggregate.py @@ -40,7 +40,6 @@ type: description: The type of the alias choices: [ "host", "network", "port", "urltable", "urltable_ports" ] - default: null type: str address: description: The address of the alias for `host`, `network` or `port` types. Use a space separator for multiple values diff --git a/plugins/modules/pfsense_alias.py b/plugins/modules/pfsense_alias.py index 2a486ad0..5c6306a5 100644 --- a/plugins/modules/pfsense_alias.py +++ b/plugins/modules/pfsense_alias.py @@ -34,7 +34,6 @@ type: description: The type of the alias choices: [ "host", "network", "port", "urltable", "urltable_ports" ] - default: null type: str address: description: The address of the alias for `host`, `network` or `port` types. Use a space separator for multiple values diff --git a/tests/unit/plugins/modules/test_pfsense_alias.py b/tests/unit/plugins/modules/test_pfsense_alias.py index 5fbeac6e..e1576245 100644 --- a/tests/unit/plugins/modules/test_pfsense_alias.py +++ b/tests/unit/plugins/modules/test_pfsense_alias.py @@ -189,13 +189,13 @@ def test_network_update_descr(self): def test_urltable_create(self): """ test creation of a new urltable alias """ alias = dict(name='acme_table', address='http://www.acme.com', descr='', type='urltable', updatefreq='10', detail='') - command = "create alias 'acme_table', type='urltable', url='http://www.acme.com', updatefreq='10', descr='', detail=''" + command = "create alias 'acme_table', type='urltable', url='http://www.acme.com', descr='', detail='', updatefreq='10'" self.do_alias_creation_test(alias, command=command, set_after=dict(url='http://www.acme.com'), unset_after=['address']) def test_urltable_create_url(self): """ test creation of a new urltable alias """ alias = dict(name='acme_table', url='http://www.acme.com', descr='', type='urltable', updatefreq='10', detail='') - command = "create alias 'acme_table', type='urltable', url='http://www.acme.com', updatefreq='10', descr='', detail=''" + command = "create alias 'acme_table', type='urltable', url='http://www.acme.com', descr='', detail='', updatefreq='10'" self.do_alias_creation_test(alias, command=command) def test_urltable_create_exclusive(self): @@ -241,7 +241,7 @@ def test_urltable_update_freq(self): def test_urltable_ports_create(self): """ test creation of a new urltable_ports alias """ alias = dict(name='acme_table', url='http://www.acme.com', descr='', type='urltable_ports', updatefreq='10', detail='') - command = "create alias 'acme_table', type='urltable_ports', url='http://www.acme.com', updatefreq='10', descr='', detail=''" + command = "create alias 'acme_table', type='urltable_ports', url='http://www.acme.com', descr='', detail='', updatefreq='10'" self.do_alias_creation_test(alias, command=command) ############## From ad5ecac5d3214e30e6e7636a2db6bc3496de5191 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Sat, 13 Jan 2024 18:26:49 -0700 Subject: [PATCH 18/62] [generate_module] Initial checkin --- misc/generate_module.py | 221 ++++++++++++++++++++++++++++++++++++++ misc/pfsense_module.py.j2 | 123 +++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100755 misc/generate_module.py create mode 100644 misc/pfsense_module.py.j2 diff --git a/misc/generate_module.py b/misc/generate_module.py new file mode 100755 index 00000000..6d711145 --- /dev/null +++ b/misc/generate_module.py @@ -0,0 +1,221 @@ +#!/usr/bin/python3 + +import argparse +import datetime +import jinja2 +import lxml.etree as ET +import lxml.html +from paramiko import SSHClient +import re +import requests +from scp import SCPClient +import sys +from urllib.parse import urlparse + +parser = argparse.ArgumentParser(description='Generate a pfsensible module.') +parser.add_argument('--url', help='The URL to scrape') +parser.add_argument('--urlfile', help='A local file copy of the URL to scrape') +parser.add_argument('--user', default='admin', help='The user to connect as') +parser.add_argument('--password', default='changeme', help='The password of user') +parser.add_argument('--module_name', help='The name of the module to generate - defaults to being based on the url') +parser.add_argument('--item_min', default='item_min', help='The name of the minimally configured item to search for in config.xml') +parser.add_argument('--item_full', default='item_full', + help='The name of the fully configured item to search for in config.xml, will be used for exmaples in the documentation') + +args = parser.parse_args() + +if args.url is not None: + parsed_uri = urlparse(args.url) + + # Login using just the base URL + login_url = '{uri.scheme}://{uri.netloc}/'.format(uri=parsed_uri) + + # Collect to host for later use to scp config.xml + host = f'{parsed_uri.netloc}' + + # Construct a likely module name from the URL + if args.module_name is None: + module_name = re.sub(r'^/(?:firewall_)?(.*)(?:_edit)\.php.*$', r'pfsense_\1', parsed_uri.path) + module_name = re.sub(r'ses$', 's', module_name) + else: + module_name = args.module_name + + # We likely don't have a valid certificate + requests.packages.urllib3.disable_warnings() + + # Start our session (need cookies for login) + client = requests.Session() + + # Retrieve the CSRF token first + r = client.get(login_url, verify=False) + csrf = re.search(".*name='__csrf_magic' value=\"([^\"]+)\".*", r.text, flags=re.MULTILINE).group(1) + + # Login to the web interface + login_data = dict(login='Login', usernamefld=args.user, passwordfld=args.password, __csrf_magic=csrf) + r = client.post(login_url, data=login_data, verify=False) + csrf = re.search(".*name='__csrf_magic' value=\"([^\"]+)\".*", r.text, flags=re.MULTILINE).group(1) + + # Retrieve the configuration web page and parse it + r = client.get(args.url, verify=False) + html = lxml.html.fromstring(r.text) + +elif args.urlfile is not None: + # Use a cached copy of the web page - get rid of this? Need to specify host and module name + html = lxml.html.parse(args.urlfile) + host = '192.168.100.2' + module_name = 'pfsense_nat_1to1' + +else: + sys.exit('You must specify one of --url or --urlfile') + +# The web page should have a single form +if len(html.forms) != 1: + sys.exit(f'Found {len(html.forms)} forms instead of a single one!') + +# Collected parameters from the web form +params = dict() + +# Collect the input elements +for input in html.forms[0].inputs: + # Skip internal items + if input.name == '__csrf_magic': + continue + + param = dict() + print(f'attrib={input.attrib}') + if isinstance(input, lxml.html.InputElement): + print(f'input name={input.name} id={input.get("id")} type={input.type} value={input.value} ' + 'text={input.text} title={input.get("title")} tail={input.tail}') + if input.type == 'checkbox': + param['type'] = 'bool' + param['value'] = input.attrib['value'] + param['example'] = 'true' + elif input.type == 'text': + param['type'] = 'str' + # Text sometimes is after the input element inside the enclosing