From 13642e2b7805b3aab2bc635c83bd6f8e0430115f Mon Sep 17 00:00:00 2001 From: Diogo Ferrario Date: Tue, 30 Sep 2025 12:57:32 +1000 Subject: [PATCH] feat: add lagg support --- plugins/module_utils/lagg.py | 129 +++++++++++++++++++++++++++++++ plugins/modules/pfsense_lagg.py | 133 ++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 plugins/module_utils/lagg.py create mode 100644 plugins/modules/pfsense_lagg.py diff --git a/plugins/module_utils/lagg.py b/plugins/module_utils/lagg.py new file mode 100644 index 00000000..7844aa17 --- /dev/null +++ b/plugins/module_utils/lagg.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2023, Example +# 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 + +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +LAGG_ARGUMENT_SPEC = dict( + state=dict(default='present', choices=['present', 'absent']), + laggif=dict(required=True, type='str'), # z.B. "lagg0", "lagg1" usw. + members=dict(required=True, type='list', elements='str'), + proto=dict(default='lacp', choices=['lacp', 'failover', 'loadbalance', 'roundrobin', 'none']), + lacptimeout=dict(default='fast', choices=['fast', 'slow']), + lagghash=dict(default='l2,l3,l4', type='str'), + descr=dict(default='', type='str'), +) + + +class PFSenseLaggModule(PFSenseModuleBase): + @staticmethod + def get_argument_spec(): + return LAGG_ARGUMENT_SPEC + + def __init__(self, module, pfsense=None): + super(PFSenseLaggModule, self).__init__(module, pfsense) + self.name = "pfsense_lagg" + self.root_elt = self.pfsense.get_element('laggs') + self.obj = dict() + if self.root_elt is None: + self.root_elt = self.pfsense.new_element('laggs') + self.pfsense.root.append(self.root_elt) + + self.setup_lagg_cmds = "" + + def _params_to_obj(self): + params = self.params + obj = dict() + + obj['laggif'] = params['laggif'] + obj['members'] = ",".join(params['members']) + obj['proto'] = params['proto'] + obj['lacptimeout'] = params['lacptimeout'] + obj['lagghash'] = params['lagghash'] + obj['descr'] = params['descr'] + + return obj + + def _validate_params(self): + if not self.params['members']: + self.module.fail_json(msg="members muss mindestens ein Interface enthalten.") + + def _find_target(self): + requested = self.obj['laggif'] + for lagg_node in self.root_elt.findall('lagg'): + node_laggif = lagg_node.findtext('laggif') or '' + if node_laggif.lower() == requested.lower(): + self.obj['laggif'] = node_laggif + return lagg_node + return None + + def _create_target(self): + return self.pfsense.new_element('lagg') + + def _copy_and_add_target(self): + super(PFSenseLaggModule, self)._copy_and_add_target() + self.setup_lagg_cmds += self._cmd_create() + + def _copy_and_update_target(self): + before_laggif = self.target_elt.find('laggif').text + (before, changed) = super(PFSenseLaggModule, self)._copy_and_update_target() + if changed: + self.setup_lagg_cmds += "pfSense_interface_destroy('{}');\n".format(before_laggif) + self.setup_lagg_cmds += self._cmd_create() + return (before, changed) + + def _pre_remove_target_elt(self): + if self.pfsense.get_interface_by_port(self.obj['laggif']) is not None: + self.module.fail_json( + msg="LAGG {} is in use therefore you can't delete it.".format(self.obj['laggif']) + ) + self.setup_lagg_cmds += "pfSense_interface_destroy('{}');\n".format(self.obj['laggif']) + + def _cmd_create(self): + cmd = "$lagg = array();\n" + cmd += "$lagg['laggif'] = '{}';\n".format(self.obj['laggif']) + cmd += "$lagg['members'] = '{}';\n".format(self.obj['members']) + cmd += "$lagg['descr'] = '{}';\n".format(self.obj['descr']) + cmd += "$lagg['proto'] = '{}';\n".format(self.obj['proto']) + cmd += "$lagg['lacptimeout'] = '{}';\n".format(self.obj['lacptimeout']) + cmd += "$lagg['lagghash'] = '{}';\n".format(self.obj['lagghash']) + cmd += "$laggif = interface_lagg_configure($lagg);\n" + cmd += "if (($laggif == NULL) || ($laggif != $lagg['laggif'])) {\n" + cmd += " pfSense_interface_destroy('{}');\n".format(self.obj['laggif']) + cmd += "} else {\n" + interface = self.pfsense.get_interface_by_port(self.obj['laggif']) + if interface is not None: + cmd += " interface_configure('{}', true);\n".format(interface) + cmd += "}\n" + return cmd + + def get_update_cmds(self): + cmd = 'require_once("filter.inc");\n' + if self.setup_lagg_cmds: + cmd += 'require_once("interfaces.inc");\n' + cmd += self.setup_lagg_cmds + cmd += "if (filter_configure() == 0) { clear_subsystem_dirty('filter'); }\n" + return cmd + + def _update(self): + return self.pfsense.phpshell(self.get_update_cmds()) + + def _get_obj_name(self): + return "'{}'".format(self.obj['laggif']) + + def _log_fields(self, before=None): + vals = '' + if before is None: + # Neu + vals += self.format_cli_field(self.obj, 'proto') + vals += self.format_cli_field(self.obj, 'members') + vals += self.format_cli_field(self.obj, 'descr') + else: + vals += self.format_updated_cli_field(self.obj, before, 'proto', add_comma=(vals)) + vals += self.format_updated_cli_field(self.obj, before, 'members', add_comma=(vals)) + vals += self.format_updated_cli_field(self.obj, before, 'descr', add_comma=(vals)) + return vals \ No newline at end of file diff --git a/plugins/modules/pfsense_lagg.py b/plugins/modules/pfsense_lagg.py new file mode 100644 index 00000000..22dc7fde --- /dev/null +++ b/plugins/modules/pfsense_lagg.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2023, Example +# 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 + +DOCUMENTATION = r""" +--- +module: pfsense_lagg +version_added: "0.1.0" +author: + - "Your Name (@your_github_handle)" +short_description: Manage pfSense LAGG (Link Aggregations) +description: + - This module manages pfSense LAGG interfaces (Link Aggregation). It can create, update, or remove LAGGs. +options: + laggif: + description: + - The name of the LAGG interface (e.g. C(lagg0), C(lagg1), etc.). + - Make sure this matches what pfSense actually stores in its config (case sensitivity may matter unless you + implement case-insensitive matching in your code). + required: true + type: str + members: + description: + - A list of physical interfaces to be aggregated, e.g. C(['igb0','igb1']). + - They must exist on the pfSense device and should not be part of any other LAGG. + required: true + type: list + elements: str + proto: + description: + - The LAGG protocol to use. Available protocols: + - C(none): Disables any traffic on this LAGG without disabling the interface itself. + - C(lacp): Uses the IEEE 802.3ad Link Aggregation Control Protocol (LACP) and the Marker Protocol. + Negotiates aggregable links with the peer into one or more Link Aggregated Groups. + - C(failover): Sends and receives traffic through the master port only. If the master port + becomes unavailable, the next active port is used. + - C(loadbalance): Balances outgoing traffic across active ports based on hashed protocol header + information, and accepts incoming traffic from any active port. (Static setup, no dynamic negotiation.) + - C(roundrobin): Distributes outgoing traffic in a round-robin fashion through all active ports, + and accepts incoming traffic from any active port. + choices: ["none", "lacp", "failover", "loadbalance", "roundrobin"] + default: "lacp" + type: str + lacptimeout: + description: + - LACP timeout mode (only relevant if proto = lacp). + - C(fast) or C(slow). Typically defaults to fast on pfSense. + choices: ["fast", "slow"] + default: "fast" + type: str + lagghash: + description: + - Hash method for load distribution. + - Possible options are: + - (l2,l3,l4) layer 2/3/4 (default) + - (l2) layer 2 (MAC addresses) + - (l3) layer 3 (IP addresses) + - (l4) layer 4 (Port numbers) + - (l2,l3) layer 2/3 (MAC + IP) + - (l3,l4) layer 3/4 (IP + Port) + - (l2,l4) layer 2/4 (MAC + Port) + default: "l2,l3,l4" + type: str + descr: + description: + - Description for the LAGG interface, for reference only (not parsed except for display). + default: "" + type: str + state: + description: + - Whether the LAGG should be present (created/updated) or absent (removed). + choices: ["present", "absent"] + default: "present" + type: str +""" + +EXAMPLES = r""" +- name: Create a LAGG (lacp) with igb0 and igb1 + pfsense_lagg: + laggif: lagg1 + members: + - igb0 + - igb1 + proto: lacp + lacptimeout: fast + lagghash: "l2,l3,l4" + descr: "WAN-LACP" + state: present + +- name: Remove that LAGG + pfsense_lagg: + laggif: lagg1 + members: + - igb0 + - igb1 + state: absent +""" + +RETURN = r""" +commands: + description: A list of pseudo-CLI commands that the module generated (for debugging purposes). + returned: always + type: list + sample: + - "create lagg 'lagg1', proto='lacp', members='igb0,igb1'" + - "update lagg 'lagg1', set proto='failover'" + - "delete lagg 'lagg1'" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.lagg import ( + PFSenseLaggModule, + LAGG_ARGUMENT_SPEC +) + +def main(): + module = AnsibleModule( + argument_spec=LAGG_ARGUMENT_SPEC, + supports_check_mode=True + ) + + pfmodule = PFSenseLaggModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() \ No newline at end of file