diff options
Diffstat (limited to 'src/helpers')
30 files changed, 859 insertions, 387 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/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 817bcc65a..d6358bef8 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 diff --git a/src/helpers/geoip-update.py b/src/helpers/geoip-update.py index 34accf2cc..22d26e538 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 @@ -25,20 +25,19 @@ def get_config(config=None): conf = config else: conf = ConfigTreeQuery() - base = ['firewall'] - 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) + return ( + 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") args = parser.parse_args() - firewall = get_config() - - if not geoip_update(firewall, force=args.force): + firewall, policy = get_config() + if not geoip_update(firewall=firewall, policy=policy, force=args.force): sys.exit(1) 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/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..7e464afd5 --- /dev/null +++ b/src/helpers/reset_section.py @@ -0,0 +1,124 @@ +#!/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 + + +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.getppid() + +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) + +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..f20adff1e 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 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 new file mode 100755 index 000000000..85ee64cb1 --- /dev/null +++ b/src/helpers/show_commit_data.py @@ -0,0 +1,56 @@ +#!/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/>. +# +# +# This script is used to show the commit data of the configuration + +import sys +from pathlib import Path +from argparse import ArgumentParser + +from vyos.config_mgmt import ConfigMgmt +from vyos.configtree import ConfigTree +from vyos.configtree import show_commit_data + +cm = ConfigMgmt() + +parser = ArgumentParser( + description='Show commit priority queue; no options compares the last two commits' +) +parser.add_argument('--active-config', help='Path to the active configuration file') +parser.add_argument('--proposed-config', help='Path to the proposed configuration file') +args = parser.parse_args() + +active_arg = args.active_config +proposed_arg = args.proposed_config + +if active_arg and not proposed_arg: + print('--proposed-config is required when --active-config is specified') + sys.exit(1) + +if not active_arg and not proposed_arg: + active = cm.get_config_tree_revision(1) + proposed = cm.get_config_tree_revision(0) +else: + if active_arg: + active = ConfigTree(Path(active_arg).read_text()) + else: + active = cm.get_config_tree_revision(0) + + proposed = ConfigTree(Path(proposed_arg).read_text()) + +ret = show_commit_data(active, proposed) +print(ret) 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..c30303c99 --- /dev/null +++ b/src/helpers/teardown-config-session.py @@ -0,0 +1,27 @@ +#!/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] + +vc = VyconfSession(pid=pid) +vc.teardown() diff --git a/src/helpers/test_commit.py b/src/helpers/test_commit.py new file mode 100755 index 000000000..cfff85b9d --- /dev/null +++ b/src/helpers/test_commit.py @@ -0,0 +1,49 @@ +#!/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/>. +# +# +# This script is used to test execution of the commit algorithm by vyos-commitd + +from pathlib import Path +from argparse import ArgumentParser +from datetime import datetime + +from vyos.configtree import ConfigTree +from vyos.configtree import test_commit + + +parser = ArgumentParser( + description='Execute commit priority queue' +) +parser.add_argument( + '--active-config', help='Path to the active configuration file', required=True +) +parser.add_argument( + '--proposed-config', help='Path to the proposed configuration file', required=True +) +args = parser.parse_args() + +active_arg = args.active_config +proposed_arg = args.proposed_config + +active = ConfigTree(Path(active_arg).read_text()) +proposed = ConfigTree(Path(proposed_arg).read_text()) + + +time_begin_commit = datetime.now() +test_commit(active, proposed) +time_end_commit = datetime.now() +print(f'commit time: {time_end_commit - time_begin_commit}') diff --git a/src/helpers/vyconf_cli.py b/src/helpers/vyconf_cli.py new file mode 100755 index 000000000..53542dd63 --- /dev/null +++ b/src/helpers/vyconf_cli.py @@ -0,0 +1,47 @@ +#!/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}') + +out = func() +if isinstance(out, bool): + # for use in shell scripts + sys.exit(int(not out)) + +print(out) diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py index 42de696ce..e20b8415f 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 diff --git a/src/helpers/vyos-certbot-renew-pki.sh b/src/helpers/vyos-certbot-renew-pki.sh index d0b663f7b..1c273d2fa 100755 --- a/src/helpers/vyos-certbot-renew-pki.sh +++ b/src/helpers/vyos-certbot-renew-pki.sh @@ -1,3 +1,3 @@ -#!/bin/sh +#!/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..5f49ca119 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 diff --git a/src/helpers/vyos-domain-resolver.py b/src/helpers/vyos-domain-resolver.py deleted file mode 100755 index 57cfcabd7..000000000 --- a/src/helpers/vyos-domain-resolver.py +++ /dev/null @@ -1,178 +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', - 'ip raw' -} - -ipv6_tables = { - 'ip6 vyos_mangle', - 'ip6 vyos_filter', - 'ip6 raw' -} - -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-failover.py b/src/helpers/vyos-failover.py index 348974364..96db947e1 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 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 new file mode 100755 index 000000000..850c5142e --- /dev/null +++ b/src/helpers/vyos-load-balancer.py @@ -0,0 +1,318 @@ +#!/usr/bin/python3 + +# 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 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_json and '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 + + init = True; + 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 + + #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']) + + 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 16083fd41..01a6a88dc 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,57 @@ # # -"""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 -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'] -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', 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 -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-save-config.py b/src/helpers/vyos-save-config.py index fa2ea0ce4..adf62b71d 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 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..80bfb6d17 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 diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name index f5de182c6..6ad7af2d6 100755 --- a/src/helpers/vyos_net_name +++ b/src/helpers/vyos_net_name @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-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 |