From 99dd6783e3c2c19f84ea0a44703c003cb0287132 Mon Sep 17 00:00:00 2001 From: "A. Wilcox" Date: Fri, 20 Nov 2020 13:54:13 -0600 Subject: ifupdown-ng NMSA: Add sysctl support --- ncserver/module/nms_ifupdownng.py | 148 +++++++++++++++++++++++++++++++++++--- 1 file changed, 140 insertions(+), 8 deletions(-) (limited to 'ncserver/module/nms_ifupdownng.py') diff --git a/ncserver/module/nms_ifupdownng.py b/ncserver/module/nms_ifupdownng.py index f844d32..5f87e8c 100644 --- a/ncserver/module/nms_ifupdownng.py +++ b/ncserver/module/nms_ifupdownng.py @@ -48,6 +48,10 @@ _CONFIG = dict() """The internal configuration handle.""" +_SYSCTL = dict() +"""The internatl sysctl configuration handle.""" + + _TRANSACTION = False """Determines if a transaction is in progress.""" @@ -105,6 +109,17 @@ def _save_unlocked(): conf_file.write(eni[:-1]) +def _sysctl_save_unlocked(): + """Save changes to sysctl.conf (and sysctl itself).""" + buf = "\n".join("{key}={val}".format(key=key, val=val) + for key, val in _SYSCTL.items()) + + with open('/etc/netconf/sysctl.conf', 'w') as conf_file: + conf_file.write(buf) + + subprocess.run(['sysctl', '-f', '/etc/netconf/sysctl.conf'], check=False) + + def _save(): """Save configuration changes, if a transaction is not in progress.""" if _TRANSACTION: @@ -113,6 +128,14 @@ def _save(): _save_unlocked() +def _sysctl_save(): + """Save sysctl changes, if a transaction is not in progress.""" + if _TRANSACTION: + return + + _sysctl_save_unlocked() + + def _find_one(iface: str, key: str): """Find a single instance of configuration +key+ for +iface+.""" if iface not in _CONFIG.keys(): @@ -165,7 +188,30 @@ def _remove_one(iface: str, key: str): def _iface_path(iface: str) -> pathlib.Path: """Retrieve the system device path for the specified interface.""" - return pathlib.Path("/sys/class/net/" + iface) + return pathlib.Path('/sys/class/net/' + iface) + + +def _load_sysctl_cfg() -> dict: + """Retrieve the desired sysctl configuration.""" + global _SYSCTL # pylint: disable=W0603 + + # Won't load during a transaction. + if _TRANSACTION: + return + + sysctl_path = pathlib.Path('/etc/netconf/sysctl.conf') + if not sysctl_path.exists(): + LOGGER.error(_("NETCONF sysctl config missing; check installation")) + return + + lines = list() + with open(sysctl_path, 'r') as sysctl_file: + lines = sysctl_file.readlines() + + _SYSCTL = dict({ + value[0]: value[1].strip() for value in + [line.split('=') for line in lines] + }) ############################# @@ -177,19 +223,58 @@ _ENI_MAPPING = {'description': 'netconf-description', """Mapping of NMSA keys to /e/n/i keys.""" +_SYSCTL_MAPPING = { + 'ipv4_forwarding': 'net.ipv4.conf.{_if}.forwarding', + 'ipv6_forwarding': 'net.ipv6.conf.{_if}.forwarding', + 'ipv6_mtu': 'net.ipv6.conf.{_if}.mtu', + 'ipv6_dad_xmit': 'net.ipv6.conf.{_if}.dad_transmits', + 'ipv6_slaac_validlft': 'net.ipv6.conf.{_if}.temp_valid_lft', + 'ipv6_slaac_preflft': 'net.ipv6.conf.{_if}.temp_prefered_lft' +} +"""Mapping of NMSA keys to sysctl nodes.""" + + +# # # # Getters # # # # + def get_one_eni(iface: str, parameter: str): """Retrieve the specified parameter from /e/n/i.""" return _find_one(iface, _ENI_MAPPING[parameter]) -def unset_one_eni(iface: str, parameter: str): - """Unset a parameter in /e/n/i.""" - _remove_one(iface, _ENI_MAPPING[parameter]) +def get_sysctl(iface: str, parameter: str): + """Retrieve the specified parameter from sysctl.conf.""" + key = _SYSCTL_MAPPING[parameter].format(_if=iface) + fallback = _SYSCTL_MAPPING[parameter].format(_if='all') + return _SYSCTL.get(key, _SYSCTL.get(fallback, None)) -def set_desc(iface: str, _, value: str): - """Set the description for the specified interface.""" - _replace_one(iface, 'netconf-description', value) + +def get_sysctl_bool(iface: str, parameter: str) -> bool: + """Retrieve the specified boolean from sysctl.conf.""" + value = get_sysctl(iface, parameter) + if value == '1': + return True + return False + + +def live_sysctl(iface: str, parameter: str): + """Retrieve the live value of a parameter from /proc/sys.""" + key = _SYSCTL_MAPPING[parameter].format(_if=iface) + + path = pathlib.Path("/proc/sys/" + key.replace('.', '/')) + if path.exists(): + with open(path, 'r') as sysctl: + return sysctl.read().strip() + + return None + + +def live_sysctl_bool(iface: str, parameter: str) -> bool: + """Retrieve a live boolean value from /proc/sys.""" + value = live_sysctl(iface, parameter) + if value is not None and value == '1': + return True + return False def live_enabled(iface: str, _): @@ -202,15 +287,55 @@ def live_enabled(iface: str, _): return False +# # # # Setters # # # # + +def set_sysctl(iface: str, parameter: str, value): + """Set a sysctl value.""" + key = _SYSCTL_MAPPING[parameter].format(_if=iface) + _SYSCTL[key] = value + _sysctl_save() + + +def set_sysctl_bool(iface: str, parameter: str, value: bool): + """Set a boolean sysctl value.""" + real = '0' + if value: + real = '1' + + set_sysctl(iface, parameter, real) + + +def set_desc(iface: str, _, value: str): + """Set the description for the specified interface.""" + _replace_one(iface, 'netconf-description', value) + + def set_auto(iface: str, _, value: bool): """Set the auto flag for the specified interface.""" _replace_one(iface, 'auto', str(value).lower()) +# # # # Unsetters # # # # + +def unset_one_eni(iface: str, parameter: str): + """Unset a parameter in /e/n/i.""" + _remove_one(iface, _ENI_MAPPING[parameter]) + + +def unset_sysctl(iface: str, parameter: str): + """Unset a sysctl.""" + key = _SYSCTL_MAPPING[parameter].format(_if=iface) + _SYSCTL.pop(key, None) + _sysctl_save() + + _PARAMETERS = { # "name": (getter, live getter, setter, unsetter) "description": (get_one_eni, get_one_eni, set_desc, unset_one_eni), - "enabled": (get_one_eni, live_enabled, set_auto, unset_one_eni) + "enabled": (get_one_eni, live_enabled, set_auto, unset_one_eni), + # XXX TODO: ipv4_enabled + "ipv4_forwarding": (get_sysctl_bool, live_sysctl_bool, + set_sysctl_bool, unset_sysctl) } """Describes all supported parameters and their methods.""" @@ -255,12 +380,15 @@ def commit(): LOGGER.warning(_("commit when no transaction is in progress")) _save_unlocked() + _sysctl_save_unlocked() + _TRANSACTION = False def get_param(iface: str, parameter: str): """Retrieve the parameter for the specified interface.""" _load_config() + _load_sysctl_cfg() if iface not in _CONFIG.keys(): LOGGER.warning( @@ -280,6 +408,7 @@ def get_param(iface: str, parameter: str): def curr_param(iface: str, parameter: str): """Retrieve the current parameter value for the specified interface.""" _load_config() + # Won't read from sysctl.conf so don't need to _load_sysctl_cfg. # XXX how do we want to handle interfaces that are missing from /e/n/i # but are still present in the system? @@ -301,6 +430,7 @@ def curr_param(iface: str, parameter: str): def set_param(iface: str, parameter: str, value): """Set the parameter for the specified interface.""" _load_config() + _load_sysctl_cfg() if parameter not in _PARAMETERS.keys(): LOGGER.error( @@ -319,6 +449,7 @@ def set_param(iface: str, parameter: str, value): def unset_param(iface: str, parameter: str): """Unset the parameter for the specified interface.""" _load_config() + _load_sysctl_cfg() if iface not in _CONFIG.keys(): LOGGER.warning( @@ -399,3 +530,4 @@ def remove_address(iface: str, addr: str): # Load immediately when we're loaded so we can go straight to a transaction # if desired. _load_config() +_load_sysctl_cfg() -- cgit v1.2.3-60-g2f50