diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/defaults.py | 4 | ||||
| -rw-r--r-- | python/vyos/ifconfig/bridge.py | 5 | ||||
| -rw-r--r-- | python/vyos/ifconfig/interface.py | 17 | ||||
| -rw-r--r-- | python/vyos/kea.py | 63 | ||||
| -rw-r--r-- | python/vyos/utils/activate.py | 126 | ||||
| -rw-r--r-- | python/vyos/utils/func.py | 28 | ||||
| -rw-r--r-- | python/vyos/utils/system.py | 34 | ||||
| -rw-r--r-- | python/vyos/vpp/control_vpp.py | 60 |
8 files changed, 258 insertions, 79 deletions
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 6aa93ebed..78721c0d2 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -102,3 +102,7 @@ SSH_DSA_DEPRECATION_WARNING: str = \ 'ED25519) to avoid authentication failures after the upgrade.' reference_tree_cache = '/usr/share/vyos/reftree.cache' + +activation_list = os.path.join(directories['config'], 'activation-list') +activation_init = os.path.join(directories['data'], 'activation-init') +activation_hint = os.path.join(directories['data'], '.activation_hint') diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 6b5f49778..5f184f221 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -386,6 +386,11 @@ class BridgeIf(Interface): value = '1' if (tmp != None) else '0' lower.set_root_guard(value) + # set learning + disable_learning = dict_search('disable_learning', interface_config) + value = '1' if disable_learning is None else '0' + lower.set_learning(value) + if 'enable_vlan' in config: add_vlan = [] native_vlan_id = None diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index febd68f44..136adb289 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -226,6 +226,11 @@ class Interface(Control): 'location': '/sys/class/net/{ifname}/brport/root_block', 'errormsg': '{ifname} is not a bridge port member' }, + 'learning': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/brport/learning', + 'errormsg': '{ifname} is not a bridge port member' + }, 'proxy_arp': { 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp', @@ -1159,6 +1164,18 @@ class Interface(Control): """ self.set_interface('bridge_port_isolation', on_or_off) + def set_learning(self, state): + """ + Set MAC address learning state on a bridge port. When disabled, + the bridge will not learn source MAC addresses from incoming frames on + this port, causing all unknown unicast traffic to be flooded. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_learning(0) + """ + self.set_interface('learning', state) + def set_proxy_arp(self, enable): """ Set per interface proxy ARP configuration diff --git a/python/vyos/kea.py b/python/vyos/kea.py index 703dd8b49..8dcd0d6eb 100644 --- a/python/vyos/kea.py +++ b/python/vyos/kea.py @@ -88,6 +88,7 @@ def _find_list_of_dict_index(lst, key='ip', value=''): idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None) return idx + def kea_test_config(process: str, config_path: str) -> tuple[bool, str]: result, output = rc_cmd(f'{process} -t {config_path}') @@ -97,6 +98,7 @@ def kea_test_config(process: str, config_path: str) -> tuple[bool, str]: find = re.search(r'Error encountered:\s([^\n$]+)', output) return (False, find[1] if find else None) + def kea_parse_options(config): options = [] @@ -159,7 +161,11 @@ def kea_parse_options(config): def kea_parse_subnet(subnet, config): - out = {'subnet': subnet, 'id': int(config['subnet_id']), 'user-context': {'enable-ping-check': False}} + out = { + 'subnet': subnet, + 'id': int(config['subnet_id']), + 'user-context': {'enable-ping-check': False}, + } if 'option' in config: out['option-data'] = kea_parse_options(config['option']) @@ -371,6 +377,7 @@ def kea6_parse_subnet(subnet, config): return out + def kea_parse_tsig_algo(algo_spec): translate = { 'md5': 'HMAC-MD5', @@ -378,15 +385,17 @@ def kea_parse_tsig_algo(algo_spec): 'sha224': 'HMAC-SHA224', 'sha256': 'HMAC-SHA256', 'sha384': 'HMAC-SHA384', - 'sha512': 'HMAC-SHA512' + 'sha512': 'HMAC-SHA512', } if algo_spec not in translate: raise ConfigError(f'Unsupported TSIG algorithm: {algo_spec}') return translate[algo_spec] + def kea_parse_enable_disable(value): return True if value == 'enable' else False + def kea_parse_ddns_settings(config): data = {} @@ -394,7 +403,9 @@ def kea_parse_ddns_settings(config): data['ddns-send-updates'] = kea_parse_enable_disable(send_updates) if override_client_update := config.get('override_client_update'): - data['ddns-override-client-update'] = kea_parse_enable_disable(override_client_update) + data['ddns-override-client-update'] = kea_parse_enable_disable( + override_client_update + ) if override_no_update := config.get('override_no_update'): data['ddns-override-no-update'] = kea_parse_enable_disable(override_no_update) @@ -403,7 +414,9 @@ def kea_parse_ddns_settings(config): data['ddns-update-on-renew'] = kea_parse_enable_disable(update_on_renew) if conflict_resolution := config.get('conflict_resolution'): - data['ddns-use-conflict-resolution'] = kea_parse_enable_disable(conflict_resolution) + data['ddns-use-conflict-resolution'] = kea_parse_enable_disable( + conflict_resolution + ) if 'replace_client_name' in config: data['ddns-replace-client-name'] = config['replace_client_name'] @@ -420,6 +433,7 @@ def kea_parse_ddns_settings(config): return data + def _ctrl_socket_command(inet, vrf_name, command, args=None): if vrf_name: vrf_append = f'-{vrf_name}' @@ -603,7 +617,9 @@ def kea_get_static_mappings(config, inet, pools=[]) -> list: return mappings -def kea_get_server_leases(config, inet, vrf_name, pools=[], state=[], origin=None) -> list: +def kea_get_server_leases( + config, inet, vrf_name, pools=[], state=[], origin=None +) -> list: """ Get DHCP server leases from active Kea DHCPv4 or DHCPv6 configuration :return list @@ -659,9 +675,10 @@ def kea_get_server_leases(config, inet, vrf_name, pools=[], state=[], origin=Non now = datetime.now(timezone.utc) if lease['valid-lft'] > 0 and lease['expire_time'] > now: - # subtraction gives us a timedelta object which can't be formatted - # with strftime so we use str(), split gets rid of the microseconds - data_lease['remaining'] = str(lease['expire_time'] - now).split('.')[0] + # clear the microseconds before subtraction for visual clarity + data_lease['remaining'] = lease['expire_time'].replace( + microsecond=0 + ) - now.replace(microsecond=0) # Do not add old leases if ( @@ -685,21 +702,31 @@ def kea_get_server_leases(config, inet, vrf_name, pools=[], state=[], origin=Non return data + def _build_relay_hex_condition(sub_option_index, value): - if value.startswith("0x"): - return f"relay4[{sub_option_index}].hex == {value}" + if value.startswith('0x'): + return f'relay4[{sub_option_index}].hex == {value}' else: - return f"relay4[{sub_option_index}].hex == 0x{value.encode().hex().lower()}" + return f'relay4[{sub_option_index}].hex == 0x{value.encode().hex().lower()}' + def kea_build_client_class_test(config): conditions = [] - if "relay_agent_information" in config: - if "circuit_id" in config["relay_agent_information"]: - conditions.append(_build_relay_hex_condition(1, config["relay_agent_information"]["circuit_id"])) - if "remote_id" in config["relay_agent_information"]: - conditions.append(_build_relay_hex_condition(2, config["relay_agent_information"]["remote_id"])) - - test = " and ".join(conditions) + if 'relay_agent_information' in config: + if 'circuit_id' in config['relay_agent_information']: + conditions.append( + _build_relay_hex_condition( + 1, config['relay_agent_information']['circuit_id'] + ) + ) + if 'remote_id' in config['relay_agent_information']: + conditions.append( + _build_relay_hex_condition( + 2, config['relay_agent_information']['remote_id'] + ) + ) + + test = ' and '.join(conditions) return test diff --git a/python/vyos/utils/activate.py b/python/vyos/utils/activate.py new file mode 100644 index 000000000..51b92a4aa --- /dev/null +++ b/python/vyos/utils/activate.py @@ -0,0 +1,126 @@ +# Copyright (C) VyOS Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +import json +import typing +from pathlib import Path + +from vyos.base import Warning as Warn +from vyos.defaults import activation_list +from vyos.defaults import activation_init +from vyos.defaults import activation_hint +from vyos.defaults import directories + + +ActiveOpt = typing.Literal['enabled', 'once', 'off', 'never'] + + +def get_activation_scripts() -> dict: + list_path = Path(activation_list) + return json.loads(list_path.read_text()) + + +def set_activation(file_name: str, value: ActiveOpt): + script_dict = get_activation_scripts() + file_key = Path(file_name).stem + script_dict[file_key] = value + list_path = Path(activation_list) + list_path.write_text(json.dumps(script_dict)) + + +def get_activation(file_name: str) -> ActiveOpt: + script_dict = get_activation_scripts() + file_key = Path(file_name).stem + return script_dict[file_key] + + +def is_active(file_name: str) -> bool: + script_dict = get_activation_scripts() + file_key = Path(file_name).stem + if script_dict[file_key] in ('enabled', 'once'): + return True + return False + + +def stable_update(new: dict, old: dict): + res = {} + for key in new.keys(): + res[key] = old[key] if key in old.keys() else new[key] + return res + + +def init_activation_list() -> bool: + """Init if activation_hint exists, left on install_image or if image was + built as raw_image""" + + init_hint = Path(activation_hint) + if not init_hint.exists(): + return False + + init_hint.unlink() + init_list = Path(activation_init) + data_list = Path(activation_list) + data_obj = json.loads(init_list.read_text()) + data_list.write_text(json.dumps(data_obj)) + + return True + + +def refresh_activation_list(): + """Refresh activation list, as will be needed after image update""" + + if init_activation_list(): + return + + new_list_path = Path(directories['data']).joinpath(Path(activation_list).name) + if not new_list_path.exists(): + return + + new_obj = json.loads(new_list_path.read_text()) + + orig_list_path = Path(activation_list) + if orig_list_path.exists(): + orig_obj = json.loads(orig_list_path.read_text()) + if orig_obj == new_obj: + return + + obj = stable_update(new_obj, orig_obj) + else: + obj = new_obj + + orig_list_path.write_text(json.dumps(obj)) + + +first_installed_boot_file = '/run/first_installed_boot' + + +def set_first_installed_boot(): + try: + Path(first_installed_boot_file).touch(exist_ok=False) + except FileExistsError: + Warn('redundant set of first_installed_boot') + + +def is_first_installed_boot(): + return Path(first_installed_boot_file).exists() + + +def set_config_path_hint(): + """The config hint allows subsequent installs to find previous disk + resident config data. It is traditionally added as part of the image + install procedure, however, for raw image builds an alternative is + needed.""" + Path(directories['config']).joinpath('.vyatta_config').touch(exist_ok=True) diff --git a/python/vyos/utils/func.py b/python/vyos/utils/func.py new file mode 100644 index 000000000..830b76c94 --- /dev/null +++ b/python/vyos/utils/func.py @@ -0,0 +1,28 @@ +# Copyright (C) VyOS Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +"""Module for functors and higher order functions.""" + + +class FalseCallable: + """Define falsy callable for use as default value for getattr of a + function from a module defined by vyos.utils.system.load_as_module""" + + def __call__(self, *args, **kwargs): + pass + + def __bool__(self): + return False diff --git a/python/vyos/utils/system.py b/python/vyos/utils/system.py index 578537544..fd5f49645 100644 --- a/python/vyos/utils/system.py +++ b/python/vyos/utils/system.py @@ -16,28 +16,34 @@ import os from subprocess import run -def sysctl_read(name: str) -> str: +def _sysctl_key(parts: list[str]) -> str: + normalized = (p.replace('.', '/') for p in parts) + return '.'.join(normalized) + +def sysctl_read(name: list[str]) -> str: """Read and return current value of sysctl() option Args: - name (str): sysctl key name + name (list[str]): sysctl key name components Returns: str: sysctl key value """ - tmp = run(['sysctl', '-nb', name], capture_output=True) - return tmp.stdout.decode() + key = _sysctl_key(name) + tmp = run(['sysctl', '-nb', key], capture_output=True) + return tmp.stdout.decode().strip() -def sysctl_write(name: str, value: str | int) -> bool: +def sysctl_write(name: list[str], value: str | int) -> bool: """Change value via sysctl() Args: - name (str): sysctl key name + name (list[str]): sysctl key name components value (str | int): sysctl key value Returns: bool: True if changed, False otherwise """ + key = _sysctl_key(name) # convert other types to string before comparison if not isinstance(value, str): value = str(value) @@ -45,7 +51,7 @@ def sysctl_write(name: str, value: str | int) -> bool: if sysctl_read(name) == value: return True # return False if sysctl call failed - if run(['sysctl', '-wq', f'{name}={value}']).returncode != 0: + if run(['sysctl', '-wq', f'{key}={value}']).returncode != 0: return False # compare old and new values # sysctl may apply value, but its actual value will be @@ -55,11 +61,11 @@ def sysctl_write(name: str, value: str | int) -> bool: # False in other cases return False -def sysctl_apply(sysctl_dict: dict[str, str], revert: bool = True) -> bool: +def sysctl_apply(sysctl_dict: dict[tuple[str, ...], str], revert: bool = True) -> bool: """Apply sysctl values. Args: - sysctl_dict (dict[str, str]): dictionary with sysctl keys with values + sysctl_dict (dict[tuple[str, ...], str]): dictionary with sysctl key components (tuple) with values revert (bool, optional): Revert to original values if new were not applied. Defaults to True. @@ -67,12 +73,12 @@ def sysctl_apply(sysctl_dict: dict[str, str], revert: bool = True) -> bool: bool: True if all params configured properly, False in other cases """ # get current values - sysctl_original: dict[str, str] = {} - for key_name in sysctl_dict.keys(): - sysctl_original[key_name] = sysctl_read(key_name) + sysctl_original: dict[tuple[str, ...], str] = {} + for key_parts in sysctl_dict.keys(): + sysctl_original[key_parts] = sysctl_read(list(key_parts)) # apply new values and revert in case one of them was not applied - for key_name, value in sysctl_dict.items(): - if not sysctl_write(key_name, value): + for key_parts, value in sysctl_dict.items(): + if not sysctl_write(list(key_parts), value): if revert: sysctl_apply(sysctl_original, revert=False) return False diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index 5f3a18848..1ad098b71 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -472,73 +472,39 @@ class VPPControl: """ vpp_pair = self.lcp_pair_find(kernel_name=ifname) if vpp_pair: - vpp_iface_name = vpp_pair['vpp_name_hw'] - vpp_pair_name = vpp_pair['vpp_name_kernel'] + vpp_iface_index = vpp_pair['vpp_index_hw'] + vpp_pair_index = vpp_pair['vpp_index_kernel'] self.__vpp_api_client.api.pppoe_add_del_cp( - dp_sw_if_index=self.get_sw_if_index(vpp_iface_name), - cp_sw_if_index=self.get_sw_if_index(vpp_pair_name), + dp_sw_if_index=vpp_iface_index, + cp_sw_if_index=vpp_pair_index, is_add=True, ) @_Decorators.api_call def get_pppoe_interface_mapping(self) -> dict: """ - Parse the output of 'show pppoe control-plane binding' to build a mapping - between data-plane and control-plane interfaces. + Build a mapping between data-plane and control-plane interfaces. Returns: - dict: Mapping of data-plane interface names/indices to control-plane interface names/indices. + dict: Mapping of data-plane interface indices to control-plane interface indices. """ - reply = self.cli_cmd('show pppoe control-plane binding').reply result = {} - - deleted_re = re.compile(r'DELETED\s*\((\d+)\)') - - lines = reply.strip().splitlines() - - # If there are less than 2 lines, assume no PPPoE configured - if len(lines) < 2: - return result - - # Skip header lines (first 2 lines) - for line in lines[2:]: - line = line.strip() - if not line: - continue - - # Split into two columns by 2+ spaces - cols = re.split(r'\s{2,}', line, maxsplit=1) - if len(cols) != 2: - continue - - # Determine key/value depending on which column has "DELETED (index)" - col_a, col_b = cols - - match_a = deleted_re.search(col_a) - match_b = deleted_re.search(col_b) - - key = int(match_a.group(1)) if match_a else col_a - value = int(match_b.group(1)) if match_b else col_b - - result[key] = value + for binding in self.__vpp_api_client.api.pppoe_cp_binding_dump(): + dp_index = binding.dp_sw_if_index + cp_index = binding.cp_sw_if_index + result[dp_index] = cp_index return result @_Decorators.api_call - def delete_pppoe_mapping(self, dp_iface: str | int, cp_iface: str | int) -> None: + def delete_pppoe_mapping(self, dp_index: int, cp_index: int) -> None: """ Delete PPPoE mapping between data-plane and control-plane interfaces. Args: - dp_iface (str|int): Name or index of an interface in data-plane. - cp_iface (str|int): Name or index of an interface in control plane. + dp_index (int): Index of an interface in data-plane. + cp_index (int): Index of an interface in control plane. """ - dp_index = dp_iface - if isinstance(dp_iface, str): - dp_index = self.get_sw_if_index(dp_iface) - cp_index = cp_iface - if isinstance(cp_iface, str): - cp_index = self.get_sw_if_index(cp_iface) self.__vpp_api_client.api.pppoe_add_del_cp( dp_sw_if_index=dp_index, cp_sw_if_index=cp_index, |
