diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..5aea05a1 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,6 @@ +# Default ignored files +/shelf/ +/workspace.xml +.idea +*.retry +*.iml diff --git a/.idea/ansible-pfsense-core.iml b/.idea/ansible-pfsense-core.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/.idea/ansible-pfsense-core.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..639900d1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..a850e5f0 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 00000000..797acea5 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/plugins/module_utils/haproxy_frontend.py b/plugins/module_utils/haproxy_frontend.py new file mode 100644 index 00000000..b8fd43b9 --- /dev/null +++ b/plugins/module_utils/haproxy_frontend.py @@ -0,0 +1,153 @@ +# -*- 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 +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']), + mode=dict(default='active', choices=['active', 'disabled']), + name=dict(required=True, type='str'), + frontend_type=dict(default='http', choices=['http', 'ssl', 'tcp']), + httpclose=dict(default='http-keep-alive', choices=['http-keep-alive', 'http-tunnel', 'httpclose', 'http-server-close', 'forceclose']), + ssloffloadcert=dict(required=False, type='str'), + ssloffloadacl_an=dict(required=False, type='bool'), + ha_acls=dict(required=False, type='str'), + ha_certificates=dict(required=False, type='str'), + clientcert_ca=dict(required=False, type='str'), + clientcert_crl=dict(required=False, type='str'), + a_actionitems=dict(required=False, type='str'), + a_errorfiles=dict(required=False, type='str'), +) + + +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_pools') if self.haproxy is not None else None + if self.root_elt is None: + self.module.fail_json(msg='Unable to find frontends XML configuration entry. Are you sure haproxy is installed ?') + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a frontend dict from module params """ + obj = dict() + obj['name'] = self.params['name'] + if params['state'] == 'present': + obj['status'] = self.params['mode'] + + self._get_ansible_param(obj, 'frontend_type', fname='type', force=True) + self._get_ansible_param(obj, 'httpclose', force=True) + self._get_ansible_param(obj, 'ssloffloadcert', force=True) + self._get_ansible_param(obj, 'ha_acls', force=True) + self._get_ansible_param(obj, 'ha_certificates', force=True) + self._get_ansible_param(obj, 'clientcert_ca', force=True) + self._get_ansible_param(obj, 'clientcert_crl', force=True) + self._get_ansible_param_bool(obj, 'ssloffloadacl_an', force=True) + self._get_ansible_param(obj, 'a_actionitems', force=True) + self._get_ansible_param(obj, 'a_errorfiles', force=True) + + 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') + self.obj['id'] = self._get_next_id() + 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, 'frontend_type') + values += self.format_cli_field(self.params, 'httpclose') + values += self.format_cli_field(self.params, 'ssloffloadcert') + values += self.format_cli_field(self.params, 'ssloffloadacl_an', fvalue=self.fvalue_bool) + values += self.format_cli_field(self.params, 'ha_acls') + values += self.format_cli_field(self.params, 'ha_certificates') + values += self.format_cli_field(self.params, 'clientcert_ca') + values += self.format_cli_field(self.params, 'clientcert_crl') + values += self.format_cli_field(self.params, 'a_actionitems') + values += self.format_cli_field(self.params, 'a_errorfiles') + else: + for param in ['type', 'ssloffloadacl_an']: + if param in before and before[param] == '': + before[param] = None + values += self.format_updated_cli_field(self.obj, before, 'frontend_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, 'ssloffloadcert', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'ssloffloadacl_an', add_comma=(values), fvalue=self.fvalue_bool) + values += self.format_updated_cli_field(self.obj, before, 'ha_acls', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'ha_certificates', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'clientcert_ca', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'clientcert_crl', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'a_actionitems', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'a_errorfiles', add_comma=(values)) + return values + + def _get_obj_name(self): + """ return obj's name """ + return "'{0}'".format(self.obj['name']) diff --git a/plugins/modules/pfsense_haproxy_frontend.py b/plugins/modules/pfsense_haproxy_frontend.py new file mode 100644 index 00000000..755cf5ac --- /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.1.0 +author: Frederic Bor (@f-bor) +short_description: Manage pfSense haproxy frontends +description: + - Manage pfSense haproxy frontends +notes: +options: + name: + description: The frontend 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 frontend + choices: [ "present", "absent" ] + default: present + type: str +""" + +EXAMPLES = """ +- name: Add frontend + pfsense_haproxy_frontend: + name: exchange + balance: leastconn + httpcheck_method: HTTP + state: present + +- name: Remove frontend + pfsense_haproxy_frontend: + 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_frontend 'exchange', balance='leastconn', httpcheck_method='HTTP'", "delete haproxy_frontend '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()