diff options
| author | Christian Poessinger <christian@poessinger.com> | 2022-09-17 22:28:34 +0200 | 
|---|---|---|
| committer | Christian Poessinger <christian@poessinger.com> | 2022-09-21 16:34:25 +0200 | 
| commit | 05df2a5f021f0c7aab7c06db645d210858b6e98d (patch) | |
| tree | 96b08be53303b3c30f24829d5538bc41dc010bea | |
| parent | 87d54b805f6fe1ede06387241117d492334bee31 (diff) | |
| download | vyos-1x-05df2a5f021f0c7aab7c06db645d210858b6e98d.tar.gz vyos-1x-05df2a5f021f0c7aab7c06db645d210858b6e98d.zip | |
ipoe: T4678: T4703: rewrite to get_config_dict()
In addition to the rewrite to make use of get_config_dict() the CLI is
slightly adjusted as specified in T4703.
* Rename vlan-id and vlan-range to simply vlan
* Rename network-mode to simply mode
* Re-use existing common Jinja2 template for Accel-PPP which are shared
  with PPPoE and SSTP server.
* Retrieve default values via defaultValue XML node
| -rw-r--r-- | data/templates/accel-ppp/chap-secrets.ipoe.j2 | 25 | ||||
| -rw-r--r-- | data/templates/accel-ppp/config_ipv6_pool.j2 | 2 | ||||
| -rw-r--r-- | data/templates/accel-ppp/ipoe.config.j2 | 142 | ||||
| -rw-r--r-- | interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i | 26 | ||||
| -rw-r--r-- | interface-definitions/include/accel-ppp/vlan.xml.i | 2 | ||||
| -rw-r--r-- | interface-definitions/service-ipoe-server.xml.in | 78 | ||||
| -rw-r--r-- | python/vyos/configdict.py | 41 | ||||
| -rw-r--r-- | python/vyos/configverify.py | 6 | ||||
| -rw-r--r-- | smoketest/configs/ipoe-server | 118 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_service_ipoe-server.py | 91 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_service_pppoe-server.py | 1 | ||||
| -rwxr-xr-x | src/conf_mode/service_ipoe-server.py | 289 | ||||
| -rwxr-xr-x | src/conf_mode/service_pppoe-server.py | 5 | ||||
| -rwxr-xr-x | src/migration-scripts/ipoe-server/0-to-1 | 127 | 
14 files changed, 402 insertions, 551 deletions
| diff --git a/data/templates/accel-ppp/chap-secrets.ipoe.j2 b/data/templates/accel-ppp/chap-secrets.ipoe.j2 index a1430ec22..43083e22e 100644 --- a/data/templates/accel-ppp/chap-secrets.ipoe.j2 +++ b/data/templates/accel-ppp/chap-secrets.ipoe.j2 @@ -1,18 +1,13 @@  # username  server  password  acceptable local IP addresses   shaper -{% for interface in auth_interfaces %} -{%     for mac in interface.mac %} -{%         if mac.rate_upload and mac.rate_download %} -{%             if mac.vlan_id %} -{{ interface.name }}.{{ mac.vlan_id }} * {{ mac.address | lower }} * {{ mac.rate_download }}/{{ mac.rate_upload }} -{%             else %} -{{ interface.name }} * {{ mac.address | lower }}  * {{ mac.rate_download }}/{{ mac.rate_upload }} -{%             endif %} -{%         else %} -{%             if mac.vlan_id %} -{{ interface.name }}.{{ mac.vlan_id }} * {{ mac.address | lower }} * -{%             else %} -{{ interface.name }} * {{ mac.address | lower }}  * -{%             endif %} +{% if authentication.interface is vyos_defined %} +{%     for iface, iface_config in authentication.interface.items() %} +{%         if iface_config.mac is vyos_defined %} +{%             for mac, mac_config in iface_config.mac.items() %} +{%                 if mac_config.vlan is vyos_defined %} +{%                     set iface = iface ~ '.' ~ mac_config.vlan %} +{%                 endif %} +{{ "%-11s" | format(iface) }} * {{ mac | lower }} * {{ mac_config.rate_limit.download ~ '/' ~ mac_config.rate_limit.upload if mac_config.rate_limit.download is vyos_defined and mac_config.rate_limit.upload is vyos_defined }} +{%             endfor %}  {%         endif %}  {%     endfor %} -{% endfor %} +{% endif %} diff --git a/data/templates/accel-ppp/config_ipv6_pool.j2 b/data/templates/accel-ppp/config_ipv6_pool.j2 index 953469577..a1562a1eb 100644 --- a/data/templates/accel-ppp/config_ipv6_pool.j2 +++ b/data/templates/accel-ppp/config_ipv6_pool.j2 @@ -1,6 +1,7 @@  {% if client_ipv6_pool is vyos_defined %}  [ipv6-nd]  AdvAutonomousFlag=1 +verbose=1  {%     if client_ipv6_pool.prefix is vyos_defined %}  [ipv6-pool] @@ -13,6 +14,7 @@ delegate={{ prefix }},{{ options.delegation_prefix }}  {%             endfor %}  {%         endif %}  {%     endif %} +  {%     if client_ipv6_pool.delegate is vyos_defined %}  [ipv6-dhcp]  verbose=1 diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index 6df12db2c..99227ea33 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -4,18 +4,15 @@  log_syslog  ipoe  shaper +{# Common authentication backend definitions #} +{% include 'accel-ppp/config_modules_auth_mode.j2' %}  ipv6pool  ipv6_nd  ipv6_dhcp  ippool -{% if auth_mode == 'radius' %} -radius -{% elif auth_mode == 'local' %} -chap-secrets -{% endif %}  [core] -thread-count={{ thread_cnt }} +thread-count={{ thread_count }}  [log]  syslog=accel-ipoe,daemon @@ -24,28 +21,34 @@ level=5  [ipoe]  verbose=1 -{% for interface in interfaces %} -{%     set tmp = 'interface=' %} -{%     if interface.vlan_mon %} -{%         set tmp = tmp ~ 're:' ~ interface.name ~ '\.\d+' %} -{%     else %} -{%         set tmp = tmp ~ interface.name %} -{%     endif %} -{{ tmp }},shared={{ interface.shared }},mode={{ interface.mode }},ifcfg={{ interface.ifcfg }}{{ ',range=' ~ interface.range if interface.range is defined and interface.range is not none }},start={{ interface.sess_start }},ipv6=1 -{% endfor %} -{% if auth_mode == 'noauth' %} +{% if interface is vyos_defined %} +{%     for iface, iface_config in interface.items() %} +{%         set tmp = 'interface=' %} +{%         if iface_config.vlan is vyos_defined %} +{%             set tmp = tmp ~ 're:' ~ iface ~ '\.\d+' %} +{%         else %} +{%             set tmp = tmp ~ iface %} +{%         endif %} +{%         set shared = '' %} +{%         if iface_config.network is vyos_defined('shared') %} +{%             set shared = 'shared=1,' %} +{%         elif iface_config.network is vyos_defined('vlan') %} +{%             set shared = 'shared=0,' %} +{%         endif %} +{{ tmp }},{{ shared }}mode={{ iface_config.mode | upper }},ifcfg=1,range={{ iface_config.client_subnet }},start=dhcpv4,ipv6=1 +{%     endfor %} +{% endif %} +{% if authentication.mode is vyos_defined('noauth') %}  noauth=1 -{%     if client_named_ip_pool %} -{%         for pool in client_named_ip_pool %} -{%             if pool.subnet is defined  %} -ip-pool={{ pool.name }} -{%             endif %} -{%             if pool.gateway_address is defined %} -gw-ip-address={{ pool.gateway_address }}/{{ pool.subnet.split('/')[1] }} +{%     if client_ip_pool.name is vyos_defined %} +{%         for pool, pool_options in client_ip_pool.name.items() %} +{%             if pool_options.subnet is vyos_defined and pool_options.gateway_address is vyos_defined %} +ip-pool={{ pool }} +gw-ip-address={{ pool_options.gateway_address }}/{{ pool_options.subnet.split('/')[1] }}  {%             endif %}  {%         endfor %}  {%     endif %} -{% elif auth_mode == 'local' %} +{% elif authentication.mode is vyos_defined('local') %}  username=ifname  password=csid  {% endif %} @@ -57,92 +60,27 @@ vlan-mon={{ interface.name }},{{ interface.vlan_mon | join(',') }}  {%     endif %}  {% endfor %} -{% if dnsv4 %} -[dns] -{%     for dns in dnsv4 %} -dns{{ loop.index }}={{ dns }} -{%     endfor %} -{% endif %} - -{% if dnsv6 %} -[ipv6-dns] -{%     for dns in dnsv6 %} -{{ dns }} -{%     endfor %} -{% endif %} - -[ipv6-nd] -verbose=1 - -[ipv6-dhcp] -verbose=1 - -{% if client_named_ip_pool %} +{% if client_ip_pool.name is vyos_defined %}  [ip-pool] -{%     for pool in client_named_ip_pool %} -{%         if pool.subnet is defined  %} -{{ pool.subnet }},name={{ pool.name }} -{%         endif %} -{%         if pool.gateway_address is defined %} -gw-ip-address={{ pool.gateway_address }}/{{ pool.subnet.split('/')[1] }} +{%     for pool, pool_options in client_ip_pool.name.items() %} +{%         if pool_options.subnet is vyos_defined and pool_options.gateway_address is vyos_defined %} +{{ pool_options.subnet }},name={{ pool }} +gw-ip-address={{ pool_options.gateway_address }}/{{ pool_options.subnet.split('/')[1] }}  {%         endif %}  {%     endfor %}  {% endif %} -{% if client_ipv6_pool %} -[ipv6-pool] -{%     for p in client_ipv6_pool %} -{{ p.prefix }},{{ p.mask }} -{%     endfor %} -{%     for p in client_ipv6_delegate_prefix %} -delegate={{ p.prefix }},{{ p.mask }} -{%     endfor %} -{% endif %} +{# Common IPv6 pool definitions #} +{% include 'accel-ppp/config_ipv6_pool.j2' %} -{% if auth_mode == 'local' %} -[chap-secrets] -chap-secrets={{ chap_secrets_file }} -{% elif auth_mode == 'radius' %} -[radius] -verbose=1 -{%     for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} -{%     endfor %} - -{%     if radius_acct_inter_jitter %} -acct-interim-jitter={{ radius_acct_inter_jitter }} -{%     endif %} +{# Common DNS name-server definition #} +{% include 'accel-ppp/config_name_server.j2' %} -acct-timeout={{ radius_acct_tmo }} -timeout={{ radius_timeout }} -max-try={{ radius_max_try }} -{%     if radius_nas_id %} -nas-identifier={{ radius_nas_id }} -{%     endif %} -{%     if radius_nas_ip %} -nas-ip-address={{ radius_nas_ip }} -{%     endif %} -{%     if radius_source_address %} -bind={{ radius_source_address }} -{%     endif %} -{%     if radius_dynamic_author %} -dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }} -{%     endif %} +{# Common chap-secrets and RADIUS server/option definitions #} +{% include 'accel-ppp/config_chap_secrets_radius.j2' %} -{%     if radius_shaper_enable %} -[shaper] -verbose=1 -{%         if radius_shaper_attr %} -attr={{ radius_shaper_attr }} -{%         endif %} -{%         if radius_shaper_multiplier %} -rate-multiplier={{ radius_shaper_multiplier }} -{%         endif %} -{%         if radius_shaper_vendor %} -vendor={{ radius_shaper_vendor }} -{%         endif %} -{%     endif %} -{% endif %} +{# Common RADIUS shaper configuration #} +{% include 'accel-ppp/config_shaper_radius.j2' %}  [cli]  tcp=127.0.0.1:2002 diff --git a/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i b/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i index 01cf0e040..774741a5e 100644 --- a/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i +++ b/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i @@ -16,19 +16,19 @@          </constraint>        </properties>        <children> -          <leafNode name="mask"> -            <properties> -              <help>Prefix length used for individual client</help> -              <valueHelp> -                <format>u32:48-128</format> -                <description>Client prefix length</description> -              </valueHelp> -              <constraint> -                <validator name="numeric" argument="--range 48-128"/> -              </constraint> -            </properties> -            <defaultValue>64</defaultValue> -          </leafNode> +        <leafNode name="mask"> +          <properties> +            <help>Prefix length used for individual client</help> +            <valueHelp> +              <format>u32:48-128</format> +              <description>Client prefix length</description> +            </valueHelp> +            <constraint> +              <validator name="numeric" argument="--range 48-128"/> +            </constraint> +          </properties> +          <defaultValue>64</defaultValue> +        </leafNode>        </children>      </tagNode>      <tagNode name="delegate"> diff --git a/interface-definitions/include/accel-ppp/vlan.xml.i b/interface-definitions/include/accel-ppp/vlan.xml.i index 7df711d4b..9a00df214 100644 --- a/interface-definitions/include/accel-ppp/vlan.xml.i +++ b/interface-definitions/include/accel-ppp/vlan.xml.i @@ -4,7 +4,7 @@      <help>VLAN monitor for automatic creation of VLAN interfaces</help>      <valueHelp>        <format>u32:1-4094</format> -      <description>VLAN for automatic creation </description> +      <description>VLAN for automatic creation</description>      </valueHelp>      <valueHelp>        <format>start-end</format> diff --git a/interface-definitions/service-ipoe-server.xml.in b/interface-definitions/service-ipoe-server.xml.in index cd3aa3638..ef8569437 100644 --- a/interface-definitions/service-ipoe-server.xml.in +++ b/interface-definitions/service-ipoe-server.xml.in @@ -10,30 +10,31 @@          <children>            <tagNode name="interface">              <properties> -              <help>Network interface to server IPoE</help> +              <help>Interface to listen dhcp or unclassified packets</help>                <completionHelp>                  <script>${vyos_completion_dir}/list_interfaces.py</script>                </completionHelp>              </properties>              <children> -              <leafNode name="network-mode"> +              <leafNode name="mode">                  <properties> -                  <help>Network Layer IPoE serves on</help> +                  <help>Client connectivity mode</help>                    <completionHelp> -                    <list>L2 L3</list> +                    <list>l2 l3</list>                    </completionHelp> -                  <constraint> -                    <regex>(L2|L3)</regex> -                  </constraint>                    <valueHelp> -                    <format>L2</format> -                    <description>client share the same subnet</description> +                    <format>l2</format> +                    <description>Client located on same interface as server</description>                    </valueHelp>                    <valueHelp> -                    <format>L3</format> -                    <description>clients are behind this router</description> +                    <format>l3</format> +                    <description>Client located behind a router</description>                    </valueHelp> +                  <constraint> +                    <regex>(l2|l3)</regex> +                  </constraint>                  </properties> +                <defaultValue>l2</defaultValue>                </leafNode>                <leafNode name="network">                  <properties> @@ -53,6 +54,7 @@                      <description>One VLAN per client</description>                    </valueHelp>                  </properties> +                <defaultValue>shared</defaultValue>                </leafNode>                <leafNode name="client-subnet">                  <properties> @@ -85,30 +87,19 @@                    </leafNode>                    <leafNode name="giaddr">                      <properties> -                      <help>address of the relay agent (Relay Agent IP Address)</help> +                      <help>Relay Agent IPv4 Address</help> +                      <valueHelp> +                        <format>ipv4</format> +                        <description>Gateway IP address</description> +                      </valueHelp> +                      <constraint> +                        <validator name="ipv4-address"/> +                      </constraint>                      </properties>                    </leafNode>                  </children>                </node> -              <leafNode name="vlan-id"> -                <properties> -                  <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help> -                  <constraint> -                    <validator name="numeric" argument="--range 1-4096"/> -                  </constraint> -                  <constraintErrorMessage>VLAN ID needs to be between 1 and 4096</constraintErrorMessage> -                  <multi/> -                </properties> -              </leafNode> -              <leafNode name="vlan-range"> -                <properties> -                  <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help> -                  <constraint> -                    <regex>(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})-(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})</regex> -                  </constraint> -                  <multi/> -                </properties> -              </leafNode> +              #include <include/accel-ppp/vlan.xml.i>              </children>            </tagNode>            #include <include/name-server-ipv4-ipv6.xml.i> @@ -120,6 +111,13 @@                <tagNode name="name">                  <properties>                    <help>Pool name</help> +                  <valueHelp> +                    <format>txt</format> +                    <description>Name of IP pool</description> +                  </valueHelp> +                  <constraint> +                    <regex>[-_a-zA-Z0-9.]+</regex> +                  </constraint>                  </properties>                  <children>                    #include <include/accel-ppp/gateway-address.xml.i> @@ -159,15 +157,15 @@                </leafNode>                <tagNode name="interface">                  <properties> -                  <help>Network interface the client mac will appear on</help> +                  <help>Network interface for client MAC addresses</help>                    <completionHelp>                      <script>${vyos_completion_dir}/list_interfaces.py</script>                    </completionHelp>                  </properties>                  <children> -                  <tagNode name="mac-address"> +                  <tagNode name="mac">                      <properties> -                      <help>Client mac address allowed to receive an IP address</help> +                      <help>Media Access Control (MAC) address</help>                        <valueHelp>                          <format>macaddr</format>                          <description>Hardware (MAC) address</description> @@ -200,13 +198,17 @@                            </leafNode>                          </children>                        </node> -                      <leafNode name="vlan-id"> +                      <leafNode name="vlan">                          <properties> -                          <help>VLAN-ID of the client network</help> +                          <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-4096"/> +                            <validator name="numeric" argument="--range 1-4094"/>                            </constraint> -                          <constraintErrorMessage>VLAN ID needs to be between 1 and 4096</constraintErrorMessage> +                          <constraintErrorMessage>VLAN IDs need to be in range 1-4094</constraintErrorMessage>                          </properties>                        </leafNode>                      </children> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 912bc94f2..53decfbf5 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -643,7 +643,9 @@ def get_accel_dict(config, base, chap_secrets):      from vyos.util import get_half_cpus      from vyos.template import is_ipv4 -    dict = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) +    dict = config.get_config_dict(base, key_mangling=('-', '_'), +                                  get_first_key=True, +                                  no_tag_node_value_mangle=True)      # We have gathered the dict representation of the CLI, but there are default      # options which we need to update into the dictionary retrived. @@ -663,6 +665,18 @@ def get_accel_dict(config, base, chap_secrets):      # added to individual local users instead - so we can simply delete them      if dict_search('client_ipv6_pool.prefix.mask', default_values):          del default_values['client_ipv6_pool']['prefix']['mask'] +        # delete empty dicts +        if len (default_values['client_ipv6_pool']['prefix']) == 0: +            del default_values['client_ipv6_pool']['prefix'] +        if len (default_values['client_ipv6_pool']) == 0: +            del default_values['client_ipv6_pool'] + +    # T2665: IPoE only - it has an interface tag node +    # added to individual local users instead - so we can simply delete them +    if dict_search('authentication.interface', default_values): +        del default_values['authentication']['interface'] +    if dict_search('interface', default_values): +        del default_values['interface']      dict = dict_merge(default_values, dict) @@ -684,11 +698,9 @@ def get_accel_dict(config, base, chap_secrets):          dict.update({'name_server_ipv4' : ns_v4, 'name_server_ipv6' : ns_v6})          del dict['name_server'] -    # Add individual RADIUS server default values +    # T2665: Add individual RADIUS server default values      if dict_search('authentication.radius.server', dict): -        # T2665          default_values = defaults(base + ['authentication', 'radius', 'server']) -          for server in dict_search('authentication.radius.server', dict):              dict['authentication']['radius']['server'][server] = dict_merge(                  default_values, dict['authentication']['radius']['server'][server]) @@ -698,22 +710,31 @@ def get_accel_dict(config, base, chap_secrets):              if 'disable_accounting' in dict['authentication']['radius']['server'][server]:                  dict['authentication']['radius']['server'][server]['acct_port'] = '0' -    # Add individual local-user default values +    # T2665: Add individual local-user default values      if dict_search('authentication.local_users.username', dict): -        # T2665          default_values = defaults(base + ['authentication', 'local-users', 'username']) -          for username in dict_search('authentication.local_users.username', dict):              dict['authentication']['local_users']['username'][username] = dict_merge(                  default_values, dict['authentication']['local_users']['username'][username]) -    # Add individual IPv6 client-pool default mask if required +    # T2665: Add individual IPv6 client-pool default mask if required      if dict_search('client_ipv6_pool.prefix', dict): -        # T2665          default_values = defaults(base + ['client-ipv6-pool', 'prefix']) -          for prefix in dict_search('client_ipv6_pool.prefix', dict):              dict['client_ipv6_pool']['prefix'][prefix] = dict_merge(                  default_values, dict['client_ipv6_pool']['prefix'][prefix]) +    # T2665: IPoE only - add individual local-user default values +    if dict_search('authentication.interface', dict): +        default_values = defaults(base + ['authentication', 'interface']) +        for interface in dict_search('authentication.interface', dict): +            dict['authentication']['interface'][interface] = dict_merge( +                default_values, dict['authentication']['interface'][interface]) + +    if dict_search('interface', dict): +        default_values = defaults(base + ['interface']) +        for interface in dict_search('interface', dict): +            dict['interface'][interface] = dict_merge(default_values, +                                                      dict['interface'][interface]) +      return dict diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 447ec795c..afa0c5b33 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -381,14 +381,14 @@ def verify_vlan_config(config):              verify_mtu_parent(c_vlan, config)              verify_mtu_parent(c_vlan, s_vlan) -def verify_accel_ppp_base_service(config): +def verify_accel_ppp_base_service(config, local_users=True):      """      Common helper function which must be used by all Accel-PPP services based      on get_config_dict()      """      # vertify auth settings -    if dict_search('authentication.mode', config) == 'local': -        if not dict_search('authentication.local_users', config): +    if local_users and dict_search('authentication.mode', config) == 'local': +        if dict_search(f'authentication.local_users', config) == None:              raise ConfigError('Authentication mode local requires local users to be configured!')          for user in dict_search('authentication.local_users.username', config): diff --git a/smoketest/configs/ipoe-server b/smoketest/configs/ipoe-server new file mode 100644 index 000000000..7699dbcb9 --- /dev/null +++ b/smoketest/configs/ipoe-server @@ -0,0 +1,118 @@ +interfaces { +    ethernet eth0 { +        address dhcp +    } +    ethernet eth1 { +        address 192.168.0.1/24 +    } +    ethernet eth2 { +    } +    loopback lo { +    } +} +nat { +    source { +        rule 100 { +            outbound-interface eth0 +            source { +                address 192.168.0.0/24 +            } +            translation { +                address masquerade +            } +        } +    } +} +service { +    ipoe-server { +        authentication { +            interface eth1 { +                mac-address 08:00:27:2f:d8:06 { +                    rate-limit { +                        download 1000 +                        upload 500 +                    } +                } +            } +            interface eth2 { +                mac-address 08:00:27:2f:d8:06 { +                } +            } +            mode local +        } +        client-ip-pool { +            name POOL1 { +                gateway-address 192.0.2.1 +                subnet 192.0.2.0/24 +            } +        } +        client-ipv6-pool { +            delegate 2001:db8:1::/48 { +                delegation-prefix 56 +            } +            prefix 2001:db8::/48 { +                mask 64 +            } +        } +        interface eth1 { +            client-subnet 192.168.0.0/24 +            network vlan +            network-mode L3 +            vlan-id 100 +            vlan-id 200 +            vlan-range 1000-2000 +            vlan-range 2500-2700 +        } +        interface eth2 { +            client-subnet 192.168.1.0/24 +        } +        name-server 10.10.1.1 +        name-server 10.10.1.2 +        name-server 2001:db8:aaa:: +        name-server 2001:db8:bbb:: +    } +    ssh { +    } +} +system { +    config-management { +        commit-revisions 100 +    } +    console { +        device ttyS0 { +            speed 115200 +        } +    } +    host-name vyos +    login { +        user vyos { +            authentication { +                encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0 +                plaintext-password "" +            } +        } +    } +    ntp { +        server 0.pool.ntp.org { +        } +        server 1.pool.ntp.org { +        } +        server 2.pool.ntp.org { +        } +    } +    syslog { +        global { +            facility all { +                level info +            } +            facility protocols { +                level debug +            } +        } +    } +} + + +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@1:conntrack-sync@1:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@13:ipoe-server@1:ipsec@5:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@6:salt@1:snmp@2:ssh@2:sstp@3:system@19:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webgui@1:webproxy@2:zone-policy@1" +// Release version: 1.3.1 diff --git a/smoketest/scripts/cli/test_service_ipoe-server.py b/smoketest/scripts/cli/test_service_ipoe-server.py new file mode 100755 index 000000000..bdab35834 --- /dev/null +++ b/smoketest/scripts/cli/test_service_ipoe-server.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 re +import unittest + +from base_accel_ppp_test import BasicAccelPPPTest +from vyos.configsession import ConfigSessionError +from vyos.util import cmd + +from configparser import ConfigParser + +ac_name = 'ACN' +interface = 'eth0' + +class TestServiceIPoEServer(BasicAccelPPPTest.TestCase): +    @classmethod +    def setUpClass(cls): +        cls._base_path = ['service', 'ipoe-server'] +        cls._config_file = '/run/accel-pppd/ipoe.conf' +        cls._chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' + +        # call base-classes classmethod +        super(TestServiceIPoEServer, cls).setUpClass() + +    def verify(self, conf): +        super().verify(conf) + +        # Validate configuration values +        accel_modules = list(conf['modules'].keys()) +        self.assertIn('log_syslog', accel_modules) +        self.assertIn('ipoe', accel_modules) +        self.assertIn('shaper', accel_modules) +        self.assertIn('ipv6pool', accel_modules) +        self.assertIn('ipv6_nd', accel_modules) +        self.assertIn('ipv6_dhcp', accel_modules) +        self.assertIn('ippool', accel_modules) + +    def basic_config(self): +        self.set(['interface', interface, 'client-subnet', '192.168.0.0/24']) + +    def test_accel_local_authentication(self): +        mac_address = '08:00:27:2f:d8:06' +        self.set(['authentication', 'interface', interface, 'mac', mac_address]) +        self.set(['authentication', 'mode', 'local']) + +        # No IPoE interface configured +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() + +        # Test configuration of local authentication for PPPoE server +        self.basic_config() + +        # commit changes +        self.cli_commit() + +        # Validate configuration values +        conf = ConfigParser(allow_no_value=True, delimiters='=') +        conf.read(self._config_file) + +        # check proper path to chap-secrets file +        self.assertEqual(conf['chap-secrets']['chap-secrets'], self._chap_secrets) + +        accel_modules = list(conf['modules'].keys()) +        self.assertIn('chap-secrets', accel_modules) + +        # basic verification +        self.verify(conf) + +        # check local users +        tmp = cmd(f'sudo cat {self._chap_secrets}') +        regex = f'{interface}\s+\*\s+{mac_address}\s+\*' +        tmp = re.findall(regex, tmp) +        self.assertTrue(tmp) + +if __name__ == '__main__': +    unittest.main(verbosity=2) + diff --git a/smoketest/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py index 17687a26b..7546c2e3d 100755 --- a/smoketest/scripts/cli/test_service_pppoe-server.py +++ b/smoketest/scripts/cli/test_service_pppoe-server.py @@ -19,7 +19,6 @@ import unittest  from base_accel_ppp_test import BasicAccelPPPTest  from configparser import ConfigParser -from vyos.configsession import ConfigSessionError  from vyos.util import read_file  from vyos.template import range_to_regex diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 61f484129..e9afd6a55 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -15,266 +15,34 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os -import re -from copy import deepcopy -from stat import S_IRUSR, S_IWUSR, S_IRGRP  from sys import exit  from vyos.config import Config +from vyos.configdict import get_accel_dict +from vyos.configverify import verify_accel_ppp_base_service +from vyos.configverify import verify_interface_exists  from vyos.template import render -from vyos.template import is_ipv4 -from vyos.template import is_ipv6 -from vyos.util import call, get_half_cpus +from vyos.util import call +from vyos.util import dict_search  from vyos import ConfigError -  from vyos import airbag  airbag.enable()  ipoe_conf = '/run/accel-pppd/ipoe.conf'  ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' -default_config_data = { -    'auth_mode': 'local', -    'auth_interfaces': [], -    'chap_secrets_file': ipoe_chap_secrets, # used in Jinja2 template -    'interfaces': [], -    'dnsv4': [], -    'dnsv6': [], -    'client_named_ip_pool': [], -    'client_ipv6_pool': [], -    'client_ipv6_delegate_prefix': [], -    'radius_server': [], -    'radius_acct_inter_jitter': '', -    'radius_acct_tmo': '3', -    'radius_max_try': '3', -    'radius_timeout': '3', -    'radius_nas_id': '', -    'radius_nas_ip': '', -    'radius_source_address': '', -    'radius_shaper_attr': '', -    'radius_shaper_enable': False, -    'radius_shaper_multiplier': '', -    'radius_shaper_vendor': '', -    'radius_dynamic_author': '', -    'thread_cnt': get_half_cpus() -} -  def get_config(config=None):      if config:          conf = config      else:          conf = Config() -    base_path = ['service', 'ipoe-server'] -    if not conf.exists(base_path): +    base = ['service', 'ipoe-server'] +    if not conf.exists(base):          return None -    conf.set_level(base_path) -    ipoe = deepcopy(default_config_data) - -    for interface in conf.list_nodes(['interface']): -        tmp  = { -            'mode': 'L2', -            'name': interface, -            'shared': '1', -            # may need a config option, can be dhcpv4 or up for unclassified pkts -            'sess_start': 'dhcpv4', -            'range': None, -            'ifcfg': '1', -            'vlan_mon': [] -        } - -        conf.set_level(base_path + ['interface', interface]) - -        if conf.exists(['network-mode']): -            tmp['mode'] = conf.return_value(['network-mode']) - -        if conf.exists(['network']): -            mode = conf.return_value(['network']) -            if mode == 'vlan': -                tmp['shared'] = '0' - -                if conf.exists(['vlan-id']): -                    tmp['vlan_mon'] += conf.return_values(['vlan-id']) - -                if conf.exists(['vlan-range']): -                    tmp['vlan_mon'] += conf.return_values(['vlan-range']) - -        if conf.exists(['client-subnet']): -            tmp['range'] = conf.return_value(['client-subnet']) - -        ipoe['interfaces'].append(tmp) - -    conf.set_level(base_path) - -    if conf.exists(['name-server']): -        for name_server in conf.return_values(['name-server']): -            if is_ipv4(name_server): -                ipoe['dnsv4'].append(name_server) -            else: -                ipoe['dnsv6'].append(name_server) - -    if conf.exists(['authentication', 'mode']): -        ipoe['auth_mode'] = conf.return_value(['authentication', 'mode']) - -    if conf.exists(['authentication', 'interface']): -        for interface in conf.list_nodes(['authentication', 'interface']): -            tmp = { -                'name': interface, -                'mac': [] -            } -            for mac in conf.list_nodes(['authentication', 'interface', interface, 'mac-address']): -                client = { -                    'address': mac, -                    'rate_download': '', -                    'rate_upload': '', -                    'vlan_id': '' -                } -                conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', mac]) - -                if conf.exists(['rate-limit', 'download']): -                    client['rate_download'] = conf.return_value(['rate-limit', 'download']) - -                if conf.exists(['rate-limit', 'upload']): -                    client['rate_upload'] = conf.return_value(['rate-limit', 'upload']) - -                if conf.exists(['vlan-id']): -                    client['vlan'] = conf.return_value(['vlan-id']) - -                tmp['mac'].append(client) - -            ipoe['auth_interfaces'].append(tmp) - -    conf.set_level(base_path) - -    # -    # authentication mode radius servers and settings -    if conf.exists(['authentication', 'mode', 'radius']): -        for server in conf.list_nodes(['authentication', 'radius', 'server']): -            radius = { -                'server' : server, -                'key' : '', -                'fail_time' : 0, -                'port' : '1812', -                'acct_port' : '1813' -            } - -            conf.set_level(base_path + ['authentication', 'radius', 'server', server]) - -            if conf.exists(['fail-time']): -                radius['fail_time'] = conf.return_value(['fail-time']) - -            if conf.exists(['port']): -                radius['port'] = conf.return_value(['port']) - -            if conf.exists(['acct-port']): -                radius['acct_port'] = conf.return_value(['acct-port']) - -            if conf.exists(['key']): -                radius['key'] = conf.return_value(['key']) - -            if not conf.exists(['disable']): -                ipoe['radius_server'].append(radius) - -    # -    # advanced radius-setting -    conf.set_level(base_path + ['authentication', 'radius']) - -    if conf.exists(['acct-interim-jitter']): -        ipoe['radius_acct_inter_jitter'] = conf.return_value(['acct-interim-jitter']) - -    if conf.exists(['acct-timeout']): -        ipoe['radius_acct_tmo'] = conf.return_value(['acct-timeout']) - -    if conf.exists(['max-try']): -        ipoe['radius_max_try'] = conf.return_value(['max-try']) - -    if conf.exists(['timeout']): -        ipoe['radius_timeout'] = conf.return_value(['timeout']) - -    if conf.exists(['nas-identifier']): -        ipoe['radius_nas_id'] = conf.return_value(['nas-identifier']) - -    if conf.exists(['nas-ip-address']): -        ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) - -    if conf.exists(['rate-limit', 'attribute']): -        ipoe['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute']) - -    if conf.exists(['rate-limit', 'enable']): -        ipoe['radius_shaper_enable'] = True - -    if conf.exists(['rate-limit', 'multiplier']): -        ipoe['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier']) - -    if conf.exists(['rate-limit', 'vendor']): -        ipoe['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor']) - -    if conf.exists(['source-address']): -        ipoe['radius_source_address'] = conf.return_value(['source-address']) - -    # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) -    if conf.exists(['dynamic-author']): -        dae = { -            'port' : '', -            'server' : '', -            'key' : '' -        } - -        if conf.exists(['dynamic-author', 'server']): -            dae['server'] = conf.return_value(['dynamic-author', 'server']) - -        if conf.exists(['dynamic-author', 'port']): -            dae['port'] = conf.return_value(['dynamic-author', 'port']) - -        if conf.exists(['dynamic-author', 'key']): -            dae['key'] = conf.return_value(['dynamic-author', 'key']) - -        ipoe['radius_dynamic_author'] = dae - - -    conf.set_level(base_path) -    # Named client-ip-pool -    if conf.exists(['client-ip-pool', 'name']): -        for name in conf.list_nodes(['client-ip-pool', 'name']): -            tmp = { -                'name': name, -                'gateway_address': '', -                'subnet': '' -            } - -            if conf.exists(['client-ip-pool', 'name', name, 'gateway-address']): -                tmp['gateway_address'] += conf.return_value(['client-ip-pool', 'name', name, 'gateway-address']) -            if conf.exists(['client-ip-pool', 'name', name, 'subnet']): -                tmp['subnet'] += conf.return_value(['client-ip-pool', 'name', name, 'subnet']) - -            ipoe['client_named_ip_pool'].append(tmp) - -    if conf.exists(['client-ipv6-pool', 'prefix']): -        for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): -            tmp = { -                'prefix': prefix, -                'mask': '64' -            } - -            if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): -                tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) - -            ipoe['client_ipv6_pool'].append(tmp) - - -    if conf.exists(['client-ipv6-pool', 'delegate']): -        for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): -            tmp = { -                'prefix': prefix, -                'mask': '' -            } - -            if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): -                tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) - -            ipoe['client_ipv6_delegate_prefix'].append(tmp) - +    # retrieve common dictionary keys +    ipoe = get_accel_dict(conf, base, ipoe_chap_secrets)      return ipoe @@ -282,26 +50,17 @@ def verify(ipoe):      if not ipoe:          return None -    if not ipoe['interfaces']: +    if 'interface' not in ipoe:          raise ConfigError('No IPoE interface configured') -    if len(ipoe['dnsv4']) > 2: -        raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') - -    if len(ipoe['dnsv6']) > 3: -        raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') - -    if ipoe['auth_mode'] == 'radius': -        if len(ipoe['radius_server']) == 0: -            raise ConfigError('RADIUS authentication requires at least one server') +    for interface in ipoe['interface']: +        verify_interface_exists(interface) -        for radius in ipoe['radius_server']: -            if not radius['key']: -                server = radius['server'] -                raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') +    #verify_accel_ppp_base_service(ipoe, local_users=False) -    if ipoe['client_ipv6_delegate_prefix'] and not ipoe['client_ipv6_pool']: -        raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!') +    if 'client_ipv6_pool' in ipoe: +        if 'delegate' in ipoe['client_ipv6_pool'] and 'prefix' not in ipoe['client_ipv6_pool']: +            raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!')      return None @@ -312,27 +71,23 @@ def generate(ipoe):      render(ipoe_conf, 'accel-ppp/ipoe.config.j2', ipoe) -    if ipoe['auth_mode'] == 'local': -        render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', ipoe) -        os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) - -    else: -        if os.path.exists(ipoe_chap_secrets): -             os.unlink(ipoe_chap_secrets) - +    if dict_search('authentication.mode', ipoe) == 'local': +        render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', +               ipoe, permission=0o640)      return None  def apply(ipoe): +    systemd_service = 'accel-ppp@ipoe.service'      if ipoe == None: -        call('systemctl stop accel-ppp@ipoe.service') +        call(f'systemctl stop {systemd_service}')          for file in [ipoe_conf, ipoe_chap_secrets]:              if os.path.exists(file):                  os.unlink(file)          return None -    call('systemctl restart accel-ppp@ipoe.service') +    call(f'systemctl reload-or-restart {systemd_service}')  if __name__ == '__main__':      try: diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index dfe73094f..ba0249efd 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -27,7 +27,6 @@ from vyos.util import call  from vyos.util import dict_search  from vyos import ConfigError  from vyos import airbag -  airbag.enable()  pppoe_conf = r'/run/accel-pppd/pppoe.conf' @@ -84,10 +83,6 @@ def generate(pppoe):      if dict_search('authentication.mode', pppoe) == 'local':          render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.config_dict.j2',                 pppoe, permission=0o640) -    else: -        if os.path.exists(pppoe_chap_secrets): -            os.unlink(pppoe_chap_secrets) -      return None diff --git a/src/migration-scripts/ipoe-server/0-to-1 b/src/migration-scripts/ipoe-server/0-to-1 index f328ebced..da1f3f761 100755 --- a/src/migration-scripts/ipoe-server/0-to-1 +++ b/src/migration-scripts/ipoe-server/0-to-1 @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2022 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 @@ -14,8 +14,11 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -# - remove primary/secondary identifier from nameserver -# - Unifi RADIUS configuration by placing it all under "authentication radius" node +# - T4703: merge vlan-id and vlan-range to vlan CLI node + +# L2|L3 -> l2|l3 +# mac-address -> mac +# network-mode -> mode  import os  import sys @@ -37,97 +40,29 @@ base = ['service', 'ipoe-server']  if not config.exists(base):      # Nothing to do      exit(0) -else: - -    # Migrate IPv4 DNS servers -    dns_base = base + ['dns-server'] -    if config.exists(dns_base): -        for server in ['server-1', 'server-2']: -          if config.exists(dns_base + [server]): -            dns = config.return_value(dns_base + [server]) -            config.set(base + ['name-server'], value=dns, replace=False) - -        config.delete(dns_base) - -    # Migrate IPv6 DNS servers -    dns_base = base + ['dnsv6-server'] -    if config.exists(dns_base): -        for server in ['server-1', 'server-2', 'server-3']: -          if config.exists(dns_base + [server]): -            dns = config.return_value(dns_base + [server]) -            config.set(base + ['name-server'], value=dns, replace=False) - -        config.delete(dns_base) - -    # Migrate radius-settings node to RADIUS and use this as base for the -    # later migration of the RADIUS servers - this will save a lot of code -    radius_settings = base + ['authentication', 'radius-settings'] -    if config.exists(radius_settings): -        config.rename(radius_settings, 'radius') - -    # Migrate RADIUS dynamic author / change of authorisation server -    dae_old = base + ['authentication', 'radius', 'dae-server'] -    if config.exists(dae_old): -        config.rename(dae_old, 'dynamic-author') -        dae_new = base + ['authentication', 'radius', 'dynamic-author'] - -        if config.exists(dae_new + ['ip-address']): -            config.rename(dae_new + ['ip-address'], 'server') - -        if config.exists(dae_new + ['secret']): -            config.rename(dae_new + ['secret'], 'key') -    # Migrate RADIUS server -    radius_server = base + ['authentication', 'radius-server'] -    if config.exists(radius_server): -        new_base = base + ['authentication', 'radius', 'server'] -        config.set(new_base) -        config.set_tag(new_base) -        for server in config.list_nodes(radius_server): -            old_base = radius_server + [server] -            config.copy(old_base, new_base + [server]) - -            # migrate key -            if config.exists(new_base + [server, 'secret']): -                config.rename(new_base + [server, 'secret'], 'key') - -            # remove old req-limit node -            if config.exists(new_base + [server, 'req-limit']): -                config.delete(new_base + [server, 'req-limit']) - -        config.delete(radius_server) - -    # Migrate IPv6 prefixes -    ipv6_base = base + ['client-ipv6-pool'] -    if config.exists(ipv6_base + ['prefix']): -        prefix_old = config.return_values(ipv6_base + ['prefix']) -        # delete old prefix CLI nodes -        config.delete(ipv6_base + ['prefix']) -        # create ned prefix tag node -        config.set(ipv6_base + ['prefix']) -        config.set_tag(ipv6_base + ['prefix']) - -        for p in prefix_old: -            prefix = p.split(',')[0] -            mask = p.split(',')[1] -            config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) - -    if config.exists(ipv6_base + ['delegate-prefix']): -        prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) -        # delete old delegate prefix CLI nodes -        config.delete(ipv6_base + ['delegate-prefix']) -        # create ned delegation tag node -        config.set(ipv6_base + ['delegate']) -        config.set_tag(ipv6_base + ['delegate']) - -        for p in prefix_old: -            prefix = p.split(',')[0] -            mask = p.split(',')[1] -            config.set(ipv6_base + ['delegate', prefix, 'delegation-prefix'], value=mask) - -    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) +if config.exists(base + ['authentication', 'interface']): +    for interface in config.list_nodes(base + ['authentication', 'interface']): +        config.rename(base + ['authentication', 'interface', interface, 'mac-address'], 'mac') + +for interface in config.list_nodes(base + ['interface']): +    base_path = base + ['interface', interface] +    for vlan in ['vlan-id', 'vlan-range']: +        if config.exists(base_path + [vlan]): +            print(interface, vlan) +            for tmp in config.return_values(base_path + [vlan]): +                config.set(base_path + ['vlan'], value=tmp, replace=False) +            config.delete(base_path + [vlan]) + +    if config.exists(base_path + ['network-mode']): +        tmp = config.return_value(base_path + ['network-mode']) +        config.delete(base_path + ['network-mode']) +        # Change L2|L3 to lower case l2|l3 +        config.set(base_path + ['mode'], value=tmp.lower()) + +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) | 
