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/galaxy.yml b/galaxy.yml index 966e439e..9cbf8151 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -21,7 +21,7 @@ authors: - Frederic Bor - taylor vories - Jan Wenzel - +- Chris Morton ### OPTIONAL but strongly recommended 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 + 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/module_utils/pfsense.py b/plugins/module_utils/pfsense.py index 7236e04d..7a970ede 100644 --- a/plugins/module_utils/pfsense.py +++ b/plugins/module_utils/pfsense.py @@ -629,6 +629,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 """ diff --git a/plugins/modules/pfsense_haproxy_frontend.py b/plugins/modules/pfsense_haproxy_frontend.py new file mode 100644 index 00000000..c0834788 --- /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: "0.6.0" +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..104cd1ec --- /dev/null +++ b/plugins/modules/pfsense_haproxy_frontend_server.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_haproxy_frontend_server +version_added: "0.6.0" +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()