diff options
Diffstat (limited to 'src')
72 files changed, 606 insertions, 396 deletions
diff --git a/src/completion/list_dumpable_interfaces.py b/src/completion/list_dumpable_interfaces.py index f9748352f..15dae8f4e 100755 --- a/src/completion/list_dumpable_interfaces.py +++ b/src/completion/list_dumpable_interfaces.py @@ -4,9 +4,9 @@ import re -from vyos.utils.process import cmd +from vyos.utils.process import cmdl if __name__ == '__main__': - out = cmd('tcpdump -D').split('\n') + out = cmdl(['tcpdump', '-D']).split('\n') intfs = " ".join(map(lambda s: re.search(r'\d+\.(\S+)\s', s).group(1), out)) print(intfs) diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 6c5385dfa..91f44d2d8 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -36,7 +36,7 @@ from vyos.utils.cpu import get_core_count from vyos.utils.file import write_file from vyos.utils.dict import dict_search from vyos.utils.process import call -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import run from vyos.utils.network import gen_mac from vyos.utils.network import get_host_identity @@ -58,15 +58,15 @@ config_storage = '/etc/containers/storage.conf' systemd_unit_path = '/run/systemd/system' -def _cmd(command): +def _cmdl(command): if os.path.exists('/tmp/vyos.container.debug'): print(command) - return cmd(command) + return cmdl(command) def network_exists(name): # Check explicit name for network, returns True if network exists - c = _cmd(f'podman network ls --quiet --filter name=^{name}$') + c = _cmdl(['podman', 'network', 'ls', '--quiet', '--filter', f'name=^{name}$']) return bool(c) @@ -668,7 +668,7 @@ def apply(container): if 'disable' in container_config: # check if there is a container by that name running - tmp = _cmd('podman ps -a --format "{{.Names}}"') + tmp = _cmdl(['podman', 'ps', '-a', '--format', '{{.Names}}']) if name in tmp: file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') call(f'systemctl stop vyos-container-{name}.service') @@ -678,7 +678,7 @@ def apply(container): continue if 'container_restart' in container and name in container['container_restart']: - cmd(f'systemctl restart vyos-container-{name}.service') + cmdl(['systemctl', 'restart', f'vyos-container-{name}.service']) if disabled_new: call('systemctl daemon-reload') diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 29181b4ef..a1a7b8529 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -30,12 +30,13 @@ from vyos.ethtool import Ethtool from vyos.firewall import fqdn_config_parse from vyos.geoip import geoip_refresh, geoip_update from vyos.template import render +from vyos.utils.convert import human_to_seconds from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive from vyos.utils.file import write_file from vyos.utils.process import call -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import rc_cmd from vyos.utils.network import get_vrf_members from vyos.utils.network import get_interface_vrf @@ -543,6 +544,13 @@ def verify(firewall): for group_name, group in firewall['group']['remote_group'].items(): if 'url' not in group: raise ConfigError(f'remote-group {group_name} must have a url configured') + if 'interval' in group: + interval = human_to_seconds(group['interval']) + if not 60 <= interval <= 2419200: + raise ConfigError( + f'remote-group {group_name} interval must be ' + 'between 60 seconds and 4 weeks' + ) offload_chains_v4 = set() offload_chains_v6 = set() @@ -733,7 +741,7 @@ def apply(firewall): raise ConfigError(f'Failed to apply firewall: {output}') # Apply firewall global-options sysctl settings - cmd(f'sysctl -f {sysctl_file}') + cmdl(['sysctl', '-f', sysctl_file]) call_dependents() diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index ec5840369..6746863b9 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -63,7 +63,7 @@ from vyos.utils.kernel import check_kmod from vyos.utils.kernel import unload_kmod from vyos.utils.process import call from vyos.utils.permission import chown -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.network import is_addr_assigned from vyos.utils.network import interface_exists @@ -848,7 +848,7 @@ def apply(openvpn): # or if address will be assign later if 'local_host' in openvpn: if not is_addr_assigned(openvpn['local_host']): - cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1') + cmdl(['sysctl', '-w', 'net.ipv4.ip_nonlocal_bind=1']) # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py index 1ad16e816..5ac93ff8b 100755 --- a/src/conf_mode/interfaces_wireless.py +++ b/src/conf_mode/interfaces_wireless.py @@ -322,7 +322,8 @@ def apply(wifi): call(f'systemctl stop wpa_supplicant@{interface}.service') if 'deleted' in wifi: - WiFiIf(**wifi).remove() + if interface_exists(interface): + WiFiIf(**wifi).remove() # run the dependents and return call_dependents() diff --git a/src/conf_mode/interfaces_wwan.py b/src/conf_mode/interfaces_wwan.py index 0fe67508a..59303196d 100755 --- a/src/conf_mode/interfaces_wwan.py +++ b/src/conf_mode/interfaces_wwan.py @@ -33,7 +33,7 @@ from vyos.configverify import verify_mtu_ipv6 from vyos.ifconfig import WWANIf from vyos.utils.dict import dict_search from vyos.utils.network import is_wwan_connected -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import call from vyos.utils.process import DEVNULL from vyos.utils.process import is_systemd_service_active @@ -138,13 +138,13 @@ def apply(wwan): # required to serve all modems. Activate ModemManager on first invocation # of any WWAN interface. if not is_systemd_service_active(service_name): - cmd(f'systemctl start {service_name}') + cmdl(['systemctl', 'start', service_name]) counter = 100 # Wait until a modem is detected and then we can continue while counter > 0: counter -= 1 - tmp = cmd('mmcli -L') + tmp = cmdl(['mmcli', '-L']) if tmp != 'No modems were found': break sleep(0.250) @@ -169,7 +169,7 @@ def apply(wwan): # We are the last WWAN interface - there are no other WWAN interfaces # remaining, thus we can stop ModemManager and free resources. if 'other_interfaces' not in wwan: - cmd(f'systemctl stop {service_name}') + cmdl(['systemctl', 'stop', service_name]) # Clean CRON helper script which is used for to re-connect when # RF signal is lost if os.path.exists(cron_script): diff --git a/src/conf_mode/load-balancing_wan.py b/src/conf_mode/load-balancing_wan.py index 3f2433fa1..502872a13 100755 --- a/src/conf_mode/load-balancing_wan.py +++ b/src/conf_mode/load-balancing_wan.py @@ -19,7 +19,7 @@ from sys import exit from vyos.config import Config from vyos.configdep import set_dependents, call_dependents from vyos.utils.dict import dict_search_args -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos import ConfigError from vyos import airbag airbag.enable() @@ -150,9 +150,9 @@ def generate(lb): def apply(lb): if not lb: - cmd(f'systemctl stop {service}') + cmdl(['systemctl', 'stop', service]) else: - cmd(f'systemctl restart {service}') + cmdl(['systemctl', 'restart', service]) call_dependents() diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 8763da886..fe3359446 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -27,7 +27,7 @@ from vyos.utils.kernel import check_kmod from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.file import write_file -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import run from vyos.utils.process import call from vyos.utils.network import interface_exists @@ -241,8 +241,8 @@ def generate(nat): def apply(nat): check_kmod(k_mod) - cmd(f'nft --file {nftables_nat_config}') - cmd(f'nft --file {nftables_static_nat_conf}') + cmdl(['nft', '--file', nftables_nat_config]) + cmdl(['nft', '--file', nftables_static_nat_conf]) if not nat or 'deleted' in nat: os.unlink(nftables_nat_config) diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py index b7d0b586d..8afeeafe4 100755 --- a/src/conf_mode/nat64.py +++ b/src/conf_mode/nat64.py @@ -32,7 +32,7 @@ from vyos.utils.dict import dict_search from vyos.utils.file import write_file from vyos.utils.kernel import check_kmod from vyos.utils.kernel import unload_kmod -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import run from vyos.utils.system import sysctl_read @@ -63,7 +63,7 @@ def verify(nat64) -> None: base_rule = base + ['source', 'rule'] # Load in existing instances so we can destroy any unknown - lines = cmd('jool instance display --csv').splitlines() + lines = cmdl(['jool', 'instance', 'display', '--csv']).splitlines() for _, instance, _ in csv.reader(lines): match = INSTANCE_REGEX.fullmatch(instance) if not match: diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index c3637c6b9..999e320eb 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -25,7 +25,7 @@ from vyos.template import render from vyos.utils.dict import dict_search from vyos.utils.kernel import check_kmod from vyos.utils.network import interface_exists -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import run from vyos.template import is_ipv6 from vyos import ConfigError @@ -142,7 +142,7 @@ def generate(nat): def apply(nat): check_kmod(k_mod) - cmd(f'nft --file {nftables_nat66_config}') + cmdl(['nft', '--file', nftables_nat66_config]) if not nat or 'deleted' in nat: os.unlink(nftables_nat66_config) diff --git a/src/conf_mode/nat_cgnat.py b/src/conf_mode/nat_cgnat.py index 312688b53..ac5b10c55 100755 --- a/src/conf_mode/nat_cgnat.py +++ b/src/conf_mode/nat_cgnat.py @@ -25,7 +25,7 @@ from logging.handlers import SysLogHandler from vyos.config import Config from vyos.configdict import is_node_changed from vyos.template import render -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import run from vyos import ConfigError from vyos import airbag @@ -416,11 +416,11 @@ def generate(config): def apply(config): if 'deleted' in config: # Cleanup cgnat - cmd('nft delete table ip cgnat') + cmdl(['nft', 'delete', 'table', 'ip', 'cgnat']) if os.path.isfile(nftables_cgnat_config): os.unlink(nftables_cgnat_config) else: - cmd(f'nft --file {nftables_cgnat_config}') + cmdl(['nft', '--file', nftables_cgnat_config]) # Delete conntrack entries # if the pool configuration has changed diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 349abb6fc..fdc0a8fd3 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -48,7 +48,7 @@ from vyos.utils.dict import dict_set_nested from vyos.utils.file import read_file from vyos.utils.network import check_port_availability from vyos.utils.process import call -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import is_systemd_service_active from vyos.utils.process import is_systemd_service_running from vyos import ConfigError @@ -124,7 +124,8 @@ def certbot_delete(certificate): if not boot_configuration_complete(): return if os.path.exists(f'{vyos_certbot_dir}/renewal/{certificate}.conf'): - cmd(f'certbot delete --non-interactive --config-dir {vyos_certbot_dir} --cert-name {certificate}') + cmdl(['certbot', 'delete', '--non-interactive', '--config-dir', + vyos_certbot_dir, '--cert-name', certificate]) def certbot_request(name: str, config: dict, dry_run: bool=True) -> None: # We do not call certbot when booting the system - there is no need to do so and @@ -134,11 +135,14 @@ def certbot_request(name: str, config: dict, dry_run: bool=True) -> None: if not boot_configuration_complete(): return None - domains = '--domains ' + ' --domains '.join(config['domain_name']) - tmp = f'certbot certonly --non-interactive --config-dir {vyos_certbot_dir} --cert-name {name} '\ - f'--standalone --agree-tos --no-eff-email --expand --server {config["url"]} '\ - f'--email {config["email"]} --key-type rsa --rsa-key-size {config["rsa_key_size"]} '\ - f'{domains}' + tmp = ['certbot', 'certonly', '--non-interactive', '--config-dir', + vyos_certbot_dir, '--cert-name', name, + '--standalone', '--agree-tos', '--no-eff-email', '--expand', + '--server', config['url'], + '--email', config['email'], '--key-type', 'rsa', + '--rsa-key-size', str(config['rsa_key_size'])] + for domain in config['domain_name']: + tmp += ['--domains', domain] listen_address = None if 'listen_address' in config: @@ -149,21 +153,22 @@ def certbot_request(name: str, config: dict, dry_run: bool=True) -> None: if ('used_by' in config and 'haproxy' in config['used_by'] and is_systemd_service_running(systemd_services['haproxy']) and not check_port_availability(listen_address, 80)): - tmp += f' --http-01-address 127.0.0.1 --http-01-port {internal_ports["certbot_haproxy"]}' + tmp += ['--http-01-address', '127.0.0.1', '--http-01-port', + str(internal_ports["certbot_haproxy"])] elif listen_address: - tmp += f' --http-01-address {listen_address}' + tmp += ['--http-01-address', listen_address] # verify() does not need to actually request a cert but only test for plausibility if dry_run: - tmp += ' --dry-run' + tmp += ['--dry-run'] - cmd(tmp, raising=ConfigError, message=f'Certbot request failed for "{name}"!') + cmdl(tmp, raising=ConfigError, message=f'Certbot request failed for "{name}"!') return None def certbot_renew(config: dict, force: bool=False) -> None: """ Renew all certificates managed via certbot """ - tmp = f'certbot renew --no-random-sleep-on-renew ' \ - f'--config-dir {vyos_certbot_dir}' + tmp = ['certbot', 'renew', '--no-random-sleep-on-renew', + '--config-dir', vyos_certbot_dir] # Determine services using ACME based certificates pre_hook_services = [] @@ -178,18 +183,18 @@ def certbot_renew(config: dict, force: bool=False) -> None: for service in pre_hook_services: if service in systemd_services: stop_services.append(systemd_services[service]) - tmp += ' --pre-hook "systemctl stop ' + ' '.join(stop_services) + '"' + tmp += ['--pre-hook', 'systemctl stop ' + ' '.join(stop_services)] if force: - tmp += ' --force-renewal' + tmp += ['--force-renewal'] try: - print(cmd(tmp, raising=ConfigError, message=f'Certbot renew failed!')) + print(cmdl(tmp, raising=ConfigError, message=f'Certbot renew failed!')) except ConfigError as e: print(e) for service in stop_services: print(f'Restarting "{service}" with non-renewed certificate...') - cmd(f'systemctl restart {service}') + cmdl(['systemctl', 'restart', service]) return None def get_config(config=None): diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py index ca12ebe7b..1d0dbd5a9 100755 --- a/src/conf_mode/policy_route.py +++ b/src/conf_mode/policy_route.py @@ -28,7 +28,7 @@ from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive from vyos.utils.file import write_file from vyos.utils.process import call -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import run from vyos.utils.network import get_vrf_tableid from vyos.utils.network import interface_exists @@ -241,7 +241,7 @@ def update_domain_resolver(policy): def apply_table_marks(policy): for route in ['route', 'route6']: if route in policy: - cmd_str = 'ip' if route == 'route' else 'ip -6' + cmd_list = ['ip'] if route == 'route' else ['ip', '-6'] tables = [] for name, pol_conf in policy[route].items(): if 'rule' in pol_conf: @@ -265,11 +265,12 @@ def apply_table_marks(policy): continue tables.append(vrf_table_id) table_mark = mark_offset - vrf_table_id - cmd(f'{cmd_str} rule add pref {vrf_table_id} fwmark {table_mark} table {vrf_table_id}') + cmdl(cmd_list + ['rule', 'add', 'pref', str(vrf_table_id), + 'fwmark', str(table_mark), 'table', str(vrf_table_id)]) def cleanup_table_marks(): - for cmd_str in ['ip', 'ip -6']: - json_rules = cmd(f'{cmd_str} -j -N rule list') + for cmd_list in [['ip'], ['ip', '-6']]: + json_rules = cmdl(cmd_list + ['-j', '-N', 'rule', 'list']) rules = loads(json_rules) for rule in rules: if 'fwmark' not in rule or 'table' not in rule: @@ -279,7 +280,7 @@ def cleanup_table_marks(): if fwmark[:2] == '0x': fwmark = int(fwmark, 16) if (int(fwmark) == (mark_offset - table)): - cmd(f'{cmd_str} rule del fwmark {fwmark} table {table}') + cmdl(cmd_list + ['rule', 'del', 'fwmark', str(fwmark), 'table', str(table)]) def apply(policy): install_result = run(f'nft --file {nftables_conf}') diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index 1e6797c60..d1c002887 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -37,6 +37,7 @@ from vyos.qos import RateLimiter from vyos.qos import RoundRobin from vyos.qos import TrafficShaper from vyos.qos import TrafficShaperHFSC +from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive from vyos.utils.process import run from vyos import ConfigError @@ -351,13 +352,23 @@ def generate(qos): def apply_interface(qos, ifname): """ Clear and re-apply QoS for a single interface only. """ - run(f'tc qdisc del dev {ifname} parent ffff:') + interface_config = dict_search_args(qos, 'interface', ifname) + if not interface_config: + interface_config = {} + + # Only tear down the ingress qdisc if this interface actually has an + # ingress QoS policy bound. Otherwise this would remove an unrelated + # ingress redirect/mirror configuration (see set_mirror_redirect()) + # that call_dependents() just re-created, and it would never come + # back as only directions present in interface_config are re-applied + # below. + if 'ingress' in interface_config: + run(f'tc qdisc del dev {ifname} parent ffff:') run(f'tc qdisc del dev {ifname} root') - if not qos or 'interface' not in qos or ifname not in qos['interface']: + if not interface_config: return None - interface_config = qos['interface'][ifname] if not verify_interface_exists(qos, ifname, state_required=True, warning_only=True): # When shaper is bound to a dialup (e.g. PPPoE) interface it is # possible that it is yet not available when the QoS code runs. diff --git a/src/conf_mode/service_monitoring_prometheus.py b/src/conf_mode/service_monitoring_prometheus.py index 79b557329..2f1b3885e 100755 --- a/src/conf_mode/service_monitoring_prometheus.py +++ b/src/conf_mode/service_monitoring_prometheus.py @@ -67,6 +67,22 @@ def get_config(config=None): with_recursive_defaults=True, ) + if 'frr_exporter' in monitoring: + # Optional collectors to enable, translated from the CLI node name + # to the upstream frr_exporter collector flag + collector_flags = {'bgp_l2_vpn': 'bgpl2vpn', 'pim': 'pim'} + collector = monitoring['frr_exporter'].get('collector', {}) + monitoring['frr_exporter']['optional_collectors'] = [ + flag for node, flag in collector_flags.items() if node in collector + ] + # peer-description carries a default value (json) which is merged into + # the config dict by with_recursive_defaults - the BGP peer description + # collector option is opt-in, so drop the default if it was never set + if not conf.exists( + base + ['frr-exporter', 'collector', 'bgp', 'peer-description'] + ): + collector.get('bgp', {}).pop('peer_description', None) + tmp = is_node_changed(conf, base + ['node-exporter', 'vrf']) if tmp: monitoring.update({'node_exporter_restart_required': {}}) diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 2271f240f..9ce73eaf8 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -28,7 +28,7 @@ from vyos.ifconfig import Section from vyos.template import render from vyos.utils.process import call from vyos.utils.permission import chown -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos import ConfigError from vyos import airbag airbag.enable() @@ -42,7 +42,7 @@ systemd_override = '/run/systemd/system/telegraf.service.d/10-override.conf' def get_nft_filter_chains(): """ Get nft chains for table filter """ try: - nft = cmd('nft --json list table ip vyos_filter') + nft = cmdl(['nft', '--json', 'list', 'table', 'ip', 'vyos_filter']) except Exception: print('nft table ip vyos_filter not found') return [] diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py index e6710223a..559aebcc8 100755 --- a/src/conf_mode/system_conntrack.py +++ b/src/conf_mode/system_conntrack.py @@ -25,7 +25,7 @@ from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive from vyos.utils.file import write_file -from vyos.utils.process import cmd, call +from vyos.utils.process import cmdl, call from vyos.utils.process import rc_cmd from vyos.template import render from vyos import ConfigError @@ -230,8 +230,7 @@ def apply(conntrack): # Add modules before nftables uses them if add_modules: - module_str = ' '.join(add_modules) - cmd(f'modprobe -a {module_str}') + cmdl(['modprobe', '-a'] + add_modules) # Load new nftables ruleset install_result, output = rc_cmd(f'nft --file {nftables_ct_file}') @@ -240,8 +239,7 @@ def apply(conntrack): # Remove modules after nftables stops using them if rm_modules: - module_str = ' '.join(rm_modules) - cmd(f'rmmod {module_str}') + cmdl(['rmmod'] + rm_modules) try: call_dependents() @@ -252,7 +250,7 @@ def apply(conntrack): # We silently ignore all errors # See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080 - cmd(f'sysctl -f {sysctl_file}') + cmdl(['sysctl', '-f', sysctl_file]) if 'log' in conntrack: call(f'systemctl restart vyos-conntrack-logger.service') diff --git a/src/conf_mode/system_host-name.py b/src/conf_mode/system_host-name.py index 2267508af..49dd9958d 100755 --- a/src/conf_mode/system_host-name.py +++ b/src/conf_mode/system_host-name.py @@ -26,7 +26,7 @@ from vyos.configdict import leaf_node_changed from vyos.defaults import systemd_services from vyos.ifconfig import Section from vyos.template import is_ip -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import call from vyos.utils.process import process_named_running from vyos import ConfigError @@ -170,7 +170,7 @@ def apply(config): # rsyslog runs into a race condition at boot time with systemd # restart rsyslog only if the hostname changed. - hostname_old = cmd('hostnamectl --static') + hostname_old = cmdl(['hostnamectl', '--static']) call(f'hostnamectl set-hostname --static {hostname_new}') # Restart services that use the hostname diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py index fec4cfa6d..01c2af339 100755 --- a/src/conf_mode/system_login.py +++ b/src/conf_mode/system_login.py @@ -47,7 +47,7 @@ from vyos.utils.dict import dict_search from vyos.utils.file import move_recursive from vyos.utils.network import is_addr_assigned from vyos.utils.permission import chown -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import call from vyos.utils.process import run from vyos.utils.process import DEVNULL @@ -365,34 +365,36 @@ def apply(login): for user, user_config in login['user'].items(): # make new user using vyatta shell and make home directory (-m), # default group of 100 (users) - command = 'useradd --create-home --no-user-group ' + command_name = 'useradd' + command = ['useradd', '--create-home', '--no-user-group'] # check if user already exists: if user in get_local_users(): # update existing account - command = 'usermod' + command_name = 'usermod' + command = ['usermod'] # all accounts use /bin/vbash - command += ' --shell /bin/vbash' - # we need to use '' quotes when passing formatted data to the shell - # else it will not work as some data parts are lost in translation + command += ['--shell', '/bin/vbash'] + # no shell is involved when using cmdl(), so no quoting is required + # even if the values contain whitespace tmp = dict_search('authentication.encrypted_password', user_config) - if tmp: command += f" --password '{tmp}'" + if tmp: command += ['--password', str(tmp)] tmp = dict_search('full_name', user_config) - if tmp: command += f" --comment '{tmp}'" + if tmp: command += ['--comment', str(tmp)] home_directory = dict_search('home_directory', user_config) if not home_directory: home_directory = f'/home/{user}' - command += f" --home '{home_directory}'" + command += ['--home', str(home_directory)] if 'operator' not in user_config: - command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk,_kea,vpp' + command += ['--groups', 'frr,frrvty,vyattacfg,sudo,adm,dip,disk,_kea,vpp'] - command += f' {user}' + command += [user] try: - cmd(command) + cmdl(command) # we should not rely on the value stored in user_config['home_directory'], as a # crazy user will choose username root or any other system user which will fail. # @@ -424,7 +426,7 @@ def apply(login): # # More details: https://github.com/vyos/vyos-1x/pull/4678#pullrequestreview-3169648265 backup_directory = f"/var/.users_backups/{user}" - if command.startswith('useradd') and os.path.exists(backup_directory): + if command_name == 'useradd' and os.path.exists(backup_directory): move_recursive(backup_directory, home_dir) chown(home_dir, user=user, group='users', recursive=True) @@ -455,7 +457,7 @@ def apply(login): lock_unlock = '--unlock' if 'disable' in user_config: lock_unlock = '--lock' - cmd(f'usermod {lock_unlock} {user}') + cmdl(['usermod', lock_unlock, user]) if 'rm_users' in login: for user in login['rm_users']: @@ -497,27 +499,27 @@ def apply(login): raise ConfigError(f'Deleting user "{user}" raised exception: {e}') # Enable/disable RADIUS in PAM configuration - cmd('pam-auth-update --disable radius-mandatory radius-optional') + cmdl(['pam-auth-update', '--disable', 'radius-mandatory', 'radius-optional']) if 'radius' in login: if login['radius'].get('security_mode', '') == 'mandatory': pam_profile = 'radius-mandatory' else: pam_profile = 'radius-optional' - cmd(f'pam-auth-update --enable {pam_profile}') + cmdl(['pam-auth-update', '--enable', pam_profile]) # Enable/disable TACACS+ in PAM configuration - cmd('pam-auth-update --disable tacplus-mandatory tacplus-optional') + cmdl(['pam-auth-update', '--disable', 'tacplus-mandatory', 'tacplus-optional']) if 'tacacs' in login: if login['tacacs'].get('security_mode', '') == 'mandatory': pam_profile = 'tacplus-mandatory' else: pam_profile = 'tacplus-optional' - cmd(f'pam-auth-update --enable {pam_profile}') + cmdl(['pam-auth-update', '--enable', pam_profile]) # Enable/disable Google authenticator - cmd('pam-auth-update --disable mfa-google-authenticator') + cmdl(['pam-auth-update', '--disable', 'mfa-google-authenticator']) if enable_otp: - cmd('pam-auth-update --enable mfa-google-authenticator') + cmdl(['pam-auth-update', '--enable', 'mfa-google-authenticator']) call_dependents() return None diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index 2ea6bfd06..8951adcff 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import platform import psutil import re @@ -36,7 +37,7 @@ from vyos.utils.dict import dict_search from vyos.utils.file import write_file from vyos.utils.file import read_file from vyos.utils.kernel import check_kmod -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import is_systemd_service_running from vyos.utils.network import is_addr_assigned from vyos.utils.network import is_intf_addr_assigned @@ -51,7 +52,14 @@ from vyos.vpp.config_resource_checks.resource_defaults import default_resource_m airbag.enable() +arch_map = { + "x86_64": "x86_64-linux-gnu", + "aarch64": "aarch64-linux-gnu", +} +arch = arch_map.get(platform.machine()) curlrc_config = r'/etc/curlrc' +openssl_config = '/etc/ssl/openssl.cnf' +openssl_fipsmodule_config = '/run/ssl/fipsmodule.cnf' ssh_config = r'/etc/ssh/ssh_config.d/91-vyos-ssh-client-options.conf' systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target' usb_autosuspend = r'/etc/udev/rules.d/40-usb-autosuspend.rules' @@ -203,6 +211,11 @@ def get_config(config=None): def verify(options): + if 'fips' in options: + fips_module = f'/usr/lib/{arch}/ossl-modules/fips.so' + if not os.path.exists(fips_module): + raise ConfigError(f'OpenSSL FIPS module not found: {fips_module}') + if 'http_client' in options: config = options['http_client'] if 'source_interface' in config: @@ -305,7 +318,19 @@ def verify(options): def generate(options): + + if 'fips' in options: + # ensure directory exists + os.makedirs(os.path.dirname(openssl_fipsmodule_config), exist_ok=True) + cmdl(['openssl', 'fipsinstall', '-module', + f'/usr/lib/{arch}/ossl-modules/fips.so', + '-out', openssl_fipsmodule_config]) + else: + if os.path.exists(openssl_fipsmodule_config): + os.remove(openssl_fipsmodule_config) + render(curlrc_config, 'system/curlrc.j2', options) + render(openssl_config, 'openssl/openssl.cnf.j2', options) render(ssh_config, 'system/ssh_config.j2', options) render(usb_autosuspend, 'system/40_usb_autosuspend.j2', options) @@ -484,12 +509,12 @@ def apply(options): kexec_required, cmdline_new = generate_cmdline_for_kexec(options) if kexec_required: if not boot_configuration_complete() and os.getenv('VYOS_CONFIGD'): - cmd( - 'kexec -l /boot/vmlinuz --initrd=/boot/initrd.img ' - f'--command-line="{cmdline_new}" --kexec-file-syscall' - ) + cmdl([ + 'kexec', '-l', '/boot/vmlinuz', '--initrd=/boot/initrd.img', + f'--command-line={cmdline_new}', '--kexec-file-syscall', + ]) os.sync() - cmd('systemctl kexec') + cmdl(['systemctl', 'kexec']) elif boot_configuration_complete(): Warning( 'Kernel configuration options have changed. ' @@ -499,9 +524,9 @@ def apply(options): # System bootup beep beep_service = 'vyos-beep.service' if 'startup_beep' in options: - cmd(f'systemctl enable {beep_service}') + cmdl(['systemctl', 'enable', beep_service]) else: - cmd(f'systemctl disable {beep_service}') + cmdl(['systemctl', 'disable', beep_service]) # Ctrl-Alt-Delete action if os.path.exists(systemd_action_file): @@ -512,6 +537,10 @@ def apply(options): elif options['ctrl_alt_delete'] == 'poweroff': os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file) + # FIPS + if 'fips' in options: + Warning('FIPS 140-3 support is incomplete and may behave unexpectedly!') + # Configure HTTP client if 'http_client' not in options: if os.path.exists(curlrc_config): @@ -531,37 +560,35 @@ def apply(options): # tuned - performance tuning if 'performance' in options: - cmd('systemctl restart tuned.service') + cmdl(['systemctl', 'restart', 'tuned.service']) # wait until daemon has started before sending configuration while not is_systemd_service_running('tuned.service'): sleep(0.250) - performance = ' '.join( - list(tuned_profiles[profile] for profile in options['performance']) - ) - cmd(f'tuned-adm profile {performance}') + performance = [tuned_profiles[profile] for profile in options['performance']] + cmdl(['tuned-adm', 'profile'] + performance) else: - cmd('systemctl stop tuned.service') + cmdl(['systemctl', 'stop', 'tuned.service']) call_dependents() # Keyboard layout - there will be always the default key inside the dict # but we check for key existence anyway if 'keyboard_layout' in options: - cmd('loadkeys {keyboard_layout}'.format(**options)) + cmdl(['loadkeys', options['keyboard_layout']]) # Enable/diable root-partition-auto-resize SystemD service if 'root_partition_auto_resize' in options: - cmd('systemctl enable root-partition-auto-resize.service') + cmdl(['systemctl', 'enable', 'root-partition-auto-resize.service']) else: - cmd('systemctl disable root-partition-auto-resize.service') + cmdl(['systemctl', 'disable', 'root-partition-auto-resize.service']) # Time format 12|24-hour if 'time_format' in options: time_format = time_format_to_locale.get(options['time_format']) - cmd(f'localectl set-locale LC_TIME={time_format}') + cmdl(['localectl', 'set-locale', f'LC_TIME={time_format}']) # Reload UDEV, required for USB auto suspend - cmd('udevadm control --reload-rules') + cmdl(['udevadm', 'control', '--reload-rules']) # Enable/disable dynamic debugging for kernel modules modules = ['wireguard'] diff --git a/src/conf_mode/system_sysctl.py b/src/conf_mode/system_sysctl.py index 8e018ec0b..d9b54b551 100755 --- a/src/conf_mode/system_sysctl.py +++ b/src/conf_mode/system_sysctl.py @@ -20,7 +20,7 @@ from sys import exit from vyos.config import Config from vyos.template import render -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos import ConfigError from vyos import airbag airbag.enable() @@ -59,7 +59,7 @@ def apply(sysctl): # We silently ignore all errors # See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080 - cmd(f'sysctl -f {config_file}') + cmdl(['sysctl', '-f', config_file]) return None if __name__ == '__main__': diff --git a/src/conf_mode/system_watchdog.py b/src/conf_mode/system_watchdog.py index 8c050f333..a2b70262f 100755 --- a/src/conf_mode/system_watchdog.py +++ b/src/conf_mode/system_watchdog.py @@ -22,7 +22,7 @@ from vyos.config import Config from vyos.base import Warning from vyos.template import render from vyos.utils.kernel import load_module -from vyos.utils.process import call, cmd +from vyos.utils.process import call, cmdl from vyos import ConfigError from vyos import airbag @@ -89,7 +89,7 @@ def _verify_watchdog_module(module: str) -> None: # Ensure the module looks like a watchdog driver and not an arbitrary module. # Use modinfo filename location as the heuristic. - filename = cmd(['modinfo', '-F', 'filename', module], raising=ConfigError) + filename = cmdl(['modinfo', '-F', 'filename', module], raising=ConfigError) filename_l = filename.strip().lower() # Accept modules located under drivers/watchdog, plus explicit exception for diff --git a/src/conf_mode/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py index 2f7b59354..c1046fdca 100644 --- a/src/conf_mode/vpp_interfaces_loopback.py +++ b/src/conf_mode/vpp_interfaces_loopback.py @@ -24,6 +24,8 @@ from vyos.configdep import set_dependents, call_dependents from vyos.utils.process import is_systemd_service_active from vyos.ifconfig.vpp import VPPLoopbackInterface +from vyos.vpp.config_deps import deps_bridge_dict +from vyos.vpp.config_verify import verify_vpp_remove_bridge_interface def get_config(config=None) -> dict: @@ -55,6 +57,12 @@ def get_config(config=None) -> dict: no_tag_node_value_mangle=True, ) + # Bridge dependency - reattach as BVI after this loopback is recreated + config['bridge_members'] = deps_bridge_dict(conf) + if ifname in config['bridge_members']: + for bridge_iface in config['bridge_members'][ifname]: + set_dependents('vpp_interfaces_bridge', conf, bridge_iface) + # NAT dependency if conf.exists(['vpp', 'nat', 'nat44']): set_dependents('vpp_nat_nat44', conf) @@ -73,6 +81,8 @@ def verify(config): if 'remove_vpp' in config: return None + verify_vpp_remove_bridge_interface(config) + if not is_systemd_service_active('vpp.service'): raise ConfigError( 'Cannot configure VPP loopback interface: vpp.service is not running' diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index e0dc43283..b5b9552e2 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -36,7 +36,7 @@ from vyos.utils.network import get_vrf_tableid from vyos.utils.network import get_vrf_members from vyos.utils.network import interface_exists from vyos.utils.process import call -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import is_systemd_service_running from vyos.utils.process import popen from vyos.utils.system import sysctl_write @@ -65,8 +65,8 @@ def has_rule(af : str, priority : int, table : str=None): """ if af not in ['-4', '-6']: raise ValueError() - command = f'ip --detail --json {af} rule show' - for tmp in loads(cmd(command)): + command = ['ip', '--detail', '--json', af, 'rule', 'show'] + for tmp in loads(cmdl(command)): if 'priority' in tmp and 'table' in tmp: if tmp['priority'] == priority and tmp['table'] == table: return True @@ -80,7 +80,7 @@ def is_nft_vrf_zone_rule_setup() -> bool: """ Check if an nftables connection tracking rule already exists """ - tmp = loads(cmd('sudo nft -j list table inet vrf_zones')) + tmp = loads(cmdl(['nft', '-j', 'list', 'table', 'inet', 'vrf_zones'], sudo=True)) num_rules = len(search("nftables[].rule[].chain", tmp)) return bool(num_rules) @@ -259,12 +259,13 @@ def apply(vrf): vrf_iface.set_dhcpv6(False) # Remove nftables conntrack zone map item - nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ \'"{tmp}"\' }}' + nft_del_element = ['delete', 'element', 'inet', 'vrf_zones', 'ct_iface_map', + '{', f'"{tmp}"', '}'] # Check if deleting is possible first to avoid raising errors - _, err = popen(f'nft --check {nft_del_element}') + _, err = popen(f'nft --check {" ".join(nft_del_element)}') if not err: # Remove map element - cmd(f'nft {nft_del_element}') + cmdl(['nft'] + nft_del_element) # Remove WireGuard fwmark routing rules created for this VRF table table_id = get_vrf_tableid(tmp) @@ -345,8 +346,9 @@ def apply(vrf): state = 'down' if 'disable' in config else 'up' vrf_if.set_admin_state(state) # Add nftables conntrack zone map item - nft_add_element = f'add element inet vrf_zones ct_iface_map {{ \'"{name}"\' : {table} }}' - cmd(f'nft {nft_add_element}') + nft_add_element = ['add', 'element', 'inet', 'vrf_zones', 'ct_iface_map', + '{', f'"{name}"', ':', str(table), '}'] + cmdl(['nft'] + nft_add_element) # Only call into nftables as long as there is nothing setup to avoid wasting # CPU time and thus lengthen the commit process @@ -355,11 +357,11 @@ def apply(vrf): # Install nftables conntrack rules only once if vrf['conntrack'] and not nft_vrf_zone_rule_setup: for chain, rule in nftables_rules.items(): - cmd(f'nft add rule inet vrf_zones {chain} {rule}') + cmdl(f'nft add rule inet vrf_zones {chain} {rule}'.split()) if 'name' not in vrf or not vrf['conntrack']: for chain, rule in nftables_rules.items(): - cmd(f'nft flush chain inet vrf_zones {chain}') + cmdl(f'nft flush chain inet vrf_zones {chain}'.split()) # Return default ip rule values if 'name' not in vrf: diff --git a/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py index bb7515a90..31c94d66b 100755 --- a/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py +++ b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py @@ -4,7 +4,7 @@ import json import re import time -from vyos.utils.process import cmd +from vyos.utils.process import cmdl def get_nft_filter_chains(): @@ -12,7 +12,7 @@ def get_nft_filter_chains(): Get list of nft chains for table filter """ try: - nft = cmd('/usr/sbin/nft --json list table ip vyos_filter') + nft = cmdl(['/usr/sbin/nft', '--json', 'list', 'table', 'ip', 'vyos_filter']) except Exception: return [] nft = json.loads(nft) @@ -30,9 +30,9 @@ def get_nftables_details(name): """ Get dict, counters packets and bytes for chain """ - command = f'/usr/sbin/nft list chain ip vyos_filter {name}' + command = ['/usr/sbin/nft', 'list', 'chain', 'ip', 'vyos_filter', name] try: - results = cmd(command) + results = cmdl(command) except: return {} diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py index 9826a6d3e..a3a66eedc 100755 --- a/src/helpers/vyos-boot-config-loader.py +++ b/src/helpers/vyos-boot-config-loader.py @@ -26,7 +26,7 @@ from vyos.defaults import config_status from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.configtree import ConfigTree -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.file import write_file STATUS_FILE = config_status @@ -103,8 +103,9 @@ def failsafe(config_file_name): 'authentication', 'encrypted-password']) - cmd(f"useradd --create-home --no-user-group --shell /bin/vbash --password '{passwd}' "\ - "--groups frr,frrvty,vyattacfg,sudo,adm,dip,disk vyos") + cmdl(['useradd', '--create-home', '--no-user-group', '--shell', '/bin/vbash', + '--password', passwd, + '--groups', 'frr,frrvty,vyattacfg,sudo,adm,dip,disk', 'vyos']) if __name__ == '__main__': if len(sys.argv) < 2: diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index 876a835ec..14e8d2243 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -25,7 +25,7 @@ from tempfile import NamedTemporaryFile, TemporaryDirectory from vyos.system.image import is_live_boot, get_running_image from vyos.tpm import clear_tpm_key, read_tpm_key, write_tpm_key from vyos.utils.io import ask_input, ask_yes_no -from vyos.utils.process import cmd, run +from vyos.utils.process import cmdl, run from vyos.defaults import directories persistpath_cmd = '/opt/vyatta/sbin/vyos-persistpath' @@ -42,7 +42,7 @@ def load_config(key): if not key: return - persist_path = cmd(persistpath_cmd).strip() + persist_path = cmdl([persistpath_cmd]).strip() image_name = get_running_image() image_path = os.path.join(persist_path, 'luks', image_name) @@ -54,11 +54,11 @@ def load_config(key): f.write(key) key_file = f.name - cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}') + cmdl(['cryptsetup', '-q', 'open', image_path, 'vyos_config', f'--key-file={key_file}']) run(f'umount -l {mount_path}') - cmd(f'mount /dev/mapper/vyos_config {mount_path}') - cmd(f'chgrp -R vyattacfg {mount_path}') + cmdl(['mount', '/dev/mapper/vyos_config', mount_path]) + cmdl(['chgrp', '-R', 'vyattacfg', mount_path]) os.unlink(key_file) @@ -73,7 +73,7 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): pass write_tpm_key(key) - persist_path = cmd(persistpath_cmd).strip() + persist_path = cmdl([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') @@ -86,7 +86,7 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): try: # Create file for encrypted config - cmd(f'fallocate -l {size}M {image_path}') + cmdl(['fallocate', '-l', f'{size}M', image_path]) # Write TPM key for slot #1 with NamedTemporaryFile(dir='/dev/shm', delete=False) as f: @@ -94,7 +94,7 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): key_file = f.name # Format and add main key to volume - cmd(f'cryptsetup -q luksFormat {image_path} {key_file}') + cmdl(['cryptsetup', '-q', 'luksFormat', image_path, key_file]) if recovery_key: # Write recovery key for slot 2 @@ -102,11 +102,11 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): f.write(recovery_key) recovery_key_file = f.name - cmd(f'cryptsetup -q luksAddKey {image_path} {recovery_key_file} --key-file={key_file}') + cmdl(['cryptsetup', '-q', 'luksAddKey', image_path, recovery_key_file, f'--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') + cmdl(['cryptsetup', '-q', 'open', image_path, 'vyos_config', f'--key-file={key_file}']) + cmdl(['mkfs.ext4', '/dev/mapper/vyos_config']) except Exception as e: print('An error occurred while creating the encrypted config volume, aborting.') @@ -119,14 +119,15 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): raise e with TemporaryDirectory() as d: - cmd(f'mount /dev/mapper/vyos_config {d}') + cmdl(['mount', '/dev/mapper/vyos_config', d]) # Move mount_path to encrypted volume shutil.copytree( mount_path, d, symlinks=True, copy_function=shutil.move, dirs_exist_ok=True ) - cmd(f'chgrp -R vyattacfg {d}') - cmd(f'umount {d}') + shutil.copytree(mount_path, d, copy_function=shutil.move, dirs_exist_ok=True) + cmdl(['chgrp', '-R', 'vyattacfg', d]) + cmdl(['umount', d]) os.unlink(key_file) @@ -134,8 +135,8 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): os.unlink(recovery_key_file) run(f'umount -l {mount_path}') - cmd(f'mount /dev/mapper/vyos_config {mount_path}') - cmd(f'chgrp vyattacfg {mount_path}') + cmdl(['mount', '/dev/mapper/vyos_config', mount_path]) + cmdl(['chgrp', 'vyattacfg', mount_path]) return True @@ -153,7 +154,7 @@ def test_decrypt(key): if not key: return - persist_path = cmd(persistpath_cmd).strip() + persist_path = cmdl([persistpath_cmd]).strip() image_name = get_running_image() image_path = os.path.join(persist_path, 'luks', image_name) @@ -165,7 +166,7 @@ def test_decrypt(key): key_file = f.name try: - cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}') + cmdl(['cryptsetup', '-q', 'open', image_path, 'vyos_config', f'--key-file={key_file}']) os.unlink(key_file) return True except: @@ -176,7 +177,7 @@ def decrypt_config(key): if not key: return - persist_path = cmd(persistpath_cmd).strip() + persist_path = cmdl([persistpath_cmd]).strip() image_name = get_running_image() image_path = os.path.join(persist_path, 'luks', image_name) original_config_path = os.path.join(persist_path, 'boot', image_name, 'rw', 'opt', 'vyatta', 'etc', 'config') @@ -188,7 +189,7 @@ def decrypt_config(key): f.write(key) key_file = f.name - cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}') + cmdl(['cryptsetup', '-q', 'open', image_path, 'vyos_config', f'--key-file={key_file}']) # unmount encrypted volume mount points run(f'umount -Alq /dev/mapper/vyos_config') @@ -202,22 +203,21 @@ def decrypt_config(key): # Mount original persistence config path if not os.path.exists(mount_path): os.mkdir(mount_path) - cmd(f'mount --bind {original_config_path} {mount_path}') + cmdl(['mount', '--bind', original_config_path, mount_path]) # Temporarily mount encrypted volume and migrate files to /config on rootfs with TemporaryDirectory() as d: - cmd(f'mount /dev/mapper/vyos_config {d}') + cmdl(['mount', '/dev/mapper/vyos_config', d]) # Move encrypted volume to /opt/vyatta/etc/config shutil.copytree( d, mount_path, symlinks=True, copy_function=shutil.move, dirs_exist_ok=True ) - cmd(f'chgrp -R vyattacfg {mount_path}') - - cmd(f'umount {d}') + cmdl(['chgrp', '-R', 'vyattacfg', mount_path]) + cmdl(['umount', d]) # Close encrypted volume - cmd('cryptsetup -q close vyos_config') + cmdl(['cryptsetup', '-q', 'close', 'vyos_config']) # Remove encrypted volume image file and key if key_file: @@ -248,7 +248,7 @@ if __name__ == '__main__': args = parser.parse_args() if args.disable or args.load: - persist_path = cmd(persistpath_cmd).strip() + persist_path = cmdl([persistpath_cmd]).strip() image_name = get_running_image() image_path = os.path.join(persist_path, 'luks', image_name) diff --git a/src/migration-scripts/https/5-to-6 b/src/migration-scripts/https/5-to-6 index 8c41f4b61..ece407a4b 100644 --- a/src/migration-scripts/https/5-to-6 +++ b/src/migration-scripts/https/5-to-6 @@ -21,7 +21,7 @@ import os from vyos.configtree import ConfigTree from vyos.defaults import directories -from vyos.utils.process import cmd +from vyos.utils.process import cmdl vyos_certbot_dir = directories['certbot'] @@ -45,7 +45,7 @@ def migrate(config: ConfigTree) -> None: # This must be run as root certbot_live = f'{vyos_certbot_dir}/live/' # we need the trailing / if os.path.exists(certbot_live): - tmp = cmd(f'sudo find {certbot_live} -maxdepth 1 -type d') + tmp = cmdl(['find', certbot_live, '-maxdepth', '1', '-type', 'd'], sudo=True) tmp = tmp.split() # tmp = ['/config/auth/letsencrypt/live', '/config/auth/letsencrypt/live/router.vyos.net'] tmp.remove(certbot_live) cert_name = tmp[0].replace(certbot_live, '') diff --git a/src/migration-scripts/system/13-to-14 b/src/migration-scripts/system/13-to-14 index 3632b4b6a..0a0d4ef67 100644 --- a/src/migration-scripts/system/13-to-14 +++ b/src/migration-scripts/system/13-to-14 @@ -24,7 +24,7 @@ import re from vyos.configtree import ConfigTree -from vyos.utils.process import cmd +from vyos.utils.process import cmdl tz_base = ['system', 'time-zone'] @@ -38,7 +38,7 @@ def migrate(config: ConfigTree) -> None: # retrieve all valid timezones try: - tz_data_raw = cmd('timedatectl list-timezones') + tz_data_raw = cmdl(['timedatectl', 'list-timezones']) except OSError: tz_data_raw = '' tz_data = tz_data_raw.split('\n') diff --git a/src/op_mode/bgp.py b/src/op_mode/bgp.py index 7f0815433..a3b2fc6d0 100755 --- a/src/op_mode/bgp.py +++ b/src/op_mode/bgp.py @@ -81,7 +81,7 @@ ArgFamily = typing.Literal['inet', 'inet6', 'l2vpn'] ArgFamilyModifier = typing.Literal['unicast', 'labeled_unicast', 'multicast', 'vpn', 'flowspec'] def reset(command: str): - from vyos.utils.process import cmd + from vyos.utils.process import cmdl tokens = command.split() @@ -101,35 +101,35 @@ def reset(command: str): tokens[index] = "*" command = " ".join(tokens) - cmd(f'vtysh -c "{command}"') + cmdl(['vtysh', '-c', command]) def show_summary(raw: bool): - from vyos.utils.process import cmd + from vyos.utils.process import cmdl if raw: from json import loads - output = cmd(f"vtysh -c 'show bgp summary json'").strip() + output = cmdl(['vtysh', '-c', 'show bgp summary json']).strip() # FRR 8.5 correctly returns an empty object when BGP is not running, # we don't need to do anything special here return loads(output) else: - output = cmd(f"vtysh -c 'show bgp summary'") + output = cmdl(['vtysh', '-c', 'show bgp summary']) return output def show_neighbors(raw: bool): - from vyos.utils.process import cmd + from vyos.utils.process import cmdl from vyos.utils.dict import dict_to_list if raw: from json import loads - output = cmd(f"vtysh -c 'show bgp neighbors json'").strip() + output = cmdl(['vtysh', '-c', 'show bgp neighbors json']).strip() d = loads(output) return dict_to_list(d, save_key_to="neighbor") else: - output = cmd(f"vtysh -c 'show bgp neighbors'") + output = cmdl(['vtysh', '-c', 'show bgp neighbors']) return output def show(raw: bool, @@ -152,8 +152,8 @@ def show(raw: bool, frr_command = frr_command_template.render(kwargs) frr_command = re.sub(r'\s+', ' ', frr_command) - from vyos.utils.process import cmd - output = cmd(f"vtysh -c '{frr_command}'") + from vyos.utils.process import cmdl + output = cmdl(['vtysh', '-c', frr_command]) if raw: from json import loads diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py index 9056e16d4..24ab261d1 100755 --- a/src/op_mode/bridge.py +++ b/src/op_mode/bridge.py @@ -21,7 +21,7 @@ import typing from tabulate import tabulate -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import rc_cmd from vyos.utils.process import call @@ -32,7 +32,7 @@ def _get_json_data(): """ Get bridge data format JSON """ - return cmd(f'bridge --json link show') + return cmdl(['bridge', '--json', 'link', 'show']) def _get_raw_data_summary(): @@ -51,7 +51,7 @@ def _get_raw_data_vlan(tunnel: bool = False): show = 'show' if tunnel: show = 'tunnel' - json_data = cmd(f'bridge --json --compressvlans vlan {show}') + json_data = cmdl(['bridge', '--json', '--compressvlans', 'vlan', show]) data_dict = json.loads(json_data) return data_dict @@ -86,7 +86,7 @@ def _get_raw_data_mdb(bridge): """Get MAC-address multicast group for the bridge brX :return list """ - json_data = cmd(f'bridge --json mdb show br {bridge}') + json_data = cmdl(['bridge', '--json', 'mdb', 'show', 'br', bridge]) data_dict = json.loads(json_data) return data_dict @@ -235,13 +235,13 @@ def _get_bridge_detail_nexthop_group(iface): def _get_bridge_detail_nexthop_group_raw(iface): - out = cmd(f'vtysh -c "show interface {iface} nexthop-group"') + out = cmdl(['vtysh', '-c', f'show interface {iface} nexthop-group']) return out def _get_bridge_detail_raw(iface): """Get interface detail json statistics""" - data = cmd(f'vtysh -c "show interface {iface} json"') + data = cmdl(['vtysh', '-c', f'show interface {iface} json']) data_dict = json.loads(data) return data_dict diff --git a/src/op_mode/cgnat.py b/src/op_mode/cgnat.py index d53f6158b..e877e98ff 100755 --- a/src/op_mode/cgnat.py +++ b/src/op_mode/cgnat.py @@ -23,14 +23,14 @@ from tabulate import tabulate import vyos.opmode from vyos.configquery import ConfigTreeQuery -from vyos.utils.process import cmd +from vyos.utils.process import cmdl CGNAT_TABLE = 'cgnat' def _get_raw_data(external_address: str = '', internal_address: str = '') -> list[dict]: """Get CGNAT dictionary and filter by external or internal address if provided.""" - cmd_output = cmd(f'nft --json list table ip {CGNAT_TABLE}') + cmd_output = cmdl(['nft', '--json', 'list', 'table', 'ip', CGNAT_TABLE]) data = json.loads(cmd_output) elements = data['nftables'][2]['map']['elem'] diff --git a/src/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py index 2a4f19607..19d1f77d8 100755 --- a/src/op_mode/clear_conntrack.py +++ b/src/op_mode/clear_conntrack.py @@ -17,11 +17,11 @@ import sys from vyos.utils.io import ask_yes_no -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import DEVNULL if not ask_yes_no("This will clear all currently tracked and expected connections. Continue?"): sys.exit(1) else: - cmd('/usr/sbin/conntrack -F', stderr=DEVNULL) - cmd('/usr/sbin/conntrack -F expect', stderr=DEVNULL) + cmdl(['/usr/sbin/conntrack', '-F'], stderr=DEVNULL) + cmdl(['/usr/sbin/conntrack', '-F', 'expect'], stderr=DEVNULL) diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py index 301d3cfb8..b6f2d944e 100755 --- a/src/op_mode/conntrack.py +++ b/src/op_mode/conntrack.py @@ -19,7 +19,7 @@ import typing import xmltodict from tabulate import tabulate -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.network import get_vrf_tableid import vyos.opmode @@ -34,7 +34,7 @@ def _get_xml_data(family, orig_zone=None): if orig_zone is not None: args.extend(['--orig-zone', str(orig_zone)]) - return cmd(['sudo', 'conntrack'] + args) + return cmdl(['conntrack'] + args, sudo=True) def _xml_to_dict(xml): @@ -67,7 +67,7 @@ def _get_raw_data(family, orig_zone=None): def _get_raw_statistics(): entries = [] - data = cmd('sudo conntrack --stats') + data = cmdl(['conntrack', '--stats'], sudo=True) data = data.replace(' \t', '').split('\n') for entry in data: entries.append(entry.split()) diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py index 0da5b3b0b..85847eda3 100755 --- a/src/op_mode/conntrack_sync.py +++ b/src/op_mode/conntrack_sync.py @@ -27,7 +27,7 @@ from vyos.configquery import CliShellApiConfigQuery from vyos.configquery import ConfigTreeQuery from vyos.utils.commit import commit_in_progress from vyos.utils.process import call -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import run conntrackd_bin = '/usr/sbin/conntrackd' @@ -99,7 +99,7 @@ def restart(): raise vyos.opmode.CommitInProgress('Cannot restart conntrackd while a commit is in progress') syslog.syslog('Restarting conntrack sync service...') - cmd('systemctl restart conntrackd.service') + cmdl(['systemctl', 'restart', 'conntrackd.service']) # request resynchronization with other systems request_sync() # send bulk update of internal-cache to other systems @@ -130,7 +130,7 @@ def reset_internal_cache(): def _show_cache(raw, opts): is_configured() - out = cmd(f'{conntrackd_bin} -C {conntrackd_config} {opts} -x') + out = cmdl([conntrackd_bin, '-C', conntrackd_config] + opts.split() + ['-x']) return from_xml(raw, out) def show_external_cache(raw: bool): diff --git a/src/op_mode/container.py b/src/op_mode/container.py index 81dc35098..0948a0d84 100755 --- a/src/op_mode/container.py +++ b/src/op_mode/container.py @@ -22,7 +22,7 @@ import subprocess from pathlib import Path from vyos.defaults import directories -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import rc_cmd from vyos.utils.process import run import vyos.opmode @@ -56,8 +56,8 @@ def clean_layer(name: str) -> int: purge_layer_by_id(layer_id) # Reinitiate the container's overlay layer - cmd(f"rm -f /run/{unit}.cid /run/{unit}.pid") - cmd(f"systemctl reset-failed {unit}") + cmdl(['rm', '-f', f'/run/{unit}.cid', f'/run/{unit}.pid']) + cmdl(['systemctl', 'reset-failed', unit]) result = run(f"systemctl start {unit}") return result @@ -65,7 +65,7 @@ def _get_json_data(command: str) -> list: """ Get container command format JSON """ - return cmd(f'{command} --format json') + return cmdl(command.split() + ['--format', 'json']) def _get_raw_data(command: str) -> list: json_data = _get_json_data(command) @@ -106,7 +106,7 @@ def delete_image(name: str, force: typing.Optional[bool] = False): if name == 'all': # gather list of all images and pass them to the removal list - name = cmd('sudo podman image ls --quiet') + name = cmdl(['podman', 'image', 'ls', '--quiet'], sudo=True) # If there are no container images left, we cannot delete them all if not name: return # replace newline with whitespace @@ -145,7 +145,7 @@ def show_container(raw: bool): if raw: return container_data else: - return cmd(command) + return cmdl(command.split()) def show_image(raw: bool): command = 'podman image ls' @@ -153,7 +153,7 @@ def show_image(raw: bool): if raw: return container_data else: - return cmd(command) + return cmdl(command.split()) def show_network(raw: bool): command = 'podman network ls' @@ -161,7 +161,7 @@ def show_network(raw: bool): if raw: return container_data else: - return cmd(command) + return cmdl(command.split()) def restart(name: str): from vyos.utils.process import rc_cmd diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py index 7c9f769f1..ee213d5da 100755 --- a/src/op_mode/dns.py +++ b/src/op_mode/dns.py @@ -23,7 +23,7 @@ import vyos.opmode from tabulate import tabulate from vyos.configquery import ConfigTreeQuery -from vyos.utils.process import cmd, rc_cmd +from vyos.utils.process import cmdl, rc_cmd from vyos.template import is_ipv4, is_ipv6 _dynamic_cache_file = r'/run/ddclient/ddclient.cache' @@ -116,10 +116,10 @@ def _get_dynamic_host_records_formatted(data): return output def _get_forwarding_statistics_raw() -> dict: - command = cmd('rec_control get-all') + command = cmdl(['rec_control', 'get-all']) data = _forwarding_data_to_dict(command) data['cache-size'] = "{0:.2f} kbytes".format( int( - cmd('rec_control get cache-bytes')) / 1024 ) + cmdl(['rec_control', 'get', 'cache-bytes'])) / 1024 ) return data def _get_forwarding_statistics_formatted(data): diff --git a/src/op_mode/evpn.py b/src/op_mode/evpn.py index a6dee0b34..c56881701 100644 --- a/src/op_mode/evpn.py +++ b/src/op_mode/evpn.py @@ -21,20 +21,20 @@ import typing import json import vyos.opmode -from vyos.utils.process import cmd +from vyos.utils.process import cmdl def show_evpn(raw: bool, command: typing.Optional[str]): if raw: command = f"{command} json" evpnDict = {} try: - evpnDict['evpn'] = json.loads(cmd(f"vtysh -c '{command}'")) + evpnDict['evpn'] = json.loads(cmdl(['vtysh', '-c', command])) except: raise vyos.opmode.DataUnavailable(f"\"{command.replace(' json', '')}\" is invalid or has no JSON option") return evpnDict else: - return cmd(f"vtysh -c '{command}'") + return cmdl(['vtysh', '-c', command]) if __name__ == '__main__': try: diff --git a/src/op_mode/file.py b/src/op_mode/file.py index 8420c5355..a95ec594a 100755 --- a/src/op_mode/file.py +++ b/src/op_mode/file.py @@ -29,7 +29,7 @@ from vyos.remote import download from vyos.remote import upload from vyos.utils.io import ask_yes_no from vyos.utils.io import print_error -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import run @@ -85,7 +85,7 @@ def zealous_copy(source: str, destination: str) -> None: os.chown(destination, stats.st_uid, stats.st_gid) def get_file_type(path: str) -> str: - return cmd(['file', '-sb', path]) + return cmdl(['file', '-sb', path]) def print_header(string: str) -> None: print('#' * 10, string, '#' * 10) @@ -135,7 +135,7 @@ def print_file_data(path: str) -> None: print(line, end='') # All other binaries get hexdumped. else: - print(cmd(['hexdump', '-C', path])) + print(cmdl(['hexdump', '-C', path])) def parse_image_path(image_path: str) -> str: """ @@ -169,7 +169,7 @@ def show_locally(path: str) -> None: if os.path.isdir(location): print_header('DIRECTORY LISTING') print('Path:\t', location) - print(cmd(['ls', '-hlFGL', '--group-directories-first', location])) + print(cmdl(['ls', '-hlFGL', '--group-directories-first', location])) elif os.path.isfile(location): print_file_info(location) print() @@ -179,7 +179,7 @@ def show_locally(path: str) -> None: sys.exit(1) sys.stdout.flush() # Call `less(1)` and wait for it to terminate before going forward. - cmd(['/usr/bin/less', '-X', temp.name], stdout=sys.stdout) + cmdl(['/usr/bin/less', '-X', temp.name], stdout=sys.stdout) # The stream to the temporary file could break for any reason. # It's much less fragile than if we streamed directly to the process stdin. # But anything could still happen and we don't want to scare the user. diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index d5cd088e6..164baf911 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -23,7 +23,7 @@ import tabulate import textwrap from vyos.config import Config -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.dict import dict_search_args signal(SIGPIPE, SIG_DFL) @@ -67,13 +67,13 @@ def get_nftables_details(family, hook, priority): aux='' if hook == 'name' or hook == 'ipv6-name': - command = f'nft list chain {suffix} vyos_filter {name_prefix}{priority}' + command = ['nft', 'list', 'chain', suffix, 'vyos_filter', f'{name_prefix}{priority}'] else: up_hook = hook.upper() - command = f'nft list chain {suffix} vyos_filter VYOS_{aux}{up_hook}_{priority}' + command = ['nft', 'list', 'chain', suffix, 'vyos_filter', f'VYOS_{aux}{up_hook}_{priority}'] try: - results = cmd(command) + results = cmdl(command) except: return {} @@ -105,9 +105,9 @@ def get_nftables_state_details(family): # no state policy for bridge return {} - command = f'nft list chain {suffix} vyos_filter VYOS_STATE_{name_suffix}' + command = ['nft', 'list', 'chain', suffix, 'vyos_filter', f'VYOS_STATE_{name_suffix}'] try: - results = cmd(command) + results = cmdl(command) except: return {} @@ -129,7 +129,7 @@ def get_nftables_group_members(family, table, name): out = [] try: - results_str = cmd(f'nft -j list set {prefix} {table} {name}') + results_str = cmdl(['nft', '-j', 'list', 'set', prefix, table, name]) results = json.loads(results_str) except: return out @@ -157,7 +157,7 @@ def get_nftables_remote_group_members(family, table, name): out = [] try: - results_str = cmd(f'nft -j list set {prefix} {table} {name}') + results_str = cmdl(['nft', '-j', 'list', 'set', prefix, table, name]) results = json.loads(results_str) except: return out diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py index 078634610..74a475c1c 100755 --- a/src/op_mode/flow_accounting_op.py +++ b/src/op_mode/flow_accounting_op.py @@ -21,7 +21,7 @@ import ipaddress from tabulate import tabulate from vyos.utils.kernel import is_module_loaded -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.logger import syslog from vyos.configquery import ConfigTreeQuery from vyos import ipt_netflow @@ -74,7 +74,7 @@ def _netflow_running(): # get list of interfaces def _get_ifaces_dict(): # run command to get ifaces list - out = cmd('/bin/ip link show') + out = cmdl(['/bin/ip', 'link', 'show']) # read output ifaces_out = out.splitlines() diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py index 095892791..63163f185 100755 --- a/src/op_mode/format_disk.py +++ b/src/op_mode/format_disk.py @@ -22,7 +22,7 @@ from datetime import datetime from vyos.utils.io import ask_yes_no from vyos.utils.process import call -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import DEVNULL from vyos.utils.disk import device_from_id @@ -66,11 +66,12 @@ def list_partitions(disk: str): def delete_partition(disk: str, partition_idx: int): - cmd(f'parted /dev/{disk} rm {partition_idx}') + cmdl(['parted', f'/dev/{disk}', 'rm', str(partition_idx)]) def format_disk_like(target: str, proto: str): - cmd(f'sfdisk -d /dev/{proto} | sfdisk --force /dev/{target}') + dump = cmdl(['sfdisk', '-d', f'/dev/{proto}']) + cmdl(['sfdisk', '--force', f'/dev/{target}'], input=dump) if __name__ == '__main__': diff --git a/src/op_mode/generate_psk.py b/src/op_mode/generate_psk.py index 816150dbf..b57951122 100644 --- a/src/op_mode/generate_psk.py +++ b/src/op_mode/generate_psk.py @@ -15,7 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import argparse -from vyos.utils.process import cmd +from vyos.utils.process import cmdl def validate_hex_size(value): @@ -42,4 +42,4 @@ if __name__ == '__main__': ) args = parser.parse_args() - print(cmd(f'openssl rand -hex {args.hex_size}'))
\ No newline at end of file + print(cmdl(['openssl', 'rand', '-hex', str(args.hex_size)]))
\ No newline at end of file diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py index 459157afd..c575db51d 100755 --- a/src/op_mode/generate_ssh_server_key.py +++ b/src/op_mode/generate_ssh_server_key.py @@ -14,11 +14,13 @@ # 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 glob + from sys import exit from vyos.defaults import directories from vyos.utils.io import ask_yes_no -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.commit import commit_in_progress if not ask_yes_no('Do you really want to remove the existing SSH host keys?'): @@ -30,6 +32,12 @@ if commit_in_progress(): conf_mode_dir = directories['conf_mode'] -cmd('rm -v /etc/ssh/ssh_host_*') -cmd('dpkg-reconfigure openssh-server') -cmd(f'{conf_mode_dir}/service_ssh.py') +host_key_files = glob.glob('/etc/ssh/ssh_host_*') +if not host_key_files: + # Match cmdl()'s default behavior of raising OSError on a non-zero exit + # code (rm would fail with "No such file or directory" if the shell glob + # had not expanded to any files) + raise OSError('No such file or directory: /etc/ssh/ssh_host_*') +cmdl(['rm', '-v'] + host_key_files) +cmdl(['dpkg-reconfigure', 'openssh-server']) +cmdl([f'{conf_mode_dir}/service_ssh.py']) diff --git a/src/op_mode/generate_tech-support_archive.py b/src/op_mode/generate_tech-support_archive.py index d64993b61..2719d231c 100755 --- a/src/op_mode/generate_tech-support_archive.py +++ b/src/op_mode/generate_tech-support_archive.py @@ -26,7 +26,7 @@ from tarfile import open as tar_open from vyos.defaults import directories from vyos.utils.process import call -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.file import get_name_from_path from vyos.remote import upload @@ -60,7 +60,7 @@ def __save_show_report_files(reports_dir: Path): '--outdir', str(reports_dir), ] - output = cmd([script_path] + arguments) + output = cmdl([script_path] + arguments) if output.strip(): print(output) @@ -74,8 +74,8 @@ def __generate_archived_files(location_path: str) -> None: """ # sync/flush journald before archiving /var/log/journal - cmd(['journalctl', '--sync']) - cmd(['journalctl', '--flush']) + cmdl(['journalctl', '--sync']) + cmdl(['journalctl', '--flush']) def __tar_filter(tarinfo): # path inside tar, because we set arcname=... below diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index 089f83670..5174ff3d2 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -69,7 +69,7 @@ from vyos.utils.io import select_entry from vyos.utils.file import chmod_2775 from vyos.utils.file import read_file from vyos.utils.file import write_file -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import run from vyos.utils.process import rc_cmd from vyos.version import get_version_data @@ -701,9 +701,10 @@ def download_file(local_file: str, remote_path: str, vrf: str, download(local_file, remote_path, progressbar=progressbar, check_space=check_space, raise_error=True) else: - vrf_cmd = f'ip vrf exec {vrf} {external_download_script} \ - --local-file {local_file} --remote-path {remote_path}' - cmd(vrf_cmd, env=environ) + cmdl([external_download_script, + '--local-file', local_file, + '--remote-path', remote_path], + env=environ, vrf=vrf) def image_fetch(image_path: str, vrf: str = None, no_prompt: bool = False) -> Path: @@ -811,6 +812,18 @@ def copy_ssh_known_hosts() -> bool: return known_hosts_files and ask_yes_no(msg, default=True) +def copy_bash_history() -> bool: + """Ask user to copy Bash history + + Returns: + bool: user's decision + """ + + history_files = get_bash_history_files() + msg = 'Would you like to copy Bash history?' + return bool(history_files) and ask_yes_no(msg, default=False) + + def console_hint() -> str: pid = getppid() if 'SUDO_USER' in environ else getpid() try: @@ -895,7 +908,7 @@ def validate_compatibility(iso_path: str, force: bool = False) -> None: """ current_data = get_version_data() current_flavor = current_data.get('flavor') - current_architecture = current_data.get('architecture') or cmd('dpkg --print-architecture') + current_architecture = current_data.get('architecture') or cmdl(['dpkg', '--print-architecture']) new_data = get_version_data(f'{iso_path}/version.json') new_flavor = new_data.get('flavor') @@ -1115,36 +1128,62 @@ def install_image() -> None: exit(1) -def get_known_hosts_files(for_root=True, for_users=True) -> list: - """Collect all existing `known_hosts` files for root and/or users under /home""" +def _get_users_relative_paths( + relative_paths: list, for_root=True, for_users=True +) -> list: + """ + Retrieve a list of file paths based on given relative paths, optionally + including files from the root directory and user home directories. + """ files = [] if for_root: - base_files = ('/root/.ssh/known_hosts', '/etc/ssh/ssh_known_hosts') - for file_path in base_files: - root_known_hosts = Path(file_path) - if root_known_hosts.exists(): - files.append(root_known_hosts) + for relative_path in relative_paths: + file_path = Path('/root') / relative_path + if file_path.exists(): + files.append(file_path) if for_users: # for each non-system user for user in get_local_users(): home_dir = Path(get_user_home_dir(user)) if home_dir.exists(): - known_hosts = home_dir / '.ssh' / 'known_hosts' - if known_hosts.exists(): - files.append(known_hosts) + for relative_path in relative_paths: + file_path = home_dir / relative_path + if file_path.exists(): + files.append(file_path) + + return [file_path.absolute() for file_path in files] + + +def get_known_hosts_files(for_root=True, for_users=True) -> list: + """Collect all existing `known_hosts` files for root and/or users under /home""" + + files = _get_users_relative_paths( + ['.ssh/known_hosts'], + for_root=for_root, + for_users=for_users, + ) + + if for_root: + # Add also additional global 'ssh_known_hosts' file + root_known_hosts = Path('/etc/ssh/ssh_known_hosts') + if root_known_hosts.exists(): + files.append(root_known_hosts) return files +def _mkdir_and_copy_file(source_path: Path, target_path: Path): + """Create the target directory if it doesn't exist and copy a file from source to target""" + + target_path.parent.mkdir(parents=True, exist_ok=True) + copy(source_path, target_path) + + def migrate_known_hosts(target_dir: str): """Copy `known_hosts` for root and all users to the new image directory""" - def _mkdir_and_copy_file(known_hosts_file, target_known_hosts): - target_known_hosts.parent.mkdir(parents=True, exist_ok=True) - copy(known_hosts_file, target_known_hosts) - # Copy root only files using default path known_hosts_files = get_known_hosts_files(for_root=True, for_users=False) for known_hosts_file in known_hosts_files: @@ -1164,6 +1203,36 @@ def migrate_known_hosts(target_dir: str): _mkdir_and_copy_file(known_hosts_file, target_known_hosts) +def get_bash_history_files(for_root=True, for_users=True) -> list: + """Collect all existing `.bash_history` files for root and/or users under /home""" + + return _get_users_relative_paths( + ['.bash_history'], + for_root=for_root, + for_users=for_users, + ) + + +def migrate_bash_history(target_dir: str): + """Copy `.bash_history` for root and all users to the new image directory""" + + # Copy root's history using its default path + history_files = get_bash_history_files(for_root=True, for_users=False) + for history_file in history_files: + target_history = Path(f'{target_dir}{history_file}') + _mkdir_and_copy_file(history_file, target_history) + + # Preserve non-root users' bash history the same way known_hosts + # is backed up: under /var/.users_backups/{user}, since regular + # user home directories are not otherwise migrated. + history_files = get_bash_history_files(for_root=False, for_users=True) + for history_file in history_files: + username = history_file.parent.name + base_dir = Path(f'{target_dir}/var/.users_backups/{username}') + target_history = base_dir / '.bash_history' + _mkdir_and_copy_file(history_file, target_history) + + @compat.grub_cfg_update def add_image(image_path: str, vrf: str = None, username: str = '', password: str = '', no_prompt: bool = False, force: bool = False) -> None: @@ -1289,19 +1358,28 @@ def add_image(image_path: str, vrf: str = None, username: str = '', chmod_2775(target_config_dir) Path(f'{target_config_dir}/.vyatta_config').touch() - target_ssh_dir: str = f'{root_dir}/boot/{image_name}/rw/etc/ssh/' + # Default persistence directory + target_persist_dir: str = f'{root_dir}/boot/{image_name}/rw' + if no_prompt or copy_ssh_host_keys(): print('Copying SSH host keys') + target_ssh_dir: str = f'{target_persist_dir}/etc/ssh/' Path(target_ssh_dir).mkdir(parents=True) host_keys: list[str] = glob('/etc/ssh/ssh_host*') for host_key in host_keys: copy(host_key, target_ssh_dir) - target_ssh_known_hosts_dir: str = f'{root_dir}/boot/{image_name}/rw' if no_prompt or copy_ssh_known_hosts(): print('Copying SSH known_hosts files') + target_ssh_known_hosts_dir: str = target_persist_dir migrate_known_hosts(target_ssh_known_hosts_dir) + # By default we should disable auto-copy under '--no-prompt' mode + if no_prompt is False and copy_bash_history(): + print('Copying bash history') + target_history_dir: str = target_persist_dir + migrate_bash_history(target_history_dir) + # copy system image and kernel files print('Copying system image files') for file in Path(f'{DIR_ISO_MOUNT}/live').iterdir(): diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py index 5c6f04476..20cc3f942 100755 --- a/src/op_mode/interfaces.py +++ b/src/op_mode/interfaces.py @@ -31,7 +31,7 @@ from vyos.utils.dict import dict_set_nested from vyos.utils.io import catch_broken_pipe from vyos.utils.network import get_interface_vrf from vyos.utils.network import interface_exists -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import rc_cmd from vyos.utils.process import call from vyos.configquery import op_mode_config_dict @@ -100,7 +100,7 @@ def _split_text(text, used=0): """ no_tty = call('tty -s') - returned = cmd('stty size') if not no_tty else '' + returned = cmdl(['stty', 'size']) if not no_tty else '' returned = returned.split() if len(returned) == 2: _, columns = tuple(int(_) for _ in returned) @@ -147,7 +147,7 @@ def _get_counter_val(prev, now): return value def _pppoe(ifname): - out = cmd('ps -C pppd -f') + out = cmdl(['ps', '-C', 'pppd', '-f']) if ifname in out: return 'C' if ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]: @@ -198,14 +198,14 @@ def _get_raw_data(ifname: typing.Optional[str], res_intf = {} cache = interface.operational.load_counters() - out = cmd(f'ip -json addr show {interface.ifname}') + out = cmdl(['ip', '-json', 'addr', 'show', interface.ifname]) res_intf_l = json.loads(out) res_intf = res_intf_l[0] if res_intf['link_type'] == 'tunnel6': # Note that 'ip -6 tun show {interface.ifname}' is not json # aware, so find in list - out = cmd('ip -json -6 tun show') + out = cmdl(['ip', '-json', '-6', 'tun', 'show']) tunnel = json.loads(out) res_intf['tunnel6'] = _find_intf_by_ifname(tunnel, interface.ifname) @@ -302,15 +302,14 @@ def _get_counter_data(ifname: typing.Optional[str], def _get_kernel_data(raw, ifname = None, detail = False, statistics = False): + address_show_cmd = ['ip', '-j', '-d', '-s', 'address', 'show'] if ifname: # Check if the interface exists if not interface_exists(ifname): raise vyos.opmode.IncorrectValue(f"{ifname} does not exist!") - int_name = f'dev {ifname}' - else: - int_name = '' + address_show_cmd += ['dev', ifname] - kernel_interface = json.loads(cmd(f'ip -j -d -s address show {int_name}')) + kernel_interface = json.loads(cmdl(address_show_cmd)) # Return early if raw if raw: @@ -345,7 +344,7 @@ def _format_kernel_data(data, detail, statistics): if 'parentdev' in interface: parentdev = interface['parentdev'] if re.match(r'^[0-9a-fA-F]{4}:', parentdev): - dev_model = cmd(f'lspci -nn -s {parentdev}').split(']:')[1].strip() + dev_model = cmdl(['lspci', '-nn', '-s', parentdev]).split(']:')[1].strip() # Get the IP addresses on interface ip_list = [] @@ -664,7 +663,7 @@ def show_vlan_to_vni(raw: bool, intf_name: typing.Optional[str], if not vid: vid = "all" - tunnel_data = json.loads(cmd(f"bridge -j vlan tunnelshow dev {intf_name} vid {vid}")) + tunnel_data = json.loads(cmdl(['bridge', '-j', 'vlan', 'tunnelshow', 'dev', intf_name, 'vid', vid])) if not tunnel_data: if vid == "all": @@ -672,7 +671,7 @@ def show_vlan_to_vni(raw: bool, intf_name: typing.Optional[str], else: raise vyos.opmode.UnconfiguredObject(f"No VLAN-to-VNI mapping found for VLAN {vid}\n") - statistics_data = json.loads(cmd(f"bridge -j -s vlan tunnelshow dev {intf_name} vid {vid}"))[0] + statistics_data = json.loads(cmdl(['bridge', '-j', '-s', 'vlan', 'tunnelshow', 'dev', intf_name, 'vid', vid]))[0] mapping_config = op_mode_config_dict(['interfaces', 'vxlan', intf_name, 'vlan-to-vni'], get_first_key=True) diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index b878789a7..9cb576414 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -23,7 +23,7 @@ from tabulate import tabulate from vyos.utils.convert import convert_data from vyos.utils.convert import seconds_to_human -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.configquery import ConfigTreeQuery from vyos.base import Warning @@ -60,7 +60,7 @@ def _get_output_swanctl_sas_from_list(ra_output_list: list) -> str: output = '' for sa_val in ra_output_list: for sa in sa_val.values(): - swanctl_output: str = cmd(f'sudo swanctl -l --ike-id {sa["uniqueid"]}') + swanctl_output: str = cmdl(['swanctl', '-l', '--ike-id', str(sa["uniqueid"])], sudo=True) output = f'{output}{swanctl_output}\n\n' return output diff --git a/src/op_mode/lldp.py b/src/op_mode/lldp.py index 0994b5d49..f03d72cf2 100755 --- a/src/op_mode/lldp.py +++ b/src/op_mode/lldp.py @@ -22,7 +22,7 @@ import typing from tabulate import tabulate from vyos.configquery import ConfigTreeQuery -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.dict import dict_search import vyos.opmode @@ -53,7 +53,7 @@ def _get_raw_data(interface=None, detail=False): tmp += f' details' if interface: tmp += f' ports {interface}' - output = cmd(tmp) + output = cmdl(tmp.split()) data = json.loads(output) if not data: return [] @@ -151,7 +151,7 @@ def show_neighbors(raw: bool, interface: typing.Optional[str], detail: typing.Op tmp = 'lldpcli -f text show neighbors details' if interface: tmp += f' ports {interface}' - return cmd(tmp) + return cmdl(tmp.split()) if __name__ == "__main__": try: diff --git a/src/op_mode/load-balancing_wan.py b/src/op_mode/load-balancing_wan.py index 6f1d00dcd..4b9233f0f 100755 --- a/src/op_mode/load-balancing_wan.py +++ b/src/op_mode/load-balancing_wan.py @@ -21,7 +21,7 @@ import sys from datetime import datetime from vyos.config import Config -from vyos.utils.process import cmd +from vyos.utils.process import cmdl import vyos.opmode @@ -87,7 +87,7 @@ def show_summary(raw: bool): @_verify def show_connection(raw: bool): - res = cmd('sudo conntrack -L -n') + res = cmdl(['conntrack', '-L', '-n'], sudo=True) lines = res.split("\n") filtered_lines = [line for line in lines if re.search(r' mark=[1-9]', line)] @@ -99,7 +99,7 @@ def show_connection(raw: bool): @_verify def show_status(raw: bool): - res = cmd('sudo nft list chain ip vyos_wanloadbalance wlb_mangle_prerouting') + res = cmdl(['nft', 'list', 'chain', 'ip', 'vyos_wanloadbalance', 'wlb_mangle_prerouting'], sudo=True) lines = res.split("\n") filtered_lines = [line.replace("\t", "") for line in lines[3:-2] if 'meta mark set' not in line] diff --git a/src/op_mode/mtr_execute.py b/src/op_mode/mtr_execute.py index b97e46a1f..a541b3add 100644 --- a/src/op_mode/mtr_execute.py +++ b/src/op_mode/mtr_execute.py @@ -23,7 +23,7 @@ from json import loads from vyos.utils.network import interface_list from vyos.utils.network import vrf_list -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import call import vyos.opmode @@ -199,7 +199,7 @@ def mtr( command = options[key]['mtr'].format(command=command, value=val) if json: - output = cmd(f'{command} {host}') + output = cmdl(command.split() + [host]) if for_api: output = loads(output) print(output) diff --git a/src/op_mode/multicast.py b/src/op_mode/multicast.py index 096d01665..29de9c29e 100755 --- a/src/op_mode/multicast.py +++ b/src/op_mode/multicast.py @@ -19,7 +19,7 @@ import sys import typing from tabulate import tabulate -from vyos.utils.process import cmd +from vyos.utils.process import cmdl import vyos.opmode @@ -32,7 +32,7 @@ def _get_raw_data(family, interface=None): tmp = f'{tmp} -j maddr show' if interface: tmp = f'{tmp} dev {interface}' - output = cmd(tmp) + output = cmdl(tmp.split()) data = json.loads(output) if not data: return [] diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index f97d7dc0f..434273622 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -25,7 +25,7 @@ from tabulate import tabulate import vyos.opmode from vyos.configquery import ConfigTreeQuery -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.dict import dict_search ArgDirection = typing.Literal['source', 'destination'] @@ -43,7 +43,7 @@ def _get_xml_translation(direction, family, address=None): tmp = f'conntrack --dump --family {family} {opt} --output xml' if address: tmp += f' --src {address}' - return cmd(tmp) + return cmdl(tmp.split()) def _xml_to_dict(xml): @@ -67,7 +67,7 @@ def _get_json_data(direction, family): if direction == 'destination': chain = 'PREROUTING' family = 'ip6' if family == 'inet6' else 'ip' - return cmd(f'nft --json list chain {family} vyos_nat {chain}') + return cmdl(['nft', '--json', 'list', 'chain', family, 'vyos_nat', chain]) def _get_raw_data_rules(direction, family): diff --git a/src/op_mode/neighbor.py b/src/op_mode/neighbor.py index 9bbad94e1..ab784f4b3 100755 --- a/src/op_mode/neighbor.py +++ b/src/op_mode/neighbor.py @@ -38,23 +38,18 @@ ArgState = typing.Literal['reachable', 'stale', 'failed', 'permanent'] def get_raw_data(family, interface=None, state=None): from json import loads - from vyos.utils.process import cmd + from vyos.utils.process import cmdl + neigh_cmd = ['ip', '--family', family, '--json', 'neighbor', 'list'] if interface: if not interface_exists(interface): raise ValueError(f"Interface '{interface}' does not exist in the system") - interface = f"dev {interface}" - else: - interface = "" + neigh_cmd += ['dev', interface] if state: - state = f"nud {state}" - else: - state = "" - - neigh_cmd = f"ip --family {family} --json neighbor list {interface} {state}" + neigh_cmd += ['nud', state] - data = loads(cmd(neigh_cmd)) + data = loads(cmdl(neigh_cmd)) return data diff --git a/src/op_mode/ntp.py b/src/op_mode/ntp.py index 42d0c1b00..2125e434c 100644 --- a/src/op_mode/ntp.py +++ b/src/op_mode/ntp.py @@ -20,7 +20,7 @@ from itertools import chain import vyos.opmode from vyos.configquery import ConfigTreeQuery -from vyos.utils.process import cmd +from vyos.utils.process import cmdl def _get_raw_data(command: str) -> dict: # Returns returns chronyc output as a dictionary @@ -88,7 +88,7 @@ def _get_raw_data(command: str) -> dict: # Get -c option command line output, splitlines, # and save comma-separated values as a flat list - output = cmd(command).splitlines() + output = cmdl(command.split()).splitlines() values = csv.reader(output) values = list(chain.from_iterable(values)) @@ -128,7 +128,7 @@ def show_activity(raw: bool): else: command = _extend_command_vrf() + command command += f" activity" - return cmd(command) + return cmdl(command.split()) def show_sources(raw: bool): _is_configured() @@ -140,7 +140,7 @@ def show_sources(raw: bool): else: command = _extend_command_vrf() + command command += f" sources -v" - return cmd(command) + return cmdl(command.split()) def show_tracking(raw: bool): _is_configured() @@ -152,7 +152,7 @@ def show_tracking(raw: bool): else: command = _extend_command_vrf() + command command += f" tracking" - return cmd(command) + return cmdl(command.split()) def show_sourcestats(raw: bool): _is_configured() @@ -164,7 +164,7 @@ def show_sourcestats(raw: bool): else: command = _extend_command_vrf() + command command += f" sourcestats -v" - return cmd(command) + return cmdl(command.split()) if __name__ == '__main__': diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 482dc91af..912840f80 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -50,7 +50,7 @@ from vyos.pki import verify_certificate from vyos.utils.io import ask_input from vyos.utils.io import ask_yes_no from vyos.utils.misc import install_into_config -from vyos.utils.process import cmd +from vyos.utils.process import cmdl CERT_REQ_END = '-----END CERTIFICATE REQUEST-----' auth_dir = '/config/auth' @@ -234,11 +234,11 @@ def install_certificate( # Show/install conf commands for certificate prefix = 'ca' if is_ca else 'certificate' - base = f'pki {prefix} {name}' + base = ['pki', prefix, name] config_paths = [] if cert: cert_pem = ''.join(encode_certificate(cert).strip().split('\n')[1:-1]) - config_paths.append(f"{base} certificate '{cert_pem}'") + config_paths.append(base + ['certificate', cert_pem]) if private_key: key_pem = ''.join( @@ -246,9 +246,9 @@ def install_certificate( .strip() .split('\n')[1:-1] ) - config_paths.append(f"{base} private key '{key_pem}'") + config_paths.append(base + ['private', 'key', key_pem]) if key_passphrase: - config_paths.append(f'{base} private password-protected') + config_paths.append(base + ['private', 'password-protected']) install_into_config(conf, config_paths) @@ -256,13 +256,13 @@ def install_certificate( def install_crl(ca_name, crl): # Show/install conf commands for crl crl_pem = ''.join(encode_certificate(crl).strip().split('\n')[1:-1]) - install_into_config(conf, [f"pki ca {ca_name} crl '{crl_pem}'"]) + install_into_config(conf, [['pki', 'ca', ca_name, 'crl', crl_pem]]) def install_dh_parameters(name, params): # Show/install conf commands for dh params dh_pem = ''.join(encode_dh_parameters(params).strip().split('\n')[1:-1]) - install_into_config(conf, [f"pki dh {name} parameters '{dh_pem}'"]) + install_into_config(conf, [['pki', 'dh', name, 'parameters', dh_pem]]) def install_ssh_key(name, public_key, private_key, passphrase=None): @@ -273,10 +273,10 @@ def install_ssh_key(name, public_key, private_key, passphrase=None): username = os.getlogin() type_key_split = key_openssh.split(' ') - base = f'system login user {username} authentication public-keys {name}' + base = ['system', 'login', 'user', username, 'authentication', 'public-keys', name] install_into_config( conf, - [f"{base} key '{type_key_split[1]}'", f"{base} type '{type_key_split[0]}'"], + [base + ['key', type_key_split[1]], base + ['type', type_key_split[0]]], ) print( encode_private_key( @@ -301,7 +301,7 @@ def install_keypair( if install_public_key: install_public_pem = ''.join(public_key_pem.strip().split('\n')[1:-1]) config_paths.append( - f"pki key-pair {name} public key '{install_public_pem}'" + ['pki', 'key-pair', name, 'public', 'key', install_public_pem] ) else: print('Public key:') @@ -316,10 +316,10 @@ def install_keypair( if install_private_key: install_private_pem = ''.join(private_key_pem.strip().split('\n')[1:-1]) config_paths.append( - f"pki key-pair {name} private key '{install_private_pem}'" + ['pki', 'key-pair', name, 'private', 'key', install_private_pem] ) if passphrase: - config_paths.append(f'pki key-pair {name} private password-protected') + config_paths.append(['pki', 'key-pair', name, 'private', 'password-protected']) else: print('Private key:') print(private_key_pem) @@ -329,8 +329,8 @@ def install_keypair( def install_openvpn_key(name, key_data, key_version='1'): config_paths = [ - f"pki openvpn shared-secret {name} key '{key_data}'", - f"pki openvpn shared-secret {name} version '{key_version}'", + ['pki', 'openvpn', 'shared-secret', name, 'key', key_data], + ['pki', 'openvpn', 'shared-secret', name, 'version', key_version], ] install_into_config(conf, config_paths) @@ -345,7 +345,7 @@ def install_wireguard_key(interface, private_key, public_key): # Check if we are running in a config session - if yes, we can directly write to the CLI install_into_config( - conf, [f"interfaces wireguard {interface} private-key '{private_key}'"] + conf, [['interfaces', 'wireguard', interface, 'private-key', private_key]] ) print(f"Corresponding public-key to use on peer system is: '{public_key}'") @@ -360,7 +360,7 @@ def install_wireguard_psk(interface, peer, psk): # Check if we are running in a config session - if yes, we can directly write to the CLI install_into_config( - conf, [f"interfaces wireguard {interface} peer {peer} preshared-key '{psk}'"] + conf, [['interfaces', 'wireguard', interface, 'peer', peer, 'preshared-key', psk]] ) @@ -883,7 +883,10 @@ def generate_keypair(name, install=False, file=False): def generate_openvpn_key(name, install=False, file=False): - result = cmd('openvpn --genkey secret /dev/stdout | grep -o "^[^#]*"') + raw_result = cmdl(['openvpn', '--genkey', 'secret', '/dev/stdout']) + # equivalent to the old `grep -o "^[^#]*"`, applied per line: keep + # everything before the first '#' on each line (comment lines become empty) + result = '\n'.join(line.split('#', 1)[0] for line in raw_result.splitlines()) if not result: print('Failed to generate OpenVPN key') @@ -911,8 +914,8 @@ def generate_openvpn_key(name, install=False, file=False): def generate_wireguard_key(interface=None, install=False): - private_key = cmd('wg genkey') - public_key = cmd('wg pubkey', input=private_key) + private_key = cmdl(['wg', 'genkey']) + public_key = cmdl(['wg', 'pubkey'], input=private_key) if interface and install: install_wireguard_key(interface, private_key, public_key) @@ -922,7 +925,7 @@ def generate_wireguard_key(interface=None, install=False): def generate_wireguard_psk(interface=None, peer=None, install=False): - psk = cmd('wg genpsk') + psk = cmdl(['wg', 'genpsk']) if interface and peer and install: install_wireguard_psk(interface, peer, psk) else: @@ -1443,11 +1446,11 @@ def renew_certbot(raw: bool, force: typing.Optional[bool] = False): # a bad time Warning(f'Directory "{certbot_config}" missing. Reinitializing PKI ' 'subsystem...\n\n') - out = cmd(f'sudo sg vyattacfg -c "{vyos_conf_scripts_dir}/pki.py"') + out = cmdl(['sg', 'vyattacfg', '-c', f'{vyos_conf_scripts_dir}/pki.py'], sudo=True) elif force: - out = cmd(f'sudo sg vyattacfg -c "{vyos_conf_scripts_dir}/pki.py certbot_renew_force"') + out = cmdl(['sg', 'vyattacfg', '-c', f'{vyos_conf_scripts_dir}/pki.py certbot_renew_force'], sudo=True) else: - out = cmd(f'sudo sg vyattacfg -c "{vyos_conf_scripts_dir}/pki.py certbot_renew"') + out = cmdl(['sg', 'vyattacfg', '-c', f'{vyos_conf_scripts_dir}/pki.py certbot_renew'], sudo=True) print(out) diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py index 966dc80f9..33ab40ecd 100755 --- a/src/op_mode/policy_route.py +++ b/src/op_mode/policy_route.py @@ -19,7 +19,7 @@ import re import tabulate from vyos.config import Config -from vyos.utils.process import cmd +from vyos.utils.process import cmdl def get_config_policy(conf, name=None, ipv6=False): config_path = ['policy'] @@ -33,9 +33,9 @@ def get_config_policy(conf, name=None, ipv6=False): def get_nftables_details(name, ipv6=False): suffix = '6' if ipv6 else '' - command = f'sudo nft list chain ip{suffix} mangle VYOS_PBR{suffix}_{name}' + command = ['nft', 'list', 'chain', f'ip{suffix}', 'mangle', f'VYOS_PBR{suffix}_{name}'] try: - results = cmd(command) + results = cmdl(command, sudo=True) except: return {} diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index a60c33dc6..0b5eaeacd 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -116,11 +116,11 @@ def check_unsaved_config(): pass def execute_shutdown(time, reboot=True, ask=True): - from vyos.utils.process import cmd + from vyos.utils.process import cmdl check_unsaved_config() - host = cmd("hostname --fqdn") + host = cmdl(['hostname', '--fqdn']) action = "reboot" if reboot else "poweroff" if not ask: @@ -133,17 +133,17 @@ def execute_shutdown(time, reboot=True, ask=True): chk_vyatta_based_reboots() ### - out = cmd(f'/sbin/shutdown {action_cmd} now', stderr=STDOUT) + out = cmdl(['/sbin/shutdown', action_cmd, 'now'], stderr=STDOUT) print(out.split(",", 1)[0]) return elif len(time) == 1: # Assume the argument is just time ts = parse_time(time[0]) if ts: - cmd(f'/sbin/shutdown {action_cmd} {time[0]}', stderr=STDOUT) + cmdl(['/sbin/shutdown', action_cmd, time[0]], stderr=STDOUT) # Inform all other logged in users about the reboot/shutdown wall_msg = f'System {action} is scheduled {time[0]}' - cmd(f'/usr/bin/wall "{wall_msg}"') + cmdl(['/usr/bin/wall', wall_msg]) else: exit(f'Invalid time "{time[0]}". The valid format is HH:MM') elif len(time) == 2: @@ -155,10 +155,10 @@ def execute_shutdown(time, reboot=True, ask=True): td = t - datetime.now() t2 = 1 + int(td.total_seconds())//60 # Get total minutes - cmd(f'/sbin/shutdown {action_cmd} {t2}', stderr=STDOUT) + cmdl(['/sbin/shutdown', action_cmd, str(t2)], stderr=STDOUT) # Inform all other logged in users about the reboot/shutdown wall_msg = f'System {action} is scheduled {time[1]} {time[0]}' - cmd(f'/usr/bin/wall "{wall_msg}"') + cmdl(['/usr/bin/wall', wall_msg]) else: if not ts: exit(f'Invalid time "{time[0]}". Uses 24 Hour Clock format') diff --git a/src/op_mode/qos.py b/src/op_mode/qos.py index 47a7a3d59..ae391f2b5 100755 --- a/src/op_mode/qos.py +++ b/src/op_mode/qos.py @@ -22,7 +22,7 @@ from tabulate import tabulate import vyos.opmode from vyos.configquery import op_mode_config_dict -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.network import interface_exists def detailed_output(dataset, headers): @@ -91,8 +91,8 @@ def show_shaper(raw: bool, ifname: typing.Optional[str], classn: typing.Optional if not policy_name: continue - class_data = json.loads(cmd(f"tc -j -s class show dev {i}")) - qdisc_data = json.loads(cmd(f"tc -j qdisc show dev {i}")) + class_data = json.loads(cmdl(['tc', '-j', '-s', 'class', 'show', 'dev', i])) + qdisc_data = json.loads(cmdl(['tc', '-j', 'qdisc', 'show', 'dev', i])) if class_dict: # Gather qdisc information (e.g. Queue Type) @@ -224,13 +224,13 @@ def show_cake(raw: bool, ifname: typing.Optional[str]): if not interface_exists(ifname): raise vyos.opmode.Error(f"{ifname} does not exist!") - cake_data = json.loads(cmd(f"tc -j -s qdisc show dev {ifname}"))[0] + cake_data = json.loads(cmdl(['tc', '-j', '-s', 'qdisc', 'show', 'dev', ifname]))[0] if cake_data: if cake_data.get('kind') == 'cake': if raw: return {'qos': {ifname: cake_data}} else: - print(cmd(f"tc -s qdisc show dev {ifname}")) + print(cmdl(['tc', '-s', 'qdisc', 'show', 'dev', ifname])) if __name__ == '__main__': try: diff --git a/src/op_mode/route.py b/src/op_mode/route.py index b11b0ccc2..30a56f86a 100755 --- a/src/op_mode/route.py +++ b/src/op_mode/route.py @@ -57,7 +57,7 @@ frr_command_template = Template(""" ArgFamily = typing.Literal['inet', 'inet6'] def show_summary(raw: bool, family: ArgFamily, table: typing.Optional[int], vrf: typing.Optional[str]): - from vyos.utils.process import cmd + from vyos.utils.process import cmdl if family == 'inet': family_cmd = 'ip' @@ -83,7 +83,8 @@ def show_summary(raw: bool, family: ArgFamily, table: typing.Optional[int], vrf: if raw: from json import loads - output = cmd(f"vtysh -c 'show {family_cmd} route {vrf_cmd} summary {table_cmd} json'").strip() + frr_command = re.sub(r'\s+', ' ', f'show {family_cmd} route {vrf_cmd} summary {table_cmd} json').strip() + output = cmdl(['vtysh', '-c', frr_command]).strip() # If there are no routes in a table, its "JSON" output is an empty string, # as of FRR 8.4.1 @@ -92,7 +93,8 @@ def show_summary(raw: bool, family: ArgFamily, table: typing.Optional[int], vrf: else: return {} else: - output = cmd(f"vtysh -c 'show {family_cmd} route {vrf_cmd} summary {table_cmd}'") + frr_command = re.sub(r'\s+', ' ', f'show {family_cmd} route {vrf_cmd} summary {table_cmd}').strip() + output = cmdl(['vtysh', '-c', frr_command]) return output def show(raw: bool, @@ -119,8 +121,8 @@ def show(raw: bool, frr_command = frr_command_template.render(kwargs) frr_command = re.sub(r'\s+', ' ', frr_command) - from vyos.utils.process import cmd - output = cmd(f"vtysh -c '{frr_command}'") + from vyos.utils.process import cmdl + output = cmdl(['vtysh', '-c', frr_command]) if raw: from json import loads diff --git a/src/op_mode/show_wwan.py b/src/op_mode/show_wwan.py index 05e7d0e75..53b94d411 100755 --- a/src/op_mode/show_wwan.py +++ b/src/op_mode/show_wwan.py @@ -18,7 +18,7 @@ import argparse from sys import exit from vyos.configquery import ConfigTreeQuery -from vyos.utils.process import cmd +from vyos.utils.process import cmdl parser = argparse.ArgumentParser() parser.add_argument("--model", help="Get module model", action="store_true") @@ -36,7 +36,7 @@ required.add_argument("--interface", help="WWAN interface name, e.g. wwan0", req def qmi_cmd(device, command, silent=False): try: - tmp = cmd(f'qmicli --device={device} --device-open-proxy {command}') + tmp = cmdl(['qmicli', f'--device={device}', '--device-open-proxy'] + command.split()) tmp = tmp.replace(f'[{cdc}] ', '') if not silent: # skip first line as this only holds the info headline diff --git a/src/op_mode/ssh.py b/src/op_mode/ssh.py index a4442c301..b74f37e5e 100755 --- a/src/op_mode/ssh.py +++ b/src/op_mode/ssh.py @@ -19,7 +19,7 @@ import json import sys import glob import vyos.opmode -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.configquery import ConfigTreeQuery from tabulate import tabulate @@ -35,9 +35,9 @@ def show_fingerprints(raw: bool, ascii: bool): for keyfile in publickeys: try: if ascii: - keydata = cmd("ssh-keygen -l -v -E sha256 -f " + keyfile).splitlines() + keydata = cmdl(['ssh-keygen', '-l', '-v', '-E', 'sha256', '-f', keyfile]).splitlines() else: - keydata = cmd("ssh-keygen -l -E sha256 -f " + keyfile).splitlines() + keydata = cmdl(['ssh-keygen', '-l', '-E', 'sha256', '-f', keyfile]).splitlines() type = keydata[0].split(None)[-1].strip("()") key_size = keydata[0].split(None)[0] fingerprint = keydata[0].split(None)[1] @@ -70,12 +70,12 @@ def show_dynamic_protection(raw: bool): attackers = [] try: # IPv4 - attackers = attackers + json.loads(cmd("nft -j list set ip sshguard attackers"))["nftables"][1]["set"]["elem"] + attackers = attackers + json.loads(cmdl(['nft', '-j', 'list', 'set', 'ip', 'sshguard', 'attackers']))["nftables"][1]["set"]["elem"] except: pass try: # IPv6 - attackers = attackers + json.loads(cmd("nft -j list set ip6 sshguard attackers"))["nftables"][1]["set"]["elem"] + attackers = attackers + json.loads(cmdl(['nft', '-j', 'list', 'set', 'ip6', 'sshguard', 'attackers']))["nftables"][1]["set"]["elem"] except: pass if attackers: diff --git a/src/op_mode/stp.py b/src/op_mode/stp.py index c2a897183..457125f12 100755 --- a/src/op_mode/stp.py +++ b/src/op_mode/stp.py @@ -20,7 +20,7 @@ import json from tabulate import tabulate import vyos.opmode -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.network import interface_exists def detailed_output(dataset, headers): @@ -34,7 +34,7 @@ def detailed_output(dataset, headers): def _get_bridge_vlan_data(iface): allowed_vlans = [] native_vlan = None - vlanData = json.loads(cmd(f"bridge -j -d vlan show")) + vlanData = json.loads(cmdl(['bridge', '-j', '-d', 'vlan', 'show'])) for vlans in vlanData: if vlans['ifname'] == iface: for allowed in vlans['vlans']: @@ -69,8 +69,8 @@ def _get_stp_data(ifname, brInfo, brStatus): #tmpInfo['bridge_id'] = brInfo.get('linkinfo').get('info_data').get('bridge_id') #tmpInfo['root_id'] = brInfo.get('linkinfo').get('info_data').get('root_id') - tmpInfo['bridge_id'] = cmd(f"cat /sys/class/net/{brInfo.get('ifname')}/bridge/bridge_id").split('.') - tmpInfo['root_id'] = cmd(f"cat /sys/class/net/{brInfo.get('ifname')}/bridge/root_id").split('.') + tmpInfo['bridge_id'] = cmdl(['cat', f"/sys/class/net/{brInfo.get('ifname')}/bridge/bridge_id"]).split('.') + tmpInfo['root_id'] = cmdl(['cat', f"/sys/class/net/{brInfo.get('ifname')}/bridge/root_id"]).split('.') # The "/sys/class/net" structure stores the IDs without separators like ':' or '.' # This adds a ':' after every 2 characters to make it resemble a MAC Address @@ -125,12 +125,15 @@ def show_stp(raw: bool, ifname: typing.Optional[str], detail: bool): else: ifname = "" - bridgeInfo = json.loads(cmd(f"ip -j -d -s link show type bridge {ifname}")) + ip_link_cmd = ['ip', '-j', '-d', '-s', 'link', 'show', 'type', 'bridge'] + if ifname: + ip_link_cmd.append(ifname) + bridgeInfo = json.loads(cmdl(ip_link_cmd)) if not bridgeInfo: raise vyos.opmode.Error(f"No Bridges configured!") - bridgeStatus = json.loads(cmd(f"bridge -j -s -d link show")) + bridgeStatus = json.loads(cmdl(['bridge', '-j', '-s', '-d', 'link', 'show'])) for bridges in bridgeInfo: output_list = [] diff --git a/src/op_mode/tech_support.py b/src/op_mode/tech_support.py index 6055cbf15..bf8bfd24c 100644 --- a/src/op_mode/tech_support.py +++ b/src/op_mode/tech_support.py @@ -19,7 +19,7 @@ import json import vyos.opmode -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.base import Warning def _get_version_data(): @@ -42,7 +42,7 @@ def _get_cpus(): return get_cpus() def _get_process_stats(): - return cmd('top --iterations 1 --batch-mode --accum-time-toggle') + return cmdl(['top', '--iterations', '1', '--batch-mode', '--accum-time-toggle']) def _get_storage(): from vyos.utils.disk import get_persistent_storage_stats @@ -51,10 +51,10 @@ def _get_storage(): def _get_devices(): devices = {} - devices["pci"] = cmd("lspci") + devices["pci"] = cmdl(["lspci"]) try: - devices["usb"] = cmd("lsusb") + devices["usb"] = cmdl(["lsusb"]) except OSError: Warning("Could not retrieve information about USB devices") devices["usb"] = {} @@ -67,7 +67,7 @@ def _get_memory(): return read_file("/proc/meminfo") def _get_processes(): - res = cmd("ps aux") + res = cmdl(["ps", "aux"]) return res @@ -82,7 +82,7 @@ def _get_interrupts(): def _get_partitions(): # XXX: as of parted 3.5, --json is completely broken # and cannot be used (outputs malformed JSON syntax) - res = cmd(f"parted --list") + res = cmdl(["parted", "--list"]) return res @@ -139,13 +139,13 @@ def _get_routes(proto): data = {} - summary = cmd(f"vtysh -c 'show {proto} route summary json'") + summary = cmdl(["vtysh", "-c", f"show {proto} route summary json"]) summary = loads(summary) data["summary"] = summary if summary["routesTotal"] < MAX_ROUTES: - rib_routes = cmd(f"vtysh -c 'show {proto} route json'") + rib_routes = cmdl(["vtysh", "-c", f"show {proto} route json"]) data["routes"] = loads(rib_routes) if summary["routesTotalFib"] < MAX_ROUTES: @@ -164,25 +164,25 @@ def _get_ipv6_routes(): def _get_ospfv2(): # XXX: OSPF output when it's not configured is an empty string, # which is not a valid JSON - output = cmd("vtysh -c 'show ip ospf json'") + output = cmdl(["vtysh", "-c", "show ip ospf json"]) if output: return json.loads(output) else: return {} def _get_ospfv3(): - output = cmd("vtysh -c 'show ipv6 ospf6 json'") + output = cmdl(["vtysh", "-c", "show ipv6 ospf6 json"]) if output: return json.loads(output) else: return {} def _get_bgp_summary(): - output = cmd("vtysh -c 'show bgp summary json'") + output = cmdl(["vtysh", "-c", "show bgp summary json"]) return json.loads(output) def _get_isis(): - output = cmd("vtysh -c 'show isis summary json'") + output = cmdl(["vtysh", "-c", "show isis summary json"]) if output: return json.loads(output) else: @@ -190,31 +190,32 @@ def _get_isis(): def _get_arp_table(): from json import loads - from vyos.utils.process import cmd + from vyos.utils.process import cmdl - arp_table = cmd("ip --json -4 neighbor show") + arp_table = cmdl(["ip", "--json", "-4", "neighbor", "show"]) return loads(arp_table) def _get_ndp_table(): from json import loads - arp_table = cmd("ip --json -6 neighbor show") + arp_table = cmdl(["ip", "--json", "-6", "neighbor", "show"]) return loads(arp_table) def _get_nftables_rules(): - nft_rules = cmd("nft list ruleset") + nft_rules = cmdl(["nft", "list", "ruleset"]) return nft_rules def _get_connections(): - from vyos.utils.process import cmd + from vyos.utils.process import cmdl - return cmd("ss -apO") + return cmdl(["ss", "-apO"]) def _get_system_packages(): from re import split - from vyos.utils.process import cmd + from vyos.utils.process import cmdl - dpkg_out = cmd(''' dpkg-query -W -f='${Package} ${Version} ${Architecture} ${db:Status-Abbrev}\n' ''') + dpkg_out = cmdl(['dpkg-query', '-W', '-f', + '${Package} ${Version} ${Architecture} ${db:Status-Abbrev}\n']) pkg_lines = split(r'\n+', dpkg_out) # Discard the header, it's five lines long diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py index a13b48866..d125d513a 100755 --- a/src/op_mode/vrf.py +++ b/src/op_mode/vrf.py @@ -21,7 +21,7 @@ import typing from tabulate import tabulate from vyos.utils.network import get_vrf_members -from vyos.utils.process import cmd +from vyos.utils.process import cmdl import vyos.opmode @@ -31,14 +31,14 @@ def _get_raw_data(name=None): If vrf name is set - get only this name data If vrf name set and not found - return [] """ - output = cmd('ip --json --brief link show type vrf') + output = cmdl(['ip', '--json', '--brief', 'link', 'show', 'type', 'vrf']) data = json.loads(output) if not data: return [] if name: is_vrf_exists = True if [vrf for vrf in data if vrf.get('ifname') == name] else False if is_vrf_exists: - output = cmd(f'ip --json --brief link show dev {name}') + output = cmdl(['ip', '--json', '--brief', 'link', 'show', 'dev', name]) data = json.loads(output) return data return [] diff --git a/src/op_mode/wireguard_client.py b/src/op_mode/wireguard_client.py index 04d91cc47..40d2e69d5 100755 --- a/src/op_mode/wireguard_client.py +++ b/src/op_mode/wireguard_client.py @@ -23,7 +23,7 @@ from ipaddress import ip_interface from vyos.ifconfig import Section from vyos.template import is_ipv4 from vyos.template import is_ipv6 -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import popen if os.geteuid() != 0: @@ -71,8 +71,11 @@ if __name__ == '__main__': if interface not in Section.interfaces('wireguard'): exit(f'WireGuard interface "{interface}" does not exist!') - wg_pubkey = cmd(f'wg show {interface} | grep "public key"').split(':')[-1].lstrip() - wg_port = cmd(f'wg show {interface} | grep "listening port"').split(':')[-1].lstrip() + wg_show_out = cmdl(['wg', 'show', interface]) + wg_pubkey_line = next((l for l in wg_show_out.splitlines() if 'public key' in l), '') + wg_pubkey = wg_pubkey_line.split(':')[-1].lstrip() + wg_port_line = next((l for l in wg_show_out.splitlines() if 'listening port' in l), '') + wg_port = wg_port_line.split(':')[-1].lstrip() # Generate WireGuard private key privkey,_ = popen('wg genkey') diff --git a/src/services/vyos-domain-resolver b/src/services/vyos-domain-resolver index 60fcb7df4..07416406e 100755 --- a/src/services/vyos-domain-resolver +++ b/src/services/vyos-domain-resolver @@ -27,11 +27,12 @@ from vyos.firewall import fqdn_resolve from vyos.ifconfig import WireGuardIf from vyos.remote import download from vyos.utils.commit import commit_in_progress +from vyos.utils.convert import human_to_seconds from vyos.utils.dict import dict_search_args from vyos.utils.kernel import WIREGUARD_REKEY_AFTER_TIME from vyos.utils.file import makedir, chmod_775, write_file, read_file from vyos.utils.network import is_valid_ipv4_address_or_range, is_valid_ipv6_address_or_range -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import run from vyos.xml_ref import get_defaults @@ -45,6 +46,7 @@ base_interfaces = ['interfaces'] firewall_config_dir = "/config/firewall" domain_state = {} +remote_group_last_update = {} ipv4_tables = { 'ip vyos_mangle', @@ -117,7 +119,7 @@ def nft_output(table, set_name, ip_list): def nft_valid_sets(): try: valid_sets = [] - sets_json = cmd('nft --json list sets') + sets_json = cmdl(['nft', '--json', 'list', 'sets']) sets_obj = json.loads(sets_json) for obj in sets_obj['nftables']: @@ -131,9 +133,16 @@ def nft_valid_sets(): except: return [] +def remote_group_interval(remote_config): + # Per-group update interval, falling back to the global resolver interval + if 'interval' in remote_config: + return human_to_seconds(remote_config['interval']) + return timeout + def update_remote_group(config): conf_lines = [] count = 0 + now = time.time() valid_sets = nft_valid_sets() remote_groups = dict_search_args(config, 'group', 'remote_group') @@ -146,6 +155,10 @@ def update_remote_group(config): for set_name, remote_config in remote_groups.items(): if 'url' not in remote_config: continue + interval = remote_group_interval(remote_config) + last_update = remote_group_last_update.get(set_name, 0) + if now - last_update < interval: + continue nft_ip_set_name = f'R_{set_name}' nft_ip6_set_name = f'R6_{set_name}' @@ -157,7 +170,12 @@ def update_remote_group(config): # Attempt to download file, use cached version if download fails try: download(list_file, remote_config['url'], raise_error=True) + remote_group_last_update[set_name] = now except Exception as err: + # Retry failed downloads at the resolver cadence instead of + # waiting the full group interval + retry = min(interval, timeout) + remote_group_last_update[set_name] = now - interval + retry url = urlsplit(remote_config['url']) _, _, netloc = url.netloc.rpartition('@') redacted_url = urlunsplit(url._replace(netloc=netloc, query='', fragment='')) @@ -204,10 +222,11 @@ def update_remote_group(config): count += 1 - nft_conf_str = "\n".join(conf_lines) + "\n" - code = run(f'nft --file -', input=nft_conf_str) + if count: + nft_conf_str = "\n".join(conf_lines) + "\n" + code = run(f'nft --file -', input=nft_conf_str) - logger.info(f'Updated {count} remote-groups in firewall - result: {code}') + logger.info(f'Updated {count} remote-groups in firewall - result: {code}') def update_fqdn(config, node): @@ -320,9 +339,25 @@ if __name__ == '__main__': logger.info(f'interval: {timeout}s - cache: {cache}') + remote_groups = dict_search_args(firewall, 'group', 'remote_group') or {} + + last_fqdn_update = 0 while True: - update_fqdn(firewall, 'firewall') - update_fqdn(nat, 'nat') + now = time.time() + + if now - last_fqdn_update >= timeout: + last_fqdn_update = now + update_fqdn(firewall, 'firewall') + update_fqdn(nat, 'nat') + update_interfaces(interfaces, 'interfaces') + update_remote_group(firewall) - update_interfaces(interfaces, 'interfaces') - time.sleep(timeout) + + # Sleep until the next scheduled update + next_due = [last_fqdn_update + timeout] + for set_name, remote_config in remote_groups.items(): + if 'url' not in remote_config: + continue + next_due.append(remote_group_last_update.get(set_name, 0) + + remote_group_interval(remote_config)) + time.sleep(max(min(next_due) - time.time(), 1)) diff --git a/src/services/vyos-netlinkd b/src/services/vyos-netlinkd index ddc8b1bf3..2cad26070 100755 --- a/src/services/vyos-netlinkd +++ b/src/services/vyos-netlinkd @@ -34,7 +34,7 @@ from vyos.ifconfig import Section from vyos.utils.boot import boot_configuration_complete from vyos.utils.commit import commit_in_progress2 from vyos.utils.dict import dict_search -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.process import is_systemd_service_active from vyos.utils.process import stop_systemd_unit @@ -149,7 +149,7 @@ def _handle_dhcp_events(operstate: Optional[str], ifname: str) -> None: # we will re-start the service and thus cancel the backoff time. if 'dhcp' in tmp: syslog.syslog(syslog.LOG_DEBUG, f'Restarting {systemdV4_service}...') - cmd(f'systemctl restart {systemdV4_service}') + cmdl(['systemctl', 'restart', systemdV4_service]) if 'dhcpv6' in tmp: v6_restart = True @@ -158,7 +158,7 @@ def _handle_dhcp_events(operstate: Optional[str], ifname: str) -> None: if v6_restart: syslog.syslog(syslog.LOG_DEBUG, f'Restarting {systemdV6_service}...') - cmd(f'systemctl restart {systemdV6_service}') + cmdl(['systemctl', 'restart', systemdV6_service]) return None diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index c6c3fa9d0..5f8f3442b 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -26,7 +26,7 @@ from queue import Queue from logging.handlers import SysLogHandler from vyos.configquery import ConfigTreeQuery -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.dict import dict_search from vyos.utils.commit import commit_in_progress @@ -39,7 +39,7 @@ logger.addHandler(logs_handler_syslog) logger.setLevel(logging.DEBUG) mdns_running_file = '/run/mdns_vrrp_active' -mdns_update_command = 'sudo /usr/libexec/vyos/conf_mode/service_mdns_repeater.py' +mdns_update_command = '/usr/libexec/vyos/conf_mode/service_mdns_repeater.py' # class for all operations class KeepalivedFifo: @@ -92,7 +92,7 @@ class KeepalivedFifo: def _run_command(self, command): logger.debug(f'Running the command: {command}') try: - cmd(command) + cmdl(command.split()) except OSError as err: logger.error(f'Unable to execute command "{command}": {err}') @@ -127,7 +127,7 @@ class KeepalivedFifo: # check and run commands for VRRP instances if n_type == 'INSTANCE': if os.path.exists(mdns_running_file): - cmd(mdns_update_command) + cmdl(mdns_update_command.split(), sudo=True) tmp = dict_search(f'group.{n_name}.transition_script.{n_state.lower()}', self.vrrp_config_dict) if tmp != None: @@ -135,7 +135,7 @@ class KeepalivedFifo: # check and run commands for VRRP sync groups elif n_type == 'GROUP': if os.path.exists(mdns_running_file): - cmd(mdns_update_command) + cmdl(mdns_update_command.split(), sudo=True) tmp = dict_search(f'sync_group.{n_name}.transition_script.{n_state.lower()}', self.vrrp_config_dict) if tmp != None: diff --git a/src/tests/test_configverify.py b/src/tests/test_configverify.py index 8f80365d7..72ed8c084 100644 --- a/src/tests/test_configverify.py +++ b/src/tests/test_configverify.py @@ -14,7 +14,7 @@ from unittest import TestCase from vyos.configverify import verify_diffie_hellman_length -from vyos.utils.process import cmd +from vyos.utils.process import cmdl dh_file = '/tmp/dh.pem' @@ -27,5 +27,5 @@ class TestDictSearch(TestCase): def test_dh_key_512(self): key_len = '512' - cmd(f'openssl dhparam -out {dh_file} {key_len}') + cmdl(['openssl', 'dhparam', '-out', dh_file, key_len]) self.assertTrue(verify_diffie_hellman_length(dh_file, key_len)) diff --git a/src/udev/vyos_net_name b/src/udev/vyos_net_name index 568734cb7..f5c3b9f1b 100755 --- a/src/udev/vyos_net_name +++ b/src/udev/vyos_net_name @@ -25,7 +25,7 @@ from sys import argv from vyos.configtree import ConfigTree from vyos.defaults import directories -from vyos.utils.process import cmd +from vyos.utils.process import cmdl from vyos.utils.boot import boot_configuration_complete from vyos.utils.locking import Lock from vyos.migrate import ConfigMigrate @@ -87,7 +87,7 @@ def get_biosdevname(ifname: str) -> str: time.sleep(1) try: - biosname = cmd(f'/sbin/biosdevname --policy all_ethN -i {ifname}') + biosname = cmdl(['/sbin/biosdevname', '--policy', 'all_ethN', '-i', ifname]) except Exception as e: logger.error(f'biosdevname error: {e}') biosname = '' diff --git a/src/validators/timezone b/src/validators/timezone index dd3e0654d..fe8e10cb6 100755 --- a/src/validators/timezone +++ b/src/validators/timezone @@ -17,7 +17,7 @@ import argparse import sys -from vyos.utils.process import cmd +from vyos.utils.process import cmdl if __name__ == '__main__': @@ -25,7 +25,7 @@ if __name__ == '__main__': parser.add_argument("--validate", action="store", required=True, help="Check if timezone is valid") args = parser.parse_args() - tz_data = cmd('timedatectl list-timezones') + tz_data = cmdl(['timedatectl', 'list-timezones']) tz_data = tz_data.split('\n') if args.validate not in tz_data: |
