diff options
89 files changed, 632 insertions, 243 deletions
diff --git a/data/templates/conserver/dropbear@.service.tmpl b/data/templates/conserver/dropbear@.service.tmpl index 4bb73f751..e355dab43 100644 --- a/data/templates/conserver/dropbear@.service.tmpl +++ b/data/templates/conserver/dropbear@.service.tmpl @@ -1,4 +1,4 @@  [Service]  ExecStart= -ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I +ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -b /etc/issue.net -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I  PIDFile=/run/conserver/dropbear.%I.pid diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl index 11e961166..b3e74c22b 100644 --- a/data/templates/dhcp-client/ipv4.tmpl +++ b/data/templates/dhcp-client/ipv4.tmpl @@ -2,7 +2,8 @@  option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;  timeout 60; -retry 300; +retry 60; +initial-interval 2;  interface "{{ ifname }}" {      send host-name "{{ dhcp_options.host_name }}"; diff --git a/data/templates/frr/vrf-vni.frr.tmpl b/data/templates/frr/vrf-vni.frr.tmpl index 51d4ede1b..299c9719e 100644 --- a/data/templates/frr/vrf-vni.frr.tmpl +++ b/data/templates/frr/vrf-vni.frr.tmpl @@ -1,7 +1,9 @@ -{% if vrf is defined and vrf is not none %} +{% if name is defined and name is not none %} +{%   for vrf, vrf_config in name.items() %}  vrf {{ vrf }} -{%   if vni is defined and vni is not none %} - vni {{ vni }} -{%   endif %} +{%     if vrf_config.vni is defined and vrf_config.vni is not none %} + vni {{ vrf_config.vni }} +{%     endif %}   exit-vrf +{%   endfor %}  {% endif %} diff --git a/data/templates/ipsec/swanctl/profile.tmpl b/data/templates/ipsec/swanctl/profile.tmpl index 948dd8f87..a5cae31c0 100644 --- a/data/templates/ipsec/swanctl/profile.tmpl +++ b/data/templates/ipsec/swanctl/profile.tmpl @@ -7,7 +7,7 @@      dmvpn-{{ name }}-{{ interface }} {          proposals = {{ ike_group[profile_conf.ike_group] | get_esp_ike_cipher | join(',') }}          version = {{ ike.key_exchange[4:] if ike is defined and ike.key_exchange is defined else "0" }} -        life_time = {{ ike.lifetime }}s +        rekey_time = {{ ike.lifetime }}s          keyingtries = 0  {%       if profile_conf.authentication is defined and profile_conf.authentication.mode is defined and profile_conf.authentication.mode == 'pre-shared-secret' %}          local { diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index 10426260a..29d74390f 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,3 +1,4 @@ +etc/cron.d  etc/cron.hourly  etc/dhcp  etc/ipsec.d diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in index 1e9c36ee5..30c7110b8 100644 --- a/interface-definitions/containers.xml.in +++ b/interface-definitions/containers.xml.in @@ -25,13 +25,17 @@              <properties>                <help>Container capabilities/permissions</help>                <completionHelp> -                <list>net-admin net-raw setpcap sys-admin sys-time</list> +                <list>net-admin net-bind-service net-raw setpcap sys-admin sys-time</list>                </completionHelp>                <valueHelp>                  <format>net-admin</format>                  <description>Network operations (interface, firewall, routing tables)</description>                </valueHelp>                <valueHelp> +                <format>net-bind-service</format> +                <description>Bind a socket to privileged ports (port numbers less than 1024)</description> +              </valueHelp> +              <valueHelp>                  <format>net-raw</format>                  <description>Permission to create raw network sockets</description>                </valueHelp> @@ -48,7 +52,7 @@                  <description>Permission to set system clock</description>                </valueHelp>                <constraint> -                <regex>^(net-admin|net-raw|setpcap|sys-admin|sys-time)$</regex> +                <regex>^(net-admin|net-bind-service|net-raw|setpcap|sys-admin|sys-time)$</regex>                </constraint>                <multi/>              </properties> diff --git a/interface-definitions/include/bgp/afi-route-target-vpn.xml.i b/interface-definitions/include/bgp/afi-route-target-vpn.xml.i index 1dc184a02..0cd0fdd76 100644 --- a/interface-definitions/include/bgp/afi-route-target-vpn.xml.i +++ b/interface-definitions/include/bgp/afi-route-target-vpn.xml.i @@ -1,7 +1,7 @@  <!-- include start from bgp/route-target-both.xml.i -->  <node name="route-target">    <properties> -    <help>Specify route distinguisher</help> +    <help>Specify route target list</help>    </properties>    <children>      <node name="vpn"> diff --git a/interface-definitions/include/nat-translation-options.xml.i b/interface-definitions/include/nat-translation-options.xml.i index defc8c0d5..df2f76397 100644 --- a/interface-definitions/include/nat-translation-options.xml.i +++ b/interface-definitions/include/nat-translation-options.xml.i @@ -16,7 +16,7 @@          </valueHelp>          <valueHelp>            <format>random</format> -          <description>Random source or destination address allocation for each connection (defaut)</description> +          <description>Random source or destination address allocation for each connection (default)</description>          </valueHelp>          <constraint>            <regex>^(persistent|random)$</regex> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index a82c0b2a6..d6a602f53 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -85,7 +85,7 @@                <constraintErrorMessage>VRF routing table must be in range from 100 to 65535</constraintErrorMessage>              </properties>            </leafNode> -          <leafNode name="vni" owner="${vyos_conf_scripts_dir}/vrf_vni.py $VAR(../@)"> +          <leafNode name="vni" owner="${vyos_conf_scripts_dir}/vrf_vni.py">              <properties>                <help>Virtual Network Identifier</help>                <!-- priority must be after BGP --> diff --git a/op-mode-definitions/show-ip-route.xml.in b/op-mode-definitions/show-ip-route.xml.in index 0a24bc45a..fdbb6859d 100644 --- a/op-mode-definitions/show-ip-route.xml.in +++ b/op-mode-definitions/show-ip-route.xml.in @@ -125,16 +125,11 @@                  </properties>                  <command>vtysh -c "show ip route tag $5"</command>                </tagNode> -              <node name="vrf"> -                <properties> -                  <help>Show IP routes in VRF</help> -                </properties> -              </node>                <tagNode name="vrf">                  <properties>                    <help>Show IP routes in VRF</help>                    <completionHelp> -                    <list><vrf></list> +                    <list>all</list>                      <path>vrf name</path>                    </completionHelp>                  </properties> diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 2e59a7afc..9d54dc78e 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -80,6 +80,23 @@ class EthernetIf(Interface):          super().__init__(ifname, **kargs)          self.ethtool = Ethtool(ifname) +    def remove(self): +        """ +        Remove interface from config. Removing the interface deconfigures all +        assigned IP addresses. +        Example: +        >>> from vyos.ifconfig import WWANIf +        >>> i = EthernetIf('eth0') +        >>> i.remove() +        """ + +        if self.exists(self.ifname): +            # interface is placed in A/D state when removed from config! It +            # will remain visible for the operating system. +            self.set_admin_state('down') + +        super().remove() +      def set_flow_control(self, enable):          """          Changes the pause parameters of the specified Ethernet device. diff --git a/python/vyos/ifconfig/wwan.py b/python/vyos/ifconfig/wwan.py index f18959a60..845c9bef9 100644 --- a/python/vyos/ifconfig/wwan.py +++ b/python/vyos/ifconfig/wwan.py @@ -26,3 +26,20 @@ class WWANIf(Interface):              'eternal': 'wwan[0-9]+$',          },      } + +    def remove(self): +        """ +        Remove interface from config. Removing the interface deconfigures all +        assigned IP addresses. +        Example: +        >>> from vyos.ifconfig import WWANIf +        >>> i = WWANIf('wwan0') +        >>> i.remove() +        """ + +        if self.exists(self.ifname): +            # interface is placed in A/D state when removed from config! It +            # will remain visible for the operating system. +            self.set_admin_state('down') + +        super().remove() diff --git a/python/vyos/util.py b/python/vyos/util.py index 9c4c29322..9aa1f98d2 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -931,3 +931,19 @@ def install_into_config(conf, config_paths, override_prompt=True):      if count > 0:          print(f'{count} value(s) installed. Use "compare" to see the pending changes, and "commit" to apply.') + +def is_wwan_connected(interface): +    """ Determine if a given WWAN interface, e.g. wwan0 is connected to the +    carrier network or not """ +    import json + +    if not interface.startswith('wwan'): +        raise ValueError(f'Specified interface "{interface}" is not a WWAN interface') + +    modem = interface.lstrip('wwan') + +    tmp = cmd(f'mmcli --modem {modem} --output-json') +    tmp = json.loads(tmp) + +    # return True/False if interface is in connected state +    return dict_search('modem.generic.state', tmp) == 'connected' diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py index 09ca89721..cc0cdaec0 100644 --- a/smoketest/scripts/cli/test_container.py +++ b/smoketest/scripts/cli/test_container.py @@ -19,7 +19,6 @@ import json  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import cmd  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_ha_vrrp.py b/smoketest/scripts/cli/test_ha_vrrp.py index 8c5bb86d8..2524bf2b1 100755 --- a/smoketest/scripts/cli/test_ha_vrrp.py +++ b/smoketest/scripts/cli/test_ha_vrrp.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.ifconfig.vrrp import VRRP  from vyos.util import cmd diff --git a/smoketest/scripts/cli/test_interfaces_geneve.py b/smoketest/scripts/cli/test_interfaces_geneve.py index 129ee71e5..6233ade6e 100755 --- a/smoketest/scripts/cli/test_interfaces_geneve.py +++ b/smoketest/scripts/cli/test_interfaces_geneve.py @@ -16,7 +16,6 @@  import unittest -from vyos.configsession import ConfigSession  from vyos.ifconfig import Interface  from vyos.util import get_interface_config diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py index 7ce1b9872..f8a6ae986 100755 --- a/smoketest/scripts/cli/test_interfaces_openvpn.py +++ b/smoketest/scripts/cli/test_interfaces_openvpn.py @@ -23,7 +23,6 @@ from netifaces import interfaces  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import cmd  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index 67edce2a0..4f1e1ee99 100755 --- a/smoketest/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -20,7 +20,6 @@ import unittest  from psutil import process_iter  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  config_file = '/etc/ppp/peers/{}' diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index 7b420cd51..f63c850d8 100755 --- a/smoketest/scripts/cli/test_interfaces_vxlan.py +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py @@ -16,7 +16,6 @@  import unittest -from vyos.configsession import ConfigSession  from vyos.ifconfig import Interface  from vyos.util import get_interface_config diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py index 3707eaac3..aaf27a2c4 100755 --- a/smoketest/scripts/cli/test_interfaces_wireguard.py +++ b/smoketest/scripts/cli/test_interfaces_wireguard.py @@ -18,7 +18,6 @@ import os  import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  base_path = ['interfaces', 'wireguard'] diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 0706f234e..75c628244 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -20,7 +20,6 @@ import json  import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import cmd  from vyos.util import dict_search diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index 7721105e0..8afe0da26 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -21,7 +21,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import cmd  from vyos.util import dict_search diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py index deaf23b05..45a4bd61e 100755 --- a/smoketest/scripts/cli/test_pki.py +++ b/smoketest/scripts/cli/test_pki.py @@ -17,7 +17,6 @@  import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  base_path = ['pki'] diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index c2288a86a..1286a768d 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import cmd diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py index a57f8d5f2..297398d3c 100755 --- a/smoketest/scripts/cli/test_protocols_bfd.py +++ b/smoketest/scripts/cli/test_protocols_bfd.py @@ -17,7 +17,6 @@  import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_protocols_igmp-proxy.py b/smoketest/scripts/cli/test_protocols_igmp-proxy.py index 1eaf21722..079b5bee5 100755 --- a/smoketest/scripts/cli/test_protocols_igmp-proxy.py +++ b/smoketest/scripts/cli/test_protocols_igmp-proxy.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import read_file  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py index 8170f2b56..f4b0a690d 100755 --- a/smoketest/scripts/cli/test_protocols_isis.py +++ b/smoketest/scripts/cli/test_protocols_isis.py @@ -17,7 +17,6 @@  import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.ifconfig import Section  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_protocols_ospfv3.py b/smoketest/scripts/cli/test_protocols_ospfv3.py index 0b4b01993..c0673629e 100755 --- a/smoketest/scripts/cli/test_protocols_ospfv3.py +++ b/smoketest/scripts/cli/test_protocols_ospfv3.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.ifconfig import Section  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py index 423cd811a..80d4e79f9 100755 --- a/smoketest/scripts/cli/test_protocols_rip.py +++ b/smoketest/scripts/cli/test_protocols_rip.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.ifconfig import Section  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_protocols_ripng.py b/smoketest/scripts/cli/test_protocols_ripng.py index add92b73d..40585e778 100755 --- a/smoketest/scripts/cli/test_protocols_ripng.py +++ b/smoketest/scripts/cli/test_protocols_ripng.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.ifconfig import Section  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py index 6d334a9f8..d9792ce8d 100755 --- a/smoketest/scripts/cli/test_protocols_rpki.py +++ b/smoketest/scripts/cli/test_protocols_rpki.py @@ -19,7 +19,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import cmd  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py index 0d3228cc7..4c4eb5a7c 100755 --- a/smoketest/scripts/cli/test_protocols_static.py +++ b/smoketest/scripts/cli/test_protocols_static.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.template import is_ipv6  from vyos.util import get_interface_config diff --git a/smoketest/scripts/cli/test_service_bcast-relay.py b/smoketest/scripts/cli/test_service_bcast-relay.py index 58b730ab4..87901869e 100755 --- a/smoketest/scripts/cli/test_service_bcast-relay.py +++ b/smoketest/scripts/cli/test_service_bcast-relay.py @@ -19,7 +19,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM  from psutil import process_iter -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  base_path = ['service', 'broadcast-relay'] diff --git a/smoketest/scripts/cli/test_service_dhcp-relay.py b/smoketest/scripts/cli/test_service_dhcp-relay.py index db2edba54..bbfd9e032 100755 --- a/smoketest/scripts/cli/test_service_dhcp-relay.py +++ b/smoketest/scripts/cli/test_service_dhcp-relay.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.ifconfig import Section  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 301f8fa31..14666db15 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import process_named_running  from vyos.util import read_file diff --git a/smoketest/scripts/cli/test_service_dhcpv6-relay.py b/smoketest/scripts/cli/test_service_dhcpv6-relay.py index 5a9dd1aa6..fc206435b 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-relay.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-relay.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.ifconfig import Section  from vyos.template import address_from_cidr diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index 3f9564e59..7177f1505 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.template import inc_ip  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 134254186..90d10d40b 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -20,7 +20,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import cmd  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py index 45ca618cb..5929f8cba 100755 --- a/smoketest/scripts/cli/test_service_dns_forwarding.py +++ b/smoketest/scripts/cli/test_service_dns_forwarding.py @@ -19,7 +19,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import read_file  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 3af63636a..8e69efd9c 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -17,7 +17,6 @@  import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.util import run  base_path = ['service', 'https'] diff --git a/smoketest/scripts/cli/test_service_mdns-repeater.py b/smoketest/scripts/cli/test_service_mdns-repeater.py index 8941f065c..f99a98da1 100755 --- a/smoketest/scripts/cli/test_service_mdns-repeater.py +++ b/smoketest/scripts/cli/test_service_mdns-repeater.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.util import process_named_running  base_path = ['service', 'mdns', 'repeater'] diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index 26b4626c2..4875fb5d1 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -19,7 +19,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.util import read_file  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py index 008271102..058835c72 100755 --- a/smoketest/scripts/cli/test_service_snmp.py +++ b/smoketest/scripts/cli/test_service_snmp.py @@ -19,9 +19,9 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.template import is_ipv4 +from vyos.template import address_from_cidr  from vyos.util import read_file  from vyos.util import process_named_running @@ -36,16 +36,29 @@ def get_config_value(key):      return tmp[0]  class TestSNMPService(VyOSUnitTestSHIM.TestCase): -    def setUp(self): +    @classmethod +    def setUpClass(cls): +        super(cls, cls).setUpClass() +          # ensure we can also run this test on a live system - so lets clean          # out the current configuration :) +        cls.cli_delete(cls, base_path) + +    def tearDown(self): +        # delete testing SNMP config          self.cli_delete(base_path) +        self.cli_commit()      def test_snmp_basic(self): +        dummy_if = 'dum7312' +        dummy_addr = '100.64.0.1/32' +        self.cli_set(['interfaces', 'dummy', dummy_if, 'address', dummy_addr]) +          # Check if SNMP can be configured and service runs          clients = ['192.0.2.1', '2001:db8::1']          networks = ['192.0.2.128/25', '2001:db8:babe::/48'] -        listen = ['127.0.0.1', '::1'] +        listen = ['127.0.0.1', '::1', address_from_cidr(dummy_addr)] +        port = '5000'          for auth in ['ro', 'rw']:              community = 'VyOS' + auth @@ -56,7 +69,7 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):                  self.cli_set(base_path + ['community', community, 'network', network])          for addr in listen: -            self.cli_set(base_path + ['listen-address', addr]) +            self.cli_set(base_path + ['listen-address', addr, 'port', port])          self.cli_set(base_path + ['contact', 'maintainers@vyos.io'])          self.cli_set(base_path + ['location', 'qemu']) @@ -68,16 +81,18 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):          # thus we need to transfor this into a proper list          config = get_config_value('agentaddress')          expected = 'unix:/run/snmpd.socket' +        self.assertIn(expected, config) +          for addr in listen:              if is_ipv4(addr): -                expected += ',udp:{}:161'.format(addr) +                expected = f'udp:{addr}:{port}'              else: -                expected += ',udp6:[{}]:161'.format(addr) - -        self.assertTrue(expected in config) +                expected = f'udp6:[{addr}]:{port}' +            self.assertIn(expected, config)          # Check for running process          self.assertTrue(process_named_running(PROCESS_NAME)) +        self.cli_delete(['interfaces', 'dummy', dummy_if])      def test_snmpv3_sha(self): @@ -86,7 +101,7 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):          self.cli_set(base_path + ['v3', 'engineid', '000000000000000000000002'])          self.cli_set(base_path + ['v3', 'group', 'default', 'mode', 'ro']) -        # check validate() - a view must be created before this can be comitted +        # check validate() - a view must be created before this can be committed          with self.assertRaises(ConfigSessionError):              self.cli_commit() @@ -152,4 +167,3 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):  if __name__ == '__main__':      unittest.main(verbosity=2) - diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index ded4d8301..a54c03919 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -20,7 +20,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import cmd  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_tftp-server.py b/smoketest/scripts/cli/test_service_tftp-server.py index aed4c6beb..1a1bf0cdf 100755 --- a/smoketest/scripts/cli/test_service_tftp-server.py +++ b/smoketest/scripts/cli/test_service_tftp-server.py @@ -19,7 +19,6 @@ import unittest  from psutil import process_iter  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import read_file  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py index 6780a93f9..8a1a03ce7 100755 --- a/smoketest/scripts/cli/test_service_webproxy.py +++ b/smoketest/scripts/cli/test_service_webproxy.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import cmd  from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_system_acceleration_qat.py b/smoketest/scripts/cli/test_system_acceleration_qat.py index 9584888d6..9e60bb211 100755 --- a/smoketest/scripts/cli/test_system_acceleration_qat.py +++ b/smoketest/scripts/cli/test_system_acceleration_qat.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  base_path = ['system', 'acceleration', 'qat'] diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index a2380981b..b2934cf04 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -19,7 +19,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.util import cmd  from vyos.util import read_file diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py new file mode 100755 index 000000000..a2b5b1481 --- /dev/null +++ b/smoketest/scripts/cli/test_system_flow-accounting.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section +from vyos.util import cmd +from vyos.util import process_named_running +from vyos.util import read_file + +PROCESS_NAME = 'uacctd' +base_path = ['system', 'flow-accounting'] + +uacctd_conf = '/etc/pmacct/uacctd.conf' + +class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): +    @classmethod +    def setUpClass(cls): +        super(cls, cls).setUpClass() + +        # ensure we can also run this test on a live system - so lets clean +        # out the current configuration :) +        cls.cli_delete(cls, base_path) + +    def tearDown(self): +        self.cli_delete(base_path) +        self.cli_commit() + +        # after service removal process must no longer run +        self.assertFalse(process_named_running(PROCESS_NAME)) + +    def test_basic(self): +        buffer_size = '5' # MiB +        self.cli_set(base_path + ['buffer-size', buffer_size]) + +        # You need to configure at least one interface for flow-accounting +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        for interface in Section.interfaces('ethernet'): +            self.cli_set(base_path + ['interface', interface]) + +        # commit changes +        self.cli_commit() + +        # verify configuration +        tmp = cmd('sudo iptables-save -t raw') +        for interface in Section.interfaces('ethernet'): +            self.assertIn(f'-A VYATTA_CT_PREROUTING_HOOK -i {interface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size 128 --nflog-threshold 100', tmp) + +        uacctd = read_file(uacctd_conf) +        # circular queue size - buffer_size +        tmp = int(buffer_size) *1024 *1024 +        self.assertIn(f'plugin_pipe_size: {tmp}', uacctd) +        # transfer buffer size - recommended value from pmacct developers 1/1000 of pipe size +        tmp = int(buffer_size) *1024 *1024 +        # do an integer division +        tmp //= 1000 +        self.assertIn(f'plugin_buffer_size: {tmp}', uacctd) + +        # Check for running process +        self.assertTrue(process_named_running(PROCESS_NAME)) + +if __name__ == '__main__': +    unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_ip.py b/smoketest/scripts/cli/test_system_ip.py index e98a4e234..83df9d99e 100755 --- a/smoketest/scripts/cli/test_system_ip.py +++ b/smoketest/scripts/cli/test_system_ip.py @@ -17,7 +17,6 @@  import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.util import read_file  base_path = ['system', 'ip'] diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py index c9c9e833d..1325d4b39 100755 --- a/smoketest/scripts/cli/test_system_ipv6.py +++ b/smoketest/scripts/cli/test_system_ipv6.py @@ -17,7 +17,6 @@  import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.util import read_file  base_path = ['system', 'ipv6'] diff --git a/smoketest/scripts/cli/test_system_lcd.py b/smoketest/scripts/cli/test_system_lcd.py index 7a39e2986..831fba979 100755 --- a/smoketest/scripts/cli/test_system_lcd.py +++ b/smoketest/scripts/cli/test_system_lcd.py @@ -19,7 +19,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM  from configparser import ConfigParser -from vyos.configsession import ConfigSession  from vyos.util import process_named_running  config_file = '/run/LCDd/LCDd.conf' diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py index 0addd630e..69a06eeac 100755 --- a/smoketest/scripts/cli/test_system_login.py +++ b/smoketest/scripts/cli/test_system_login.py @@ -24,7 +24,6 @@ from distutils.version import LooseVersion  from platform import release as kernel_version  from subprocess import Popen, PIPE -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import cmd  from vyos.util import read_file diff --git a/smoketest/scripts/cli/test_system_nameserver.py b/smoketest/scripts/cli/test_system_nameserver.py index 50dc466c2..58c84988e 100755 --- a/smoketest/scripts/cli/test_system_nameserver.py +++ b/smoketest/scripts/cli/test_system_nameserver.py @@ -19,7 +19,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.util import read_file diff --git a/smoketest/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py index 2b86ebd7c..e8cc64463 100755 --- a/smoketest/scripts/cli/test_system_ntp.py +++ b/smoketest/scripts/cli/test_system_ntp.py @@ -19,7 +19,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.template import address_from_cidr  from vyos.template import netmask_from_cidr diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 93569c4ec..c710aec6e 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -307,7 +307,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):          swanctl_lines = [              f'proposals = aes128-sha1-modp1024,aes256-sha1-modp1024',              f'version = 1', -            f'life_time = {ike_lifetime}s', +            f'rekey_time = {ike_lifetime}s',              f'rekey_time = {esp_lifetime}s',              f'esp_proposals = aes128-sha1-modp1024,aes256-sha1-modp1024,3des-md5-modp1024',              f'local_ts = dynamic[gre]', diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index cad3b1182..b0e859b5c 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -18,7 +18,6 @@ import unittest  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.util import process_named_running  OCSERV_CONF = '/run/ocserv/ocserv.conf' diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index f36d16344..5ffa9c086 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -22,7 +22,6 @@ import unittest  from netifaces import interfaces  from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.ifconfig import Interface  from vyos.ifconfig import Section @@ -58,7 +57,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          for vrf in vrfs:              self.assertNotIn(vrf, interfaces()) -    def test_vrf_table_id(self): +    def test_vrf_vni_and_table_id(self):          table = '1000'          for vrf in vrfs:              base = base_path + ['name', vrf] @@ -70,6 +69,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):                  self.cli_commit()              self.cli_set(base + ['table', table]) +            self.cli_set(base + ['vni', table])              if vrf == 'green':                  self.cli_set(base + ['disable']) @@ -101,6 +101,11 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):              #  ...              regex = f'{table}\s+{vrf}\s+#\s+{description}'              self.assertTrue(re.findall(regex, iproute2_config)) + +            frrconfig = self.getFRRconfig(f'vrf {vrf}') +            self.assertIn(f' vni {table}', frrconfig) + +            # Increment table ID for the next run              table = str(int(table) + 1)      def test_vrf_loopback_ips(self): @@ -178,5 +183,42 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):              section = Section.section(interface)              self.cli_delete(['interfaces', section, interface, 'vrf']) +    def test_vrf_static_route(self): +        table = '100' +        for vrf in vrfs: +            next_hop = f'192.0.{table}.1' +            prefix = f'10.0.{table}.0/24' +            base = base_path + ['name', vrf] + +            self.cli_set(base + ['vni', table]) + +            # check validate() - a table ID is mandatory +            with self.assertRaises(ConfigSessionError): +                self.cli_commit() + +            self.cli_set(base + ['table', table]) +            self.cli_set(base + ['protocols', 'static', 'route', prefix, 'next-hop', next_hop]) + +            table = str(int(table) + 1) + +        # commit changes +        self.cli_commit() + +        # Verify VRF configuration +        table = '100' +        for vrf in vrfs: +            next_hop = f'192.0.{table}.1' +            prefix = f'10.0.{table}.0/24' + +            self.assertTrue(vrf in interfaces()) +            vrf_if = Interface(vrf) + +            frrconfig = self.getFRRconfig(f'vrf {vrf}') +            self.assertIn(f' vni {table}', frrconfig) +            self.assertIn(f' ip route {prefix} {next_hop}', frrconfig) + +            # Increment table ID for the next run +            table = str(int(table) + 1) +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 1e76147dd..3b8fae710 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -634,10 +634,10 @@ def generate(openvpn):  def apply(openvpn):      interface = openvpn['ifname'] -    call(f'systemctl stop openvpn@{interface}.service')      # Do some cleanup when OpenVPN is disabled/deleted      if 'deleted' in openvpn or 'disable' in openvpn: +        call(f'systemctl stop openvpn@{interface}.service')          for cleanup_file in glob(f'/run/openvpn/{interface}.*'):              if os.path.isfile(cleanup_file):                  os.unlink(cleanup_file) @@ -649,7 +649,7 @@ def apply(openvpn):      # No matching OpenVPN process running - maybe it got killed or none      # existed - nevertheless, spawn new OpenVPN process -    call(f'systemctl start openvpn@{interface}.service') +    call(f'systemctl reload-or-restart openvpn@{interface}.service')      o = VTunIf(**openvpn)      o.update(openvpn) diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index faa5eb628..f013e5411 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -25,7 +25,9 @@ from vyos.configverify import verify_interface_exists  from vyos.configverify import verify_vrf  from vyos.ifconfig import WWANIf  from vyos.util import cmd +from vyos.util import call  from vyos.util import dict_search +from vyos.util import DEVNULL  from vyos import ConfigError  from vyos import airbag  airbag.enable() @@ -88,7 +90,7 @@ def apply(wwan):          options += ',user={user},password={password}'.format(**wwan['authentication'])      command = f'{base_cmd} --simple-connect="{options}"' -    cmd(command) +    call(command, stdout=DEVNULL)      w.update(wwan)      return None diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 2a420b193..e1852f2ce 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -20,13 +20,17 @@ from sys import exit  from vyos.config import Config  from vyos.configverify import verify_vrf -from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random +from vyos.snmpv3_hashgen import plaintext_to_md5 +from vyos.snmpv3_hashgen import plaintext_to_sha1 +from vyos.snmpv3_hashgen import random  from vyos.template import render  from vyos.template import is_ipv4 -from vyos.util import call, chmod_755 +from vyos.util import call +from vyos.util import chmod_755  from vyos.validate import is_addr_assigned  from vyos.version import get_version_data -from vyos import ConfigError, airbag +from vyos import ConfigError +from vyos import airbag  airbag.enable()  config_file_client  = r'/etc/snmp/snmp.conf' @@ -410,19 +414,20 @@ def verify(snmp):          port = listen[1]          protocol = snmp['protocol'] +        tmp = None          if is_ipv4(addr):              # example: udp:127.0.0.1:161 -            listen = f'{protocol}:{addr}:{port}' +            tmp = f'{protocol}:{addr}:{port}'          elif snmp['ipv6_enabled']:              # example: udp6:[::1]:161 -            listen = f'{protocol}6:[{addr}]:{port}' +            tmp = f'{protocol}6:[{addr}]:{port}'          # We only wan't to configure addresses that exist on the system.          # Hint the user if they don't exist          if is_addr_assigned(addr): -            snmp['listen_on'].append(listen) +            if tmp: snmp['listen_on'].append(tmp)          else: -            print('WARNING: SNMP listen address {0} not configured!'.format(addr)) +            print(f'WARNING: SNMP listen address {addr} not configured!')      verify_vrf(snmp) diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py index e9d6a339c..2220d7b66 100755 --- a/src/conf_mode/system-login-banner.py +++ b/src/conf_mode/system-login-banner.py @@ -37,7 +37,7 @@ PRELOGIN_NET_FILE = r'/etc/issue.net'  POSTLOGIN_FILE = r'/etc/motd'  default_config_data = { -    'issue': 'Welcome to VyOS - \\n \\l\n', +    'issue': 'Welcome to VyOS - \\n \\l\n\n',      'issue_net': 'Welcome to VyOS\n',      'motd': motd  } diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 919083ac4..38c0c4463 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -18,7 +18,6 @@ import os  from sys import exit  from json import loads -from tempfile import NamedTemporaryFile  from vyos.config import Config  from vyos.configdict import node_changed @@ -31,10 +30,12 @@ from vyos.util import get_interface_config  from vyos.util import popen  from vyos.util import run  from vyos import ConfigError +from vyos import frr  from vyos import airbag  airbag.enable() -config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' +config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf' +nft_vrf_config = '/tmp/nftables-vrf-zones'  def list_rules():      command = 'ip -j -4 rule show' @@ -128,8 +129,8 @@ def verify(vrf):  def generate(vrf):      render(config_file, 'vrf/vrf.conf.tmpl', vrf)      # Render nftables zones config -    vrf['nft_vrf_zones'] = NamedTemporaryFile().name -    render(vrf['nft_vrf_zones'], 'firewall/nftables-vrf-zones.tmpl', vrf) + +    render(nft_vrf_config, 'firewall/nftables-vrf-zones.tmpl', vrf)      return None @@ -165,8 +166,9 @@ def apply(vrf):          _, err = popen('nft list table inet vrf_zones')          # If not, create a table          if err: -            cmd(f'nft -f {vrf["nft_vrf_zones"]}') -            os.unlink(vrf['nft_vrf_zones']) +            if os.path.exists(nft_vrf_config): +                cmd(f'nft -f {nft_vrf_config}') +                os.unlink(nft_vrf_config)          for name, config in vrf['name'].items():              table = config['table'] diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py index 87ee8f2d1..50d60f0dc 100755 --- a/src/conf_mode/vrf_vni.py +++ b/src/conf_mode/vrf_vni.py @@ -32,32 +32,23 @@ def get_config(config=None):      else:          conf = Config() -    # This script only works with a passed VRF name -    if len(argv) < 1: -        raise NotImplementedError -    vrf = argv[1] +    base = ['vrf'] +    vrf = conf.get_config_dict(base, get_first_key=True) +    return vrf -    # "assemble" dict - easier here then use a full blown get_config_dict() -    # on a single leafNode -    vni = { 'vrf' : vrf } -    tmp = conf.return_value(['vrf', 'name', vrf, 'vni']) -    if tmp: vni.update({ 'vni' : tmp }) - -    return vni - -def verify(vni): +def verify(vrf):      return None -def generate(vni): -    vni['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vni) +def generate(vrf): +    vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vrf)      return None -def apply(vni): +def apply(vrf):      # add configuration to FRR      frr_cfg = frr.FRRConfig()      frr_cfg.load_configuration(frr_daemon) -    frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '') -    frr_cfg.add_before(r'(interface .*|line vty)', vni['new_frr_config']) +    frr_cfg.modify_section(f'^vrf .+$', '') +    frr_cfg.add_before(r'(interface .*|line vty)', vrf['new_frr_config'])      frr_cfg.commit_configuration(frr_daemon)      # Save configuration to /run/frr/config/frr.conf diff --git a/src/etc/cron.d/check-wwan b/src/etc/cron.d/check-wwan new file mode 100644 index 000000000..28190776f --- /dev/null +++ b/src/etc/cron.d/check-wwan @@ -0,0 +1 @@ +*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py diff --git a/src/etc/systemd/system/openvpn@.service.d/10-override.conf b/src/etc/systemd/system/openvpn@.service.d/10-override.conf index 03fe6b587..775a2d7ba 100644 --- a/src/etc/systemd/system/openvpn@.service.d/10-override.conf +++ b/src/etc/systemd/system/openvpn@.service.d/10-override.conf @@ -7,6 +7,7 @@ WorkingDirectory=  WorkingDirectory=/run/openvpn  ExecStart=  ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid +ExecReload=/bin/kill -HUP $MAINPID  User=openvpn  Group=openvpn  AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE diff --git a/src/helpers/vyos-check-wwan.py b/src/helpers/vyos-check-wwan.py new file mode 100755 index 000000000..c6e6c54b7 --- /dev/null +++ b/src/helpers/vyos-check-wwan.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +from vyos.configquery import VbashOpRun +from vyos.configquery import ConfigTreeQuery + +from vyos.util import is_wwan_connected +from vyos.util import call + +conf = ConfigTreeQuery() +dict = conf.get_config_dict(['interfaces', 'wwan'], key_mangling=('-', '_'), +                            get_first_key=True) + +for interface, interface_config in dict.items(): +    if not is_wwan_connected(interface): +        if 'disable' in interface_config: +            # do not restart this interface as it's disabled by the user +            continue + +        #op = VbashOpRun() +        #op.run(['connect', 'interface', interface]) +        call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py') + +exit(0) diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name index 13fb9e31f..e21d8c9ff 100755 --- a/src/helpers/vyos_net_name +++ b/src/helpers/vyos_net_name @@ -144,7 +144,19 @@ def get_configfile_interfaces() -> dict:          logging.critical(f"OSError {e}")          exit(1) -    config = ConfigTree(config_file) +    try: +        config = ConfigTree(config_file) +    except Exception: +        logging.debug(f"updating component version string syntax") +        try: +            # this will update the component version string in place, for +            # updates 1.2 --> 1.3/1.4 +            os.system(f'/usr/libexec/vyos/run-config-migration.py {config_path} --virtual --set-vintage=vyos') +            with open(config_path) as f: +                config_file = f.read() +            config = ConfigTree(config_file) +        except Exception as e: +            logging.critical(f"ConfigTree error: {e}")      base = ['interfaces', 'ethernet']      if config.exists(base): diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index a773aa28e..ffc574362 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -17,21 +17,19 @@  import os  import argparse -from sys import exit  from psutil import process_iter -from time import strftime, localtime, time  from vyos.util import call +from vyos.util import DEVNULL +from vyos.util import is_wwan_connected -def check_interface(interface): +def check_ppp_interface(interface):      if not os.path.isfile(f'/etc/ppp/peers/{interface}'): -        print(f'Interface {interface}: invalid!') +        print(f'Interface {interface} does not exist!')          exit(1)  def check_ppp_running(interface): -    """ -    Check if ppp process is running in the interface in question -    """ +    """ Check if PPP process is running in the interface in question """      for p in process_iter():          if "pppd" in p.name():              if interface in p.cmdline(): @@ -40,32 +38,46 @@ def check_ppp_running(interface):      return False  def connect(interface): -    """ -    Connect PPP interface -    """ -    check_interface(interface) +    """ Connect dialer interface """ -    # Check if interface is already dialed -    if os.path.isdir(f'/sys/class/net/{interface}'): -        print(f'Interface {interface}: already connected!') -    elif check_ppp_running(interface): -        print(f'Interface {interface}: connection is beeing established!') +    if interface.startswith('ppp'): +        check_ppp_interface(interface) +        # Check if interface is already dialed +        if os.path.isdir(f'/sys/class/net/{interface}'): +            print(f'Interface {interface}: already connected!') +        elif check_ppp_running(interface): +            print(f'Interface {interface}: connection is beeing established!') +        else: +            print(f'Interface {interface}: connecting...') +            call(f'systemctl restart ppp@{interface}.service') +    elif interface.startswith('wwan'): +        if is_wwan_connected(interface): +            print(f'Interface {interface}: already connected!') +        else: +            call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py')      else: -        print(f'Interface {interface}: connecting...') -        call(f'systemctl restart ppp@{interface}.service') +        print(f'Unknown interface {interface}, can not connect. Aborting!')  def disconnect(interface): -    """ -    Disconnect PPP interface -    """ -    check_interface(interface) +    """ Disconnect dialer interface """ -    # Check if interface is already down -    if not check_ppp_running(interface): -        print(f'Interface {interface}: connection is already down') +    if interface.startswith('ppp'): +        check_ppp_interface(interface) + +        # Check if interface is already down +        if not check_ppp_running(interface): +            print(f'Interface {interface}: connection is already down') +        else: +            print(f'Interface {interface}: disconnecting...') +            call(f'systemctl stop ppp@{interface}.service') +    elif interface.startswith('wwan'): +        if not is_wwan_connected(interface): +            print(f'Interface {interface}: connection is already down') +        else: +            modem = interface.lstrip('wwan') +            call(f'mmcli --modem {modem} --simple-disconnect', stdout=DEVNULL)      else: -        print(f'Interface {interface}: disconnecting...') -        call(f'systemctl stop ppp@{interface}.service') +        print(f'Unknown interface {interface}, can not disconnect. Aborting!')  def main():      parser = argparse.ArgumentParser() diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index 3d50eb938..eac068274 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -94,10 +94,8 @@ def split_text(text, used=0):      used: number of characted already used in the screen      """      no_tty = call('tty -s') -    if no_tty: -        return text.split() -    returned = cmd('stty size') +    returned = cmd('stty size') if not no_tty else ''      if len(returned) == 2:          rows, columns = [int(_) for _ in returned]      else: diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql index 580c0eb7f..29f58f709 100644 --- a/src/services/api/graphql/README.graphql +++ b/src/services/api/graphql/README.graphql @@ -10,7 +10,7 @@ to run with that address as default router by requesting these 'mutations'  in the GraphQL playground:  mutation { -  createInterfaceEthernet (data: {interface: "eth1", +  CreateInterfaceEthernet (data: {interface: "eth1",                                    address: "192.168.0.1/24",                                    description: "BOB"}) {      success @@ -22,7 +22,7 @@ mutation {  }  mutation { -  createDhcpServer(data: {sharedNetworkName: "BOB", +  CreateDhcpServer(data: {sharedNetworkName: "BOB",                            subnet: "192.168.0.0/24",                            defaultRouter: "192.168.0.1",                            nameServer: "192.168.0.1", @@ -42,6 +42,38 @@ mutation {    }  } +To save the configuration, use the following mutation: + +mutation { +  SaveConfigFile(data: {fileName: "/config/config.boot"}) { +    success +    errors +    data { +      fileName +    } +  } +} + +N.B. fileName can be empty (fileName: "") or data can be empty (data: {}) to +save to /config/config.boot; to save to an alternative path, specify +fileName. + +Similarly, using the same 'endpoint' (meaning the form of the request and +resolver; the actual enpoint for all GraphQL requests is +https://hostname/graphql), one can load an arbitrary config file from a +path. + +mutation { +  LoadConfigFile(data: {fileName: "/home/vyos/config.boot"}) { +    success +    errors +    data { +      fileName +    } +  } +} + +  The GraphQL playground will be found at:  https://{{ host_address }}/graphql @@ -57,22 +89,23 @@ What's here:  services  ├── api  │   └── graphql +│       ├── bindings.py  │       ├── graphql  │       │   ├── directives.py  │       │   ├── __init__.py  │       │   ├── mutations.py  │       │   └── schema +│       │       ├── config_file.graphql  │       │       ├── dhcp_server.graphql  │       │       ├── interface_ethernet.graphql  │       │       └── schema.graphql +│       ├── README.graphql  │       ├── recipes -│       │   ├── dhcp_server.py  │       │   ├── __init__.py -│       │   ├── interface_ethernet.py -│       │   ├── recipe.py +│       │   ├── session.py  │       │   └── templates -│       │       ├── dhcp_server.tmpl -│       │       └── interface_ethernet.tmpl +│       │       ├── create_dhcp_server.tmpl +│       │       └── create_interface_ethernet.tmpl  │       └── state.py  ├── vyos-configd  ├── vyos-hostsd @@ -90,13 +123,14 @@ the Ur-data; the GraphQL schema is produced from those files, located in  Resolvers for the schema Mutation fields are dynamically generated using a  'directive' added to the respective schema field. The directive, -'@generate', is handled by the class 'DataDirective' in -'api/graphql/graphql/directives.py', which calls the 'make_resolver' function in -'api/graphql/graphql/mutations.py'; the produced resolver calls the appropriate -wrapper in 'api/graphql/recipes', with base class doing the (overridable) -configuration steps of calling all defined 'set'/'delete' commands. - -Integrating the above with vyos-http-api-server is ~10 lines of code. +'@configure', is handled by the class 'ConfigureDirective' in +'api/graphql/graphql/directives.py', which calls the +'make_configure_resolver' function in 'api/graphql/graphql/mutations.py'; +the produced resolver calls the appropriate wrapper in +'api/graphql/recipes', with base class doing the (overridable) configuration +steps of calling all defined 'set'/'delete' commands. + +Integrating the above with vyos-http-api-server is 4 lines of code.  What needs to be done: diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py new file mode 100644 index 000000000..1fbe13d0c --- /dev/null +++ b/src/services/api/graphql/bindings.py @@ -0,0 +1,13 @@ +import vyos.defaults +from . graphql.mutations import mutation +from . graphql.directives import directives_dict +from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers + +def generate_schema(): +    api_schema_dir = vyos.defaults.directories['api_schema'] + +    type_defs = load_schema_from_path(api_schema_dir) + +    schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives=directives_dict) + +    return schema diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index 651421c35..f5cd88acd 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -1,12 +1,11 @@  from ariadne import SchemaDirectiveVisitor, ObjectType -from . mutations import make_resolver +from . mutations import make_configure_resolver, make_config_file_resolver -class DataDirective(SchemaDirectiveVisitor): -    """ -    Class providing implementation of 'generate' directive in schema. +def non(arg): +    pass -    """ -    def visit_field_definition(self, field, object_type): +class VyosDirective(SchemaDirectiveVisitor): +    def visit_field_definition(self, field, object_type, make_resolver=non):          name = f'{field.type}'          # field.type contains the return value of the mutation; trim value          # to produce canonical name @@ -15,3 +14,24 @@ class DataDirective(SchemaDirectiveVisitor):          func = make_resolver(name)          field.resolve = func          return field + + +class ConfigureDirective(VyosDirective): +    """ +    Class providing implementation of 'configure' directive in schema. + +    """ +    def visit_field_definition(self, field, object_type): +        super().visit_field_definition(field, object_type, +                                       make_resolver=make_configure_resolver) + +class ConfigFileDirective(VyosDirective): +    """ +    Class providing implementation of 'configfile' directive in schema. + +    """ +    def visit_field_definition(self, field, object_type): +        super().visit_field_definition(field, object_type, +                                       make_resolver=make_config_file_resolver) + +directives_dict = {"configure": ConfigureDirective, "configfile": ConfigFileDirective} diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 98c665c9a..8a28b13d7 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -6,10 +6,11 @@ from graphql import GraphQLResolveInfo  from makefun import with_signature  from .. import state +from api.graphql.recipes.session import Session  mutation = ObjectType("Mutation") -def make_resolver(mutation_name): +def make_resolver(mutation_name, class_name, session_func):      """Dynamically generate a resolver for the mutation named in the      schema by 'mutation_name'. @@ -19,11 +20,11 @@ def make_resolver(mutation_name):      functools.wraps.      :raise Exception: -        encapsulating ConfigErrors, or internal errors +        raising ConfigErrors, or internal errors      """ -    class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1) +      func_base_name = convert_camel_case_to_snake(class_name) -    resolver_name = f'resolve_create_{func_base_name}' +    resolver_name = f'resolve_{func_base_name}'      func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'      @mutation.field(mutation_name) @@ -40,10 +41,17 @@ def make_resolver(mutation_name):              data = kwargs['data']              session = state.settings['app'].state.vyos_session -            mod = import_module(f'api.graphql.recipes.{func_base_name}') -            klass = getattr(mod, class_name) +            # one may override the session functions with a local subclass +            try: +                mod = import_module(f'api.graphql.recipes.{func_base_name}') +                klass = getattr(mod, class_name) +            except ImportError: +                # otherwise, dynamically generate subclass to invoke subclass +                # name based templates +                klass = type(class_name, (Session,), {})              k = klass(session, data) -            k.configure() +            method = getattr(k, session_func) +            method()              return {                  "success": True, @@ -57,4 +65,16 @@ def make_resolver(mutation_name):      return func_impl +def make_configure_resolver(mutation_name): +    class_name = mutation_name +    return make_resolver(mutation_name, class_name, 'configure') +def make_config_file_resolver(mutation_name): +    if 'Save' in mutation_name: +        class_name = mutation_name.replace('Save', '', 1) +        return make_resolver(mutation_name, class_name, 'save') +    elif 'Load' in mutation_name: +        class_name = mutation_name.replace('Load', '', 1) +        return make_resolver(mutation_name, class_name, 'load') +    else: +        raise Exception diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql new file mode 100644 index 000000000..31ab26b9e --- /dev/null +++ b/src/services/api/graphql/graphql/schema/config_file.graphql @@ -0,0 +1,27 @@ +input SaveConfigFileInput { +    fileName: String +} + +type SaveConfigFile { +    fileName: String +} + +type SaveConfigFileResult { +    data: SaveConfigFile +    success: Boolean! +    errors: [String] +} + +input LoadConfigFileInput { +    fileName: String! +} + +type LoadConfigFile { +    fileName: String! +} + +type LoadConfigFileResult { +    data: LoadConfigFile +    success: Boolean! +    errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql index 9f741a0a5..25f091bfa 100644 --- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql +++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql @@ -1,4 +1,4 @@ -input dhcpServerConfigInput { +input DhcpServerConfigInput {      sharedNetworkName: String      subnet: String      defaultRouter: String @@ -13,7 +13,7 @@ input dhcpServerConfigInput {      dnsForwardingListenAddress: String  } -type dhcpServerConfig { +type DhcpServerConfig {      sharedNetworkName: String      subnet: String      defaultRouter: String @@ -28,8 +28,8 @@ type dhcpServerConfig {      dnsForwardingListenAddress: String  } -type createDhcpServerResult { -    data: dhcpServerConfig +type CreateDhcpServerResult { +    data: DhcpServerConfig      success: Boolean!      errors: [String]  } diff --git a/src/services/api/graphql/graphql/schema/firewall_group.graphql b/src/services/api/graphql/graphql/schema/firewall_group.graphql new file mode 100644 index 000000000..efe7de632 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/firewall_group.graphql @@ -0,0 +1,47 @@ +input CreateFirewallAddressGroupInput { +    name: String! +    address: [String] +} + +type CreateFirewallAddressGroup { +    name: String! +    address: [String] +} + +type CreateFirewallAddressGroupResult { +    data: CreateFirewallAddressGroup +    success: Boolean! +    errors: [String] +} + +input UpdateFirewallAddressGroupMembersInput { +    name: String! +    address: [String!]! +} + +type UpdateFirewallAddressGroupMembers { +    name: String! +    address: [String!]! +} + +type UpdateFirewallAddressGroupMembersResult { +    data: UpdateFirewallAddressGroupMembers +    success: Boolean! +    errors: [String] +} + +input RemoveFirewallAddressGroupMembersInput { +    name: String! +    address: [String!]! +} + +type RemoveFirewallAddressGroupMembers { +    name: String! +    address: [String!]! +} + +type RemoveFirewallAddressGroupMembersResult { +    data: RemoveFirewallAddressGroupMembers +    success: Boolean! +    errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql index fdcf97bad..32438b315 100644 --- a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql +++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql @@ -1,18 +1,18 @@ -input interfaceEthernetConfigInput { +input InterfaceEthernetConfigInput {      interface: String      address: String      replace: Boolean = true      description: String  } -type interfaceEthernetConfig { +type InterfaceEthernetConfig {      interface: String      address: String      description: String  } -type createInterfaceEthernetResult { -    data: interfaceEthernetConfig +type CreateInterfaceEthernetResult { +    data: InterfaceEthernetConfig      success: Boolean!      errors: [String]  } diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 8a5e17962..9e97a0d60 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -7,9 +7,15 @@ type Query {      _dummy: String  } -directive @generate on FIELD_DEFINITION +directive @configure on FIELD_DEFINITION +directive @configfile on FIELD_DEFINITION  type Mutation { -    createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate -    createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate +    CreateDhcpServer(data: DhcpServerConfigInput) : CreateDhcpServerResult @configure +    CreateInterfaceEthernet(data: InterfaceEthernetConfigInput) : CreateInterfaceEthernetResult @configure +    CreateFirewallAddressGroup(data: CreateFirewallAddressGroupInput) : CreateFirewallAddressGroupResult @configure +    UpdateFirewallAddressGroupMembers(data: UpdateFirewallAddressGroupMembersInput) : UpdateFirewallAddressGroupMembersResult @configure +    RemoveFirewallAddressGroupMembers(data: RemoveFirewallAddressGroupMembersInput) : RemoveFirewallAddressGroupMembersResult @configure +    SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configfile +    LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configfile  } diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py deleted file mode 100644 index 3edb3028e..000000000 --- a/src/services/api/graphql/recipes/dhcp_server.py +++ /dev/null @@ -1,13 +0,0 @@ - -from . recipe import Recipe - -class DhcpServer(Recipe): -    def __init__(self, session, command_file): -        super().__init__(session, command_file) - -    # Define any custom processing of parameters here by overriding -    # configure: -    # -    # def configure(self): -    #     self.data = transform_data(self.data) -    #     super().configure() diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py deleted file mode 100644 index f88f5924f..000000000 --- a/src/services/api/graphql/recipes/interface_ethernet.py +++ /dev/null @@ -1,13 +0,0 @@ - -from . recipe import Recipe - -class InterfaceEthernet(Recipe): -    def __init__(self, session, command_file): -        super().__init__(session, command_file) - -    # Define any custom processing of parameters here by overriding -    # configure: -    # -    # def configure(self): -    #     self.data = transform_data(self.data) -    #     super().configure() diff --git a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py new file mode 100644 index 000000000..cde30c27a --- /dev/null +++ b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py @@ -0,0 +1,21 @@ + +from . session import Session + +class RemoveFirewallAddressGroupMembers(Session): +    def __init__(self, session, data): +        super().__init__(session, data) + +    # Define any custom processing of parameters here by overriding +    # configure: +    # +    # def configure(self): +    #     self._data = transform_data(self._data) +    #     super().configure() +    #     self.clean_up() + +    def configure(self): +        super().configure() + +        group_name = self._data['name'] +        path = ['firewall', 'group', 'address-group', group_name] +        self.delete_path_if_childless(path) diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/session.py index 8fbb9e0bf..b96cc1753 100644 --- a/src/services/api/graphql/recipes/recipe.py +++ b/src/services/api/graphql/recipes/session.py @@ -1,27 +1,17 @@  from ariadne import convert_camel_case_to_snake  import vyos.defaults +from vyos.config import Config  from vyos.template import render -class Recipe(object): +class Session(object):      def __init__(self, session, data):          self._session = session -        self.data = data +        self._data = data          self._name = convert_camel_case_to_snake(type(self).__name__) -    @property -    def data(self): -        return self.__data - -    @data.setter -    def data(self, data): -        if isinstance(data, dict): -            self.__data = data -        else: -            raise ValueError("data must be of type dict") -      def configure(self):          session = self._session -        data = self.data +        data = self._data          func_base_name = self._name          tmpl_file = f'{func_base_name}.tmpl' @@ -46,4 +36,30 @@ class Recipe(object):          except Exception as error:              raise error +    def delete_path_if_childless(self, path): +        session = self._session +        config = Config(session.get_session_env()) +        if not config.list_nodes(path): +            session.delete(path) +            session.commit() + +    def save(self): +        session = self._session +        data = self._data +        if 'file_name' not in data or not data['file_name']: +            data['file_name'] = '/config/config.boot' + +        try: +            session.save_config(data['file_name']) +        except Exception as error: +            raise error + +    def load(self): +        session = self._session +        data = self._data +        try: +            session.load_config(data['file_name']) +            session.commit() +        except Exception as error: +            raise error diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl index 70de43183..70de43183 100644 --- a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl +++ b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl new file mode 100644 index 000000000..a890d0086 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl @@ -0,0 +1,4 @@ +set firewall group address-group {{ name }} +{% for add in address %} +set firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl index d9d7ed691..d9d7ed691 100644 --- a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl +++ b/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl new file mode 100644 index 000000000..458f3e5fc --- /dev/null +++ b/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +delete firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl new file mode 100644 index 000000000..f56c61231 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +set firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index cb4ce4072..aa7ac6708 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -32,16 +32,13 @@ from fastapi.responses import HTMLResponse  from fastapi.exceptions import RequestValidationError  from fastapi.routing import APIRoute  from pydantic import BaseModel, StrictStr, validator -from starlette.datastructures import FormData, MutableHeaders +from starlette.datastructures import FormData  from starlette.formparsers import FormParser, MultiPartParser  from multipart.multipart import parse_options_header -from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers  from ariadne.asgi import GraphQL  import vyos.config -import vyos.defaults -  from vyos.configsession import ConfigSession, ConfigSessionError  import api.graphql.state @@ -69,11 +66,11 @@ def load_server_config():      return config  def check_auth(key_list, key): -    id = None +    key_id = None      for k in key_list:          if k['key'] == key: -            id = k['id'] -    return id +            key_id = k['id'] +    return key_id  def error(code, msg):      resp = {"success": False, "error": msg, "data": None} @@ -223,10 +220,10 @@ responses = {  def auth_required(data: ApiModel):      key = data.key      api_keys = app.state.vyos_keys -    id = check_auth(api_keys, key) -    if not id: +    key_id = check_auth(api_keys, key) +    if not key_id:          raise HTTPException(status_code=401, detail="Valid API key is required") -    app.state.vyos_id = id +    app.state.vyos_id = key_id  # override Request and APIRoute classes in order to convert form request to json;  # do all explicit validation here, for backwards compatability of error messages; @@ -613,16 +610,11 @@ def show_op(data: ShowModel):  # GraphQL integration  ### -api.graphql.state.init() - -from api.graphql.graphql.mutations import mutation -from api.graphql.graphql.directives import DataDirective +from api.graphql.bindings import generate_schema -api_schema_dir = vyos.defaults.directories['api_schema'] - -type_defs = load_schema_from_path(api_schema_dir) +api.graphql.state.init() -schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective}) +schema = generate_schema()  app.add_route('/graphql', GraphQL(schema, debug=True)) @@ -640,16 +632,16 @@ if __name__ == '__main__':      try:          server_config = load_server_config() -    except Exception as e: -        logger.critical("Failed to load the HTTP API server config: {0}".format(e)) +    except Exception as err: +        logger.critical(f"Failed to load the HTTP API server config: {err}") -    session = ConfigSession(os.getpid()) +    config_session = ConfigSession(os.getpid()) -    app.state.vyos_session = session +    app.state.vyos_session = config_session      app.state.vyos_keys = server_config['api_keys'] -    app.state.vyos_debug = True if server_config['debug'] == 'true' else False -    app.state.vyos_strict = True if server_config['strict'] == 'true' else False +    app.state.vyos_debug = bool(server_config['debug'] == 'true') +    app.state.vyos_strict = bool(server_config['strict'] == 'true')      api.graphql.state.settings['app'] = app @@ -657,6 +649,6 @@ if __name__ == '__main__':          uvicorn.run(app, host=server_config["listen_address"],                           port=int(server_config["port"]),                           proxy_headers=True) -    except OSError as e: -        logger.critical(f"OSError {e}") +    except OSError as err: +        logger.critical(f"OSError {err}")          sys.exit(1)  | 
