diff options
Diffstat (limited to 'src')
38 files changed, 1188 insertions, 662 deletions
diff --git a/src/completion/list_ipsec_profile_tunnels.py b/src/completion/list_ipsec_profile_tunnels.py new file mode 100644 index 000000000..df6c52f6d --- /dev/null +++ b/src/completion/list_ipsec_profile_tunnels.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import argparse + +from vyos.config import Config +from vyos.util import dict_search + +def get_tunnels_from_ipsecprofile(profile): + config = Config() + base = ['vpn', 'ipsec', 'profile', profile, 'bind'] + profile_conf = config.get_config_dict(base, effective=True, key_mangling=('-', '_')) + tunnels = [] + + try: + for tunnel in (dict_search('bind.tunnel', profile_conf) or []): + tunnels.append(tunnel) + except: + pass + + return tunnels + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--profile", type=str, help="List tunnels per profile") + args = parser.parse_args() + + tunnels = [] + + tunnels = get_tunnels_from_ipsecprofile(args.profile) + + print(" ".join(tunnels)) + diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index bf83416b2..aceb27fb0 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -16,6 +16,7 @@ import os +from hashlib import sha256 from ipaddress import ip_address from ipaddress import ip_network from json import dumps as json_write @@ -25,9 +26,10 @@ from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configdict import is_node_changed +from vyos.configverify import verify_vrf +from vyos.ifconfig import Interface from vyos.util import call from vyos.util import cmd -from vyos.util import dict_search from vyos.util import run from vyos.util import rc_cmd from vyos.util import write_file @@ -166,21 +168,29 @@ def verify(container): raise ConfigError(f'Container network "{network_name}" does not exist!') if 'address' in container_config['network'][network_name]: - address = container_config['network'][network_name]['address'] - network = None - if is_ipv4(address): - network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0] - elif is_ipv6(address): - network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0] - - # Specified container IP address must belong to network prefix - if ip_address(address) not in ip_network(network): - raise ConfigError(f'Used container address "{address}" not in network "{network}"!') - - # We can not use the first IP address of a network prefix as this is used by podman - if ip_address(address) == ip_network(network)[1]: - raise ConfigError(f'IP address "{address}" can not be used for a container, '\ - 'reserved for the container engine!') + cnt_ipv4 = 0 + cnt_ipv6 = 0 + for address in container_config['network'][network_name]['address']: + network = None + if is_ipv4(address): + network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0] + cnt_ipv4 += 1 + elif is_ipv6(address): + network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0] + cnt_ipv6 += 1 + + # Specified container IP address must belong to network prefix + if ip_address(address) not in ip_network(network): + raise ConfigError(f'Used container address "{address}" not in network "{network}"!') + + # We can not use the first IP address of a network prefix as this is used by podman + if ip_address(address) == ip_network(network)[1]: + raise ConfigError(f'IP address "{address}" can not be used for a container, '\ + 'reserved for the container engine!') + + if cnt_ipv4 > 1 or cnt_ipv6 > 1: + raise ConfigError(f'Only one IP address per address family can be used for '\ + f'container "{name}". {cnt_ipv4} IPv4 and {cnt_ipv6} IPv6 address(es)!') if 'device' in container_config: for dev, dev_config in container_config['device'].items(): @@ -242,6 +252,8 @@ def verify(container): if v6_prefix > 1: raise ConfigError(f'Only one IPv6 prefix can be defined for network "{network}"!') + # Verify VRF exists + verify_vrf(network_config) # A network attached to a container can not be deleted if {'network_remove', 'name'} <= set(container): @@ -250,9 +262,11 @@ def verify(container): if 'network' in container_config and network in container_config['network']: raise ConfigError(f'Can not remove network "{network}", used by container "{container}"!') - if 'registry' in container and 'authentication' in container['registry']: - for registry, registry_config in container['registry']['authentication'].items(): - if not {'username', 'password'} <= set(registry_config): + if 'registry' in container: + for registry, registry_config in container['registry'].items(): + if 'authentication' not in registry_config: + continue + if not {'username', 'password'} <= set(registry_config['authentication']): raise ConfigError('If registry username or or password is defined, so must be the other!') return None @@ -338,9 +352,13 @@ def generate_run_arguments(name, container_config): ip_param = '' networks = ",".join(container_config['network']) for network in container_config['network']: - if 'address' in container_config['network'][network]: - address = container_config['network'][network]['address'] - ip_param = f'--ip {address}' + if 'address' not in container_config['network'][network]: + continue + for address in container_config['network'][network]['address']: + if is_ipv6(address): + ip_param += f' --ip6 {address}' + else: + ip_param += f' --ip {address}' return f'{container_base_cmd} --net {networks} {ip_param} {entrypoint} {image} {command} {command_arguments}'.strip() @@ -355,33 +373,26 @@ def generate(container): if 'network' in container: for network, network_config in container['network'].items(): tmp = { - 'cniVersion' : '0.4.0', - 'name' : network, - 'plugins' : [{ - 'type': 'bridge', - 'bridge': f'cni-{network}', - 'isGateway': True, - 'ipMasq': False, - 'hairpinMode': False, - 'ipam' : { - 'type': 'host-local', - 'routes': [], - 'ranges' : [], - }, - }] + 'name': network, + 'id' : sha256(f'{network}'.encode()).hexdigest(), + 'driver': 'bridge', + 'network_interface': f'pod-{network}', + 'subnets': [], + 'ipv6_enabled': False, + 'internal': False, + 'dns_enabled': True, + 'ipam_options': { + 'driver': 'host-local' + } } - for prefix in network_config['prefix']: - net = [{'gateway' : inc_ip(prefix, 1), 'subnet' : prefix}] - tmp['plugins'][0]['ipam']['ranges'].append(net) + net = {'subnet' : prefix, 'gateway' : inc_ip(prefix, 1)} + tmp['subnets'].append(net) - # install per address-family default orutes - default_route = '0.0.0.0/0' if is_ipv6(prefix): - default_route = '::/0' - tmp['plugins'][0]['ipam']['routes'].append({'dst': default_route}) + tmp['ipv6_enabled'] = True - write_file(f'/etc/cni/net.d/{network}.conflist', json_write(tmp, indent=2)) + write_file(f'/etc/containers/networks/{network}.json', json_write(tmp, indent=2)) if 'registry' in container: cmd = f'podman logout --all' @@ -432,10 +443,7 @@ def apply(container): # Delete old networks if needed if 'network_remove' in container: for network in container['network_remove']: - call(f'podman network rm {network}') - tmp = f'/etc/cni/net.d/{network}.conflist' - if os.path.exists(tmp): - os.unlink(tmp) + call(f'podman network rm {network} >/dev/null 2>&1') # Add container disabled_new = False @@ -459,12 +467,26 @@ def apply(container): os.unlink(file_path) continue - if name in dict_search('container_restart', container): + if 'container_restart' in container and name in container['container_restart']: cmd(f'systemctl restart vyos-container-{name}.service') if disabled_new: call('systemctl daemon-reload') + # Start network and assign it to given VRF if requested. this can only be done + # after the containers got started as the podman network interface will + # only be enabled by the first container and yet I do not know how to enable + # the network interface in advance + if 'network' in container: + for network, network_config in container['network'].items(): + network_name = f'pod-{network}' + # T5147: Networks are started only as soon as there is a consumer. + # If only a network is created in the first place, no need to assign + # it to a VRF as there's no consumer, yet. + if os.path.exists(f'/sys/class/net/{network_name}'): + tmp = Interface(network_name) + tmp.set_vrf(network_config.get('vrf', '')) + return None if __name__ == '__main__': diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index c41a442df..190587980 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -282,6 +282,9 @@ def verify_rule(firewall, rule_conf, ipv6): if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group') + if 'port' in side_conf and dict_search_args(side_conf, 'group', 'port_group'): + raise ConfigError(f'{side} port-group and port cannot both be defined') + if 'log_options' in rule_conf: if 'log' not in rule_conf or 'enable' not in rule_conf['log']: raise ConfigError('log-options defined, but log is not enable') diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index ce5e63928..b0c38e8d3 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -159,6 +159,8 @@ def generate(https): server_block['port'] = data.get('listen-port', '443') name = data.get('server-name', ['_']) server_block['name'] = name + allow_client = data.get('allow-client', {}) + server_block['allow_client'] = allow_client.get('address', []) server_block_list.append(server_block) # get certificate data diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 13d84a6fe..6f227b0d1 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -597,7 +597,7 @@ def generate_pki_files(openvpn): def generate(openvpn): interface = openvpn['ifname'] directory = os.path.dirname(cfg_file.format(**openvpn)) - plugin_dir = '/usr/lib/openvpn' + openvpn['plugin_dir'] = '/usr/lib/openvpn' # create base config directory on demand makedir(directory, user, group) # enforce proper permissions on /run/openvpn diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index cf553f0e8..66505e58d 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -484,26 +484,15 @@ def generate(bgp): if not bgp or 'deleted' in bgp: return None - bgp['protocol'] = 'bgp' # required for frr/vrf.route-map.frr.j2 - bgp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', bgp) bgp['frr_bgpd_config'] = render_to_string('frr/bgpd.frr.j2', bgp) - return None def apply(bgp): bgp_daemon = 'bgpd' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'(\s+)?ip protocol bgp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in bgp: - frr_cfg.add_before(frr.default_add_before, bgp['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) - # Generate empty helper string which can be ammended to FRR commands, it # will be either empty (default VRF) or contain the "vrf <name" statement vrf = '' diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py index c1a1a45e1..609b39065 100755 --- a/src/conf_mode/protocols_eigrp.py +++ b/src/conf_mode/protocols_eigrp.py @@ -69,8 +69,6 @@ def get_config(config=None): # Merge policy dict into "regular" config dict eigrp = dict_merge(tmp, eigrp) - import pprint - pprint.pprint(eigrp) return eigrp def verify(eigrp): @@ -80,24 +78,14 @@ def generate(eigrp): if not eigrp or 'deleted' in eigrp: return None - eigrp['protocol'] = 'eigrp' # required for frr/vrf.route-map.frr.j2 - eigrp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', eigrp) eigrp['frr_eigrpd_config'] = render_to_string('frr/eigrpd.frr.j2', eigrp) def apply(eigrp): eigrp_daemon = 'eigrpd' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'(\s+)?ip protocol eigrp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in eigrp: - frr_cfg.add_before(frr.default_add_before, eigrp['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) - # Generate empty helper string which can be ammended to FRR commands, it # will be either empty (default VRF) or contain the "vrf <name" statement vrf = '' diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index cb8ea3be4..af2937db8 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -203,7 +203,7 @@ def verify(isis): if list(set(global_range) & set(local_range)): raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\ f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!') - + # Check for a blank or invalid value per prefix if dict_search('segment_routing.prefix', isis): for prefix, prefix_config in isis['segment_routing']['prefix'].items(): @@ -218,7 +218,7 @@ def verify(isis): if dict_search('segment_routing.prefix', isis): for prefix, prefix_config in isis['segment_routing']['prefix'].items(): if 'absolute' in prefix_config: - if ("explicit_null" in prefix_config['absolute']) and ("no_php_flag" in prefix_config['absolute']): + if ("explicit_null" in prefix_config['absolute']) and ("no_php_flag" in prefix_config['absolute']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') elif 'index' in prefix_config: @@ -232,25 +232,15 @@ def generate(isis): if not isis or 'deleted' in isis: return None - isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.j2 - isis['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', isis) isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.j2', isis) return None def apply(isis): isis_daemon = 'isisd' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section('(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in isis: - frr_cfg.add_before(frr.default_add_before, isis['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) - # Generate empty helper string which can be ammended to FRR commands, it # will be either empty (default VRF) or contain the "vrf <name" statement vrf = '' diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index eb64afa0c..fbb876123 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -256,25 +256,15 @@ def generate(ospf): if not ospf or 'deleted' in ospf: return None - ospf['protocol'] = 'ospf' # required for frr/vrf.route-map.frr.j2 - ospf['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', ospf) ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.j2', ospf) return None def apply(ospf): ospf_daemon = 'ospfd' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section('(\s+)?ip protocol ospf route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in ospf: - frr_cfg.add_before(frr.default_add_before, ospf['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) - # Generate empty helper string which can be ammended to FRR commands, it # will be either empty (default VRF) or contain the "vrf <name" statement vrf = '' @@ -292,6 +282,7 @@ def apply(ospf): if 'frr_ospfd_config' in ospf: frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config']) + frr_cfg.commit_configuration(ospf_daemon) return None diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index 1e2c02d03..ee1fdd399 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -146,25 +146,15 @@ def generate(ospfv3): if not ospfv3 or 'deleted' in ospfv3: return None - ospfv3['protocol'] = 'ospf6' # required for frr/vrf.route-map.v6.frr.j2 - ospfv3['frr_zebra_config'] = render_to_string('frr/vrf.route-map.v6.frr.j2', ospfv3) ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.j2', ospfv3) return None def apply(ospfv3): ospf6_daemon = 'ospf6d' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section('(\s+)?ipv6 protocol ospf6 route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in ospfv3: - frr_cfg.add_before(frr.default_add_before, ospfv3['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) - # Generate empty helper string which can be ammended to FRR commands, it # will be either empty (default VRF) or contain the "vrf <name" statement vrf = '' diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 3e5ebb805..7b6150696 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -105,20 +105,14 @@ def generate(static): def apply(static): static_daemon = 'staticd' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'^ip protocol static route-map [-a-zA-Z0-9.]+', '') - frr_cfg.commit_configuration(zebra_daemon) frr_cfg.load_configuration(static_daemon) if 'vrf' in static: vrf = static['vrf'] - frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit', remove_stop_mark=True) + frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit-vrf', remove_stop_mark=True) else: frr_cfg.modify_section(r'^ip route .*') frr_cfg.modify_section(r'^ipv6 route .*') diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 363408679..47510ce80 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import socket import json from sys import exit @@ -57,6 +58,13 @@ def get_nft_filter_chains(): return chain_list +def get_hostname() -> str: + try: + hostname = socket.getfqdn() + except socket.gaierror: + hostname = socket.gethostname() + return hostname + def get_config(config=None): if config: conf = config @@ -79,6 +87,7 @@ def get_config(config=None): monitoring = dict_merge(default_values, monitoring) monitoring['custom_scripts_dir'] = custom_scripts_dir + monitoring['hostname'] = get_hostname() monitoring['interfaces_ethernet'] = Section.interfaces('ethernet', vlan=False) monitoring['nft_chains'] = get_nft_filter_chains() diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index 0c5063ed3..95865c690 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -18,12 +18,15 @@ from sys import exit from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_route_map +from vyos.template import render_to_string from vyos.util import call from vyos.util import dict_search from vyos.util import sysctl_write from vyos.util import write_file from vyos.xml import defaults from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() @@ -40,13 +43,30 @@ def get_config(config=None): default_values = defaults(base) opt = dict_merge(default_values, opt) + # When working with FRR we need to know the corresponding address-family + opt['afi'] = 'ip' + + # We also need the route-map information from the config + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'], + get_first_key=True)}} + # Merge policy dict into "regular" config dict + opt = dict_merge(tmp, opt) return opt def verify(opt): - pass + if 'protocol' in opt: + for protocol, protocol_options in opt['protocol'].items(): + if 'route_map' in protocol_options: + verify_route_map(protocol_options['route_map'], opt) + return def generate(opt): - pass + if 'protocol' in opt: + opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt) + return def apply(opt): # Apply ARP threshold values @@ -78,6 +98,18 @@ def apply(opt): value = '1' if (tmp != None) else '0' sysctl_write('net.ipv4.fib_multipath_hash_policy', value) + if 'protocol' in opt: + zebra_daemon = 'zebra' + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + # The route-map used for the FIB (zebra) is part of the zebra daemon + frr_cfg.load_configuration(zebra_daemon) + frr_cfg.modify_section(r'ip protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') + if 'frr_zebra_config' in opt: + frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config']) + frr_cfg.commit_configuration(zebra_daemon) + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py index 26aacf46b..b6d3a79c3 100755 --- a/src/conf_mode/system-ipv6.py +++ b/src/conf_mode/system-ipv6.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,11 +19,14 @@ import os from sys import exit from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_route_map +from vyos.template import render_to_string from vyos.util import dict_search from vyos.util import sysctl_write from vyos.util import write_file from vyos.xml import defaults from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() @@ -41,13 +44,30 @@ def get_config(config=None): default_values = defaults(base) opt = dict_merge(default_values, opt) + # When working with FRR we need to know the corresponding address-family + opt['afi'] = 'ipv6' + + # We also need the route-map information from the config + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'], + get_first_key=True)}} + # Merge policy dict into "regular" config dict + opt = dict_merge(tmp, opt) return opt def verify(opt): - pass + if 'protocol' in opt: + for protocol, protocol_options in opt['protocol'].items(): + if 'route_map' in protocol_options: + verify_route_map(protocol_options['route_map'], opt) + return def generate(opt): - pass + if 'protocol' in opt: + opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt) + return def apply(opt): # configure multipath @@ -78,6 +98,18 @@ def apply(opt): if name == 'accept_dad': write_file(os.path.join(root, name), value) + if 'protocol' in opt: + zebra_daemon = 'zebra' + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + # The route-map used for the FIB (zebra) is part of the zebra daemon + frr_cfg.load_configuration(zebra_daemon) + frr_cfg.modify_section(r'ipv6 protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') + if 'frr_zebra_config' in opt: + frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config']) + frr_cfg.commit_configuration(zebra_daemon) + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index d15fe399d..fbb013cf3 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -40,6 +40,7 @@ from vyos import airbag airbag.enable() autologout_file = "/etc/profile.d/autologout.sh" +limits_file = "/etc/security/limits.d/10-vyos.conf" radius_config_file = "/etc/pam_radius_auth.conf" # LOGIN_TIMEOUT from /etc/loign.defs minus 10 sec @@ -164,6 +165,9 @@ def verify(login): if ipv6_count > 1: raise ConfigError('Only one IPv6 source-address can be set!') + if 'max_login_session' in login and 'timeout' not in login: + raise ConfigError('"login timeout" must be configured!') + return None @@ -226,6 +230,14 @@ def generate(login): if os.path.isfile(radius_config_file): os.unlink(radius_config_file) + # /etc/security/limits.d/10-vyos.conf + if 'max_login_session' in login: + render(limits_file, 'login/limits.j2', login, + permission=0o644, user='root', group='root') + else: + if os.path.isfile(limits_file): + os.unlink(limits_file) + if 'timeout' in login: render(autologout_file, 'login/autologout.j2', login, permission=0o755, user='root', group='root') diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index 7550c411e..986a19972 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -44,6 +44,8 @@ default_pptp = { 'radius_nas_ip' : '', 'radius_source_address' : '', 'radius_shaper_attr' : '', + 'radius_shaper_enable': False, + 'radius_shaper_multiplier': '', 'radius_shaper_vendor': '', 'radius_dynamic_author' : '', 'chap_secrets_file': pptp_chap_secrets, # used in Jinja2 template @@ -183,15 +185,18 @@ def get_config(config=None): pptp['radius_dynamic_author'] = dae + # Rate limit + if conf.exists(['rate-limit', 'attribute']): + pptp['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute']) + if conf.exists(['rate-limit', 'enable']): - pptp['radius_shaper_attr'] = 'Filter-Id' - c_attr = ['rate-limit', 'enable', 'attribute'] - if conf.exists(c_attr): - pptp['radius_shaper_attr'] = conf.return_value(c_attr) - - c_vendor = ['rate-limit', 'enable', 'vendor'] - if conf.exists(c_vendor): - pptp['radius_shaper_vendor'] = conf.return_value(c_vendor) + pptp['radius_shaper_enable'] = True + + if conf.exists(['rate-limit', 'multiplier']): + pptp['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier']) + + if conf.exists(['rate-limit', 'vendor']): + pptp['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor']) conf.set_level(base_path) if conf.exists(['client-ip-pool']): diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index c17cca3bd..a7ef4cb5c 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -20,9 +20,12 @@ from sys import exit from json import loads from vyos.config import Config +from vyos.configdict import dict_merge from vyos.configdict import node_changed +from vyos.configverify import verify_route_map from vyos.ifconfig import Interface from vyos.template import render +from vyos.template import render_to_string from vyos.util import call from vyos.util import cmd from vyos.util import dict_search @@ -99,6 +102,14 @@ def get_config(config=None): routes = vrf_routing(conf, name) if routes: vrf['vrf_remove'][name]['route'] = routes + # We also need the route-map information from the config + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'], + get_first_key=True)}} + # Merge policy dict into "regular" config dict + vrf = dict_merge(tmp, vrf) return vrf def verify(vrf): @@ -116,35 +127,50 @@ def verify(vrf): reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", "get", "inet", "mtu", "link", "type", "vrf"] table_ids = [] - for name, config in vrf['name'].items(): + for name, vrf_config in vrf['name'].items(): # Reserved VRF names if name in reserved_names: raise ConfigError(f'VRF name "{name}" is reserved and connot be used!') # table id is mandatory - if 'table' not in config: + if 'table' not in vrf_config: raise ConfigError(f'VRF "{name}" table id is mandatory!') # routing table id can't be changed - OS restriction if os.path.isdir(f'/sys/class/net/{name}'): tmp = str(dict_search('linkinfo.info_data.table', get_interface_config(name))) - if tmp and tmp != config['table']: + if tmp and tmp != vrf_config['table']: raise ConfigError(f'VRF "{name}" table id modification not possible!') # VRf routing table ID must be unique on the system - if config['table'] in table_ids: + if vrf_config['table'] in table_ids: raise ConfigError(f'VRF "{name}" table id is not unique!') - table_ids.append(config['table']) + table_ids.append(vrf_config['table']) + + tmp = dict_search('ip.protocol', vrf_config) + if tmp != None: + for protocol, protocol_options in tmp.items(): + if 'route_map' in protocol_options: + verify_route_map(protocol_options['route_map'], vrf) + + tmp = dict_search('ipv6.protocol', vrf_config) + if tmp != None: + for protocol, protocol_options in tmp.items(): + if 'route_map' in protocol_options: + verify_route_map(protocol_options['route_map'], vrf) return None def generate(vrf): + # Render iproute2 VR helper names render(config_file, 'iproute2/vrf.conf.j2', vrf) # Render nftables zones config render(nft_vrf_config, 'firewall/nftables-vrf-zones.j2', vrf) - return None + # Render VRF Kernel/Zebra route-map filters + vrf['frr_zebra_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf) + return None def apply(vrf): # Documentation @@ -249,6 +275,17 @@ def apply(vrf): nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}' cmd(f'nft {nft_add_element}') + # Apply FRR filters + zebra_daemon = 'zebra' + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + # The route-map used for the FIB (zebra) is part of the zebra daemon + frr_cfg.load_configuration(zebra_daemon) + frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True) + if 'frr_zebra_config' in vrf: + frr_cfg.add_before(frr.default_add_before, vrf['frr_zebra_config']) + frr_cfg.commit_configuration(zebra_daemon) # return to default lookup preference when no VRF is configured if 'name' not in vrf: diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py deleted file mode 100755 index 585fdbebf..000000000 --- a/src/conf_mode/vrf_vni.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020-2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sys import argv -from sys import exit - -from vyos.config import Config -from vyos.template import render_to_string -from vyos import ConfigError -from vyos import frr -from vyos import airbag -airbag.enable() - -frr_daemon = 'zebra' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - base = ['vrf'] - vrf = conf.get_config_dict(base, get_first_key=True) - return vrf - -def verify(vrf): - return None - -def generate(vrf): - vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.j2', vrf) - return None - -def apply(vrf): - # add configuration to FRR - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True) - if 'new_frr_config' in vrf: - frr_cfg.add_before(frr.default_add_before, vrf['new_frr_config']) - frr_cfg.commit_configuration(frr_daemon) - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/03-vyatta-dhclient-hook index 49bb18372..49bb18372 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/03-vyatta-dhclient-hook diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/99-run-user-hooks b/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks index 442419d79..442419d79 100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/99-run-user-hooks +++ b/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook index 1f1926e17..1f1926e17 100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py index bf25a7331..688c7af2a 100755 --- a/src/etc/opennhrp/opennhrp-script.py +++ b/src/etc/opennhrp/opennhrp-script.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,7 +17,7 @@ import os import re import sys -import vici +import vyos.ipsec from json import loads from pathlib import Path @@ -51,9 +51,8 @@ def vici_get_ipsec_uniqueid(conn: str, src_nbma: str, logger.info( f'Resolving IKE unique ids for: conn: {conn}, ' f'src_nbma: {src_nbma}, dst_nbma: {dst_nbma}') - session: vici.Session = vici.Session() list_ikeid: list[str] = [] - list_sa = session.list_sas({'ike': conn}) + list_sa: list = vyos.ipsec.get_vici_sas_by_name(conn, None) for sa in list_sa: if sa[conn]['local-host'].decode('ascii') == src_nbma \ and sa[conn]['remote-host'].decode('ascii') == dst_nbma: @@ -78,16 +77,7 @@ def vici_ike_terminate(list_ikeid: list[str]) -> bool: return False try: - session = vici.Session() - for ikeid in list_ikeid: - logger.info(f'Terminating IKE SA with id {ikeid}') - session_generator = session.terminate( - {'ike-id': ikeid, 'timeout': '-1'}) - # a dummy `for` loop is required because of requirements - # from vici. Without a full iteration on the output, the - # command to vici may not be executed completely - for _ in session_generator: - pass + vyos.ipsec.terminate_vici_ikeid_list(list_ikeid) return True except Exception as err: logger.error(f'Failed to terminate SA for IKE ids {list_ikeid}: {err}') @@ -180,19 +170,7 @@ def vici_initiate(conn: str, child_sa: str, src_addr: str, f'Trying to initiate connection. Name: {conn}, child sa: {child_sa}, ' f'src_addr: {src_addr}, dst_addr: {dest_addr}') try: - session = vici.Session() - session_generator = session.initiate({ - 'ike': conn, - 'child': child_sa, - 'timeout': '-1', - 'my-host': src_addr, - 'other-host': dest_addr - }) - # a dummy `for` loop is required because of requirements - # from vici. Without a full iteration on the output, the - # command to vici may not be executed completely - for _ in session_generator: - pass + vyos.ipsec.vici_initiate(conn, child_sa, src_addr, dest_addr) return True except Exception as err: logger.error(f'Unable to initiate connection {err}') @@ -218,8 +196,11 @@ def vici_terminate(conn: str, src_addr: str, dest_addr: str) -> None: f'No active sessions found for IKE profile {conn}, ' f'local NBMA {src_addr}, remote NBMA {dest_addr}') else: - vici_ike_terminate(ikeid_list) - + try: + vyos.ipsec.terminate_vici_ikeid_list(ikeid_list) + except Exception as err: + logger.error( + f'Failed to terminate SA for IKE ids {ikeid_list}: {err}') def iface_up(interface: str) -> None: """Proceed tunnel interface UP event diff --git a/src/etc/systemd/system/hostapd@.service.d/override.conf b/src/etc/systemd/system/hostapd@.service.d/override.conf index bb8e81d7a..926c07f94 100644 --- a/src/etc/systemd/system/hostapd@.service.d/override.conf +++ b/src/etc/systemd/system/hostapd@.service.d/override.conf @@ -1,6 +1,8 @@ [Unit] After= After=vyos-router.service +ConditionFileNotEmpty= +ConditionFileNotEmpty=/run/hostapd/%i.conf [Service] WorkingDirectory=/run/hostapd diff --git a/src/helpers/vyos-failover.py b/src/helpers/vyos-failover.py index 0de945f20..03fb42f57 100755 --- a/src/helpers/vyos-failover.py +++ b/src/helpers/vyos-failover.py @@ -30,7 +30,7 @@ my_name = Path(__file__).stem def is_route_exists(route, gateway, interface, metric): """Check if route with expected gateway, dev and metric exists""" - rc, data = rc_cmd(f'sudo ip --json route show protocol failover {route} ' + rc, data = rc_cmd(f'ip --json route show protocol failover {route} ' f'via {gateway} dev {interface} metric {metric}') if rc == 0: data = json.loads(data) @@ -72,6 +72,7 @@ def get_best_route_options(route, debug=False): f'best_metric: {best_metric}, best_iface: {best_interface}') return best_gateway, best_interface, best_metric + def is_port_open(ip, port): """ Check connection to remote host and port @@ -91,32 +92,54 @@ def is_port_open(ip, port): finally: s.close() -def is_target_alive(target=None, iface='', proto='icmp', port=None, debug=False): - """ - Host availability check by ICMP, ARP, TCP - Return True if target checks is successful - % is_target_alive('192.0.2.1', 'eth1', proto='arp') - True +def is_target_alive(target_list=None, iface='', proto='icmp', port=None, debug=False): + """Check the availability of each target in the target_list using + the specified protocol ICMP, ARP, TCP + + Args: + target_list (list): A list of IP addresses or hostnames to check. + iface (str): The name of the network interface to use for the check. + proto (str): The protocol to use for the check. Options are 'icmp', 'arp', or 'tcp'. + port (int): The port number to use for the TCP check. Only applicable if proto is 'tcp'. + debug (bool): If True, print debug information during the check. + + Returns: + bool: True if all targets are reachable, False otherwise. + + Example: + % is_target_alive(['192.0.2.1', '192.0.2.5'], 'eth1', proto='arp') + True """ if iface != '': iface = f'-I {iface}' - if proto == 'icmp': - command = f'/usr/bin/ping -q {target} {iface} -n -c 2 -W 1' - rc, response = rc_cmd(command) - if debug: print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') - if rc == 0: - return True - elif proto == 'arp': - command = f'/usr/bin/arping -b -c 2 -f -w 1 -i 1 {iface} {target}' - rc, response = rc_cmd(command) - if debug: print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') - if rc == 0: - return True - elif proto == 'tcp' and port is not None: - return True if is_port_open(target, port) else False - else: - return False + + for target in target_list: + match proto: + case 'icmp': + command = f'/usr/bin/ping -q {target} {iface} -n -c 2 -W 1' + rc, response = rc_cmd(command) + if debug: + print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') + if rc != 0: + return False + + case 'arp': + command = f'/usr/bin/arping -b -c 2 -f -w 1 -i 1 {iface} {target}' + rc, response = rc_cmd(command) + if debug: + print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') + if rc != 0: + return False + + case _ if proto == 'tcp' and port is not None: + if not is_port_open(target, port): + return False + + case _: + return False + + return True if __name__ == '__main__': diff --git a/src/migration-scripts/bgp/3-to-4 b/src/migration-scripts/bgp/3-to-4 new file mode 100755 index 000000000..0df2fbec4 --- /dev/null +++ b/src/migration-scripts/bgp/3-to-4 @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T5150: Rework CLI definitions to apply route-maps between routing daemons +# and zebra/kernel + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +bgp_base = ['protocols', 'bgp'] +# Check if BGP is configured - if so, migrate the CLI node +if config.exists(bgp_base): + if config.exists(bgp_base + ['route-map']): + tmp = config.return_value(bgp_base + ['route-map']) + + config.set(['system', 'ip', 'protocol', 'bgp', 'route-map'], value=tmp) + config.set_tag(['system', 'ip', 'protocol']) + config.delete(bgp_base + ['route-map']) + + +# Check if vrf names are configured. Check if BGP is configured - if so, migrate +# the CLI node(s) +if config.exists(['vrf', 'name']): + for vrf in config.list_nodes(['vrf', 'name']): + vrf_base = ['vrf', 'name', vrf] + if config.exists(vrf_base + ['protocols', 'bgp', 'route-map']): + tmp = config.return_value(vrf_base + ['protocols', 'bgp', 'route-map']) + + config.set(vrf_base + ['ip', 'protocol', 'bgp', 'route-map'], value=tmp) + config.set_tag(vrf_base + ['ip', 'protocol', 'bgp']) + config.delete(vrf_base + ['protocols', 'bgp', 'route-map']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/isis/2-to-3 b/src/migration-scripts/isis/2-to-3 new file mode 100755 index 000000000..4490feb0a --- /dev/null +++ b/src/migration-scripts/isis/2-to-3 @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T5150: Rework CLI definitions to apply route-maps between routing daemons +# and zebra/kernel + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +isis_base = ['protocols', 'isis'] +# Check if IS-IS is configured - if so, migrate the CLI node +if config.exists(isis_base): + if config.exists(isis_base + ['route-map']): + tmp = config.return_value(isis_base + ['route-map']) + + config.set(['system', 'ip', 'protocol', 'isis', 'route-map'], value=tmp) + config.set_tag(['system', 'ip', 'protocol']) + config.delete(isis_base + ['route-map']) + +# Check if vrf names are configured. Check if IS-IS is configured - if so, +# migrate the CLI node(s) +if config.exists(['vrf', 'name']): + for vrf in config.list_nodes(['vrf', 'name']): + vrf_base = ['vrf', 'name', vrf] + if config.exists(vrf_base + ['protocols', 'isis', 'route-map']): + tmp = config.return_value(vrf_base + ['protocols', 'isis', 'route-map']) + + config.set(vrf_base + ['ip', 'protocol', 'isis', 'route-map'], value=tmp) + config.set_tag(vrf_base + ['ip', 'protocol', 'isis']) + config.delete(vrf_base + ['protocols', 'isis', 'route-map']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/ospf/1-to-2 b/src/migration-scripts/ospf/1-to-2 new file mode 100755 index 000000000..a6beaf04e --- /dev/null +++ b/src/migration-scripts/ospf/1-to-2 @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T5150: Rework CLI definitions to apply route-maps between routing daemons +# and zebra/kernel + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +ospf_base = ['protocols', 'ospf'] +# Check if OSPF is configured - if so, migrate the CLI node +if config.exists(ospf_base): + if config.exists(ospf_base + ['route-map']): + tmp = config.return_value(ospf_base + ['route-map']) + + config.set(['system', 'ip', 'protocol', 'ospf', 'route-map'], value=tmp) + config.set_tag(['system', 'ip', 'protocol']) + config.delete(ospf_base + ['route-map']) + +ospfv3_base = ['protocols', 'ospfv3'] +# Check if OSPFv3 is configured - if so, migrate the CLI node +if config.exists(ospfv3_base): + if config.exists(ospfv3_base + ['route-map']): + tmp = config.return_value(ospfv3_base + ['route-map']) + + config.set(['system', 'ipv6', 'protocol', 'ospfv3', 'route-map'], value=tmp) + config.set_tag(['system', 'ipv6', 'protocol']) + config.delete(ospfv3_base + ['route-map']) + +# Check if vrf names are configured. Check if OSPF/OSPFv3 is configured - if so, +# migrate the CLI node(s) +if config.exists(['vrf', 'name']): + for vrf in config.list_nodes(['vrf', 'name']): + vrf_base = ['vrf', 'name', vrf] + if config.exists(vrf_base + ['protocols', 'ospf', 'route-map']): + tmp = config.return_value(vrf_base + ['protocols', 'ospf', 'route-map']) + + config.set(vrf_base + ['ip', 'protocol', 'ospf', 'route-map'], value=tmp) + config.set_tag(vrf_base + ['ip', 'protocol', 'ospf']) + config.delete(vrf_base + ['protocols', 'ospf', 'route-map']) + + if config.exists(vrf_base + ['protocols', 'ospfv3', 'route-map']): + tmp = config.return_value(vrf_base + ['protocols', 'ospfv3', 'route-map']) + + config.set(vrf_base + ['ipv6', 'protocol', 'ospfv3', 'route-map'], value=tmp) + config.set_tag(vrf_base + ['ipv6', 'protocol', 'ospfv6']) + config.delete(vrf_base + ['protocols', 'ospfv3', 'route-map']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/quagga/10-to-11 b/src/migration-scripts/quagga/10-to-11 new file mode 100755 index 000000000..04fc16f79 --- /dev/null +++ b/src/migration-scripts/quagga/10-to-11 @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T5150: Rework CLI definitions to apply route-maps between routing daemons +# and zebra/kernel + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +static_base = ['protocols', 'static'] +# Check if static routes are configured - if so, migrate the CLI node +if config.exists(static_base): + if config.exists(static_base + ['route-map']): + tmp = config.return_value(static_base + ['route-map']) + + config.set(['system', 'ip', 'protocol', 'static', 'route-map'], value=tmp) + config.set_tag(['system', 'ip', 'protocol']) + config.delete(static_base + ['route-map']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/rip/0-to-1 b/src/migration-scripts/rip/0-to-1 new file mode 100755 index 000000000..60d510001 --- /dev/null +++ b/src/migration-scripts/rip/0-to-1 @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T5150: Rework CLI definitions to apply route-maps between routing daemons +# and zebra/kernel + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +ripng_base = ['protocols', 'ripng'] +# Check if RIPng is configured - if so, migrate the CLI node +if config.exists(ripng_base): + if config.exists(ripng_base + ['route-map']): + tmp = config.return_value(ripng_base + ['route-map']) + + config.set(['system', 'ipv6', 'protocol', 'ripng', 'route-map'], value=tmp) + config.set_tag(['system', 'ipv6', 'protocol']) + config.delete(ripng_base + ['route-map']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index 41da14065..fe7f252ba 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -264,8 +264,10 @@ def show_pool_statistics(raw: bool, family: ArgFamily, pool: typing.Optional[str def show_server_leases(raw: bool, family: ArgFamily, pool: typing.Optional[str], sorted: typing.Optional[str], state: typing.Optional[ArgState]): # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if not is_systemd_service_running('isc-dhcp-server.service'): - Warning('DHCP server is configured but not started. Data may be stale.') + v = '6' if family == 'inet6' else '' + service_name = 'DHCPv6' if family == 'inet6' else 'DHCP' + if not is_systemd_service_running(f'isc-dhcp-server{v}.service'): + Warning(f'{service_name} server is configured but not started. Data may be stale.') v = 'v6' if family == 'inet6' else '' if pool and pool not in _get_dhcp_pools(family=family): diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py index 263a3b6a5..2cba33cc8 100755 --- a/src/op_mode/dynamic_dns.py +++ b/src/op_mode/dynamic_dns.py @@ -16,69 +16,63 @@ import os import argparse -import jinja2 import sys import time +from tabulate import tabulate from vyos.config import Config from vyos.util import call cache_file = r'/run/ddclient/ddclient.cache' -OUT_TMPL_SRC = """ -{% for entry in hosts %} -ip address : {{ entry.ip }} -host-name : {{ entry.host }} -last update : {{ entry.time }} -update-status: {{ entry.status }} +columns = { + 'host': 'Hostname', + 'ipv4': 'IPv4 address', + 'status-ipv4': 'IPv4 status', + 'ipv6': 'IPv6 address', + 'status-ipv6': 'IPv6 status', + 'mtime': 'Last update', +} + + +def _get_formatted_host_records(host_data): + data_entries = [] + for entry in host_data: + data_entries.append([entry.get(key) for key in columns.keys()]) + + header = columns.values() + output = tabulate(data_entries, header, numalign='left') + return output -{% endfor %} -""" def show_status(): # A ddclient status file must not always exist if not os.path.exists(cache_file): sys.exit(0) - data = { - 'hosts': [] - } + data = [] with open(cache_file, 'r') as f: for line in f: if line.startswith('#'): continue - outp = { - 'host': '', - 'ip': '', - 'time': '' - } - - if 'host=' in line: - host = line.split('host=')[1] - if host: - outp['host'] = host.split(',')[0] - - if 'ip=' in line: - ip = line.split('ip=')[1] - if ip: - outp['ip'] = ip.split(',')[0] - - if 'mtime=' in line: - mtime = line.split('mtime=')[1] - if mtime: - outp['time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(int(mtime.split(',')[0], base=10))) + props = {} + # ddclient cache rows have properties in 'key=value' format separated by comma + # we pick up the ones we are interested in + for kvraw in line.split(' ')[0].split(','): + k, v = kvraw.split('=') + if k in columns.keys(): + props[k] = v - if 'status=' in line: - status = line.split('status=')[1] - if status: - outp['status'] = status.split(',')[0] + # Convert mtime to human readable format + if 'mtime' in props: + props['mtime'] = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(int(props['mtime'], base=10))) - data['hosts'].append(outp) + data.append(props) - tmpl = jinja2.Template(OUT_TMPL_SRC) - print(tmpl.render(data)) + print(_get_formatted_host_records(data)) def update_ddns(): diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index 6acde08ea..7f4fb72e5 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -13,6 +13,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. + import re import sys import typing @@ -487,6 +488,67 @@ def reset_ra(username: typing.Optional[str] = None): vyos.ipsec.terminate_vici_ikeid_list(list_sa_id) +def reset_profile_dst(profile: str, tunnel: str, nbma_dst: str): + if profile and tunnel and nbma_dst: + ike_sa_name = f'dmvpn-{profile}-{tunnel}' + try: + # Get IKE SAs + sa_list = convert_data( + vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None)) + if not sa_list: + raise vyos.opmode.IncorrectValue( + f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting') + sa_nbma_list = list([x for x in sa_list if + ike_sa_name in x and x[ike_sa_name][ + 'remote-host'] == nbma_dst]) + if not sa_nbma_list: + raise vyos.opmode.IncorrectValue( + f'SA(s) for profile {profile} tunnel {tunnel} remote-host {nbma_dst} not found, aborting') + # terminate IKE SAs + vyos.ipsec.terminate_vici_ikeid_list(list( + [x[ike_sa_name]['uniqueid'] for x in sa_nbma_list if + ike_sa_name in x])) + # initiate IKE SAs + for ike in sa_nbma_list: + if ike_sa_name in ike: + vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn', + ike[ike_sa_name]['local-host'], + ike[ike_sa_name]['remote-host']) + print( + f'Profile {profile} tunnel {tunnel} remote-host {nbma_dst} reset result: success') + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.UnconfiguredSubsystem(err) + except (vyos.ipsec.ViciCommandError) as err: + raise vyos.opmode.IncorrectValue(err) + + +def reset_profile_all(profile: str, tunnel: str): + if profile and tunnel: + ike_sa_name = f'dmvpn-{profile}-{tunnel}' + try: + # Get IKE SAs + sa_list: list = convert_data( + vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None)) + if not sa_list: + raise vyos.opmode.IncorrectValue( + f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting') + # terminate IKE SAs + vyos.ipsec.terminate_vici_by_name(ike_sa_name, None) + # initiate IKE SAs + for ike in sa_list: + if ike_sa_name in ike: + vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn', + ike[ike_sa_name]['local-host'], + ike[ike_sa_name]['remote-host']) + print( + f'Profile {profile} tunnel {tunnel} remote-host {ike[ike_sa_name]["remote-host"]} reset result: success') + print(f'Profile {profile} tunnel {tunnel} reset result: success') + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.UnconfiguredSubsystem(err) + except (vyos.ipsec.ViciCommandError) as err: + raise vyos.opmode.IncorrectValue(err) + + def show_sa(raw: bool): sa_data = _get_raw_data_sas() if raw: diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py index 37fdbcbeb..d9ae965c5 100755 --- a/src/op_mode/openvpn.py +++ b/src/op_mode/openvpn.py @@ -16,6 +16,7 @@ # # +import json import os import sys import typing @@ -25,6 +26,7 @@ import vyos.opmode from vyos.util import bytes_to_human from vyos.util import commit_in_progress from vyos.util import call +from vyos.util import rc_cmd from vyos.config import Config ArgMode = typing.Literal['client', 'server', 'site_to_site'] @@ -63,7 +65,7 @@ def _get_interface_status(mode: str, interface: str) -> dict: } if not os.path.exists(status_file): - raise vyos.opmode.DataUnavailable('No information for interface {interface}') + return data with open(status_file, 'r') as f: lines = f.readlines() @@ -142,6 +144,25 @@ def _get_interface_status(mode: str, interface: str) -> dict: return data + +def _get_interface_state(iface): + rc, out = rc_cmd(f'ip --json link show dev {iface}') + try: + data = json.loads(out) + except: + return 'DOWN' + return data[0].get('operstate', 'DOWN') + + +def _get_interface_description(iface): + rc, out = rc_cmd(f'ip --json link show dev {iface}') + try: + data = json.loads(out) + except: + return '' + return data[0].get('ifalias', '') + + def _get_raw_data(mode: str) -> list: data: list = [] conf = Config() @@ -154,6 +175,8 @@ def _get_raw_data(mode: str) -> list: conf_dict[x]['mode'].replace('-', '_') == mode] for intf in interfaces: d = _get_interface_status(mode, intf) + d['state'] = _get_interface_state(intf) + d['description'] = _get_interface_description(intf) d['local_host'] = conf_dict[intf].get('local-host', '') d['local_port'] = conf_dict[intf].get('local-port', '') if conf.exists(f'interfaces openvpn {intf} server client'): diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 1e78c3a03..b054690b0 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -87,6 +87,9 @@ def get_config_certificate(name=None): def get_certificate_ca(cert, ca_certs): # Find CA certificate for given certificate + if not ca_certs: + return None + for ca_name, ca_dict in ca_certs.items(): if 'certificate' not in ca_dict: continue diff --git a/src/op_mode/sflow.py b/src/op_mode/sflow.py new file mode 100755 index 000000000..88f70d6bd --- /dev/null +++ b/src/op_mode/sflow.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import dbus +import sys + +from tabulate import tabulate + +from vyos.configquery import ConfigTreeQuery +from vyos.util import cmd + +import vyos.opmode + + +def _get_raw_sflow(): + bus = dbus.SystemBus() + config = ConfigTreeQuery() + + interfaces = config.values('system sflow interface') + servers = config.list_nodes('system sflow server') + + sflow = bus.get_object('net.sflow.hsflowd', '/net/sflow/hsflowd') + sflow_telemetry = dbus.Interface( + sflow, dbus_interface='net.sflow.hsflowd.telemetry') + agent_address = sflow_telemetry.GetAgent() + samples_dropped = int(sflow_telemetry.Get('dropped_samples')) + packet_drop_sent = int(sflow_telemetry.Get('event_samples')) + samples_packet_sent = int(sflow_telemetry.Get('flow_samples')) + samples_counter_sent = int(sflow_telemetry.Get('counter_samples')) + datagrams_sent = int(sflow_telemetry.Get('datagrams')) + rtmetric_samples = int(sflow_telemetry.Get('rtmetric_samples')) + event_samples_suppressed = int(sflow_telemetry.Get('event_samples_suppressed')) + samples_suppressed = int(sflow_telemetry.Get('flow_samples_suppressed')) + counter_samples_suppressed = int( + sflow_telemetry.Get("counter_samples_suppressed")) + version = sflow_telemetry.GetVersion() + + sflow_dict = { + 'agent_address': agent_address, + 'sflow_interfaces': interfaces, + 'sflow_servers': servers, + 'counter_samples_sent': samples_counter_sent, + 'datagrams_sent': datagrams_sent, + 'packet_drop_sent': packet_drop_sent, + 'packet_samples_dropped': samples_dropped, + 'packet_samples_sent': samples_packet_sent, + 'rtmetric_samples': rtmetric_samples, + 'event_samples_suppressed': event_samples_suppressed, + 'flow_samples_suppressed': samples_suppressed, + 'counter_samples_suppressed': counter_samples_suppressed, + 'hsflowd_version': version + } + return sflow_dict + + +def _get_formatted_sflow(data): + table = [ + ['Agent address', f'{data.get("agent_address")}'], + ['sFlow interfaces', f'{data.get("sflow_interfaces", "n/a")}'], + ['sFlow servers', f'{data.get("sflow_servers", "n/a")}'], + ['Counter samples sent', f'{data.get("counter_samples_sent")}'], + ['Datagrams sent', f'{data.get("datagrams_sent")}'], + ['Packet samples sent', f'{data.get("packet_samples_sent")}'], + ['Packet samples dropped', f'{data.get("packet_samples_dropped")}'], + ['Packet drops sent', f'{data.get("packet_drop_sent")}'], + ['Packet drops suppressed', f'{data.get("event_samples_suppressed")}'], + ['Flow samples suppressed', f'{data.get("flow_samples_suppressed")}'], + ['Counter samples suppressed', f'{data.get("counter_samples_suppressed")}'] + ] + + return tabulate(table) + + +def show(raw: bool): + + config = ConfigTreeQuery() + if not config.exists('system sflow'): + raise vyos.opmode.UnconfiguredSubsystem( + '"system sflow" is not configured!') + + sflow_data = _get_raw_sflow() + if raw: + return sflow_data + else: + return _get_formatted_sflow(sflow_data) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py deleted file mode 100755 index eac068274..000000000 --- a/src/op_mode/show_interfaces.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2017-2021 VyOS maintainers and contributors <maintainers@vyos.io> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see <http://www.gnu.org/licenses/>. - -import os -import re -import sys -import glob -import argparse - -from vyos.ifconfig import Section -from vyos.ifconfig import Interface -from vyos.ifconfig import VRRP -from vyos.util import cmd, call - - -# interfaces = Sections.reserved() -interfaces = ['eno', 'ens', 'enp', 'enx', 'eth', 'vmnet', 'lo', 'tun', 'wan', 'pppoe'] -glob_ifnames = '/sys/class/net/({})*'.format('|'.join(interfaces)) - - -actions = {} -def register(name): - """ - Decorator to register a function into actions with a name. - `actions[name]' can be used to call the registered functions. - We wrap each function in a SIGPIPE handler as all registered functions - can be subject to a broken pipe if there are a lot of interfaces. - """ - def _register(function): - def handled_function(*args, **kwargs): - try: - function(*args, **kwargs) - except BrokenPipeError: - # Flush output to /dev/null and bail out. - os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) - sys.exit(1) - actions[name] = handled_function - return handled_function - return _register - - -def filtered_interfaces(ifnames, iftypes, vif, vrrp): - """ - get all the interfaces from the OS and returns them - ifnames can be used to filter which interfaces should be considered - - ifnames: a list of interfaces names to consider, empty do not filter - return an instance of the interface class - """ - if isinstance(iftypes, list): - for iftype in iftypes: - yield from filtered_interfaces(ifnames, iftype, vif, vrrp) - - for ifname in Section.interfaces(iftypes): - # Bail out early if interface name not part of our search list - if ifnames and ifname not in ifnames: - continue - - # As we are only "reading" from the interface - we must use the - # generic base class which exposes all the data via a common API - interface = Interface(ifname, create=False, debug=False) - - # VLAN interfaces have a '.' in their name by convention - if vif and not '.' in ifname: - continue - - if vrrp: - vrrp_interfaces = VRRP.active_interfaces() - if ifname not in vrrp_interfaces: - continue - - yield interface - - -def split_text(text, used=0): - """ - take a string and attempt to split it to fit with the width of the screen - - text: the string to split - used: number of characted already used in the screen - """ - no_tty = call('tty -s') - - returned = cmd('stty size') if not no_tty else '' - if len(returned) == 2: - rows, columns = [int(_) for _ in returned] - else: - rows, columns = (40, 80) - - desc_len = columns - used - - line = '' - for word in text.split(): - if len(line) + len(word) < desc_len: - line = f'{line} {word}' - continue - if line: - yield line[1:] - else: - line = f'{line} {word}' - - yield line[1:] - - -def get_counter_val(clear, now): - """ - attempt to correct a counter if it wrapped, copied from perl - - clear: previous counter - now: the current counter - """ - # This function has to deal with both 32 and 64 bit counters - if clear == 0: - return now - - # device is using 64 bit values assume they never wrap - value = now - clear - if (now >> 32) != 0: - return value - - # The counter has rolled. If the counter has rolled - # multiple times since the clear value, then this math - # is meaningless. - if (value < 0): - value = (4294967296 - clear) + now - - return value - - -@register('help') -def usage(*args): - print(f"Usage: {sys.argv[0]} [intf=NAME|intf-type=TYPE|vif|vrrp] action=ACTION") - print(f" NAME = " + ' | '.join(Section.interfaces())) - print(f" TYPE = " + ' | '.join(Section.sections())) - print(f" ACTION = " + ' | '.join(actions)) - sys.exit(1) - - -@register('allowed') -def run_allowed(**kwarg): - sys.stdout.write(' '.join(Section.interfaces())) - - -def pppoe(ifname): - out = cmd(f'ps -C pppd -f') - if ifname in out: - return 'C' - elif ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]: - return 'D' - return '' - - -@register('show') -def run_show_intf(ifnames, iftypes, vif, vrrp): - handled = [] - for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): - handled.append(interface.ifname) - cache = interface.operational.load_counters() - - out = cmd(f'ip addr show {interface.ifname}') - out = re.sub(f'^\d+:\s+','',out) - if re.search('link/tunnel6', out): - tunnel = cmd(f'ip -6 tun show {interface.ifname}') - # tun0: ip/ipv6 remote ::2 local ::1 encaplimit 4 hoplimit 64 tclass inherit flowlabel inherit (flowinfo 0x00000000) - tunnel = re.sub('.*encap', 'encap', tunnel) - out = re.sub('(\n\s+)(link/tunnel6)', f'\g<1>{tunnel}\g<1>\g<2>', out) - - print(out) - - timestamp = int(cache.get('timestamp', 0)) - if timestamp: - when = interface.operational.strtime(timestamp) - print(f' Last clear: {when}') - - description = interface.get_alias() - if description: - print(f' Description: {description}') - - print() - print(interface.operational.formated_stats()) - - for ifname in ifnames: - if ifname not in handled and ifname.startswith('pppoe'): - state = pppoe(ifname) - if not state: - continue - string = { - 'C': 'Coming up', - 'D': 'Link down', - }[state] - print('{}: {}'.format(ifname, string)) - - -@register('show-brief') -def run_show_intf_brief(ifnames, iftypes, vif, vrrp): - format1 = '%-16s %-33s %-4s %s' - format2 = '%-16s %s' - - print('Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down') - print(format1 % ("Interface", "IP Address", "S/L", "Description")) - print(format1 % ("---------", "----------", "---", "-----------")) - - handled = [] - for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): - handled.append(interface.ifname) - - oper_state = interface.operational.get_state() - admin_state = interface.get_admin_state() - - intf = [interface.ifname,] - - oper = ['u', ] if oper_state in ('up', 'unknown') else ['D', ] - admin = ['u', ] if admin_state in ('up', 'unknown') else ['A', ] - addrs = [_ for _ in interface.get_addr() if not _.startswith('fe80::')] or ['-', ] - descs = list(split_text(interface.get_alias(),0)) - - while intf or oper or admin or addrs or descs: - i = intf.pop(0) if intf else '' - a = addrs.pop(0) if addrs else '' - d = descs.pop(0) if descs else '' - s = [admin.pop(0)] if admin else [] - l = [oper.pop(0)] if oper else [] - if len(a) < 33: - print(format1 % (i, a, '/'.join(s+l), d)) - else: - print(format2 % (i, a)) - print(format1 % ('', '', '/'.join(s+l), d)) - - for ifname in ifnames: - if ifname not in handled and ifname.startswith('pppoe'): - state = pppoe(ifname) - if not state: - continue - string = { - 'C': 'u/D', - 'D': 'A/D', - }[state] - print(format1 % (ifname, '', string, '')) - - -@register('show-count') -def run_show_counters(ifnames, iftypes, vif, vrrp): - formating = '%-12s %10s %10s %10s %10s' - print(formating % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes')) - - for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): - oper = interface.operational.get_state() - - if oper not in ('up','unknown'): - continue - - stats = interface.operational.get_stats() - cache = interface.operational.load_counters() - print(formating % ( - interface.ifname, - get_counter_val(cache['rx_packets'], stats['rx_packets']), - get_counter_val(cache['rx_bytes'], stats['rx_bytes']), - get_counter_val(cache['tx_packets'], stats['tx_packets']), - get_counter_val(cache['tx_bytes'], stats['tx_bytes']), - )) - - -@register('clear') -def run_clear_intf(ifnames, iftypes, vif, vrrp): - for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): - print(f'Clearing {interface.ifname}') - interface.operational.clear_counters() - - -@register('reset') -def run_reset_intf(ifnames, iftypes, vif, vrrp): - for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): - interface.operational.reset_counters() - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(add_help=False, description='Show interface information') - parser.add_argument('--intf', action="store", type=str, default='', help='only show the specified interface(s)') - parser.add_argument('--intf-type', action="store", type=str, default='', help='only show the specified interface type') - parser.add_argument('--action', action="store", type=str, default='show', help='action to perform') - parser.add_argument('--vif', action='store_true', default=False, help="only show vif interfaces") - parser.add_argument('--vrrp', action='store_true', default=False, help="only show vrrp interfaces") - parser.add_argument('--help', action='store_true', default=False, help="show help") - - args = parser.parse_args() - - def missing(*args): - print('Invalid action [{args.action}]') - usage() - - actions.get(args.action, missing)( - [_ for _ in args.intf.split(' ') if _], - [_ for _ in args.intf_type.split(' ') if _], - args.vif, - args.vrrp - ) diff --git a/src/op_mode/show_techsupport_report.py b/src/op_mode/show_techsupport_report.py new file mode 100644 index 000000000..782004144 --- /dev/null +++ b/src/op_mode/show_techsupport_report.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from typing import List +from vyos.util import rc_cmd +from vyos.ifconfig import Section +from vyos.ifconfig import Interface + + +def print_header(command: str) -> None: + """Prints a command with headers '-'. + + Example: + + % print_header('Example command') + + --------------- + Example command + --------------- + """ + header_length = len(command) * '-' + print(f"\n{header_length}\n{command}\n{header_length}") + + +def execute_command(command: str, header_text: str) -> None: + """Executes a command and prints the output with a header. + + Example: + % execute_command('uptime', "Uptime of the system") + + -------------------- + Uptime of the system + -------------------- + 20:21:57 up 9:04, 5 users, load average: 0.00, 0.00, 0.0 + + """ + print_header(header_text) + try: + rc, output = rc_cmd(command) + print(output) + except Exception as e: + print(f"Error executing command: {command}") + print(f"Error message: {e}") + + +def op(cmd: str) -> str: + """Returns a command with the VyOS operational mode wrapper.""" + return f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}' + + +def get_ethernet_interfaces() -> List[Interface]: + """Returns a list of Ethernet interfaces.""" + return Section.interfaces('ethernet') + + +def show_version() -> None: + """Prints the VyOS version and package changes.""" + execute_command(op('show version'), 'VyOS Version and Package Changes') + + +def show_config_file() -> None: + """Prints the contents of a configuration file with a header.""" + execute_command('cat /opt/vyatta/etc/config/config.boot', 'Configuration file') + + +def show_running_config() -> None: + """Prints the running configuration.""" + execute_command(op('show configuration'), 'Running configuration') + + +def show_package_repository_config() -> None: + """Prints the package repository configuration file.""" + execute_command('cat /etc/apt/sources.list', 'Package Repository Configuration File') + execute_command('ls -l /etc/apt/sources.list.d/', 'Repositories') + + +def show_user_startup_scripts() -> None: + """Prints the user startup scripts.""" + execute_command('cat /config/scripts/vyos-postconfig-bootup.script', 'User Startup Scripts') + + +def show_frr_config() -> None: + """Prints the FRR configuration.""" + execute_command('vtysh -c "show run"', 'FRR configuration') + + +def show_interfaces() -> None: + """Prints the interfaces.""" + execute_command(op('show interfaces'), 'Interfaces') + + +def show_interface_statistics() -> None: + """Prints the interface statistics.""" + execute_command('ip -s link show', 'Interface statistics') + + +def show_physical_interface_statistics() -> None: + """Prints the physical interface statistics.""" + execute_command('/usr/bin/true', 'Physical Interface statistics') + for iface in get_ethernet_interfaces(): + # Exclude vlans + if '.' in iface: + continue + execute_command(f'ethtool --driver {iface}', f'ethtool --driver {iface}') + execute_command(f'ethtool --statistics {iface}', f'ethtool --statistics {iface}') + execute_command(f'ethtool --show-ring {iface}', f'ethtool --show-ring {iface}') + execute_command(f'ethtool --show-coalesce {iface}', f'ethtool --show-coalesce {iface}') + execute_command(f'ethtool --pause {iface}', f'ethtool --pause {iface}') + execute_command(f'ethtool --show-features {iface}', f'ethtool --show-features {iface}') + execute_command(f'ethtool --phy-statistics {iface}', f'ethtool --phy-statistics {iface}') + execute_command('netstat --interfaces', 'netstat --interfaces') + execute_command('netstat --listening', 'netstat --listening') + execute_command('cat /proc/net/dev', 'cat /proc/net/dev') + + +def show_bridge() -> None: + """Show bridge interfaces.""" + execute_command(op('show bridge'), 'Show bridge') + + +def show_arp() -> None: + """Prints ARP entries.""" + execute_command(op('show arp'), 'ARP Table (Total entries)') + execute_command(op('show ipv6 neighbors'), 'show ipv6 neighbors') + + +def show_route() -> None: + """Prints routing information.""" + + cmd_list_route = [ + "show ip route bgp | head -108", + "show ip route cache", + "show ip route connected", + "show ip route forward", + "show ip route isis | head -108", + "show ip route kernel", + "show ip route ospf | head -108", + "show ip route rip", + "show ip route static", + "show ip route summary", + "show ip route supernets-only", + "show ip route table all", + "show ip route vrf all", + "show ipv6 route bgp | head 108", + "show ipv6 route cache", + "show ipv6 route connected", + "show ipv6 route forward", + "show ipv6 route isis", + "show ipv6 route kernel", + "show ipv6 route ospf", + "show ipv6 route rip", + "show ipv6 route static", + "show ipv6 route summary", + "show ipv6 route table all", + "show ipv6 route vrf all", + ] + for command in cmd_list_route: + execute_command(op(command), command) + + +def show_firewall() -> None: + """Prints firweall information.""" + execute_command('sudo nft list ruleset', 'nft list ruleset') + + +def show_system() -> None: + """Prints system parameters.""" + execute_command(op('show system image version'), 'Show System Image Version') + execute_command(op('show system image storage'), 'Show System Image Storage') + + +def show_date() -> None: + """Print the current date.""" + execute_command('date', 'Current Time') + + +def show_installed_packages() -> None: + """Prints installed packages.""" + execute_command('dpkg --list', 'Installed Packages') + + +def show_loaded_modules() -> None: + """Prints loaded modules /proc/modules""" + execute_command('cat /proc/modules', 'Loaded Modules') + + +def show_cpu_statistics() -> None: + """Prints CPU statistics.""" + execute_command('/usr/bin/true', 'CPU') + execute_command('lscpu', 'Installed CPU\'s') + execute_command('top --iterations 1 --batch-mode --accum-time-toggle', 'Cumulative CPU Time Used by Running Processes') + execute_command('cat /proc/loadavg', 'Load Average') + + +def show_system_interrupts() -> None: + """Prints system interrupts.""" + execute_command('cat /proc/interrupts', 'Hardware Interrupt Counters') + + +def show_soft_irqs() -> None: + """Prints soft IRQ's.""" + execute_command('cat /proc/softirqs', 'Soft IRQ\'s') + + +def show_softnet_statistics() -> None: + """Prints softnet statistics.""" + execute_command('cat /proc/net/softnet_stat', 'cat /proc/net/softnet_stat') + + +def show_running_processes() -> None: + """Prints current running processes""" + execute_command('ps -ef', 'Running Processes') + + +def show_memory_usage() -> None: + """Prints memory usage""" + execute_command('/usr/bin/true', 'Memory') + execute_command('cat /proc/meminfo', 'Installed Memory') + execute_command('free', 'Memory Usage') + + +def list_disks(): + disks = set() + with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': + disks.add(fields[3]) + return disks + + +def show_storage() -> None: + """Prints storage information.""" + execute_command('cat /proc/devices', 'Devices') + execute_command('cat /proc/partitions', 'Partitions') + + for disk in list_disks(): + execute_command(f'fdisk --list /dev/{disk}', f'Partitioning for disk {disk}') + + +def main(): + # Configuration data + show_version() + show_config_file() + show_running_config() + show_package_repository_config() + show_user_startup_scripts() + show_frr_config() + + # Interfaces + show_interfaces() + show_interface_statistics() + show_physical_interface_statistics() + show_bridge() + show_arp() + + # Routing + show_route() + + # Firewall + show_firewall() + + # System + show_system() + show_date() + show_installed_packages() + show_loaded_modules() + + # CPU + show_cpu_statistics() + show_system_interrupts() + show_soft_irqs() + show_softnet_statistics() + + # Memory + show_memory_usage() + + # Storage + show_storage() + + # Processes + show_running_processes() + + # TODO: Get information from clouds + + +if __name__ == "__main__": + main() diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py index 2392cfe92..b81d1693e 100755 --- a/src/op_mode/vpn_ipsec.py +++ b/src/op_mode/vpn_ipsec.py @@ -16,12 +16,12 @@ import re import argparse -from subprocess import TimeoutExpired from vyos.util import call SWANCTL_CONF = '/etc/swanctl/swanctl.conf' + def get_peer_connections(peer, tunnel, return_all = False): search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*' matches = [] @@ -34,57 +34,6 @@ def get_peer_connections(peer, tunnel, return_all = False): matches.append(result[1]) return matches -def reset_peer(peer, tunnel): - if not peer: - print('Invalid peer, aborting') - return - - conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all')) - - if not conns: - print('Tunnel(s) not found, aborting') - return - - result = True - for conn in conns: - try: - call(f'/usr/sbin/ipsec down {conn}{{*}}', timeout = 10) - call(f'/usr/sbin/ipsec up {conn}', timeout = 10) - except TimeoutExpired as e: - print(f'Timed out while resetting {conn}') - result = False - - - print('Peer reset result: ' + ('success' if result else 'failed')) - -def get_profile_connection(profile, tunnel = None): - search = rf'(dmvpn-{profile}-[\w]+)' if tunnel == 'all' else rf'(dmvpn-{profile}-{tunnel})' - with open(SWANCTL_CONF, 'r') as f: - for line in f.readlines(): - result = re.search(search, line) - if result: - return result[1] - return None - -def reset_profile(profile, tunnel): - if not profile: - print('Invalid profile, aborting') - return - - if not tunnel: - print('Invalid tunnel, aborting') - return - - conn = get_profile_connection(profile) - - if not conn: - print('Profile not found, aborting') - return - - call(f'/usr/sbin/ipsec down {conn}') - result = call(f'/usr/sbin/ipsec up {conn}') - - print('Profile reset result: ' + ('success' if result == 0 else 'failed')) def debug_peer(peer, tunnel): peer = peer.replace(':', '-') @@ -119,6 +68,7 @@ def debug_peer(peer, tunnel): for conn in conns: call(f'/usr/sbin/ipsec statusall | grep {conn}') + if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--action', help='Control action', required=True) @@ -127,9 +77,6 @@ if __name__ == '__main__': args = parser.parse_args() - if args.action == 'reset-peer': - reset_peer(args.name, args.tunnel) - elif args.action == "reset-profile": - reset_profile(args.name, args.tunnel) - elif args.action == "vpn-debug": + + if args.action == "vpn-debug": debug_peer(args.name, args.tunnel) |