From c90a405ae99896e928d190a79a609f9ca2bbcc23 Mon Sep 17 00:00:00 2001 From: kdhlab Date: Fri, 13 Sep 2024 12:10:17 -0400 Subject: [PATCH 1/4] Test --- .../module_utils/interfaces_settings_utils.py | 424 ++++++++++++++++++ plugins/module_utils/module_index.py | 31 ++ plugins/modules/interfaces_settings.py | 271 +++++++++++ 3 files changed, 726 insertions(+) create mode 100644 plugins/module_utils/interfaces_settings_utils.py create mode 100644 plugins/modules/interfaces_settings.py diff --git a/plugins/module_utils/interfaces_settings_utils.py b/plugins/module_utils/interfaces_settings_utils.py new file mode 100644 index 00000000..4754b341 --- /dev/null +++ b/plugins/module_utils/interfaces_settings_utils.py @@ -0,0 +1,424 @@ +# Copyright: (c) 2024, Puzzle ITC, Kilian Soltermann +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +interfaces_settings_utils module_utils: Module_utils to configure OPNsense interface settings +""" + +from dataclasses import dataclass, asdict, field +from typing import List, Optional, Dict, Any + + +from xml.etree.ElementTree import Element, ElementTree, SubElement + +from ansible_collections.puzzle.opnsense.plugins.module_utils import ( + xml_utils, + opnsense_utils, +) +from ansible_collections.puzzle.opnsense.plugins.module_utils.config_utils import ( + OPNsenseModuleConfig, +) + +class OPNSenseInterfaceNotFoundError(Exception): + """ + Exception raised when an Interface is not found. + """ + +class OPNSenseGetInterfacesError(Exception): + """ + Exception raised if the function can't query the local device + """ + + +@dataclass +class InterfaceSetting: + """ + Represents a network interface with optional description and extra attributes. + + Attributes: + identifier (str): Unique ID for the interface. + descr (Optional[str]): Description of the interface. + extra_attrs (Dict[str, Any]): Additional attributes for configuration. + + Methods: + __init__: Initializes with ID, device, and optional description. + from_xml: Creates an instance from XML. + to_etree: Serializes instance to XML, handling special cases. + from_ansible_module_params: Creates from Ansible params. + """ + + identifier: str + #device: str + descr: Optional[str] = None + + # since only the above attributes are needed, the rest is handled here + extra_attrs: Dict[str, Any] = field(default_factory=dict, repr=False) + + def __init__( + self, + identifier: Optional[str] = None, + #device: Optional[str] = None, + descr: Optional[str] = None, + **kwargs, + ): + if identifier is not None: + self.identifier = identifier + # if device is not None: + # self.device = device + if descr is not None: + self.descr = descr + self.extra_attrs = kwargs + + @staticmethod + def from_xml(element: Element) -> "InterfaceSetting": + """ + Converts XML element to InterfaceSetting instance. + + Args: + element (Element): XML element representing an interface. + + Returns: + InterfaceSetting: An instance with attributes derived from the XML. + + Processes XML to dict, assigning 'identifier' and 'device' from keys and + 'if' element. Assumes single key processing. + """ + + interface_setting_dict: dict = xml_utils.etree_to_dict(element) + + for key, value in interface_setting_dict.items(): + value["descr"] = key # Move the key to a new "identifier" field + if "if" in value: + if_key = value.pop("if", None) + if if_key is not None: + value["if"] = if_key + break # Only process the first key, assuming there's only one + + # Return only the content of the dictionary without the key + return InterfaceSetting(**interface_setting_dict.popitem()[1]) + + def to_etree(self) -> Element: + """ + Serializes the instance to an XML Element, including extra attributes. + + Returns: + Element: XML representation of the instance. + + Creates an XML element with identifier, and description. Handles + serialization of additional attributes, excluding specified exceptions and + handling specific attribute cases like alias and DHCP options. Assumes + boolean values translate to '1' for true. + """ + + interface_setting_dict: dict = asdict(self) + + exceptions = ["dhcphostname", "mtu", "subnet", "gateway", "media", "mediaopt"] + + # Create the main element + main_element = Element(interface_setting_dict["identifier"]) + + # Special handling for 'device' and 'descr' + # SubElement(main_element, "if").text = interface_setting_dict.get("device") + SubElement(main_element, "descr").text = interface_setting_dict.get("descr") + + # handle special cases + if getattr(self, "alias-subnet", None): + interface_setting_dict["extra_attrs"]["alias-subnet"] = getattr( + self, "alias-subnet", None + ) + + interface_setting_dict["extra_attrs"]["alias-address"] = getattr( + self, "alias-address", None + ) + + if getattr(self, "dhcp6-ia-pd-len", None): + interface_setting_dict["extra_attrs"]["dhcp6-ia-pd-len"] = getattr( + self, "dhcp6-ia-pd-len", None + ) + + if getattr(self, "track6-interface", None): + interface_setting_dict["extra_attrs"]["track6-interface"] = getattr( + self, "track6-interface", None + ) + + if getattr(self, "track6-prefix-id", None): + interface_setting_dict["extra_attrs"]["track6-prefix-id"] = getattr( + self, "track6-prefix-id", None + ) + + # Serialize extra attributes + for key, value in interface_setting_dict["extra_attrs"].items(): + if ( + key + in [ + "spoofmac", + "alias-address", + "alias-subnet", + "dhcp6-ia-pd-len", + "adv_dhcp_pt_timeout", + "adv_dhcp_pt_retry", + "adv_dhcp_pt_select_timeout", + "adv_dhcp_pt_reboot", + "adv_dhcp_pt_backoff_cutoff", + "adv_dhcp_pt_initial_interval", + "adv_dhcp_pt_values", + "adv_dhcp_send_options", + "adv_dhcp_request_options", + "adv_dhcp_required_options", + "adv_dhcp_option_modifiers", + "adv_dhcp_config_advanced", + "adv_dhcp_config_file_override", + "adv_dhcp_config_file_override_path", + "dhcprejectfrom", + "track6-interface", + "track6-prefix-id", + ] + and value is None + ): + sub_element = SubElement(main_element, key) + if value is None and key not in exceptions: + continue + sub_element = SubElement(main_element, key) + if value is True: + sub_element.text = "1" + elif value is not None: + sub_element.text = str(value) + + return main_element + + @classmethod + def from_ansible_module_params(cls, params: dict) -> "InterfaceSetting": + """ + Creates an instance from Ansible module parameters. + + Args: + params (dict): Parameters from an Ansible module. + + Returns: + User: An instance of InterfaceSetting. + + Filters out None values from the provided parameters and uses them to + instantiate the class, focusing on 'identifier', 'device', and 'descr'. + """ + + interface_setting_dict = { + "identifier": params.get("identifier"), + # "device": params.get("device"), + "descr": params.get("description"), + } + + interface_setting_dict = { + key: value + for key, value in interface_setting_dict.items() + if value is not None + } + + return cls(**interface_setting_dict) + + +class InterfacesSet(OPNsenseModuleConfig): + """ + Manages network interface interfaces for OPNsense configurations. + + Inherits from OPNsenseModuleConfig, offering methods for managing + interface interfaces within an OPNsense config file. + + Attributes: + _interfaces_settings (List[InterfaceSetting]): List of interfaces. + + Methods: + __init__(self, path="/conf/config.xml"): Initializes InterfacesSet and loads interfaces. + _load_interfaces() -> List["interface_setting"]: Loads interface interfaces from config. + changed() -> bool: Checks if current interfaces differ from the loaded ones. + update(InterfaceSetting: InterfaceSetting): Updates an interface, + errors if not found. + find(**kwargs) -> Optional[InterfaceSetting]: Finds an interface matching + specified attributes. + save() -> bool: Saves changes to the config file if there are modifications. + """ + + _interfaces_settings: List[InterfaceSetting] + + def __init__(self, path: str = "/conf/config.xml"): + super().__init__( + module_name="interfaces_settings", + config_context_names=["interfaces"], + path=path, + ) + + self._config_xml_tree = self._load_config() + self._interfaces_settings = self._load_interfaces() + + def _load_interfaces(self) -> List["InterfaceSetting"]: + + element_tree_interfaces: Element = self.get("interfaces") + + return [ + InterfaceSetting.from_xml(element_tree_interface) + for element_tree_interface in element_tree_interfaces + ] + + @property + def changed(self) -> bool: + """ + Evaluates whether there have been changes to user or group configurations that are not yet + reflected in the saved system configuration. This property serves as a check to determine + if updates have been made in memory to the user or group lists that differ from what is + currently persisted in the system's configuration files. + Returns: + bool: True if there are changes to the user or group configurations that have not been + persisted yet; False otherwise. + The method works by comparing the current in-memory representations of users and groups + against the versions loaded from the system's configuration files. A difference in these + lists indicates that changes have been made in the session that have not been saved, thus + prompting the need for a save operation to update the system configuration accordingly. + Note: + This property should be consulted before performing a save operation to avoid + unnecessary writes to the system configuration when no changes have been made. + """ + + return bool(str(self._interfaces_settings) != str(self._load_interfaces())) + + def get_interfaces(self) -> List[InterfaceSetting]: + """ + Retrieves a list of interface interfaces from an OPNSense device via a PHP function. + + The function queries the device using specified PHP requirements and config functions. + It processes the stdout, extracts interface data, and handles errors. + + Returns: + list[InterfaceSetting]: A list of interface interfaces parsed + from the PHP function's output. + + Raises: + OPNSenseGetInterfacesError: If an error occurs during the retrieval + or parsing process, + or if no interfaces are found. + """ + + # load requirements + php_requirements = self._config_maps["interfaces_settings"][ + "php_requirements" + ] + php_command = """ + /* get physical network interfaces */ + foreach (get_interface_list() as $key => $item) { + echo $key.','; + } + /* get virtual network interfaces */ + foreach (plugins_devices() as $item){ + foreach ($item["names"] as $key => $if ) { + echo $key.','; + } + } + """ + + # run php function + result = opnsense_utils.run_command( + php_requirements=php_requirements, + command=php_command, + ) + + # check for stderr + if result.get("stderr"): + raise OPNSenseGetInterfacesError( + "error encounterd while getting interfaces" + ) + + # parse list + interface_list: list[str] = [ + item.strip() + for item in result.get("stdout").split(",") + if item.strip() and item.strip() != "None" + ] + + # check parsed list length + if len(interface_list) < 1: + raise OPNSenseGetInterfacesError( + "error encounterd while getting interfaces, less than one interface available" + ) + + return interface_list + + def update(self, interface_setting: InterfaceSetting) -> None: + """ + Updates an interface setting in the set. + + Checks for interface existence and updates or raises errors accordingly. + + Args: + interface_setting (InterfaceSetting): The interface interface to update. + + Raises: + OPNSenseDeviceNotFoundError: If device is not found. + OPNSenseInterfaceNotFoundError: If device is not found. + """ + + def find(self, **kwargs) -> Optional[InterfaceSetting]: + """ + Searches for an interface interface that matches given criteria. + + Iterates through the list of interface interfaces, checking if each one + matches all provided keyword arguments. If a match is found, returns the + corresponding interface interface. If no match is found, returns None. + + Args: + **kwargs: Key-value pairs to match against attributes of interface interfaces. + + Returns: + Optional[InterfaceSetting]: The first interface interface that matches + the criteria, or None if no match is found. + """ + + for interface_setting in self._interfaces_settings: + match = all( + getattr(interface_setting, key, None) == value + for key, value in kwargs.items() + ) + if match: + return interface_setting + return None + + def save(self) -> bool: + """ + Saves the current state of interface interfaces to the OPNsense configuration file. + + Checks if there have been changes to the interface interfaces. If not, it + returns False indicating no need to save. It then locates the parent element + for interface interfaces in the XML tree and replaces existing entries with + the updated set from memory. After updating, it writes the new XML tree to + the configuration file and reloads the configuration to reflect changes. + + Returns: + bool: True if changes were saved successfully, False if no changes were detected. + + Note: + This method assumes that 'parent_element' correctly refers to the container + of interface elements within the configuration file. + """ + + if not self.changed: + return False + + # Use 'find' to get the single parent element + parent_element = self._config_xml_tree.find( + self._config_maps["interfaces_settings"]["interfaces"] + ) + + # Assuming 'parent_element' correctly refers to the container of interface elements + for interface_element in list(parent_element): + parent_element.remove(interface_element) + + # Now, add updated interface elements + parent_element.extend( + [ + interface_setting.to_etree() + for interface_setting in self._interfaces_settings + ] + ) + + # Write the updated XML tree to the file + tree = ElementTree(self._config_xml_tree) + tree.write(self._config_path, encoding="utf-8", xml_declaration=True) + + return True diff --git a/plugins/module_utils/module_index.py b/plugins/module_utils/module_index.py index 1194c8ba..0a4a6d3a 100644 --- a/plugins/module_utils/module_index.py +++ b/plugins/module_utils/module_index.py @@ -770,6 +770,37 @@ }, }, }, + "interfaces_settings": { + "interfaces": "interfaces", + "identifier": "identifier", + "device": "device", + "description": "description", + "enabled": "enabled", + "locked": "locked", + "block_private": "block_private", + "block_bogons": "block_bogons", + "ipv4_address": "ipv4_address", + "ipv4_subnet": "ipv4_subnet", + "ipv4_gateway": "ipv4_gateway", + "ipv6_address": "ipv6_address", + "ipv6_subnet": "ipv6_subnet", + "ipv6_gateway": "ipv6_gateway", + "track6_interface": "track6_interface", + "track6_prefix_id": "track6_prefix_id", + "mac_address": "mac_address", + "promiscuous_mode": "promiscuous_mode", + "mtu": "mtu", + "mss": "mss", + "dynamic_gateway": "dynamic_gateway", + "php_requirements": [ + "/usr/local/etc/inc/config.inc", + "/usr/local/etc/inc/interfaces.inc", + "/usr/local/etc/inc/filter.inc", + "/usr/local/etc/inc/util.inc", + "/usr/local/etc/inc/system.inc", + ], + "configure_functions": {}, + }, "system_high_availability_settings": { # Add other mappings here "hasync": "hasync", diff --git a/plugins/modules/interfaces_settings.py b/plugins/modules/interfaces_settings.py new file mode 100644 index 00000000..836e56e2 --- /dev/null +++ b/plugins/modules/interfaces_settings.py @@ -0,0 +1,271 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2024, Kilian Soltermann , Puzzle ITC +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""interfaces_settings module: Module to configure OPNsense interface settings""" + +# pylint: disable=duplicate-code +__metaclass__ = type + +# https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html +# fmt: off +DOCUMENTATION = r''' +--- +author: + - Kyle Hammond (@kdhlab) +module: interfaces_settings +version_added: "1.2.1" +short_description: This module can be used to configure assigned interface settings +description: + - Module to configure interface settings. +options: + identifier: + description: + - "Technical identifier of the interface, used by hasync for example" + type: str + required: true + device: + description: + - Physical Device Name eg. vtnet0, ipsec1000 etc,. + type: str + required: true + description: + description: + - Interface name shown in the GUI. Identifier in capital letters if not provided. + - Input will be trimmed, as no whitespaces are allowed. + type: str + required: false + enabled: + description: + - Enable or disable the interface + type: bool + required: false + locked: + description: + - Prevent interface removal + type: bool + required: false + block_private: + description: + - When set, this option blocks traffic from IP addresses that are reserved for private networks as per RFC 1918 (10/8, 172.16/12, 192.168/16) as well as loopback addresses (127/8) and Carrier-grade NAT addresses (100.64/10). This option should only be set for WAN interfaces that use the public IP address space. + type: bool + required: false + block_bogons: + description: + - When set, this option blocks traffic from IP addresses that are reserved for private networks as per RFC 1918 (10/8, 172.16/12, 192.168/16) as well as loopback addresses (127/8) and Carrier-grade NAT addresses (100.64/10). This option should only be set for WAN interfaces that use the public IP address space. + type: bool + required: false + ipv4_configuration_type: + description: + - + type: str + required: false + ipv6_configuration_type: + description: + - + type: str + required: false + ipv4_address: + description: + - + type: str + required: false + ipv4_subnet: + description: + - + type: int + required: false + ipv4_gateway: + description: + - + type: str + required: false + ipv6_address: + description: + - + type: str + required: false + ipv6_subnet: + description: + - + type: int + required: false + ipv6_gateway: + description: + - + type: str + required: false + track6_interface: + description: + - + type: str + required: false + track6_prefix_id: + description: + - + type: int + required: false + mac_address: + description: + - + type: str + required: false + promiscuous_mode: + description: + - + type: bool + required: false + mtu: + description: + - If you leave this field blank, the adapter's default MTU will be used. This is typically 1500 bytes but can vary in some circumstances. + type: int + required: false + mss: + description: + - If you enter a value in this field, then MSS clamping for TCP connections to the value entered above minus 40 (IPv4) or 60 (IPv6) will be in effect (TCP/IP header size). + type: int + required: false + dynamic_gateway: + description: + - If the destination is directly reachable via an interface requiring no intermediary system to act as a gateway, you can select this option which allows dynamic gateways to be created without direct target addresses. Some tunnel types support this. + type: bool + required: false +''' + +EXAMPLES = r''' +- name: Assign Vagrant interface to device em4 + puzzle.opnsense.interfaces_settings: + identifier: "VAGRANT" + device: "em4" + +- name: Create new assignment + puzzle.opnsense.interfaces_settings: + identifier: "lan" + device: "vtnet1" + description: "lan_interface" +''' + +RETURN = ''' +opnsense_configure_output: + description: A list of the executed OPNsense configure function along with their respective stdout, stderr and rc + returned: always + type: list + sample: + - function: filter_configure + params: + - 'true' + rc: 0 + stderr: '' + stderr_lines: [] + stdout: '' + stdout_lines: [] + + - function: rrd_configure + params: + - 'true' + rc: 0 + stderr: '' + stderr_lines: [] + stdout: Generating RRD graphs...done. + stdout_lines: + - Generating RRD graphs...done. +''' +# fmt: on + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.puzzle.opnsense.plugins.module_utils.interfaces_settings_utils import ( + InterfacesSet, + InterfaceSetting, + OPNSenseDeviceNotFoundError, + #OPNSenseDeviceAlreadyAssignedError, + OPNSenseGetInterfacesError, +) + + +def main(): + """ + Main function of the interfaces_settings module + """ + + module_args = { + "identifier": {"type": "str", "required": False}, + #"device": {"type": "str", "required": False}, + "description": {"type": "str", "required": False}, + "enabled": {"type": "bool", "required": False, "default": False}, + "locked": {"type": "bool", "required": False, "default": False}, + "block_private": {"type": "bool", "required": False, "default": False}, + "block_bogons": {"type": "bool", "required": False, "default": False}, + "ipv4_address": {"type": "str", "required": False}, + "ipv4_subnet": {"type": "int", "required": False}, + "ipv4_gateway": {"type": "str", "required": False}, + "ipv6_address": {"type": "str", "required": False}, + "ipv6_subnet": {"type": "int", "required": False}, + "ipv6_gateway": {"type": "str", "required": False}, + "track6_interface": {"type": "str", "required": False}, + "track6_prefix_id": {"type": "int", "required": False}, + "mac_address": {"type": "str", "required": False}, + "promiscuous_mode": {"type": "bool", "required": False, "default": False}, + "mtu": {"type": "int", "required": False}, + "mss": {"type": "int", "required": False}, + "dynamic_gateway": {"type": "bool", "required": False, "default": False}, + } + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + required_one_of=[ + ["identifier", "description"], + ], + ) + + # https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html + # https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html#return-block + result = { + "changed": False, + "invocation": module.params, + "diff": None, + } + + interface_setting = InterfaceSetting.from_ansible_module_params(module.params) + + with InterfacesSet() as interfaces_set: + + try: + interfaces_set.update(interface_setting) + + except ( + OPNSenseDeviceNotFoundError + ) as opnsense_device_not_found_error_error_message: + module.fail_json(msg=str(opnsense_device_not_found_error_error_message)) + + # except ( + # OPNSenseDeviceAlreadyAssignedError + # ) as opnsense_device_already_assigned_error_message: + # module.fail_json(msg=str(opnsense_device_already_assigned_error_message)) + + except OPNSenseGetInterfacesError as opnsense_get_interfaces_error_message: + module.fail_json(msg=str(opnsense_get_interfaces_error_message)) + + if interfaces_set.changed: + result["diff"] = interfaces_set.diff + result["changed"] = True + + if interfaces_set.changed and not module.check_mode: + interfaces_set.save() + result["opnsense_configure_output"] = interfaces_set.apply_settings() + + for cmd_result in result["opnsense_configure_output"]: + if cmd_result["rc"] != 0: + module.fail_json( + msg="Apply of the OPNsense settings failed", + details=cmd_result, + ) + + # Return results + module.exit_json(**result) + + +if __name__ == "__main__": + main() From 7efffa46daddd8258e8a1f241084e9dae016b22e Mon Sep 17 00:00:00 2001 From: kdhlab Date: Fri, 13 Sep 2024 13:06:07 -0400 Subject: [PATCH 2/4] testing --- .../interfaces_assignments_utils.py | 15 ++- .../module_utils/interfaces_settings_utils.py | 1 + plugins/modules/interfaces_assignments.py | 112 ++++++++++++++++++ plugins/modules/interfaces_settings.py | 51 ++++++-- 4 files changed, 162 insertions(+), 17 deletions(-) diff --git a/plugins/module_utils/interfaces_assignments_utils.py b/plugins/module_utils/interfaces_assignments_utils.py index 56d818fd..e2aced60 100644 --- a/plugins/module_utils/interfaces_assignments_utils.py +++ b/plugins/module_utils/interfaces_assignments_utils.py @@ -59,6 +59,7 @@ class InterfaceAssignment: identifier: str device: str descr: Optional[str] = None + enable: Optional[bool] = False # since only the above attributes are needed, the rest is handled here extra_attrs: Dict[str, Any] = field(default_factory=dict, repr=False) @@ -68,6 +69,7 @@ def __init__( identifier: str, device: str, descr: Optional[str] = None, + enable: Optional[bool] = False, **kwargs, ): self.identifier = identifier @@ -127,7 +129,10 @@ def to_etree(self) -> Element: # Special handling for 'device' and 'descr' SubElement(main_element, "if").text = interface_assignment_dict.get("device") SubElement(main_element, "descr").text = interface_assignment_dict.get("descr") - + if getattr(self, "enable", True): + SubElement(main_element, "enable").text = "1" + else: + SubElement(main_element, "enable").text = "0" # handle special cases if getattr(self, "alias-subnet", None): interface_assignment_dict["extra_attrs"]["alias-subnet"] = getattr( @@ -152,7 +157,6 @@ def to_etree(self) -> Element: interface_assignment_dict["extra_attrs"]["track6-prefix-id"] = getattr( self, "track6-prefix-id", None ) - # Serialize extra attributes for key, value in interface_assignment_dict["extra_attrs"].items(): if ( @@ -212,6 +216,7 @@ def from_ansible_module_params(cls, params: dict) -> "InterfaceAssignment": "identifier": params.get("identifier"), "device": params.get("device"), "descr": params.get("description"), + "enable": params.get("enabled"), } interface_assignment_dict = { @@ -219,7 +224,6 @@ def from_ansible_module_params(cls, params: dict) -> "InterfaceAssignment": for key, value in interface_assignment_dict.items() if value is not None } - return cls(**interface_assignment_dict) @@ -393,6 +397,7 @@ def update(self, interface_assignment: InterfaceAssignment) -> None: identifier=interface_assignment.identifier, device=interface_assignment.device, descr=interface_assignment.descr, + enable=interface_assignment.enable, ) self._interfaces_assignments.append(interface_to_create) @@ -404,14 +409,14 @@ def update(self, interface_assignment: InterfaceAssignment) -> None: or interface_assignment.device == interface_to_update.device ): - if interface_assignment.identifier in identifier_list_set: + if interface_assignment.identifier in identifier_list_set or interface_assignment.device == interface_to_update.device: # Merge extra_attrs interface_assignment.extra_attrs.update(interface_to_update.extra_attrs) # Update the existing interface interface_to_update.__dict__.update(interface_assignment.__dict__) - + else: raise OPNSenseDeviceAlreadyAssignedError( "This device is already assigned, please unassign this device first" diff --git a/plugins/module_utils/interfaces_settings_utils.py b/plugins/module_utils/interfaces_settings_utils.py index 4754b341..8e88c8b0 100644 --- a/plugins/module_utils/interfaces_settings_utils.py +++ b/plugins/module_utils/interfaces_settings_utils.py @@ -151,6 +151,7 @@ def to_etree(self) -> Element: if ( key in [ + "enable", "spoofmac", "alias-address", "alias-subnet", diff --git a/plugins/modules/interfaces_assignments.py b/plugins/modules/interfaces_assignments.py index 2ea53729..4fd23ab9 100644 --- a/plugins/modules/interfaces_assignments.py +++ b/plugins/modules/interfaces_assignments.py @@ -37,6 +37,101 @@ - Input will be trimmed, as no whitespaces are allowed. type: str required: false + enabled: + description: + - Enable or disable the interface + type: bool + required: false + locked: + description: + - Prevent interface removal + type: bool + required: false + block_private: + description: + - When set, this option blocks traffic from IP addresses that are reserved for private networks as per RFC 1918 (10/8, 172.16/12, 192.168/16) as well as loopback addresses (127/8) and Carrier-grade NAT addresses (100.64/10). This option should only be set for WAN interfaces that use the public IP address space. + type: bool + required: false + block_bogons: + description: + - When set, this option blocks traffic from IP addresses that are reserved for private networks as per RFC 1918 (10/8, 172.16/12, 192.168/16) as well as loopback addresses (127/8) and Carrier-grade NAT addresses (100.64/10). This option should only be set for WAN interfaces that use the public IP address space. + type: bool + required: false + ipv4_configuration_type: + description: + - + type: str + required: false + ipv6_configuration_type: + description: + - + type: str + required: false + ipv4_address: + description: + - + type: str + required: false + ipv4_subnet: + description: + - + type: int + required: false + ipv4_gateway: + description: + - + type: str + required: false + ipv6_address: + description: + - + type: str + required: false + ipv6_subnet: + description: + - + type: int + required: false + ipv6_gateway: + description: + - + type: str + required: false + track6_interface: + description: + - + type: str + required: false + track6_prefix_id: + description: + - + type: int + required: false + mac_address: + description: + - + type: str + required: false + promiscuous_mode: + description: + - + type: bool + required: false + mtu: + description: + - If you leave this field blank, the adapter's default MTU will be used. This is typically 1500 bytes but can vary in some circumstances. + type: int + required: false + mss: + description: + - If you enter a value in this field, then MSS clamping for TCP connections to the value entered above minus 40 (IPv4) or 60 (IPv6) will be in effect (TCP/IP header size). + type: int + required: false + dynamic_gateway: + description: + - If the destination is directly reachable via an interface requiring no intermediary system to act as a gateway, you can select this option which allows dynamic gateways to be created without direct target addresses. Some tunnel types support this. + type: bool + required: false ''' EXAMPLES = r''' @@ -98,6 +193,23 @@ def main(): "identifier": {"type": "str", "required": True}, "device": {"type": "str", "required": True}, "description": {"type": "str", "required": False}, + "enabled": {"type": "bool", "required": False, "default": False}, + "locked": {"type": "bool", "required": False, "default": False}, + "block_private": {"type": "bool", "required": False, "default": False}, + "block_bogons": {"type": "bool", "required": False, "default": False}, + "ipv4_address": {"type": "str", "required": False}, + "ipv4_subnet": {"type": "int", "required": False}, + "ipv4_gateway": {"type": "str", "required": False}, + "ipv6_address": {"type": "str", "required": False}, + "ipv6_subnet": {"type": "int", "required": False}, + "ipv6_gateway": {"type": "str", "required": False}, + "track6_interface": {"type": "str", "required": False}, + "track6_prefix_id": {"type": "int", "required": False}, + "mac_address": {"type": "str", "required": False}, + "promiscuous_mode": {"type": "bool", "required": False, "default": False}, + "mtu": {"type": "int", "required": False}, + "mss": {"type": "int", "required": False}, + "dynamic_gateway": {"type": "bool", "required": False, "default": False}, } module = AnsibleModule( diff --git a/plugins/modules/interfaces_settings.py b/plugins/modules/interfaces_settings.py index 836e56e2..42914070 100644 --- a/plugins/modules/interfaces_settings.py +++ b/plugins/modules/interfaces_settings.py @@ -26,11 +26,6 @@ - "Technical identifier of the interface, used by hasync for example" type: str required: true - device: - description: - - Physical Device Name eg. vtnet0, ipsec1000 etc,. - type: str - required: true description: description: - Interface name shown in the GUI. Identifier in capital letters if not provided. @@ -172,17 +167,50 @@ stdout_lines: - Generating RRD graphs...done. ''' +# pylint: enable=duplicate-code # fmt: on - +import ipaddress +from xml.etree import ElementTree +from xml.etree.ElementTree import Element from ansible.module_utils.basic import AnsibleModule -from ansible_collections.puzzle.opnsense.plugins.module_utils.interfaces_settings_utils import ( - InterfacesSet, - InterfaceSetting, - OPNSenseDeviceNotFoundError, - #OPNSenseDeviceAlreadyAssignedError, +from ansible_collections.puzzle.opnsense.plugins.module_utils.config_utils import ( + OPNsenseModuleConfig, + UnsupportedModuleSettingError, + UnsupportedVersionForModule, +) + +from ansible_collections.puzzle.opnsense.plugins.module_utils.interfaces_assignments_utils import ( OPNSenseGetInterfacesError, ) +from ansible_collections.puzzle.opnsense.plugins.module_utils import ( + opnsense_utils, + version_utils, +) + + +def validate_ipv4(ipaddr: str) -> bool: + """ + Check if the given string is an IPv4 address + """ + digits = ipaddr.split(".") + if len(digits) != 4: + return False + for num in digits: + if not (num.isdigit() and int(num) < 256): + return False + return True + + +def validate_ip(ipaddr: str) -> bool: + """ + Check if the given string is an IPv4 or IPv6 address + """ + try: + ipaddress.ip_network(ipaddr, strict=False) + return True + except ValueError: + return False def main(): """ @@ -191,7 +219,6 @@ def main(): module_args = { "identifier": {"type": "str", "required": False}, - #"device": {"type": "str", "required": False}, "description": {"type": "str", "required": False}, "enabled": {"type": "bool", "required": False, "default": False}, "locked": {"type": "bool", "required": False, "default": False}, From f1856f12f257523936bfa6e1bc9ff9be048e16cf Mon Sep 17 00:00:00 2001 From: kdhlab Date: Mon, 16 Sep 2024 16:17:06 -0400 Subject: [PATCH 3/4] Enable/Disable working --- .../interfaces_assignments_utils.py | 44 +++++++++++-------- plugins/modules/interfaces_assignments.py | 10 ++--- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/plugins/module_utils/interfaces_assignments_utils.py b/plugins/module_utils/interfaces_assignments_utils.py index e2aced60..aef117da 100644 --- a/plugins/module_utils/interfaces_assignments_utils.py +++ b/plugins/module_utils/interfaces_assignments_utils.py @@ -7,6 +7,7 @@ from dataclasses import dataclass, asdict, field from typing import List, Optional, Dict, Any +from pprint import pprint from xml.etree.ElementTree import Element, ElementTree, SubElement @@ -68,8 +69,8 @@ def __init__( self, identifier: str, device: str, - descr: Optional[str] = None, enable: Optional[bool] = False, + descr: Optional[str] = None, **kwargs, ): self.identifier = identifier @@ -77,6 +78,7 @@ def __init__( if descr is not None: self.descr = descr self.extra_attrs = kwargs + self.enable = enable @staticmethod def from_xml(element: Element) -> "InterfaceAssignment": @@ -129,10 +131,8 @@ def to_etree(self) -> Element: # Special handling for 'device' and 'descr' SubElement(main_element, "if").text = interface_assignment_dict.get("device") SubElement(main_element, "descr").text = interface_assignment_dict.get("descr") - if getattr(self, "enable", True): - SubElement(main_element, "enable").text = "1" - else: - SubElement(main_element, "enable").text = "0" + if getattr(self, "enable", None): + SubElement(main_element, "enable").text = "1" # handle special cases if getattr(self, "alias-subnet", None): interface_assignment_dict["extra_attrs"]["alias-subnet"] = getattr( @@ -380,19 +380,17 @@ def update(self, interface_assignment: InterfaceAssignment) -> None: raise OPNSenseDeviceNotFoundError( "Device was not found on OPNsense Instance!" ) - - interface_to_update: Optional[InterfaceAssignment] = next( - ( - interface - for interface in self._interfaces_assignments - if interface.device == interface_assignment.device + for interface in self._interfaces_assignments: + if ( + interface.device == interface_assignment.device or interface.identifier == interface_assignment.identifier - ), - None, - ) - + ): + interface_to_update = interface + print(interface_to_update) + break + else: + interface_to_update = None if not interface_to_update: - interface_to_create: InterfaceAssignment = InterfaceAssignment( identifier=interface_assignment.identifier, device=interface_assignment.device, @@ -401,7 +399,7 @@ def update(self, interface_assignment: InterfaceAssignment) -> None: ) self._interfaces_assignments.append(interface_to_create) - + pprint(interface_to_create) return if ( @@ -420,8 +418,18 @@ def update(self, interface_assignment: InterfaceAssignment) -> None: else: raise OPNSenseDeviceAlreadyAssignedError( "This device is already assigned, please unassign this device first" - ) + ) + elif interface_assignment.enable != interface_to_update.enable: + if interface_assignment.enable: + # Merge extra_attrs + interface_assignment.extra_attrs.update(interface_to_update.extra_attrs) + + # Update the existing interface + interface_to_update.__dict__.update(interface_assignment.__dict__) + else: + interface_assignment.enable = False + interface_to_update.__dict__.update(interface_assignment.__dict__) else: raise OPNSenseDeviceAlreadyAssignedError( "This device is already assigned, please unassign this device first" diff --git a/plugins/modules/interfaces_assignments.py b/plugins/modules/interfaces_assignments.py index 4fd23ab9..9a3e2ff8 100644 --- a/plugins/modules/interfaces_assignments.py +++ b/plugins/modules/interfaces_assignments.py @@ -87,11 +87,11 @@ - type: str required: false - ipv6_subnet: - description: - - - type: int - required: false + # ipv6_subnet: + # description: + # - + # type: int + # required: false ipv6_gateway: description: - From 19e41f37fa8a649f7674f9700fc06b6ac18c8a9e Mon Sep 17 00:00:00 2001 From: kdhlab Date: Mon, 16 Sep 2024 17:00:29 -0400 Subject: [PATCH 4/4] Testing --- .../interfaces_assignments_utils.py | 16 +- .../module_utils/interfaces_settings_utils.py | 425 ------------------ plugins/module_utils/module_index.py | 31 -- plugins/modules/interfaces_assignments.py | 10 +- plugins/modules/interfaces_settings.py | 298 ------------ 5 files changed, 20 insertions(+), 760 deletions(-) delete mode 100644 plugins/module_utils/interfaces_settings_utils.py delete mode 100644 plugins/modules/interfaces_settings.py diff --git a/plugins/module_utils/interfaces_assignments_utils.py b/plugins/module_utils/interfaces_assignments_utils.py index aef117da..4fbcf01d 100644 --- a/plugins/module_utils/interfaces_assignments_utils.py +++ b/plugins/module_utils/interfaces_assignments_utils.py @@ -61,6 +61,7 @@ class InterfaceAssignment: device: str descr: Optional[str] = None enable: Optional[bool] = False + lock: Optional[bool] = False # since only the above attributes are needed, the rest is handled here extra_attrs: Dict[str, Any] = field(default_factory=dict, repr=False) @@ -69,8 +70,9 @@ def __init__( self, identifier: str, device: str, - enable: Optional[bool] = False, descr: Optional[str] = None, + enable: Optional[bool] = False, + lock: Optional[bool] = False, **kwargs, ): self.identifier = identifier @@ -133,6 +135,10 @@ def to_etree(self) -> Element: SubElement(main_element, "descr").text = interface_assignment_dict.get("descr") if getattr(self, "enable", None): SubElement(main_element, "enable").text = "1" + # Enumerate the basic attributes if the interface is enabled + + if getattr(self, "lock", None): + SubElement(main_element, "lock").text = "1" # handle special cases if getattr(self, "alias-subnet", None): interface_assignment_dict["extra_attrs"]["alias-subnet"] = getattr( @@ -217,6 +223,14 @@ def from_ansible_module_params(cls, params: dict) -> "InterfaceAssignment": "device": params.get("device"), "descr": params.get("description"), "enable": params.get("enabled"), + "lock": params.get("locked"), + # "blockpriv": params.get("block_private"), + # "blockbogons": params.get("block_bogons"), + # "spoofmac": params.get("mac_address"), + # "promisc": params.get("promiscuous_mode"), + # "mtu": params.get("mtu"), + # "mss": params.get("mss"), + # "gateway_interface": params.get("dynamic_gateway"), } interface_assignment_dict = { diff --git a/plugins/module_utils/interfaces_settings_utils.py b/plugins/module_utils/interfaces_settings_utils.py deleted file mode 100644 index 8e88c8b0..00000000 --- a/plugins/module_utils/interfaces_settings_utils.py +++ /dev/null @@ -1,425 +0,0 @@ -# Copyright: (c) 2024, Puzzle ITC, Kilian Soltermann -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) - -""" -interfaces_settings_utils module_utils: Module_utils to configure OPNsense interface settings -""" - -from dataclasses import dataclass, asdict, field -from typing import List, Optional, Dict, Any - - -from xml.etree.ElementTree import Element, ElementTree, SubElement - -from ansible_collections.puzzle.opnsense.plugins.module_utils import ( - xml_utils, - opnsense_utils, -) -from ansible_collections.puzzle.opnsense.plugins.module_utils.config_utils import ( - OPNsenseModuleConfig, -) - -class OPNSenseInterfaceNotFoundError(Exception): - """ - Exception raised when an Interface is not found. - """ - -class OPNSenseGetInterfacesError(Exception): - """ - Exception raised if the function can't query the local device - """ - - -@dataclass -class InterfaceSetting: - """ - Represents a network interface with optional description and extra attributes. - - Attributes: - identifier (str): Unique ID for the interface. - descr (Optional[str]): Description of the interface. - extra_attrs (Dict[str, Any]): Additional attributes for configuration. - - Methods: - __init__: Initializes with ID, device, and optional description. - from_xml: Creates an instance from XML. - to_etree: Serializes instance to XML, handling special cases. - from_ansible_module_params: Creates from Ansible params. - """ - - identifier: str - #device: str - descr: Optional[str] = None - - # since only the above attributes are needed, the rest is handled here - extra_attrs: Dict[str, Any] = field(default_factory=dict, repr=False) - - def __init__( - self, - identifier: Optional[str] = None, - #device: Optional[str] = None, - descr: Optional[str] = None, - **kwargs, - ): - if identifier is not None: - self.identifier = identifier - # if device is not None: - # self.device = device - if descr is not None: - self.descr = descr - self.extra_attrs = kwargs - - @staticmethod - def from_xml(element: Element) -> "InterfaceSetting": - """ - Converts XML element to InterfaceSetting instance. - - Args: - element (Element): XML element representing an interface. - - Returns: - InterfaceSetting: An instance with attributes derived from the XML. - - Processes XML to dict, assigning 'identifier' and 'device' from keys and - 'if' element. Assumes single key processing. - """ - - interface_setting_dict: dict = xml_utils.etree_to_dict(element) - - for key, value in interface_setting_dict.items(): - value["descr"] = key # Move the key to a new "identifier" field - if "if" in value: - if_key = value.pop("if", None) - if if_key is not None: - value["if"] = if_key - break # Only process the first key, assuming there's only one - - # Return only the content of the dictionary without the key - return InterfaceSetting(**interface_setting_dict.popitem()[1]) - - def to_etree(self) -> Element: - """ - Serializes the instance to an XML Element, including extra attributes. - - Returns: - Element: XML representation of the instance. - - Creates an XML element with identifier, and description. Handles - serialization of additional attributes, excluding specified exceptions and - handling specific attribute cases like alias and DHCP options. Assumes - boolean values translate to '1' for true. - """ - - interface_setting_dict: dict = asdict(self) - - exceptions = ["dhcphostname", "mtu", "subnet", "gateway", "media", "mediaopt"] - - # Create the main element - main_element = Element(interface_setting_dict["identifier"]) - - # Special handling for 'device' and 'descr' - # SubElement(main_element, "if").text = interface_setting_dict.get("device") - SubElement(main_element, "descr").text = interface_setting_dict.get("descr") - - # handle special cases - if getattr(self, "alias-subnet", None): - interface_setting_dict["extra_attrs"]["alias-subnet"] = getattr( - self, "alias-subnet", None - ) - - interface_setting_dict["extra_attrs"]["alias-address"] = getattr( - self, "alias-address", None - ) - - if getattr(self, "dhcp6-ia-pd-len", None): - interface_setting_dict["extra_attrs"]["dhcp6-ia-pd-len"] = getattr( - self, "dhcp6-ia-pd-len", None - ) - - if getattr(self, "track6-interface", None): - interface_setting_dict["extra_attrs"]["track6-interface"] = getattr( - self, "track6-interface", None - ) - - if getattr(self, "track6-prefix-id", None): - interface_setting_dict["extra_attrs"]["track6-prefix-id"] = getattr( - self, "track6-prefix-id", None - ) - - # Serialize extra attributes - for key, value in interface_setting_dict["extra_attrs"].items(): - if ( - key - in [ - "enable", - "spoofmac", - "alias-address", - "alias-subnet", - "dhcp6-ia-pd-len", - "adv_dhcp_pt_timeout", - "adv_dhcp_pt_retry", - "adv_dhcp_pt_select_timeout", - "adv_dhcp_pt_reboot", - "adv_dhcp_pt_backoff_cutoff", - "adv_dhcp_pt_initial_interval", - "adv_dhcp_pt_values", - "adv_dhcp_send_options", - "adv_dhcp_request_options", - "adv_dhcp_required_options", - "adv_dhcp_option_modifiers", - "adv_dhcp_config_advanced", - "adv_dhcp_config_file_override", - "adv_dhcp_config_file_override_path", - "dhcprejectfrom", - "track6-interface", - "track6-prefix-id", - ] - and value is None - ): - sub_element = SubElement(main_element, key) - if value is None and key not in exceptions: - continue - sub_element = SubElement(main_element, key) - if value is True: - sub_element.text = "1" - elif value is not None: - sub_element.text = str(value) - - return main_element - - @classmethod - def from_ansible_module_params(cls, params: dict) -> "InterfaceSetting": - """ - Creates an instance from Ansible module parameters. - - Args: - params (dict): Parameters from an Ansible module. - - Returns: - User: An instance of InterfaceSetting. - - Filters out None values from the provided parameters and uses them to - instantiate the class, focusing on 'identifier', 'device', and 'descr'. - """ - - interface_setting_dict = { - "identifier": params.get("identifier"), - # "device": params.get("device"), - "descr": params.get("description"), - } - - interface_setting_dict = { - key: value - for key, value in interface_setting_dict.items() - if value is not None - } - - return cls(**interface_setting_dict) - - -class InterfacesSet(OPNsenseModuleConfig): - """ - Manages network interface interfaces for OPNsense configurations. - - Inherits from OPNsenseModuleConfig, offering methods for managing - interface interfaces within an OPNsense config file. - - Attributes: - _interfaces_settings (List[InterfaceSetting]): List of interfaces. - - Methods: - __init__(self, path="/conf/config.xml"): Initializes InterfacesSet and loads interfaces. - _load_interfaces() -> List["interface_setting"]: Loads interface interfaces from config. - changed() -> bool: Checks if current interfaces differ from the loaded ones. - update(InterfaceSetting: InterfaceSetting): Updates an interface, - errors if not found. - find(**kwargs) -> Optional[InterfaceSetting]: Finds an interface matching - specified attributes. - save() -> bool: Saves changes to the config file if there are modifications. - """ - - _interfaces_settings: List[InterfaceSetting] - - def __init__(self, path: str = "/conf/config.xml"): - super().__init__( - module_name="interfaces_settings", - config_context_names=["interfaces"], - path=path, - ) - - self._config_xml_tree = self._load_config() - self._interfaces_settings = self._load_interfaces() - - def _load_interfaces(self) -> List["InterfaceSetting"]: - - element_tree_interfaces: Element = self.get("interfaces") - - return [ - InterfaceSetting.from_xml(element_tree_interface) - for element_tree_interface in element_tree_interfaces - ] - - @property - def changed(self) -> bool: - """ - Evaluates whether there have been changes to user or group configurations that are not yet - reflected in the saved system configuration. This property serves as a check to determine - if updates have been made in memory to the user or group lists that differ from what is - currently persisted in the system's configuration files. - Returns: - bool: True if there are changes to the user or group configurations that have not been - persisted yet; False otherwise. - The method works by comparing the current in-memory representations of users and groups - against the versions loaded from the system's configuration files. A difference in these - lists indicates that changes have been made in the session that have not been saved, thus - prompting the need for a save operation to update the system configuration accordingly. - Note: - This property should be consulted before performing a save operation to avoid - unnecessary writes to the system configuration when no changes have been made. - """ - - return bool(str(self._interfaces_settings) != str(self._load_interfaces())) - - def get_interfaces(self) -> List[InterfaceSetting]: - """ - Retrieves a list of interface interfaces from an OPNSense device via a PHP function. - - The function queries the device using specified PHP requirements and config functions. - It processes the stdout, extracts interface data, and handles errors. - - Returns: - list[InterfaceSetting]: A list of interface interfaces parsed - from the PHP function's output. - - Raises: - OPNSenseGetInterfacesError: If an error occurs during the retrieval - or parsing process, - or if no interfaces are found. - """ - - # load requirements - php_requirements = self._config_maps["interfaces_settings"][ - "php_requirements" - ] - php_command = """ - /* get physical network interfaces */ - foreach (get_interface_list() as $key => $item) { - echo $key.','; - } - /* get virtual network interfaces */ - foreach (plugins_devices() as $item){ - foreach ($item["names"] as $key => $if ) { - echo $key.','; - } - } - """ - - # run php function - result = opnsense_utils.run_command( - php_requirements=php_requirements, - command=php_command, - ) - - # check for stderr - if result.get("stderr"): - raise OPNSenseGetInterfacesError( - "error encounterd while getting interfaces" - ) - - # parse list - interface_list: list[str] = [ - item.strip() - for item in result.get("stdout").split(",") - if item.strip() and item.strip() != "None" - ] - - # check parsed list length - if len(interface_list) < 1: - raise OPNSenseGetInterfacesError( - "error encounterd while getting interfaces, less than one interface available" - ) - - return interface_list - - def update(self, interface_setting: InterfaceSetting) -> None: - """ - Updates an interface setting in the set. - - Checks for interface existence and updates or raises errors accordingly. - - Args: - interface_setting (InterfaceSetting): The interface interface to update. - - Raises: - OPNSenseDeviceNotFoundError: If device is not found. - OPNSenseInterfaceNotFoundError: If device is not found. - """ - - def find(self, **kwargs) -> Optional[InterfaceSetting]: - """ - Searches for an interface interface that matches given criteria. - - Iterates through the list of interface interfaces, checking if each one - matches all provided keyword arguments. If a match is found, returns the - corresponding interface interface. If no match is found, returns None. - - Args: - **kwargs: Key-value pairs to match against attributes of interface interfaces. - - Returns: - Optional[InterfaceSetting]: The first interface interface that matches - the criteria, or None if no match is found. - """ - - for interface_setting in self._interfaces_settings: - match = all( - getattr(interface_setting, key, None) == value - for key, value in kwargs.items() - ) - if match: - return interface_setting - return None - - def save(self) -> bool: - """ - Saves the current state of interface interfaces to the OPNsense configuration file. - - Checks if there have been changes to the interface interfaces. If not, it - returns False indicating no need to save. It then locates the parent element - for interface interfaces in the XML tree and replaces existing entries with - the updated set from memory. After updating, it writes the new XML tree to - the configuration file and reloads the configuration to reflect changes. - - Returns: - bool: True if changes were saved successfully, False if no changes were detected. - - Note: - This method assumes that 'parent_element' correctly refers to the container - of interface elements within the configuration file. - """ - - if not self.changed: - return False - - # Use 'find' to get the single parent element - parent_element = self._config_xml_tree.find( - self._config_maps["interfaces_settings"]["interfaces"] - ) - - # Assuming 'parent_element' correctly refers to the container of interface elements - for interface_element in list(parent_element): - parent_element.remove(interface_element) - - # Now, add updated interface elements - parent_element.extend( - [ - interface_setting.to_etree() - for interface_setting in self._interfaces_settings - ] - ) - - # Write the updated XML tree to the file - tree = ElementTree(self._config_xml_tree) - tree.write(self._config_path, encoding="utf-8", xml_declaration=True) - - return True diff --git a/plugins/module_utils/module_index.py b/plugins/module_utils/module_index.py index 0a4a6d3a..1194c8ba 100644 --- a/plugins/module_utils/module_index.py +++ b/plugins/module_utils/module_index.py @@ -770,37 +770,6 @@ }, }, }, - "interfaces_settings": { - "interfaces": "interfaces", - "identifier": "identifier", - "device": "device", - "description": "description", - "enabled": "enabled", - "locked": "locked", - "block_private": "block_private", - "block_bogons": "block_bogons", - "ipv4_address": "ipv4_address", - "ipv4_subnet": "ipv4_subnet", - "ipv4_gateway": "ipv4_gateway", - "ipv6_address": "ipv6_address", - "ipv6_subnet": "ipv6_subnet", - "ipv6_gateway": "ipv6_gateway", - "track6_interface": "track6_interface", - "track6_prefix_id": "track6_prefix_id", - "mac_address": "mac_address", - "promiscuous_mode": "promiscuous_mode", - "mtu": "mtu", - "mss": "mss", - "dynamic_gateway": "dynamic_gateway", - "php_requirements": [ - "/usr/local/etc/inc/config.inc", - "/usr/local/etc/inc/interfaces.inc", - "/usr/local/etc/inc/filter.inc", - "/usr/local/etc/inc/util.inc", - "/usr/local/etc/inc/system.inc", - ], - "configure_functions": {}, - }, "system_high_availability_settings": { # Add other mappings here "hasync": "hasync", diff --git a/plugins/modules/interfaces_assignments.py b/plugins/modules/interfaces_assignments.py index 9a3e2ff8..345a08b3 100644 --- a/plugins/modules/interfaces_assignments.py +++ b/plugins/modules/interfaces_assignments.py @@ -72,11 +72,11 @@ - type: str required: false - ipv4_subnet: - description: - - - type: int - required: false + # ipv4_subnet: + # description: + # - + # type: int + # required: false ipv4_gateway: description: - diff --git a/plugins/modules/interfaces_settings.py b/plugins/modules/interfaces_settings.py deleted file mode 100644 index 42914070..00000000 --- a/plugins/modules/interfaces_settings.py +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2024, Kilian Soltermann , Puzzle ITC -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -"""interfaces_settings module: Module to configure OPNsense interface settings""" - -# pylint: disable=duplicate-code -__metaclass__ = type - -# https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html -# fmt: off -DOCUMENTATION = r''' ---- -author: - - Kyle Hammond (@kdhlab) -module: interfaces_settings -version_added: "1.2.1" -short_description: This module can be used to configure assigned interface settings -description: - - Module to configure interface settings. -options: - identifier: - description: - - "Technical identifier of the interface, used by hasync for example" - type: str - required: true - description: - description: - - Interface name shown in the GUI. Identifier in capital letters if not provided. - - Input will be trimmed, as no whitespaces are allowed. - type: str - required: false - enabled: - description: - - Enable or disable the interface - type: bool - required: false - locked: - description: - - Prevent interface removal - type: bool - required: false - block_private: - description: - - When set, this option blocks traffic from IP addresses that are reserved for private networks as per RFC 1918 (10/8, 172.16/12, 192.168/16) as well as loopback addresses (127/8) and Carrier-grade NAT addresses (100.64/10). This option should only be set for WAN interfaces that use the public IP address space. - type: bool - required: false - block_bogons: - description: - - When set, this option blocks traffic from IP addresses that are reserved for private networks as per RFC 1918 (10/8, 172.16/12, 192.168/16) as well as loopback addresses (127/8) and Carrier-grade NAT addresses (100.64/10). This option should only be set for WAN interfaces that use the public IP address space. - type: bool - required: false - ipv4_configuration_type: - description: - - - type: str - required: false - ipv6_configuration_type: - description: - - - type: str - required: false - ipv4_address: - description: - - - type: str - required: false - ipv4_subnet: - description: - - - type: int - required: false - ipv4_gateway: - description: - - - type: str - required: false - ipv6_address: - description: - - - type: str - required: false - ipv6_subnet: - description: - - - type: int - required: false - ipv6_gateway: - description: - - - type: str - required: false - track6_interface: - description: - - - type: str - required: false - track6_prefix_id: - description: - - - type: int - required: false - mac_address: - description: - - - type: str - required: false - promiscuous_mode: - description: - - - type: bool - required: false - mtu: - description: - - If you leave this field blank, the adapter's default MTU will be used. This is typically 1500 bytes but can vary in some circumstances. - type: int - required: false - mss: - description: - - If you enter a value in this field, then MSS clamping for TCP connections to the value entered above minus 40 (IPv4) or 60 (IPv6) will be in effect (TCP/IP header size). - type: int - required: false - dynamic_gateway: - description: - - If the destination is directly reachable via an interface requiring no intermediary system to act as a gateway, you can select this option which allows dynamic gateways to be created without direct target addresses. Some tunnel types support this. - type: bool - required: false -''' - -EXAMPLES = r''' -- name: Assign Vagrant interface to device em4 - puzzle.opnsense.interfaces_settings: - identifier: "VAGRANT" - device: "em4" - -- name: Create new assignment - puzzle.opnsense.interfaces_settings: - identifier: "lan" - device: "vtnet1" - description: "lan_interface" -''' - -RETURN = ''' -opnsense_configure_output: - description: A list of the executed OPNsense configure function along with their respective stdout, stderr and rc - returned: always - type: list - sample: - - function: filter_configure - params: - - 'true' - rc: 0 - stderr: '' - stderr_lines: [] - stdout: '' - stdout_lines: [] - - - function: rrd_configure - params: - - 'true' - rc: 0 - stderr: '' - stderr_lines: [] - stdout: Generating RRD graphs...done. - stdout_lines: - - Generating RRD graphs...done. -''' -# pylint: enable=duplicate-code -# fmt: on -import ipaddress -from xml.etree import ElementTree -from xml.etree.ElementTree import Element -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.puzzle.opnsense.plugins.module_utils.config_utils import ( - OPNsenseModuleConfig, - UnsupportedModuleSettingError, - UnsupportedVersionForModule, -) - -from ansible_collections.puzzle.opnsense.plugins.module_utils.interfaces_assignments_utils import ( - OPNSenseGetInterfacesError, -) - -from ansible_collections.puzzle.opnsense.plugins.module_utils import ( - opnsense_utils, - version_utils, -) - - -def validate_ipv4(ipaddr: str) -> bool: - """ - Check if the given string is an IPv4 address - """ - digits = ipaddr.split(".") - if len(digits) != 4: - return False - for num in digits: - if not (num.isdigit() and int(num) < 256): - return False - return True - - -def validate_ip(ipaddr: str) -> bool: - """ - Check if the given string is an IPv4 or IPv6 address - """ - try: - ipaddress.ip_network(ipaddr, strict=False) - return True - except ValueError: - return False - -def main(): - """ - Main function of the interfaces_settings module - """ - - module_args = { - "identifier": {"type": "str", "required": False}, - "description": {"type": "str", "required": False}, - "enabled": {"type": "bool", "required": False, "default": False}, - "locked": {"type": "bool", "required": False, "default": False}, - "block_private": {"type": "bool", "required": False, "default": False}, - "block_bogons": {"type": "bool", "required": False, "default": False}, - "ipv4_address": {"type": "str", "required": False}, - "ipv4_subnet": {"type": "int", "required": False}, - "ipv4_gateway": {"type": "str", "required": False}, - "ipv6_address": {"type": "str", "required": False}, - "ipv6_subnet": {"type": "int", "required": False}, - "ipv6_gateway": {"type": "str", "required": False}, - "track6_interface": {"type": "str", "required": False}, - "track6_prefix_id": {"type": "int", "required": False}, - "mac_address": {"type": "str", "required": False}, - "promiscuous_mode": {"type": "bool", "required": False, "default": False}, - "mtu": {"type": "int", "required": False}, - "mss": {"type": "int", "required": False}, - "dynamic_gateway": {"type": "bool", "required": False, "default": False}, - } - - module = AnsibleModule( - argument_spec=module_args, - supports_check_mode=True, - required_one_of=[ - ["identifier", "description"], - ], - ) - - # https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html - # https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html#return-block - result = { - "changed": False, - "invocation": module.params, - "diff": None, - } - - interface_setting = InterfaceSetting.from_ansible_module_params(module.params) - - with InterfacesSet() as interfaces_set: - - try: - interfaces_set.update(interface_setting) - - except ( - OPNSenseDeviceNotFoundError - ) as opnsense_device_not_found_error_error_message: - module.fail_json(msg=str(opnsense_device_not_found_error_error_message)) - - # except ( - # OPNSenseDeviceAlreadyAssignedError - # ) as opnsense_device_already_assigned_error_message: - # module.fail_json(msg=str(opnsense_device_already_assigned_error_message)) - - except OPNSenseGetInterfacesError as opnsense_get_interfaces_error_message: - module.fail_json(msg=str(opnsense_get_interfaces_error_message)) - - if interfaces_set.changed: - result["diff"] = interfaces_set.diff - result["changed"] = True - - if interfaces_set.changed and not module.check_mode: - interfaces_set.save() - result["opnsense_configure_output"] = interfaces_set.apply_settings() - - for cmd_result in result["opnsense_configure_output"]: - if cmd_result["rc"] != 0: - module.fail_json( - msg="Apply of the OPNsense settings failed", - details=cmd_result, - ) - - # Return results - module.exit_json(**result) - - -if __name__ == "__main__": - main()