summaryrefslogtreecommitdiff
path: root/src/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'src/helpers')
-rwxr-xr-xsrc/helpers/add-system-version.py (renamed from src/helpers/system-versions-foot.py)14
-rwxr-xr-xsrc/helpers/commit-confirm-notify.py48
-rwxr-xr-xsrc/helpers/config_dependency.py79
-rwxr-xr-xsrc/helpers/latest-image-url.py21
-rwxr-xr-xsrc/helpers/run-config-activation.py83
-rwxr-xr-xsrc/helpers/run-config-migration.py128
-rwxr-xr-xsrc/helpers/vyos-config-encrypt.py273
-rwxr-xr-xsrc/helpers/vyos-domain-resolver.py176
-rwxr-xr-xsrc/helpers/vyos-load-balancer.py312
-rwxr-xr-xsrc/helpers/vyos-load-config.py21
-rwxr-xr-xsrc/helpers/vyos-merge-config.py17
-rwxr-xr-xsrc/helpers/vyos-save-config.py15
-rwxr-xr-xsrc/helpers/vyos_net_name153
13 files changed, 965 insertions, 375 deletions
diff --git a/src/helpers/system-versions-foot.py b/src/helpers/add-system-version.py
index 9614f0d28..5270ee7d3 100755
--- a/src/helpers/system-versions-foot.py
+++ b/src/helpers/add-system-version.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-# Copyright 2019, 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -15,14 +15,6 @@
# 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 sys
-import vyos.defaults
-from vyos.component_version import write_system_footer
+from vyos.component_version import add_system_version
-sys.stdout.write("\n\n")
-if vyos.defaults.cfg_vintage == 'vyos':
- write_system_footer(None, vintage='vyos')
-elif vyos.defaults.cfg_vintage == 'vyatta':
- write_system_footer(None, vintage='vyatta')
-else:
- write_system_footer(None, vintage='vyos')
+add_system_version()
diff --git a/src/helpers/commit-confirm-notify.py b/src/helpers/commit-confirm-notify.py
index 8d7626c78..af6167651 100755
--- a/src/helpers/commit-confirm-notify.py
+++ b/src/helpers/commit-confirm-notify.py
@@ -2,30 +2,56 @@
import os
import sys
import time
+from argparse import ArgumentParser
# Minutes before reboot to trigger notification.
intervals = [1, 5, 15, 60]
-def notify(interval):
- s = "" if interval == 1 else "s"
+parser = ArgumentParser()
+parser.add_argument(
+ 'minutes', type=int, help='minutes before rollback to trigger notification'
+)
+parser.add_argument(
+ '--reboot', action='store_true', help="use 'soft' rollback instead of reboot"
+)
+
+
+def notify(interval, reboot=False):
+ s = '' if interval == 1 else 's'
time.sleep((minutes - interval) * 60)
- message = ('"[commit-confirm] System is going to reboot in '
- f'{interval} minute{s} to rollback the last commit.\n'
- 'Confirm your changes to cancel the reboot."')
- os.system("wall -n " + message)
+ if reboot:
+ message = (
+ '"[commit-confirm] System will reboot in '
+ f'{interval} minute{s}\nto rollback the last commit.\n'
+ 'Confirm your changes to cancel the reboot."'
+ )
+ os.system('wall -n ' + message)
+ else:
+ message = (
+ '"[commit-confirm] System will reload previous config in '
+ f'{interval} minute{s}\nto rollback the last commit.\n'
+ 'Confirm your changes to cancel the reload."'
+ )
+ os.system('wall -n ' + message)
+
-if __name__ == "__main__":
+if __name__ == '__main__':
# Must be run as root to call wall(1) without a banner.
- if len(sys.argv) != 2 or os.getuid() != 0:
+ if os.getuid() != 0:
print('This script requires superuser privileges.', file=sys.stderr)
exit(1)
- minutes = int(sys.argv[1])
+
+ args = parser.parse_args()
+
+ minutes = args.minutes
+ reboot = args.reboot
+
# Drop the argument from the list so that the notification
# doesn't kick in immediately.
if minutes in intervals:
intervals.remove(minutes)
for interval in sorted(intervals, reverse=True):
if minutes >= interval:
- notify(interval)
- minutes -= (minutes - interval)
+ notify(interval, reboot=reboot)
+ minutes -= minutes - interval
exit(0)
diff --git a/src/helpers/config_dependency.py b/src/helpers/config_dependency.py
index 50c72956e..817bcc65a 100755
--- a/src/helpers/config_dependency.py
+++ b/src/helpers/config_dependency.py
@@ -18,22 +18,75 @@
import os
import sys
+import json
from argparse import ArgumentParser
from argparse import ArgumentTypeError
-
-try:
- from vyos.configdep import check_dependency_graph
- from vyos.defaults import directories
-except ImportError:
- # allow running during addon package build
- _here = os.path.dirname(__file__)
- sys.path.append(os.path.join(_here, '../../python/vyos'))
- from configdep import check_dependency_graph
- from defaults import directories
+from graphlib import TopologicalSorter, CycleError
# addon packages will need to specify the dependency directory
-dependency_dir = os.path.join(directories['data'],
- 'config-mode-dependencies')
+data_dir = '/usr/share/vyos/'
+dependency_dir = os.path.join(data_dir, 'config-mode-dependencies')
+
+def dict_merge(source, destination):
+ from copy import deepcopy
+ tmp = deepcopy(destination)
+
+ for key, value in source.items():
+ if key not in tmp:
+ tmp[key] = value
+ elif isinstance(source[key], dict):
+ tmp[key] = dict_merge(source[key], tmp[key])
+
+ return tmp
+
+def read_dependency_dict(dependency_dir: str = dependency_dir) -> dict:
+ res = {}
+ for dep_file in os.listdir(dependency_dir):
+ if not dep_file.endswith('.json'):
+ continue
+ path = os.path.join(dependency_dir, dep_file)
+ with open(path) as f:
+ d = json.load(f)
+ if dep_file == 'vyos-1x.json':
+ res = dict_merge(res, d)
+ else:
+ res = dict_merge(d, res)
+
+ return res
+
+def graph_from_dependency_dict(d: dict) -> dict:
+ g = {}
+ 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
+ # distinguished
+ for el in list(d[k]):
+ g[k] |= set(d[k][el])
+
+ return g
+
+def is_acyclic(d: dict) -> bool:
+ g = graph_from_dependency_dict(d)
+ ts = TopologicalSorter(g)
+ try:
+ # get node iterator
+ order = ts.static_order()
+ # try iteration
+ _ = [*order]
+ except CycleError:
+ return False
+
+ return True
+
+def check_dependency_graph(dependency_dir: str = dependency_dir,
+ supplement: str = None) -> bool:
+ d = read_dependency_dict(dependency_dir=dependency_dir)
+ if supplement is not None:
+ with open(supplement) as f:
+ d = dict_merge(json.load(f), d)
+
+ return is_acyclic(d)
def path_exists(s):
if not os.path.exists(s):
@@ -50,8 +103,10 @@ def main():
args = vars(parser.parse_args())
if not check_dependency_graph(**args):
+ print("dependency error: cycle exists")
sys.exit(1)
+ print("dependency graph acyclic")
sys.exit(0)
if __name__ == '__main__':
diff --git a/src/helpers/latest-image-url.py b/src/helpers/latest-image-url.py
new file mode 100755
index 000000000..ea201ef7c
--- /dev/null
+++ b/src/helpers/latest-image-url.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+
+import sys
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.version import get_remote_version
+
+
+if __name__ == '__main__':
+ image_path = ''
+
+ config = ConfigTreeQuery()
+ if config.exists('system update-check url'):
+ configured_url_version = config.value('system update-check url')
+ remote_url_list = get_remote_version(configured_url_version)
+ if remote_url_list:
+ image_path = remote_url_list[0].get('url')
+ else:
+ sys.exit(1)
+
+ print(image_path)
diff --git a/src/helpers/run-config-activation.py b/src/helpers/run-config-activation.py
new file mode 100755
index 000000000..58293702a
--- /dev/null
+++ b/src/helpers/run-config-activation.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 re
+import logging
+from pathlib import Path
+from argparse import ArgumentParser
+
+from vyos.compose_config import ComposeConfig
+from vyos.compose_config import ComposeConfigError
+from vyos.defaults import directories
+
+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")
+
+args = parser.parse_args()
+
+checkpoint_file = '/run/vyos-activate-checkpoint'
+log_file = Path(directories['config']).joinpath('vyos-activate.log')
+
+logger = logging.getLogger(__name__)
+fh = logging.FileHandler(log_file)
+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
+ logger.setLevel(logging.DEBUG)
+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 Path(file_name).stem
+
+script_dir = Path(directories['activate'])
+
+if args.test_script:
+ script_list = [script_dir.joinpath(args.test_script)]
+else:
+ script_list = sorted(script_dir.glob('*.py'), key=sort_key)
+
+config_file = args.config_file
+config_str = Path(config_file).read_text()
+
+compose = ComposeConfig(config_str, checkpoint_file=debug)
+
+for file in script_list:
+ file = file.as_posix()
+ 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)
diff --git a/src/helpers/run-config-migration.py b/src/helpers/run-config-migration.py
index ce647ad0a..e6ce97363 100755
--- a/src/helpers/run-config-migration.py
+++ b/src/helpers/run-config-migration.py
@@ -1,86 +1,78 @@
-#!/usr/bin/python3
-
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2024 VyOS maintainers and contributors
#
-# 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 Lesser General Public License
-# along with this library. If not, see <http://www.gnu.org/licenses/>.
+# 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
-import argparse
-import datetime
-
-from vyos.utils.process import cmd
-from vyos.migrator import Migrator, VirtualMigrator
-
-def main():
- argparser = argparse.ArgumentParser(
- formatter_class=argparse.RawTextHelpFormatter)
- argparser.add_argument('config_file', type=str,
- help="configuration file to migrate")
- argparser.add_argument('--force', action='store_true',
- help="Force calling of all migration scripts.")
- argparser.add_argument('--set-vintage', type=str,
- choices=['vyatta', 'vyos'],
- help="Set the format for the config version footer in config"
- " file:\n"
- "set to 'vyatta':\n"
- "(for '/* === vyatta-config-version ... */' format)\n"
- "or 'vyos':\n"
- "(for '// vyos-config-version ...' format).")
- argparser.add_argument('--virtual', action='store_true',
- help="Update the format of the trailing comments in"
- " config file,\nfrom 'vyatta' to 'vyos'; no migration"
- " scripts are run.")
- args = argparser.parse_args()
+import time
+from argparse import ArgumentParser
+from shutil import copyfile
- config_file_name = args.config_file
- force_on = args.force
- vintage = args.set_vintage
- virtual = args.virtual
+from vyos.migrate import ConfigMigrate
+from vyos.migrate import ConfigMigrateError
- if not os.access(config_file_name, os.R_OK):
- print("Read error: {}.".format(config_file_name))
- sys.exit(1)
+parser = ArgumentParser()
+parser.add_argument('config_file', type=str,
+ help="configuration file to migrate")
+parser.add_argument('--test-script', type=str,
+ help="test named script")
+parser.add_argument('--output-file', type=str,
+ help="write to named output file instead of config file")
+parser.add_argument('--force', action='store_true',
+ help="force run of all migration scripts")
- if not os.access(config_file_name, os.W_OK):
- print("Write error: {}.".format(config_file_name))
- sys.exit(1)
+args = parser.parse_args()
- separator = "."
- backup_file_name = separator.join([config_file_name,
- '{0:%Y-%m-%d-%H%M%S}'.format(datetime.datetime.now()),
- 'pre-migration'])
+config_file = args.config_file
+out_file = args.output_file
+test_script = args.test_script
+force = args.force
- cmd(f'cp -p {config_file_name} {backup_file_name}')
+if not os.access(config_file, os.R_OK):
+ print(f"Config file '{config_file}' not readable")
+ sys.exit(1)
- if not virtual:
- virtual_migration = VirtualMigrator(config_file_name)
- virtual_migration.run()
+if out_file is None:
+ if not os.access(config_file, os.W_OK):
+ print(f"Config file '{config_file}' not writeable")
+ sys.exit(1)
+else:
+ try:
+ open(out_file, 'w').close()
+ except OSError:
+ print(f"Output file '{out_file}' not writeable")
+ sys.exit(1)
- migration = Migrator(config_file_name, force=force_on)
- migration.run()
+config_migrate = ConfigMigrate(config_file, force=force, output_file=out_file)
- if not migration.config_changed():
- os.remove(backup_file_name)
- else:
- virtual_migration = VirtualMigrator(config_file_name,
- set_vintage=vintage)
+if test_script:
+ # run_script and exit
+ config_migrate.run_script(test_script)
+ sys.exit(0)
- virtual_migration.run()
+backup = None
+if out_file is None:
+ timestr = time.strftime("%Y%m%d-%H%M%S")
+ backup = f'{config_file}.{timestr}.pre-migration'
+ copyfile(config_file, backup)
- if not virtual_migration.config_changed():
- os.remove(backup_file_name)
+try:
+ config_migrate.run()
+except ConfigMigrateError as e:
+ print(f'Error: {e}')
+ sys.exit(1)
-if __name__ == '__main__':
- main()
+if backup is not None and not config_migrate.config_modified:
+ os.unlink(backup)
diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py
new file mode 100755
index 000000000..84860bd6a
--- /dev/null
+++ b/src/helpers/vyos-config-encrypt.py
@@ -0,0 +1,273 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 shutil
+import sys
+
+from argparse import ArgumentParser
+from cryptography.fernet import Fernet
+from tempfile import NamedTemporaryFile
+from tempfile import TemporaryDirectory
+
+from vyos.tpm import clear_tpm_key
+from vyos.tpm import read_tpm_key
+from vyos.tpm import write_tpm_key
+from vyos.utils.io import ask_input, ask_yes_no
+from vyos.utils.process import cmd
+
+persistpath_cmd = '/opt/vyatta/sbin/vyos-persistpath'
+mount_paths = ['/config', '/opt/vyatta/etc/config']
+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_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
+
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ 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}')
+
+ os.unlink(key_file)
+
+ return True
+
+def encrypt_config(key, recovery_key):
+ if is_opened():
+ raise Exception('An encrypted config volume is already mapped')
+
+ # Clear and write key to 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)
+
+ luks_folder = os.path.join(persist_path, 'luks')
+
+ if not os.path.isdir(luks_folder):
+ os.mkdir(luks_folder)
+
+ image_name = get_current_image()
+ image_path = os.path.join(luks_folder, image_name)
+
+ # 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
+
+ # 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
+
+ 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')
+
+ 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)
+
+ cmd(f'umount {d}')
+
+ os.unlink(key_file)
+
+ 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}')
+
+ return True
+
+def decrypt_config(key):
+ if not key:
+ return
+
+ persist_path = cmd(persistpath_cmd).strip()
+ image_name = get_current_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
+
+ 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}')
+
+ # 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')
+
+ # 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')
+
+ cmd(f'umount {d}')
+
+ # Close encrypted volume
+ cmd('cryptsetup -q close vyos_config')
+
+ # Remove encrypted volume image file and key
+ if key_file:
+ os.unlink(key_file)
+ os.unlink(image_path)
+
+ try:
+ clear_tpm_key()
+ except:
+ pass
+
+ return True
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Must specify action.")
+ 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()
+
+ tpm_exists = os.path.exists('/sys/class/tpm/tpm0')
+
+ key = None
+ recovery_key = None
+ need_recovery = False
+
+ 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:
+ 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
+
+ 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"')
+
+ 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()
+
+ if len(recovery_key) >= 32:
+ break
+
+ print('Invalid key - must be at least 32 characters, try again.')
+ else:
+ recovery_key = Fernet.generate_key()
+
+ try:
+ if args.disable:
+ decrypt_config(key or recovery_key)
+
+ print('Encrypted config volume has been disabled')
+ print('Contents have been migrated to /config 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')
+ elif args.enable and tpm_exists:
+ encrypt_config(key, recovery_key)
+
+ print('Encrypted config volume has been enabled with TPM')
+ print('Backup the recovery key in a safe place!')
+ print('Recovery key: ' + recovery_key.decode())
+ elif args.enable:
+ encrypt_config(recovery_key)
+
+ print('Encrypted config volume has been enabled without TPM')
+ print('Backup the key in a safe place!')
+ print('Key: ' + recovery_key.decode())
+ except Exception as e:
+ word = 'decrypt' if args.disable or args.load else 'encrypt'
+ print(f'Failed to {word} config: {e}')
diff --git a/src/helpers/vyos-domain-resolver.py b/src/helpers/vyos-domain-resolver.py
deleted file mode 100755
index 05aae48ff..000000000
--- a/src/helpers/vyos-domain-resolver.py
+++ /dev/null
@@ -1,176 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2022-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 json
-import time
-
-from vyos.configdict import dict_merge
-from vyos.configquery import ConfigTreeQuery
-from vyos.firewall import fqdn_config_parse
-from vyos.firewall import fqdn_resolve
-from vyos.utils.commit import commit_in_progress
-from vyos.utils.dict import dict_search_args
-from vyos.utils.process import cmd
-from vyos.utils.process import run
-from vyos.xml_ref import get_defaults
-
-base = ['firewall']
-timeout = 300
-cache = False
-
-domain_state = {}
-
-ipv4_tables = {
- 'ip vyos_mangle',
- 'ip vyos_filter',
- 'ip vyos_nat'
-}
-
-ipv6_tables = {
- 'ip6 vyos_mangle',
- 'ip6 vyos_filter'
-}
-
-def get_config(conf):
- firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
-
- default_values = get_defaults(base, get_first_key=True)
-
- firewall = dict_merge(default_values, firewall)
-
- global timeout, cache
-
- if 'resolver_interval' in firewall:
- timeout = int(firewall['resolver_interval'])
-
- if 'resolver_cache' in firewall:
- cache = True
-
- fqdn_config_parse(firewall)
-
- return firewall
-
-def resolve(domains, ipv6=False):
- global domain_state
-
- ip_list = set()
-
- for domain in domains:
- resolved = fqdn_resolve(domain, ipv6=ipv6)
-
- if resolved and cache:
- domain_state[domain] = resolved
- elif not resolved:
- if domain not in domain_state:
- continue
- resolved = domain_state[domain]
-
- ip_list = ip_list | resolved
- return ip_list
-
-def nft_output(table, set_name, ip_list):
- output = [f'flush set {table} {set_name}']
- if ip_list:
- ip_str = ','.join(ip_list)
- output.append(f'add element {table} {set_name} {{ {ip_str} }}')
- return output
-
-def nft_valid_sets():
- try:
- valid_sets = []
- sets_json = cmd('nft --json list sets')
- sets_obj = json.loads(sets_json)
-
- for obj in sets_obj['nftables']:
- if 'set' in obj:
- family = obj['set']['family']
- table = obj['set']['table']
- name = obj['set']['name']
- valid_sets.append((f'{family} {table}', name))
-
- return valid_sets
- except:
- return []
-
-def update(firewall):
- conf_lines = []
- count = 0
-
- valid_sets = nft_valid_sets()
-
- domain_groups = dict_search_args(firewall, 'group', 'domain_group')
- if domain_groups:
- for set_name, domain_config in domain_groups.items():
- if 'address' not in domain_config:
- continue
-
- nft_set_name = f'D_{set_name}'
- domains = domain_config['address']
-
- ip_list = resolve(domains, ipv6=False)
- for table in ipv4_tables:
- if (table, nft_set_name) in valid_sets:
- conf_lines += nft_output(table, nft_set_name, ip_list)
-
- ip6_list = resolve(domains, ipv6=True)
- for table in ipv6_tables:
- if (table, nft_set_name) in valid_sets:
- conf_lines += nft_output(table, nft_set_name, ip6_list)
- count += 1
-
- for set_name, domain in firewall['ip_fqdn'].items():
- table = 'ip vyos_filter'
- nft_set_name = f'FQDN_{set_name}'
-
- ip_list = resolve([domain], ipv6=False)
-
- if (table, nft_set_name) in valid_sets:
- conf_lines += nft_output(table, nft_set_name, ip_list)
- count += 1
-
- for set_name, domain in firewall['ip6_fqdn'].items():
- table = 'ip6 vyos_filter'
- nft_set_name = f'FQDN_{set_name}'
-
- ip_list = resolve([domain], ipv6=True)
- if (table, nft_set_name) in valid_sets:
- conf_lines += nft_output(table, nft_set_name, ip_list)
- count += 1
-
- nft_conf_str = "\n".join(conf_lines) + "\n"
- code = run(f'nft --file -', input=nft_conf_str)
-
- print(f'Updated {count} sets - result: {code}')
-
-if __name__ == '__main__':
- print(f'VyOS domain resolver')
-
- count = 1
- while commit_in_progress():
- if ( count % 60 == 0 ):
- print(f'Commit still in progress after {count}s - waiting')
- count += 1
- time.sleep(1)
-
- conf = ConfigTreeQuery()
- firewall = get_config(conf)
-
- print(f'interval: {timeout}s - cache: {cache}')
-
- while True:
- update(firewall)
- time.sleep(timeout)
diff --git a/src/helpers/vyos-load-balancer.py b/src/helpers/vyos-load-balancer.py
new file mode 100755
index 000000000..2f07160b4
--- /dev/null
+++ b/src/helpers/vyos-load-balancer.py
@@ -0,0 +1,312 @@
+#!/usr/bin/python3
+
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import os
+import signal
+import sys
+import time
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.utils.commit import commit_in_progress
+from vyos.utils.network import get_interface_address
+from vyos.utils.process import rc_cmd
+from vyos.utils.process import run
+from vyos.xml_ref import get_defaults
+from vyos.wanloadbalance import health_ping_host
+from vyos.wanloadbalance import health_ping_host_ttl
+from vyos.wanloadbalance import parse_dhcp_nexthop
+from vyos.wanloadbalance import parse_ppp_nexthop
+
+nftables_wlb_conf = '/run/nftables_wlb.conf'
+wlb_status_file = '/run/wlb_status.json'
+wlb_pid_file = '/run/wlb_daemon.pid'
+sleep_interval = 5 # Main loop sleep interval
+
+def health_check(ifname, conf, state, test_defaults):
+ # Run health tests for interface
+
+ if get_ipv4_address(ifname) is None:
+ return False
+
+ if 'test' not in conf:
+ resp_time = test_defaults['resp-time']
+ target = conf['nexthop']
+
+ if target == 'dhcp':
+ target = state['dhcp_nexthop']
+
+ if not target:
+ return False
+
+ return health_ping_host(target, ifname, wait_time=resp_time)
+
+ for test_id, test_conf in conf['test'].items():
+ check_type = test_conf['type']
+
+ if check_type == 'ping':
+ resp_time = test_conf['resp_time']
+ target = test_conf['target']
+ if not health_ping_host(target, ifname, wait_time=resp_time):
+ return False
+ elif check_type == 'ttl':
+ target = test_conf['target']
+ ttl_limit = test_conf['ttl_limit']
+ if not health_ping_host_ttl(target, ifname, ttl_limit=ttl_limit):
+ return False
+ elif check_type == 'user-defined':
+ script = test_conf['test_script']
+ rc = run(script)
+ if rc != 0:
+ return False
+
+ return True
+
+def on_state_change(lb, ifname, state):
+ # Run hook on state change
+ if 'hook' in lb:
+ script_path = os.path.join('/config/scripts/', lb['hook'])
+ env = {
+ 'WLB_INTERFACE_NAME': ifname,
+ 'WLB_INTERFACE_STATE': 'ACTIVE' if state else 'FAILED'
+ }
+
+ code = run(script_path, env=env)
+ if code != 0:
+ print('WLB hook returned non-zero error code')
+
+ print(f'INFO: State change: {ifname} -> {state}')
+
+def get_ipv4_address(ifname):
+ # Get primary ipv4 address on interface (for source nat)
+ addr_json = get_interface_address(ifname)
+ if 'addr_info' in addr_json and len(addr_json['addr_info']) > 0:
+ for addr_info in addr_json['addr_info']:
+ if addr_info['family'] == 'inet':
+ if 'local' in addr_info:
+ return addr_json['addr_info'][0]['local']
+ return None
+
+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)
+
+ table_num = lb['health_state'][ifname]['table_number']
+
+ if dhcp_nexthop_addr and lb['health_state'][ifname]['dhcp_nexthop'] != dhcp_nexthop_addr:
+ lb['health_state'][ifname]['dhcp_nexthop'] = dhcp_nexthop_addr
+ run(f'ip route replace table {table_num} default dev {ifname} via {dhcp_nexthop_addr}')
+
+ if_addr = get_ipv4_address(ifname)
+ if if_addr and if_addr != lb['health_state'][ifname]['if_addr']:
+ lb['health_state'][ifname]['if_addr'] = if_addr
+ return True
+
+ return False
+
+def nftables_update(lb):
+ # Atomically reload nftables table from template
+ if not os.path.exists(nftables_wlb_conf):
+ lb['first_install'] = True
+ elif 'first_install' in lb:
+ del lb['first_install']
+
+ render(nftables_wlb_conf, 'load-balancing/nftables-wlb.j2', lb)
+
+ rc, out = rc_cmd(f'nft -f {nftables_wlb_conf}')
+
+ if rc != 0:
+ print('ERROR: Failed to apply WLB nftables config')
+ print('Output:', out)
+ return False
+
+ return True
+
+def cleanup(lb):
+ if 'interface_health' in lb:
+ index = 1
+ for ifname, health_conf in lb['interface_health'].items():
+ table_num = lb['mark_offset'] + index
+ run(f'ip route del table {table_num} default')
+ run(f'ip rule del fwmark {hex(table_num)} table {table_num}')
+ index += 1
+
+ run(f'nft delete table ip vyos_wanloadbalance')
+
+def get_config():
+ conf = Config()
+ base = ['load-balancing', 'wan']
+ lb = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, with_recursive_defaults=True)
+
+ lb['test_defaults'] = get_defaults(base + ['interface-health', 'A', 'test', 'B'], get_first_key=True)
+
+ return lb
+
+if __name__ == '__main__':
+ while commit_in_progress():
+ print("Notice: Waiting for commit to complete...")
+ time.sleep(1)
+
+ lb = get_config()
+
+ lb['health_state'] = {}
+ lb['mark_offset'] = 0xc8
+
+ # Create state dicts, interface address and nexthop, install routes and ip rules
+ if 'interface_health' in lb:
+ index = 1
+ for ifname, health_conf in lb['interface_health'].items():
+ table_num = lb['mark_offset'] + index
+ addr = get_ipv4_address(ifname)
+ lb['health_state'][ifname] = {
+ 'if_addr': addr,
+ 'failure_count': 0,
+ 'success_count': 0,
+ 'last_success': 0,
+ 'last_failure': 0,
+ 'state': addr is not None,
+ 'state_changed': False,
+ 'table_number': table_num,
+ 'mark': hex(table_num)
+ }
+
+ if health_conf['nexthop'] == 'dhcp':
+ lb['health_state'][ifname]['dhcp_nexthop'] = None
+
+ dynamic_nexthop_update(lb, ifname)
+ 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}')
+
+ index += 1
+
+ nftables_update(lb)
+
+ run('ip route flush cache')
+
+ if 'flush_connections' in lb:
+ run('conntrack --delete')
+ run('conntrack -F expect')
+
+ with open(wlb_status_file, 'w') as f:
+ f.write(json.dumps(lb['health_state']))
+
+ # Signal handler SIGUSR2 -> dhcpcd update
+ def handle_sigusr2(signum, frame):
+ for ifname, health_conf in lb['interface_health'].items():
+ if 'nexthop' in health_conf and health_conf['nexthop'] == 'dhcp':
+ retval = dynamic_nexthop_update(lb, ifname)
+
+ if retval:
+ nftables_update(lb)
+
+ # Signal handler SIGTERM -> exit
+ def handle_sigterm(signum, frame):
+ if os.path.exists(wlb_status_file):
+ os.unlink(wlb_status_file)
+
+ if os.path.exists(wlb_pid_file):
+ os.unlink(wlb_pid_file)
+
+ if os.path.exists(nftables_wlb_conf):
+ os.unlink(nftables_wlb_conf)
+
+ cleanup(lb)
+ sys.exit(0)
+
+ signal.signal(signal.SIGUSR2, handle_sigusr2)
+ signal.signal(signal.SIGINT, handle_sigterm)
+ signal.signal(signal.SIGTERM, handle_sigterm)
+
+ with open(wlb_pid_file, 'w') as f:
+ f.write(str(os.getpid()))
+
+ # Main loop
+
+ try:
+ while True:
+ ip_change = False
+
+ if 'interface_health' in lb:
+ for ifname, health_conf in lb['interface_health'].items():
+ state = lb['health_state'][ifname]
+
+ result = health_check(ifname, health_conf, state=state, test_defaults=lb['test_defaults'])
+
+ state_changed = result != state['state']
+ state['state_changed'] = False
+
+ if result:
+ state['failure_count'] = 0
+ state['success_count'] += 1
+ state['last_success'] = time.time()
+ if state_changed and state['success_count'] >= int(health_conf['success_count']):
+ state['state'] = True
+ state['state_changed'] = True
+ elif not result:
+ state['failure_count'] += 1
+ state['success_count'] = 0
+ state['last_failure'] = time.time()
+ if state_changed and state['failure_count'] >= int(health_conf['failure_count']):
+ state['state'] = False
+ state['state_changed'] = True
+
+ if state['state_changed']:
+ state['if_addr'] = get_ipv4_address(ifname)
+ on_state_change(lb, ifname, state['state'])
+
+ if dynamic_nexthop_update(lb, ifname):
+ ip_change = True
+
+ if any(state['state_changed'] for ifname, state in lb['health_state'].items()):
+ if not nftables_update(lb):
+ break
+
+ run('ip route flush cache')
+
+ if 'flush_connections' in lb:
+ run('conntrack --delete')
+ run('conntrack -F expect')
+
+ with open(wlb_status_file, 'w') as f:
+ f.write(json.dumps(lb['health_state']))
+ elif ip_change:
+ nftables_update(lb)
+
+ time.sleep(sleep_interval)
+ except Exception as e:
+ print('WLB ERROR:', e)
+
+ if os.path.exists(wlb_status_file):
+ os.unlink(wlb_status_file)
+
+ if os.path.exists(wlb_pid_file):
+ os.unlink(wlb_pid_file)
+
+ if os.path.exists(nftables_wlb_conf):
+ os.unlink(nftables_wlb_conf)
+
+ cleanup(lb)
diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py
index 4ec865454..16083fd41 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 VyOS maintainers and contributors
+# Copyright (C) 2019-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
@@ -30,7 +30,8 @@ import tempfile
import vyos.defaults
import vyos.remote
from vyos.configsource import ConfigSourceSession, VyOSError
-from vyos.migrator import Migrator, VirtualMigrator, MigratorError
+from vyos.migrate import ConfigMigrate
+from vyos.migrate import ConfigMigrateError
class LoadConfig(ConfigSourceSession):
"""A subclass for calling 'loadFile'.
@@ -81,22 +82,16 @@ with tempfile.NamedTemporaryFile() as fp:
with open(fp.name, 'w') as fd:
fd.write(config_string)
- virtual_migration = VirtualMigrator(fp.name)
+ config_migrate = ConfigMigrate(fp.name)
try:
- virtual_migration.run()
- except MigratorError as err:
- sys.exit('{}'.format(err))
-
- migration = Migrator(fp.name)
- try:
- migration.run()
- except MigratorError as err:
- sys.exit('{}'.format(err))
+ config_migrate.run()
+ except ConfigMigrateError as err:
+ sys.exit(err)
try:
config.load_config(fp.name)
except VyOSError as err:
- sys.exit('{}'.format(err))
+ sys.exit(err)
if config.session_changed():
print("Load complete. Use 'commit' to make changes effective.")
diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py
index 35424626e..5ef845ac2 100755
--- a/src/helpers/vyos-merge-config.py
+++ b/src/helpers/vyos-merge-config.py
@@ -22,7 +22,8 @@ import vyos.remote
from vyos.config import Config
from vyos.configtree import ConfigTree
-from vyos.migrator import Migrator, VirtualMigrator
+from vyos.migrate import ConfigMigrate
+from vyos.migrate import ConfigMigrateError
from vyos.utils.process import cmd
from vyos.utils.process import DEVNULL
@@ -61,15 +62,11 @@ with tempfile.NamedTemporaryFile() as file_to_migrate:
with open(file_to_migrate.name, 'w') as fd:
fd.write(config_file)
- virtual_migration = VirtualMigrator(file_to_migrate.name)
- virtual_migration.run()
-
- migration = Migrator(file_to_migrate.name)
- migration.run()
-
- if virtual_migration.config_changed() or migration.config_changed():
- with open(file_to_migrate.name, 'r') as fd:
- config_file = fd.read()
+ config_migrate = ConfigMigrate(file_to_migrate.name)
+ try:
+ config_migrate.run()
+ except ConfigMigrateError as e:
+ sys.exit(e)
merge_config_tree = ConfigTree(config_file)
diff --git a/src/helpers/vyos-save-config.py b/src/helpers/vyos-save-config.py
index 518bd9864..fa2ea0ce4 100755
--- a/src/helpers/vyos-save-config.py
+++ b/src/helpers/vyos-save-config.py
@@ -23,7 +23,7 @@ from argparse import ArgumentParser
from vyos.config import Config
from vyos.remote import urlc
-from vyos.component_version import system_footer
+from vyos.component_version import add_system_version
from vyos.defaults import directories
DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot')
@@ -50,14 +50,13 @@ if re.match(r'\w+:/', save_file):
config = Config()
ct = config.get_config_tree(effective=True)
+# pylint: disable=consider-using-with
write_file = save_file if remote_save is None else NamedTemporaryFile(delete=False).name
-with open(write_file, 'w') as f:
- # config_tree is None before boot configuration is complete;
- # automated saves should check boot_configuration_complete
- if ct is not None:
- f.write(ct.to_string())
- f.write("\n")
- f.write(system_footer())
+
+# 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 json_file is not None and ct is not None:
try:
diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name
index 8c0992414..f5de182c6 100755
--- a/src/helpers/vyos_net_name
+++ b/src/helpers/vyos_net_name
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2023 VyOS maintainers and contributors
+# 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
@@ -18,42 +18,35 @@ import os
import re
import time
import logging
+import logging.handlers
import tempfile
-import threading
+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.migrator import VirtualMigrator
+from vyos.utils.locking import Lock
+from vyos.migrate import ConfigMigrate
+# Define variables
vyos_udev_dir = directories['vyos_udev_dir']
-vyos_log_dir = '/run/udev/log'
-vyos_log_file = os.path.join(vyos_log_dir, 'vyos-net-name')
-
config_path = '/opt/vyatta/etc/config/config.boot'
-lock = threading.Lock()
-
-try:
- os.mkdir(vyos_log_dir)
-except FileExistsError:
- pass
-
-logging.basicConfig(filename=vyos_log_file, level=logging.DEBUG)
def is_available(intfs: dict, intf_name: str) -> bool:
- """ Check if interface name is already assigned
- """
+ """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]
+ """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))
@@ -62,21 +55,22 @@ def find_available(intfs: dict, prefix: str) -> str:
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")
+ """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)
+ return 'eth' + str(int(intf[1]) - 2)
else:
- return "eth" + str(intf[1])
+ return 'eth' + str(intf[1])
return ifname
+
def get_biosdevname(ifname: str) -> str:
- """ Use legacy vyatta-biosdevname to query for name
+ """Use legacy vyatta-biosdevname to query for name
This is carried over for compatability only, and will likely be dropped
going forward.
@@ -95,11 +89,12 @@ def get_biosdevname(ifname: str) -> str:
try:
biosname = cmd(f'/sbin/biosdevname --policy all_ethN -i {ifname}')
except Exception as e:
- logging.error(f'biosdevname error: {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
@@ -112,18 +107,18 @@ def leave_rescan_hint(intf_name: str, hwid: str):
except FileExistsError:
pass
except Exception as e:
- logging.critical(f"Error creating rescan hint directory: {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:
- logging.critical(f"OSError {e}")
+ logger.critical(f'OSError {e}')
+
def get_configfile_interfaces() -> dict:
- """Read existing interfaces from config file
- """
+ """Read existing interfaces from config file"""
interfaces: dict = {}
if not os.path.isfile(config_path):
@@ -134,28 +129,31 @@ def get_configfile_interfaces() -> dict:
with open(config_path) as f:
config_file = f.read()
except OSError as e:
- logging.critical(f"OSError {e}")
+ logger.critical(f'OSError {e}')
exit(1)
try:
config = ConfigTree(config_file)
except Exception:
try:
- logging.debug(f"updating component version string syntax")
+ 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)
- virtual_migration = VirtualMigrator(fp.name)
- virtual_migration.run()
+ 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:
- logging.critical(f"ConfigTree error: {e}")
+ logger.critical(f'ConfigTree error: {e}')
+ exit(1)
base = ['interfaces', 'ethernet']
if config.exists(base):
@@ -163,11 +161,13 @@ def get_configfile_interfaces() -> dict:
for intf in eth_intfs:
path = base + [intf, 'hw-id']
if not config.exists(path):
- logging.warning(f"no 'hw-id' entry for {intf}")
+ logger.warning(f"no 'hw-id' entry for {intf}")
continue
hwid = config.return_value(path)
if hwid in list(interfaces):
- logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}")
+ logger.warning(
+ f'multiple entries for {hwid}: {interfaces[hwid]}, {intf}'
+ )
continue
interfaces[hwid] = intf
@@ -177,21 +177,23 @@ def get_configfile_interfaces() -> dict:
for intf in wlan_intfs:
path = base + [intf, 'hw-id']
if not config.exists(path):
- logging.warning(f"no 'hw-id' entry for {intf}")
+ logger.warning(f"no 'hw-id' entry for {intf}")
continue
hwid = config.return_value(path)
if hwid in list(interfaces):
- logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}")
+ logger.warning(
+ f'multiple entries for {hwid}: {interfaces[hwid]}, {intf}'
+ )
continue
interfaces[hwid] = intf
- logging.debug(f"config file entries: {interfaces}")
+ logger.debug(f'config file entries: {interfaces}')
return interfaces
+
def add_assigned_interfaces(intfs: dict):
- """Add interfaces found by previous invocation of udev rule
- """
+ """Add interfaces found by previous invocation of udev rule"""
if not os.path.isdir(vyos_udev_dir):
return
@@ -201,55 +203,74 @@ def add_assigned_interfaces(intfs: dict):
with open(path) as f:
hwid = f.read().rstrip()
except OSError as e:
- logging.error(f"OSError {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
- """
- logging.info(f"lookup {intf_name}, {hwid}")
+ """Called on boot by vyos-router: 'coldplug' in vyatta_net_name"""
+ logger.info(f'lookup {intf_name}, {hwid}')
interfaces = get_configfile_interfaces()
- logging.debug(f"config file interfaces are {interfaces}")
+ logger.debug(f'config file interfaces are {interfaces}')
if hwid in list(interfaces):
- logging.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'")
+ logger.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'")
return interfaces[hwid]
add_assigned_interfaces(interfaces)
- logging.debug(f"adding assigned interfaces: {interfaces}")
+ logger.debug(f'adding assigned interfaces: {interfaces}')
if predefined:
newname = predefined
- logging.info(f"predefined interface name for '{intf_name}' is '{newname}'")
+ logger.info(f"predefined interface name for '{intf_name}' is '{newname}'")
else:
newname = get_biosdevname(intf_name)
- logging.info(f"biosdevname returned '{newname}' for '{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)
- logging.info(f"new name for '{intf_name}' is '{newname}'")
+ 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 len(argv) > 3:
- predef_name = argv[3]
-else:
- predef_name = ''
-
-lock.acquire()
-if not boot_configuration_complete():
- res = on_boot_event(argv[1], argv[2], predefined=predef_name)
- logging.debug(f"on boot, returned name is {res}")
- print(res)
-else:
- logging.debug("boot configuration complete")
-lock.release()
+
+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')