diff options
90 files changed, 712 insertions, 320 deletions
diff --git a/data/templates/accel-ppp/l2tp.config.tmpl b/data/templates/accel-ppp/l2tp.config.tmpl index 070a966b7..a2a2382fa 100644 --- a/data/templates/accel-ppp/l2tp.config.tmpl +++ b/data/templates/accel-ppp/l2tp.config.tmpl @@ -57,6 +57,9 @@ bind={{ outside_addr }} {% if lns_shared_secret %} secret={{ lns_shared_secret }} {% endif %} +{% if lns_host_name %} +host-name={{ lns_host_name }} +{% endif %} [client-ip-range] 0.0.0.0/0 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 c934b7cdb..fcc2846a5 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/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index c96b57fb8..c2b0c2ef9 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -257,16 +257,3 @@ auth {{ hash }} auth-user-pass {{ auth_user_pass_file }} auth-retry nointeract {% endif %} - -{% if openvpn_option is defined and openvpn_option is not none %} -# -# Custom options added by user (not validated) -# -{% for option in openvpn_option %} -{% for argument in option.split('--') %} -{% if argument is defined and argument != '' %} ---{{ argument }} -{% endif %} -{% endfor %} -{% endfor %} -{% endif %} diff --git a/data/templates/openvpn/service-override.conf.tmpl b/data/templates/openvpn/service-override.conf.tmpl new file mode 100644 index 000000000..069bdbd08 --- /dev/null +++ b/data/templates/openvpn/service-override.conf.tmpl @@ -0,0 +1,20 @@ +[Service] +ExecStart= +ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid +{%- if openvpn_option is defined and openvpn_option is not none %} +{% for option in openvpn_option %} +{# Remove the '--' prefix from variable if it is presented #} +{% if option.startswith('--') %} +{% set option = option.split('--', maxsplit=1)[1] %} +{% endif %} +{# Workaround to pass '--push' options properly. Previously openvpn accepted this option without values in double-quotes #} +{# But now it stopped doing this, so we need to add them for compatibility #} +{# HOWEVER! This is a raw option and we do not promise that this or any other trick will work for all the cases. #} +{# Using 'openvpn-option' you take all responsibility for compatibility for yourself. #} +{% if option.startswith('push') and not (option.startswith('push "') and option.endswith('"')) %} +{% set option = 'push \"%s\"'|format(option.split('push ', maxsplit=1)[1]) %} +{% endif %} + --{{ option }} +{%- endfor %} +{% endif %} + diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index c075db898..0c0c203ea 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,3 +1,4 @@ +etc/cron.d etc/dhcp etc/netplug etc/ppp diff --git a/interface-definitions/include/certificate-ca.xml.i b/interface-definitions/include/certificate-ca.xml.i index b97378658..f42e2ebbd 100644 --- a/interface-definitions/include/certificate-ca.xml.i +++ b/interface-definitions/include/certificate-ca.xml.i @@ -7,7 +7,7 @@ <description>File in /config/auth directory</description> </valueHelp> <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> + <validator name="file-exists" argument="--directory /config/auth --error"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/certificate-key.xml.i b/interface-definitions/include/certificate-key.xml.i index 1db9dd069..7d5c2dc03 100644 --- a/interface-definitions/include/certificate-key.xml.i +++ b/interface-definitions/include/certificate-key.xml.i @@ -7,7 +7,7 @@ <description>File in /config/auth directory</description> </valueHelp> <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> + <validator name="file-exists" argument="--directory /config/auth --error"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/certificate.xml.i b/interface-definitions/include/certificate.xml.i index fb5be45cc..3598075a9 100644 --- a/interface-definitions/include/certificate.xml.i +++ b/interface-definitions/include/certificate.xml.i @@ -7,7 +7,7 @@ <description>File in /config/auth directory</description> </valueHelp> <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> + <validator name="file-exists" argument="--directory /config/auth --error"/> </constraint> </properties> </leafNode> 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/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in index 8bcede159..ff3219866 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn_l2tp.xml.in @@ -33,6 +33,11 @@ <help>Tunnel password used to authenticate the client (LAC)</help> </properties> </leafNode> + <leafNode name="host-name"> + <properties> + <help>Sent to the client (LAC) in the Host-Name attribute</help> + </properties> + </leafNode> </children> </node> <leafNode name="ccp-disable"> diff --git a/op-mode-definitions/show-ip-route.xml.in b/op-mode-definitions/show-ip-route.xml.in index 729572b4a..740993693 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 d06b0a842..4ae350634 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -81,6 +81,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 9f01d504d..1834b78bd 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -717,3 +717,19 @@ def is_systemd_service_running(service): Copied from: https://unix.stackexchange.com/a/435317 """ tmp = cmd(f'systemctl show --value -p SubState {service}') return bool((tmp == 'running')) + +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_ha_vrrp.py b/smoketest/scripts/cli/test_ha_vrrp.py index 6121e2da6..751fd105e 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 8a18d8344..692206f63 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 base_interfaces_test import BasicInterfaceTest class GeneveInterfaceTest(BasicInterfaceTest.TestCase): diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py index 1a52a0a5b..24df0af4d 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 3412ebae0..402fb4af5 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 from vyos.util import read_file diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index 0fba0f460..184b411d7 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 d31ec0332..5562a697d 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 diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 0706f234e..75c628244 100644 --- 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_policy.py b/smoketest/scripts/cli/test_policy.py index cdd2ad820..f1d195381 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_policy_local-route.py b/smoketest/scripts/cli/test_policy_local-route.py index c742a930b..627e3da02 100755 --- a/smoketest/scripts/cli/test_policy_local-route.py +++ b/smoketest/scripts/cli/test_policy_local-route.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_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py index 0c4ed86d7..46a2bdcfa 100755 --- a/smoketest/scripts/cli/test_protocols_bfd.py +++ b/smoketest/scripts/cli/test_protocols_bfd.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_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index b261e4164..073064939 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.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_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 482162b0e..8abdd6d37 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 c4eb3fdd8..7a11bcd2a 100755 --- a/smoketest/scripts/cli/test_protocols_ospfv3.py +++ b/smoketest/scripts/cli/test_protocols_ospfv3.py @@ -19,7 +19,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.ifconfig import Section from vyos.util import cmd 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 6f2028f2b..020b981fe 100755 --- a/smoketest/scripts/cli/test_protocols_rip.py +++ b/smoketest/scripts/cli/test_protocols_rip.py @@ -19,7 +19,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.ifconfig import Section from vyos.util import cmd 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 3380dc78b..b360c31d7 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 8212e9469..924d04bb4 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 de9b48de4..d1f8004b9 100755 --- a/smoketest/scripts/cli/test_protocols_static.py +++ b/smoketest/scripts/cli/test_protocols_static.py @@ -23,7 +23,6 @@ from base_vyostest_shim import VyOSUnitTestSHIM from netifaces import interfaces -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.ifconfig import Interface from vyos.ifconfig import Section 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 d8a87ffd4..fd1bd055c 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 8005eb319..44e27828d 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 3ed7655e9..d2e708384 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 b1092c3e5..c6efd1b6c 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 01b875867..6f58ce3d3 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 d47bd452d..d12cc7d58 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 0a86f58b8..894ea73ff 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_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index bf528c8b7..ccac0820d 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -17,14 +17,16 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM - -from vyos.configsession import ConfigSession from vyos.util import process_named_running +from vyos.util import cmd +from os import path, mkdir OCSERV_CONF = '/run/ocserv/ocserv.conf' -base_path = ['vpn', 'openconnect'] -cert = '/etc/ssl/certs/ssl-cert-snakeoil.pem' -cert_key = '/etc/ssl/private/ssl-cert-snakeoil.key' +base_path = ['vpn', 'openconnect'] +cert_dir = '/config/auth/' +ca_cert = f'{cert_dir}ca.crt' +ssl_cert = f'{cert_dir}server.crt' +ssl_key = f'{cert_dir}server.key' class TestVpnOpenconnect(VyOSUnitTestSHIM.TestCase): def tearDown(self): @@ -39,9 +41,9 @@ class TestVpnOpenconnect(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ["authentication", "local-users", "username", user, "password", password]) self.cli_set(base_path + ["authentication", "mode", "local"]) self.cli_set(base_path + ["network-settings", "client-ip-settings", "subnet", "192.0.2.0/24"]) - self.cli_set(base_path + ["ssl", "ca-cert-file", cert]) - self.cli_set(base_path + ["ssl", "cert-file", cert]) - self.cli_set(base_path + ["ssl", "key-file", cert_key]) + self.cli_set(base_path + ["ssl", "ca-cert-file", ca_cert]) + self.cli_set(base_path + ["ssl", "cert-file", ssl_cert]) + self.cli_set(base_path + ["ssl", "key-file", ssl_key]) self.cli_commit() @@ -49,4 +51,21 @@ class TestVpnOpenconnect(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running('ocserv-main')) if __name__ == '__main__': + if not path.exists(cert_dir): + mkdir(cert_dir) + + # Our SSL certificates need a subject ... + subject = '/C=DE/ST=BY/O=VyOS/localityName=Cloud/commonName=vyos/' \ + 'organizationalUnitName=VyOS/emailAddress=maintainers@vyos.io/' + + # Generate mandatory SSL certificate + tmp = f'openssl req -newkey rsa:4096 -new -nodes -x509 -days 3650 '\ + f'-keyout {ssl_key} -out {ssl_cert} -subj {subject}' + cmd(tmp) + + # Generate "CA" + tmp = f'openssl req -new -x509 -key {ssl_key} -out {ca_cert} '\ + f'-subj {subject}' + cmd(tmp) + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_sstp.py b/smoketest/scripts/cli/test_vpn_sstp.py index 033338685..b7b7729b2 100755 --- a/smoketest/scripts/cli/test_vpn_sstp.py +++ b/smoketest/scripts/cli/test_vpn_sstp.py @@ -18,10 +18,12 @@ import unittest from base_accel_ppp_test import BasicAccelPPPTest from vyos.util import cmd +from os import path, mkdir -ca_cert = '/tmp/ca.crt' -ssl_cert = '/tmp/server.crt' -ssl_key = '/tmp/server.key' +cert_dir = '/config/auth/' +ca_cert = f'{cert_dir}ca.crt' +ssl_cert = f'{cert_dir}server.crt' +ssl_key = f'{cert_dir}server.key' class TestVPNSSTPServer(BasicAccelPPPTest.TestCase): def setUp(self): @@ -41,6 +43,9 @@ class TestVPNSSTPServer(BasicAccelPPPTest.TestCase): super().basic_config() if __name__ == '__main__': + if not path.exists(cert_dir): + mkdir(cert_dir) + # Our SSL certificates need a subject ... subject = '/C=DE/ST=BY/O=VyOS/localityName=Cloud/commonName=vyos/' \ 'organizationalUnitName=VyOS/emailAddress=maintainers@vyos.io/' diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index 01d2e8c39..0f006ca3c 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -23,7 +23,6 @@ from base_vyostest_shim import VyOSUnitTestSHIM from netifaces import interfaces -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.ifconfig import Interface from vyos.ifconfig import Section diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 5d537dadf..ae35ed3c4 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -51,6 +51,7 @@ user = 'openvpn' group = 'openvpn' cfg_file = '/run/openvpn/{ifname}.conf' +service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf' def checkCertHeader(header, filename): """ @@ -434,6 +435,11 @@ def generate(openvpn): if os.path.isdir(ccd_dir): rmtree(ccd_dir, ignore_errors=True) + # Remove systemd directories with overrides + service_dir = os.path.dirname(service_file.format(**openvpn)) + if os.path.isdir(service_dir): + rmtree(service_dir, ignore_errors=True) + if 'deleted' in openvpn or 'disable' in openvpn: return None @@ -477,14 +483,20 @@ def generate(openvpn): render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn, formater=lambda _: _.replace(""", '"'), user=user, group=group) + # Render 20-override.conf for OpenVPN service + render(service_file.format(**openvpn), 'openvpn/service-override.conf.tmpl', openvpn, + formater=lambda _: _.replace(""", '"'), user=user, group=group) + # Reload systemd services config to apply an override + call(f'systemctl daemon-reload') + return None 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) @@ -496,7 +508,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') conf = VTunIf.get_config() conf['device_type'] = openvpn['device_type'] diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index 31c599145..cb46b3723 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.template import render from vyos import ConfigError from vyos import airbag @@ -89,7 +91,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 3990e5735..0fbe90cce 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' @@ -401,19 +405,20 @@ def verify(snmp): addr = listen[0] port = listen[1] + tmp = None if is_ipv4(addr): # example: udp:127.0.0.1:161 - listen = 'udp:' + addr + ':' + port + tmp = f'udp:{addr}:{port}' elif snmp['ipv6_enabled']: # example: udp6:[::1]:161 - listen = 'udp6:' + '[' + addr + ']' + ':' + port + tmp = f'udp6:[{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 a40d932e0..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/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index e970d2ef5..86aa9af09 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -291,6 +291,8 @@ def get_config(config=None): # LNS secret if conf.exists(['lns', 'shared-secret']): l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret']) + if conf.exists(['lns', 'host-name']): + l2tp['lns_host_name'] = conf.return_value(['lns', 'host-name']) if conf.exists(['ccp-disable']): l2tp['ccp_disable'] = True diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 070009722..215dfb37a 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -56,24 +56,13 @@ def verify(sstp): # # SSL certificate checks # - tmp = dict_search('ssl.ca_cert_file', sstp) - if tmp: - if not os.path.isfile(tmp): - raise ConfigError(f'SSL CA certificate "{tmp}" does not exist!') - tmp = dict_search('ssl.cert_file', sstp) if not tmp: raise ConfigError(f'SSL public key file required!') - else: - if not os.path.isfile(tmp): - raise ConfigError(f'SSL public key "{tmp}" does not exist!') tmp = dict_search('ssl.key_file', sstp) if not tmp: raise ConfigError(f'SSL private key file required!') - else: - if not os.path.isfile(tmp): - raise ConfigError(f'SSL private key "{tmp}" does not exist!') def generate(sstp): if not sstp: 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/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/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/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 aef2d8060..281d25e30 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -85,10 +85,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/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py index 503366dd8..beb632fa8 100755 --- a/src/op_mode/show_ipsec_sa.py +++ b/src/op_mode/show_ipsec_sa.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -29,35 +29,22 @@ def convert(text): def alphanum_key(key): return [convert(c) for c in re.split('([0-9]+)', str(key))] -try: - session = vici.Session() - sas = session.list_sas() -except PermissionError: - print("You do not have a permission to connect to the IPsec daemon") - sys.exit(1) -except ConnectionRefusedError: - print("IPsec is not runing") - sys.exit(1) -except Exception as e: - print("An error occured: {0}".format(e)) - sys.exit(1) - -sa_data = [] - -for sa in sas: - # list_sas() returns a list of single-item dicts - for peer in sa: - parent_sa = sa[peer] - child_sas = parent_sa["child-sas"] - installed_sas = {k: v for k, v in child_sas.items() if v["state"] == b"INSTALLED"} - - # parent_sa["state"] = IKE state, child_sas["state"] = ESP state +def format_output(conns, sas): + sa_data = [] + + for peer, parent_conn in conns.items(): + if peer not in sas: + continue + + parent_sa = sas[peer] + child_sas = parent_sa['child-sas'] + installed_sas = {v['name'].decode(): v for k, v in child_sas.items() if v["state"] == b"INSTALLED"} + + state = 'down' + uptime = 'N/A' + if parent_sa["state"] == b"ESTABLISHED" and installed_sas: state = "up" - else: - state = "down" - - uptime = "N/A" remote_host = parent_sa["remote-host"].decode() remote_id = parent_sa["remote-id"].decode() @@ -66,53 +53,79 @@ for sa in sas: remote_id = "N/A" # The counters can only be obtained from the child SAs - if not installed_sas: - data = [peer, state, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"] - sa_data.append(data) - else: - for csa in installed_sas: - isa = installed_sas[csa] - csa_name = isa['name'] - csa_name = csa_name.decode() - - bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode())) - bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode())) - bytes_str = "{0}/{1}".format(bytes_in, bytes_out) - - pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si) - pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si) - pkts_str = "{0}/{1}".format(pkts_in, pkts_out) - # Remove B from <1K values - pkts_str = re.sub(r'B', r'', pkts_str) - - uptime = vyos.util.seconds_to_human(isa['install-time'].decode()) - - enc = isa["encr-alg"].decode() - if "encr-keysize" in isa: - key_size = isa["encr-keysize"].decode() - else: - key_size = "" - if "integ-alg" in isa: - hash = isa["integ-alg"].decode() - else: - hash = "" - if "dh-group" in isa: - dh_group = isa["dh-group"].decode() - else: - dh_group = "" - - proposal = enc - if key_size: - proposal = "{0}_{1}".format(proposal, key_size) - if hash: - proposal = "{0}/{1}".format(proposal, hash) - if dh_group: - proposal = "{0}/{1}".format(proposal, dh_group) - - data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal] + for child_conn in parent_conn['children']: + if child_conn not in installed_sas: + data = [child_conn, "down", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"] sa_data.append(data) - -headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"] -sa_data = sorted(sa_data, key=alphanum_key) -output = tabulate.tabulate(sa_data, headers) -print(output) + continue + + isa = installed_sas[child_conn] + csa_name = isa['name'] + csa_name = csa_name.decode() + + bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode())) + bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode())) + bytes_str = "{0}/{1}".format(bytes_in, bytes_out) + + pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si) + pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si) + pkts_str = "{0}/{1}".format(pkts_in, pkts_out) + # Remove B from <1K values + pkts_str = re.sub(r'B', r'', pkts_str) + + uptime = vyos.util.seconds_to_human(isa['install-time'].decode()) + + enc = isa["encr-alg"].decode() + if "encr-keysize" in isa: + key_size = isa["encr-keysize"].decode() + else: + key_size = "" + if "integ-alg" in isa: + hash = isa["integ-alg"].decode() + else: + hash = "" + if "dh-group" in isa: + dh_group = isa["dh-group"].decode() + else: + dh_group = "" + + proposal = enc + if key_size: + proposal = "{0}_{1}".format(proposal, key_size) + if hash: + proposal = "{0}/{1}".format(proposal, hash) + if dh_group: + proposal = "{0}/{1}".format(proposal, dh_group) + + data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal] + sa_data.append(data) + return sa_data + +if __name__ == '__main__': + try: + session = vici.Session() + conns = {} + sas = {} + + for conn in session.list_conns(): + for key in conn: + conns[key] = conn[key] + + for sa in session.list_sas(): + for key in sa: + sas[key] = sa[key] + + headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"] + sa_data = format_output(conns, sas) + sa_data = sorted(sa_data, key=alphanum_key) + output = tabulate.tabulate(sa_data, headers) + print(output) + except PermissionError: + print("You do not have a permission to connect to the IPsec daemon") + sys.exit(1) + except ConnectionRefusedError: + print("IPsec is not runing") + sys.exit(1) + except Exception as e: + print("An error occured: {0}".format(e)) + sys.exit(1) 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) |