From 051bcc5a5dab005778b8576eae63732011eb5c0e Mon Sep 17 00:00:00 2001 From: Cosmo Date: Tue, 8 Mar 2022 17:35:14 -0600 Subject: [PATCH 1/5] got workign w/ new collecitons --- plugins/module_utils/pfsense.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/module_utils/pfsense.py b/plugins/module_utils/pfsense.py index 9e2d18b4..6309280d 100644 --- a/plugins/module_utils/pfsense.py +++ b/plugins/module_utils/pfsense.py @@ -582,6 +582,8 @@ def find_schedule_elt(self, name): """ return schedule elt if found """ return self.find_elt_xpath("./schedules/schedule[name='{0}']".format(name)) + + @staticmethod def uniqid(prefix='', more_entropy=False): """ return an identifier based on time """ From ba432930d36217f13904ee1f615df5753efb064d Mon Sep 17 00:00:00 2001 From: Cosmo Date: Thu, 10 Mar 2022 14:09:02 -0600 Subject: [PATCH 2/5] updated to use new syntax --- .gitignore | 2 + plugins/module_utils/haproxy_frontend.py | 163 ++++++++++++++++++ .../module_utils/haproxy_frontend_server.py | 139 +++++++++++++++ plugins/modules/pfsense_haproxy_frontend.py | 136 +++++++++++++++ .../pfsense_haproxy_frontend_server.py | 92 ++++++++++ 5 files changed, 532 insertions(+) create mode 100644 plugins/module_utils/haproxy_frontend.py create mode 100644 plugins/module_utils/haproxy_frontend_server.py create mode 100644 plugins/modules/pfsense_haproxy_frontend.py create mode 100644 plugins/modules/pfsense_haproxy_frontend_server.py diff --git a/.gitignore b/.gitignore index 2eae9cdf..cf35404b 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,5 @@ venv.bak/ # ansible-galaxy package *.tar.gz + +.DS_Store diff --git a/plugins/module_utils/haproxy_frontend.py b/plugins/module_utils/haproxy_frontend.py new file mode 100644 index 00000000..f9fa2ebd --- /dev/null +++ b/plugins/module_utils/haproxy_frontend.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Chris Morton, cosmo@cosmo.2y.net +# 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 re +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +HAPROXY_FRONTEND_ARGUMENT_SPEC = dict( + state=dict(default='present', choices=['present', 'absent']), + name=dict(required=True, type='str'), + status=dict(required=True, type='str'), + desc=dict(required=True, type='str'), + type=dict(default='http', choices=['http', 'https']), + httpclose=dict(default='http-keep-alive', choices=['http-keep-alive']), + backend_serverpool=dict(required=False, type='str'), + ssloffloadcert=dict(required=False, type='str'), + ssloffloadcert_type_search=dict(default='descr', type='str'), + ssloffloadacl_an=dict(required=False, type='str'), + max_connections=dict(default=100, type='int'), + addhttp_https_redirect=dict(required=False, type='bool') +) + + +class PFSenseHaproxyFrontendModule(PFSenseModuleBase): + """ module managing pfsense haproxy frontends """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return HAPROXY_FRONTEND_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseHaproxyFrontendModule, self).__init__(module, pfsense) + self.name = "pfsense_haproxy_frontend" + self.obj = dict() + + pkgs_elt = self.pfsense.get_element('installedpackages') + self.haproxy = pkgs_elt.find('haproxy') if pkgs_elt is not None else None + self.root_elt = self.haproxy.find('ha_backends') if self.haproxy is not None else None + if self.root_elt is None: + self.module.fail_json(msg='Unable to find frontends (ha_backends) XML configuration entry. Are you sure haproxy is installed ?') + + self.servers = None + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a frontend dict from module params """ + params = self.params + + obj = dict() + obj['name'] = self.params['name'] + if self.params['state'] == 'present': + self._get_ansible_param(obj, 'desc') + self._get_ansible_param(obj, 'type') + self._get_ansible_param(obj, 'status') + self._get_ansible_param(obj, 'httpclose') + self._get_ansible_param(obj, 'backend_serverpool') + self._get_ansible_param(obj, 'max_connections') + + if 'ssloffloadcert' in params and params['ssloffloadcert'] is not None and params['ssloffloadcert'] != '': + search_field_type='type' + if 'ssloffloadcert_type_search' in params and params['ssloffloadcert_type_search'] is not None and params['ssloffloadcert_type_search'] != '': + search_field_type = params['ssloffloadcert_type_search'] + + cert_elt = self.pfsense.find_cert_elt(params['ssloffloadcert'], search_field=search_field_type) + if cert_elt is None: + self.module.fail_json(msg='%s is not a valid certificate ' % (params['ssloffloadcert'])) + obj['ssloffloadcert'] = cert_elt.find('refid').text + + self._get_ansible_param(obj, 'ssloffloadacl_an') + + #check for redirect + if 'addhttp_https_redirect' in params and params['addhttp_https_redirect'] is not None and params['addhttp_https_redirect'] != '' and params['addhttp_https_redirect']: + #add redirect rules + aval = dict() + val = dict() + val['action'] = 'http-request_redirect' + val['http-request_redirectrule'] = 'scheme https' + aval['item'] = val + obj['a_actionitems'] = aval + + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + # check name + if re.search(r'[^a-zA-Z0-9\.\-_]', self.params['name']) is not None: + self.module.fail_json(msg="The field 'name' contains invalid characters.") + + ############################## + # XML processing + # + def _create_target(self): + """ create the XML target_elt """ + server_elt = self.pfsense.new_element('item') + return server_elt + + def _find_target(self): + """ find the XML target_elt """ + for item_elt in self.root_elt: + if item_elt.tag != 'item': + continue + name_elt = item_elt.find('name') + if name_elt is not None and name_elt.text == self.obj['name']: + return item_elt + return None + + def _get_next_id(self): + """ get next free haproxy id """ + max_id = 99 + id_elts = self.haproxy.findall('.//id') + for id_elt in id_elts: + if id_elt.text is None: + continue + ha_id = int(id_elt.text) + if ha_id > max_id: + max_id = ha_id + return str(max_id + 1) + + ############################## + # run + # + def _update(self): + """ make the target pfsense reload haproxy """ + return self.pfsense.phpshell('''require_once("haproxy/haproxy.inc"); +$result = haproxy_check_and_run($savemsg, true); if ($result) unlink_if_exists($d_haproxyconfdirty_path);''') + + ############################## + # Logging + # + 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.params, 'desc') + values += self.format_cli_field(self.params, 'type') + values += self.format_cli_field(self.params, 'httpclose') + values += self.format_cli_field(self.params, 'backend_serverpool') + values += self.format_cli_field(self.params, 'ssloffloadcert') + values += self.format_cli_field(self.params, 'ssloffloadacl_an') + values += self.format_cli_field(self.params, 'max_connections') + else: + values += self.format_updated_cli_field(self.obj, before, 'desc', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'type', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'httpclose', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'backend_serverpool', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'ssloffloadcert', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'ssloffloadacl_an', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'max_connections', add_comma=(values)) + return values + + def _get_obj_name(self): + """ return obj's name """ + return "'{0}'".format(self.obj['name']) diff --git a/plugins/module_utils/haproxy_frontend_server.py b/plugins/module_utils/haproxy_frontend_server.py new file mode 100644 index 00000000..d3e56ac9 --- /dev/null +++ b/plugins/module_utils/haproxy_frontend_server.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Chris Morton, cosmo@cosmo.2y.net +# 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 re +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +HAPROXY_FRONTEND_SERVER_ARGUMENT_SPEC = dict( + state=dict(default='present', choices=['present', 'absent']), + frontend=dict(required=True, type='str'), + extaddr=dict(required=True, type='str'), + extaddr_port=dict(required=True, type='int'), + extaddr_ssl=dict(required=True, type='str'), +) + + +class PFSenseHaproxyFrontendServerModule(PFSenseModuleBase): + """ module managing pfsense haproxy frontends """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return HAPROXY_FRONTEND_SERVER_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseHaproxyFrontendServerModule, self).__init__(module, pfsense) + self.name = "pfsense_haproxy_frontend_server" + self.root_elt = None + self.obj = dict() + + pkgs_elt = self.pfsense.get_element('installedpackages') + self.haproxy = pkgs_elt.find('haproxy') if pkgs_elt is not None else None + self.frontends = self.haproxy.find('ha_backends') if self.haproxy is not None else None + if self.frontends is None: + self.module.fail_json(msg='Unable to find frontends (ha_backends) XML configuration entry. Are you sure haproxy is installed ?') + + self.frontend = None + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a frontend dict from module params """ + obj = dict() + self._get_ansible_param(obj, 'extaddr') + self._get_ansible_param(obj, 'extaddr_port') + self._get_ansible_param(obj, 'extaddr_ssl') + obj['name'] = "'{0}_{1}'".format(self.params['extaddr'],self.params['extaddr_port']) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + + #get the frontend + self.frontend = self._find_frontend(self.params['frontend']) + if self.frontend is None: + self.module.fail_json(msg="The frontend named '{0}' does not exist".format(self.params['frontend'])) + + #setup the a_extaddr if we dont hav eit + self.root_elt = self.frontend.find('a_extaddr') + if self.root_elt is None: + self.root_elt = self.pfsense.new_element('a_extaddr') + self.frontend.append(self.root_elt) + + ############################## + # XML processing + # + def _create_target(self): + """ create the XML target_elt """ + server_elt = self.pfsense.new_element('item') + return server_elt + + def _find_frontend(self, name): + """ return the target frontend_elt if found """ + for item_elt in self.frontends: + if item_elt.tag != 'item': + continue + name_elt = item_elt.find('name') + if name_elt is not None and name_elt.text == name: + return item_elt + return None + + def _find_target(self): + """ find the XML target_elt """ + for item_elt in self.root_elt: + if item_elt.tag != 'item': + continue + name_elt = item_elt.find('name') + print(name_elt) + if name_elt is not None and name_elt.text == self.obj['name']: + return item_elt + return None + + def _get_next_id(self): + """ get next free haproxy id """ + max_id = 99 + id_elts = self.haproxy.findall('.//id') + for id_elt in id_elts: + if id_elt.text is None: + continue + ha_id = int(id_elt.text) + if ha_id > max_id: + max_id = ha_id + return str(max_id + 1) + + ############################## + # run + # + def _update(self): + """ make the target pfsense reload haproxy """ + return self.pfsense.phpshell('''require_once("haproxy/haproxy.inc"); +$result = haproxy_check_and_run($savemsg, true); if ($result) unlink_if_exists($d_haproxyconfdirty_path);''') + + ############################## + # Logging + # + 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.params, 'extaddr') + values += self.format_cli_field(self.params, 'extaddr_port') + values += self.format_cli_field(self.params, 'extaddr_ssl') + else: + values += self.format_updated_cli_field(self.obj, before, 'extaddr', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'extaddr_port', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'extaddr_ssl', add_comma=(values)) + return values + + def _get_obj_name(self): + """ return obj's name """ + return "'{0}_{1}'".format(self.obj['extaddr'],self.obj['extaddr_port']) diff --git a/plugins/modules/pfsense_haproxy_frontend.py b/plugins/modules/pfsense_haproxy_frontend.py new file mode 100644 index 00000000..3c5da241 --- /dev/null +++ b/plugins/modules/pfsense_haproxy_frontend.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Frederic Bor +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_haproxy_frontend +version_added: "2.10" +author: Chris Morton (@cosmosified) +short_description: Manage pfSense haproxy frontends +description: + - Manage pfSense haproxy frontends +notes: +options: + name: + description: The backend name. + required: true + type: str + balance: + description: The load balancing option. + required: false + type: str + choices: ['none', 'roundrobin', 'static-rr', 'leastconn', 'source', 'uri'] + default: 'none' + balance_urilen: + description: Indicates that the algorithm should only consider that many characters at the beginning of the URI to compute the hash. + required: false + type: int + balance_uridepth: + description: Indicates the maximum directory depth to be used to compute the hash. One level is counted for each slash in the request. + required: false + type: int + balance_uriwhole: + description: Allow using whole URI including url parameters behind a question mark. + required: false + type: bool + connection_timeout: + description: The time (in milliseconds) we give up if the connection does not complete within (default 30000). + required: false + type: int + server_timeout: + description: The time (in milliseconds) we accept to wait for data from the server, or for the server to accept data (default 30000). + required: false + type: int + retries: + description: After a connection failure to a server, it is possible to retry, potentially on another server. + required: false + type: int + check_type: + description: Health check method. + type: str + choices: ['none', 'Basic', 'HTTP', 'Agent', 'LDAP', 'MySQL', 'PostgreSQL', 'Redis', 'SMTP', 'ESMTP', 'SSL'] + default: 'none' + check_frequency: + description: The check interval (in milliseconds). For HTTP/HTTPS defaults to 1000 if left blank. For TCP no check will be performed if left empty. + required: false + type: int + log_checks: + description: When this option is enabled, any change of the health check status or to the server's health will be logged. + required: false + type: bool + httpcheck_method: + description: HTTP check method. + required: false + type: str + choices: ['OPTIONS', 'HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'TRACE'] + monitor_uri: + description: Url used by http check requests. + required: false + type: str + monitor_httpversion: + description: Defaults to "HTTP/1.0" if left blank. + required: false + type: str + monitor_username: + description: Username used in checks (MySQL and PostgreSQL) + required: false + type: str + monitor_domain: + description: Domain used in checks (SMTP and ESMTP) + required: false + type: str + state: + description: State in which to leave the backend + choices: [ "present", "absent" ] + default: present + type: str +""" + +EXAMPLES = """ +- name: Add backend + pfsense_haproxy_backend: + name: exchange + balance: leastconn + httpcheck_method: HTTP + state: present + +- name: Remove backend + pfsense_haproxy_backend: + name: exchange + state: absent +""" + +RETURN = """ +commands: + description: the set of commands that would be pushed to the remote device (if pfSense had a CLI) + returned: always + type: list + sample: ["create haproxy_backend 'exchange', balance='leastconn', httpcheck_method='HTTP'", "delete haproxy_backend 'exchange'"] +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.haproxy_frontend import PFSenseHaproxyFrontendModule, HAPROXY_FRONTEND_ARGUMENT_SPEC + + +def main(): + module = AnsibleModule( + argument_spec=HAPROXY_FRONTEND_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseHaproxyFrontendModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/pfsense_haproxy_frontend_server.py b/plugins/modules/pfsense_haproxy_frontend_server.py new file mode 100644 index 00000000..9f4d869f --- /dev/null +++ b/plugins/modules/pfsense_haproxy_frontend_server.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021 Chris Morton, cosmo@cosmo.2y.net +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_haproxy_frontend_server +version_added: "2.10" +author: Chris Morton (@cosmosified) +short_description: Manage pfSense haproxy frontend servers +description: + - Manage pfSense haproxy servers +notes: +options: + frontend: + description: The frontend name. + required: true + type: str + extaddr: + description: The external address [wan_ipv4, lan_ipv4, etc]. + required: true + type: str + extaddr_port: + description: The port + required: true + type: str + extaddr_ssl: + description: Whether this is listening on ssl + required: false + type: str + state: + description: State in which to leave the backend server + choices: [ "present", "absent" ] + default: present + type: str +""" + +EXAMPLES = """ +- name: Add front server ip + pfsense_haproxy_frontend_server: + frontend: exchange + extaddr: wan_ipv4 + port: 443 + extaddr_ssl: yes + state: present + +- name: Remove frontend ip + pfsense_haproxy_backend_server: + backend: exchange + name: exchange.acme.org + state: absent +""" + +RETURN = """ +commands: + description: the set of commands that would be pushed to the remote device (if pfSense had a CLI) + returned: always + type: list + sample: [ + "create haproxy_backend_server 'exchange.acme.org' on 'exchange', status='active', address='exchange.acme.org', port=443", + "delete haproxy_backend_server 'exchange.acme.org' on 'exchange'" + ] +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.haproxy_frontend_server import ( + PFSenseHaproxyFrontendServerModule, + HAPROXY_FRONTEND_SERVER_ARGUMENT_SPEC, +) + + +def main(): + module = AnsibleModule( + argument_spec=HAPROXY_FRONTEND_SERVER_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseHaproxyFrontendServerModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From 49e61f5b9a335fe5ec30e2fa5272793bff65de13 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Thu, 10 Mar 2022 14:10:17 -0600 Subject: [PATCH 3/5] added to readme --- galaxy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy.yml b/galaxy.yml index 9a965f83..6327db0f 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -21,7 +21,7 @@ authors: - Frederic Bor - taylor vories - Jan Wenzel - +- Chris Morton ### OPTIONAL but strongly recommended From efe5d0b15a56107bed96e5cad91e66f341a1badd Mon Sep 17 00:00:00 2001 From: Chris Morton Date: Mon, 24 Apr 2023 16:50:31 -0500 Subject: [PATCH 4/5] used teh interfaces.py from the pf23branch --- plugins/module_utils/__impl/interfaces.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/module_utils/__impl/interfaces.py b/plugins/module_utils/__impl/interfaces.py index 0cfe0dff..9e0859b7 100644 --- a/plugins/module_utils/__impl/interfaces.py +++ b/plugins/module_utils/__impl/interfaces.py @@ -140,3 +140,4 @@ def parse_interface(self, interface, fail=True, with_virtual=True): if fail: self.module.fail_json(msg='%s is not a valid interface' % (interface)) return None + From 5f8cf5eb4c38a5db127d2308a8eb2a1306b60e86 Mon Sep 17 00:00:00 2001 From: Chris Morton Date: Wed, 26 Apr 2023 06:37:46 -0500 Subject: [PATCH 5/5] updated author and version TODO: unit tests for haproxy frontend --- plugins/modules/pfsense_haproxy_frontend.py | 2 +- plugins/modules/pfsense_haproxy_frontend_server.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/pfsense_haproxy_frontend.py b/plugins/modules/pfsense_haproxy_frontend.py index 3c5da241..c0834788 100644 --- a/plugins/modules/pfsense_haproxy_frontend.py +++ b/plugins/modules/pfsense_haproxy_frontend.py @@ -14,7 +14,7 @@ DOCUMENTATION = """ --- module: pfsense_haproxy_frontend -version_added: "2.10" +version_added: "0.6.0" author: Chris Morton (@cosmosified) short_description: Manage pfSense haproxy frontends description: diff --git a/plugins/modules/pfsense_haproxy_frontend_server.py b/plugins/modules/pfsense_haproxy_frontend_server.py index 9f4d869f..104cd1ec 100644 --- a/plugins/modules/pfsense_haproxy_frontend_server.py +++ b/plugins/modules/pfsense_haproxy_frontend_server.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright: (c) 2021 Chris Morton, cosmo@cosmo.2y.net +# Copyright: (c) 2023 Chris Morton, cosmo@cosmo.2y.net # 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 @@ -14,7 +14,7 @@ DOCUMENTATION = """ --- module: pfsense_haproxy_frontend_server -version_added: "2.10" +version_added: "0.6.0" author: Chris Morton (@cosmosified) short_description: Manage pfSense haproxy frontend servers description: