Skip to content

Commit

Permalink
initial commit of the system_settings_general module
Browse files Browse the repository at this point in the history
  • Loading branch information
rekup committed Sep 17, 2023
1 parent 441e23c commit 25a85cd
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 3 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ antsibull-docs = "==2.3.1"
coverage = "==6.5.0"
# See current bug https://github.com/ansible-community/molecule-plugins/issues/176
molecule-plugins = {extras = ["vagrant"], git = "https://github.com/ansible-community/molecule-plugins.git"}
pyyaml = "==6.0.1"

[requires]
python_version = "3.10"
24 changes: 21 additions & 3 deletions plugins/module_utils/config_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from typing import Any
from xml.etree import ElementTree
import yaml

from ansible_collections.puzzle.opnsense.plugins.module_utils import xml_utils

Expand Down Expand Up @@ -36,15 +37,18 @@ class OPNsenseConfig:
"""
_config_path: str
_config_dict: dict
_check_mode: bool

def __init__(self, path: str = "/conf/config.xml"):
def __init__(self, path: str = "/conf/config.xml", check_mode = False):
"""
Initializes an instance of OPNsenseConfig.
:param path: The path to the OPNsense config file (default: "/conf/config.xml").
:param check_mode: Whether or not the OPNsenseConfig context manager should run in checkmode or not (default: `False`).
"""
self._config_path = path
self._config_dict = self._parse_config_from_file()
self._check_mode = check_mode

def __enter__(self) -> "OPNsenseConfig":
return self
Expand All @@ -57,7 +61,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
"""
if exc_type:
raise exc_type(f"Exception occurred: {exc_val}")
if self.changed:
if self.changed and not self._check_mode:
raise RuntimeError("Config has changed. Cannot exit without saving.")

def __getitem__(self, key: Any) -> Any:
Expand All @@ -80,8 +84,10 @@ def save(self) -> bool:
"""
Saves the config dictionary to the config file if changes have been made.
:return: True if changes were saved, False if no changes were detected.
:return: True if changes were saved or checkmode is active, False if no changes were detected.
"""
if self._check_mode: return True

if self.changed:
new_config_root = xml_utils.dict_to_etree("opnsense", self._config_dict)[0]
new_tree = ElementTree.ElementTree(new_config_root)
Expand All @@ -99,3 +105,15 @@ def changed(self) -> bool:
"""
orig_dict = self._parse_config_from_file()
return orig_dict != self._config_dict

@property
def diff(self) -> dict:
"""
Returns the diff in the config
:return: dict of config diffs
"""
return dict(
before=yaml.safe_dump(self._parse_config_from_file()),
after=yaml.safe_dump(self._config_dict)
)
131 changes: 131 additions & 0 deletions plugins/module_utils/opnsense_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Copyright: (c) 2023, Reto Kupferschmid <[email protected]>, Puzzle ITC
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

"""Utilities used to apply OPNsense config changes"""

from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

from typing import List
import subprocess

def _run_function(php_requirements: List[str], configure_function: str, configure_params: List = []) -> str:
"""
Execute a php function optional with parameters
:param php_requirements: A list os strings containing the location of php files which must be included to execute the function.
:param configure_function: The php function to call.
:param configure_params: An optional list of parameters to pass to the function.
:return: Returns the stdout generated by the command
"""

# assemble the php require statements
requirements_string = " ".join(
["require '" + req + "';" for req in php_requirements]
)
params_string = ",".join(configure_params)

# assemble php command
php_cmd = f"{requirements_string} {configure_function}({params_string});"

# run command
cmd_result = subprocess.run(
[
"php",
"-r",
php_cmd,
],
stdout=subprocess.PIPE,
check=True, # raise exception if program fails
)
return cmd_result.stdout


def system_settings_general() -> List[str]:
"""
Execute the required php function to apply settings in the System -> Settings -> General (system_general.php) view.
https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/www/system_general.php#L227
:return: Returns a list os strings containing the stdout of all the commands executed
"""

# requirements to execute the various functions can be found in the respective php file
# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/www/system_general.php#L30
php_requirements = [
"/usr/local/etc/inc/config.inc",
"/usr/local/etc/inc/util.inc",
"/usr/local/etc/inc/system.inc",
"/usr/local/etc/inc/interfaces.lib.inc",
"/usr/local/etc/inc/interfaces.inc",
"/usr/local/etc/inc/filter.inc",
]

cmd_output = []
# the order of commands executed is relevant
# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/www/system_general.php#L227

# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/etc/inc/system.inc#L935
cmd_output.append(
_run_function(
php_requirements=php_requirements,
configure_function="system_timezone_configure",
configure_params=["true"], # first param: verbose
)
)

# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/etc/inc/system.inc#L864
cmd_output.append(
_run_function(
php_requirements=php_requirements,
configure_function="system_trust_configure",
configure_params=["true"], # first param: verbose
)
)

# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/etc/inc/system.inc#L864
cmd_output.append(
_run_function(
php_requirements=php_requirements,
configure_function="system_hostname_configure",
configure_params=["true"], # first param: verbose
)
)

# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/etc/inc/system.inc#L506
cmd_output.append(
_run_function(
php_requirements=php_requirements,
configure_function="system_resolver_configure",
configure_params=["true"], # first param: verbose
)
)

# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/etc/inc/plugins.inc#L251
cmd_output.append(
_run_function(
php_requirements=php_requirements,
configure_function="plugins_configure",
configure_params=["'dns'", "true"], # first param: hook, second param: verbose
)
)

# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/etc/inc/plugins.inc#L251
cmd_output.append(
_run_function(
php_requirements=php_requirements,
configure_function="plugins_configure",
configure_params=["'dhcp'", "true"], # first param: hook, second param: verbose
)
)

# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/etc/inc/filter.inc#L125
cmd_output.append(
_run_function(
php_requirements=php_requirements,
configure_function="filter_configure",
configure_params=["true"], # first param: verbose
)
)
return cmd_output
154 changes: 154 additions & 0 deletions plugins/modules/system_settings_general.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2023, Reto Kupferschmid <[email protected]>, Puzzle ITC
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

"""Example module: Show minimal functionality of OPNsenseConfig class"""

__metaclass__ = type

# https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html
DOCUMENTATION = r"""
---
author:
- Reto Kupferschmid (@rekup)
module: system_settings_general
short_description: Configure general settings mainly concern network-related settings like the hostname.
description:
- Module to configure general system settings
options:
hostname:
description:
- Hostname without domain, e.g.: V(firewall)
type: str
required: false
domain:
description:
- The domain, e.g. V(mycorp.com), V(home), V(office), V(private), etc.
- Do not use V(local)as a domain name. It will cause local hosts running mDNS (avahi, bonjour, etc.) to be unable to resolve local hosts not running mDNS.
type: str
required: false
"""

EXAMPLES = r"""
- name: Set hostname to opnsense
puzzle.opnsense.system_settings_general:
hostname: "opnsense"
- name: Set domain to mycorp.com
puzzle.opnsense.system_settings_general:
domain: mycorp.com
"""

RETURN = r""" # """

import re

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.puzzle.opnsense.plugins.module_utils import (
config_utils,
opnsense_utils,
)

HOSTNAME_INDEX = 1
DOMAIN_INDEX = 2

def get_hostname(settings):
return settings[HOSTNAME_INDEX]


def get_domain(settings):
return settings[DOMAIN_INDEX]


def is_hostname(hostname: str) -> bool:
"""
Validates hostnames
:param hostname: A string containing the hostname
:return: True if the provided hostname is valid, False if it's invalid
"""

# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/etc/inc/util.inc#L704
hostname_regex = r"^(?:(?:[a-z0-9_]|[a-z0-9_][a-z0-9_\-]*[a-z0-9_])\.)*(?:[a-z0-9_]|[a-z0-9_][a-z0-9_\-]*[a-z0-9_])$"
return re.match(hostname_regex, hostname) is not None

def is_domain(domain: str) -> bool:
"""
Validates domain
:param hostname: A string containing the domain
:return: True if the provided domain is valid, False if it's invalid
"""

# https://github.com/opnsense/core/blob/cbaf7cee1f0a6fabd1ec4c752a5d169c402976dc/src/etc/inc/util.inc#L716
domain_regex = r"^(?:(?:[a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*(?:[a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$"
return re.match(domain_regex, domain) is not None

def main():
"""
Main function of the system_settings_general module
"""

module_args = dict(
hostname=dict(type="str", required=False),
domain=dict(type="str", required=False),
)

module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
required_one_of=[
["domain", "hostname"],
],
)

# 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,
"msg": "",
}

hostname_param = module.params.get("hostname")
domain_param = module.params.get("domain")

with config_utils.OPNsenseConfig(check_mode=module.check_mode) as config_mgr:
# Get system settings
system_settings = config_mgr["system"]
current_hostname = get_hostname(system_settings)
current_domain = get_domain(system_settings)

if hostname_param:
if not is_hostname(hostname_param):
module.fail_json(msg="Invalid hostname parameter specified")

if hostname_param != current_hostname["hostname"]:
current_hostname["hostname"] = hostname_param

if domain_param:
if not is_domain(domain_param):
module.fail_json(msg="Invalid domain parameter specified")

if domain_param != current_domain["domain"]:
current_domain["domain"] = domain_param

if config_mgr.changed:
result["diff"] = config_mgr.diff
result["changed"] = True

if config_mgr.changed and not module.check_mode:
config_mgr.save()
result[
"opnsense_configure_output"
] = opnsense_utils.system_settings_general()

# Return results
module.exit_json(**result)

if __name__ == "__main__":
main()

0 comments on commit 25a85cd

Please sign in to comment.