diff options
Diffstat (limited to 'src/helpers')
-rwxr-xr-x | src/helpers/commit-confirm-notify.py | 1 | ||||
-rwxr-xr-x | src/helpers/config_dependency.py | 58 | ||||
-rwxr-xr-x | src/helpers/run-config-migration.py | 2 | ||||
-rwxr-xr-x | src/helpers/vyos-boot-config-loader.py | 2 | ||||
-rwxr-xr-x | src/helpers/vyos-check-wwan.py | 2 | ||||
-rwxr-xr-x | src/helpers/vyos-domain-group-resolve.py | 60 | ||||
-rwxr-xr-x | src/helpers/vyos-domain-resolver.py | 179 | ||||
-rwxr-xr-x | src/helpers/vyos-failover.py | 235 | ||||
-rwxr-xr-x | src/helpers/vyos-interface-rescan.py | 2 | ||||
-rwxr-xr-x | src/helpers/vyos-merge-config.py | 7 | ||||
-rwxr-xr-x | src/helpers/vyos-save-config.py | 58 | ||||
-rwxr-xr-x | src/helpers/vyos-sudo.py | 2 | ||||
-rwxr-xr-x | src/helpers/vyos_config_sync.py | 192 | ||||
-rwxr-xr-x | src/helpers/vyos_net_name | 7 |
14 files changed, 735 insertions, 72 deletions
diff --git a/src/helpers/commit-confirm-notify.py b/src/helpers/commit-confirm-notify.py index eb7859ffa..8d7626c78 100755 --- a/src/helpers/commit-confirm-notify.py +++ b/src/helpers/commit-confirm-notify.py @@ -17,6 +17,7 @@ def notify(interval): if __name__ == "__main__": # Must be run as root to call wall(1) without a banner. if len(sys.argv) != 2 or os.getuid() != 0: + print('This script requires superuser privileges.', file=sys.stderr) exit(1) minutes = int(sys.argv[1]) # Drop the argument from the list so that the notification diff --git a/src/helpers/config_dependency.py b/src/helpers/config_dependency.py new file mode 100755 index 000000000..50c72956e --- /dev/null +++ b/src/helpers/config_dependency.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 sys +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 + +# addon packages will need to specify the dependency directory +dependency_dir = os.path.join(directories['data'], + 'config-mode-dependencies') + +def path_exists(s): + if not os.path.exists(s): + raise ArgumentTypeError("Must specify a valid vyos-1x dependency directory") + return s + +def main(): + parser = ArgumentParser(description='generate and save dict from xml defintions') + parser.add_argument('--dependency-dir', type=path_exists, + default=dependency_dir, + help='location of vyos-1x dependency directory') + parser.add_argument('--supplement', type=str, + help='supplemental dependency file') + args = vars(parser.parse_args()) + + if not check_dependency_graph(**args): + sys.exit(1) + + sys.exit(0) + +if __name__ == '__main__': + main() diff --git a/src/helpers/run-config-migration.py b/src/helpers/run-config-migration.py index cc7166c22..ce647ad0a 100755 --- a/src/helpers/run-config-migration.py +++ b/src/helpers/run-config-migration.py @@ -20,7 +20,7 @@ import sys import argparse import datetime -from vyos.util import cmd +from vyos.utils.process import cmd from vyos.migrator import Migrator, VirtualMigrator def main(): diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py index b9cc87bfa..01b06526d 100755 --- a/src/helpers/vyos-boot-config-loader.py +++ b/src/helpers/vyos-boot-config-loader.py @@ -26,7 +26,7 @@ from datetime import datetime from vyos.defaults import directories, config_status from vyos.configsession import ConfigSession, ConfigSessionError from vyos.configtree import ConfigTree -from vyos.util import cmd +from vyos.utils.process import cmd STATUS_FILE = config_status TRACE_FILE = '/tmp/boot-config-trace' diff --git a/src/helpers/vyos-check-wwan.py b/src/helpers/vyos-check-wwan.py index 2ff9a574f..334f08dd3 100755 --- a/src/helpers/vyos-check-wwan.py +++ b/src/helpers/vyos-check-wwan.py @@ -17,7 +17,7 @@ from vyos.configquery import VbashOpRun from vyos.configquery import ConfigTreeQuery -from vyos.util import is_wwan_connected +from vyos.utils.network import is_wwan_connected conf = ConfigTreeQuery() dict = conf.get_config_dict(['interfaces', 'wwan'], key_mangling=('-', '_'), diff --git a/src/helpers/vyos-domain-group-resolve.py b/src/helpers/vyos-domain-group-resolve.py deleted file mode 100755 index 6b677670b..000000000 --- a/src/helpers/vyos-domain-group-resolve.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2022 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 time - -from vyos.configquery import ConfigTreeQuery -from vyos.firewall import get_ips_domains_dict -from vyos.firewall import nft_add_set_elements -from vyos.firewall import nft_flush_set -from vyos.firewall import nft_init_set -from vyos.firewall import nft_update_set_elements -from vyos.util import call - - -base = ['firewall', 'group', 'domain-group'] -check_required = True -# count_failed = 0 -# Timeout in sec between checks -timeout = 300 - -domain_state = {} - -if __name__ == '__main__': - - while check_required: - config = ConfigTreeQuery() - if config.exists(base): - domain_groups = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - for set_name, domain_config in domain_groups.items(): - list_domains = domain_config['address'] - elements = [] - ip_dict = get_ips_domains_dict(list_domains) - - for domain in list_domains: - # Resolution succeeded, update domain state - if domain in ip_dict: - domain_state[domain] = ip_dict[domain] - elements += ip_dict[domain] - # Resolution failed, use previous domain state - elif domain in domain_state: - elements += domain_state[domain] - - # Resolve successful - if elements: - nft_update_set_elements(f'D_{set_name}', elements) - time.sleep(timeout) diff --git a/src/helpers/vyos-domain-resolver.py b/src/helpers/vyos-domain-resolver.py new file mode 100755 index 000000000..eac3d37af --- /dev/null +++ b/src/helpers/vyos-domain-resolver.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022-2023 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 os +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 -j 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 -f -', 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 new file mode 100755 index 000000000..cc7610370 --- /dev/null +++ b/src/helpers/vyos-failover.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022-2023 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 argparse +import json +import subprocess +import socket +import time + +from vyos.utils.process import rc_cmd +from pathlib import Path +from systemd import journal + + +my_name = Path(__file__).stem + + +def is_route_exists(route, gateway, interface, metric): + """Check if route with expected gateway, dev and metric exists""" + rc, data = rc_cmd(f'ip --json route show protocol failover {route} ' + f'via {gateway} dev {interface} metric {metric}') + if rc == 0: + data = json.loads(data) + if len(data) > 0: + return True + return False + + +def get_best_route_options(route, debug=False): + """ + Return current best route ('gateway, interface, metric) + + % get_best_route_options('203.0.113.1') + ('192.168.0.1', 'eth1', 1) + + % get_best_route_options('203.0.113.254') + (None, None, None) + """ + rc, data = rc_cmd(f'ip --detail --json route show protocol failover {route}') + if rc == 0: + data = json.loads(data) + if len(data) == 0: + print(f'\nRoute {route} for protocol failover was not found') + return None, None, None + # Fake metric 999 by default + # Search route with the lowest metric + best_metric = 999 + for entry in data: + if debug: print('\n', entry) + metric = entry.get('metric') + gateway = entry.get('gateway') + iface = entry.get('dev') + if metric < best_metric: + best_metric = metric + best_gateway = gateway + best_interface = iface + if debug: + print(f'### Best_route exists: {route}, best_gateway: {best_gateway}, ' + f'best_metric: {best_metric}, best_iface: {best_interface}') + return best_gateway, best_interface, best_metric + + +def is_port_open(ip, port): + """ + Check connection to remote host and port + Return True if host alive + + % is_port_open('example.com', 8080) + True + """ + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + s.settimeout(2) + try: + s.connect((ip, int(port))) + s.shutdown(socket.SHUT_RDWR) + return True + except: + return False + finally: + s.close() + + +def is_target_alive(target_list=None, + iface='', + proto='icmp', + port=None, + debug=False, + policy='any-available') -> bool: + """Check the availability of each target in the target_list using + the specified protocol ICMP, ARP, TCP + + Args: + target_list (list): A list of IP addresses or hostnames to check. + iface (str): The name of the network interface to use for the check. + proto (str): The protocol to use for the check. Options are 'icmp', 'arp', or 'tcp'. + port (int): The port number to use for the TCP check. Only applicable if proto is 'tcp'. + debug (bool): If True, print debug information during the check. + policy (str): The policy to use for the check. Options are 'any-available' or 'all-available'. + + Returns: + bool: True if all targets are reachable according to the policy, False otherwise. + + Example: + % is_target_alive(['192.0.2.1', '192.0.2.5'], 'eth1', proto='arp', policy='all-available') + True + """ + if iface != '': + iface = f'-I {iface}' + + num_reachable_targets = 0 + for target in target_list: + match proto: + case 'icmp': + command = f'/usr/bin/ping -q {target} {iface} -n -c 2 -W 1' + rc, response = rc_cmd(command) + if debug: + print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') + if rc == 0: + num_reachable_targets += 1 + if policy == 'any-available': + return True + + case 'arp': + command = f'/usr/bin/arping -b -c 2 -f -w 1 -i 1 {iface} {target}' + rc, response = rc_cmd(command) + if debug: + print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') + if rc == 0: + num_reachable_targets += 1 + if policy == 'any-available': + return True + + case _ if proto == 'tcp' and port is not None: + if is_port_open(target, port): + num_reachable_targets += 1 + if policy == 'any-available': + return True + + case _: + return False + + if policy == 'all-available' and num_reachable_targets == len(target_list): + return True + + return False + + +if __name__ == '__main__': + # Parse command arguments and get config + parser = argparse.ArgumentParser() + parser.add_argument('-c', + '--config', + action='store', + help='Path to protocols failover configuration', + required=True, + type=Path) + + args = parser.parse_args() + try: + config_path = Path(args.config) + config = json.loads(config_path.read_text()) + except Exception as err: + print( + f'Configuration file "{config_path}" does not exist or malformed: {err}' + ) + exit(1) + + # Useful debug info to console, use debug = True + # sudo systemctl stop vyos-failover.service + # sudo /usr/libexec/vyos/vyos-failover.py --config /run/vyos-failover.conf + debug = False + + while(True): + + for route, route_config in config.get('route').items(): + + exists_gateway, exists_iface, exists_metric = get_best_route_options(route, debug=debug) + + for next_hop, nexthop_config in route_config.get('next_hop').items(): + conf_iface = nexthop_config.get('interface') + conf_metric = int(nexthop_config.get('metric')) + port = nexthop_config.get('check').get('port') + port_opt = f'port {port}' if port else '' + policy = nexthop_config.get('check').get('policy') + proto = nexthop_config.get('check').get('type') + target = nexthop_config.get('check').get('target') + timeout = nexthop_config.get('check').get('timeout') + + # Route not found in the current routing table + if not is_route_exists(route, next_hop, conf_iface, conf_metric): + if debug: print(f" [NEW_ROUTE_DETECTED] route: [{route}]") + # Add route if check-target alive + if is_target_alive(target, conf_iface, proto, port, debug=debug, policy=policy): + if debug: print(f' [ ADD ] -- ip route add {route} via {next_hop} dev {conf_iface} ' + f'metric {conf_metric} proto failover\n###') + rc, command = rc_cmd(f'ip route add {route} via {next_hop} dev {conf_iface} ' + f'metric {conf_metric} proto failover') + # If something is wrong and gateway not added + # Example: Error: Next-hop has invalid gateway. + if rc !=0: + if debug: print(f'{command} -- return-code [RC: {rc}] {next_hop} dev {conf_iface}') + else: + journal.send(f'ip route add {route} via {next_hop} dev {conf_iface} ' + f'metric {conf_metric} proto failover', SYSLOG_IDENTIFIER=my_name) + else: + if debug: print(f' [ TARGET_FAIL ] target checks fails for [{target}], do nothing') + journal.send(f'Check fail for route {route} target {target} proto {proto} ' + f'{port_opt}', SYSLOG_IDENTIFIER=my_name) + + # Route was added, check if the target is alive + # We should delete route if check fails only if route exists in the routing table + if not is_target_alive(target, conf_iface, proto, port, debug=debug, policy=policy) and \ + is_route_exists(route, next_hop, conf_iface, conf_metric): + if debug: + print(f'Nexh_hop {next_hop} fail, target not response') + print(f' [ DEL ] -- ip route del {route} via {next_hop} dev {conf_iface} ' + f'metric {conf_metric} proto failover [DELETE]') + rc_cmd(f'ip route del {route} via {next_hop} dev {conf_iface} metric {conf_metric} proto failover') + journal.send(f'ip route del {route} via {next_hop} dev {conf_iface} ' + f'metric {conf_metric} proto failover', SYSLOG_IDENTIFIER=my_name) + + time.sleep(int(timeout)) diff --git a/src/helpers/vyos-interface-rescan.py b/src/helpers/vyos-interface-rescan.py index 1ac1810e0..012357259 100755 --- a/src/helpers/vyos-interface-rescan.py +++ b/src/helpers/vyos-interface-rescan.py @@ -24,7 +24,7 @@ import netaddr from vyos.configtree import ConfigTree from vyos.defaults import directories -from vyos.util import get_cfg_group_id +from vyos.utils.permission import get_cfg_group_id debug = False diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index 14df2734b..8997705fe 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2023 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 @@ -20,11 +20,12 @@ import os import tempfile import vyos.defaults import vyos.remote + from vyos.config import Config from vyos.configtree import ConfigTree from vyos.migrator import Migrator, VirtualMigrator -from vyos.util import cmd, DEVNULL - +from vyos.utils.process import cmd +from vyos.utils.process import DEVNULL if (len(sys.argv) < 2): print("Need config file name to merge.") diff --git a/src/helpers/vyos-save-config.py b/src/helpers/vyos-save-config.py new file mode 100755 index 000000000..8af4a7916 --- /dev/null +++ b/src/helpers/vyos-save-config.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# +import os +import re +import sys +from tempfile import NamedTemporaryFile + +from vyos.config import Config +from vyos.remote import urlc +from vyos.component_version import system_footer +from vyos.defaults import directories + +DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot') +remote_save = None + +if len(sys.argv) > 1: + save_file = sys.argv[1] +else: + save_file = DEFAULT_CONFIG_PATH + +if re.match(r'\w+:/', save_file): + try: + remote_save = urlc(save_file) + except ValueError as e: + sys.exit(e) + +config = Config() +ct = config.get_config_tree(effective=True) + +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()) + +if remote_save is not None: + try: + remote_save.upload(write_file) + finally: + os.remove(write_file) diff --git a/src/helpers/vyos-sudo.py b/src/helpers/vyos-sudo.py index 3e4c196d9..75dd7f29d 100755 --- a/src/helpers/vyos-sudo.py +++ b/src/helpers/vyos-sudo.py @@ -18,7 +18,7 @@ import os import sys -from vyos.util import is_admin +from vyos.utils.permission import is_admin if __name__ == '__main__': diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py new file mode 100755 index 000000000..7cfa8fe88 --- /dev/null +++ b/src/helpers/vyos_config_sync.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 json +import requests +import urllib3 +import logging +from typing import Optional, List, Union, Dict, Any + +from vyos.config import Config +from vyos.template import bracketize_ipv6 + + +CONFIG_FILE = '/run/config_sync_conf.conf' + +# Logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +logger.name = os.path.basename(__file__) + +# API +API_HEADERS = {'Content-Type': 'application/json'} + + +def post_request(url: str, + data: str, + headers: Dict[str, str]) -> requests.Response: + """Sends a POST request to the specified URL + + Args: + url (str): The URL to send the POST request to. + data (Dict[str, Any]): The data to send with the POST request. + headers (Dict[str, str]): The headers to include with the POST request. + + Returns: + requests.Response: The response object representing the server's response to the request + """ + + response = requests.post(url, + data=data, + headers=headers, + verify=False, + timeout=timeout) + return response + + +def retrieve_config(section: str = None) -> Optional[Dict[str, Any]]: + """Retrieves the configuration from the local server. + + Args: + section: str: The section of the configuration to retrieve. Default is None. + + Returns: + Optional[Dict[str, Any]]: The retrieved configuration as a dictionary, or None if an error occurred. + """ + if section is None: + section = [] + else: + section = section.split() + + conf = Config() + config = conf.get_config_dict(section, get_first_key=True) + if config: + return config + return None + + +def set_remote_config( + address: str, + key: str, + op: str, + path: str = None, + section: Optional[str] = None) -> Optional[Dict[str, Any]]: + """Loads the VyOS configuration in JSON format to a remote host. + + Args: + address (str): The address of the remote host. + key (str): The key to use for loading the configuration. + path (Optional[str]): The path of the configuration. Default is None. + section (Optional[str]): The section of the configuration to load. Default is None. + + Returns: + Optional[Dict[str, Any]]: The response from the remote host as a dictionary, or None if an error occurred. + """ + + if path is None: + path = [] + else: + path = path.split() + headers = {'Content-Type': 'application/json'} + + # Disable the InsecureRequestWarning + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + url = f'https://{address}/configure-section' + data = json.dumps({ + 'op': mode, + 'path': path, + 'section': section, + 'key': key + }) + + try: + config = post_request(url, data, headers) + return config.json() + except requests.exceptions.RequestException as e: + print(f"An error occurred: {e}") + logger.error(f"An error occurred: {e}") + return None + + +def is_section_revised(section: str) -> bool: + from vyos.config_mgmt import is_node_revised + return is_node_revised([section]) + + +def config_sync(secondary_address: str, + secondary_key: str, + sections: List[str], + mode: str): + """Retrieve a config section from primary router in JSON format and send it to + secondary router + """ + # Config sync only if sections changed + if not any(map(is_section_revised, sections)): + return + + logger.info( + f"Config synchronization: Mode={mode}, Secondary={secondary_address}" + ) + + # Sync sections ("nat", "firewall", etc) + for section in sections: + config_json = retrieve_config(section=section) + # Check if config path deesn't exist, for example "set nat" + # we set empty value for config_json data + # As we cannot send to the remote host section "nat None" config + if not config_json: + config_json = "" + logger.debug( + f"Retrieved config for section '{section}': {config_json}") + set_config = set_remote_config(address=secondary_address, + key=secondary_key, + op=mode, + path=section, + section=config_json) + logger.debug(f"Set config for section '{section}': {set_config}") + + +if __name__ == '__main__': + # Read configuration from file + if not os.path.exists(CONFIG_FILE): + logger.error(f"Post-commit: No config file '{CONFIG_FILE}' exists") + exit(0) + + with open(CONFIG_FILE, 'r') as f: + config_data = f.read() + + config = json.loads(config_data) + + mode = config.get('mode') + secondary_address = config.get('secondary', {}).get('address') + secondary_address = bracketize_ipv6(secondary_address) + secondary_key = config.get('secondary', {}).get('key') + sections = config.get('section') + timeout = int(config.get('secondary', {}).get('timeout')) + + if not all([ + mode, secondary_address, secondary_key, sections + ]): + logger.error( + "Missing required configuration data for config synchronization.") + exit(0) + + config_sync(secondary_address, secondary_key, + sections, mode) diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name index 1798e92db..8c0992414 100755 --- a/src/helpers/vyos_net_name +++ b/src/helpers/vyos_net_name @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 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 @@ -13,8 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# import os import re @@ -26,7 +24,8 @@ from sys import argv from vyos.configtree import ConfigTree from vyos.defaults import directories -from vyos.util import cmd, boot_configuration_complete +from vyos.utils.process import cmd +from vyos.utils.boot import boot_configuration_complete from vyos.migrator import VirtualMigrator vyos_udev_dir = directories['vyos_udev_dir'] |