diff options
| -rw-r--r-- | data/templates/accel-ppp/pptp.config.j2 | 10 | ||||
| -rw-r--r-- | interface-definitions/include/version/pptp-version.xml.i | 2 | ||||
| -rw-r--r-- | interface-definitions/service_ipoe-server.xml.in | 150 | ||||
| -rw-r--r-- | interface-definitions/vpn_l2tp.xml.in | 34 | ||||
| -rw-r--r-- | interface-definitions/vpn_pptp.xml.in | 96 | ||||
| -rw-r--r-- | op-mode-definitions/file.xml.in | 86 | ||||
| -rw-r--r-- | python/vyos/accel_ppp_util.py | 4 | ||||
| -rw-r--r-- | python/vyos/remote.py | 6 | ||||
| -rw-r--r-- | schema/op-mode-definition.rnc | 8 | ||||
| -rw-r--r-- | schema/op-mode-definition.rng | 5 | ||||
| -rwxr-xr-x | scripts/build-command-op-templates | 7 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_vpn_l2tp.py | 2 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_vpn_pptp.py | 160 | ||||
| -rwxr-xr-x | src/completion/list_images.py | 44 | ||||
| -rwxr-xr-x | src/conf_mode/qos.py | 8 | ||||
| -rwxr-xr-x | src/conf_mode/service_ipoe-server.py | 12 | ||||
| -rwxr-xr-x | src/conf_mode/service_pppoe-server.py | 8 | ||||
| -rwxr-xr-x | src/conf_mode/vpn_l2tp.py | 7 | ||||
| -rwxr-xr-x | src/migration-scripts/pptp/4-to-5 | 66 | ||||
| -rwxr-xr-x | src/op_mode/file.py | 383 | 
20 files changed, 724 insertions, 374 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/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/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_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/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)  | 
