summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/config-mode-dependencies/vyos-1x.json3
-rw-r--r--data/templates/frr/bgpd.frr.j218
-rw-r--r--data/templates/frr/rpki.frr.j22
-rw-r--r--interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i1
-rw-r--r--interface-definitions/include/bgp/protocol-common-config.xml.i35
-rw-r--r--interface-definitions/include/version/rpki-version.xml.i2
-rw-r--r--interface-definitions/protocols_rpki.xml.in8
-rw-r--r--interface-definitions/system_frr.xml.in2
-rw-r--r--op-mode-definitions/file.xml.in86
-rw-r--r--op-mode-definitions/show-system.xml.in4
-rw-r--r--op-mode-definitions/show-version.xml.in2
-rw-r--r--python/vyos/qos/trafficshaper.py2
-rw-r--r--python/vyos/remote.py6
-rw-r--r--python/vyos/system/disk.py15
-rw-r--r--python/vyos/utils/config.py9
-rw-r--r--schema/op-mode-definition.rnc8
-rw-r--r--schema/op-mode-definition.rng5
-rwxr-xr-xscripts/build-command-op-templates7
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py43
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rpki.py10
-rwxr-xr-xsmoketest/scripts/cli/test_service_https.py2
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_l2tp.py41
-rwxr-xr-xsmoketest/scripts/cli/test_vrf.py7
-rwxr-xr-xsrc/completion/list_images.py44
-rwxr-xr-xsrc/conf_mode/protocols_rpki.py6
-rwxr-xr-xsrc/conf_mode/qos.py8
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py10
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py2
-rwxr-xr-xsrc/conf_mode/vrf.py14
-rw-r--r--src/etc/sysctl.d/30-vyos-router.conf2
-rwxr-xr-xsrc/init/vyos-router4
-rwxr-xr-xsrc/migration-scripts/rpki/1-to-251
-rwxr-xr-xsrc/op_mode/file.py383
-rwxr-xr-xsrc/op_mode/generate_ipsec_debug_archive.py3
-rwxr-xr-xsrc/op_mode/image_installer.py5
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 = []