diff options
35 files changed, 772 insertions, 78 deletions
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index 4fd94d895..b62603e34 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -29,6 +29,9 @@ "openconnect": ["vpn_openconnect"], "sstp": ["vpn_sstp"] }, + "vpn_l2tp": { + "ipsec": ["vpn_ipsec"] + }, "qos": { "bonding": ["interfaces_bonding"], "bridge": ["interfaces_bridge"], diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index e02fdd1bb..23f81348b 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -225,10 +225,10 @@ neighbor {{ neighbor }} route-map {{ afi_config.route_map.import }} in {% endif %} {% if afi_config.prefix_list.export is vyos_defined %} - neighbor {{ neighbor }} prefix-list {{ afi_config.prefix_list.export }} out + neighbor {{ neighbor }} prefix-list {{ afi_config.prefix_list.export }} out {% endif %} {% if afi_config.prefix_list.import is vyos_defined %} - neighbor {{ neighbor }} prefix-list {{ afi_config.prefix_list.import }} in + neighbor {{ neighbor }} prefix-list {{ afi_config.prefix_list.import }} in {% endif %} {% if afi_config.soft_reconfiguration.inbound is vyos_defined %} neighbor {{ neighbor }} soft-reconfiguration inbound @@ -237,10 +237,10 @@ neighbor {{ neighbor }} unsuppress-map {{ afi_config.unsuppress_map }} {% endif %} {% if afi_config.disable_send_community.extended is vyos_defined %} - no neighbor {{ neighbor }} send-community extended + no neighbor {{ neighbor }} send-community extended {% endif %} {% if afi_config.disable_send_community.standard is vyos_defined %} - no neighbor {{ neighbor }} send-community standard + no neighbor {{ neighbor }} send-community standard {% endif %} neighbor {{ neighbor }} activate exit-address-family @@ -473,6 +473,7 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% endfor %} {% endif %} exit-address-family + ! {% endfor %} {% endif %} ! @@ -530,6 +531,9 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% endif %} {% endfor %} {% endif %} +{% if parameters.allow_martian_nexthop is vyos_defined %} + bgp allow-martian-nexthop +{% endif %} {% if parameters.always_compare_med is vyos_defined %} bgp always-compare-med {% endif %} @@ -590,6 +594,12 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% if parameters.graceful_shutdown is vyos_defined %} bgp graceful-shutdown {% endif %} +{% if parameters.no_hard_administrative_reset is vyos_defined %} + no bgp hard-administrative-reset +{% endif %} +{% if parameters.labeled_unicast is vyos_defined %} + bgp labeled-unicast {{ parameters.labeled_unicast }} +{% endif %} {% if parameters.log_neighbor_changes is vyos_defined %} bgp log-neighbor-changes {% endif %} diff --git a/data/templates/frr/rpki.frr.j2 b/data/templates/frr/rpki.frr.j2 index 9a549d6de..384cbbe52 100644 --- a/data/templates/frr/rpki.frr.j2 +++ b/data/templates/frr/rpki.frr.j2 @@ -5,7 +5,7 @@ rpki {% for peer, peer_config in cache.items() %} {# port is mandatory and preference uses a default value #} {% if peer_config.ssh.username is vyos_defined %} - rpki cache {{ peer | replace('_', '-') }} {{ peer_config.port }} {{ peer_config.ssh.username }} {{ peer_config.ssh.private_key_file }} {{ peer_config.ssh.public_key_file }} {{ peer_config.ssh.known_hosts_file }} preference {{ peer_config.preference }} + rpki cache {{ peer | replace('_', '-') }} {{ peer_config.port }} {{ peer_config.ssh.username }} {{ peer_config.ssh.private_key_file }} {{ peer_config.ssh.public_key_file }} preference {{ peer_config.preference }} {% else %} rpki cache {{ peer | replace('_', '-') }} {{ peer_config.port }} preference {{ peer_config.preference }} {% endif %} diff --git a/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i index c8ad68700..a433f7cc6 100644 --- a/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i +++ b/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i @@ -1,5 +1,4 @@ <!-- include start from bgp/neighbor-afi-ipv4-ipv6-common.xml.i --> - <leafNode name="addpath-tx-all"> <properties> <help>Use addpath to advertise all paths to a neighbor</help> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index 9895b025c..ea6e75bbd 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -1219,6 +1219,12 @@ <help>BGP parameters</help> </properties> <children> + <leafNode name="allow-martian-nexthop"> + <properties> + <help>Allow Martian nexthops to be received in the NLRI from a peer</help> + <valueless/> + </properties> + </leafNode> <leafNode name="always-compare-med"> <properties> <help>Always compare MEDs from different neighbors</help> @@ -1576,6 +1582,35 @@ <valueless/> </properties> </leafNode> + <leafNode name="no-hard-administrative-reset"> + <properties> + <help>Do not send hard reset CEASE Notification for 'Administrative Reset'</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="labeled-unicast"> + <properties> + <help>BGP Labeled-unicast options</help> + <completionHelp> + <list>explicit-null ipv4-explicit-null ipv6-explicit-null</list> + </completionHelp> + <valueHelp> + <format>explicit-null</format> + <description>Use explicit-null label values for all local prefixes</description> + </valueHelp> + <valueHelp> + <format>ipv4-explicit-null</format> + <description>Use IPv4 explicit-null label value for IPv4 local prefixes</description> + </valueHelp> + <valueHelp> + <format>ipv6-explicit-null</format> + <description>Use IPv6 explicit-null label value for IPv4 local prefixes</description> + </valueHelp> + <constraint> + <regex>(explicit-null|ipv4-explicit-null|ipv6-explicit-null)</regex> + </constraint> + </properties> + </leafNode> <leafNode name="log-neighbor-changes"> <properties> <help>Log neighbor up/down changes and reset reason</help> diff --git a/interface-definitions/include/version/rpki-version.xml.i b/interface-definitions/include/version/rpki-version.xml.i index 2fff259a8..45ff4fbfb 100644 --- a/interface-definitions/include/version/rpki-version.xml.i +++ b/interface-definitions/include/version/rpki-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/rpki-version.xml.i --> -<syntaxVersion component='rpki' version='1'></syntaxVersion> +<syntaxVersion component='rpki' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/protocols_rpki.xml.in b/interface-definitions/protocols_rpki.xml.in index e9fd04b5f..6a38b2961 100644 --- a/interface-definitions/protocols_rpki.xml.in +++ b/interface-definitions/protocols_rpki.xml.in @@ -46,14 +46,6 @@ <help>RPKI SSH connection settings</help> </properties> <children> - <leafNode name="known-hosts-file"> - <properties> - <help>RPKI SSH known hosts file</help> - <constraint> - <validator name="file-path"/> - </constraint> - </properties> - </leafNode> <leafNode name="private-key-file"> <properties> <help>RPKI SSH private key file</help> diff --git a/interface-definitions/system_frr.xml.in b/interface-definitions/system_frr.xml.in index 76001b392..28242dfe4 100644 --- a/interface-definitions/system_frr.xml.in +++ b/interface-definitions/system_frr.xml.in @@ -4,7 +4,7 @@ <children> <node name="frr" owner="${vyos_conf_scripts_dir}/system_frr.py"> <properties> - <help>Configure FRR parameters</help> + <help>Configure FRRouting parameters</help> <!-- Before components that use FRR --> <priority>150</priority> </properties> diff --git a/op-mode-definitions/file.xml.in b/op-mode-definitions/file.xml.in new file mode 100644 index 000000000..549b9ad92 --- /dev/null +++ b/op-mode-definitions/file.xml.in @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <tagNode name="file"> + <properties> + <help>Show the contents of a file, a directory or an image</help> + <completionHelp><imagePath/></completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/file.py --show $3</command> + </tagNode> + </children> + </node> + <node name="copy"> + <properties> + <help>Copy an object</help> + </properties> + <children> + <tagNode name="file"> + <properties> + <help>Copy a file or a directory</help> + <completionHelp><imagePath/></completionHelp> + </properties> + <children> + <tagNode name="to"> + <properties> + <help>Destination path</help> + <completionHelp><imagePath/></completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/file.py --copy $3 $5 + </command> + </tagNode> + </children> + </tagNode> + </children> + </node> + <node name="delete"> + <properties> + <help>Delete an object</help> + </properties> + <children> + <tagNode name="file"> + <properties> + <help>Delete a local file, possibly from an image</help> + <completionHelp><imagePath/></completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/file.py --delete $3</command> + </tagNode> + </children> + </node> + <node name="clone"> + <properties> + <help>Clone an object</help> + </properties> + <children> + <node name="system"> + <properties> + <help>Clone a system object</help> + </properties> + <children> + <tagNode name="config"> + <properties> + <help>Clone the current system configuration to an image</help> + <completionHelp> + <script>${vyos_completion_dir}/list_images.py --no-running</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/file.py --clone $4</command> + <children> + <tagNode name="from"> + <properties> + <help>Clone system configuration from an image to another one</help> + <completionHelp> + <list>running</list> + <script>${vyos_completion_dir}/list_images.py</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/file.py --clone-from $6 $4</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-system.xml.in b/op-mode-definitions/show-system.xml.in index 116c7460f..6873b816b 100644 --- a/op-mode-definitions/show-system.xml.in +++ b/op-mode-definitions/show-system.xml.in @@ -150,7 +150,7 @@ </children> </tagNode> </children> - </node> + </node> <node name="users"> <properties> <help>Show user account information</help> @@ -239,7 +239,7 @@ </node> <leafNode name="routing-daemons"> <properties> - <help>Show Quagga routing daemons</help> + <help>Show FRRouting daemons</help> </properties> <command>vtysh -c "show daemons"</command> </leafNode> diff --git a/op-mode-definitions/show-version.xml.in b/op-mode-definitions/show-version.xml.in index d9c4738af..36e68ff79 100644 --- a/op-mode-definitions/show-version.xml.in +++ b/op-mode-definitions/show-version.xml.in @@ -22,7 +22,7 @@ </leafNode> <leafNode name="frr"> <properties> - <help>Show Quagga version information</help> + <help>Show FRRouting version information</help> </properties> <command>vtysh -c "show version"</command> </leafNode> diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py index d6705cc77..7d580baa2 100644 --- a/python/vyos/qos/trafficshaper.py +++ b/python/vyos/qos/trafficshaper.py @@ -39,7 +39,7 @@ class TrafficShaper(QoSBase): # need a bigger r2q if going fast than 16 mbits/sec if (speed_bps // r2q) >= MAXQUANTUM: # integer division - r2q = ceil(speed_bps // MAXQUANTUM) + r2q = ceil(speed_bps / MAXQUANTUM) else: # if there is a slow class then may need smaller value if 'class' in config: diff --git a/python/vyos/remote.py b/python/vyos/remote.py index 830770d11..129e65772 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -54,7 +54,7 @@ class InteractivePolicy(MissingHostKeyPolicy): def missing_host_key(self, client, hostname, key): print_error(f"Host '{hostname}' not found in known hosts.") print_error('Fingerprint: ' + key.get_fingerprint().hex()) - if is_interactive() and ask_yes_no('Do you wish to continue?'): + if sys.stdin.isatty() and ask_yes_no('Do you wish to continue?'): if client._host_keys_filename\ and ask_yes_no('Do you wish to permanently add this host/key pair to known hosts?'): client._host_keys.add(hostname, key.get_name(), key) @@ -445,8 +445,10 @@ def download(local_path, urlstring, progressbar=False, check_space=False, if raise_error: raise print_error(f'Unable to download "{urlstring}": {err}') + sys.exit(1) except KeyboardInterrupt: print_error('\nDownload aborted by user.') + sys.exit(1) def upload(local_path, urlstring, progressbar=False, source_host='', source_port=0, timeout=10.0): @@ -455,8 +457,10 @@ def upload(local_path, urlstring, progressbar=False, urlc(urlstring, progressbar, False, source_host, source_port, timeout).upload(local_path) except Exception as err: print_error(f'Unable to upload "{urlstring}": {err}') + sys.exit(1) except KeyboardInterrupt: print_error('\nUpload aborted by user.') + sys.exit(1) def get_remote_config(urlstring, source_host='', source_port=0): """ diff --git a/python/vyos/system/disk.py b/python/vyos/system/disk.py index 7860d719f..c8908cd5c 100644 --- a/python/vyos/system/disk.py +++ b/python/vyos/system/disk.py @@ -16,6 +16,7 @@ from json import loads as json_loads from os import sync from dataclasses import dataclass +from time import sleep from psutil import disk_partitions @@ -207,13 +208,25 @@ def find_device(mountpoint: str) -> str: Returns: str: Path to device, Empty if not found """ - mounted_partitions = disk_partitions() + mounted_partitions = disk_partitions(all=True) for partition in mounted_partitions: if partition.mountpoint == mountpoint: return partition.mountpoint return '' +def wait_for_umount(mountpoint: str = '') -> None: + """Wait (within reason) for umount to complete + """ + i = 0 + while find_device(mountpoint): + i += 1 + if i == 5: + print(f'Warning: {mountpoint} still mounted') + break + sleep(1) + + def disks_size() -> dict[str, int]: """Get a dictionary with physical disks and their sizes diff --git a/python/vyos/utils/config.py b/python/vyos/utils/config.py index bd363ce46..33047010b 100644 --- a/python/vyos/utils/config.py +++ b/python/vyos/utils/config.py @@ -1,4 +1,4 @@ -# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -31,4 +31,9 @@ def read_saved_value(path: list): if not ct.exists(path): return '' res = ct.return_values(path) - return res[0] if len(res) == 1 else res + if len(res) == 1: + return res[0] + res = ct.list_nodes(path) + if len(res) == 1: + return ' '.join(res) + return res diff --git a/schema/op-mode-definition.rnc b/schema/op-mode-definition.rnc index cbe51e6dc..ad41700b9 100644 --- a/schema/op-mode-definition.rnc +++ b/schema/op-mode-definition.rnc @@ -95,13 +95,15 @@ command = element command # completionHelp tags contain information about allowed values of a node that is used for generating # tab completion in the CLI frontend and drop-down lists in GUI frontends -# It is only meaninful for leaf nodes +# It is only meaningful for leaf nodes # Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>), # as a configuration path (e.g. <path>interfaces ethernet</path>), -# or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script> +# as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>, +# or to enable built-in image path completion (<imagePath/>). completionHelp = element completionHelp { (element list { text })* & (element path { text })* & - (element script { text })* + (element script { text })* & + (element imagePath { empty })? } diff --git a/schema/op-mode-definition.rng b/schema/op-mode-definition.rng index 900f41e27..a255aeb73 100644 --- a/schema/op-mode-definition.rng +++ b/schema/op-mode-definition.rng @@ -162,6 +162,11 @@ <text/> </element> </zeroOrMore> + <optional> + <element name="imagePath"> + <empty/> + </element> + </optional> </interleave> </element> </define> diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates index b008596dc..46ad634b9 100755 --- a/scripts/build-command-op-templates +++ b/scripts/build-command-op-templates @@ -100,6 +100,7 @@ def get_properties(p): scripts = c.findall("script") paths = c.findall("path") lists = c.findall("list") + comptype = c.find("imagePath") # Current backend doesn't support multiple allowed: tags # so we get to emulate it @@ -110,8 +111,12 @@ def get_properties(p): comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\" && echo".format(i.text)) for i in scripts: comp_exprs.append("{0}".format(i.text)) + if comptype is not None: + props["comp_type"] = "imagefiles" + comp_exprs.append("echo -n \"<imagefiles>\"") comp_help = " && ".join(comp_exprs) props["comp_help"] = comp_help + except: props["comp_help"] = [] @@ -127,6 +132,8 @@ def make_node_def(props, command): help = props["help"] help = fill(help, width=64, subsequent_indent='\t\t\t') node_def += f'help: {help}\n' + if "comp_type" in props: + node_def += f'comptype: {props["comp_type"]}\n' if "comp_help" in props: node_def += f'allowed: {props["comp_help"]}\n' if command is not None: diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 2dbc30a41..08a6e1696 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -319,8 +319,11 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): tcp_keepalive_interval = '77' tcp_keepalive_probes = '22' - self.cli_set(base_path + ['parameters', 'router-id', router_id]) + self.cli_set(base_path + ['parameters', 'allow-martian-nexthop']) + self.cli_set(base_path + ['parameters', 'no-hard-administrative-reset']) self.cli_set(base_path + ['parameters', 'log-neighbor-changes']) + self.cli_set(base_path + ['parameters', 'labeled-unicast', 'explicit-null']) + self.cli_set(base_path + ['parameters', 'router-id', router_id]) # System AS number MUST be defined - as this is set in setUp() we remove # this once for testing of the proper error @@ -367,12 +370,15 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): frrconfig = self.getFRRconfig(f'router bgp {ASN}') self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' bgp router-id {router_id}', frrconfig) + self.assertIn(f' bgp allow-martian-nexthop', frrconfig) self.assertIn(f' bgp log-neighbor-changes', frrconfig) self.assertIn(f' bgp default local-preference {local_pref}', frrconfig) self.assertIn(f' bgp conditional-advertisement timer {cond_adv_timer}', frrconfig) self.assertIn(f' bgp fast-convergence', frrconfig) self.assertIn(f' bgp graceful-restart stalepath-time {stalepath_time}', frrconfig) self.assertIn(f' bgp graceful-shutdown', frrconfig) + self.assertIn(f' no bgp hard-administrative-reset', frrconfig) + self.assertIn(f' bgp labeled-unicast explicit-null', frrconfig) self.assertIn(f' bgp bestpath as-path multipath-relax', frrconfig) self.assertIn(f' bgp bestpath bandwidth default-weight-for-missing', frrconfig) self.assertIn(f' bgp bestpath compare-routerid', frrconfig) @@ -1178,22 +1184,15 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' sid vpn export {sid}', afiv6_config) self.assertIn(f' nexthop vpn export {nexthop_ipv6}', afiv4_config) - def test_bgp_25_ipv4_ipv6_labeled_unicast_peer_group(self): + def test_bgp_25_ipv4_labeled_unicast_peer_group(self): pg_ipv4 = 'foo4' - pg_ipv6 = 'foo6' - ipv4_max_prefix = '20' - ipv6_max_prefix = '200' ipv4_prefix = '192.0.2.0/24' - ipv6_prefix = '2001:db8:1000::/64' self.cli_set(base_path + ['listen', 'range', ipv4_prefix, 'peer-group', pg_ipv4]) - self.cli_set(base_path + ['listen', 'range', ipv6_prefix, 'peer-group', pg_ipv6]) - + self.cli_set(base_path + ['parameters', 'labeled-unicast', 'ipv4-explicit-null']) self.cli_set(base_path + ['peer-group', pg_ipv4, 'address-family', 'ipv4-labeled-unicast', 'maximum-prefix', ipv4_max_prefix]) self.cli_set(base_path + ['peer-group', pg_ipv4, 'remote-as', 'external']) - self.cli_set(base_path + ['peer-group', pg_ipv6, 'address-family', 'ipv6-labeled-unicast', 'maximum-prefix', ipv6_max_prefix]) - self.cli_set(base_path + ['peer-group', pg_ipv6, 'remote-as', 'external']) self.cli_commit() @@ -1201,15 +1200,33 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'router bgp {ASN}', frrconfig) self.assertIn(f' neighbor {pg_ipv4} peer-group', frrconfig) self.assertIn(f' neighbor {pg_ipv4} remote-as external', frrconfig) - self.assertIn(f' neighbor {pg_ipv6} peer-group', frrconfig) - self.assertIn(f' neighbor {pg_ipv6} remote-as external', frrconfig) self.assertIn(f' bgp listen range {ipv4_prefix} peer-group {pg_ipv4}', frrconfig) - self.assertIn(f' bgp listen range {ipv6_prefix} peer-group {pg_ipv6}', frrconfig) + self.assertIn(f' bgp labeled-unicast ipv4-explicit-null', frrconfig) afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast') self.assertIn(f' neighbor {pg_ipv4} activate', afiv4_config) self.assertIn(f' neighbor {pg_ipv4} maximum-prefix {ipv4_max_prefix}', afiv4_config) + def test_bgp_26_ipv6_labeled_unicast_peer_group(self): + pg_ipv6 = 'foo6' + ipv6_max_prefix = '200' + ipv6_prefix = '2001:db8:1000::/64' + + self.cli_set(base_path + ['listen', 'range', ipv6_prefix, 'peer-group', pg_ipv6]) + self.cli_set(base_path + ['parameters', 'labeled-unicast', 'ipv6-explicit-null']) + + self.cli_set(base_path + ['peer-group', pg_ipv6, 'address-family', 'ipv6-labeled-unicast', 'maximum-prefix', ipv6_max_prefix]) + self.cli_set(base_path + ['peer-group', pg_ipv6, 'remote-as', 'external']) + + self.cli_commit() + + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f' neighbor {pg_ipv6} peer-group', frrconfig) + self.assertIn(f' neighbor {pg_ipv6} remote-as external', frrconfig) + self.assertIn(f' bgp listen range {ipv6_prefix} peer-group {pg_ipv6}', frrconfig) + self.assertIn(f' bgp labeled-unicast ipv6-explicit-null', frrconfig) + afiv6_config = self.getFRRconfig(' address-family ipv6 labeled-unicast') self.assertIn(f' neighbor {pg_ipv6} activate', afiv6_config) self.assertIn(f' neighbor {pg_ipv6} maximum-prefix {ipv6_max_prefix}', afiv6_config) diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py index ab3f076ac..b43c626c4 100755 --- a/smoketest/scripts/cli/test_protocols_rpki.py +++ b/smoketest/scripts/cli/test_protocols_rpki.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -26,7 +26,6 @@ from vyos.utils.process import process_named_running base_path = ['protocols', 'rpki'] PROCESS_NAME = 'bgpd' -rpki_known_hosts = '/config/auth/known_hosts' rpki_ssh_key = '/config/auth/id_rsa_rpki' rpki_ssh_pub = f'{rpki_ssh_key}.pub' @@ -91,7 +90,6 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): self.assertIn(f'rpki cache {peer} {port} preference {preference}', frrconfig) def test_rpki_ssh(self): - self.skipTest('Currently untested, see: https://github.com/FRRouting/frr/issues/7978') polling = '7200' cache = { '192.0.2.3' : { @@ -114,7 +112,6 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['cache', peer, 'ssh', 'username', peer_config['username']]) self.cli_set(base_path + ['cache', peer, 'ssh', 'public-key-file', rpki_ssh_pub]) self.cli_set(base_path + ['cache', peer, 'ssh', 'private-key-file', rpki_ssh_key]) - self.cli_set(base_path + ['cache', peer, 'ssh', 'known-hosts-file', rpki_known_hosts]) # commit changes self.cli_commit() @@ -127,7 +124,7 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): port = peer_config['port'] preference = peer_config['preference'] username = peer_config['username'] - self.assertIn(f'rpki cache {peer} {port} {username} {rpki_ssh_key} {rpki_known_hosts} preference {preference}', frrconfig) + self.assertIn(f'rpki cache {peer} {port} {username} {rpki_ssh_key} {rpki_ssh_pub} preference {preference}', frrconfig) def test_rpki_verify_preference(self): @@ -156,7 +153,4 @@ if __name__ == '__main__': if not os.path.isfile(rpki_ssh_key): cmd(f'ssh-keygen -t rsa -f {rpki_ssh_key} -N ""') - if not os.path.isfile(rpki_known_hosts): - cmd(f'touch {rpki_known_hosts}') - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 8d9b8459e..94eade2d7 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -19,6 +19,7 @@ import json from requests import request from urllib3.exceptions import InsecureRequestWarning +from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM from base_vyostest_shim import ignore_warning @@ -305,6 +306,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) self.cli_commit() + sleep(2) r = request('POST', url, verify=False, headers=headers, data=payload) # api configured; expect 200 diff --git a/smoketest/scripts/cli/test_vpn_l2tp.py b/smoketest/scripts/cli/test_vpn_l2tp.py index dc75ba5f9..c3b5b500d 100755 --- a/smoketest/scripts/cli/test_vpn_l2tp.py +++ b/smoketest/scripts/cli/test_vpn_l2tp.py @@ -54,6 +54,47 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): self.assertEqual(conf['modules']['auth_mschap_v2'], None) + def test_vpn_l2tp_dependence_ipsec_swanctl(self): + # Test config vpn for tasks T3843 and T5926 + + base_path = ['vpn', 'l2tp', 'remote-access'] + # make precondition + self.cli_set(['interfaces', 'dummy', 'dum0', 'address', '203.0.113.1/32']) + self.cli_set(['vpn', 'ipsec', 'interface', 'dum0']) + + self.cli_commit() + # check ipsec apply to swanctl + self.assertEqual('', cmd('echo vyos | sudo -S swanctl -L ')) + + self.cli_set(base_path + ['authentication', 'local-users', 'username', 'foo', 'password', 'bar']) + self.cli_set(base_path + ['authentication', 'mode', 'local']) + self.cli_set(base_path + ['authentication', 'protocols', 'chap']) + self.cli_set(base_path + ['client-ip-pool', 'first', 'range', '10.200.100.100-10.200.100.110']) + self.cli_set(base_path + ['description', 'VPN - REMOTE']) + self.cli_set(base_path + ['name-server', '1.1.1.1']) + self.cli_set(base_path + ['ipsec-settings', 'authentication', 'mode', 'pre-shared-secret']) + self.cli_set(base_path + ['ipsec-settings', 'authentication', 'pre-shared-secret', 'SeCret']) + self.cli_set(base_path + ['ipsec-settings', 'ike-lifetime', '8600']) + self.cli_set(base_path + ['ipsec-settings', 'lifetime', '3600']) + self.cli_set(base_path + ['outside-address', '203.0.113.1']) + self.cli_set(base_path + ['gateway-address', '203.0.113.1']) + + self.cli_commit() + + # check l2tp apply to swanctl + self.assertTrue('l2tp_remote_access:' in cmd('echo vyos | sudo -S swanctl -L ')) + + self.cli_delete(['vpn', 'l2tp']) + self.cli_commit() + + # check l2tp apply to swanctl after delete config + self.assertEqual('', cmd('echo vyos | sudo -S swanctl -L ')) + + # need to correct tearDown test + self.basic_config() + self.cli_set(base_path + ['authentication', 'protocols', 'chap']) + self.cli_commit() + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index 6207a1b41..a3090ee41 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -53,14 +53,17 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # call base-classes classmethod super(VRFTest, cls).setUpClass() + def setUp(self): + # VRF strict_most ist always enabled + tmp = read_file('/proc/sys/net/vrf/strict_mode') + self.assertEqual(tmp, '1') + def tearDown(self): # delete all VRFs self.cli_delete(base_path) self.cli_commit() for vrf in vrfs: self.assertNotIn(vrf, interfaces()) - # If there is no VRF defined, strict_mode should be off - self.assertEqual(sysctl_read('net.vrf.strict_mode'), '0') def test_vrf_vni_and_table_id(self): base_table = '1000' diff --git a/src/completion/list_images.py b/src/completion/list_images.py new file mode 100755 index 000000000..eae29c084 --- /dev/null +++ b/src/completion/list_images.py @@ -0,0 +1,44 @@ +#!/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 argparse +import os +import sys + +from vyos.system.image import is_live_boot +from vyos.system.image import get_running_image + + +parser = argparse.ArgumentParser(description='list available system images') +parser.add_argument('--no-running', action='store_true', + help='do not display the currently running image') + +def get_images(omit_running: bool = False) -> list[str]: + if is_live_boot(): + return [] + images = os.listdir("/lib/live/mount/persistence/boot") + if omit_running: + images.remove(get_running_image()) + if 'grub' in images: + images.remove('grub') + if 'efi' in images: + images.remove('efi') + return sorted(images) + +if __name__ == '__main__': + args = parser.parse_args() + print("\n".join(get_images(omit_running=args.no_running))) + sys.exit(0) diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py index 05e876f3b..0fc14e868 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -63,11 +63,11 @@ def verify(rpki): preferences.append(preference) if 'ssh' in peer_config: - files = ['private_key_file', 'public_key_file', 'known_hosts_file'] + files = ['private_key_file', 'public_key_file'] for file in files: if file not in peer_config['ssh']: - raise ConfigError('RPKI+SSH requires username, public/private ' \ - 'keys and known-hosts file to be defined!') + raise ConfigError('RPKI+SSH requires username and public/private ' \ + 'key file to be defined!') filename = peer_config['ssh'][file] if not os.path.exists(filename): diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index 40d7a6c16..4a0b4d0c5 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -36,7 +36,7 @@ from vyos.qos import RateLimiter from vyos.qos import RoundRobin from vyos.qos import TrafficShaper from vyos.qos import TrafficShaperHFSC -from vyos.utils.process import call +from vyos.utils.process import run from vyos.utils.dict import dict_search_recursive from vyos import ConfigError from vyos import airbag @@ -205,8 +205,8 @@ def apply(qos): # Always delete "old" shapers first for interface in interfaces(): # Ignore errors (may have no qdisc) - call(f'tc qdisc del dev {interface} parent ffff:') - call(f'tc qdisc del dev {interface} root') + run(f'tc qdisc del dev {interface} parent ffff:') + run(f'tc qdisc del dev {interface} root') call_dependents() diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index fc87d9539..266381754 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -19,6 +19,7 @@ import os from sys import exit from vyos.config import Config +from vyos.configdep import call_dependents, set_dependents from vyos.configdict import get_accel_dict from vyos.template import render from vyos.utils.process import call @@ -41,6 +42,9 @@ def get_config(config=None): else: conf = Config() base = ['vpn', 'l2tp', 'remote-access'] + + set_dependents('ipsec', conf) + if not conf.exists(base): return None @@ -87,10 +91,10 @@ def apply(l2tp): for file in [l2tp_chap_secrets, l2tp_conf]: if os.path.exists(file): os.unlink(file) + else: + call('systemctl restart accel-ppp@l2tp.service') - return None - - call('systemctl restart accel-ppp@l2tp.service') + call_dependents() if __name__ == '__main__': diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 421ac6997..08e4fc6db 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -91,7 +91,7 @@ def verify(ocserv): if not ocserv["authentication"]['radius']['server']: raise ConfigError('Openconnect authentication mode radius requires at least one RADIUS server') if "local" in ocserv["authentication"]["mode"]: - if not ocserv["authentication"]["local_users"]: + if not ocserv.get("authentication", {}).get("local_users"): raise ConfigError('openconnect mode local required at least one user') if not ocserv["authentication"]["local_users"]["username"]: raise ConfigError('openconnect mode local required at least one user') diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index f2c544aa6..a2f4956be 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -27,7 +27,6 @@ from vyos.ifconfig import Interface from vyos.template import render from vyos.template import render_to_string from vyos.utils.dict import dict_search -from vyos.utils.kernel import check_kmod from vyos.utils.network import get_interface_config from vyos.utils.network import get_vrf_members from vyos.utils.network import interface_exists @@ -223,18 +222,6 @@ def apply(vrf): # Delete the VRF Kernel interface call(f'ip link delete dev {tmp}') - # Enable/Disable VRF strict mode - # When net.vrf.strict_mode=0 (default) it is possible to associate multiple - # VRF devices to the same table. Conversely, when net.vrf.strict_mode=1 a - # table can be associated to a single VRF device. - # - # A VRF table can be used by the VyOS CLI only once (ensured by verify()), - # this simply adds an additional Kernel safety net - strict_mode = '0' - # Set to 1 if any VRF is defined - if 'name' in vrf: strict_mode = '1' - sysctl_write('net.vrf.strict_mode', strict_mode) - if 'name' in vrf: # Linux routing uses rules to find tables - routing targets are then # looked up in those tables. If the lookup got a matching route, the @@ -323,7 +310,6 @@ def apply(vrf): if __name__ == '__main__': try: - check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index 6291be5f0..c9b8ef8fe 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -108,3 +108,5 @@ net.ipv4.tcp_congestion_control=bbr # Disable IPv6 Segment Routing packets by default net.ipv6.conf.all.seg6_enabled = 0 net.ipv6.conf.default.seg6_enabled = 0 + +net.vrf.strict_mode = 1 diff --git a/src/init/vyos-router b/src/init/vyos-router index aaecbf2a1..2b4fac5ef 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -448,6 +448,10 @@ start () restore_if_missing_postconfig_script run_postconfig_scripts + tmp=$(${vyos_libexec_dir}/read-saved-value.py --path "protocols rpki cache") + if [ ! -z $tmp ]; then + vtysh -c "rpki start" + fi } stop() diff --git a/src/migration-scripts/rpki/1-to-2 b/src/migration-scripts/rpki/1-to-2 new file mode 100755 index 000000000..559440bba --- /dev/null +++ b/src/migration-scripts/rpki/1-to-2 @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T6011: rpki: known-hosts-file is no longer supported bxy FRR CLI, +# remove VyOS CLI node + +from sys import exit +from sys import argv +from vyos.configtree import ConfigTree + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['protocols', 'rpki'] +config = ConfigTree(config_file) + +# Nothing to do +if not config.exists(base): + exit(0) + +if config.exists(base + ['cache']): + for cache in config.list_nodes(base + ['cache']): + ssh_node = base + ['cache', cache, 'ssh'] + if config.exists(ssh_node + ['known-hosts-file']): + config.delete(ssh_node + ['known-hosts-file']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/file.py b/src/op_mode/file.py new file mode 100755 index 000000000..bf13bed6f --- /dev/null +++ b/src/op_mode/file.py @@ -0,0 +1,383 @@ +#!/usr/bin/python3 + +# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# 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 argparse +import contextlib +import datetime +import grp +import os +import pwd +import shutil +import sys +import tempfile + +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 run + + +parser = argparse.ArgumentParser(description='view, copy or remove files and directories', + formatter_class=argparse.RawDescriptionHelpFormatter) +parser.epilog = """ +TYPE is one of 'remote', 'image' and 'local'. +A local path is <path> or ~/<path>. +A remote path is <scheme>://<urn>. +An image path is <image>:<path>. + +Clone operation is between images only. +Copy operation does not support directories from remote locations. +Delete operation does not support remote paths. +""" +operations = parser.add_mutually_exclusive_group(required=True) +operations.add_argument('--show', nargs=1, help='show the contents of file PATH of type TYPE', + metavar=('PATH')) +operations.add_argument('--copy', nargs=2, help='copy SRC to DEST', + metavar=('SRC', 'DEST')) +operations.add_argument('--delete', nargs=1, help='delete file PATH', + metavar=('PATH')) +operations.add_argument('--clone', help='clone config from running image to IMG', + metavar='IMG') +operations.add_argument('--clone-from', nargs=2, help='clone config from image SRC to image DEST', + metavar=('SRC', 'DEST')) + +## Helper procedures +def fix_terminal() -> None: + """ + Reset terminal after potential breakage caused by abrupt exits. + """ + run('stty sane') + +def get_types(arg: str) -> tuple[str, str]: + """ + Determine whether the argument shows a local, image or remote path. + """ + schemes = ['http', 'https', 'ftp', 'ftps', 'sftp', 'ssh', 'scp', 'tftp'] + s = arg.split("://", 1) + if len(s) != 2: + return 'local', arg + elif s[0] in schemes: + return 'remote', arg + else: + return 'image', arg + +def zealous_copy(source: str, destination: str) -> None: + # Even shutil.copy2() doesn't preserve ownership across copies. + # So we need to resort to this. + stats = os.stat(source) + shutil.copy2(source, destination) + os.chown(destination, stats.st_uid, stats.st_gid) + +def get_file_type(path: str) -> str: + return cmd(['file', '-sb', path]) + +def print_header(string: str) -> None: + print('#' * 10, string, '#' * 10) + +def octal_to_symbolic(octal: str) -> str: + perms = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx'] + result = "" + # We discard all but the last three digits because we're only + # interested in the permission bits. + for i in octal[-3:]: + result += perms[int(i)] + return result + +def get_user_and_group(stats: os.stat_result) -> tuple[str, str]: + try: + user = pwd.getpwuid(stats.st_uid).pw_name + except (KeyError, PermissionError): + user = str(stats.st_uid) + try: + group = grp.getgrgid(stats.st_gid).gr_name + except (KeyError, PermissionError): + group = str(stats.st_gid) + return user, group + +def print_file_info(path: str) -> None: + stats = os.stat(path) + username, groupname = get_user_and_group(stats) + mtime = datetime.datetime.fromtimestamp(stats.st_mtime).strftime("%F %X") + print_header('FILE INFO') + print(f'Path:\t\t{path}') + # File type is determined through `file(1)`. + print(f'Type:\t\t{get_file_type(path)}') + # Owner user and group + print(f'Owner:\t\t{username}:{groupname}') + # Permissions are converted from raw int to octal string to symbolic string. + print(f'Permissions:\t{octal_to_symbolic(oct(stats.st_mode))}') + # Last date of modification + print(f'Modified:\t{mtime}') + +def print_file_data(path: str) -> None: + print_header('FILE DATA') + file_type = get_file_type(path) + # Human-readable files are streamed line-by-line. + if 'text' in file_type: + with open(path, 'r') as f: + for line in f: + print(line, end='') + # tcpdump files go to TShark. + elif 'pcap' in file_type or os.path.splitext(path)[1] == '.pcap': + print(cmd(['sudo', 'tshark', '-r', path])) + # All other binaries get hexdumped. + else: + print(cmd(['hexdump', '-C', path])) + +def parse_image_path(image_path: str) -> str: + """ + my-image:/foo/bar -> /lib/live/mount/persistence/boot/my-image/rw/foo/bar + """ + image_name, path = image_path.split('://', 1) + if image_name == 'running': + image_root = '/' + elif image_name == 'disk-install': + image_root = '/lib/live/mount/persistence/' + else: + image_root = os.path.join('/lib/live/mount/persistence/boot', image_name, 'rw') + if not os.path.isdir(image_root): + print_error(f'Image {image_name} not found.') + sys.exit(1) + return os.path.join(image_root, path) + + +## Show procedures +def show_locally(path: str) -> None: + """ + Display the contents of a local file or directory. + """ + location = os.path.realpath(os.path.expanduser(path)) + # Temporarily redirect stdout to a throwaway file for `less(1)` to read. + # The output could be potentially too hefty for an in-memory StringIO. + temp = tempfile.NamedTemporaryFile('w', delete=False) + try: + with contextlib.redirect_stdout(temp): + # Just a directory. Call `ls(1)` and bail. + if os.path.isdir(location): + print_header('DIRECTORY LISTING') + print('Path:\t', location) + print(cmd(['ls', '-hlFGL', '--group-directories-first', location])) + elif os.path.isfile(location): + print_file_info(location) + print() + print_file_data(location) + else: + print_error(f'File or directory {path} not found.') + 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) + # 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. + except (BrokenPipeError, EOFError, KeyboardInterrupt, OSError): + fix_terminal() + sys.exit(1) + finally: + os.remove(temp.name) + +def show(type: str, path: str) -> None: + if type == 'remote': + temp = tempfile.NamedTemporaryFile(delete=False) + download(temp.name, path) + show_locally(temp.name) + os.remove(temp.name) + elif type == 'image': + show_locally(parse_image_path(path)) + elif type == 'local': + show_locally(path) + else: + print_error(f'Unknown target for showing: {type}') + print_error('Valid types are "remote", "image" and "local".') + sys.exit(1) + + +## Copying procedures +def copy(source_type: str, source_path: str, + destination_type: str, destination_path: str) -> None: + """ + Copy a file or directory locally, remotely or to and from an image. + Directory uploads and downloads not supported. + """ + source = '' + try: + # Download to a temporary file and use that as the source. + if source_type == 'remote': + source = tempfile.NamedTemporaryFile(delete=False).name + download(source, source_path) + # Prepend the image root to the path. + elif source_type == 'image': + source = parse_image_path(source_path) + elif source_type == 'local': + source = source_path + else: + print_error(f'Unknown source type: {source_type}') + print_error(f'Valid source types are "remote", "image" and "local".') + sys.exit(1) + + # Directly upload the file. + if destination_type == 'remote': + if os.path.isdir(source): + print_error(f'Cannot upload {source}. Directory uploads not supported.') + sys.exit(1) + upload(source, destination_path) + # No need to duplicate local copy operations for image copying. + elif destination_type == 'image': + copy('local', source, 'local', parse_image_path(destination_path)) + # Try to preserve metadata when copying. + elif destination_type == 'local': + if os.path.isdir(destination_path): + destination_path = os.path.join(destination_path, os.path.basename(source)) + if os.path.isdir(source): + shutil.copytree(source, destination_path, copy_function=zealous_copy) + else: + zealous_copy(source, destination_path) + else: + print_error(f'Unknown destination type: {source_type}') + print_error(f'Valid destination types are "remote", "image" and "local".') + sys.exit(1) + except OSError: + import traceback + # We can't check for every single user error (eg copying a directory to a file) + # so we just let a curtailed stack trace provide a descriptive error. + print_error(f'Failed to copy {source_path} to {destination_path}.') + traceback.print_exception(*sys.exc_info()[:2], None) + sys.exit(1) + else: + # To prevent a duplicate message. + if destination_type != 'image': + print('Copy successful.') + finally: + # Clean up temporary file. + if source_type == 'remote': + os.remove(source) + + +## Deletion procedures +def delete_locally(path: str) -> None: + """ + Remove a local file or directory. + """ + try: + if os.path.isdir(path): + if (ask_yes_no(f'Do you want to remove {path} with all its contents?')): + shutil.rmtree(path) + print(f'Directory {path} removed.') + else: + print('Operation aborted.') + elif os.path.isfile(path): + if (ask_yes_no(f'Do you want to remove {path}?')): + os.remove(path) + print(f'File {path} removed.') + else: + print('Operation aborted.') + else: + raise OSError(f'File or directory {path} not found.') + except OSError: + import traceback + print_error(f'Failed to delete {path}.') + traceback.print_exception(*sys.exc_info()[:2], None) + sys.exit(1) + +def delete(type: str, path: str) -> None: + if type == 'local': + delete_locally(path) + elif type == 'image': + delete_locally(parse_image_path(path)) + else: + print_error(f'Unknown target for deletion: {type}') + print_error('Valid types are "image" and "local".') + sys.exit(1) + + +## Cloning procedures +def clone(source: str, destination: str) -> None: + if os.geteuid(): + print_error('Only the superuser can run this command.') + sys.exit(1) + if destination == 'running' or destination == 'disk-install': + print_error(f'Cannot clone config to {destination}.') + sys.exit(1) + # If `source` is None, then we're going to copy from the running image. + if source is None or source == 'running': + source_path = '/config' + # For the warning message only. + source = 'the current' + else: + source_path = parse_image_path(source + ':/config') + destination_path = parse_image_path(destination + ':/config') + backup_path = destination_path + '.preclone' + + if not os.path.isdir(source_path): + print_error(f'Source image {source} does not exist.') + sys.exit(1) + if not os.path.isdir(destination_path): + print_error(f'Destination image {destination} does not exist.') + sys.exit(1) + print(f'WARNING: This operation will erase /config data in image {destination}.') + print(f'/config data in {source} image will be copied over in its place.') + print(f'The existing /config data in {destination} image will be backed up to /config.preclone.') + + if ask_yes_no('Are you sure you want to continue?'): + try: + if os.path.isdir(backup_path): + print('Removing previous backup...') + shutil.rmtree(backup_path) + print('Making new backup...') + shutil.move(destination_path, backup_path) + except: + print('Something went wrong during the backup process!') + print('Cowardly refusing to proceed with cloning.') + raise + # Copy new config from image. + try: + shutil.copytree(source_path, destination_path, copy_function=zealous_copy) + except: + print('Cloning failed! Reverting to backup!') + # Delete leftover files from the botched cloning. + shutil.rmtree(destination_path, ignore_errors=True) + # Restore backup before bailing out. + shutil.copytree(backup_path, destination_path, copy_function=zealous_copy) + raise + else: + print(f'Successfully cloned config from {source} to {destination}.') + finally: + shutil.rmtree(backup_path) + else: + print('Operation aborted.') + +if __name__ == '__main__': + args = parser.parse_args() + try: + if args.show: + show(*get_types(args.show[0])) + elif args.copy: + copy(*get_types(args.copy[0]), + *get_types(args.copy[1])) + elif args.delete: + delete(*get_types(args.delete[0])) + elif args.clone_from: + clone(*args.clone_from) + elif args.clone: + # Pass None as source image to copy from local image. + clone(None, args.clone) + except KeyboardInterrupt: + print_error('Operation cancelled by user.') + sys.exit(1) + sys.exit(0) diff --git a/src/op_mode/generate_ipsec_debug_archive.py b/src/op_mode/generate_ipsec_debug_archive.py index 60195d48b..ca2eeb511 100755 --- a/src/op_mode/generate_ipsec_debug_archive.py +++ b/src/op_mode/generate_ipsec_debug_archive.py @@ -24,7 +24,6 @@ from vyos.utils.process import rc_cmd # define a list of commands that needs to be executed CMD_LIST: list[str] = [ - 'ipsec status', 'swanctl -L', 'swanctl -l', 'swanctl -P', @@ -36,7 +35,7 @@ CMD_LIST: list[str] = [ 'ip route | head -100', 'ip route show table 220' ] -JOURNALCTL_CMD: str = 'journalctl -b -n 10000 /usr/lib/ipsec/charon' +JOURNALCTL_CMD: str = 'journalctl --no-hostname --boot --unit strongswan.service' # execute a command and save the output to a file def save_stdout(command: str, file: Path) -> None: diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index 501e9b804..d677c2cf8 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -599,6 +599,8 @@ def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None: print('Unmounting target filesystems') for mountpoint in mounts: disk.partition_umount(mountpoint) + for mountpoint in mounts: + disk.wait_for_umount(mountpoint) if remove_items: print('Removing temporary files') for remove_item in remove_items: @@ -606,7 +608,8 @@ def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None: if Path(remove_item).is_file(): Path(remove_item).unlink() if Path(remove_item).is_dir(): - rmtree(remove_item) + rmtree(remove_item, ignore_errors=True) + def cleanup_raid(details: raid.RaidDetails) -> None: efiparts = [] |