summaryrefslogtreecommitdiff
path: root/src/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'src/helpers')
-rwxr-xr-xsrc/helpers/add-system-version.py2
-rwxr-xr-xsrc/helpers/config_dependency.py6
-rwxr-xr-xsrc/helpers/geoip-update.py79
-rwxr-xr-xsrc/helpers/priority.py2
-rwxr-xr-xsrc/helpers/read-saved-value.py2
-rwxr-xr-xsrc/helpers/reset_section.py127
-rwxr-xr-xsrc/helpers/run-config-activation.py91
-rwxr-xr-xsrc/helpers/run-config-migration.py9
-rwxr-xr-xsrc/helpers/set_vyconf_backend.py43
-rwxr-xr-xsrc/helpers/show_commit_data.py2
-rwxr-xr-xsrc/helpers/strip-private.py2
-rwxr-xr-xsrc/helpers/teardown-config-session.py32
-rwxr-xr-xsrc/helpers/test_commit.py2
-rwxr-xr-xsrc/helpers/validate-config.py48
-rwxr-xr-xsrc/helpers/vyconf_cli.py50
-rwxr-xr-xsrc/helpers/vyos-boot-config-loader.py21
-rwxr-xr-xsrc/helpers/vyos-certbot-renew-pki.sh3
-rwxr-xr-xsrc/helpers/vyos-check-wwan.py2
-rwxr-xr-xsrc/helpers/vyos-config-encrypt.py244
-rwxr-xr-xsrc/helpers/vyos-failover.py604
-rwxr-xr-xsrc/helpers/vyos-interface-rescan.py2
-rwxr-xr-xsrc/helpers/vyos-load-balancer.py114
-rwxr-xr-xsrc/helpers/vyos-load-config.py102
-rwxr-xr-xsrc/helpers/vyos-merge-config.py143
-rwxr-xr-xsrc/helpers/vyos-request-configd-update.py31
-rwxr-xr-xsrc/helpers/vyos-save-config.py54
-rwxr-xr-xsrc/helpers/vyos-sudo.py33
-rwxr-xr-xsrc/helpers/vyos_config_sync.py107
-rwxr-xr-xsrc/helpers/vyos_net_name276
-rw-r--r--src/helpers/write-config-file-value.py48
30 files changed, 1502 insertions, 779 deletions
diff --git a/src/helpers/add-system-version.py b/src/helpers/add-system-version.py
index 5270ee7d3..70bbd2202 100755
--- a/src/helpers/add-system-version.py
+++ b/src/helpers/add-system-version.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/src/helpers/config_dependency.py b/src/helpers/config_dependency.py
index 817bcc65a..4a7383cc2 100755
--- a/src/helpers/config_dependency.py
+++ b/src/helpers/config_dependency.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -59,7 +59,7 @@ def graph_from_dependency_dict(d: dict) -> dict:
for k in list(d):
g[k] = set()
# add the dependencies for every sub-case; should there be cases
- # that are mutally exclusive in the future, the graphs will be
+ # that are mutually exclusive in the future, the graphs will be
# distinguished
for el in list(d[k]):
g[k] |= set(d[k][el])
@@ -94,7 +94,7 @@ def path_exists(s):
return s
def main():
- parser = ArgumentParser(description='generate and save dict from xml defintions')
+ parser = ArgumentParser(description='generate and save dict from xml definitions')
parser.add_argument('--dependency-dir', type=path_exists,
default=dependency_dir,
help='location of vyos-1x dependency directory')
diff --git a/src/helpers/geoip-update.py b/src/helpers/geoip-update.py
index 34accf2cc..1879ab7ef 100755
--- a/src/helpers/geoip-update.py
+++ b/src/helpers/geoip-update.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -13,32 +13,77 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Note: script is used both in opmode and by build hook `40-init-geoip-database.chroot`
import argparse
import sys
from vyos.configquery import ConfigTreeQuery
-from vyos.firewall import geoip_update
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = ConfigTreeQuery()
- base = ['firewall']
+from vyos.geoip import geoip_download_dbip
+from vyos.geoip import geoip_download_maxmind
+from vyos.geoip import db_initialise
+from vyos.geoip import db_is_initialised
+from vyos.geoip import db_import_dbip_ranges
+from vyos.geoip import db_import_maxmind_ranges
+from vyos.geoip import geoip_update
- if not conf.exists(base):
- return None
-
- return conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
+def get_config(conf):
+ return (
+ conf.get_config_dict(['firewall', 'global-options', 'geoip'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True, with_defaults=True),
+ conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True) if conf.exists(['firewall']) else None,
+ conf.get_config_dict(['policy'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True) if conf.exists(['policy']) else None,
+ )
if __name__ == '__main__':
parser = argparse.ArgumentParser()
- parser.add_argument("--force", help="Force update", action="store_true")
+ parser.add_argument("--init", help="Initialise", action="store_true")
args = parser.parse_args()
- firewall = get_config()
+ if args.init:
+ db_initialise()
+ db_import_dbip_ranges(delete_file=True)
+ sys.exit(0)
+
+ conf = ConfigTreeQuery()
+
+ if not conf.exists(['system', 'name-server']):
+ print('There are no system name-servers configured')
+ sys.exit(1)
+
+ options, firewall, policy = get_config(conf)
+
+ if not db_is_initialised():
+ db_initialise()
+
+ if options['provider'] == 'db-ip':
+ print('Downloading latest DB-IP database...')
+ if not geoip_download_dbip():
+ print('Failed to download, aborting.')
+ sys.exit(1)
+
+ print('Extracting database...')
+ if not db_import_dbip_ranges(delete_file=True):
+ print('Failed to extract, aborting.')
+ sys.exit(1)
+
+ elif options['provider'] == 'maxmind':
+ account_id = options['maxmind_account_id']
+ license_key = options['maxmind_license_key']
+ lite = 'maxmind_lite' in options
+
+ print('Downloading latest MaxMind database...')
+ if not geoip_download_maxmind(account_id, license_key, lite):
+ print('Failed to download, aborting.')
+ sys.exit(1)
+
+ print('Extracting database...')
+ if not db_import_maxmind_ranges(delete_file=True):
+ print('Failed to extract, aborting.')
+ sys.exit(1)
- if not geoip_update(firewall, force=args.force):
+ if not geoip_update(firewall=firewall, policy=policy):
sys.exit(1)
diff --git a/src/helpers/priority.py b/src/helpers/priority.py
index 04186104c..6630889af 100755
--- a/src/helpers/priority.py
+++ b/src/helpers/priority.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/helpers/read-saved-value.py b/src/helpers/read-saved-value.py
index 1463e9ffe..f4048f373 100755
--- a/src/helpers/read-saved-value.py
+++ b/src/helpers/read-saved-value.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/helpers/reset_section.py b/src/helpers/reset_section.py
new file mode 100755
index 000000000..2d5695d6c
--- /dev/null
+++ b/src/helpers/reset_section.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+
+import argparse
+import sys
+import os
+import grp
+
+from vyos.configsession import ConfigSession
+from vyos.config import Config
+from vyos.configdiff import get_config_diff
+from vyos.xml_ref import is_leaf
+from vyos.utils.commit import wait_for_commit_lock
+
+
+CFG_GROUP = 'vyattacfg'
+DEBUG = False
+
+
+def type_str_to_list(value):
+ if isinstance(value, str):
+ return value.split()
+ raise argparse.ArgumentTypeError('path must be a whitespace separated string')
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument('path', type=type_str_to_list, help='section to reload/rollback')
+parser.add_argument('--pid', help='pid of config session')
+
+group = parser.add_mutually_exclusive_group()
+group.add_argument('--reload', action='store_true', help='retry proposed commit')
+group.add_argument(
+ '--rollback', action='store_true', default=True, help='rollback to stable commit'
+)
+
+args = parser.parse_args()
+
+path = args.path
+reload = args.reload
+rollback = args.rollback
+pid = args.pid
+
+try:
+ if is_leaf(path):
+ sys.exit('path is leaf node: neither allowed nor useful')
+except ValueError:
+ if DEBUG:
+ sys.exit('nonexistent path: neither allowed nor useful')
+ else:
+ sys.exit()
+
+test = Config()
+in_session = test.in_session()
+
+if in_session:
+ if reload:
+ sys.exit('reset_section reload not available inside of a config session')
+
+ diff = get_config_diff(test)
+ if not diff.is_node_changed(path):
+ # No discrepancies at path after commit, hence no error to revert.
+ sys.exit()
+
+ del diff
+else:
+ if not reload:
+ sys.exit('reset_section rollback not available outside of a config session')
+
+del test
+
+
+session_id = int(pid) if pid else os.getpid()
+
+if in_session:
+ # check hint left by vyshim when ConfigError is from apply stage
+ hint_name = f'/tmp/apply_{session_id}'
+ if not os.path.exists(hint_name):
+ # no apply error; exit
+ sys.exit()
+ else:
+ # cleanup hint and continue with reset
+ os.unlink(hint_name)
+
+cfg_group = grp.getgrnam(CFG_GROUP)
+os.setgid(cfg_group.gr_gid)
+os.umask(0o002)
+
+wait_for_commit_lock()
+
+shared = not bool(reload)
+
+session = ConfigSession(session_id, shared=shared)
+
+session_env = session.get_session_env()
+config = Config(session_env)
+
+d = config.get_config_dict(path, effective=True, get_first_key=True)
+
+if in_session:
+ session.discard()
+
+session.delete(path)
+session.commit()
+
+if not d:
+ # nothing more to do in either case of reload/rollback
+ sys.exit()
+
+session.set_section(path, d)
+out = session.commit()
+print(out)
diff --git a/src/helpers/run-config-activation.py b/src/helpers/run-config-activation.py
index 58293702a..7e7a6571e 100755
--- a/src/helpers/run-config-activation.py
+++ b/src/helpers/run-config-activation.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -14,20 +14,33 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import re
+
+import sys
import logging
+
from pathlib import Path
from argparse import ArgumentParser
from vyos.compose_config import ComposeConfig
from vyos.compose_config import ComposeConfigError
+from vyos.utils.activate import refresh_activation_list
+from vyos.utils.activate import get_activation_scripts
+from vyos.utils.activate import get_activation
+from vyos.utils.activate import set_activation
+from vyos.utils.activate import is_active
+from vyos.utils.system import load_as_module
+from vyos.utils.func import FalseCallable
from vyos.defaults import directories
+from vyos.defaults import activation_list
+
parser = ArgumentParser()
-parser.add_argument('config_file', type=str,
- help="configuration file to modify with system-specific settings")
-parser.add_argument('--test-script', type=str,
- help="test effect of named script")
+parser.add_argument(
+ 'config_file',
+ type=str,
+ help='configuration file to modify with system-specific settings',
+)
+parser.add_argument('--test-script', type=str, help='test effect of named script')
args = parser.parse_args()
@@ -40,6 +53,7 @@ formatter = logging.Formatter('%(message)s')
fh.setFormatter(formatter)
logger.addHandler(fh)
+
if 'vyos-activate-debug' in Path('/proc/cmdline').read_text():
print(f'\nactivate-debug enabled: file {checkpoint_file}_* on error')
debug = checkpoint_file
@@ -48,36 +62,67 @@ else:
debug = None
logger.setLevel(logging.INFO)
-def sort_key(s: Path):
- s = s.stem
- pre, rem = re.match(r'(\d*)(?:-)?(.+)', s).groups()
- return int(pre or 0), rem
def file_ext(file_name: str) -> str:
- """Return an identifier from file name for checkpoint file extension.
- """
+ """Return an identifier from file name for checkpoint file extension."""
return Path(file_name).stem
+
+refresh_activation_list()
+
+if not Path(activation_list).exists():
+ logger.error('Missing config activation list!')
+ sys.exit(1)
+
script_dir = Path(directories['activate'])
if args.test_script:
- script_list = [script_dir.joinpath(args.test_script)]
+ script_list = [script_dir.joinpath(args.test_script).stem]
else:
- script_list = sorted(script_dir.glob('*.py'), key=sort_key)
+ script_list = list(get_activation_scripts())
config_file = args.config_file
config_str = Path(config_file).read_text()
compose = ComposeConfig(config_str, checkpoint_file=debug)
+false_call = FalseCallable()
+
+update_config = False
for file in script_list:
- file = file.as_posix()
+ if not is_active(file):
+ continue
+
logger.info(f'calling {file}')
- try:
- compose.apply_file(file, func_name='activate')
- except ComposeConfigError as e:
- if debug:
- compose.write(f'{compose.checkpoint_file}_{file_ext(file)}')
- logger.error(f'config-activation error in {file}: {e}')
-
-compose.write(config_file, with_version=True)
+
+ mod_name = Path(file).stem.replace('-', '_')
+ mod = load_as_module(mod_name, script_dir.joinpath(f'{file}.py').as_posix())
+
+ pre_condition = getattr(mod, 'pre_condition', false_call)
+ post_condition = getattr(mod, 'post_condition', false_call)
+ activate = getattr(mod, 'activate', false_call)
+
+ if not activate:
+ logger.error(f'missing activate function in {file}')
+ continue
+
+ if not pre_condition or pre_condition():
+ try:
+ compose.apply_func(activate)
+ except ComposeConfigError as e:
+ if debug:
+ compose.write(f'{compose.checkpoint_file}_{file_ext(file)}')
+ logger.error(f'config-activation error in {file}: {e}')
+ update_config = False
+ break
+
+ if post_condition:
+ post_condition()
+
+ if get_activation(file) == 'once':
+ set_activation(file, 'off')
+
+ update_config = True
+
+if update_config:
+ compose.write(config_file, with_version=True)
diff --git a/src/helpers/run-config-migration.py b/src/helpers/run-config-migration.py
index e6ce97363..6a3533644 100755
--- a/src/helpers/run-config-migration.py
+++ b/src/helpers/run-config-migration.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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 time
from argparse import ArgumentParser
from shutil import copyfile
+from vyos.utils.file import read_file
from vyos.migrate import ConfigMigrate
from vyos.migrate import ConfigMigrateError
@@ -76,3 +77,9 @@ except ConfigMigrateError as e:
if backup is not None and not config_migrate.config_modified:
os.unlink(backup)
+
+# T1771: add knob on Kernel command-line to simulate failed config migrator run
+# used to test if the automatic image reboot works.
+kernel_cmdline = read_file('/proc/cmdline')
+if 'vyos-fail-migration' in kernel_cmdline.split():
+ sys.exit(1)
diff --git a/src/helpers/set_vyconf_backend.py b/src/helpers/set_vyconf_backend.py
new file mode 100755
index 000000000..dddbe12f6
--- /dev/null
+++ b/src/helpers/set_vyconf_backend.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+# N.B. only for use within testing framework; explicit invocation will leave
+# system in inconsistent state.
+
+import os
+import sys
+from argparse import ArgumentParser
+
+from vyos.utils.backend import set_vyconf_backend
+
+if os.getuid() != 0:
+ sys.exit('Requires root privileges')
+
+parser = ArgumentParser()
+parser.add_argument('--disable', action='store_true',
+ help='enable/disable vyconf backend')
+parser.add_argument('--no-prompt', action='store_true',
+ help='confirm without prompt')
+
+args = parser.parse_args()
+
+match args.disable:
+ case False:
+ set_vyconf_backend(True, no_prompt=args.no_prompt)
+ case True:
+ set_vyconf_backend(False, no_prompt=args.no_prompt)
diff --git a/src/helpers/show_commit_data.py b/src/helpers/show_commit_data.py
index d507ed9a4..85ee64cb1 100755
--- a/src/helpers/show_commit_data.py
+++ b/src/helpers/show_commit_data.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2025 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/helpers/strip-private.py b/src/helpers/strip-private.py
index cb29069cf..71b7c079a 100755
--- a/src/helpers/strip-private.py
+++ b/src/helpers/strip-private.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-# Copyright 2021-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/src/helpers/teardown-config-session.py b/src/helpers/teardown-config-session.py
new file mode 100755
index 000000000..49a3f5bc5
--- /dev/null
+++ b/src/helpers/teardown-config-session.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+
+from vyos.vyconf_session import VyconfSession
+
+if len(sys.argv) < 2:
+ sys.exit('session pid is required')
+
+pid = sys.argv[1]
+
+try:
+ vc = VyconfSession(pid=int(pid), extant=True)
+except ValueError:
+ # as this script is called on any config session exit, ignore non-existent
+ pass
+else:
+ vc.teardown()
diff --git a/src/helpers/test_commit.py b/src/helpers/test_commit.py
index 00a413687..cfff85b9d 100755
--- a/src/helpers/test_commit.py
+++ b/src/helpers/test_commit.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2025 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/helpers/validate-config.py b/src/helpers/validate-config.py
new file mode 100755
index 000000000..1c77d2240
--- /dev/null
+++ b/src/helpers/validate-config.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import sys
+import argparse
+
+from vyos.configtree import ConfigTree
+from vyos.configtree import validate_tree_filter
+from vyos.component_version import add_system_version
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument('config_file', help='config file to validate')
+parser.add_argument('--filtered-config', help='write valid subset of config file')
+
+args = parser.parse_args()
+
+config_file = args.config_file
+filtered_config = args.filtered_config
+
+with open(config_file) as f:
+ config_str = f.read()
+
+config_tree = ConfigTree(config_str)
+
+valid_tree, out = validate_tree_filter(config_tree)
+
+if filtered_config:
+ add_system_version(valid_tree.to_string(), filtered_config)
+
+if out:
+ print(out)
+
+sys.exit(int(bool(out)))
diff --git a/src/helpers/vyconf_cli.py b/src/helpers/vyconf_cli.py
new file mode 100755
index 000000000..9a614675b
--- /dev/null
+++ b/src/helpers/vyconf_cli.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import os
+import sys
+
+from vyos.vyconf_session import VyconfSession
+
+
+pid = os.getppid()
+
+vs = VyconfSession(pid=pid)
+
+script_path = sys.argv[0]
+script_name = os.path.basename(script_path)
+# drop prefix 'vy_' if present
+if script_name.startswith('vy_'):
+ func_name = script_name[3:]
+else:
+ func_name = script_name
+
+if hasattr(vs, func_name):
+ func = getattr(vs, func_name)
+else:
+ sys.exit(f'Call unimplemented: {func_name}')
+
+res = func()
+if isinstance(res, bool):
+ # for use in shell scripts
+ sys.exit(int(not res))
+
+if isinstance(res, tuple):
+ out, err = res
+ print(out)
+ sys.exit(err)
diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py
index 42de696ce..9826a6d3e 100755
--- a/src/helpers/vyos-boot-config-loader.py
+++ b/src/helpers/vyos-boot-config-loader.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -13,8 +13,6 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#
import os
import sys
@@ -23,10 +21,13 @@ import grp
import traceback
from datetime import datetime
-from vyos.defaults import directories, config_status
-from vyos.configsession import ConfigSession, ConfigSessionError
+from vyos.defaults import directories
+from vyos.defaults import config_status
+from vyos.configsession import ConfigSession
+from vyos.configsession import ConfigSessionError
from vyos.configtree import ConfigTree
from vyos.utils.process import cmd
+from vyos.utils.file import write_file
STATUS_FILE = config_status
TRACE_FILE = '/tmp/boot-config-trace'
@@ -68,16 +69,16 @@ def trace_to_file(trace_file_name):
print('{0}'.format(e))
def failsafe(config_file_name):
- fail_msg = """
+ fail_msg = f"""
!!!!!
There were errors loading the configuration
- Please examine the errors in
- {0}
- and correct
+ Please examine the errors in:
+ {TRACE_FILE}
!!!!!
- """.format(TRACE_FILE)
+ """
print(fail_msg, file=sys.stderr)
+ write_file('/run/motd.d/9999-boot-config-error', fail_msg)
users = [x[0] for x in pwd.getpwall()]
if 'vyos' in users:
diff --git a/src/helpers/vyos-certbot-renew-pki.sh b/src/helpers/vyos-certbot-renew-pki.sh
deleted file mode 100755
index 1c273d2fa..000000000
--- a/src/helpers/vyos-certbot-renew-pki.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/vbash
-source /opt/vyatta/etc/functions/script-template
-/usr/libexec/vyos/conf_mode/pki.py certbot_renew
diff --git a/src/helpers/vyos-check-wwan.py b/src/helpers/vyos-check-wwan.py
index 334f08dd3..4768ddf3f 100755
--- a/src/helpers/vyos-check-wwan.py
+++ b/src/helpers/vyos-check-wwan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py
index 84860bd6a..e035040e1 100755
--- a/src/helpers/vyos-config-encrypt.py
+++ b/src/helpers/vyos-config-encrypt.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -20,43 +20,32 @@ import sys
from argparse import ArgumentParser
from cryptography.fernet import Fernet
-from tempfile import NamedTemporaryFile
-from tempfile import TemporaryDirectory
+from tempfile import NamedTemporaryFile, TemporaryDirectory
-from vyos.tpm import clear_tpm_key
-from vyos.tpm import read_tpm_key
-from vyos.tpm import write_tpm_key
+from vyos.system.image import is_live_boot, get_running_image
+from vyos.tpm import clear_tpm_key, read_tpm_key, write_tpm_key
from vyos.utils.io import ask_input, ask_yes_no
-from vyos.utils.process import cmd
+from vyos.utils.process import cmd, run
+from vyos.defaults import directories
persistpath_cmd = '/opt/vyatta/sbin/vyos-persistpath'
-mount_paths = ['/config', '/opt/vyatta/etc/config']
+# mount_path is /opt/vyatta/etc/config as of this writing
+mount_path = directories['config']
+mount_path_old = f'{mount_path}.old'
dm_device = '/dev/mapper/vyos_config'
+
def is_opened():
return os.path.exists(dm_device)
-def get_current_image():
- with open('/proc/cmdline', 'r') as f:
- args = f.read().split(" ")
- for arg in args:
- if 'vyos-union' in arg:
- k, v = arg.split("=")
- path_split = v.split("/")
- return path_split[-1]
- return None
-
def load_config(key):
if not key:
return
persist_path = cmd(persistpath_cmd).strip()
- image_name = get_current_image()
+ image_name = get_running_image()
image_path = os.path.join(persist_path, 'luks', image_name)
- if not os.path.exists(image_path):
- raise Exception("Encrypted config volume doesn't exist")
-
if is_opened():
print('Encrypted config volume is already mounted')
return
@@ -67,24 +56,22 @@ def load_config(key):
cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
- for path in mount_paths:
- cmd(f'mount /dev/mapper/vyos_config {path}')
- cmd(f'chgrp -R vyattacfg {path}')
+ run(f'umount -l {mount_path}')
+ cmd(f'mount /dev/mapper/vyos_config {mount_path}')
+ cmd(f'chgrp -R vyattacfg {mount_path}')
os.unlink(key_file)
return True
-def encrypt_config(key, recovery_key):
- if is_opened():
- raise Exception('An encrypted config volume is already mapped')
-
+def encrypt_config(key, recovery_key=None, is_tpm=True):
# Clear and write key to TPM
- try:
- clear_tpm_key()
- except:
- pass
- write_tpm_key(key)
+ if is_tpm:
+ try:
+ clear_tpm_key()
+ except:
+ pass
+ write_tpm_key(key)
persist_path = cmd(persistpath_cmd).strip()
size = ask_input('Enter size of encrypted config partition (MB): ', numeric_only=True, default=512)
@@ -94,38 +81,49 @@ def encrypt_config(key, recovery_key):
if not os.path.isdir(luks_folder):
os.mkdir(luks_folder)
- image_name = get_current_image()
+ image_name = get_running_image()
image_path = os.path.join(luks_folder, image_name)
- # Create file for encrypted config
- cmd(f'fallocate -l {size}M {image_path}')
+ try:
+ # Create file for encrypted config
+ cmd(f'fallocate -l {size}M {image_path}')
- # Write TPM key for slot #1
- with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
- f.write(key)
- key_file = f.name
+ # Write TPM key for slot #1
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
- # Format and add main key to volume
- cmd(f'cryptsetup -q luksFormat {image_path} {key_file}')
+ # Format and add main key to volume
+ cmd(f'cryptsetup -q luksFormat {image_path} {key_file}')
- if recovery_key:
- # Write recovery key for slot 2
- with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
- f.write(recovery_key)
- recovery_key_file = f.name
+ if recovery_key:
+ # Write recovery key for slot 2
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(recovery_key)
+ recovery_key_file = f.name
- cmd(f'cryptsetup -q luksAddKey {image_path} {recovery_key_file} --key-file={key_file}')
+ cmd(f'cryptsetup -q luksAddKey {image_path} {recovery_key_file} --key-file={key_file}')
- # Open encrypted volume and format with ext4
- cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
- cmd('mkfs.ext4 /dev/mapper/vyos_config')
+ # Open encrypted volume and format with ext4
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+ cmd('mkfs.ext4 /dev/mapper/vyos_config')
+ except Exception as e:
+ print('An error occurred while creating the encrypted config volume, aborting.')
+
+ if os.path.exists('/dev/mapper/vyos_config'):
+ run('cryptsetup -q close vyos_config')
+
+ if os.path.exists(image_path):
+ os.unlink(image_path)
+
+ raise e
with TemporaryDirectory() as d:
cmd(f'mount /dev/mapper/vyos_config {d}')
- # Move /config to encrypted volume
- shutil.copytree('/config', d, copy_function=shutil.move, dirs_exist_ok=True)
-
+ # Move mount_path to encrypted volume
+ shutil.copytree(mount_path, d, copy_function=shutil.move, dirs_exist_ok=True)
+ cmd(f'chgrp -R vyattacfg {d}')
cmd(f'umount {d}')
os.unlink(key_file)
@@ -133,22 +131,53 @@ def encrypt_config(key, recovery_key):
if recovery_key:
os.unlink(recovery_key_file)
- for path in mount_paths:
- cmd(f'mount /dev/mapper/vyos_config {path}')
- cmd(f'chgrp vyattacfg {path}')
+ run(f'umount -l {mount_path}')
+ cmd(f'mount /dev/mapper/vyos_config {mount_path}')
+ cmd(f'chgrp vyattacfg {mount_path}')
return True
-def decrypt_config(key):
+def config_backup_folder(base):
+ # Get next available backup folder
+ if not os.path.exists(base):
+ return base
+
+ idx = 1
+ while os.path.exists(f'{base}.{idx}'):
+ idx += 1
+ return f'{base}.{idx}'
+
+def test_decrypt(key):
if not key:
return
persist_path = cmd(persistpath_cmd).strip()
- image_name = get_current_image()
+ image_name = get_running_image()
image_path = os.path.join(persist_path, 'luks', image_name)
- if not os.path.exists(image_path):
- raise Exception("Encrypted config volume doesn't exist")
+ key_file = None
+
+ if not is_opened():
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ try:
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+ os.unlink(key_file)
+ return True
+ except:
+ return False
+ return False
+
+def decrypt_config(key):
+ if not key:
+ return
+
+ persist_path = cmd(persistpath_cmd).strip()
+ image_name = get_running_image()
+ image_path = os.path.join(persist_path, 'luks', image_name)
+ original_config_path = os.path.join(persist_path, 'boot', image_name, 'rw', 'opt', 'vyatta', 'etc', 'config')
key_file = None
@@ -160,22 +189,26 @@ def decrypt_config(key):
cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
# unmount encrypted volume mount points
- for path in mount_paths:
- if os.path.ismount(path):
- cmd(f'umount {path}')
+ run(f'umount -Alq /dev/mapper/vyos_config')
+
+ # If /opt/vyatta/etc/config is populated, move to /opt/vyatta/etc/config.old
+ if len(os.listdir(mount_path)) > 0:
+ backup_path = config_backup_folder(mount_path_old)
+ print(f'Moving existing {mount_path} folder to {backup_path}')
+ shutil.move(mount_path, backup_path)
- # If /config is populated, move to /config.old
- if len(os.listdir('/config')) > 0:
- print('Moving existing /config folder to /config.old')
- shutil.move('/config', '/config.old')
+ # Mount original persistence config path
+ if not os.path.exists(mount_path):
+ os.mkdir(mount_path)
+ cmd(f'mount --bind {original_config_path} {mount_path}')
# Temporarily mount encrypted volume and migrate files to /config on rootfs
with TemporaryDirectory() as d:
cmd(f'mount /dev/mapper/vyos_config {d}')
- # Move encrypted volume to /config
- shutil.copytree(d, '/config', copy_function=shutil.move, dirs_exist_ok=True)
- cmd(f'chgrp -R vyattacfg /config')
+ # Move encrypted volume to /opt/vyatta/etc/config
+ shutil.copytree(d, mount_path, copy_function=shutil.move, dirs_exist_ok=True)
+ cmd(f'chgrp -R vyattacfg {mount_path}')
cmd(f'umount {d}')
@@ -188,7 +221,8 @@ def decrypt_config(key):
os.unlink(image_path)
try:
- clear_tpm_key()
+ if ask_yes_no('Do you want to clear the TPM? This will cause issues if other system images use the key'):
+ clear_tpm_key()
except:
pass
@@ -199,12 +233,28 @@ if __name__ == '__main__':
print("Must specify action.")
sys.exit(1)
+ if is_live_boot():
+ print("Config encryption not available on live-ISO environment")
+ sys.exit(1)
+
parser = ArgumentParser(description='Config encryption')
parser.add_argument('--disable', help='Disable encryption', action="store_true")
parser.add_argument('--enable', help='Enable encryption', action="store_true")
parser.add_argument('--load', help='Load encrypted config volume', action="store_true")
args = parser.parse_args()
+ if args.disable or args.load:
+ persist_path = cmd(persistpath_cmd).strip()
+ image_name = get_running_image()
+ image_path = os.path.join(persist_path, 'luks', image_name)
+
+ if not os.path.exists(image_path):
+ print('Encrypted config volume does not exist, aborting.')
+ sys.exit(0)
+ elif args.enable and is_opened():
+ print('An encrypted config volume is already mapped, aborting.')
+ sys.exit(0)
+
tpm_exists = os.path.exists('/sys/class/tpm/tpm0')
key = None
@@ -213,30 +263,43 @@ if __name__ == '__main__':
question_key_str = 'recovery key' if tpm_exists else 'key'
- if tpm_exists:
- if args.enable:
- key = Fernet.generate_key()
- elif args.disable or args.load:
+ if not is_opened():
+ if tpm_exists:
+ existing_key = None
+
try:
- key = read_tpm_key()
- need_recovery = False
- except:
- print('Failed to read key from TPM, recovery key required')
- need_recovery = True
- else:
- need_recovery = True
+ existing_key = read_tpm_key()
+ except: pass
+
+ if args.enable:
+ if existing_key:
+ print('WARNING: An encryption key already exists in the TPM.')
+ print('If you choose not to use the existing key, any system image')
+ print('using the old key will need the recovery key.')
+ if existing_key and ask_yes_no('Do you want to use the existing TPM key?'):
+ key = existing_key
+ else:
+ key = Fernet.generate_key()
+ elif args.disable or args.load:
+ if existing_key and test_decrypt(existing_key):
+ need_recovery = False
+ else:
+ print('TPM key invalid or not found, recovery key required')
+ need_recovery = True
+ else:
+ need_recovery = True
if args.enable and not tpm_exists:
print('WARNING: VyOS will boot into a default config when encrypted without a TPM')
print('You will need to manually login with default credentials and use "encryption load"')
- print('to mount the encrypted volume and use "load /config/config.boot"')
+ print(f'to mount the encrypted volume and use "load {mount_path}/config.boot"')
if not ask_yes_no('Are you sure you want to proceed?'):
sys.exit(0)
if need_recovery or (args.enable and not ask_yes_no(f'Automatically generate a {question_key_str}?', default=True)):
while True:
- recovery_key = ask_input(f'Enter {question_key_str}:', default=None).encode()
+ recovery_key = ask_input(f'Enter {question_key_str}:', default=None, no_echo=True).encode()
if len(recovery_key) >= 32:
break
@@ -250,12 +313,12 @@ if __name__ == '__main__':
decrypt_config(key or recovery_key)
print('Encrypted config volume has been disabled')
- print('Contents have been migrated to /config on rootfs')
+ print(f'Contents have been migrated to {mount_path} on rootfs')
elif args.load:
load_config(key or recovery_key)
print('Encrypted config volume has been mounted')
- print('Use "load /config/config.boot" to load configuration')
+ print(f'Use "load {mount_path}/config.boot" to load configuration')
elif args.enable and tpm_exists:
encrypt_config(key, recovery_key)
@@ -263,7 +326,10 @@ if __name__ == '__main__':
print('Backup the recovery key in a safe place!')
print('Recovery key: ' + recovery_key.decode())
elif args.enable:
- encrypt_config(recovery_key)
+ if recovery_key != ask_input('Confirm key:', default=None, no_echo=True).encode():
+ raise ValueError("Keys did not match!")
+
+ encrypt_config(recovery_key, is_tpm=False)
print('Encrypted config volume has been enabled without TPM')
print('Backup the key in a safe place!')
diff --git a/src/helpers/vyos-failover.py b/src/helpers/vyos-failover.py
index 348974364..22f8f1d35 100755
--- a/src/helpers/vyos-failover.py
+++ b/src/helpers/vyos-failover.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -15,22 +15,50 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
+import atexit
import json
-import socket
+import signal
import time
+from collections import namedtuple
+from vyos.template import get_dhcp_router
from vyos.utils.process import rc_cmd
+from vyos.utils.process import run
from pathlib import Path
from systemd import journal
my_name = Path(__file__).stem
+# Timeout between configuration reading
+# When no checks timeouts worked (e.g. no config files)
+config_timeout = 1
-def is_route_exists(route, gateway, interface, metric):
+# Useful debug info to console, use debug = True
+# sudo systemctl stop vyos-failover.service
+# sudo /usr/libexec/vyos/vyos-failover.py --config /run/vyos-failover.conf
+debug = False
+debug_output_journal = False
+debug_output_print = True
+
+
+def print_debug(*args, **kwargs):
+ if debug:
+ if debug_output_print:
+ print(*args, **kwargs)
+ if debug_output_journal:
+ journal.send(*args, **kwargs, SYSLOG_IDENTIFIER=my_name)
+
+
+def wrap_vrf(command, vrf):
+ if not vrf:
+ return command
+ return f"sudo ip vrf exec {vrf} {command}"
+
+
+def is_route_exists(ip_args):
"""Check if route with expected gateway, dev and metric exists"""
- rc, data = rc_cmd(f'ip --json route show protocol failover {route} '
- f'via {gateway} dev {interface} metric {metric}')
+ rc, data = rc_cmd(f'ip --json route show {ip_args}')
if rc == 0:
data = json.loads(data)
if len(data) > 0:
@@ -38,41 +66,7 @@ def is_route_exists(route, gateway, interface, metric):
return False
-def get_best_route_options(route, debug=False):
- """
- Return current best route ('gateway, interface, metric)
-
- % get_best_route_options('203.0.113.1')
- ('192.168.0.1', 'eth1', 1)
-
- % get_best_route_options('203.0.113.254')
- (None, None, None)
- """
- rc, data = rc_cmd(f'ip --detail --json route show protocol failover {route}')
- if rc == 0:
- data = json.loads(data)
- if len(data) == 0:
- print(f'\nRoute {route} for protocol failover was not found')
- return None, None, None
- # Fake metric 999 by default
- # Search route with the lowest metric
- best_metric = 999
- for entry in data:
- if debug: print('\n', entry)
- metric = entry.get('metric')
- gateway = entry.get('gateway')
- iface = entry.get('dev')
- if metric < best_metric:
- best_metric = metric
- best_gateway = gateway
- best_interface = iface
- if debug:
- print(f'### Best_route exists: {route}, best_gateway: {best_gateway}, '
- f'best_metric: {best_metric}, best_iface: {best_interface}')
- return best_gateway, best_interface, best_metric
-
-
-def is_port_open(ip, port):
+def is_port_open(ip, port, vrf=''):
"""
Check connection to remote host and port
Return True if host alive
@@ -80,33 +74,29 @@ def is_port_open(ip, port):
% is_port_open('example.com', 8080)
True
"""
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
- s.settimeout(2)
- try:
- s.connect((ip, int(port)))
- s.shutdown(socket.SHUT_RDWR)
- return True
- except:
- return False
- finally:
- s.close()
-
-
-def is_target_alive(target_list=None,
- iface='',
- proto='icmp',
- port=None,
- debug=False,
- policy='any-available') -> bool:
- """Check the availability of each target in the target_list using
+ cmd = wrap_vrf(f"nc -w2 -z {ip} {port}", vrf)
+ rc, data = rc_cmd(cmd)
+ return rc == 0
+
+
+def is_target_alive(
+ targets=None,
+ iface='',
+ proto='icmp',
+ port=None,
+ nexthop_vrf='',
+ policy='any-available',
+) -> bool:
+ """Check the availability of each target in the target_dict using
the specified protocol ICMP, ARP, TCP
Args:
- target_list (list): A list of IP addresses or hostnames to check.
+ targets (tuple of TargetNamedTuple): A dict: keys are IP addresses to check, values - dicts with options.
+ Possible keys (all optional): 'vrf' and 'interface'.
iface (str): The name of the network interface to use for the check.
proto (str): The protocol to use for the check. Options are 'icmp', 'arp', or 'tcp'.
port (int): The port number to use for the TCP check. Only applicable if proto is 'tcp'.
- debug (bool): If True, print debug information during the check.
+ nexthop_vrf (str): Nexthop VRF name - if specific vrf is not given for target, use this one
policy (str): The policy to use for the check. Options are 'any-available' or 'all-available'.
Returns:
@@ -120,30 +110,41 @@ def is_target_alive(target_list=None,
iface = f'-I {iface}'
num_reachable_targets = 0
- for target in target_list:
+ for options in targets:
+ target = options.target
+ vrf = options.vrf if options.vrf else nexthop_vrf
+ # don't use nexthop interface if 'vrf' is given
+ iface_opt = iface if vrf == nexthop_vrf else ''
+ # in any case if 'interface' is given, use it
+ if options.interface:
+ iface_opt = f'-I {options.interface}'
match proto:
case 'icmp':
- command = f'/usr/bin/ping -q {target} {iface} -n -c 2 -W 1'
+ command = f'/usr/bin/ping -q {target} {iface_opt} -n -c 2 -W 1'
+ command = wrap_vrf(command, vrf)
rc, response = rc_cmd(command)
- if debug:
- print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]')
+ print_debug(
+ f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]'
+ )
if rc == 0:
num_reachable_targets += 1
if policy == 'any-available':
return True
case 'arp':
- command = f'/usr/bin/arping -b -c 2 -f -w 1 -i 1 {iface} {target}'
+ command = f'/usr/bin/arping -b -c 2 -f -w 1 -i 1 {iface_opt} {target}'
+ command = wrap_vrf(command, vrf)
rc, response = rc_cmd(command)
- if debug:
- print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]')
+ print_debug(
+ f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]'
+ )
if rc == 0:
num_reachable_targets += 1
if policy == 'any-available':
return True
case _ if proto == 'tcp' and port is not None:
- if is_port_open(target, port):
+ if is_port_open(target, port, vrf=vrf):
num_reachable_targets += 1
if policy == 'any-available':
return True
@@ -151,85 +152,426 @@ def is_target_alive(target_list=None,
case _:
return False
- if policy == 'all-available' and num_reachable_targets == len(target_list):
+ if policy == 'all-available' and num_reachable_targets == len(targets):
return True
return False
-if __name__ == '__main__':
- # Parse command arguments and get config
- parser = argparse.ArgumentParser()
- parser.add_argument('-c',
- '--config',
- action='store',
- help='Path to protocols failover configuration',
- required=True,
- type=Path)
+TargetNamedTuple = namedtuple(
+ 'TargetConfig',
+ [
+ 'target',
+ 'vrf',
+ 'interface',
+ ],
+)
+
+NextHopNamedTuple = namedtuple(
+ 'NextHopConfig',
+ [
+ 'route',
+ 'dhcp_interface',
+ 'next_hop',
+ 'vrf',
+ 'vrf_opt',
+ 'conf_iface',
+ 'conf_metric',
+ 'port',
+ 'port_opt',
+ 'policy',
+ 'proto',
+ 'targets',
+ 'pretty_targets',
+ 'timeout',
+ 'onlink',
+ ],
+)
+
+
+def get_nexthop_config_vars(
+ destination, vrf, vrf_opt, nexthop_config, next_hop, dhcp_interface
+):
+ port = nexthop_config.get('check').get('port')
+
+ targets = tuple(
+ TargetNamedTuple(
+ target=target,
+ vrf=target_config.get('vrf', None),
+ interface=target_config.get('interface', None),
+ )
+ for target, target_config in nexthop_config.get('check').get('target').items()
+ )
+
+ # for print to journal and debug
+ pretty_targets = []
+ for target in targets:
+ p = target.target
+ options = []
+ if target.vrf:
+ options.append(f"vrf: {target.vrf}")
+ if target.interface:
+ options.append(f"interface: {target.interface}")
+ if options:
+ p += ' (' + ', '.join(options) + ')'
+ pretty_targets.append(p)
+ pretty_targets = ', '.join(pretty_targets)
+
+ return NextHopNamedTuple(
+ route=destination,
+ dhcp_interface=dhcp_interface,
+ next_hop=next_hop,
+ vrf=vrf,
+ vrf_opt=vrf_opt,
+ # For next-hop interface is mandatory
+ # For dhcp-interface it may be not given, then dhcp-interface is used
+ conf_iface=nexthop_config.get('interface', dhcp_interface),
+ conf_metric=int(nexthop_config.get('metric')),
+ port=port,
+ port_opt=f'port {port}' if port else '',
+ policy=nexthop_config.get('check').get('policy'),
+ proto=nexthop_config.get('check').get('type'),
+ targets=targets,
+ pretty_targets=pretty_targets,
+ timeout=nexthop_config.get('check').get('timeout'),
+ onlink='onlink' if 'onlink' in nexthop_config else '',
+ )
+
+
+RouteNamedTuple = namedtuple(
+ 'RouteConfig',
+ [
+ 'destination',
+ 'vrf',
+ 'vrf_opt',
+ 'config_path',
+ 'nexthops',
+ ],
+)
+
+
+def get_route_config(route, route_config, config_path, vrf):
+ vrf_opt = f'vrf {vrf}' if vrf else ''
+ nexthops = []
+ if route_config.get('next_hop'):
+ nexthops.extend(
+ get_nexthop_config_vars(route, vrf, vrf_opt, nexthop_config, next_hop, None)
+ for next_hop, nexthop_config in route_config.get('next_hop').items()
+ )
+ if route_config.get('dhcp_interface'):
+ nexthops.extend(
+ get_nexthop_config_vars(
+ route, vrf, vrf_opt, dhcp_nexthop_config, None, interface
+ )
+ for interface, dhcp_nexthop_config in route_config.get(
+ 'dhcp_interface'
+ ).items()
+ )
+ nexthops = tuple(nexthops)
+ return RouteNamedTuple(
+ destination=route,
+ vrf=vrf,
+ vrf_opt=vrf_opt,
+ config_path=config_path,
+ nexthops=nexthops,
+ )
+
+
+def parse_config(config, path):
+ parsed = []
+ vrf = config.get('vrf_context', '')
+ for route, route_config in config.get('route').items():
+ parsed.append(get_route_config(route, route_config, path, vrf))
+ return parsed
+
+
+def flush_all_routes():
+ print_debug("flush_all_routes called")
+ flush_cmd = 'ip route flush protocol failover table all'
+ run(flush_cmd)
+ journal.send(
+ flush_cmd,
+ SYSLOG_IDENTIFIER=my_name,
+ )
+
+
+kill_called = False
+
+
+def kill_handler(*args):
+ global kill_called
+ if kill_called:
+ return
+ kill_called = True
+ print_debug(f"kill_handler called for signal {args[0]}")
+
+
+def get_ip_command_args(nhc):
+ return (
+ f'{nhc.route} via {nhc.next_hop} dev {nhc.conf_iface} '
+ f'{nhc.onlink} metric {nhc.conf_metric} {nhc.vrf_opt} proto failover'
+ )
+
+
+def delete_route(ip_args):
+ print_debug(f' [ DEL ] -- ip route del {ip_args} [DELETE]')
+ rc_cmd(f'ip route del {ip_args}')
+ journal.send(
+ f'ip route del {ip_args}',
+ SYSLOG_IDENTIFIER=my_name,
+ )
+
+
+def update_configuration(last_modification_times, all_routes, config_dir):
+ """
+ Updates configuration:
+ rechecks config_dir for new/updated files,
+ deletes routes that were deleted from configuration
+
+ Args:
+ last_modification_times(dict): keys: relative path to file, value: last modification time.
+ Is updated.
+ all_routes(list): list of routes that were configured in previous call.
+ Is updated.
+ config_dir(Path): path to configuration directory
+ """
- args = parser.parse_args()
try:
- config_path = Path(args.config)
- config = json.loads(config_path.read_text())
- except Exception as err:
- print(
- f'Configuration file "{config_path}" does not exist or malformed: {err}'
- )
+ # First check if there are any changes at all
+ have_changes = False
+ for child in config_dir.iterdir():
+ file_key = str(child)
+ if file_key not in last_modification_times:
+ have_changes = True
+ print_debug(f"New file '{child}', have changes, rereading all")
+ break
+ modtime = child.stat().st_mtime_ns
+ if modtime != last_modification_times[file_key]:
+ have_changes = True
+ print_debug(f"File '{child} modified, have changes, rereading all...")
+ break
+
+ if not have_changes:
+ print_debug("No changes in configuration detected.")
+ return
+
+ last_modification_times.clear()
+ new_routes = []
+
+ # It is important that in configuration directory there MUST be
+ # only files generated by conf_mode/protocols_failover.py - otherwise
+ # the script won't be able to detect when all VRFs are disabled and
+ # won't be able to stop the service gracefully
+ for child in config_dir.iterdir():
+ if not child.is_file():
+ print(
+ f"Path {child} under configuration dir is not a file! Please clean configuration directory {config_dir}."
+ )
+ exit(1)
+
+ modtime = child.stat().st_mtime_ns
+ file_key = str(child)
+ last_modification_times[file_key] = modtime
+
+ try:
+ config = json.loads(child.read_text())
+ print_debug(f"Config from '{child}': {config}")
+ except OSError as err:
+ print(f'Configuration file "{child}" could not be read: {err}')
+ exit(1)
+ except json.JSONDecodeError as err:
+ print(
+ f'Configuration file "{child}" could not be parsed as JSON: {err}'
+ )
+ exit(1)
+ except UnicodeDecodeError as err:
+ print(f'Configuration file "{child}" has Unicode errors: {err}')
+ exit(1)
+
+ parsed_config = parse_config(config, file_key)
+ new_routes.extend(parsed_config)
+ except OSError as err:
+ print(f'Configuration dir "{config_dir}" does not exist or not readable: {err}')
exit(1)
- # Useful debug info to console, use debug = True
- # sudo systemctl stop vyos-failover.service
- # sudo /usr/libexec/vyos/vyos-failover.py --config /run/vyos-failover.conf
- debug = False
+ old_routes_set = set(all_routes)
+ new_routes_set = set(new_routes)
+
+ delete_routes = old_routes_set - new_routes_set
+ add_routes = new_routes_set - old_routes_set
+
+ # Delete not needed routes
+ for route_config in delete_routes:
+ print_debug(
+ f"Deleting route {route_config}, not present in updated configuration"
+ )
+ for nhc in route_config.nexthops:
+ ip_args = get_ip_command_args(nhc)
+ if is_route_exists(ip_args):
+ delete_route(ip_args)
+ all_routes.remove(route_config)
+
+ # Add new routes
+ print_debug(f"Adding routes {add_routes}, new in updated configuration")
+ all_routes.extend(add_routes)
- while(True):
+ print_debug(f"All routes: {all_routes}")
- for route, route_config in config.get('route').items():
- exists_gateway, exists_iface, exists_metric = get_best_route_options(route, debug=debug)
+def process_dhcp_interface(nhc, nexthop_by_dhcp_nexthop):
+ """
+ Processes NextHopNamedTuple with dhcp_interface != None
+ Return NextHopNamedTuple with next_hop equal to DHCP gateway of nhc.
+ If there is no gateway for nhc, return None
+
+ Args:
+ nhc(NextHopNamedTuple): configuration with dhcp_interface
+ nexthop_by_dhcp_nexthop(dict): dict with previous returned values
+ """
+ cur_dhcpgw = get_dhcp_router(nhc.dhcp_interface)
+ if not cur_dhcpgw:
+ cur_dhcpgw = False
+
+ if nhc in nexthop_by_dhcp_nexthop:
+ prev_dhcpgw = nexthop_by_dhcp_nexthop[nhc].next_hop
+ else:
+ prev_dhcpgw = False
+
+ # Equal - do nothing, just return previous value
+ if prev_dhcpgw == cur_dhcpgw:
+ if not cur_dhcpgw:
+ return None
+ return nexthop_by_dhcp_nexthop[nhc]
+
+ print_debug(
+ f"DHCP Gateway changed for interface {nhc.dhcp_interface} from '{prev_dhcpgw}' to '{cur_dhcpgw}'"
+ )
+
+ # dhcpgw differ and there was previous dhcpgw
+ if prev_dhcpgw:
+ prevnhc = nexthop_by_dhcp_nexthop.pop(nhc)
+ print_debug(
+ f"Deleting previous nexthop {prevnhc} because of DHCP interface change"
+ )
+ ip_args = get_ip_command_args(prevnhc)
+ if is_route_exists(ip_args):
+ delete_route(ip_args)
+
+ newnhc = None
+ # dhcpgw differ and there is new dhcpgw
+ if cur_dhcpgw:
+ newnhc = nhc._replace(next_hop=cur_dhcpgw, dhcp_interface=None)
+ print_debug(f"Saving new nexthop {newnhc} because of DHCP interface change")
+ nexthop_by_dhcp_nexthop[nhc] = newnhc
+
+ return newnhc
- for next_hop, nexthop_config in route_config.get('next_hop').items():
- conf_iface = nexthop_config.get('interface')
- conf_metric = int(nexthop_config.get('metric'))
- port = nexthop_config.get('check').get('port')
- port_opt = f'port {port}' if port else ''
- policy = nexthop_config.get('check').get('policy')
- proto = nexthop_config.get('check').get('type')
- target = nexthop_config.get('check').get('target')
- timeout = nexthop_config.get('check').get('timeout')
- onlink = 'onlink' if 'onlink' in nexthop_config else ''
+
+if __name__ == '__main__':
+ print_debug(f"{my_name} started")
+
+ # Parse command arguments and get config
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '-c',
+ '--config-dir',
+ action='store',
+ help='Path to protocols failover configuration dir',
+ required=True,
+ type=Path,
+ )
+
+ args = parser.parse_args()
+ config_dir = Path(args.config_dir)
+
+ last_modification_times = {}
+ all_routes = []
+
+
+ # Clean all `failover` routes now and at exit
+ flush_all_routes()
+ atexit.register(flush_all_routes)
+ signal.signal(signal.SIGINT, kill_handler)
+ signal.signal(signal.SIGTERM, kill_handler)
+
+ # keys: NextHopNamedTuple with dhcp_interface != None
+ # values: NextHopNamedTuple with next_hop != None
+ # Translates nexthop with dhcp_interface to usual nexthop
+ nexthop_by_dhcp_nexthop = {}
+
+ had_sleeps = True
+ while not kill_called:
+ # Check in case daemon was launched without routes
+ if not had_sleeps:
+ time.sleep(int(config_timeout))
+
+ update_configuration(last_modification_times, all_routes, config_dir)
+ had_sleeps = False
+
+ for route_config in all_routes:
+ if kill_called:
+ break
+ route = route_config.destination
+ vrf = route_config.vrf
+ vrf_opt = route_config.vrf_opt
+
+ for nhc in route_config.nexthops:
+ if nhc.dhcp_interface:
+ nhc = process_dhcp_interface(nhc, nexthop_by_dhcp_nexthop)
+ if not nhc:
+ continue
+
+ next_hop = nhc.next_hop
+ ip_args = get_ip_command_args(nhc)
+
+ is_alive = is_target_alive(
+ nhc.targets,
+ nhc.conf_iface,
+ nhc.proto,
+ nhc.port,
+ nexthop_vrf=vrf,
+ policy=nhc.policy,
+ )
# Route not found in the current routing table
- if not is_route_exists(route, next_hop, conf_iface, conf_metric):
- if debug: print(f" [NEW_ROUTE_DETECTED] route: [{route}]")
+ if not is_route_exists(ip_args):
+ print_debug(f" [NEW_ROUTE_DETECTED] route: [{route} {vrf_opt}]")
# Add route if check-target alive
- if is_target_alive(target, conf_iface, proto, port, debug=debug, policy=policy):
- if debug: print(f' [ ADD ] -- ip route add {route} via {next_hop} dev {conf_iface} '
- f'metric {conf_metric} proto failover\n###')
- rc, command = rc_cmd(f'ip route add {route} via {next_hop} dev {conf_iface} '
- f'{onlink} metric {conf_metric} proto failover')
+ if is_alive:
+ print_debug(f' [ ADD ] -- ip route add {ip_args}\n###')
+ rc, command = rc_cmd(f'ip route add {ip_args}')
# If something is wrong and gateway not added
# Example: Error: Next-hop has invalid gateway.
- if rc !=0:
- if debug: print(f'{command} -- return-code [RC: {rc}] {next_hop} dev {conf_iface}')
+ if rc != 0:
+ print_debug(
+ f'{command} -- return-code [RC: {rc}] {next_hop} dev {nhc.conf_iface}'
+ )
else:
- journal.send(f'ip route add {route} via {next_hop} dev {conf_iface} '
- f'{onlink} metric {conf_metric} proto failover', SYSLOG_IDENTIFIER=my_name)
+ journal.send(
+ f'ip route add {ip_args}',
+ SYSLOG_IDENTIFIER=my_name,
+ )
else:
- if debug: print(f' [ TARGET_FAIL ] target checks fails for [{target}], do nothing')
- journal.send(f'Check fail for route {route} target {target} proto {proto} '
- f'{port_opt}', SYSLOG_IDENTIFIER=my_name)
-
- # Route was added, check if the target is alive
- # We should delete route if check fails only if route exists in the routing table
- if not is_target_alive(target, conf_iface, proto, port, debug=debug, policy=policy) and \
- is_route_exists(route, next_hop, conf_iface, conf_metric):
- if debug:
- print(f'Nexh_hop {next_hop} fail, target not response')
- print(f' [ DEL ] -- ip route del {route} via {next_hop} dev {conf_iface} '
- f'metric {conf_metric} proto failover [DELETE]')
- rc_cmd(f'ip route del {route} via {next_hop} dev {conf_iface} metric {conf_metric} proto failover')
- journal.send(f'ip route del {route} via {next_hop} dev {conf_iface} '
- f'metric {conf_metric} proto failover', SYSLOG_IDENTIFIER=my_name)
-
- time.sleep(int(timeout))
+ print_debug(
+ f' [ TARGET_FAIL ] target checks fails for [{nhc.pretty_targets}], do nothing'
+ )
+ journal.send(
+ f'Check fail for route {route} interface "{nhc.conf_iface}" target {nhc.pretty_targets} proto {nhc.proto} '
+ f'{nhc.port_opt}',
+ SYSLOG_IDENTIFIER=my_name,
+ )
+ else:
+ # Route was added, check if the target is alive
+ # We should delete route if check fails only if route exists in the routing table
+ if not is_alive:
+ print_debug(
+ f"Next_hop {next_hop} fail, target check didn't pass"
+ )
+ delete_route(ip_args)
+
+ had_sleeps = True
+ time.sleep(int(nhc.timeout))
+ if kill_called:
+ break
+
+ print_debug(f"Out of main loop, {kill_called=}")
diff --git a/src/helpers/vyos-interface-rescan.py b/src/helpers/vyos-interface-rescan.py
index 012357259..fea9bca1c 100755
--- a/src/helpers/vyos-interface-rescan.py
+++ b/src/helpers/vyos-interface-rescan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/helpers/vyos-load-balancer.py b/src/helpers/vyos-load-balancer.py
index 30329fd5c..127ce05e5 100755
--- a/src/helpers/vyos-load-balancer.py
+++ b/src/helpers/vyos-load-balancer.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-# Copyright 2024-2025 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -24,6 +24,7 @@ import time
from vyos.config import Config
from vyos.template import render
from vyos.utils.commit import commit_in_progress
+from vyos.utils.dict import dict_search_args
from vyos.utils.network import get_interface_address
from vyos.utils.process import rc_cmd
from vyos.utils.process import run
@@ -102,15 +103,36 @@ def get_ipv4_address(ifname):
return addr_json['addr_info'][0]['local']
return None
+def get_dynamic_nexthop(ifname: str) -> str | None | bool:
+ '''
+ Resolve the dynamic next-hop for a WAN interface.
+
+ Determines the current default gateway learned dynamically on the interface:
+ - PPPoE interfaces (`pppoe*`): uses `parse_ppp_nexthop`.
+ - Other interfaces (e.g. DHCP): uses `parse_dhcp_nexthop`.
+
+ Return values:
+ - str: IPv4 next-hop address
+ - None: when DHCP lease has no router value
+ - False: when PPPoE nexthop state file is missing
+
+ Args:
+ ifname: Interface name (e.g. 'pppoe0', 'eth0').
+
+ Returns:
+ See above for possible values.
+ '''
+ if ifname.startswith('pppoe'):
+ return parse_ppp_nexthop(ifname)
+ else:
+ return parse_dhcp_nexthop(ifname)
+
def dynamic_nexthop_update(lb, ifname):
# Update on DHCP/PPP address/nexthop changes
# Return True if nftables needs to be updated - IP change
if 'dhcp_nexthop' in lb['health_state'][ifname]:
- if ifname[:5] == 'pppoe':
- dhcp_nexthop_addr = parse_ppp_nexthop(ifname)
- else:
- dhcp_nexthop_addr = parse_dhcp_nexthop(ifname)
+ dhcp_nexthop_addr = get_dynamic_nexthop(ifname)
table_num = lb['health_state'][ifname]['table_number']
@@ -125,6 +147,40 @@ def dynamic_nexthop_update(lb, ifname):
return False
+def restore_default_route(lb: dict, ifname: str) -> None:
+ """
+ Restores a missing default route for a WAN interface in its policy routing table.
+
+ When a link flap or DHCP/PPP renegotiation removes the per-interface default route,
+ this function checks the interface’s assigned table for an existing default entry.
+ If none is found, it determines the proper next-hop (from DHCP, PPP, or static config)
+ and reinstalls the route using:
+ ip route replace table <table_num> default dev <ifname> via <nexthop>
+
+ @param lb Load-balancer state/config dictionary.
+ @param ifname Interface name whose default route should be verified and restored.
+ @returns None — exits quietly if the table number or next-hop cannot be found.
+ """
+ table_num = dict_search_args(lb, 'health_state', ifname, 'table_number')
+ if not table_num:
+ return
+
+ rc, out = rc_cmd(f'ip -j route show default table {table_num}')
+ if rc == 0:
+ rt_table = json.loads(out)
+ if len(rt_table) > 0:
+ return
+ else:
+ if 'dhcp_nexthop' in lb['health_state'][ifname]:
+ nexthop_addr = get_dynamic_nexthop(ifname)
+ else:
+ nexthop_addr = dict_search_args(lb, 'interface_health', ifname, 'nexthop')
+
+ if nexthop_addr:
+ run(f'ip route replace table {table_num} default dev {ifname} via {nexthop_addr}')
+ else:
+ return
+
def nftables_update(lb):
# Atomically reload nftables table from template
if not os.path.exists(nftables_wlb_conf):
@@ -148,7 +204,17 @@ def cleanup(lb):
index = 1
for ifname, health_conf in lb['interface_health'].items():
table_num = lb['mark_offset'] + index
+ suppress_prio = lb['mark_offset'] + index
+ table_prio = suppress_prio + 100
run(f'ip route del table {table_num} default')
+ run(
+ f'ip rule del fwmark {hex(table_num)} table main '
+ f'suppress_prefixlength 0 priority {suppress_prio}'
+ )
+ run(
+ f'ip rule del fwmark {hex(table_num)} table {table_num} '
+ f'priority {table_prio}'
+ )
run(f'ip rule del fwmark {hex(table_num)} table {table_num}')
index += 1
@@ -160,6 +226,14 @@ def get_config():
lb = conf.get_config_dict(base, key_mangling=('-', '_'),
get_first_key=True, with_recursive_defaults=True)
+ lb['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ # prune limit key if not set by user
+ for rule in lb.get('rule', []):
+ if lb.from_defaults(['rule', rule, 'limit']):
+ del lb['rule'][rule]['limit']
+
lb['test_defaults'] = get_defaults(base + ['interface-health', 'A', 'test', 'B'], get_first_key=True)
return lb
@@ -199,7 +273,19 @@ if __name__ == '__main__':
else:
run(f'ip route replace table {table_num} default dev {ifname} via {health_conf["nexthop"]}')
- run(f'ip rule add fwmark {hex(table_num)} table {table_num}')
+ suppress_prio = lb['mark_offset'] + index
+ table_prio = suppress_prio + 100
+ if 'only_default_route' in lb:
+ run(
+ f'ip rule add fwmark {hex(table_num)} table main '
+ f'suppress_prefixlength 0 priority {suppress_prio}'
+ )
+ run(
+ f'ip rule add fwmark {hex(table_num)} table {table_num} '
+ f'priority {table_prio}'
+ )
+ else:
+ run(f'ip rule add fwmark {hex(table_num)} table {table_num}')
index += 1
@@ -208,8 +294,8 @@ if __name__ == '__main__':
run('ip route flush cache')
if 'flush_connections' in lb:
- run('conntrack --delete')
- run('conntrack -F expect')
+ for _state in lb['health_state'].values():
+ run(f'conntrack --delete --mark {_state["table_number"]}')
with open(wlb_status_file, 'w') as f:
f.write(json.dumps(lb['health_state']))
@@ -246,6 +332,7 @@ if __name__ == '__main__':
# Main loop
+ init = True;
try:
while True:
ip_change = False
@@ -274,6 +361,11 @@ if __name__ == '__main__':
state['state'] = False
state['state_changed'] = True
+ #Force state changed to trigger the first write
+ if init == True:
+ state['state_changed'] = True
+ init = False
+
if state['state_changed']:
state['if_addr'] = get_ipv4_address(ifname)
on_state_change(lb, ifname, state['state'])
@@ -281,6 +373,8 @@ if __name__ == '__main__':
if dynamic_nexthop_update(lb, ifname):
ip_change = True
+ restore_default_route(lb, ifname)
+
if any(state['state_changed'] for ifname, state in lb['health_state'].items()):
if not nftables_update(lb):
break
@@ -288,8 +382,8 @@ if __name__ == '__main__':
run('ip route flush cache')
if 'flush_connections' in lb:
- run('conntrack --delete')
- run('conntrack -F expect')
+ for _state in lb['health_state'].values():
+ run(f'conntrack --delete --mark {_state["table_number"]}')
with open(wlb_status_file, 'w') as f:
f.write(json.dumps(lb['health_state']))
diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py
index 16083fd41..7c49e9c3a 100755
--- a/src/helpers/vyos-load-config.py
+++ b/src/helpers/vyos-load-config.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -16,84 +16,60 @@
#
#
-"""Load config file from within config session.
-Config file specified by URI or path (without scheme prefix).
-Example: load https://somewhere.net/some.config
- or
- load /tmp/some.config
-"""
-
import os
import sys
-import gzip
+import argparse
import tempfile
-import vyos.defaults
-import vyos.remote
-from vyos.configsource import ConfigSourceSession, VyOSError
+
+from vyos.remote import get_config_file
+from vyos.config import Config
from vyos.migrate import ConfigMigrate
from vyos.migrate import ConfigMigrateError
+from vyos.load_config import load as load_config
+from vyos.defaults import directories
-class LoadConfig(ConfigSourceSession):
- """A subclass for calling 'loadFile'.
- This does not belong in configsource.py, and only has a single caller.
- """
- def load_config(self, path):
- return self._run(['/bin/cli-shell-api','loadFile',path])
-file_name = sys.argv[1] if len(sys.argv) > 1 else 'config.boot'
-configdir = vyos.defaults.directories['config']
-protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp']
+default_config_file = os.path.join(directories['config'], 'config.boot')
-def get_local_config(filename):
- if os.path.isfile(filename):
- fname = filename
- elif os.path.isfile(os.path.join(configdir, filename)):
- fname = os.path.join(configdir, filename)
- else:
- sys.exit(f"No such file '{filename}'")
+parser = argparse.ArgumentParser()
+parser.add_argument('config_file', nargs='?', help='config file to load')
+parser.add_argument(
+ '--migrate', action='store_true', help='migrate config file before merge'
+)
- if fname.endswith('.gz'):
- with gzip.open(fname, 'rb') as f:
- try:
- config_str = f.read().decode()
- except OSError as e:
- sys.exit(e)
- else:
- with open(fname, 'r') as f:
- try:
- config_str = f.read()
- except OSError as e:
- sys.exit(e)
+args = parser.parse_args()
- return config_str
-
-if any(file_name.startswith(f'{x}://') for x in protocols):
- config_string = vyos.remote.get_remote_config(file_name)
- if not config_string:
- sys.exit(f"No such config file at '{file_name}'")
-else:
- config_string = get_local_config(file_name)
+file_name = args.config_file if args.config_file else default_config_file
-config = LoadConfig()
+# pylint: disable=consider-using-with
+file_path = tempfile.NamedTemporaryFile(delete=False).name
+err = get_config_file(file_name, file_path)
+if err:
+ os.remove(file_path)
+ sys.exit(err)
-print(f"Loading configuration from '{file_name}'")
+if args.migrate:
+ migrate = ConfigMigrate(file_path)
+ try:
+ migrate.run()
+ except ConfigMigrateError as e:
+ os.remove(file_path)
+ sys.exit(e)
-with tempfile.NamedTemporaryFile() as fp:
- with open(fp.name, 'w') as fd:
- fd.write(config_string)
+config = Config()
- config_migrate = ConfigMigrate(fp.name)
- try:
- config_migrate.run()
- except ConfigMigrateError as err:
- sys.exit(err)
+if config.vyconf_session is not None:
+ out, err = config.vyconf_session.load_config(file_path)
+ if err:
+ os.remove(file_path)
+ sys.exit(out)
+ print(out)
+else:
+ load_config(file_path)
- try:
- config.load_config(fp.name)
- except VyOSError as err:
- sys.exit(err)
+os.remove(file_path)
if config.session_changed():
print("Load complete. Use 'commit' to make changes effective.")
else:
- print("No configuration changes to commit.")
+ print('No configuration changes to commit.')
diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py
index 5ef845ac2..e8a696eb5 100755
--- a/src/helpers/vyos-merge-config.py
+++ b/src/helpers/vyos-merge-config.py
@@ -1,108 +1,101 @@
#!/usr/bin/python3
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
+# 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
+# published by the Free Software Foundation.
#
-# This library is distributed in the hope that it will be useful,
+# This program 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.
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
#
-# 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 os
import sys
+import shlex
+import argparse
import tempfile
-import vyos.defaults
-import vyos.remote
+from vyos.remote import get_config_file
from vyos.config import Config
from vyos.configtree import ConfigTree
+from vyos.configtree import mask_inclusive
+from vyos.configtree import merge
from vyos.migrate import ConfigMigrate
from vyos.migrate import ConfigMigrateError
-from vyos.utils.process import cmd
-from vyos.utils.process import DEVNULL
+from vyos.load_config import load_explicit
-if (len(sys.argv) < 2):
- print("Need config file name to merge.")
- print("Usage: merge <config file> [config path]")
- sys.exit(0)
-file_name = sys.argv[1]
+parser = argparse.ArgumentParser()
+parser.add_argument('config_file', help='config file to merge from')
+parser.add_argument(
+ '--destructive', action='store_true', help='replace values with those of merge file'
+)
+parser.add_argument('--paths', nargs='+', help='only merge from listed paths')
+parser.add_argument(
+ '--migrate', action='store_true', help='migrate config file before merge'
+)
-configdir = vyos.defaults.directories['config']
+args = parser.parse_args()
-protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp']
+file_name = args.config_file
+paths = [shlex.split(s) for s in args.paths] if args.paths else []
-if any(x in file_name for x in protocols):
- config_file = vyos.remote.get_remote_config(file_name)
- if not config_file:
- sys.exit("No config file by that name.")
-else:
- canonical_path = "{0}/{1}".format(configdir, file_name)
- first_err = None
- try:
- with open(canonical_path, 'r') as f:
- config_file = f.read()
- except Exception as err:
- first_err = err
- try:
- with open(file_name, 'r') as f:
- config_file = f.read()
- except Exception as err:
- print(first_err)
- print(err)
- sys.exit(1)
-
-with tempfile.NamedTemporaryFile() as file_to_migrate:
- with open(file_to_migrate.name, 'w') as fd:
- fd.write(config_file)
-
- config_migrate = ConfigMigrate(file_to_migrate.name)
+# pylint: disable=consider-using-with
+file_path = tempfile.NamedTemporaryFile(delete=False).name
+err = get_config_file(file_name, file_path)
+if err:
+ os.remove(file_path)
+ sys.exit(err)
+
+if args.migrate:
+ migrate = ConfigMigrate(file_path)
try:
- config_migrate.run()
+ migrate.run()
except ConfigMigrateError as e:
+ os.remove(file_path)
sys.exit(e)
-merge_config_tree = ConfigTree(config_file)
+with open(file_path) as f:
+ merge_str = f.read()
+
+merge_ct = ConfigTree(merge_str)
-effective_config = Config()
-effective_config_tree = effective_config._running_config
+if paths:
+ mask = ConfigTree('')
+ for p in paths:
+ mask.set(p)
-effective_cmds = effective_config_tree.to_commands()
-merge_cmds = merge_config_tree.to_commands()
+ merge_ct = mask_inclusive(merge_ct, mask)
-effective_cmd_list = effective_cmds.splitlines()
-merge_cmd_list = merge_cmds.splitlines()
+with open(file_path, 'w') as f:
+ f.write(merge_ct.to_string())
-effective_cmd_set = set(effective_cmd_list)
-add_cmds = [ cmd for cmd in merge_cmd_list if cmd not in effective_cmd_set ]
+config = Config()
-path = None
-if (len(sys.argv) > 2):
- path = sys.argv[2:]
- if (not effective_config_tree.exists(path) and not
- merge_config_tree.exists(path)):
- print("path {} does not exist in either effective or merge"
- " config; will use root.".format(path))
- path = None
- else:
- path = " ".join(path)
+if config.vyconf_session is not None:
+ out, err = config.vyconf_session.merge_config(
+ file_path, destructive=args.destructive
+ )
+ if err:
+ os.remove(file_path)
+ sys.exit(out)
+ print(out)
+else:
+ session_ct = config.get_config_tree()
+ merge_res = merge(session_ct, merge_ct, destructive=args.destructive)
-if path:
- add_cmds = [ cmd for cmd in add_cmds if path in cmd ]
+ load_explicit(merge_res)
-for add in add_cmds:
- try:
- cmd(f'/opt/vyatta/sbin/my_{add}', shell=True, stderr=DEVNULL)
- except OSError as err:
- print(err)
+os.remove(file_path)
-if effective_config.session_changed():
+if config.session_changed():
print("Merge complete. Use 'commit' to make changes effective.")
else:
- print("No configuration changes to commit.")
+ print('No configuration changes to commit.')
diff --git a/src/helpers/vyos-request-configd-update.py b/src/helpers/vyos-request-configd-update.py
new file mode 100755
index 000000000..55ed0eef9
--- /dev/null
+++ b/src/helpers/vyos-request-configd-update.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+
+import json
+import zmq
+
+from vyos.utils.commit import wait_for_commit_lock
+from vyos.defaults import vyos_configd_socket_path
+
+context = zmq.Context()
+
+request = {
+ 'type': 'node',
+ 'last': True,
+ 'data': '/usr/libexec/vyos/conf_mode/protocols_static.py',
+}
+request = json.dumps(request)
+
+print("Waiting for commit lock...")
+wait_for_commit_lock()
+
+print("Connecting to vyos-configd server...")
+socket = context.socket(zmq.REQ)
+socket.connect(vyos_configd_socket_path)
+
+print(f"Sending request {request}...")
+socket.send_string(request)
+
+message = socket.recv()
+print(f"Received reply {request} [ {message} ]")
+
+print("All done")
diff --git a/src/helpers/vyos-save-config.py b/src/helpers/vyos-save-config.py
index fa2ea0ce4..208fb8ae8 100755
--- a/src/helpers/vyos-save-config.py
+++ b/src/helpers/vyos-save-config.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -23,15 +23,22 @@ from argparse import ArgumentParser
from vyos.config import Config
from vyos.remote import urlc
-from vyos.component_version import add_system_version
+from vyos.component_version import add_system_version_string
from vyos.defaults import directories
+from vyos.utils.file import write_file
+from vyos.utils.file import write_file_sync
+from vyos.utils.file import write_file_atomic
+from vyos.utils.file import file_is_persistent
DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot')
remote_save = None
parser = ArgumentParser(description='Save configuration')
parser.add_argument('file', type=str, nargs='?', help='Save configuration to file')
-parser.add_argument('--write-json-file', type=str, help='Save JSON of configuration to file')
+parser.add_argument(
+ '--write-json-file', type=str, help='Save JSON of configuration to file'
+)
+
args = parser.parse_args()
file = args.file
json_file = args.write_json_file
@@ -47,16 +54,43 @@ if re.match(r'\w+:/', save_file):
except ValueError as e:
sys.exit(e)
+
config = Config()
ct = config.get_config_tree(effective=True)
+# The effective config is None before boot configuration is complete.
+# Nothing to write, nor do we want to invite saving an empty string:
+# exit gracefully.
+if ct is None:
+ sys.exit()
+
+config_str = ct.to_string()
+versioned_config_str = add_system_version_string(config_str)
+
# pylint: disable=consider-using-with
-write_file = save_file if remote_save is None else NamedTemporaryFile(delete=False).name
+file_to_write = (
+ save_file if remote_save is None else NamedTemporaryFile(delete=False).name
+)
-# config_tree is None before boot configuration is complete;
-# automated saves should check boot_configuration_complete
-config_str = None if ct is None else ct.to_string()
-add_system_version(config_str, write_file)
+if file_is_persistent(file_to_write):
+ if os.geteuid() == 0:
+ try:
+ write_file_atomic(file_to_write, versioned_config_str)
+ except OSError as e:
+ print(f'failed to write config file, write_file_atomic: {e}')
+ sys.exit(1)
+ else:
+ try:
+ write_file_sync(file_to_write, versioned_config_str)
+ except OSError as e:
+ print(f'failed to write config file, write_file_sync: {e}')
+ sys.exit(1)
+else:
+ try:
+ write_file(file_to_write, versioned_config_str)
+ except Exception as e: # pylint: disable=broad-exception-caught
+ print(f'failed to write config file, write_file: {e}')
+ sys.exit(1)
if json_file is not None and ct is not None:
try:
@@ -67,6 +101,6 @@ if json_file is not None and ct is not None:
if remote_save is not None:
try:
- remote_save.upload(write_file)
+ remote_save.upload(file_to_write)
finally:
- os.remove(write_file)
+ os.remove(file_to_write)
diff --git a/src/helpers/vyos-sudo.py b/src/helpers/vyos-sudo.py
deleted file mode 100755
index 75dd7f29d..000000000
--- a/src/helpers/vyos-sudo.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright 2019 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
-# 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 os
-import sys
-
-from vyos.utils.permission import is_admin
-
-
-if __name__ == '__main__':
- if len(sys.argv) < 2:
- print('Missing command argument')
- sys.exit(1)
-
- if not is_admin():
- print('This account is not authorized to run this command')
- sys.exit(1)
-
- os.execvp('sudo', ['sudo'] + sys.argv[1:])
diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py
index 9d9aec376..9e425d7c4 100755
--- a/src/helpers/vyos_config_sync.py
+++ b/src/helpers/vyos_config_sync.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -17,6 +17,7 @@
#
import os
+import sys
import json
import requests
import urllib3
@@ -26,6 +27,9 @@ from typing import Optional, List, Tuple, Dict, Any
from vyos.config import Config
from vyos.configtree import ConfigTree
from vyos.configtree import mask_inclusive
+from vyos.configtree import mask_exclusive
+from vyos.defaults import config_sync_exclusion_list
+from vyos.derivedtree import subtree_from_list_of_partial_paths
from vyos.template import bracketize_ipv6
@@ -40,9 +44,12 @@ logger.name = os.path.basename(__file__)
API_HEADERS = {'Content-Type': 'application/json'}
-def post_request(url: str,
- data: str,
- headers: Dict[str, str]) -> requests.Response:
+def post_request(
+ url: str,
+ data: str,
+ params: Dict[str, Any],
+ headers: Dict[str, str],
+) -> requests.Response:
"""Sends a POST request to the specified URL
Args:
@@ -54,16 +61,20 @@ def post_request(url: str,
requests.Response: The response object representing the server's response to the request
"""
- response = requests.post(url,
- data=data,
- headers=headers,
- verify=False,
- timeout=timeout)
+ response = requests.post(
+ url,
+ data=data,
+ params=params,
+ headers=headers,
+ verify=False,
+ timeout=timeout,
+ )
return response
-
-def retrieve_config(sections: List[list[str]]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+def retrieve_config(
+ sections: List[list[str]], exclusions: List[list[str]]
+) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""Retrieves the configuration from the local server.
Args:
@@ -76,18 +87,32 @@ def retrieve_config(sections: List[list[str]]) -> Tuple[Dict[str, Any], Dict[str
- config: The subtree of masked config data, as a dictionary.
"""
- mask = ConfigTree('')
- for section in sections:
- mask.set(section)
- mask_dict = json.loads(mask.to_json())
-
config = Config()
config_tree = config.get_config_tree()
- masked = mask_inclusive(config_tree, mask)
+
+ # set inclusion mask
+ mask_in = ConfigTree('')
+ for section in sections:
+ mask_in.set(section)
+ mask_in_str = mask_in.write_internal_string()
+
+ ## set exclusion mask
+ # pass global settings, read at startup:
+ exclude_list = exclusions
+ # read local settings from Config
+ # ... exclude_list += ...
+ mask_ex = subtree_from_list_of_partial_paths(config_tree, exclude_list)
+ mask_ex_str = json.dumps(exclude_list)
+
+ masked = mask_inclusive(config_tree, mask_in)
+ masked = mask_exclusive(masked, mask_ex)
+
+ mask_dict = {'inclusive': mask_in_str, 'exclusive': mask_ex_str}
config_dict = json.loads(masked.to_json())
return mask_dict, config_dict
+
def set_remote_config(
address: str,
key: str,
@@ -116,6 +141,10 @@ def set_remote_config(
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
url = f'https://{address}:{port}/configure-section'
+ params = {
+ # Ask the remote API to perform the configure and commit workflow asynchronously
+ 'in_background': True,
+ }
data = json.dumps({
'op': op,
'mask': mask,
@@ -124,7 +153,7 @@ def set_remote_config(
})
try:
- config = post_request(url, data, headers)
+ config = post_request(url, data, params, headers)
return config.json()
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
@@ -137,14 +166,19 @@ def is_section_revised(section: List[str]) -> bool:
return is_node_revised(section)
-def config_sync(secondary_address: str,
- secondary_key: str,
- sections: List[list[str]],
- mode: str,
- secondary_port: int):
+def config_sync(
+ secondary_address: str,
+ secondary_key: str,
+ sections: List[list[str]],
+ mode: str,
+ secondary_port: int,
+ exclusions: List[list[str]],
+):
"""Retrieve a config section from primary router in JSON format and send it to
secondary router
"""
+ # pylint: disable=too-many-arguments
+
if not any(map(is_section_revised, sections)):
return
@@ -153,7 +187,7 @@ def config_sync(secondary_address: str,
)
# Sync sections ("nat", "firewall", etc)
- mask_dict, config_dict = retrieve_config(sections)
+ mask_dict, config_dict = retrieve_config(sections, exclusions)
logger.debug(
f"Retrieved config for sections '{sections}': {config_dict}")
@@ -171,7 +205,19 @@ if __name__ == '__main__':
# Read configuration from file
if not os.path.exists(CONFIG_FILE):
logger.error(f"Post-commit: No config file '{CONFIG_FILE}' exists")
- exit(0)
+ sys.exit()
+
+ try:
+ with open(config_sync_exclusion_list) as f:
+ exclude_list = json.load(f)
+ except FileNotFoundError:
+ logger.error(f"Exclusion list '{config_sync_exclusion_list}' not found")
+ sys.exit()
+ except (json.JSONDecodeError, OSError) as e:
+ logger.error(
+ f"Failed to load config-sync exclusion list '{config_sync_exclusion_list}': {e}"
+ )
+ sys.exit()
with open(CONFIG_FILE, 'r') as f:
config_data = f.read()
@@ -188,7 +234,7 @@ if __name__ == '__main__':
if not all([mode, secondary_address, secondary_key, sections]):
logger.error("Missing required configuration data for config synchronization.")
- exit(0)
+ sys.exit()
# Generate list_sections of sections/subsections
# [
@@ -202,4 +248,11 @@ if __name__ == '__main__':
else:
list_sections.append([section])
- config_sync(secondary_address, secondary_key, list_sections, mode, secondary_port)
+ config_sync(
+ secondary_address,
+ secondary_key,
+ list_sections,
+ mode,
+ secondary_port,
+ exclude_list,
+ )
diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name
deleted file mode 100755
index f5de182c6..000000000
--- a/src/helpers/vyos_net_name
+++ /dev/null
@@ -1,276 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2021-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
-# published by the Free Software Foundation.
-#
-# This program 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-import time
-import logging
-import logging.handlers
-import tempfile
-from pathlib import Path
-from sys import argv
-
-from vyos.configtree import ConfigTree
-from vyos.defaults import directories
-from vyos.utils.process import cmd
-from vyos.utils.boot import boot_configuration_complete
-from vyos.utils.locking import Lock
-from vyos.migrate import ConfigMigrate
-
-# Define variables
-vyos_udev_dir = directories['vyos_udev_dir']
-config_path = '/opt/vyatta/etc/config/config.boot'
-
-
-def is_available(intfs: dict, intf_name: str) -> bool:
- """Check if interface name is already assigned"""
- if intf_name in list(intfs.values()):
- return False
- return True
-
-
-def find_available(intfs: dict, prefix: str) -> str:
- """Find lowest indexed iterface name that is not assigned"""
- index_list = [
- int(x.replace(prefix, '')) for x in list(intfs.values()) if prefix in x
- ]
- index_list.sort()
- # find 'holes' in list, if any
- missing = sorted(set(range(index_list[0], index_list[-1])) - set(index_list))
- if missing:
- return f'{prefix}{missing[0]}'
-
- return f'{prefix}{len(index_list)}'
-
-
-def mod_ifname(ifname: str) -> str:
- """Check interface with names eX and return ifname on the next format eth{ifindex} - 2"""
- if re.match('^e[0-9]+$', ifname):
- intf = ifname.split('e')
- if intf[1]:
- if int(intf[1]) >= 2:
- return 'eth' + str(int(intf[1]) - 2)
- else:
- return 'eth' + str(intf[1])
-
- return ifname
-
-
-def get_biosdevname(ifname: str) -> str:
- """Use legacy vyatta-biosdevname to query for name
-
- This is carried over for compatability only, and will likely be dropped
- going forward.
- XXX: This throws an error, and likely has for a long time, unnoticed
- since vyatta_net_name redirected stderr to /dev/null.
- """
- intf = mod_ifname(ifname)
-
- if 'eth' not in intf:
- return intf
- if os.path.isdir('/proc/xen'):
- return intf
-
- time.sleep(1)
-
- try:
- biosname = cmd(f'/sbin/biosdevname --policy all_ethN -i {ifname}')
- except Exception as e:
- logger.error(f'biosdevname error: {e}')
- biosname = ''
-
- return intf if biosname == '' else biosname
-
-
-def leave_rescan_hint(intf_name: str, hwid: str):
- """Write interface information reported by udev
-
- This script is called while the root mount is still read-only. Leave
- information in /run/udev: file name, the interface; contents, the
- hardware id.
- """
- try:
- os.mkdir(vyos_udev_dir)
- except FileExistsError:
- pass
- except Exception as e:
- logger.critical(f'Error creating rescan hint directory: {e}')
- exit(1)
-
- try:
- with open(os.path.join(vyos_udev_dir, intf_name), 'w') as f:
- f.write(hwid)
- except OSError as e:
- logger.critical(f'OSError {e}')
-
-
-def get_configfile_interfaces() -> dict:
- """Read existing interfaces from config file"""
- interfaces: dict = {}
-
- if not os.path.isfile(config_path):
- # If the case, then we are running off of livecd; return empty
- return interfaces
-
- try:
- with open(config_path) as f:
- config_file = f.read()
- except OSError as e:
- logger.critical(f'OSError {e}')
- exit(1)
-
- try:
- config = ConfigTree(config_file)
- except Exception:
- try:
- logger.debug('updating component version string syntax')
- # this will update the component version string syntax,
- # required for updates 1.2 --> 1.3/1.4
- with tempfile.NamedTemporaryFile() as fp:
- with open(fp.name, 'w') as fd:
- fd.write(config_file)
- config_migrate = ConfigMigrate(fp.name)
- if config_migrate.syntax_update_needed():
- config_migrate.update_syntax()
- config_migrate.write_config()
- with open(fp.name) as fd:
- config_file = fd.read()
-
- config = ConfigTree(config_file)
-
- except Exception as e:
- logger.critical(f'ConfigTree error: {e}')
- exit(1)
-
- base = ['interfaces', 'ethernet']
- if config.exists(base):
- eth_intfs = config.list_nodes(base)
- for intf in eth_intfs:
- path = base + [intf, 'hw-id']
- if not config.exists(path):
- logger.warning(f"no 'hw-id' entry for {intf}")
- continue
- hwid = config.return_value(path)
- if hwid in list(interfaces):
- logger.warning(
- f'multiple entries for {hwid}: {interfaces[hwid]}, {intf}'
- )
- continue
- interfaces[hwid] = intf
-
- base = ['interfaces', 'wireless']
- if config.exists(base):
- wlan_intfs = config.list_nodes(base)
- for intf in wlan_intfs:
- path = base + [intf, 'hw-id']
- if not config.exists(path):
- logger.warning(f"no 'hw-id' entry for {intf}")
- continue
- hwid = config.return_value(path)
- if hwid in list(interfaces):
- logger.warning(
- f'multiple entries for {hwid}: {interfaces[hwid]}, {intf}'
- )
- continue
- interfaces[hwid] = intf
-
- logger.debug(f'config file entries: {interfaces}')
-
- return interfaces
-
-
-def add_assigned_interfaces(intfs: dict):
- """Add interfaces found by previous invocation of udev rule"""
- if not os.path.isdir(vyos_udev_dir):
- return
-
- for intf in os.listdir(vyos_udev_dir):
- path = os.path.join(vyos_udev_dir, intf)
- try:
- with open(path) as f:
- hwid = f.read().rstrip()
- except OSError as e:
- logger.error(f'OSError {e}')
- continue
- intfs[hwid] = intf
-
-
-def on_boot_event(intf_name: str, hwid: str, predefined: str = '') -> str:
- """Called on boot by vyos-router: 'coldplug' in vyatta_net_name"""
- logger.info(f'lookup {intf_name}, {hwid}')
- interfaces = get_configfile_interfaces()
- logger.debug(f'config file interfaces are {interfaces}')
-
- if hwid in list(interfaces):
- logger.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'")
- return interfaces[hwid]
-
- add_assigned_interfaces(interfaces)
- logger.debug(f'adding assigned interfaces: {interfaces}')
-
- if predefined:
- newname = predefined
- logger.info(f"predefined interface name for '{intf_name}' is '{newname}'")
- else:
- newname = get_biosdevname(intf_name)
- logger.info(f"biosdevname returned '{newname}' for '{intf_name}'")
-
- if not is_available(interfaces, newname):
- prefix = re.sub(r'\d+$', '', newname)
- newname = find_available(interfaces, prefix)
-
- logger.info(f"new name for '{intf_name}' is '{newname}'")
-
- leave_rescan_hint(newname, hwid)
-
- return newname
-
-
-def hotplug_event():
- # Not yet implemented, since interface-rescan will only be run on boot.
- pass
-
-
-if __name__ == '__main__':
- # Set up logging to syslog
- syslog_handler = logging.handlers.SysLogHandler(address='/dev/log')
- formatter = logging.Formatter(f'{Path(__file__).name}: %(message)s')
- syslog_handler.setFormatter(formatter)
-
- logger = logging.getLogger()
- logger.addHandler(syslog_handler)
- logger.setLevel(logging.DEBUG)
-
- logger.debug(f'Started with arguments: {argv}')
-
- if len(argv) > 3:
- predef_name = argv[3]
- else:
- predef_name = ''
-
- lock = Lock('vyos_net_name')
- # Wait 60 seconds for other running scripts to finish
- lock.acquire(60)
-
- if not boot_configuration_complete():
- res = on_boot_event(argv[1], argv[2], predefined=predef_name)
- logger.debug(f'on boot, returned name is {res}')
- print(res)
- else:
- logger.debug('boot configuration complete')
-
- lock.release()
- logger.debug('Finished')
diff --git a/src/helpers/write-config-file-value.py b/src/helpers/write-config-file-value.py
new file mode 100644
index 000000000..f3993223c
--- /dev/null
+++ b/src/helpers/write-config-file-value.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from argparse import ArgumentParser
+from shlex import split as shlex_split
+
+from vyos.utils.config import write_saved_value
+
+def _split_quoted(s: str) -> list[str]:
+ parts = shlex_split(s)
+ if not parts:
+ raise ValueError('empty string')
+ return parts
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+ parser.add_argument(
+ '--path',
+ required=True,
+ help='Quoted CLI path, e.g. "system console device ttyS1 speed"',
+ )
+ parser.add_argument(
+ '--value',
+ required=False,
+ help='Value for the node, e.g. "9600". If omitted, creates a valueless node.',
+ )
+ parser.add_argument(
+ '--config-file',
+ required=True,
+ help=f'Path to saved config.boot',
+ )
+ args = parser.parse_args()
+
+ path = _split_quoted(args.path)
+ write_saved_value(path, value=args.value, config_path=args.config_file)