summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/accel-ppp/pptp.config.j210
-rw-r--r--data/templates/frr/rpki.frr.j26
-rw-r--r--interface-definitions/high-availability.xml.in9
-rw-r--r--interface-definitions/include/version/pptp-version.xml.i2
-rw-r--r--interface-definitions/protocols_rpki.xml.in30
-rw-r--r--interface-definitions/service_ipoe-server.xml.in150
-rw-r--r--interface-definitions/vpn_l2tp.xml.in34
-rw-r--r--interface-definitions/vpn_pptp.xml.in96
-rw-r--r--op-mode-definitions/file.xml.in86
-rw-r--r--python/vyos/accel_ppp_util.py4
-rw-r--r--python/vyos/remote.py6
-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_rpki.py25
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_l2tp.py2
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_pptp.py160
-rwxr-xr-xsrc/completion/list_images.py44
-rwxr-xr-xsrc/conf_mode/qos.py8
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py12
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py8
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py7
-rwxr-xr-xsrc/migration-scripts/https/5-to-64
-rwxr-xr-xsrc/migration-scripts/pptp/4-to-566
-rwxr-xr-xsrc/op_mode/file.py383
25 files changed, 782 insertions, 390 deletions
diff --git a/data/templates/accel-ppp/pptp.config.j2 b/data/templates/accel-ppp/pptp.config.j2
index 7fe4b17bf..290e6235d 100644
--- a/data/templates/accel-ppp/pptp.config.j2
+++ b/data/templates/accel-ppp/pptp.config.j2
@@ -9,15 +9,7 @@ ippool
{# Common IPv6 definitions #}
{% include 'accel-ppp/config_modules_ipv6.j2' %}
{# Common authentication protocols (pap, chap ...) #}
-{% if authentication.require is vyos_defined %}
-{% if authentication.require == 'chap' %}
-auth_chap_md5
-{% elif authentication.require == 'mschap' %}
-auth_mschap_v1
-{% else %}
-auth_{{ authentication.require.replace('-', '_') }}
-{% endif %}
-{% endif %}
+{% include 'accel-ppp/config_modules_auth_protocols.j2' %}
[core]
thread-count={{ thread_count }}
diff --git a/data/templates/frr/rpki.frr.j2 b/data/templates/frr/rpki.frr.j2
index 384cbbe52..59724102c 100644
--- a/data/templates/frr/rpki.frr.j2
+++ b/data/templates/frr/rpki.frr.j2
@@ -11,8 +11,14 @@ rpki
{% endif %}
{% endfor %}
{% endif %}
+{% if expire_interval is vyos_defined %}
+ rpki expire_interval {{ expire_interval }}
+{% endif %}
{% if polling_period is vyos_defined %}
rpki polling_period {{ polling_period }}
{% endif %}
+{% if retry_interval is vyos_defined %}
+ rpki retry_interval {{ retry_interval }}
+{% endif %}
exit
!
diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in
index aa23888a4..59f0f1052 100644
--- a/interface-definitions/high-availability.xml.in
+++ b/interface-definitions/high-availability.xml.in
@@ -276,8 +276,17 @@
<format>ipv6net</format>
<description>IPv6 address and prefix length</description>
</valueHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address</description>
+ </valueHelp>
<constraint>
<validator name="ip-host"/>
+ <validator name="ip-address"/>
</constraint>
</properties>
<children>
diff --git a/interface-definitions/include/version/pptp-version.xml.i b/interface-definitions/include/version/pptp-version.xml.i
index 3e1482ecc..a877d77ff 100644
--- a/interface-definitions/include/version/pptp-version.xml.i
+++ b/interface-definitions/include/version/pptp-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/pptp-version.xml.i -->
-<syntaxVersion component='pptp' version='4'></syntaxVersion>
+<syntaxVersion component='pptp' version='5'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/protocols_rpki.xml.in b/interface-definitions/protocols_rpki.xml.in
index 6a38b2961..a2a0a2799 100644
--- a/interface-definitions/protocols_rpki.xml.in
+++ b/interface-definitions/protocols_rpki.xml.in
@@ -67,12 +67,25 @@
</node>
</children>
</tagNode>
+ <leafNode name="expire-interval">
+ <properties>
+ <help>Interval to wait before expiring the cache</help>
+ <valueHelp>
+ <format>u32:600-172800</format>
+ <description>Interval in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 600-172800"/>
+ </constraint>
+ </properties>
+ <defaultValue>7200</defaultValue>
+ </leafNode>
<leafNode name="polling-period">
<properties>
- <help>RPKI cache polling period</help>
+ <help>Cache polling interval</help>
<valueHelp>
<format>u32:1-86400</format>
- <description>Polling period in seconds</description>
+ <description>Interval in seconds</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 1-86400"/>
@@ -80,6 +93,19 @@
</properties>
<defaultValue>300</defaultValue>
</leafNode>
+ <leafNode name="retry-interval">
+ <properties>
+ <help>Retry interval to connect to the cache server</help>
+ <valueHelp>
+ <format>u32:1-7200</format>
+ <description>Interval in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-7200"/>
+ </constraint>
+ </properties>
+ <defaultValue>600</defaultValue>
+ </leafNode>
</children>
</node>
</children>
diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in
index eeec2aeef..23d6e54d1 100644
--- a/interface-definitions/service_ipoe-server.xml.in
+++ b/interface-definitions/service_ipoe-server.xml.in
@@ -8,6 +8,81 @@
<priority>900</priority>
</properties>
<children>
+ <node name="authentication">
+ <properties>
+ <help>Client authentication methods</help>
+ </properties>
+ <children>
+ #include <include/accel-ppp/auth-mode.xml.i>
+ <tagNode name="interface">
+ <properties>
+ <help>Network interface for client MAC addresses</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="mac">
+ <properties>
+ <help>Media Access Control (MAC) address</help>
+ <valueHelp>
+ <format>macaddr</format>
+ <description>Hardware (MAC) address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <node name="rate-limit">
+ <properties>
+ <help>Upload/Download speed limits</help>
+ </properties>
+ <children>
+ <leafNode name="upload">
+ <properties>
+ <help>Upload bandwidth limit in kbits/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="download">
+ <properties>
+ <help>Download bandwidth limit in kbits/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="vlan">
+ <properties>
+ <help>VLAN monitor for automatic creation of VLAN interfaces</help>
+ <valueHelp>
+ <format>u32:1-4094</format>
+ <description>Client VLAN id</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4094"/>
+ </constraint>
+ <constraintErrorMessage>VLAN IDs need to be in range 1-4094</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ #include <include/radius-auth-server-ipv4.xml.i>
+ #include <include/accel-ppp/radius-additions.xml.i>
+ <node name="radius">
+ <children>
+ #include <include/accel-ppp/radius-additions-rate-limit.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
<tagNode name="interface">
<properties>
<help>Interface to listen dhcp or unclassified packets</help>
@@ -107,81 +182,6 @@
#include <include/accel-ppp/client-ip-pool.xml.i>
#include <include/accel-ppp/gateway-address-multi.xml.i>
#include <include/accel-ppp/client-ipv6-pool.xml.i>
- <node name="authentication">
- <properties>
- <help>Client authentication methods</help>
- </properties>
- <children>
- #include <include/accel-ppp/auth-mode.xml.i>
- <tagNode name="interface">
- <properties>
- <help>Network interface for client MAC addresses</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces</script>
- </completionHelp>
- </properties>
- <children>
- <tagNode name="mac">
- <properties>
- <help>Media Access Control (MAC) address</help>
- <valueHelp>
- <format>macaddr</format>
- <description>Hardware (MAC) address</description>
- </valueHelp>
- <constraint>
- <validator name="mac-address"/>
- </constraint>
- </properties>
- <children>
- <node name="rate-limit">
- <properties>
- <help>Upload/Download speed limits</help>
- </properties>
- <children>
- <leafNode name="upload">
- <properties>
- <help>Upload bandwidth limit in kbits/sec</help>
- <constraint>
- <validator name="numeric" argument="--range 1-65535"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="download">
- <properties>
- <help>Download bandwidth limit in kbits/sec</help>
- <constraint>
- <validator name="numeric" argument="--range 1-65535"/>
- </constraint>
- </properties>
- </leafNode>
- </children>
- </node>
- <leafNode name="vlan">
- <properties>
- <help>VLAN monitor for automatic creation of VLAN interfaces</help>
- <valueHelp>
- <format>u32:1-4094</format>
- <description>Client VLAN id</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-4094"/>
- </constraint>
- <constraintErrorMessage>VLAN IDs need to be in range 1-4094</constraintErrorMessage>
- </properties>
- </leafNode>
- </children>
- </tagNode>
- </children>
- </tagNode>
- <node name="radius">
- <children>
- #include <include/accel-ppp/radius-additions-rate-limit.xml.i>
- </children>
- </node>
- #include <include/radius-auth-server-ipv4.xml.i>
- #include <include/accel-ppp/radius-additions.xml.i>
- </children>
- </node>
#include <include/accel-ppp/default-pool.xml.i>
#include <include/accel-ppp/default-ipv6-pool.xml.i>
</children>
diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in
index 942690bca..6148e3269 100644
--- a/interface-definitions/vpn_l2tp.xml.in
+++ b/interface-definitions/vpn_l2tp.xml.in
@@ -13,6 +13,23 @@
<help>Remote access L2TP VPN</help>
</properties>
<children>
+ <node name="authentication">
+ <properties>
+ <help>Authentication for remote access L2TP VPN</help>
+ </properties>
+ <children>
+ #include <include/accel-ppp/auth-local-users.xml.i>
+ #include <include/accel-ppp/auth-mode.xml.i>
+ #include <include/accel-ppp/auth-protocols.xml.i>
+ #include <include/radius-auth-server-ipv4.xml.i>
+ #include <include/accel-ppp/radius-additions.xml.i>
+ <node name="radius">
+ <children>
+ #include <include/accel-ppp/radius-additions-rate-limit.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
#include <include/accel-ppp/max-concurrent-sessions.xml.i>
#include <include/accel-ppp/mtu-128-16384.xml.i>
<leafNode name="mtu">
@@ -117,23 +134,6 @@
#include <include/accel-ppp/client-ipv6-pool.xml.i>
#include <include/generic-description.xml.i>
#include <include/dhcp-interface.xml.i>
- <node name="authentication">
- <properties>
- <help>Authentication for remote access L2TP VPN</help>
- </properties>
- <children>
- #include <include/accel-ppp/auth-protocols.xml.i>
- #include <include/accel-ppp/auth-mode.xml.i>
- #include <include/accel-ppp/auth-local-users.xml.i>
- #include <include/radius-auth-server-ipv4.xml.i>
- #include <include/accel-ppp/radius-additions.xml.i>
- <node name="radius">
- <children>
- #include <include/accel-ppp/radius-additions-rate-limit.xml.i>
- </children>
- </node>
- </children>
- </node>
#include <include/accel-ppp/ppp-options.xml.i>
#include <include/accel-ppp/default-pool.xml.i>
#include <include/accel-ppp/default-ipv6-pool.xml.i>
diff --git a/interface-definitions/vpn_pptp.xml.in b/interface-definitions/vpn_pptp.xml.in
index d23086c02..2e2a3bec4 100644
--- a/interface-definitions/vpn_pptp.xml.in
+++ b/interface-definitions/vpn_pptp.xml.in
@@ -13,6 +13,23 @@
<help>Remote access PPTP VPN</help>
</properties>
<children>
+ <node name="authentication">
+ <properties>
+ <help>Authentication for remote access PPTP VPN</help>
+ </properties>
+ <children>
+ #include <include/accel-ppp/auth-local-users.xml.i>
+ #include <include/accel-ppp/auth-mode.xml.i>
+ #include <include/accel-ppp/auth-protocols.xml.i>
+ #include <include/radius-auth-server-ipv4.xml.i>
+ #include <include/accel-ppp/radius-additions.xml.i>
+ <node name="radius">
+ <children>
+ #include <include/accel-ppp/radius-additions-rate-limit.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
#include <include/accel-ppp/max-concurrent-sessions.xml.i>
#include <include/accel-ppp/mtu-128-16384.xml.i>
<leafNode name="mtu">
@@ -30,85 +47,6 @@
#include <include/name-server-ipv4-ipv6.xml.i>
#include <include/accel-ppp/wins-server.xml.i>
#include <include/accel-ppp/client-ip-pool.xml.i>
- <node name="authentication">
- <properties>
- <help>Authentication for remote access PPTP VPN</help>
- </properties>
- <children>
- <leafNode name="require">
- <properties>
- <help>Authentication protocol for remote access peer PPTP VPN</help>
- <completionHelp>
- <list>pap chap mschap mschap-v2</list>
- </completionHelp>
- <valueHelp>
- <format>pap</format>
- <description>Require the peer to authenticate itself using PAP [Password Authentication Protocol].</description>
- </valueHelp>
- <valueHelp>
- <format>chap</format>
- <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description>
- </valueHelp>
- <valueHelp>
- <format>mschap</format>
- <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description>
- </valueHelp>
- <valueHelp>
- <format>mschap-v2</format>
- <description>Require the peer to authenticate itself using MS-CHAPv2 [Microsoft Challenge Handshake Authentication Protocol, Version 2].</description>
- </valueHelp>
- <constraint>
- <regex>(pap|chap|mschap|mschap-v2)</regex>
- </constraint>
- </properties>
- <defaultValue>mschap-v2</defaultValue>
- </leafNode>
- #include <include/accel-ppp/auth-mode.xml.i>
- <node name="local-users">
- <properties>
- <help>Local user authentication for remote access PPTP VPN</help>
- </properties>
- <children>
- <tagNode name="username">
- <properties>
- <help>User name for authentication</help>
- </properties>
- <children>
- #include <include/generic-disable-node.xml.i>
- <leafNode name="password">
- <properties>
- <help>Password for authentication</help>
- </properties>
- </leafNode>
- <leafNode name="static-ip">
- <properties>
- <help>Static client IP address</help>
- </properties>
- <defaultValue>*</defaultValue>
- </leafNode>
- </children>
- </tagNode>
- </children>
- </node>
- <node name="radius">
- <children>
- #include <include/accel-ppp/radius-additions-rate-limit.xml.i>
- </children>
- </node>
- #include <include/radius-auth-server-ipv4.xml.i>
- #include <include/accel-ppp/radius-additions.xml.i>
- <node name="radius">
- <children>
- <leafNode name="timeout">
- <defaultValue>30</defaultValue>
- </leafNode>
- <leafNode name="acct-timeout">
- <defaultValue>30</defaultValue>
- </leafNode>
- </children>
- </node>
- </children>
- </node>
#include <include/accel-ppp/default-pool.xml.i>
#include <include/accel-ppp/client-ipv6-pool.xml.i>
#include <include/accel-ppp/default-ipv6-pool.xml.i>
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/python/vyos/accel_ppp_util.py b/python/vyos/accel_ppp_util.py
index d60402e48..bd0c46a19 100644
--- a/python/vyos/accel_ppp_util.py
+++ b/python/vyos/accel_ppp_util.py
@@ -144,6 +144,10 @@ def verify_accel_ppp_base_service(config, local_users=True):
if "key" not in radius_config:
raise ConfigError(f'Missing RADIUS secret key for server "{server}"')
+ if dict_search('authentication.radius.dynamic_author.server', config):
+ if not dict_search('authentication.radius.dynamic_author.key', config):
+ raise ConfigError('DAE/CoA server key required!')
+
if "name_server_ipv4" in config:
if len(config["name_server_ipv4"]) > 2:
raise ConfigError(
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/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_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py
index b43c626c4..c52c0dd76 100755
--- a/smoketest/scripts/cli/test_protocols_rpki.py
+++ b/smoketest/scripts/cli/test_protocols_rpki.py
@@ -52,27 +52,28 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):
self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
def test_rpki(self):
- polling = '7200'
+ expire_interval = '3600'
+ polling_period = '600'
+ retry_interval = '300'
cache = {
'192.0.2.1' : {
'port' : '8080',
- 'preference' : '1'
- },
- '192.0.2.2' : {
- 'port' : '9090',
- 'preference' : '2'
+ 'preference' : '10'
},
'2001:db8::1' : {
'port' : '1234',
- 'preference' : '3'
+ 'preference' : '30'
},
- '2001:db8::2' : {
+ 'rpki.vyos.net' : {
'port' : '5678',
- 'preference' : '4'
+ 'preference' : '40'
},
}
- self.cli_set(base_path + ['polling-period', polling])
+ self.cli_set(base_path + ['expire-interval', expire_interval])
+ self.cli_set(base_path + ['polling-period', polling_period])
+ self.cli_set(base_path + ['retry-interval', retry_interval])
+
for peer, peer_config in cache.items():
self.cli_set(base_path + ['cache', peer, 'port', peer_config['port']])
self.cli_set(base_path + ['cache', peer, 'preference', peer_config['preference']])
@@ -82,7 +83,9 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):
# Verify FRR configuration
frrconfig = self.getFRRconfig('rpki')
- self.assertIn(f'rpki polling_period {polling}', frrconfig)
+ self.assertIn(f'rpki expire_interval {expire_interval}', frrconfig)
+ self.assertIn(f'rpki polling_period {polling_period}', frrconfig)
+ self.assertIn(f'rpki retry_interval {retry_interval}', frrconfig)
for peer, peer_config in cache.items():
port = peer_config['port']
diff --git a/smoketest/scripts/cli/test_vpn_l2tp.py b/smoketest/scripts/cli/test_vpn_l2tp.py
index e253f0e49..c3b5b500d 100755
--- a/smoketest/scripts/cli/test_vpn_l2tp.py
+++ b/smoketest/scripts/cli/test_vpn_l2tp.py
@@ -39,7 +39,7 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase):
pass
def test_l2tp_server_authentication_protocols(self):
- # Test configuration of local authentication for PPPoE server
+ # Test configuration of local authentication protocols
self.basic_config()
# explicitly test mschap-v2 - no special reason
diff --git a/smoketest/scripts/cli/test_vpn_pptp.py b/smoketest/scripts/cli/test_vpn_pptp.py
index 40dcb7f80..ac46d210d 100755
--- a/smoketest/scripts/cli/test_vpn_pptp.py
+++ b/smoketest/scripts/cli/test_vpn_pptp.py
@@ -40,165 +40,5 @@ class TestVPNPPTPServer(BasicAccelPPPTest.TestCase):
def basic_protocol_specific_config(self):
pass
- def test_accel_local_authentication(self):
- # Test configuration of local authentication
- self.basic_config()
-
- # upload / download limit
- user = "test"
- password = "test2"
- static_ip = "100.100.100.101"
- upload = "5000"
- download = "10000"
-
- self.set(
- [
- "authentication",
- "local-users",
- "username",
- user,
- "password",
- password,
- ]
- )
- self.set(
- [
- "authentication",
- "local-users",
- "username",
- user,
- "static-ip",
- static_ip,
- ]
- )
-
- # commit changes
- self.cli_commit()
-
- # Validate configuration values
- conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
- conf.read(self._config_file)
-
- # check proper path to chap-secrets file
- self.assertEqual(conf["chap-secrets"]["chap-secrets"], self._chap_secrets)
-
- # basic verification
- self.verify(conf)
-
- # check local users
- tmp = cmd(f"sudo cat {self._chap_secrets}")
- regex = f"{user}\s+\*\s+{password}\s+{static_ip}\s"
- tmp = re.findall(regex, tmp)
- self.assertTrue(tmp)
-
- # Check local-users default value(s)
- self.delete(["authentication", "local-users", "username", user, "static-ip"])
- # commit changes
- self.cli_commit()
-
- # check local users
- tmp = cmd(f"sudo cat {self._chap_secrets}")
- regex = f"{user}\s+\*\s+{password}\s+\*\s"
- tmp = re.findall(regex, tmp)
- self.assertTrue(tmp)
-
- def test_accel_radius_authentication(self):
- # Test configuration of RADIUS authentication for PPPoE server
- self.basic_config()
-
- radius_server = "192.0.2.22"
- radius_key = "secretVyOS"
- radius_port = "2000"
- radius_port_acc = "3000"
-
- self.set(["authentication", "mode", "radius"])
- self.set(
- ["authentication", "radius", "server", radius_server, "key", radius_key]
- )
- self.set(
- [
- "authentication",
- "radius",
- "server",
- radius_server,
- "port",
- radius_port,
- ]
- )
- self.set(
- [
- "authentication",
- "radius",
- "server",
- radius_server,
- "acct-port",
- radius_port_acc,
- ]
- )
-
- nas_id = "VyOS-PPPoE"
- nas_ip = "7.7.7.7"
- self.set(["authentication", "radius", "nas-identifier", nas_id])
- self.set(["authentication", "radius", "nas-ip-address", nas_ip])
-
- source_address = "1.2.3.4"
- self.set(["authentication", "radius", "source-address", source_address])
-
- # commit changes
- self.cli_commit()
-
- # Validate configuration values
- conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
- conf.read(self._config_file)
-
- # basic verification
- self.verify(conf)
-
- # check auth
- self.assertTrue(conf["radius"].getboolean("verbose"))
- self.assertEqual(conf["radius"]["acct-timeout"], "30")
- self.assertEqual(conf["radius"]["timeout"], "30")
- self.assertEqual(conf["radius"]["max-try"], "3")
-
- self.assertEqual(conf["radius"]["nas-identifier"], nas_id)
- self.assertEqual(conf["radius"]["nas-ip-address"], nas_ip)
- self.assertEqual(conf["radius"]["bind"], source_address)
-
- server = conf["radius"]["server"].split(",")
- self.assertEqual(radius_server, server[0])
- self.assertEqual(radius_key, server[1])
- self.assertEqual(f"auth-port={radius_port}", server[2])
- self.assertEqual(f"acct-port={radius_port_acc}", server[3])
- self.assertEqual(f"req-limit=0", server[4])
- self.assertEqual(f"fail-time=0", server[5])
-
- #
- # Disable Radius Accounting
- #
- self.delete(["authentication", "radius", "server", radius_server, "acct-port"])
- self.set(
- [
- "authentication",
- "radius",
- "server",
- radius_server,
- "disable-accounting",
- ]
- )
-
- # commit changes
- self.cli_commit()
-
- conf.read(self._config_file)
-
- server = conf["radius"]["server"].split(",")
- self.assertEqual(radius_server, server[0])
- self.assertEqual(radius_key, server[1])
- self.assertEqual(f"auth-port={radius_port}", server[2])
- self.assertEqual(f"acct-port=0", server[3])
- self.assertEqual(f"req-limit=0", server[4])
- self.assertEqual(f"fail-time=0", server[5])
-
-
if __name__ == '__main__':
unittest.main(verbosity=2)
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/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/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index 6df6f3dc7..5f72b983c 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -26,6 +26,7 @@ from vyos.utils.process import call
from vyos.utils.dict import dict_search
from vyos.accel_ppp_util import get_pools_in_order
from vyos.accel_ppp_util import verify_accel_ppp_ip_pool
+from vyos.accel_ppp_util import verify_accel_ppp_base_service
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -68,18 +69,9 @@ def verify(ipoe):
raise ConfigError('Option "client-subnet" incompatible with "vlan"!'
'Use "ipoe client-ip-pool" instead.')
+ verify_accel_ppp_base_service(ipoe, local_users=False)
verify_accel_ppp_ip_pool(ipoe)
- if dict_search('authentication.mode', ipoe) == 'radius':
- if not dict_search('authentication.radius.server', ipoe):
- raise ConfigError('RADIUS authentication requires at least one server')
-
- for server in dict_search('authentication.radius.server', ipoe):
- radius_config = ipoe['authentication']['radius']['server'][server]
- if 'key' not in radius_config:
- raise ConfigError(f'Missing RADIUS secret key for server "{server}"')
-
-
return None
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index 31299a15c..c2dfbdb44 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -68,6 +68,7 @@ def verify(pppoe):
return None
verify_accel_ppp_base_service(pppoe)
+ verify_accel_ppp_ip_pool(pppoe)
if 'wins_server' in pppoe and len(pppoe['wins_server']) > 2:
raise ConfigError('Not more then two WINS name-servers can be configured')
@@ -79,13 +80,6 @@ def verify(pppoe):
for interface in pppoe['interface']:
verify_interface_exists(interface)
- verify_accel_ppp_ip_pool(pppoe)
-
- if dict_search('authentication.radius.dynamic_author.server', pppoe):
- if not dict_search('authentication.radius.dynamic_author.key', pppoe):
- raise ConfigError('DA/CoE server key required!')
-
-
return None
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index 4ca717814..266381754 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -27,7 +27,6 @@ from vyos.utils.dict import dict_search
from vyos.accel_ppp_util import verify_accel_ppp_base_service
from vyos.accel_ppp_util import verify_accel_ppp_ip_pool
from vyos.accel_ppp_util import get_pools_in_order
-from vyos.base import Warning
from vyos import ConfigError
from vyos import airbag
@@ -64,14 +63,8 @@ def verify(l2tp):
return None
verify_accel_ppp_base_service(l2tp)
-
- if dict_search('authentication.radius.dynamic_author.server', l2tp):
- if not dict_search('authentication.radius.dynamic_author.key', l2tp):
- raise ConfigError('DA/CoE server key required!')
-
verify_accel_ppp_ip_pool(l2tp)
-
if 'wins_server' in l2tp and len(l2tp['wins_server']) > 2:
raise ConfigError(
'Not more then two WINS name-servers can be configured')
diff --git a/src/migration-scripts/https/5-to-6 b/src/migration-scripts/https/5-to-6
index 0090adccb..72e9e31f7 100755
--- a/src/migration-scripts/https/5-to-6
+++ b/src/migration-scripts/https/5-to-6
@@ -79,7 +79,7 @@ if config.exists(base + ['virtual-host']):
tmp = config.return_values(allow_path)
allow_client.extend(tmp)
- port_path = base + ['virtual-host', virtual_host, 'listen-port']
+ port_path = base + ['virtual-host', virtual_host, 'port']
if config.exists(port_path):
tmp = config.return_value(port_path)
listen_port.append(tmp)
@@ -99,8 +99,6 @@ if config.exists(base + ['virtual-host']):
for address in listen_address:
config.set(base + ['listen-address'], value=address, replace=False)
-
-
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
diff --git a/src/migration-scripts/pptp/4-to-5 b/src/migration-scripts/pptp/4-to-5
new file mode 100755
index 000000000..d4b3f9a14
--- /dev/null
+++ b/src/migration-scripts/pptp/4-to-5
@@ -0,0 +1,66 @@
+#!/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/>.
+
+# - Move 'require' from 'protocols' in 'authentication' node
+# - Migrate to new default values in radius timeout and acct-timeout
+
+import os
+
+from sys import argv
+from sys import exit
+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()
+
+config = ConfigTree(config_file)
+base = ['vpn', 'pptp', 'remote-access']
+
+if not config.exists(base):
+ exit(0)
+
+#migrate require to protocols
+require_path = base + ['authentication', 'require']
+if config.exists(require_path):
+ protocols = list(config.return_values(require_path))
+ for protocol in protocols:
+ config.set(base + ['authentication', 'protocols'], value=protocol,
+ replace=False)
+ config.delete(require_path)
+else:
+ config.set(base + ['authentication', 'protocols'], value='mschap-v2')
+
+radius_path = base + ['authentication', 'radius']
+if config.exists(radius_path):
+ if not config.exists(radius_path + ['timeout']):
+ config.set(radius_path + ['timeout'], value=3)
+ if not config.exists(radius_path + ['acct-timeout']):
+ config.set(radius_path + ['acct-timeout'], value=3)
+
+
+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)