From db0d7b01f3f285448bfb9ce69de8c094e2376ef2 Mon Sep 17 00:00:00 2001 From: genofire Date: Mon, 8 May 2023 14:51:36 +0000 Subject: [PATCH 1/3] feat: add virtual-ip module --- plugins/modules/pfsense_virtualip.py | 314 +++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 plugins/modules/pfsense_virtualip.py diff --git a/plugins/modules/pfsense_virtualip.py b/plugins/modules/pfsense_virtualip.py new file mode 100644 index 00000000..197f6a02 --- /dev/null +++ b/plugins/modules/pfsense_virtualip.py @@ -0,0 +1,314 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Frederic Bor +# Copyright: (c) 2021, Jan Wenzel +# Copyright: (c) 2023, Martin Müller +# 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_virtualip +version_added: "0.6.2" +author: Jan Wenzel (@coffeelover) +short_description: Manage pfSense virtual ip settings +description: + - Manage pfSense virtual ip settings +notes: +options: + mode: + description: Type + required: True + type: str + choices: [ "proxyarp", "carp", "ipalias", "other" ] + descr: + description: Description + required: False + type: str + interface: + description: Interface + required: True + type: str + vhid: + description: VHID Group + required: False + type: int + advbase: + description: Advertising Frequency Base + required: False + type: int + advskew: + description: Advertising Frequency Skew + required: False + type: int + password: + description: Virtual IP Password + required: False + type: str + uniqid: + description: Unique ID of Virtual IP in configuration + required: False + type: str + type: + description: Address Type + required: False + type: str + choices: [ "single" ] + default: single + subnet_bits: + description: Network's subnet mask + required: False + type: int + default: 32 + subnet: + description: Network subnet + required: False + type: str + state: + description: State in which to leave the Virtual IP + choices: [ "present", "absent" ] + default: present + type: str +""" + +EXAMPLES = """ +- name: Setup Home vip + pfsense_virtualip: + mode: "carp" + descr: "HOME VIP" + interface: "opt2" + vhid: 24 + advbase: 1, + advskew: 0, + password": "xaequae0sheiB7sh" + uniqid": "vip_home" + subnet_bits": 24 + subnet": "10.1.1.1" + state": "present" +""" + +RETURN = """ +""" + +import re +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + + +VIRTUALIP_ARGUMENT_SPEC = dict( + mode=dict(required=True, choices=['proxyarp', 'carp', 'ipalias', 'other'], type='str'), + interface=dict(required=True, type='str'), + vhid=dict(type='int'), + advskew=dict(type='int'), + advbase=dict(type='int'), + password=dict(type='str', no_log=True), + uniqid=dict(type='str'), + descr=dict(type='str'), + type=dict(type='str', choices=['single'], default='single'), + subnet_bits=dict(type='int', default=32), + subnet=dict(type='str'), + state=dict(default='present', choices=['present', 'absent'], type='str'), +) + +VIRTUALIP_REQUIRED_IF = [ + ["mode", "carp", ["uniqid", "password", "vhid", "advbase"]], + ["mode", "ipalias", ["uniqid"]], +] + + +# fields that are not written to pfsense +skip_list = ['state'] + + +class PFSenseVirtualIPModule(PFSenseModuleBase): + """ module managing pfsense virtual ip settings """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return VIRTUALIP_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseVirtualIPModule, self).__init__(module, pfsense) + self.name = "virtualip" + self.root_elt = self.pfsense.get_element('virtualip') + self.obj = dict() + + if self.root_elt is None: + self.root_elt = self.pfsense.new_element('virtualip') + self.pfsense.root.append(self.root_elt) + + ############################## + # params processing + # + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = dict() + self.obj = obj + + def _set_param(target, param): + if params.get(param) is not None: + if isinstance(params[param], str): + target[param] = params[param] + else: + target[param] = str(params[param]) + + def _set_param_bool(target, param): + if params.get(param) is not None: + value = params.get(param) + if value is True and param not in target: + target[param] = '' + elif value is False and param in target: + del target[param] + + for param in VIRTUALIP_ARGUMENT_SPEC: + if param not in skip_list: + if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool': + _set_param_bool(obj, param) + else: + _set_param(obj, param) + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + params = self.params + return + + ############################## + # XML processing + # + def _create_target(self): + """ create the XML target_elt """ + return self.pfsense.new_element('vip') + + def _find_target(self): + """ find the XML target elt """ + for vip_elt in self.root_elt: + if self.params['mode'] in ['ipalias', 'carp']: + if vip_elt.find('uniqid') is not None and vip_elt.find('uniqid').text == self.params['uniqid']: + return vip_elt + else: + if vip_elt.find('descr') is not None and vip_elt.find('descr').text == self.params['descr']: + return vip_elt + return None + + def _remove_deleted_params(self): + """ Remove from target_elt a few deleted params """ + changed = False + for param in VIRTUALIP_ARGUMENT_SPEC: + if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool': + if self.pfsense.remove_deleted_param_from_elt(self.target_elt, param, self.obj): + changed = True + + return changed + + def _update(self): + """ make the target pfsense reload """ + cmd = ''' +require_once("globals.inc"); +require_once("functions.inc"); +require_once("filter.inc"); +require_once("shaper.inc"); +require_once("interfaces.inc"); +require_once("util.inc"); +$check_carp = false; +$retval = 0; +''' + + if self.params.get('mode') in ['carp', 'ipalias']: + cmd += '$uniqid = "' + self.params.get('uniqid') + '";\n' + cmd += '$subnet = "' + self.params.get('subnet') + '";\n' + cmd += '$interface = "' + self.params.get('interface') + '";\n' + cmd += '$vipif = get_real_interface($interface);\n' + + if self.params.get('state') == 'present': + if self.params.get('mode') in ['carp', 'ipalias']: + cmd += '$check_carp = true;\n' + cmd += 'foreach ($config["virtualip"]["vip"] as $vip) {\n' + cmd += 'if ($vip["uniqid"] == $uniqid) {\n' + cmd += 'interface_' + self.params.get('mode') + '_configure($vip);\n' + cmd += '}\n}\n' + else: + if self.params.get('mode') == 'carp': + cmd += 'if (does_interface_exist($vipif)) {\n' + cmd += 'if (is_ipaddrv6($subnet)) {\n' + cmd += 'mwexec("/sbin/ifconfig " . escapeshellarg($vipif) . " inet6 " . escapeshellarg($subnet) . " delete");\n' + cmd += '} else {\n' + cmd += 'pfSense_interface_deladdress($vipif, $subnet);\n' + cmd += '}\n}\n' + elif self.params.get('mode') == 'ipalias': + cmd += 'if (does_interface_exist($vipif)) {\n' + cmd += 'if (is_ipaddrv6($subnet)) {\n' + cmd += 'mwexec("/sbin/ifconfig " . escapeshellarg($vipif) . " inet6 " . escapeshellarg($subnet) . " -alias");\n' + cmd += '} else {\n' + cmd += 'pfSense_interface_deladdress($vipif, $subnet);\n' + cmd += '}\n}\n' + + cmd += ''' +if ($check_carp === true && !get_carp_status()) { + set_single_sysctl("net.inet.carp.allow", "1"); +} +$retval |= filter_configure(); +$retval |= mwexec("/etc/rc.filter_synchronize"); +clear_subsystem_dirty('vip');''' + + return self.pfsense.phpshell(cmd) + + ############################## + # Logging + # + @staticmethod + def _get_obj_name(): + """ return obj's name """ + return "vip" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + if before is None: + for param in VIRTUALIP_ARGUMENT_SPEC: + if param not in skip_list: + if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool) + else: + values += self.format_cli_field(self.obj, param) + else: + for param in VIRTUALIP_ARGUMENT_SPEC: + if param not in skip_list: + if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool': + values += self.format_updated_cli_field(self.obj, before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + else: + values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values), log_none=False) + + return values + + +def main(): + module = AnsibleModule( + argument_spec=VIRTUALIP_ARGUMENT_SPEC, + required_if=VIRTUALIP_REQUIRED_IF, + supports_check_mode=True) + + pfmodule = PFSenseVirtualIPModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From 31b98c966713ba4167b1b43adb255c21725f5142 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Tue, 28 Jan 2025 20:34:47 -0700 Subject: [PATCH 2/3] [pfsense_virtualip] Drop unused imports --- plugins/modules/pfsense_virtualip.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/modules/pfsense_virtualip.py b/plugins/modules/pfsense_virtualip.py index 197f6a02..b1ad1831 100644 --- a/plugins/modules/pfsense_virtualip.py +++ b/plugins/modules/pfsense_virtualip.py @@ -4,6 +4,7 @@ # Copyright: (c) 2018, Frederic Bor # Copyright: (c) 2021, Jan Wenzel # Copyright: (c) 2023, Martin Müller +# Copyright: (c) 2025, Orion Poplawski # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function @@ -98,8 +99,6 @@ RETURN = """ """ -import re -from copy import deepcopy from ansible.module_utils.basic import AnsibleModule from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase From c0faf2db1e9f297486a53bd6b93053dff69d7fc9 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Tue, 28 Jan 2025 21:11:26 -0700 Subject: [PATCH 3/3] [pfsense_virtualip] Doc cleanup; Generate uniqid --- plugins/modules/pfsense_virtualip.py | 46 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/plugins/modules/pfsense_virtualip.py b/plugins/modules/pfsense_virtualip.py index b1ad1831..3f15eb25 100644 --- a/plugins/modules/pfsense_virtualip.py +++ b/plugins/modules/pfsense_virtualip.py @@ -22,59 +22,61 @@ author: Jan Wenzel (@coffeelover) short_description: Manage pfSense virtual ip settings description: - - Manage pfSense virtual ip settings + - Manage pfSense virtual ip settings. notes: options: mode: - description: Type + description: Type of the virtual ip. required: True type: str choices: [ "proxyarp", "carp", "ipalias", "other" ] descr: - description: Description + description: Description of the virtual ip. required: False type: str interface: - description: Interface + description: Interface of the virtual ip. required: True type: str vhid: - description: VHID Group + description: VHID Group of the virtual ip. Required if `mode` is `carp`, unused for others. Valid value is from 1 to 255. required: False type: int advbase: - description: Advertising Frequency Base + description: + - Advertising frequency base. Used with `mode` of `carp`. The frequency that this machine will advertise. 0 means usually master. + - Otherwise the lowest combination of both base and skew values in the cluster determines the master. + - Valid value is from 1 to 254. required: False type: int advskew: - description: Advertising Frequency Skew + description: Advertising frequency skew. + - Advertising frequency skew. Used with `mode` of `carp`. The frequency that this machine will advertise. 0 means usually master. + - Otherwise the lowest combination of both base and skew values in the cluster determines the master. + - Valid value is from 0 to 254. required: False type: int password: - description: Virtual IP Password - required: False - type: str - uniqid: - description: Unique ID of Virtual IP in configuration + description: Virtual IP password. Required if `mode` is `carp`, unused for others. required: False type: str type: - description: Address Type + description: Address type - `single` or `network. Used for `proxyarp` or `other`. required: False type: str - choices: [ "single" ] + choices: [ "single", "network" ] default: single subnet_bits: - description: Network's subnet mask + description: Network's subnet mask if address type is `network`. required: False type: int default: 32 subnet: - description: Network subnet + description: Network address or subnet. required: False type: str state: - description: State in which to leave the Virtual IP + description: State in which to leave the Virtual IP. choices: [ "present", "absent" ] default: present type: str @@ -87,10 +89,9 @@ descr: "HOME VIP" interface: "opt2" vhid: 24 - advbase: 1, - advskew: 0, + advbase: 1 + advskew: 0 password": "xaequae0sheiB7sh" - uniqid": "vip_home" subnet_bits": 24 subnet": "10.1.1.1" state": "present" @@ -110,7 +111,6 @@ advskew=dict(type='int'), advbase=dict(type='int'), password=dict(type='str', no_log=True), - uniqid=dict(type='str'), descr=dict(type='str'), type=dict(type='str', choices=['single'], default='single'), subnet_bits=dict(type='int', default=32), @@ -119,8 +119,7 @@ ) VIRTUALIP_REQUIRED_IF = [ - ["mode", "carp", ["uniqid", "password", "vhid", "advbase"]], - ["mode", "ipalias", ["uniqid"]], + ["mode", "carp", ["password", "vhid", "advbase", "advskew"]], ] @@ -193,6 +192,7 @@ def _validate_params(self): # def _create_target(self): """ create the XML target_elt """ + self.obj['uniqid'] = self.pfsense.uniqid() return self.pfsense.new_element('vip') def _find_target(self):