diff options
-rw-r--r-- | data/templates/high-availability/keepalived.conf.j2 | 6 | ||||
-rw-r--r-- | data/templates/login/default_motd.j2 | 14 | ||||
-rw-r--r-- | data/templates/vyos-hostsd/hosts.j2 | 1 | ||||
-rw-r--r-- | interface-definitions/high-availability.xml.in | 1 | ||||
-rw-r--r-- | python/vyos/configdep.py | 53 | ||||
-rw-r--r-- | python/vyos/ethtool.py | 2 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_high-availability_vrrp.py | 27 | ||||
-rwxr-xr-x | src/conf_mode/high-availability.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/system_login_banner.py | 22 | ||||
-rwxr-xr-x | src/services/vyos-configd | 19 | ||||
-rw-r--r-- | src/shim/vyshim.c | 12 |
11 files changed, 139 insertions, 28 deletions
diff --git a/data/templates/high-availability/keepalived.conf.j2 b/data/templates/high-availability/keepalived.conf.j2 index 50b910ca3..240161748 100644 --- a/data/templates/high-availability/keepalived.conf.j2 +++ b/data/templates/high-availability/keepalived.conf.j2 @@ -100,7 +100,11 @@ vrrp_instance {{ name }} { nopreempt {% endif %} {% if group_config.peer_address is vyos_defined %} - unicast_peer { {{ group_config.peer_address }} } + unicast_peer { +{% for peer_address in group_config.peer_address %} + {{ peer_address }} +{% endfor %} + } {% endif %} {% if group_config.hello_source_address is vyos_defined %} {% if group_config.peer_address is vyos_defined %} diff --git a/data/templates/login/default_motd.j2 b/data/templates/login/default_motd.j2 new file mode 100644 index 000000000..8584d261a --- /dev/null +++ b/data/templates/login/default_motd.j2 @@ -0,0 +1,14 @@ +Welcome to VyOS! + + ┌── ┐ + . VyOS {{ version_data.version }} + └ ──┘ {{ version_data.release_train }} + + * Documentation: https://docs.vyos.io/en/{{ version_data.release_train | replace('current', 'latest') }} + * Project news: https://blog.vyos.io + * Bug reports: https://vyos.dev + +You can change this banner using "set system login banner post-login" command. + +VyOS is a free software distribution that includes multiple components, +you can check individual component licenses under /usr/share/doc/*/copyright diff --git a/data/templates/vyos-hostsd/hosts.j2 b/data/templates/vyos-hostsd/hosts.j2 index 71fa335da..62ecf3ad0 100644 --- a/data/templates/vyos-hostsd/hosts.j2 +++ b/data/templates/vyos-hostsd/hosts.j2 @@ -4,6 +4,7 @@ # Local host 127.0.0.1 localhost +127.0.1.1 {{ host_name }} # The following lines are desirable for IPv6 capable hosts ::1 localhost ip6-localhost ip6-loopback diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in index af8f1ba68..558404882 100644 --- a/interface-definitions/high-availability.xml.in +++ b/interface-definitions/high-availability.xml.in @@ -195,6 +195,7 @@ <constraint> <validator name="ip-address"/> </constraint> + <multi/> </properties> </leafNode> <leafNode name="no-preempt"> diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py index 64727d355..73bd9ea96 100644 --- a/python/vyos/configdep.py +++ b/python/vyos/configdep.py @@ -1,4 +1,4 @@ -# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -33,7 +33,14 @@ if typing.TYPE_CHECKING: dependency_dir = os.path.join(directories['data'], 'config-mode-dependencies') -dependent_func: dict[str, list[typing.Callable]] = {} +local_dependent_func: dict[str, list[typing.Callable]] = {} + +DEBUG = False +FORCE_LOCAL = False + +def debug_print(s: str): + if DEBUG: + print(s) def canon_name(name: str) -> str: return os.path.splitext(name)[0].replace('-', '_') @@ -45,6 +52,26 @@ def canon_name_of_path(path: str) -> str: def caller_name() -> str: return stack()[2].filename +def name_of(f: typing.Callable) -> str: + return f.__name__ + +def names_of(l: list[typing.Callable]) -> list[str]: + return [name_of(f) for f in l] + +def remove_redundant(l: list[typing.Callable]) -> list[typing.Callable]: + names = set() + for e in reversed(l): + _ = l.remove(e) if name_of(e) in names else names.add(name_of(e)) + +def append_uniq(l: list[typing.Callable], e: typing.Callable): + """Append an element, removing earlier occurrences + + The list of dependencies is generally short and traversing the list on + each append is preferable to the cost of redundant script invocation. + """ + l.append(e) + remove_redundant(l) + def read_dependency_dict(dependency_dir: str = dependency_dir) -> dict: res = {} for dep_file in os.listdir(dependency_dir): @@ -95,16 +122,30 @@ def set_dependents(case: str, config: 'Config', tagnode: typing.Optional[str] = None): d = get_dependency_dict(config) k = canon_name_of_path(caller_name()) - l = dependent_func.setdefault(k, []) + tag_ext = f'_{tagnode}' if tagnode is not None else '' + if hasattr(config, 'dependent_func') and not FORCE_LOCAL: + dependent_func = getattr(config, 'dependent_func') + l = dependent_func.setdefault('vyos_configd', []) + else: + dependent_func = local_dependent_func + l = dependent_func.setdefault(k, []) for target in d[k][case]: func = def_closure(target, config, tagnode) - l.append(func) + func.__name__ = f'{target}{tag_ext}' + append_uniq(l, func) + debug_print(f'set_dependents: caller {k}, dependents {names_of(l)}') -def call_dependents(): +def call_dependents(dependent_func: dict = None): k = canon_name_of_path(caller_name()) - l = dependent_func.get(k, []) + if dependent_func is None or FORCE_LOCAL: + dependent_func = local_dependent_func + l = dependent_func.get(k, []) + else: + l = dependent_func.get('vyos_configd', []) + debug_print(f'call_dependents: caller {k}, dependents {names_of(l)}') while l: f = l.pop(0) + debug_print(f'calling: {f.__name__}') f() def called_as_dependent() -> bool: diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index ba638b280..f20fa452e 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -150,7 +150,7 @@ class Ethtool: self._eee = True # read current EEE setting, this returns: # EEE status: disabled || EEE status: enabled - inactive || EEE status: enabled - active - self._eee_enabled = bool('enabled' in out.splitlines()[2]) + self._eee_enabled = bool('enabled' in out.splitlines()[1]) def check_auto_negotiation_supported(self): """ Check if the NIC supports changing auto-negotiation """ diff --git a/smoketest/scripts/cli/test_high-availability_vrrp.py b/smoketest/scripts/cli/test_high-availability_vrrp.py index 2b7991cc2..9ba06aef6 100755 --- a/smoketest/scripts/cli/test_high-availability_vrrp.py +++ b/smoketest/scripts/cli/test_high-availability_vrrp.py @@ -237,6 +237,33 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'track_interface', config) self.assertIn(f' {none_vrrp_interface}', config) + def test_05_set_multiple_peer_address(self): + group = 'VyOS-WAN' + vlan_id = '24' + vip = '100.64.24.1/24' + peer_address_1 = '192.0.2.1' + peer_address_2 = '192.0.2.2' + vrid = '150' + group_base = base_path + ['vrrp', 'group', group] + + self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', '100.64.24.11/24']) + self.cli_set(group_base + ['interface', vrrp_interface]) + self.cli_set(group_base + ['address', vip]) + self.cli_set(group_base + ['peer-address', peer_address_1]) + self.cli_set(group_base + ['peer-address', peer_address_2]) + self.cli_set(group_base + ['vrid', vrid]) + + # commit changes + self.cli_commit() + + config = getConfig(f'vrrp_instance {group}') + + self.assertIn(f'interface {vrrp_interface}', config) + self.assertIn(f'virtual_router_id {vrid}', config) + self.assertIn(f'unicast_peer', config) + self.assertIn(f' {peer_address_1}', config) + self.assertIn(f' {peer_address_2}', config) + def test_check_health_script(self): sync_group = 'VyOS' diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index 7f8849de9..c726db8b2 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -116,8 +116,9 @@ def verify(ha): raise ConfigError(f'VRRP group "{group}" uses IPv4 but hello-source-address is IPv6!') if 'peer_address' in group_config: - if is_ipv6(group_config['peer_address']): - raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!') + for peer_address in group_config['peer_address']: + if is_ipv6(peer_address): + raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!') if vaddrs6: tmp = {'interface': interface, 'vrid': vrid, 'ipver': 'IPv6'} @@ -130,8 +131,9 @@ def verify(ha): raise ConfigError(f'VRRP group "{group}" uses IPv6 but hello-source-address is IPv4!') if 'peer_address' in group_config: - if is_ipv4(group_config['peer_address']): - raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!') + for peer_address in group_config['peer_address']: + if is_ipv4(peer_address): + raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!') # Check sync groups if 'vrrp' in ha and 'sync_group' in ha['vrrp']: for sync_group, sync_config in ha['vrrp']['sync_group'].items(): diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py index 65fa04417..923e1bf57 100755 --- a/src/conf_mode/system_login_banner.py +++ b/src/conf_mode/system_login_banner.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -18,30 +18,26 @@ from sys import exit from copy import deepcopy from vyos.config import Config +from vyos.template import render from vyos.utils.file import write_file +from vyos.version import get_version_data from vyos import ConfigError from vyos import airbag airbag.enable() -try: - with open('/usr/share/vyos/default_motd') as f: - motd = f.read() -except: - # Use an empty banner if the default banner file cannot be read - motd = "\n" - PRELOGIN_FILE = r'/etc/issue' PRELOGIN_NET_FILE = r'/etc/issue.net' POSTLOGIN_FILE = r'/etc/motd' default_config_data = { 'issue': 'Welcome to VyOS - \\n \\l\n\n', - 'issue_net': '', - 'motd': motd + 'issue_net': '' } def get_config(config=None): banner = deepcopy(default_config_data) + banner['version_data'] = get_version_data() + if config: conf = config else: @@ -92,7 +88,11 @@ def generate(banner): def apply(banner): write_file(PRELOGIN_FILE, banner['issue']) write_file(PRELOGIN_NET_FILE, banner['issue_net']) - write_file(POSTLOGIN_FILE, banner['motd']) + if 'motd' in banner: + write_file(POSTLOGIN_FILE, banner['motd']) + else: + render(POSTLOGIN_FILE, 'login/default_motd.j2', banner, + permission=0o644, user='root', group='root') return None diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 355182b26..648a017d5 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,6 +19,7 @@ import sys import grp import re import json +import typing import logging import signal import importlib.util @@ -29,6 +30,7 @@ from vyos.defaults import directories from vyos.utils.boot import boot_configuration_complete from vyos.configsource import ConfigSourceString from vyos.configsource import ConfigSourceError +from vyos.configdep import call_dependents from vyos.config import Config from vyos import ConfigError @@ -198,10 +200,12 @@ def initialization(socket): return None config = Config(config_source=configsource) + dependent_func: dict[str, list[typing.Callable]] = {} + setattr(config, 'dependent_func', dependent_func) return config -def process_node_data(config, data) -> int: +def process_node_data(config, data, last: bool = False) -> int: if not config: logger.critical(f"Empty config") return R_ERROR_DAEMON @@ -223,11 +227,18 @@ def process_node_data(config, data) -> int: args.insert(0, f'{script_name}.py') if script_name not in include_set: + # call dependents now if last element of prio queue is run + # independent of configd + if last: + call_dependents(dependent_func=config.dependent_func) return R_PASS with stdout_redirected(session_out, session_mode): result = run_script(conf_mode_scripts[script_name], config, args) + if last: + call_dependents(dependent_func=config.dependent_func) + return result def remove_if_file(f: str): @@ -281,7 +292,9 @@ if __name__ == '__main__': socket.send(resp.encode()) config = initialization(socket) elif message["type"] == "node": - res = process_node_data(config, message["data"]) + if message["last"]: + logger.debug(f'final element of priority queue') + res = process_node_data(config, message["data"], message["last"]) response = res.to_bytes(1, byteorder=sys.byteorder) logger.debug(f"Sending response {res}") socket.send(response) diff --git a/src/shim/vyshim.c b/src/shim/vyshim.c index cae8b6152..41723e7a4 100644 --- a/src/shim/vyshim.c +++ b/src/shim/vyshim.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 VyOS maintainers and contributors + * Copyright (C) 2020-2024 VyOS maintainers and contributors * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 or later as @@ -49,6 +49,7 @@ #define GET_SESSION "cli-shell-api --show-working-only --show-show-defaults --show-ignore-edit showConfig" #define COMMIT_MARKER "/var/tmp/initial_in_commit" +#define QUEUE_MARKER "/var/tmp/last_in_queue" enum { SUCCESS = 1 << 0, @@ -77,6 +78,7 @@ int main(int argc, char* argv[]) int ex_index; int init_timeout = 0; + int last = 0; debug_print("Connecting to vyos-configd ...\n"); zmq_connect(requester, SOCKET_PATH); @@ -101,10 +103,16 @@ int main(int argc, char* argv[]) return ret; } + if (access(QUEUE_MARKER, F_OK) != -1) { + last = 1; + remove(QUEUE_MARKER); + } + char error_code[1]; debug_print("Sending node data ...\n"); - char *string_node_data_msg = mkjson(MKJSON_OBJ, 2, + char *string_node_data_msg = mkjson(MKJSON_OBJ, 3, MKJSON_STRING, "type", "node", + MKJSON_BOOL, "last", last, MKJSON_STRING, "data", &string_node_data[0]); zmq_send(requester, string_node_data_msg, strlen(string_node_data_msg), 0); |