summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Jenkinsfile14
-rw-r--r--Makefile21
-rw-r--r--data/interface-types.json19
-rw-r--r--data/templates/bcast-relay/udp-broadcast-relay.tmpl7
-rw-r--r--data/templates/dhcp-relay/config.tmpl17
-rw-r--r--data/templates/dhcp-server/daemon.tmpl8
-rw-r--r--data/templates/dhcp-server/dhcpd.conf.tmpl195
-rw-r--r--data/templates/dhcpv6-relay/config.tmpl4
-rw-r--r--data/templates/dhcpv6-server/daemon.tmpl8
-rw-r--r--data/templates/dhcpv6-server/dhcpdv6.conf.tmpl78
-rw-r--r--data/templates/dns-forwarding/recursor.conf.tmpl44
-rw-r--r--data/templates/dynamic-dns/ddclient.conf.tmpl49
-rw-r--r--data/templates/frr-bfd/bfd.frr.tmpl16
-rw-r--r--data/templates/https/nginx.default.tmpl69
-rw-r--r--data/templates/igmp-proxy/igmpproxy.conf.tmpl37
-rw-r--r--data/templates/igmp/igmp.frr.tmpl41
-rw-r--r--data/templates/ipoe-server/chap-secrets.tmpl18
-rw-r--r--data/templates/ipoe-server/ipoe.config.tmpl123
-rw-r--r--data/templates/ipsec/ipsec.conf.tmpl3
-rw-r--r--data/templates/ipsec/ipsec.secrets.tmpl7
-rw-r--r--data/templates/ipsec/remote-access.tmpl28
-rw-r--r--data/templates/l2tp/chap-secrets.tmpl10
-rw-r--r--data/templates/l2tp/l2tp.config.tmpl173
-rw-r--r--data/templates/lldp/lldpd.tmpl3
-rw-r--r--data/templates/lldp/vyos.conf.tmpl20
-rw-r--r--data/templates/mdns-repeater/mdns-repeater.tmpl2
-rw-r--r--data/templates/mpls/ldpd.frr.tmpl58
-rw-r--r--data/templates/netflow/uacctd.conf.tmpl69
-rw-r--r--data/templates/ntp/ntp.conf.tmpl38
-rw-r--r--data/templates/openvpn/client.conf.tmpl16
-rw-r--r--data/templates/openvpn/server.conf.tmpl236
-rw-r--r--data/templates/pim/pimd.frr.tmpl34
-rw-r--r--data/templates/pppoe-server/chap-secrets.tmpl10
-rw-r--r--data/templates/pppoe-server/pppoe.config.tmpl228
-rw-r--r--data/templates/pppoe/ip-down.script.tmpl28
-rw-r--r--data/templates/pppoe/ip-pre-up.script.tmpl19
-rw-r--r--data/templates/pppoe/ip-up.script.tmpl47
-rw-r--r--data/templates/pppoe/ipv6-up.script.tmpl41
-rw-r--r--data/templates/pppoe/peer.tmpl72
-rw-r--r--data/templates/pptp/chap-secrets.tmpl6
-rw-r--r--data/templates/pptp/pptp.config.tmpl87
-rw-r--r--data/templates/router-advert/radvd.conf.tmpl37
-rw-r--r--data/templates/salt-minion/minion.tmpl63
-rw-r--r--data/templates/snmp/etc.snmp.conf.tmpl4
-rw-r--r--data/templates/snmp/etc.snmpd.conf.tmpl118
-rw-r--r--data/templates/snmp/usr.snmpd.conf.tmpl6
-rw-r--r--data/templates/snmp/var.snmpd.conf.tmpl16
-rw-r--r--data/templates/ssh/sshd_config.tmpl125
-rw-r--r--data/templates/sstp/chap-secrets.tmpl10
-rw-r--r--data/templates/sstp/sstp.config.tmpl114
-rw-r--r--data/templates/syslog/logrotate.tmpl12
-rw-r--r--data/templates/syslog/rsyslog.conf.tmpl44
-rw-r--r--data/templates/system-login/pam_radius_auth.conf.tmpl13
-rw-r--r--data/templates/tftp-server/default.tmpl2
-rw-r--r--data/templates/vrf/vrf.conf.tmpl8
-rw-r--r--data/templates/vrrp/daemon.tmpl5
-rw-r--r--data/templates/vrrp/keepalived.conf.tmpl97
-rw-r--r--data/templates/wifi/cfg80211.conf.tmpl3
-rw-r--r--data/templates/wifi/crda.tmpl3
-rw-r--r--data/templates/wifi/hostapd.conf.tmpl728
-rw-r--r--data/templates/wifi/wpa_supplicant.conf.tmpl9
-rw-r--r--data/templates/wwan/chat.tmpl6
-rw-r--r--data/templates/wwan/ip-down.script.tmpl26
-rw-r--r--data/templates/wwan/ip-pre-up.script.tmpl23
-rw-r--r--data/templates/wwan/ip-up.script.tmpl25
-rw-r--r--data/templates/wwan/peer.tmpl30
-rw-r--r--debian/changelog6
-rw-r--r--debian/control5
-rw-r--r--debian/vyos-1x.install3
-rw-r--r--interface-definitions/flow-accounting-conf.xml.in2
-rw-r--r--interface-definitions/https.xml.in59
-rw-r--r--interface-definitions/include/interface-description.xml.i4
-rw-r--r--interface-definitions/include/interface-mtu-64-8024.xml.i13
-rw-r--r--interface-definitions/include/interface-mtu-68-1500.xml.i13
-rw-r--r--interface-definitions/include/interface-vrf.xml.i12
-rw-r--r--interface-definitions/include/ipv6-address.xml.i22
-rw-r--r--interface-definitions/include/ipv6-disable-forwarding.xml.i6
-rw-r--r--interface-definitions/include/ipv6-dup-addr-detect-transmits.xml.i16
-rw-r--r--interface-definitions/include/port-number.xml.i12
-rw-r--r--interface-definitions/include/radius-server.xml.i56
-rw-r--r--interface-definitions/include/vif-s.xml.i1
-rw-r--r--interface-definitions/include/vif.xml.i8
-rw-r--r--interface-definitions/interfaces-bonding.xml.in10
-rw-r--r--interface-definitions/interfaces-bridge.xml.in12
-rw-r--r--interface-definitions/interfaces-dummy.xml.in3
-rw-r--r--interface-definitions/interfaces-ethernet.xml.in10
-rw-r--r--interface-definitions/interfaces-geneve.xml.in2
-rw-r--r--interface-definitions/interfaces-l2tpv3.xml.in11
-rw-r--r--interface-definitions/interfaces-loopback.xml.in2
-rw-r--r--interface-definitions/interfaces-openvpn.xml.in21
-rw-r--r--interface-definitions/interfaces-pppoe.xml.in6
-rw-r--r--interface-definitions/interfaces-pseudo-ethernet.xml.in14
-rw-r--r--interface-definitions/interfaces-tunnel.xml.in280
-rw-r--r--interface-definitions/interfaces-vxlan.xml.in25
-rw-r--r--interface-definitions/interfaces-wireguard.xml.in34
-rw-r--r--interface-definitions/interfaces-wireless.xml.in53
-rw-r--r--interface-definitions/interfaces-wirelessmodem.xml.in81
-rw-r--r--interface-definitions/protocols-igmp.xml.in88
-rw-r--r--interface-definitions/protocols-mpls.xml.in98
-rw-r--r--interface-definitions/protocols-pim.xml.in96
-rw-r--r--interface-definitions/salt-minion.xml.in85
-rw-r--r--interface-definitions/service-router-advert.xml.in266
-rw-r--r--interface-definitions/system-login.xml.in49
-rw-r--r--interface-definitions/system-syslog.xml.in14
-rw-r--r--interface-definitions/vpn-l2tp.xml.in (renamed from interface-definitions/l2tp-server.xml.in)0
-rw-r--r--interface-definitions/vpn-sstp.xml.in (renamed from interface-definitions/sstp.xml.in)214
-rw-r--r--interface-definitions/vrf.xml.in47
-rw-r--r--interface-definitions/vrrp.xml.in39
-rw-r--r--op-mode-definitions/reset-ip-igmp.xml24
-rw-r--r--op-mode-definitions/reset-ip-multicast.xml24
-rw-r--r--op-mode-definitions/show-interfaces-pppoe.xml9
-rw-r--r--op-mode-definitions/show-interfaces-wirelessmodem.xml45
-rw-r--r--op-mode-definitions/show-ip-igmp.xml48
-rw-r--r--op-mode-definitions/show-ip-multicast.xml12
-rw-r--r--op-mode-definitions/show-ip-pim.xml72
-rw-r--r--op-mode-definitions/show-mpls.xml62
-rw-r--r--op-mode-definitions/show-ntp.xml4
-rw-r--r--op-mode-definitions/show-raid.xml2
-rw-r--r--op-mode-definitions/show-system-info.xml7
-rw-r--r--op-mode-definitions/show-systemintegrity.xml14
-rw-r--r--op-mode-definitions/show-vrf.xml22
-rw-r--r--op-mode-definitions/traceroute.xml40
-rw-r--r--python/setup.py2
-rw-r--r--python/vyos/__init__.py2
-rw-r--r--python/vyos/airbag.py169
-rw-r--r--python/vyos/authutils.py8
-rw-r--r--python/vyos/config.py15
-rw-r--r--python/vyos/configdict.py27
-rw-r--r--python/vyos/configsession.py8
-rw-r--r--python/vyos/configtree.py62
-rw-r--r--python/vyos/ifconfig.py1920
-rw-r--r--python/vyos/ifconfig/__init__.py39
-rw-r--r--python/vyos/ifconfig/afi.py19
-rw-r--r--python/vyos/ifconfig/bond.py279
-rw-r--r--python/vyos/ifconfig/bridge.py189
-rw-r--r--python/vyos/ifconfig/control.py154
-rw-r--r--python/vyos/ifconfig/dhcp.py268
-rw-r--r--python/vyos/ifconfig/dummy.py37
-rw-r--r--python/vyos/ifconfig/ethernet.py257
-rw-r--r--python/vyos/ifconfig/geneve.py64
-rw-r--r--python/vyos/ifconfig/interface.py738
-rw-r--r--python/vyos/ifconfig/l2tpv3.py113
-rw-r--r--python/vyos/ifconfig/loopback.py58
-rw-r--r--python/vyos/ifconfig/macvlan.py67
-rw-r--r--python/vyos/ifconfig/pppoe.py33
-rw-r--r--python/vyos/ifconfig/register.py95
-rw-r--r--python/vyos/ifconfig/stp.py70
-rw-r--r--python/vyos/ifconfig/tunnel.py324
-rw-r--r--python/vyos/ifconfig/vlan.py142
-rw-r--r--python/vyos/ifconfig/vtun.py34
-rw-r--r--python/vyos/ifconfig/vxlan.py106
-rw-r--r--python/vyos/ifconfig/wireguard.py222
-rw-r--r--python/vyos/ifconfig/wireless.py82
-rw-r--r--python/vyos/ifconfig_vlan.py64
-rw-r--r--python/vyos/interfaces.py99
-rw-r--r--python/vyos/ioctl.py8
-rw-r--r--python/vyos/migrator.py11
-rw-r--r--python/vyos/remote.py29
-rw-r--r--python/vyos/util.py205
-rw-r--r--python/vyos/validate.py134
-rw-r--r--python/vyos/version.py15
-rwxr-xr-xscripts/build-command-op-templates2
-rwxr-xr-xscripts/build-command-templates5
-rwxr-xr-xsrc/completion/list_dumpable_interfaces.py8
-rwxr-xr-xsrc/completion/list_interfaces.py34
-rwxr-xr-xsrc/completion/list_local.py24
-rwxr-xr-xsrc/completion/list_openvpn_clients.py4
-rwxr-xr-xsrc/completion/list_wlm_peers.sh6
-rwxr-xr-xsrc/conf_mode/accel_l2tp.py840
-rwxr-xr-xsrc/conf_mode/accel_sstp.py469
-rwxr-xr-xsrc/conf_mode/arp.py7
-rwxr-xr-xsrc/conf_mode/bcast_relay.py64
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py71
-rwxr-xr-xsrc/conf_mode/dhcp_server.py290
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py37
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py128
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py138
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py84
-rwxr-xr-xsrc/conf_mode/firewall_options.py28
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py155
-rwxr-xr-xsrc/conf_mode/host_name.py21
-rwxr-xr-xsrc/conf_mode/http-api.py13
-rwxr-xr-xsrc/conf_mode/https.py144
-rwxr-xr-xsrc/conf_mode/igmp_proxy.py98
-rwxr-xr-xsrc/conf_mode/intel_qat.py24
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py100
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py63
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py38
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py81
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py25
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py100
-rwxr-xr-xsrc/conf_mode/interfaces-loopback.py8
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py417
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py211
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py128
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py646
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py81
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py433
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py1038
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py237
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py157
-rwxr-xr-xsrc/conf_mode/le_cert.py29
-rwxr-xr-xsrc/conf_mode/lldp.py102
-rwxr-xr-xsrc/conf_mode/mdns_repeater.py50
-rwxr-xr-xsrc/conf_mode/ntp.py78
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py62
-rwxr-xr-xsrc/conf_mode/protocols_igmp.py121
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py178
-rwxr-xr-xsrc/conf_mode/protocols_pim.py148
-rwxr-xr-xsrc/conf_mode/salt-minion.py144
-rwxr-xr-xsrc/conf_mode/service-ipoe.py218
-rwxr-xr-xsrc/conf_mode/service-pppoe.py313
-rwxr-xr-xsrc/conf_mode/service-router-advert.py178
-rwxr-xr-xsrc/conf_mode/snmp.py225
-rwxr-xr-xsrc/conf_mode/ssh.py155
-rwxr-xr-xsrc/conf_mode/system-ip.py4
-rwxr-xr-xsrc/conf_mode/system-ipv6.py4
-rwxr-xr-xsrc/conf_mode/system-login-banner.py28
-rwxr-xr-xsrc/conf_mode/system-login.py102
-rwxr-xr-xsrc/conf_mode/system-options.py5
-rwxr-xr-xsrc/conf_mode/system-syslog.py96
-rwxr-xr-xsrc/conf_mode/system-timezone.py6
-rwxr-xr-xsrc/conf_mode/system-wifi-regdom.py41
-rwxr-xr-xsrc/conf_mode/tftp_server.py66
-rwxr-xr-xsrc/conf_mode/vpn-pptp.py168
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py402
-rwxr-xr-xsrc/conf_mode/vrf.py276
-rwxr-xr-xsrc/conf_mode/vrrp.py203
-rwxr-xr-xsrc/conf_mode/vyos_cert.py30
-rwxr-xr-xsrc/etc/init.d/isc-dhcpv4-server4
-rwxr-xr-xsrc/etc/init.d/isc-dhcpv6-relay4
-rwxr-xr-xsrc/etc/init.d/isc-dhcpv6-server4
-rwxr-xr-xsrc/etc/ppp/ip-pre-up51
-rw-r--r--src/etc/rsyslog.d/01-auth.conf14
-rw-r--r--src/etc/udev/rules.d/99-vyos-wwan.rules11
-rwxr-xr-xsrc/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py20
-rwxr-xr-xsrc/helpers/run-config-migration.py29
-rwxr-xr-xsrc/helpers/validate-value.py4
-rwxr-xr-xsrc/helpers/vyos-boot-config-loader.py24
-rwxr-xr-xsrc/helpers/vyos-bridge-sync.py14
-rwxr-xr-xsrc/helpers/vyos-load-config.py22
-rwxr-xr-xsrc/helpers/vyos-merge-config.py27
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/1-to-210
-rwxr-xr-xsrc/migration-scripts/https/0-to-169
-rwxr-xr-xsrc/migration-scripts/https/1-to-254
-rwxr-xr-xsrc/migration-scripts/interfaces/5-to-6119
-rwxr-xr-xsrc/migration-scripts/interfaces/6-to-763
-rwxr-xr-xsrc/migration-scripts/interfaces/7-to-859
-rwxr-xr-xsrc/migration-scripts/interfaces/8-to-952
-rwxr-xr-xsrc/migration-scripts/quagga/3-to-425
-rwxr-xr-xsrc/migration-scripts/quagga/4-to-563
-rwxr-xr-xsrc/migration-scripts/quagga/5-to-663
-rwxr-xr-xsrc/migration-scripts/sstp/0-to-1130
-rwxr-xr-xsrc/migration-scripts/sstp/1-to-2110
-rwxr-xr-xsrc/migration-scripts/system/12-to-1313
-rwxr-xr-xsrc/op_mode/clear_conntrack.py6
-rwxr-xr-xsrc/op_mode/connect_disconnect.py9
-rwxr-xr-xsrc/op_mode/dns_forwarding_reset.py21
-rwxr-xr-xsrc/op_mode/dns_forwarding_statistics.py6
-rwxr-xr-xsrc/op_mode/dynamic_dns.py6
-rwxr-xr-xsrc/op_mode/flow_accounting_op.py46
-rwxr-xr-xsrc/op_mode/format_disk.py19
-rwxr-xr-xsrc/op_mode/generate_ssh_server_key.py10
-rwxr-xr-xsrc/op_mode/lldp_op.py14
-rwxr-xr-xsrc/op_mode/powerctrl.py56
-rwxr-xr-xsrc/op_mode/reset_openvpn.py38
-rwxr-xr-xsrc/op_mode/reset_vpn.py46
-rwxr-xr-xsrc/op_mode/restart_dhcp_relay.py6
-rwxr-xr-xsrc/op_mode/restart_frr.py11
-rwxr-xr-xsrc/op_mode/show_acceleration.py32
-rwxr-xr-xsrc/op_mode/show_dhcp.py7
-rwxr-xr-xsrc/op_mode/show_dhcpv6.py6
-rwxr-xr-xsrc/op_mode/show_openvpn.py4
-rwxr-xr-xsrc/op_mode/show_vpn_ra.py18
-rwxr-xr-xsrc/op_mode/show_vrf.py67
-rwxr-xr-xsrc/op_mode/show_wireless.py12
-rwxr-xr-xsrc/op_mode/snmp.py3
-rwxr-xr-xsrc/op_mode/snmp_ifmib.py33
-rwxr-xr-xsrc/op_mode/system_integrity.py5
-rwxr-xr-xsrc/op_mode/version.py17
-rwxr-xr-xsrc/op_mode/vrrp.py23
-rwxr-xr-xsrc/op_mode/wireguard.py25
-rw-r--r--src/pam-configs/radius6
-rwxr-xr-xsrc/services/vyos-http-api-server43
-rwxr-xr-xsrc/system/keepalived-fifo.py188
-rwxr-xr-xsrc/system/vrrp-script-wrapper.py64
-rwxr-xr-xsrc/validators/fqdn27
-rwxr-xr-xsrc/validators/mac-address18
-rwxr-xr-xsrc/validators/timezone8
-rwxr-xr-xsrc/validators/vrf-name40
-rw-r--r--tests/data/config.valid4
291 files changed, 15744 insertions, 8028 deletions
diff --git a/Jenkinsfile b/Jenkinsfile
index ed98477f2..7a79b0f43 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -36,7 +36,7 @@ def isCustomBuild() {
def gitURI = 'git@github.com:vyos/' + getGitRepoName()
def httpURI = 'https://github.com/vyos/' + getGitRepoName()
- return ! ((getGitRepoURL() == gitURI) || (getGitRepoURL() == httpURI))
+ return !((getGitRepoURL() == gitURI) || (getGitRepoURL() == httpURI)) || env.CHANGE_ID
}
def setDescription() {
@@ -74,7 +74,13 @@ node('Docker') {
script {
// create container name on demand
def branchName = getGitBranchName()
- if (branchName == "master") {
+ // Adjust PR target branch name so we can re-map it to the proper
+ // Docker image. CHANGE_ID is set only for pull requests, so it is
+ // safe to access the pullRequest global variable
+ if (env.CHANGE_ID) {
+ branchName = "${env.CHANGE_TARGET}".toLowerCase()
+ }
+ if (branchName.equals("master")) {
branchName = "current"
}
env.DOCKER_IMAGE = "vyos/vyos-build:" + branchName
@@ -92,7 +98,6 @@ pipeline {
}
options {
disableConcurrentBuilds()
- skipDefaultCheckout()
timeout(time: 30, unit: 'MINUTES')
timestamps()
}
@@ -101,8 +106,7 @@ pipeline {
steps {
script {
dir('build') {
- git branch: getGitBranchName(),
- url: getGitRepoURL()
+ checkout scm
}
}
}
diff --git a/Makefile b/Makefile
index f859f2f45..62bcd473f 100644
--- a/Makefile
+++ b/Makefile
@@ -36,22 +36,41 @@ interface_definitions: $(BUILD_DIR) $(obj)
rm -f $(TMPL_DIR)/firewall/node.def
rm -f $(TMPL_DIR)/interfaces/node.def
rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif-s/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif-s/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ipv6/node.def
+ rm -f $(TMPL_DIR)/interfaces/l2tpv3/node.tag/ipv6/node.def
+ rm -f $(TMPL_DIR)/interfaces/openvpn/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/pppoe/node.tag/ip/node.def
rm -f $(TMPL_DIR)/interfaces/pppoe/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ipv6/node.def
+ rm -f $(TMPL_DIR)/interfaces/tunnel/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/vxlan/node.tag/ip/node.def
- rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/vxlan/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/ipv6/node.def
+ rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ipv6/node.def
+ rm -f $(TMPL_DIR)/interfaces/wirelessmodem/node.tag/ipv6/node.def
rm -f $(TMPL_DIR)/protocols/node.def
rm -f $(TMPL_DIR)/protocols/static/node.def
rm -f $(TMPL_DIR)/system/node.def
diff --git a/data/interface-types.json b/data/interface-types.json
deleted file mode 100644
index f174d3c39..000000000
--- a/data/interface-types.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "loopback": "lo",
- "dummy": "dum",
- "ethernet": "eth",
- "bonding": "bond",
- "bridge": "br",
- "pseudo-ethernet": "peth",
- "openvpn": "vtun",
- "tunnel": "tun",
- "vti": "vti",
- "l2tpv3": "l2tpeth",
- "vxlan": "vxlan",
- "wireguard": "wg",
- "wireless": "wlan",
- "wirelessmodem": "wlm",
- "input": "ifb",
- "pppoe": "pppoe",
- "geneve": "gnv"
-}
diff --git a/data/templates/bcast-relay/udp-broadcast-relay.tmpl b/data/templates/bcast-relay/udp-broadcast-relay.tmpl
new file mode 100644
index 000000000..3d8c3fe94
--- /dev/null
+++ b/data/templates/bcast-relay/udp-broadcast-relay.tmpl
@@ -0,0 +1,7 @@
+### Autogenerated by bcast_relay.py ###
+
+# UDP broadcast relay configuration for instance {{ id }}
+{%- if description %}
+# Comment: {{ description }}
+{% endif %}
+DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}"
diff --git a/data/templates/dhcp-relay/config.tmpl b/data/templates/dhcp-relay/config.tmpl
new file mode 100644
index 000000000..7203ae9fb
--- /dev/null
+++ b/data/templates/dhcp-relay/config.tmpl
@@ -0,0 +1,17 @@
+### Autogenerated by dhcp_relay.py ###
+
+# Defaults for isc-dhcp-relay initscript
+# sourced by /etc/init.d/isc-dhcp-relay
+
+#
+# This is a POSIX shell fragment
+#
+
+# What servers should the DHCP relay forward requests to?
+SERVERS="{{ server | join(' ') }}"
+
+# On what interfaces should the DHCP relay (dhrelay) serve DHCP requests?
+INTERFACES="{{ interface | join(' ') }}"
+
+# Additional options that are passed to the DHCP relay daemon?
+OPTIONS="-4 {{ options | join(' ') }}"
diff --git a/data/templates/dhcp-server/daemon.tmpl b/data/templates/dhcp-server/daemon.tmpl
new file mode 100644
index 000000000..f88032d38
--- /dev/null
+++ b/data/templates/dhcp-server/daemon.tmpl
@@ -0,0 +1,8 @@
+### Autogenerated by dhcp_server.py ###
+
+# sourced by /etc/init.d/isc-dhcpv4-server
+
+DHCPD_CONF={{ config_file }}
+DHCPD_PID={{ pid_file }}
+OPTIONS="-4 -lf {{ lease_file }}"
+INTERFACES=""
diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl
new file mode 100644
index 000000000..5f5129451
--- /dev/null
+++ b/data/templates/dhcp-server/dhcpd.conf.tmpl
@@ -0,0 +1,195 @@
+
+### Autogenerated by dhcp_server.py ###
+
+# For options please consult the following website:
+# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
+#
+# log-facility local7;
+
+{% if hostfile_update %}
+on release {
+ set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
+ set ClientIp = binary-to-ascii(10, 8, ".",leased-address);
+ set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6));
+ set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
+ execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain);
+}
+
+on expiry {
+ set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
+ set ClientIp = binary-to-ascii(10, 8, ".",leased-address);
+ set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6));
+ set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
+ execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain);
+}
+{% endif %}
+{%- if host_decl_name %}
+use-host-decl-names on;
+{%- endif %}
+ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %};
+{% if static_route -%}
+option rfc3442-static-route code 121 = array of integer 8;
+option windows-static-route code 249 = array of integer 8;
+{%- endif %}
+{% if wpad -%}
+option wpad-url code 252 = text;
+{% endif %}
+
+{%- if global_parameters %}
+# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated
+{%- for param in global_parameters %}
+{{ param }}
+{%- endfor -%}
+{%- endif %}
+
+# Failover configuration
+{% for network in shared_network %}
+{%- if not network.disabled -%}
+{%- for subnet in network.subnet %}
+{%- if subnet.failover_name -%}
+failover peer "{{ subnet.failover_name }}" {
+{%- if subnet.failover_status == 'primary' %}
+ primary;
+ mclt 1800;
+ split 128;
+{%- elif subnet.failover_status == 'secondary' %}
+ secondary;
+{%- endif %}
+ address {{ subnet.failover_local_addr }};
+ port 520;
+ peer address {{ subnet.failover_peer_addr }};
+ peer port 520;
+ max-response-delay 30;
+ max-unacked-updates 10;
+ load balance max seconds 3;
+}
+{% endif -%}
+{% endfor -%}
+{% endif -%}
+{% endfor %}
+
+# Shared network configration(s)
+{% for network in shared_network %}
+{%- if not network.disabled -%}
+shared-network {{ network.name }} {
+ {%- if network.authoritative %}
+ authoritative;
+ {%- endif %}
+ {%- if network.network_parameters %}
+ # The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated
+ {%- for param in network.network_parameters %}
+ {{ param }}
+ {%- endfor %}
+ {%- endif %}
+ {%- for subnet in network.subnet %}
+ subnet {{ subnet.address }} netmask {{ subnet.netmask }} {
+ {%- if subnet.dns_server %}
+ option domain-name-servers {{ subnet.dns_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.domain_search %}
+ option domain-search {{ subnet.domain_search | join(', ') }};
+ {%- endif %}
+ {%- if subnet.ntp_server %}
+ option ntp-servers {{ subnet.ntp_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.pop_server %}
+ option pop-server {{ subnet.pop_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.smtp_server %}
+ option smtp-server {{ subnet.smtp_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.time_server %}
+ option time-servers {{ subnet.time_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.wins_server %}
+ option netbios-name-servers {{ subnet.wins_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.static_route %}
+ option rfc3442-static-route {{ subnet.static_route }}{% if subnet.rfc3442_default_router %}, {{ subnet.rfc3442_default_router }}{% endif %};
+ option windows-static-route {{ subnet.static_route }};
+ {%- endif %}
+ {%- if subnet.ip_forwarding %}
+ option ip-forwarding true;
+ {%- endif -%}
+ {%- if subnet.default_router %}
+ option routers {{ subnet.default_router }};
+ {%- endif -%}
+ {%- if subnet.server_identifier %}
+ option dhcp-server-identifier {{ subnet.server_identifier }};
+ {%- endif -%}
+ {%- if subnet.domain_name %}
+ option domain-name "{{ subnet.domain_name }}";
+ {%- endif -%}
+ {%- if subnet.subnet_parameters %}
+ # The following {{ subnet.subnet_parameters | length }} line(s) were added as subnet-parameters in the CLI and have not been validated
+ {%- for param in subnet.subnet_parameters %}
+ {{ param }}
+ {%- endfor -%}
+ {%- endif %}
+ {%- if subnet.tftp_server %}
+ option tftp-server-name "{{ subnet.tftp_server }}";
+ {%- endif -%}
+ {%- if subnet.bootfile_name %}
+ option bootfile-name "{{ subnet.bootfile_name }}";
+ filename "{{ subnet.bootfile_name }}";
+ {%- endif -%}
+ {%- if subnet.bootfile_server %}
+ next-server {{ subnet.bootfile_server }};
+ {%- endif -%}
+ {%- if subnet.time_offset %}
+ option time-offset {{ subnet.time_offset }};
+ {%- endif -%}
+ {%- if subnet.wpad_url %}
+ option wpad-url "{{ subnet.wpad_url }}";
+ {%- endif -%}
+ {%- if subnet.client_prefix_length %}
+ option subnet-mask {{ subnet.client_prefix_length }};
+ {%- endif -%}
+ {% if subnet.lease %}
+ default-lease-time {{ subnet.lease }};
+ max-lease-time {{ subnet.lease }};
+ {%- endif -%}
+ {%- for host in subnet.static_mapping %}
+ {% if not host.disabled -%}
+ host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} {
+ {%- if host.ip_address %}
+ fixed-address {{ host.ip_address }};
+ {%- endif %}
+ hardware ethernet {{ host.mac_address }};
+ {%- if host.static_parameters %}
+ # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated
+ {%- for param in host.static_parameters %}
+ {{ param }}
+ {%- endfor -%}
+ {%- endif %}
+ }
+ {%- endif %}
+ {%- endfor %}
+ {%- if subnet.failover_name %}
+ pool {
+ failover peer "{{ subnet.failover_name }}";
+ deny dynamic bootp clients;
+ {%- for range in subnet.range %}
+ range {{ range.start }} {{ range.stop }};
+ {%- endfor %}
+ }
+ {%- else %}
+ {%- for range in subnet.range %}
+ range {{ range.start }} {{ range.stop }};
+ {%- endfor %}
+ {%- endif %}
+ }
+ {%- endfor %}
+ on commit {
+ set shared-networkname = "{{ network.name }}";
+ {% if hostfile_update -%}
+ set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
+ set ClientIp = binary-to-ascii(10, 8, ".", leased-address);
+ set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6));
+ set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
+ execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain);
+ {%- endif %}
+ }
+}
+{%- endif %}
+{% endfor %}
diff --git a/data/templates/dhcpv6-relay/config.tmpl b/data/templates/dhcpv6-relay/config.tmpl
new file mode 100644
index 000000000..28f7a1a58
--- /dev/null
+++ b/data/templates/dhcpv6-relay/config.tmpl
@@ -0,0 +1,4 @@
+### Autogenerated by dhcpv6_relay.py ###
+
+# Defaults for isc-dhcpv6-relay initscript sourced by /etc/init.d/isc-dhcpv6-relay
+OPTIONS="-6 -l {{ listen_addr | join(' -l ') }} -u {{ upstream_addr | join(' -u ') }} {{ options | join(' ') }}"
diff --git a/data/templates/dhcpv6-server/daemon.tmpl b/data/templates/dhcpv6-server/daemon.tmpl
new file mode 100644
index 000000000..a4967e7c3
--- /dev/null
+++ b/data/templates/dhcpv6-server/daemon.tmpl
@@ -0,0 +1,8 @@
+### Autogenerated by dhcpv6_server.py ###
+
+# sourced by /etc/init.d/isc-dhcpv6-server
+
+DHCPD_CONF={{ config_file }}
+DHCPD_PID={{ pid_file }}
+OPTIONS="-6 -lf {{ lease_file }}"
+INTERFACES=""
diff --git a/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl
new file mode 100644
index 000000000..80d620fcf
--- /dev/null
+++ b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl
@@ -0,0 +1,78 @@
+### Autogenerated by dhcpv6_server.py ###
+
+# For options please consult the following website:
+# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
+
+log-facility local7;
+{%- if preference %}
+option dhcp6.preference {{ preference }};
+{%- endif %}
+
+# Shared network configration(s)
+{% for network in shared_network %}
+{%- if not network.disabled -%}
+shared-network {{ network.name }} {
+ {%- for subnet in network.subnet %}
+ subnet6 {{ subnet.network }} {
+ {%- for range in subnet.range6_prefix %}
+ range6 {{ range.prefix }}{{ " temporary" if range.temporary }};
+ {%- endfor %}
+ {%- for range in subnet.range6 %}
+ range6 {{ range.start }} {{ range.stop }};
+ {%- endfor %}
+ {%- if subnet.domain_search %}
+ option dhcp6.domain-search {{ subnet.domain_search | join(', ') }};
+ {%- endif %}
+ {%- if subnet.lease_def %}
+ default-lease-time {{ subnet.lease_def }};
+ {%- endif %}
+ {%- if subnet.lease_max %}
+ max-lease-time {{ subnet.lease_max }};
+ {%- endif %}
+ {%- if subnet.lease_min %}
+ min-lease-time {{ subnet.lease_min }};
+ {%- endif %}
+ {%- if subnet.dns_server %}
+ option dhcp6.name-servers {{ subnet.dns_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.nis_domain %}
+ option dhcp6.nis-domain-name "{{ subnet.nis_domain }}";
+ {%- endif %}
+ {%- if subnet.nis_server %}
+ option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.nisp_domain %}
+ option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}";
+ {%- endif %}
+ {%- if subnet.nisp_server %}
+ option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }};
+ {%- endif %}
+ {%- if subnet.sip_address %}
+ option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }};
+ {%- endif %}
+ {%- if subnet.sip_hostname %}
+ option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }};
+ {%- endif %}
+ {%- if subnet.sntp_server %}
+ option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }};
+ {%- endif %}
+ {%- for host in subnet.static_mapping %}
+ {% if not host.disabled -%}
+ host {{ network.name }}_{{ host.name }} {
+ {%- if host.client_identifier %}
+ host-identifier option dhcp6.client-id {{ host.client_identifier }};
+ {%- endif %}
+ {%- if host.ipv6_address %}
+ fixed-address6 {{ host.ipv6_address }};
+ {%- endif %}
+ }
+ {%- endif %}
+ {%- endfor %}
+ }
+ {%- endfor %}
+ on commit {
+ set shared-networkname = "{{ network.name }}";
+ }
+}
+{%- endif %}
+{% endfor %}
diff --git a/data/templates/dns-forwarding/recursor.conf.tmpl b/data/templates/dns-forwarding/recursor.conf.tmpl
new file mode 100644
index 000000000..9d1e019fa
--- /dev/null
+++ b/data/templates/dns-forwarding/recursor.conf.tmpl
@@ -0,0 +1,44 @@
+### Autogenerated by dns_forwarding.py ###
+
+# XXX: pdns recursor doesn't like whitespace near entry separators,
+# especially in the semicolon-separated lists of name servers.
+# Please be careful if you edit the template.
+
+# Non-configurable defaults
+daemon=yes
+threads=1
+allow-from={{ allow_from | join(',') }}
+log-common-errors=yes
+non-local-bind=yes
+query-local-address=0.0.0.0
+query-local-address6=::
+
+# cache-size
+max-cache-entries={{ cache_size }}
+
+# negative TTL for NXDOMAIN
+max-negative-ttl={{ negative_ttl }}
+
+# ignore-hosts-file
+export-etc-hosts={{ export_hosts_file }}
+
+# listen-on
+local-address={{ listen_on | join(',') }}
+
+# dnssec
+dnssec={{ dnssec }}
+
+# forward-zones / recursion
+#
+# statement is only inserted if either one forwarding domain or nameserver is configured
+# if nothing is given at all, powerdns will act as a real recursor and resolve all requests by its own
+#
+{% if name_servers or domains %}forward-zones-recurse=
+{%- for d in domains %}
+{{ d.name }}={{ d.servers | join(";") }}
+{{- ", " if not loop.last -}}
+{%- endfor -%}
+{%- if name_servers -%}
+{%- if domains -%}, {% endif -%}.={{ name_servers | join(';') }}
+{% endif %}
+{% endif %}
diff --git a/data/templates/dynamic-dns/ddclient.conf.tmpl b/data/templates/dynamic-dns/ddclient.conf.tmpl
new file mode 100644
index 000000000..22cb38f4e
--- /dev/null
+++ b/data/templates/dynamic-dns/ddclient.conf.tmpl
@@ -0,0 +1,49 @@
+
+### Autogenerated by dynamic_dns.py ###
+daemon=1m
+syslog=yes
+ssl=yes
+pid={{ pid_file }}
+cache={{ cache_file }}
+
+{% for interface in interfaces -%}
+
+#
+# ddclient configuration for interface "{{ interface.interface }}":
+#
+{% if interface.web_url -%}
+use=web, web='{{ interface.web_url}}' {%- if interface.web_skip %}, web-skip='{{ interface.web_skip }}'{% endif %}
+{% else -%}
+use=if, if={{ interface.interface }}
+{% endif -%}
+
+{% for rfc in interface.rfc2136 -%}
+{% for record in rfc.record %}
+# RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }}
+server={{ rfc.server }}
+protocol=nsupdate
+password={{ rfc.keyfile }}
+ttl={{ rfc.ttl }}
+zone={{ rfc.zone }}
+{{ record }}
+{% endfor -%}
+{% endfor -%}
+
+{% for srv in interface.service %}
+{% for host in srv.host %}
+# DynDNS provider configuration for {{ host }}
+protocol={{ srv.protocol }},
+max-interval=28d,
+login={{ srv.login }},
+password='{{ srv.password }}',
+{% if srv.server -%}
+server={{ srv.server }},
+{% endif -%}
+{% if srv.zone -%}
+zone={{ srv.zone }},
+{% endif -%}
+{{ host }}
+{% endfor %}
+{% endfor %}
+
+{% endfor %}
diff --git a/data/templates/frr-bfd/bfd.frr.tmpl b/data/templates/frr-bfd/bfd.frr.tmpl
new file mode 100644
index 000000000..7df4bfd01
--- /dev/null
+++ b/data/templates/frr-bfd/bfd.frr.tmpl
@@ -0,0 +1,16 @@
+!
+bfd
+{% for peer in old_peers -%}
+ no peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %}
+{% endfor -%}
+!
+{% for peer in new_peers -%}
+ peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %}
+ detect-multiplier {{ peer.multiplier }}
+ receive-interval {{ peer.rx_interval }}
+ transmit-interval {{ peer.tx_interval }}
+ {% if peer.echo_mode %}echo-mode{% endif %}
+ {% if peer.echo_interval != '' %}echo-interval {{ peer.echo_interval }}{% endif %}
+ {% if not peer.shutdown %}no {% endif %}shutdown
+{% endfor -%}
+!
diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl
new file mode 100644
index 000000000..f4f2c1848
--- /dev/null
+++ b/data/templates/https/nginx.default.tmpl
@@ -0,0 +1,69 @@
+### Autogenerated by https.py ###
+# Default server configuration
+#
+server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+ server_name _;
+ return 301 https://$server_name$request_uri;
+}
+
+{% for server in server_block_list %}
+server {
+
+ # SSL configuration
+ #
+{% if server.address == '*' %}
+ listen {{ server.port }} ssl;
+ listen [::]:{{ server.port }} ssl;
+{% else %}
+ listen {{ server.address }}:{{ server.port }} ssl;
+{% endif %}
+
+{% for name in server.name %}
+ server_name {{ name }};
+{% endfor %}
+
+{% if server.certbot %}
+ ssl_certificate /etc/letsencrypt/live/{{ server.certbot_dir }}/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/{{ server.certbot_dir }}/privkey.pem;
+ include /etc/letsencrypt/options-ssl-nginx.conf;
+ ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
+{% elif server.vyos_cert %}
+ include {{ server.vyos_cert.conf }};
+{% else %}
+ #
+ # Self signed certs generated by the ssl-cert package
+ # Don't use them in a production server!
+ #
+ include snippets/snakeoil.conf;
+{% endif %}
+
+ # proxy settings for HTTP API, if enabled; 503, if not
+ location ~ /(retrieve|configure|config-file|image|generate|show) {
+{% if server.api %}
+ proxy_pass http://localhost:{{ server.api.port }};
+ proxy_read_timeout 600;
+ proxy_buffering off;
+{% else %}
+ return 503;
+{% endif %}
+ }
+
+ error_page 501 502 503 =200 @50*_json;
+
+{% if api_somewhere %}
+ location @50*_json {
+ default_type application/json;
+ return 200 '{"error": "service https api unavailable at this proxy address: set service https api-restrict virtual-host"}';
+ }
+{% else %}
+ location @50*_json {
+ default_type application/json;
+ return 200 '{"error": "Start service in configuration mode: set service https api"}';
+ }
+{% endif %}
+
+}
+
+{% endfor %}
diff --git a/data/templates/igmp-proxy/igmpproxy.conf.tmpl b/data/templates/igmp-proxy/igmpproxy.conf.tmpl
new file mode 100644
index 000000000..c7fc5cef5
--- /dev/null
+++ b/data/templates/igmp-proxy/igmpproxy.conf.tmpl
@@ -0,0 +1,37 @@
+########################################################
+#
+# autogenerated by igmp_proxy.py
+#
+# The configuration file must define one upstream
+# interface, and one or more downstream interfaces.
+#
+# If multicast traffic originates outside the
+# upstream subnet, the "altnet" option can be
+# used in order to define legal multicast sources.
+# (Se example...)
+#
+# The "quickleave" should be used to avoid saturation
+# of the upstream link. The option should only
+# be used if it's absolutely nessecary to
+# accurately imitate just one Client.
+#
+########################################################
+
+{% if not disable_quickleave -%}
+quickleave
+{% endif -%}
+
+{% for interface in interfaces %}
+# Configuration for {{ interface.name }} ({{ interface.role }} interface)
+{% if interface.role == 'disabled' -%}
+phyint {{ interface.name }} disabled
+{%- else -%}
+phyint {{ interface.name }} {{ interface.role }} ratelimit 0 threshold {{ interface.threshold }}
+{%- endif -%}
+{%- for subnet in interface.alt_subnet %}
+ altnet {{ subnet }}
+{%- endfor %}
+{%- for subnet in interface.whitelist %}
+ whitelist {{ subnet }}
+{%- endfor %}
+{% endfor %}
diff --git a/data/templates/igmp/igmp.frr.tmpl b/data/templates/igmp/igmp.frr.tmpl
new file mode 100644
index 000000000..de4696c1f
--- /dev/null
+++ b/data/templates/igmp/igmp.frr.tmpl
@@ -0,0 +1,41 @@
+!
+{% for iface in old_ifaces -%}
+interface {{ iface }}
+{% for group in old_ifaces[iface].gr_join -%}
+{% if old_ifaces[iface].gr_join[group] -%}
+{% for source in old_ifaces[iface].gr_join[group] -%}
+no ip igmp join {{ group }} {{ source }}
+{% endfor -%}
+{% else -%}
+no ip igmp join {{ group }}
+{% endif -%}
+{% endfor -%}
+no ip igmp
+!
+{% endfor -%}
+{% for iface in ifaces -%}
+interface {{ iface }}
+{% if ifaces[iface].version -%}
+ip igmp version {{ ifaces[iface].version }}
+{% else -%}
+{# IGMP default version 3 #}
+ip igmp
+{% endif -%}
+{% if ifaces[iface].query_interval -%}
+ip igmp query-interval {{ ifaces[iface].query_interval }}
+{% endif -%}
+{% if ifaces[iface].query_max_resp_time -%}
+ip igmp query-max-response-time {{ ifaces[iface].query_max_resp_time }}
+{% endif -%}
+{% for group in ifaces[iface].gr_join -%}
+{% if ifaces[iface].gr_join[group] -%}
+{% for source in ifaces[iface].gr_join[group] -%}
+ip igmp join {{ group }} {{ source }}
+{% endfor -%}
+{% else -%}
+ip igmp join {{ group }}
+{% endif -%}
+{% endfor -%}
+!
+{% endfor -%}
+!
diff --git a/data/templates/ipoe-server/chap-secrets.tmpl b/data/templates/ipoe-server/chap-secrets.tmpl
new file mode 100644
index 000000000..707718e94
--- /dev/null
+++ b/data/templates/ipoe-server/chap-secrets.tmpl
@@ -0,0 +1,18 @@
+# username server password acceptable local IP addresses shaper
+{% for aifc in auth['auth_if'] %}
+{% for mac in auth['auth_if'][aifc] %}
+{% if (auth['auth_if'][aifc][mac]['up']) and (auth['auth_if'][aifc][mac]['down']) %}
+{% if auth['auth_if'][aifc][mac]['vlan'] %}
+{{aifc}}.{{auth['auth_if'][aifc][mac]['vlan']}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}}
+{% else %}
+{{aifc}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}}
+{% endif %}
+{% else %}
+{% if auth['auth_if'][aifc][mac]['vlan'] %}
+{{aifc}}.{{auth['auth_if'][aifc][mac]['vlan']}}\t*\t{{mac.lower()}}\t*
+{% else %}
+{{aifc}}\t*\t{{mac.lower()}}\t*
+{% endif %}
+{% endif %}
+{% endfor %}
+{% endfor %}
diff --git a/data/templates/ipoe-server/ipoe.config.tmpl b/data/templates/ipoe-server/ipoe.config.tmpl
new file mode 100644
index 000000000..109bc0d92
--- /dev/null
+++ b/data/templates/ipoe-server/ipoe.config.tmpl
@@ -0,0 +1,123 @@
+### generated by ipoe.py ###
+[modules]
+log_syslog
+ipoe
+shaper
+ipv6pool
+ipv6_nd
+ipv6_dhcp
+{% if auth['mech'] == 'radius' %}
+radius
+{% endif -%}
+ippool
+{% if auth['mech'] == 'local' %}
+chap-secrets
+{% endif %}
+
+[core]
+thread-count={{thread_cnt}}
+
+[log]
+syslog=accel-ipoe,daemon
+copy=1
+level=5
+
+[ipoe]
+verbose=1
+{% for intfc in interfaces %}
+{% if interfaces[intfc]['vlan_mon'] %}
+interface=re:{{intfc}}\.\d+,\
+{% else %}
+interface={{intfc}},\
+{% endif %}
+shared={{interfaces[intfc]['shared']}},\
+mode={{interfaces[intfc]['mode']}},\
+ifcfg={{interfaces[intfc]['ifcfg']}},\
+range={{interfaces[intfc]['range']}},\
+start={{interfaces[intfc]['sess_start']}},\
+ipv6=1
+{% endfor %}
+{% if auth['mech'] == 'noauth' %}
+noauth=1
+{% endif %}
+{% if auth['mech'] == 'local' %}
+username=ifname
+password=csid
+{% endif %}
+
+{%- for intfc in interfaces %}
+{% if (interfaces[intfc]['shared'] == '0') and (interfaces[intfc]['vlan_mon']) %}
+vlan-mon={{intfc}},{{interfaces[intfc]['vlan_mon']|join(',')}}
+{% endif %}
+{% endfor %}
+
+{% if (dns['server1']) or (dns['server2']) %}
+[dns]
+{% if dns['server1'] %}
+dns1={{dns['server1']}}
+{% endif -%}
+{% if dns['server2'] %}
+dns2={{dns['server2']}}
+{% endif -%}
+{% endif -%}
+
+{% if (dnsv6['server1']) or (dnsv6['server2']) or (dnsv6['server3']) %}
+[dnsv6]
+dns={{dnsv6['server1']}}
+dns={{dnsv6['server2']}}
+dns={{dnsv6['server3']}}
+{% endif %}
+
+[ipv6-nd]
+verbose=1
+
+[ipv6-dhcp]
+verbose=1
+
+{% if ipv6['prfx'] %}
+[ipv6-pool]
+{% for prfx in ipv6['prfx'] %}
+{{prfx}}
+{% endfor %}
+{% for pd in ipv6['pd'] %}
+delegate={{pd}}
+{% endfor %}
+{% endif %}
+
+{% if auth['mech'] == 'local' %}
+[chap-secrets]
+chap-secrets=/etc/accel-ppp/ipoe/chap-secrets
+{% endif %}
+
+{% if auth['mech'] == 'radius' %}
+[radius]
+verbose=1
+{% for srv in auth['radius'] %}
+server={{srv}},{{auth['radius'][srv]['secret']}},\
+req-limit={{auth['radius'][srv]['req-limit']}},\
+fail-time={{auth['radius'][srv]['fail-time']}}
+{% endfor %}
+{% if auth['radsettings']['dae-server']['ip-address'] %}
+dae-server={{auth['radsettings']['dae-server']['ip-address']}}:\
+{{auth['radsettings']['dae-server']['port']}},\
+{{auth['radsettings']['dae-server']['secret']}}
+{% endif -%}
+{% if auth['radsettings']['acct-timeout'] %}
+acct-timeout={{auth['radsettings']['acct-timeout']}}
+{% endif -%}
+{% if auth['radsettings']['max-try'] %}
+max-try={{auth['radsettings']['max-try']}}
+{% endif -%}
+{% if auth['radsettings']['timeout'] %}
+timeout={{auth['radsettings']['timeout']}}
+{% endif -%}
+{% if auth['radsettings']['nas-ip-address'] %}
+nas-ip-address={{auth['radsettings']['nas-ip-address']}}
+{% endif -%}
+{% if auth['radsettings']['nas-identifier'] %}
+nas-identifier={{auth['radsettings']['nas-identifier']}}
+{% endif -%}
+{% endif %}
+
+[cli]
+tcp=127.0.0.1:2002
diff --git a/data/templates/ipsec/ipsec.conf.tmpl b/data/templates/ipsec/ipsec.conf.tmpl
new file mode 100644
index 000000000..d0b60765b
--- /dev/null
+++ b/data/templates/ipsec/ipsec.conf.tmpl
@@ -0,0 +1,3 @@
+{{delim_ipsec_l2tp_begin}}
+include {{ipsec_ra_conn_file}}
+{{delim_ipsec_l2tp_end}}
diff --git a/data/templates/ipsec/ipsec.secrets.tmpl b/data/templates/ipsec/ipsec.secrets.tmpl
new file mode 100644
index 000000000..55c010a3b
--- /dev/null
+++ b/data/templates/ipsec/ipsec.secrets.tmpl
@@ -0,0 +1,7 @@
+{{delim_ipsec_l2tp_begin}}
+{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %}
+{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}"
+{% elif ipsec_l2tp_auth_mode == 'x509' %}
+: RSA {{server_key_file_copied}}
+{% endif%}
+{{delim_ipsec_l2tp_end}}
diff --git a/data/templates/ipsec/remote-access.tmpl b/data/templates/ipsec/remote-access.tmpl
new file mode 100644
index 000000000..fae48232f
--- /dev/null
+++ b/data/templates/ipsec/remote-access.tmpl
@@ -0,0 +1,28 @@
+{{delim_ipsec_l2tp_begin}}
+conn {{ra_conn_name}}
+ type=transport
+ left={{outside_addr}}
+ leftsubnet=%dynamic[/1701]
+ rightsubnet=%dynamic
+ mark_in=%unique
+ auto=add
+ ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024!
+ dpddelay=15
+ dpdtimeout=45
+ dpdaction=clear
+ esp=aes256-sha1,3des-sha1!
+ rekey=no
+{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %}
+ authby=secret
+ leftauth=psk
+ rightauth=psk
+{% elif ipsec_l2tp_auth_mode == 'x509' %}
+ authby=rsasig
+ leftrsasigkey=%cert
+ rightrsasigkey=%cert
+ rightca=%same
+ leftcert={{server_cert_file_copied}}
+{% endif %}
+ ikelifetime={{ipsec_l2tp_ike_lifetime}}
+ keylife={{ipsec_l2tp_lifetime}}
+{{delim_ipsec_l2tp_end}}
diff --git a/data/templates/l2tp/chap-secrets.tmpl b/data/templates/l2tp/chap-secrets.tmpl
new file mode 100644
index 000000000..0db295fdc
--- /dev/null
+++ b/data/templates/l2tp/chap-secrets.tmpl
@@ -0,0 +1,10 @@
+# username server password acceptable local IP addresses shaper
+{% for user in authentication['local-users'] %}
+{% if authentication['local-users'][user]['state'] == 'enabled' %}
+{% if authentication['local-users'][user]['upload'] and authentication['local-users'][user]['download'] %}
+{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }} {{ authentication['local-users'][user]['download'] }} / {{ authentication['local-users'][user]['upload'] }}
+{% else %}
+{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }}
+{% endif %}
+{% endif %}
+{% endfor %}
diff --git a/data/templates/l2tp/l2tp.config.tmpl b/data/templates/l2tp/l2tp.config.tmpl
new file mode 100644
index 000000000..b8637e256
--- /dev/null
+++ b/data/templates/l2tp/l2tp.config.tmpl
@@ -0,0 +1,173 @@
+### generated by accel_l2tp.py ###
+[modules]
+log_syslog
+l2tp
+chap-secrets
+{% for proto in authentication['auth_proto']: %}
+{{proto}}
+{% endfor%}
+{% if authentication['mode'] == 'radius' %}
+radius
+{% endif -%}
+ippool
+shaper
+ipv6pool
+ipv6_nd
+ipv6_dhcp
+
+[core]
+thread-count={{thread_cnt}}
+
+[log]
+syslog=accel-l2tp,daemon
+copy=1
+level=5
+
+{% if dns %}
+[dns]
+{% if dns[0] %}
+dns1={{dns[0]}}
+{% endif %}
+{% if dns[1] %}
+dns2={{dns[1]}}
+{% endif %}
+{% endif -%}
+
+{% if dnsv6 %}
+[ipv6-dns]
+{% for srv in dnsv6: %}
+{{srv}}
+{% endfor %}
+{% endif %}
+
+{% if wins %}
+[wins]
+{% if wins[0] %}
+wins1={{wins[0]}}
+{% endif %}
+{% if wins[1] %}
+wins2={{wins[1]}}
+{% endif %}
+{% endif -%}
+
+[l2tp]
+verbose=1
+ifname=l2tp%d
+ppp-max-mtu={{mtu}}
+mppe={{authentication['mppe']}}
+{% if outside_addr %}
+bind={{outside_addr}}
+{% endif %}
+{% if lns_shared_secret %}
+secret={{lns_shared_secret}}
+{% endif %}
+
+[client-ip-range]
+0.0.0.0/0
+
+{% if (client_ip_pool) or (client_ip_subnets) %}
+[ip-pool]
+{% if client_ip_pool %}
+{{client_ip_pool}}
+{% endif -%}
+{% if client_ip_subnets %}
+{% for sn in client_ip_subnets %}
+{{sn}}
+{% endfor -%}
+{% endif %}
+{% endif %}
+{% if gateway_address %}
+gw-ip-address={{gateway_address}}
+{% endif %}
+
+{% if authentication['mode'] == 'local' %}
+[chap-secrets]
+chap-secrets=/etc/accel-ppp/l2tp/chap-secrets
+{% if gateway_address %}
+gw-ip-address={{gateway_address}}
+{% endif %}
+{% endif %}
+
+[ppp]
+verbose=1
+check-ip=1
+single-session=replace
+{% if idle_timeout %}
+lcp-echo-timeout={{idle_timeout}}
+{% endif %}
+{% if ppp_options['lcp-echo-interval'] %}
+lcp-echo-interval={{ppp_options['lcp-echo-interval']}}
+{% else %}
+lcp-echo-interval=30
+{% endif %}
+{% if ppp_options['lcp-echo-failure'] %}
+lcp-echo-failure={{ppp_options['lcp-echo-failure']}}
+{% else %}
+lcp-echo-failure=3
+{% endif %}
+{% if ccp_disable %}
+ccp=0
+{% endif %}
+{% if client_ipv6_pool %}
+ipv6=allow
+{% endif %}
+
+{% if authentication['mode'] == 'radius' %}
+[radius]
+{% for rsrv in authentication['radiussrv']: %}
+server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\
+req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\
+fail-time={{authentication['radiussrv'][rsrv]['fail-time']}}
+{% endfor %}
+{% if authentication['radiusopt']['timeout'] %}
+timeout={{authentication['radiusopt']['timeout']}}
+{% endif %}
+{% if authentication['radiusopt']['acct-timeout'] %}
+acct-timeout={{authentication['radiusopt']['acct-timeout']}}
+{% endif %}
+{% if authentication['radiusopt']['max-try'] %}
+max-try={{authentication['radiusopt']['max-try']}}
+{% endif %}
+{% if authentication['radiusopt']['nas-id'] %}
+nas-identifier={{authentication['radiusopt']['nas-id']}}
+{% endif %}
+{% if authentication['radius_source_address'] %}
+nas-ip-address={{authentication['radius_source_address']}}
+{% endif -%}
+{% if authentication['radiusopt']['dae-srv'] %}
+dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\
+{{authentication['radiusopt']['dae-srv']['port']}},\
+{{authentication['radiusopt']['dae-srv']['secret']}}
+{% endif -%}
+gw-ip-address={{gateway_address}}
+verbose=1
+{% endif -%}
+
+{% if client_ipv6_pool %}
+[ipv6-pool]
+{% for prfx in client_ipv6_pool.prefix: %}
+{{prfx}}
+{% endfor %}
+{% for prfx in client_ipv6_pool.delegate_prefix: %}
+delegate={{prfx}}
+{% endfor %}
+{% endif %}
+
+{% if client_ipv6_pool['delegate_prefix'] %}
+[ipv6-dhcp]
+verbose=1
+{% endif %}
+
+{% if authentication['radiusopt']['shaper'] %}
+[shaper]
+verbose=1
+attr={{authentication['radiusopt']['shaper']['attr']}}
+{% if authentication['radiusopt']['shaper']['vendor'] %}
+vendor={{authentication['radiusopt']['shaper']['vendor']}}
+{% endif -%}
+{% endif %}
+
+[cli]
+tcp=127.0.0.1:2004
+sessions-columns=ifname,username,calling-sid,ip,{{ip6_column}}{{ip6_dp_column}}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime
+
diff --git a/data/templates/lldp/lldpd.tmpl b/data/templates/lldp/lldpd.tmpl
new file mode 100644
index 000000000..3db955b48
--- /dev/null
+++ b/data/templates/lldp/lldpd.tmpl
@@ -0,0 +1,3 @@
+### Autogenerated by lldp.py ###
+DAEMON_ARGS="-M 4{% if options.snmp %} -x{% endif %}{% if options.cdp %} -c{% endif %}{% if options.edp %} -e{% endif %}{% if options.fdp %} -f{% endif %}{% if options.sonmp %} -s{% endif %}"
+
diff --git a/data/templates/lldp/vyos.conf.tmpl b/data/templates/lldp/vyos.conf.tmpl
new file mode 100644
index 000000000..e724f42c6
--- /dev/null
+++ b/data/templates/lldp/vyos.conf.tmpl
@@ -0,0 +1,20 @@
+### Autogenerated by lldp.py ###
+
+configure system platform VyOS
+configure system description "VyOS {{ options.description }}"
+{% if options.listen_on -%}
+configure system interface pattern "{{ ( options.listen_on | select('equalto','all') | map('replace','all','*') | list + options.listen_on | select('equalto','!all') | map('replace','!all','!*') | list + options.listen_on | reject('equalto','all') | reject('equalto','!all') | list ) | unique | join(",") }}"
+{%- endif %}
+{% if options.mgmt_addr -%}
+configure system ip management pattern {{ options.mgmt_addr | join(",") }}
+{%- endif %}
+{%- for loc in location -%}
+{%- if loc.elin %}
+configure ports {{ loc.name }} med location elin "{{ loc.elin }}"
+{%- endif %}
+{%- if loc.coordinate_based %}
+configure ports {{ loc.name }} med location coordinate {% if loc.coordinate_based.latitude %}latitude {{ loc.coordinate_based.latitude }}{% endif %} {% if loc.coordinate_based.longitude %}longitude {{ loc.coordinate_based.longitude }}{% endif %} {% if loc.coordinate_based.altitude %}altitude {{ loc.coordinate_based.altitude }} m{% endif %} {% if loc.coordinate_based.datum %}datum {{ loc.coordinate_based.datum }}{% endif %}
+{%- endif %}
+
+
+{% endfor %}
diff --git a/data/templates/mdns-repeater/mdns-repeater.tmpl b/data/templates/mdns-repeater/mdns-repeater.tmpl
new file mode 100644
index 000000000..3fc4db67e
--- /dev/null
+++ b/data/templates/mdns-repeater/mdns-repeater.tmpl
@@ -0,0 +1,2 @@
+### Autogenerated by mdns_repeater.py ###
+DAEMON_ARGS="{{ interfaces | join(' ') }}"
diff --git a/data/templates/mpls/ldpd.frr.tmpl b/data/templates/mpls/ldpd.frr.tmpl
new file mode 100644
index 000000000..bbff88ae5
--- /dev/null
+++ b/data/templates/mpls/ldpd.frr.tmpl
@@ -0,0 +1,58 @@
+!
+{% if mpls_ldp -%}
+mpls ldp
+{% if old_router_id -%}
+no router-id {{ old_router_id }}
+{% endif -%}
+{% if router_id -%}
+router-id {{ router_id }}
+{% endif -%}
+{% for neighbor_id in old_ldp.neighbors -%}
+no neighbor {{neighbor_id}} password {{old_ldp.neighbors[neighbor_id].password}}
+{% endfor -%}
+{% for neighbor_id in ldp.neighbors -%}
+neighbor {{neighbor_id}} password {{ldp.neighbors[neighbor_id].password}}
+{% endfor -%}
+address-family ipv4
+label local allocate host-routes
+{% if old_ldp.d_transp_ipv4 -%}
+no discovery transport-address {{ old_ldp.d_transp_ipv4 }}
+{% endif -%}
+{% if ldp.d_transp_ipv4 -%}
+discovery transport-address {{ ldp.d_transp_ipv4 }}
+{% endif -%}
+{% for interface in old_ldp.interfaces -%}
+no interface {{interface}}
+{% endfor -%}
+{% for interface in ldp.interfaces -%}
+interface {{interface}}
+{% endfor -%}
+!
+!
+exit-address-family
+!
+{% if ldp.d_transp_ipv6 -%}
+address-family ipv6
+label local allocate host-routes
+{% if old_ldp.d_transp_ipv6 -%}
+no discovery transport-address {{ old_ldp.d_transp_ipv6 }}
+{% endif -%}
+{% if ldp.d_transp_ipv6 -%}
+discovery transport-address {{ ldp.d_transp_ipv6 }}
+{% endif -%}
+{% for interface in old_ldp.interfaces -%}
+no interface {{interface}}
+{% endfor -%}
+{% for interface in ldp.interfaces -%}
+interface {{interface}}
+{% endfor -%}
+!
+exit-address-family
+{% else -%}
+no address-family ipv6
+{% endif -%}
+!
+{% else -%}
+no mpls ldp
+{% endif -%}
+!
diff --git a/data/templates/netflow/uacctd.conf.tmpl b/data/templates/netflow/uacctd.conf.tmpl
new file mode 100644
index 000000000..d8615566f
--- /dev/null
+++ b/data/templates/netflow/uacctd.conf.tmpl
@@ -0,0 +1,69 @@
+# Genereated from VyOS configuration
+daemonize: true
+promisc: false
+pidfile: /var/run/uacctd.pid
+uacctd_group: 2
+uacctd_nl_size: 2097152
+snaplen: {{ snaplen }}
+aggregate: in_iface,src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows
+plugin_pipe_size: {{ templatecfg['plugin_pipe_size'] }}
+plugin_buffer_size: {{ templatecfg['plugin_buffer_size'] }}
+{%- if templatecfg['syslog-facility'] != none %}
+syslog: {{ templatecfg['syslog-facility'] }}
+{%- endif %}
+{%- if templatecfg['disable-imt'] == none %}
+imt_path: /tmp/uacctd.pipe
+imt_mem_pools_number: 169
+{%- endif %}
+plugins:
+{%- if templatecfg['netflow']['servers'] != none -%}
+ {% for server in templatecfg['netflow']['servers'] %}
+ {%- if loop.last -%}nfprobe[nf_{{ server['address'] }}]{%- else %}nfprobe[nf_{{ server['address'] }}],{%- endif %}
+ {%- endfor -%}
+ {% set plugins_presented = true %}
+{%- endif %}
+{%- if templatecfg['sflow']['servers'] != none -%}
+ {% if plugins_presented -%}
+ {%- for server in templatecfg['sflow']['servers'] -%}
+ ,sfprobe[sf_{{ server['address'] }}]
+ {%- endfor %}
+ {%- else %}
+ {%- for server in templatecfg['sflow']['servers'] %}
+ {%- if loop.last -%}sfprobe[sf_{{ server['address'] }}]{%- else %}sfprobe[sf_{{ server['address'] }}],{%- endif %}
+ {%- endfor %}
+ {%- endif -%}
+ {% set plugins_presented = true %}
+{%- endif %}
+{%- if templatecfg['disable-imt'] == none %}
+ {%- if plugins_presented -%},memory{%- else %}memory{%- endif %}
+{%- endif %}
+{%- if templatecfg['netflow']['servers'] != none %}
+{%- for server in templatecfg['netflow']['servers'] %}
+nfprobe_receiver[nf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }}
+nfprobe_version[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['version'] }}
+{%- if templatecfg['netflow']['engine-id'] != none %}
+nfprobe_engine[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['engine-id'] }}
+{%- endif %}
+{%- if templatecfg['netflow']['max-flows'] != none %}
+nfprobe_maxflows[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['max-flows'] }}
+{%- endif %}
+{%- if templatecfg['netflow']['sampling-rate'] != none %}
+sampling_rate[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['sampling-rate'] }}
+{%- endif %}
+{%- if templatecfg['netflow']['source-ip'] != none %}
+nfprobe_source_ip[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['source-ip'] }}
+{%- endif %}
+{%- if templatecfg['netflow']['timeout_string'] != '' %}
+nfprobe_timeouts[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['timeout_string'] }}
+{%- endif %}
+{%- endfor %}
+{%- endif %}
+{%- if templatecfg['sflow']['servers'] != none %}
+{%- for server in templatecfg['sflow']['servers'] %}
+sfprobe_receiver[sf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }}
+sfprobe_agentip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['agent-address'] }}
+{%- if templatecfg['sflow']['sampling-rate'] != none %}
+sampling_rate[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['sampling-rate'] }}
+{%- endif %}
+{%- endfor %}
+{% endif %}
diff --git a/data/templates/ntp/ntp.conf.tmpl b/data/templates/ntp/ntp.conf.tmpl
new file mode 100644
index 000000000..52042d218
--- /dev/null
+++ b/data/templates/ntp/ntp.conf.tmpl
@@ -0,0 +1,38 @@
+### Autogenerated by ntp.py ###
+
+#
+# Non-configurable defaults
+#
+driftfile /var/lib/ntp/ntp.drift
+# By default, only allow ntpd to query time sources, ignore any incoming requests
+restrict default noquery nopeer notrap nomodify
+# Local users have unrestricted access, allowing reconfiguration via ntpdc
+restrict 127.0.0.1
+restrict -6 ::1
+
+#
+# Configurable section
+#
+
+{% if servers -%}
+{% for s in servers -%}
+# Server configuration for: {{ s.name }}
+server {{ s.name }} iburst {{ s.options | join(" ") }}
+{% endfor -%}
+{% endif %}
+
+{% if allowed_networks -%}
+{% for n in allowed_networks -%}
+# Client configuration for network: {{ n.network }}
+restrict {{ n.address }} mask {{ n.netmask }} nomodify notrap nopeer
+
+{% endfor -%}
+{% endif %}
+
+{% if listen_address -%}
+# NTP should listen on configured addresses only
+interface ignore wildcard
+{% for a in listen_address -%}
+interface listen {{ a }}
+{% endfor -%}
+{% endif %}
diff --git a/data/templates/openvpn/client.conf.tmpl b/data/templates/openvpn/client.conf.tmpl
new file mode 100644
index 000000000..3099f2ca7
--- /dev/null
+++ b/data/templates/openvpn/client.conf.tmpl
@@ -0,0 +1,16 @@
+### Autogenerated by interfaces-openvpn.py ###
+
+{% if ip -%}
+ifconfig-push {{ ip }} {{ remote_netmask }}
+{% endif -%}
+{% for route in push_route -%}
+push "route {{ route }}"
+{% endfor -%}
+
+{% for net in subnet -%}
+iroute {{ net }}
+{% endfor -%}
+
+{% if disable -%}
+disable
+{% endif -%}
diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl
new file mode 100644
index 000000000..d0af3d2e3
--- /dev/null
+++ b/data/templates/openvpn/server.conf.tmpl
@@ -0,0 +1,236 @@
+### Autogenerated by interfaces-openvpn.py ###
+#
+# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
+# for individual keyword definition
+
+{% if description -%}
+# {{ description }}
+
+{% endif -%}
+
+verb 3
+status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30
+writepid /var/run/openvpn/{{ intf }}.pid
+
+user {{ uid }}
+group {{ gid }}
+
+dev-type {{ type }}
+dev {{ intf }}
+persist-key
+iproute /usr/libexec/vyos/system/unpriv-ip
+
+proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %}
+
+{%- if local_host %}
+local {{ local_host }}
+{%- endif %}
+
+{%- if mode == 'server' and protocol == 'udp' and not local_host %}
+multihome
+{%- endif %}
+
+{%- if local_port %}
+lport {{ local_port }}
+{%- endif %}
+
+{% if remote_port -%}
+rport {{ remote_port }}
+{% endif %}
+
+{%- if remote_host %}
+{%- for remote in remote_host -%}
+remote {{ remote }}
+{% endfor -%}
+{% endif -%}
+
+{% if shared_secret_file %}
+secret {{ shared_secret_file }}
+{%- endif %}
+
+{%- if persistent_tunnel %}
+persist-tun
+{%- endif %}
+
+{%- if redirect_gateway %}
+push "redirect-gateway {{ redirect_gateway }}"
+{%- endif %}
+
+{%- if compress_lzo %}
+compress lzo
+{%- endif %}
+
+{% if 'client' in mode -%}
+#
+# OpenVPN Client mode
+#
+client
+nobind
+
+{% elif 'server' in mode -%}
+#
+# OpenVPN Server mode
+#
+
+{%- if server_topology %}
+topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %}
+{%- endif %}
+
+{%- if bridge_member %}
+server-bridge nogw
+{%- else %}
+server {{ server_subnet }}
+{%- endif %}
+
+{%- if server_max_conn %}
+max-clients {{ server_max_conn }}
+{%- endif %}
+
+{%- if client %}
+client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }}
+{%- endif %}
+
+{%- if server_reject_unconfigured %}
+ccd-exclusive
+{%- endif %}
+
+keepalive {{ ping_interval }} {{ ping_restart }}
+management /tmp/openvpn-mgmt-intf unix
+
+{% for route in server_push_route -%}
+push "route {{ route }}"
+{% endfor -%}
+
+{% for ns in server_dns_nameserver -%}
+push "dhcp-option DNS {{ ns }}"
+{% endfor -%}
+
+{%- if server_domain -%}
+push "dhcp-option DOMAIN {{ server_domain }}"
+{% endif %}
+
+{% else -%}
+#
+# OpenVPN site-2-site mode
+#
+ping {{ ping_interval }}
+ping-restart {{ ping_restart }}
+
+{% if local_address_subnet -%}
+ifconfig {{ local_address }} {{ local_address_subnet }}
+{%- elif remote_address -%}
+ifconfig {{ local_address }} {{ remote_address }}
+{%- endif %}
+
+{% endif -%}
+
+{% if tls -%}
+# TLS options
+{%- if tls_ca_cert %}
+ca {{ tls_ca_cert }}
+{%- endif %}
+
+{%- if tls_cert %}
+cert {{ tls_cert }}
+{%- endif %}
+
+{%- if tls_key %}
+key {{ tls_key }}
+{%- endif %}
+
+{%- if tls_crypt %}
+tls-crypt {{ tls_crypt }}
+{%- endif %}
+
+{%- if tls_crl %}
+crl-verify {{ tls_crl }}
+{%- endif %}
+
+{%- if tls_version_min %}
+tls-version-min {{tls_version_min}}
+{%- endif %}
+
+{%- if tls_dh %}
+dh {{ tls_dh }}
+{%- endif %}
+
+{%- if tls_auth %}
+tls-auth {{tls_auth}}
+{%- endif %}
+
+{%- if tls_role %}
+{%- if 'active' in tls_role %}
+tls-client
+{%- elif 'passive' in tls_role %}
+tls-server
+{%- endif %}
+{%- endif %}
+
+{%- endif %}
+
+# Encryption options
+{%- if encryption %}
+{% if encryption == 'des' -%}
+cipher des-cbc
+{%- elif encryption == '3des' -%}
+cipher des-ede3-cbc
+{%- elif encryption == 'bf128' -%}
+cipher bf-cbc
+keysize 128
+{%- elif encryption == 'bf256' -%}
+cipher bf-cbc
+keysize 25
+{%- elif encryption == 'aes128gcm' -%}
+cipher aes-128-gcm
+{%- elif encryption == 'aes128' -%}
+cipher aes-128-cbc
+{%- elif encryption == 'aes192gcm' -%}
+cipher aes-192-gcm
+{%- elif encryption == 'aes192' -%}
+cipher aes-192-cbc
+{%- elif encryption == 'aes256gcm' -%}
+cipher aes-256-gcm
+{%- elif encryption == 'aes256' -%}
+cipher aes-256-cbc
+{%- endif -%}
+{%- endif %}
+
+{%- if ncp_ciphers %}
+ncp-ciphers {{ncp_ciphers}}
+{%- endif %}
+{%- if disable_ncp %}
+ncp-disable
+{%- endif %}
+
+{% if hash -%}
+auth {{ hash }}
+{%- endif -%}
+
+{%- if auth %}
+auth-user-pass /tmp/openvpn-{{ intf }}-pw
+auth-retry nointeract
+{%- endif %}
+
+# DEPRECATED This option will be removed in OpenVPN 2.5
+# Until OpenVPN v2.3 the format of the X.509 Subject fields was formatted like this:
+# /C=US/L=Somewhere/CN=John Doe/emailAddress=john@example.com In addition the old
+# behaviour was to remap any character other than alphanumeric, underscore ('_'),
+# dash ('-'), dot ('.'), and slash ('/') to underscore ('_'). The X.509 Subject
+# string as returned by the tls_id environmental variable, could additionally
+# contain colon (':') or equal ('='). When using the --compat-names option, this
+# old formatting and remapping will be re-enabled again. This is purely implemented
+# for compatibility reasons when using older plug-ins or scripts which does not
+# handle the new formatting or UTF-8 characters.
+#
+# See https://phabricator.vyos.net/T1512
+compat-names
+
+{% if options -%}
+#
+# Custom options added by user (not validated)
+#
+
+{% for option in options -%}
+{{ option }}
+{% endfor -%}
+{%- endif %}
diff --git a/data/templates/pim/pimd.frr.tmpl b/data/templates/pim/pimd.frr.tmpl
new file mode 100644
index 000000000..1d1532c60
--- /dev/null
+++ b/data/templates/pim/pimd.frr.tmpl
@@ -0,0 +1,34 @@
+!
+{% for rp_addr in old_pim.rp -%}
+{% for group in old_pim.rp[rp_addr] -%}
+no ip pim rp {{ rp_addr }} {{ group }}
+{% endfor -%}
+{% endfor -%}
+{% if old_pim.rp_keep_alive -%}
+no ip pim rp keep-alive-timer {{ old_pim.rp_keep_alive }}
+{% endif -%}
+{% for iface in old_pim.ifaces -%}
+interface {{ iface }}
+no ip pim
+!
+{% endfor -%}
+{% for iface in pim.ifaces -%}
+interface {{ iface }}
+ip pim
+{% if pim.ifaces[iface].dr_prio -%}
+ip pim drpriority {{ pim.ifaces[iface].dr_prio }}
+{% endif -%}
+{% if pim.ifaces[iface].hello -%}
+ip pim hello {{ pim.ifaces[iface].hello }}
+{% endif -%}
+!
+{% endfor -%}
+{% for rp_addr in pim.rp -%}
+{% for group in pim.rp[rp_addr] -%}
+ip pim rp {{ rp_addr }} {{ group }}
+{% endfor -%}
+{% endfor -%}
+{% if pim.rp_keep_alive -%}
+ip pim rp keep-alive-timer {{ pim.rp_keep_alive }}
+{% endif -%}
+!
diff --git a/data/templates/pppoe-server/chap-secrets.tmpl b/data/templates/pppoe-server/chap-secrets.tmpl
new file mode 100644
index 000000000..907ac6ed7
--- /dev/null
+++ b/data/templates/pppoe-server/chap-secrets.tmpl
@@ -0,0 +1,10 @@
+# username server password acceptable local IP addresses shaper
+{% for user in authentication['local-users'] %}
+{% if authentication['local-users'][user]['state'] == 'enabled' %}
+{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %}
+{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }} {{ authentication['local-users'][user]['download'] }} / {{ authentication['local-users'][user]['upload'] }}
+{% else %}
+{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }}
+{% endif %}
+{% endif %}
+{% endfor %}
diff --git a/data/templates/pppoe-server/pppoe.config.tmpl b/data/templates/pppoe-server/pppoe.config.tmpl
new file mode 100644
index 000000000..d44c0aa93
--- /dev/null
+++ b/data/templates/pppoe-server/pppoe.config.tmpl
@@ -0,0 +1,228 @@
+
+### generated by accel_pppoe.py ###
+[modules]
+log_syslog
+pppoe
+{% if authentication['mode'] == 'radius' %}
+radius
+{% endif %}
+ippool
+{% if ppp_options['ipv6'] != 'deny' %}
+ipv6pool
+ipv6_nd
+ipv6_dhcp
+{% endif %}
+chap-secrets
+auth_pap
+auth_chap_md5
+auth_mschap_v1
+auth_mschap_v2
+#pppd_compat
+shaper
+{% if snmp == 'enable' or snmp == 'enable-ma' %}
+net-snmp
+{% endif %}
+{% if limits %}
+connlimit
+{% endif %}
+
+[core]
+thread-count={{thread_cnt}}
+
+[log]
+syslog=accel-pppoe,daemon
+copy=1
+level=5
+
+{% if snmp == 'enable-ma' %}
+[snmp]
+master=1
+{% endif -%}
+
+[client-ip-range]
+disable
+
+{% if ppp_gw %}
+[ip-pool]
+gw-ip-address={{ppp_gw}}
+{% if client_ip_pool %}
+{{client_ip_pool}}
+{% endif -%}
+
+{% if client_ip_subnets %}
+{% for sn in client_ip_subnets %}
+{{sn}}
+{% endfor %}
+{% endif %}
+{% endif -%}
+
+{% if client_ipv6_pool %}
+[ipv6-pool]
+{% for prfx in client_ipv6_pool['prefix']: %}
+{{prfx}}
+{% endfor %}
+{% for prfx in client_ipv6_pool['delegate-prefix']: %}
+delegate={{prfx}}
+{% endfor %}
+{% endif %}
+
+{% if dns %}
+[dns]
+{% if dns[0] %}
+dns1={{dns[0]}}
+{% endif -%}
+{% if dns[1] %}
+dns2={{dns[1]}}
+{% endif -%}
+{% endif %}
+
+{% if dnsv6 %}
+[ipv6-dns]
+{% for srv in dnsv6: %}
+{{srv}}
+{% endfor %}
+{% endif %}
+
+{% if wins %}
+[wins]
+{% if wins[0] %}
+wins1={{wins[0]}}
+{% endif %}
+{% if wins[1] %}
+wins2={{wins[1]}}
+{% endif -%}
+{% endif -%}
+
+{% if authentication['mode'] == 'local' %}
+[chap-secrets]
+chap-secrets=/etc/accel-ppp/pppoe/chap-secrets
+{% endif -%}
+
+{% if authentication['mode'] == 'radius' %}
+[radius]
+{% for rsrv in authentication['radiussrv']: %}
+server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\
+req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\
+fail-time={{authentication['radiussrv'][rsrv]['fail-time']}}
+{% endfor %}
+{% if authentication['radiusopt']['timeout'] %}
+timeout={{authentication['radiusopt']['timeout']}}
+{% endif %}
+{% if authentication['radiusopt']['acct-timeout'] %}
+acct-timeout={{authentication['radiusopt']['acct-timeout']}}
+{% endif %}
+{% if authentication['radiusopt']['max-try'] %}
+max-try={{authentication['radiusopt']['max-try']}}
+{% endif %}
+{% if authentication['radiusopt']['nas-id'] %}
+nas-identifier={{authentication['radiusopt']['nas-id']}}
+{% endif %}
+{% if authentication['radiusopt']['nas-ip'] %}
+nas-ip-address={{authentication['radiusopt']['nas-ip']}}
+{% endif -%}
+{% if authentication['radiusopt']['dae-srv'] %}
+dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\
+{{authentication['radiusopt']['dae-srv']['port']}},\
+{{authentication['radiusopt']['dae-srv']['secret']}}
+{% endif -%}
+gw-ip-address={{ppp_gw}}
+verbose=1
+
+{% if authentication['radiusopt']['shaper'] %}
+[shaper]
+verbose=1
+attr={{authentication['radiusopt']['shaper']['attr']}}
+{% if authentication['radiusopt']['shaper']['vendor'] %}
+vendor={{authentication['radiusopt']['shaper']['vendor']}}
+{% endif -%}
+{% endif -%}
+{% endif %}
+
+[ppp]
+verbose=1
+check-ip=1
+{% if not sesscrtl == 'disable' %}
+single-session={{sesscrtl}}
+{% endif -%}
+{% if ppp_options['ccp'] %}
+ccp=1
+{% endif %}
+{% if ppp_options['min-mtu'] %}
+min-mtu={{ppp_options['min-mtu']}}
+{% else %}
+min-mtu={{mtu}}
+{% endif %}
+{% if ppp_options['mru'] %}
+mru={{ppp_options['mru']}}
+{% endif %}
+{% if ppp_options['mppe'] %}
+mppe={{ppp_options['mppe']}}
+{% else %}
+mppe=prefer
+{% endif %}
+{% if ppp_options['lcp-echo-interval'] %}
+lcp-echo-interval={{ppp_options['lcp-echo-interval']}}
+{% else %}
+lcp-echo-interval=30
+{% endif %}
+{% if ppp_options['lcp-echo-timeout'] %}
+lcp-echo-timeout={{ppp_options['lcp-echo-timeout']}}
+{% endif %}
+{% if ppp_options['lcp-echo-failure'] %}
+lcp-echo-failure={{ppp_options['lcp-echo-failure']}}
+{% else %}
+lcp-echo-failure=3
+{% endif %}
+{% if ppp_options['ipv4'] %}
+ipv4={{ppp_options['ipv4']}}
+{% endif %}
+{% if client_ipv6_pool %}
+ipv6=allow
+{% endif %}
+
+{% if ppp_options['ipv6'] %}
+ipv6={{ppp_options['ipv6']}}
+{% if ppp_options['ipv6-intf-id'] %}
+ipv6-intf-id={{ppp_options['ipv6-intf-id']}}
+{% endif %}
+{% if ppp_options['ipv6-peer-intf-id'] %}
+ipv6-peer-intf-id={{ppp_options['ipv6-peer-intf-id']}}
+{% endif %}
+{% if ppp_options['ipv6-accept-peer-intf-id'] %}
+ipv6-accept-peer-intf-id={{ppp_options['ipv6-accept-peer-intf-id']}}
+{% endif %}
+{% endif %}
+mtu={{mtu}}
+
+[pppoe]
+verbose=1
+{% if concentrator %}
+ac-name={{concentrator}}
+{% endif %}
+{% if interface %}
+{% for int in interface %}
+interface={{int}}
+{% if interface[int]['vlans'] %}
+vlan-mon={{int}},{{interface[int]['vlans']|join(',')}}
+interface=re:{{int}}\.\d+
+{% endif %}
+{% endfor -%}
+{% endif -%}
+
+{% if svc_name %}
+service-name={{svc_name|join(',')}}
+{% endif -%}
+
+{% if pado_delay %}
+pado-delay={{pado_delay}}
+{% endif %}
+
+{% if limits %}
+[connlimit]
+limit={{limits['conn-limit']}}
+burst={{limits['burst']}}
+timeout={{limits['timeout']}}
+{% endif %}
+
+[cli]
+tcp=127.0.0.1:2001
diff --git a/data/templates/pppoe/ip-down.script.tmpl b/data/templates/pppoe/ip-down.script.tmpl
new file mode 100644
index 000000000..a68fc099c
--- /dev/null
+++ b/data/templates/pppoe/ip-down.script.tmpl
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+# As PPPoE is an "on demand" interface we need to re-configure it when it
+# becomes up
+if [ "$6" != "{{ intf }}" ]; then
+ exit
+fi
+
+# add some info to syslog
+DIALER_PID=$(cat /var/run/{{ intf }}.pid)
+logger -t pppd[$DIALER_PID] "executing $0"
+
+{% if not on_demand %}
+# See https://phabricator.vyos.net/T2248. Determine if we are enslaved to a
+# VRF, this is needed to properly insert the default route.
+VRF_NAME=""
+if [ -d /sys/class/net/{{ intf }}/upper_* ]; then
+ # Determine upper (VRF) interface
+ VRF=$(basename $(ls -d /sys/class/net/{{ intf }}/upper_*))
+ # Remove upper_ prefix from result string
+ VRF=${VRF#"upper_"}
+ # Populate variable to run in VR context
+ VRF_NAME="vrf ${VRF_NAME}"
+fi
+
+# Always delete default route when interface goes down
+vtysh -c "conf t" ${VRF_NAME} -c "no ip route 0.0.0.0/0 {{ intf }} ${VRF_NAME}"
+{% endif %}
diff --git a/data/templates/pppoe/ip-pre-up.script.tmpl b/data/templates/pppoe/ip-pre-up.script.tmpl
new file mode 100644
index 000000000..9a7ba7d57
--- /dev/null
+++ b/data/templates/pppoe/ip-pre-up.script.tmpl
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+# As PPPoE is an "on demand" interface we need to re-configure it when it
+# becomes up
+
+if [ "$6" != "{{ intf }}" ]; then
+ exit
+fi
+
+# add some info to syslog
+DIALER_PID=$(cat /var/run/{{ intf }}.pid)
+logger -t pppd[$DIALER_PID] "executing $0"
+
+echo "{{ description }}" > /sys/class/net/{{ intf }}/ifalias
+
+{% if vrf -%}
+logger -t pppd[$DIALER_PID] "configuring dialer interface $6 for VRF {{ vrf }}"
+ip link set dev {{ intf }} master {{ vrf }}
+{% endif %}
diff --git a/data/templates/pppoe/ip-up.script.tmpl b/data/templates/pppoe/ip-up.script.tmpl
new file mode 100644
index 000000000..697ebcc20
--- /dev/null
+++ b/data/templates/pppoe/ip-up.script.tmpl
@@ -0,0 +1,47 @@
+#!/bin/sh
+
+# As PPPoE is an "on demand" interface we need to re-configure it when it
+# becomes up
+if [ "$6" != "{{ intf }}" ]; then
+ exit
+fi
+
+{% if not on_demand %}
+# See https://phabricator.vyos.net/T2248 & T2220. Determine if we are enslaved
+# to a VRF, this is needed to properly insert the default route.
+
+# add some info to syslog
+DIALER_PID=$(cat /var/run/{{ intf }}.pid)
+logger -t pppd[$DIALER_PID] "executing $0"
+
+SED_OPT="^ip route"
+VRF_NAME=""
+if [ -d /sys/class/net/{{ intf }}/upper_* ]; then
+ # Determine upper (VRF) interface
+ VRF=$(basename $(ls -d /sys/class/net/{{ intf }}/upper_*))
+ # Remove upper_ prefix from result string
+ VRF=${VRF#"upper_"}
+ # generate new SED command
+ SED_OPT="vrf ${VRF}"
+ # generate vtysh option
+ VRF_NAME="vrf ${VRF}"
+fi
+
+{% if 'auto' in default_route -%}
+# Only insert a new default route if there is no default route configured
+routes=$(vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep 0.0.0.0/0 | wc -l)
+if [ "$routes" -ne 0 ]; then
+ exit 1
+fi
+
+{% elif 'force' in default_route -%}
+# Retrieve current static default routes and remove it from the routing table
+vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep 0.0.0.0/0 | while read route ; do
+ vtysh -c "conf t" ${VTY_OPT} -c "no ${route} ${VRF_NAME}"
+done
+{% endif %}
+
+# Add default route to default or VRF routing table
+vtysh -c "conf t" ${VTY_OPT} -c "ip route 0.0.0.0/0 {{ intf }} ${VRF_NAME}"
+logger -t pppd[$DIALER_PID] "added default route via {{ intf }} ${VRF_NAME}"
+{% endif %}
diff --git a/data/templates/pppoe/ipv6-up.script.tmpl b/data/templates/pppoe/ipv6-up.script.tmpl
new file mode 100644
index 000000000..a4b08ddaf
--- /dev/null
+++ b/data/templates/pppoe/ipv6-up.script.tmpl
@@ -0,0 +1,41 @@
+#!/bin/sh
+
+# As PPPoE is an "on demand" interface we need to re-configure it when it
+# becomes up
+
+if [ "$6" != "{{ intf }}" ]; then
+ exit
+fi
+
+{% if ipv6_autoconf -%}
+# add some info to syslog
+DIALER_PID=$(cat /var/run/{{ intf }}.pid)
+logger -t pppd[$DIALER_PID] "executing $0"
+logger -t pppd[$DIALER_PID] "configuring interface {{ intf }} via {{ source_interface }}"
+
+# Configure interface-specific Host/Router behaviour.
+# Note: It is recommended to have the same setting on all interfaces; mixed
+# router/host scenarios are rather uncommon. Possible values are:
+#
+# 0 Forwarding disabled
+# 1 Forwarding enabled
+#
+echo 1 > /proc/sys/net/ipv6/conf/{{ intf }}/forwarding
+
+# Accept Router Advertisements; autoconfigure using them.
+#
+# It also determines whether or not to transmit Router
+# Solicitations. If and only if the functional setting is to
+# accept Router Advertisements, Router Solicitations will be
+# transmitted. Possible values are:
+#
+# 0 Do not accept Router Advertisements.
+# 1 Accept Router Advertisements if forwarding is disabled.
+# 2 Overrule forwarding behaviour. Accept Router Advertisements
+# even if forwarding is enabled.
+#
+echo 2 > /proc/sys/net/ipv6/conf/{{ intf }}/accept_ra
+
+# Autoconfigure addresses using Prefix Information in Router Advertisements.
+echo 1 > /proc/sys/net/ipv6/conf/{{ intf }}/autoconfigure
+{% endif %}
diff --git a/data/templates/pppoe/peer.tmpl b/data/templates/pppoe/peer.tmpl
new file mode 100644
index 000000000..36d108cee
--- /dev/null
+++ b/data/templates/pppoe/peer.tmpl
@@ -0,0 +1,72 @@
+### Autogenerated by interfaces-pppoe.py ###
+
+{% if description %}
+# {{ description }}
+{% endif %}
+
+# Require peer to provide the local IP address if it is not
+# specified explicitly in the config file.
+noipdefault
+
+# Don't show the password in logfiles:
+hide-password
+
+# Standard Link Control Protocol (LCP) parameters:
+lcp-echo-interval 20
+lcp-echo-failure 3
+
+# RFC 2516, paragraph 7 mandates that the following options MUST NOT be
+# requested and MUST be rejected if requested by the peer:
+# Address-and-Control-Field-Compression (ACFC)
+noaccomp
+
+# Asynchronous-Control-Character-Map (ACCM)
+default-asyncmap
+
+# Override any connect script that may have been set in /etc/ppp/options.
+connect /bin/true
+
+# Don't try to authenticate the remote node
+noauth
+
+# Don't try to proxy ARP for the remote endpoint. User can set proxy
+# arp entries up manually if they wish. More importantly, having
+# the "proxyarp" parameter set disables the "defaultroute" option.
+noproxyarp
+
+# Unlimited connection attempts
+maxfail 0
+
+plugin rp-pppoe.so
+{{ source_interface }}
+persist
+ifname {{ intf }}
+ipparam {{ intf }}
+debug
+logfile {{ logfile }}
+mtu {{ mtu }}
+mru {{ mtu }}
+user "{{ auth_username }}"
+password "{{ auth_password }}"
+{% if name_server -%}
+usepeerdns
+{% endif %}
+{% if ipv6_enable -%}
++ipv6
+ipv6cp-use-ipaddr
+{% endif %}
+{% if service_name -%}
+rp_pppoe_service "{{ service_name }}"
+{% endif %}
+{% if on_demand %}
+demand
+# See T2249. PPP default route options should only be set when in on-demand
+# mode. As soon as we are not in on-demand mode the default-route handling is
+# passed to the ip-up.d/ip-down.s scripts which is required for VRF support.
+{% if 'auto' in default_route -%}
+defaultroute
+{% elif 'force' in default_route -%}
+defaultroute
+replacedefaultroute
+{% endif %}
+{% endif %}
diff --git a/data/templates/pptp/chap-secrets.tmpl b/data/templates/pptp/chap-secrets.tmpl
new file mode 100644
index 000000000..f93f4607b
--- /dev/null
+++ b/data/templates/pptp/chap-secrets.tmpl
@@ -0,0 +1,6 @@
+# username server password acceptable local IP addresses
+{% for user in authentication['local-users'] %}
+{% if authentication['local-users'][user]['state'] == 'enabled' %}
+{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }}
+{% endif %}
+{% endfor %}
diff --git a/data/templates/pptp/pptp.config.tmpl b/data/templates/pptp/pptp.config.tmpl
new file mode 100644
index 000000000..2596507af
--- /dev/null
+++ b/data/templates/pptp/pptp.config.tmpl
@@ -0,0 +1,87 @@
+
+### generated by accel_pptp.py ###
+[modules]
+log_syslog
+pptp
+ippool
+chap-secrets
+{% if authentication['auth_proto'] %}
+{{ authentication['auth_proto'] }}
+{% else %}
+auth_mschap_v2
+{% endif %}
+{% if authentication['mode'] == 'radius' %}
+radius
+{% endif -%}
+
+[core]
+thread-count={{thread_cnt}}
+
+[log]
+syslog=accel-pptp,daemon
+copy=1
+level=5
+
+{% if dns %}
+[dns]
+{% if dns[0] %}
+dns1={{dns[0]}}
+{% endif %}
+{% if dns[1] %}
+dns2={{dns[1]}}
+{% endif %}
+{% endif %}
+
+{% if wins %}
+[wins]
+{% if wins[0] %}
+wins1={{wins[0]}}
+{% endif %}
+{% if wins[1] %}
+wins2={{wins[1]}}
+{% endif %}
+{% endif %}
+
+[pptp]
+ifname=pptp%d
+{% if outside_addr %}
+bind={{outside_addr}}
+{% endif %}
+verbose=1
+ppp-max-mtu={{mtu}}
+mppe={{authentication['mppe']}}
+echo-interval=10
+echo-failure=3
+
+
+[client-ip-range]
+0.0.0.0/0
+
+[ip-pool]
+tunnel={{client_ip_pool}}
+gw-ip-address={{gw_ip}}
+
+{% if authentication['mode'] == 'local' %}
+[chap-secrets]
+chap-secrets=/etc/accel-ppp/pptp/chap-secrets
+{% endif %}
+
+[ppp]
+verbose=5
+check-ip=1
+single-session=replace
+
+{% if authentication['mode'] == 'radius' %}
+[radius]
+{% for rsrv in authentication['radiussrv']: %}
+server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\
+req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\
+fail-time={{authentication['radiussrv'][rsrv]['fail-time']}}
+{% endfor %}
+timeout=30
+acct-timeout=30
+max-try=3
+{%endif %}
+
+[cli]
+tcp=127.0.0.1:2003
diff --git a/data/templates/router-advert/radvd.conf.tmpl b/data/templates/router-advert/radvd.conf.tmpl
new file mode 100644
index 000000000..2768f6f2e
--- /dev/null
+++ b/data/templates/router-advert/radvd.conf.tmpl
@@ -0,0 +1,37 @@
+### Autogenerated by service-router-advert.py ###
+
+{% for i in interfaces -%}
+interface {{ i.name }} {
+ IgnoreIfMissing on;
+ AdvDefaultPreference {{ i.default_preference }};
+ AdvManagedFlag {{ i.managed_flag }};
+ MaxRtrAdvInterval {{ i.interval_max }};
+{% if i.interval_min %}
+ MinRtrAdvInterval {{ i.interval_min }};
+{% endif %}
+ AdvReachableTime {{ i.reachable_time }};
+ AdvIntervalOpt {{ i.send_advert }};
+ AdvSendAdvert {{ i.send_advert }};
+{% if i.default_lifetime %}
+ AdvDefaultLifetime {{ i.default_lifetime }};
+{% endif %}
+{% if i.link_mtu %}
+ AdvLinkMTU {{ i.link_mtu }};
+{% endif %}
+ AdvOtherConfigFlag {{ i.other_config_flag }};
+ AdvRetransTimer {{ i.retrans_timer }};
+ AdvCurHopLimit {{ i.hop_limit }};
+{% for p in i.prefixes %}
+ prefix {{ p.prefix }} {
+ AdvAutonomous {{ p.autonomous_flag }};
+ AdvValidLifetime {{ p.valid_lifetime }};
+ AdvOnLink {{ p.on_link }};
+ AdvPreferredLifetime {{ p.preferred_lifetime }};
+ };
+{% endfor %}
+{% if i.name_server %}
+ RDNSS {{ i.name_server | join(" ") }} {
+ };
+{% endif %}
+};
+{% endfor -%}
diff --git a/data/templates/salt-minion/minion.tmpl b/data/templates/salt-minion/minion.tmpl
new file mode 100644
index 000000000..5e50d588c
--- /dev/null
+++ b/data/templates/salt-minion/minion.tmpl
@@ -0,0 +1,63 @@
+### Autogenerated by salt-minion.py ###
+
+##### Primary configuration settings #####
+##########################################
+
+# The hash_type is the hash to use when discovering the hash of a file on
+# the master server. The default is sha256, but md5, sha1, sha224, sha384 and
+# sha512 are also supported.
+#
+# WARNING: While md5 and sha1 are also supported, do not use them due to the
+# high chance of possible collisions and thus security breach.
+#
+# Prior to changing this value, the master should be stopped and all Salt
+# caches should be cleared.
+hash_type: {{ hash_type }}
+
+##### Logging settings #####
+##########################################
+# The location of the minion log file
+# The minion log can be sent to a regular file, local path name, or network
+# location. Remote logging works best when configured to use rsyslogd(8) (e.g.:
+# ``file:///dev/log``), with rsyslogd(8) configured for network logging. The URI
+# format is: <file|udp|tcp>://<host|socketpath>:<port-if-required>/<log-facility>
+#log_file: /var/log/salt/minion
+#log_file: file:///dev/log
+#log_file: udp://loghost:10514
+#
+log_file: {{ log_file }}
+
+# The level of messages to send to the console.
+# One of 'garbage', 'trace', 'debug', info', 'warning', 'error', 'critical'.
+#
+# The following log levels are considered INSECURE and may log sensitive data:
+# ['garbage', 'trace', 'debug']
+#
+# Default: 'warning'
+log_level: {{ log_level }}
+
+# Set the location of the salt master server, if the master server cannot be
+# resolved, then the minion will fail to start.
+master:
+{% for host in master -%}
+- {{ host }}
+{% endfor %}
+
+# The user to run salt
+user: {{ user }}
+
+# The directory to store the pki information in
+pki_dir: /config/salt/pki/minion
+
+# Explicitly declare the id for this minion to use, if left commented the id
+# will be the hostname as returned by the python call: socket.getfqdn()
+# Since salt uses detached ids it is possible to run multiple minions on the
+# same machine but with different ids, this can be useful for salt compute
+# clusters.
+id: {{ salt_id }}
+
+
+# The number of minutes between mine updates.
+mine_interval: {{ mine_interval }}
+
+verify_master_pubkey_sign: {{ verify_master_pubkey_sign }}
diff --git a/data/templates/snmp/etc.snmp.conf.tmpl b/data/templates/snmp/etc.snmp.conf.tmpl
new file mode 100644
index 000000000..159578906
--- /dev/null
+++ b/data/templates/snmp/etc.snmp.conf.tmpl
@@ -0,0 +1,4 @@
+### Autogenerated by snmp.py ###
+{% if trap_source -%}
+clientaddr {{ trap_source }}
+{% endif %}
diff --git a/data/templates/snmp/etc.snmpd.conf.tmpl b/data/templates/snmp/etc.snmpd.conf.tmpl
new file mode 100644
index 000000000..1659abf93
--- /dev/null
+++ b/data/templates/snmp/etc.snmpd.conf.tmpl
@@ -0,0 +1,118 @@
+### Autogenerated by snmp.py ###
+
+# non configurable defaults
+sysObjectID 1.3.6.1.4.1.44641
+sysServices 14
+master agentx
+agentXPerms 0777 0777
+pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias
+smuxpeer .1.3.6.1.2.1.83
+smuxpeer .1.3.6.1.2.1.157
+smuxsocket localhost
+
+# linkUp/Down configure the Event MIB tables to monitor
+# the ifTable for network interfaces being taken up or down
+# for making internal queries to retrieve any necessary information
+iquerySecName {{ vyos_user }}
+
+# Modified from the default linkUpDownNotification
+# to include more OIDs and poll more frequently
+notificationEvent linkUpTrap linkUp ifIndex ifDescr ifType ifAdminStatus ifOperStatus
+notificationEvent linkDownTrap linkDown ifIndex ifDescr ifType ifAdminStatus ifOperStatus
+monitor -r 10 -e linkUpTrap "Generate linkUp" ifOperStatus != 2
+monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2
+
+########################
+# configurable section #
+########################
+
+# Default system description is VyOS version
+sysDescr VyOS {{ version }}
+
+{% if description %}
+# Description
+SysDescr {{ description }}
+{%- endif %}
+
+# Listen
+agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161{% if ipv6_enabled %},udp6:161{% endif %}{% endif %}
+
+# SNMP communities
+{%- for c in communities %}
+
+{%- if c.network_v4 %}
+{%- for network in c.network_v4 %}
+{{ c.authorization }}community {{ c.name }} {{ network }}
+{%- endfor %}
+{%- elif not c.has_source %}
+{{ c.authorization }}community {{ c.name }}
+{%- endif %}
+
+{%- if c.network_v6 %}
+{%- for network in c.network_v6 %}
+{{ c.authorization }}community6 {{ c.name }} {{ network }}
+{%- endfor %}
+{%- elif not c.has_source %}
+{{ c.authorization }}community6 {{ c.name }}
+{%- endif %}
+
+{%- endfor %}
+
+{% if contact %}
+# system contact information
+SysContact {{ contact }}
+{%- endif %}
+
+{% if location %}
+# system location information
+SysLocation {{ location }}
+{%- endif %}
+
+{% if smux_peers -%}
+# additional smux peers
+{%- for sp in smux_peers %}
+smuxpeer {{ sp }}
+{%- endfor %}
+{%- endif %}
+
+{% if trap_targets -%}
+# if there is a problem - tell someone!
+{%- for t in trap_targets %}
+trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }}
+{%- endfor %}
+{%- endif %}
+
+{%- if v3_enabled %}
+#
+# SNMPv3 stuff goes here
+#
+# views
+{%- for v in v3_views %}
+{%- for oid in v.oids %}
+view {{ v.name }} included .{{ oid.oid }}
+{%- endfor %}
+{%- endfor %}
+
+# access
+# context sec.model sec.level match read write notif
+{%- for g in v3_groups %}
+access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none
+{%- endfor %}
+
+# trap-target
+{%- for t in v3_traps %}
+trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ v3_engineid }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }}
+{%- endfor %}
+
+# group
+{%- for u in v3_users %}
+group {{ u.group }} usm {{ u.name }}
+{% endfor %}
+{%- endif %}
+
+{% if script_ext %}
+# extension scripts
+{%- for ext in script_ext|sort(attribute='name') %}
+extend {{ ext.name }} {{ ext.script }}
+{%- endfor %}
+{% endif %}
diff --git a/data/templates/snmp/usr.snmpd.conf.tmpl b/data/templates/snmp/usr.snmpd.conf.tmpl
new file mode 100644
index 000000000..9c0337fa8
--- /dev/null
+++ b/data/templates/snmp/usr.snmpd.conf.tmpl
@@ -0,0 +1,6 @@
+### Autogenerated by snmp.py ###
+{%- for u in v3_users %}
+{{ u.mode }}user {{ u.name }}
+{%- endfor %}
+
+rwuser {{ vyos_user }}
diff --git a/data/templates/snmp/var.snmpd.conf.tmpl b/data/templates/snmp/var.snmpd.conf.tmpl
new file mode 100644
index 000000000..0b8e9f291
--- /dev/null
+++ b/data/templates/snmp/var.snmpd.conf.tmpl
@@ -0,0 +1,16 @@
+### Autogenerated by snmp.py ###
+# user
+{%- for u in v3_users %}
+{%- if u.authOID == 'none' %}
+createUser {{ u.name }}
+{%- elif u.authPassword %}
+createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }}
+{%- else %}
+usmUser 1 3 {{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x
+{%- endif %}
+{%- endfor %}
+
+createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES
+{%- if v3_engineid %}
+oldEngineID {{ v3_engineid }}
+{%- endif %}
diff --git a/data/templates/ssh/sshd_config.tmpl b/data/templates/ssh/sshd_config.tmpl
new file mode 100644
index 000000000..5deb5232a
--- /dev/null
+++ b/data/templates/ssh/sshd_config.tmpl
@@ -0,0 +1,125 @@
+### Autogenerated by ssh.py ###
+
+# Non-configurable defaults
+Protocol 2
+HostKey /etc/ssh/ssh_host_rsa_key
+HostKey /etc/ssh/ssh_host_dsa_key
+HostKey /etc/ssh/ssh_host_ecdsa_key
+HostKey /etc/ssh/ssh_host_ed25519_key
+SyslogFacility AUTH
+LoginGraceTime 120
+StrictModes yes
+PubkeyAuthentication yes
+IgnoreRhosts yes
+HostbasedAuthentication no
+PermitEmptyPasswords no
+ChallengeResponseAuthentication no
+X11Forwarding yes
+X11DisplayOffset 10
+PrintMotd no
+PrintLastLog yes
+TCPKeepAlive yes
+Banner /etc/issue.net
+Subsystem sftp /usr/lib/openssh/sftp-server
+UsePAM yes
+HostKey /etc/ssh/ssh_host_rsa_key
+
+# Specifies whether sshd should look up the remote host name,
+# and to check that the resolved host name for the remote IP
+# address maps back to the very same IP address.
+UseDNS {{ host_validation }}
+
+# Specifies the port number that sshd listens on. The default is 22.
+# Multiple options of this type are permitted.
+{% if mport|length != 0 %}
+{% for p in mport %}
+Port {{ p }}
+{% endfor %}
+{% else %}
+Port {{ port }}
+{% endif %}
+
+# Gives the verbosity level that is used when logging messages from sshd
+LogLevel {{ log_level }}
+
+# Specifies whether root can log in using ssh
+PermitRootLogin no
+
+# Specifies whether password authentication is allowed
+PasswordAuthentication {{ password_authentication }}
+
+{% if listen_on %}
+# Specifies the local addresses sshd should listen on
+{% for a in listen_on %}
+ListenAddress {{ a }}
+{% endfor %}
+{{ "\n" }}
+{% endif %}
+
+{%- if ciphers %}
+# Specifies the ciphers allowed. Multiple ciphers must be comma-separated.
+#
+# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/
+Ciphers {{ ciphers | join(",") }}
+{{ "\n" }}
+{% endif %}
+
+{%- if mac %}
+# Specifies the available MAC (message authentication code) algorithms. The MAC
+# algorithm is used for data integrity protection. Multiple algorithms must be
+# comma-separated.
+#
+# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/
+MACs {{ mac | join(",") }}
+{{ "\n" }}
+{% endif %}
+
+{%- if key_exchange %}
+# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must
+# be comma-separated.
+#
+# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/
+KexAlgorithms {{ key_exchange | join(",") }}
+{{ "\n" }}
+{% endif %}
+
+{%- if allow_users %}
+# This keyword can be followed by a list of user name patterns, separated by spaces.
+# If specified, login is allowed only for user names that match one of the patterns.
+# Only user names are valid, a numerical user ID is not recognized.
+AllowUsers {{ allow_users | join(" ") }}
+{{ "\n" }}
+{% endif %}
+
+{%- if allow_groups %}
+# This keyword can be followed by a list of group name patterns, separated by spaces.
+# If specified, login is allowed only for users whose primary group or supplementary
+# group list matches one of the patterns. Only group names are valid, a numerical group
+# ID is not recognized.
+AllowGroups {{ allow_groups | join(" ") }}
+{{ "\n" }}
+{% endif %}
+
+{%- if deny_users %}
+# This keyword can be followed by a list of user name patterns, separated by spaces.
+# Login is disallowed for user names that match one of the patterns. Only user names
+# are valid, a numerical user ID is not recognized.
+DenyUsers {{ deny_users | join(" ") }}
+{{ "\n" }}
+{% endif %}
+
+{%- if deny_groups %}
+# This keyword can be followed by a list of group name patterns, separated by spaces.
+# Login is disallowed for users whose primary group or supplementary group list matches
+# one of the patterns. Only group names are valid, a numerical group ID is not recognized.
+DenyGroups {{ deny_groups | join(" ") }}
+{{ "\n" }}
+{% endif %}
+
+{%- if client_keepalive %}
+# Sets a timeout interval in seconds after which if no data has been received from the client,
+# sshd will send a message through the encrypted channel to request a response from the client.
+# The default is 0, indicating that these messages will not be sent to the client.
+# This option applies to protocol version 2 only.
+ClientAliveInterval {{ client_keepalive }}
+{% endif %}
diff --git a/data/templates/sstp/chap-secrets.tmpl b/data/templates/sstp/chap-secrets.tmpl
new file mode 100644
index 000000000..dd00d7bd0
--- /dev/null
+++ b/data/templates/sstp/chap-secrets.tmpl
@@ -0,0 +1,10 @@
+# username server password acceptable local IP addresses shaper
+{% for user in local_users %}
+{% if user.state == 'enabled' %}
+{% if user.upload and user.download %}
+{{ "%-12s" | format(user.name) }} * {{ "%-16s" | format(user.password) }} {{ "%-16s" | format(user.ip) }} {{ user.download }} / {{ user.upload }}
+{% else %}
+{{ "%-12s" | format(user.name) }} * {{ "%-16s" | format(user.password) }} {{ "%-16s" | format(user.ip) }}
+{% endif %}
+{% endif %}
+{% endfor %}
diff --git a/data/templates/sstp/sstp.config.tmpl b/data/templates/sstp/sstp.config.tmpl
new file mode 100644
index 000000000..19805358e
--- /dev/null
+++ b/data/templates/sstp/sstp.config.tmpl
@@ -0,0 +1,114 @@
+### generated by vpn_sstp.py ###
+[modules]
+log_syslog
+sstp
+shaper
+{% if auth_mode == 'local' %}
+chap-secrets
+{% elif auth_mode == 'radius' %}
+radius
+{% endif -%}
+ippool
+
+{% for proto in auth_proto %}
+{{proto}}
+{% endfor %}
+
+[core]
+thread-count={{thread_cnt}}
+
+[common]
+single-session=replace
+
+[log]
+syslog=accel-sstp,daemon
+copy=1
+level=5
+
+[client-ip-range]
+disable
+
+[sstp]
+verbose=1
+accept=ssl
+ssl-ca-file={{ ssl_ca }}
+ssl-pemfile={{ ssl_cert }}
+ssl-keyfile={{ ssl_key }}
+
+{% if client_ip_pool %}
+[ip-pool]
+gw-ip-address={{ client_gateway }}
+{% for subnet in client_ip_pool %}
+{{ subnet }}
+{% endfor %}
+{% endif %}
+
+{% if dnsv4 %}
+[dns]
+{% for dns in dnsv4 -%}
+dns{{ loop.index }}={{ dns }}
+{% endfor -%}
+{% endif %}
+
+{% if auth_mode == 'local' %}
+[chap-secrets]
+chap-secrets=/etc/accel-ppp/sstp/chap-secrets
+{% elif auth_mode == 'radius' %}
+[radius]
+verbose=1
+{% for r in radius_server %}
+server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }}
+{% endfor -%}
+
+acct-timeout={{ radius_acct_tmo }}
+timeout={{ radius_timeout }}
+max-try={{ radius_max_try }}
+
+{% if radius_nas_id %}
+nas-identifier={{ radius_nas_id }}
+{% endif -%}
+{% if radius_nas_ip %}
+nas-ip-address={{ radius_nas_ip }}
+{% endif -%}
+{% if radius_source_address %}
+bind={{ radius_source_address }}
+{% endif -%}
+
+
+{% if radius_dynamic_author %}
+dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }}
+{% endif -%}
+{% endif %}
+
+[ppp]
+verbose=1
+check-ip=1
+{% if mtu %}
+mtu={{ mtu }}
+{% endif -%}
+
+{% if ppp_mppe %}
+mppe={{ ppp_mppe }}
+{% endif -%}
+{% if ppp_echo_interval %}
+lcp-echo-interval={{ ppp_echo_interval }}
+{% endif -%}
+{% if ppp_echo_failure %}
+lcp-echo-failure={{ ppp_echo_failure }}
+{% endif -%}
+{% if ppp_echo_timeout %}
+lcp-echo-timeout={{ ppp_echo_timeout }}
+{% endif %}
+
+{% if radius_shaper_attr %}
+[shaper]
+verbose=1
+attr={{ radius_shaper_attr }}
+{% if radius_shaper_vendor %}
+vendor={{ radius_shaper_vendor }}
+{% endif -%}
+{% endif %}
+
+[cli]
+tcp=127.0.0.1:2005
+
diff --git a/data/templates/syslog/logrotate.tmpl b/data/templates/syslog/logrotate.tmpl
new file mode 100644
index 000000000..f758265e4
--- /dev/null
+++ b/data/templates/syslog/logrotate.tmpl
@@ -0,0 +1,12 @@
+{% for file in files %}
+{{files[file]['log-file']}} {
+ missingok
+ notifempty
+ create
+ rotate {{files[file]['max-files']}}
+ size={{files[file]['max-size']//1024}}k
+ postrotate
+ invoke-rc.d rsyslog rotate > /dev/null
+ endscript
+}
+{% endfor %}
diff --git a/data/templates/syslog/rsyslog.conf.tmpl b/data/templates/syslog/rsyslog.conf.tmpl
new file mode 100644
index 000000000..bc3f7667b
--- /dev/null
+++ b/data/templates/syslog/rsyslog.conf.tmpl
@@ -0,0 +1,44 @@
+## generated by syslog.py ##
+## file based logging
+{% if files['global']['marker'] -%}
+$ModLoad immark
+{% if files['global']['marker-interval'] %}
+$MarkMessagePeriod {{files['global']['marker-interval']}}
+{% endif %}
+{% endif -%}
+{% if files['global']['preserver_fqdn'] -%}
+$PreserveFQDN on
+{% endif -%}
+{% for file in files %}
+$outchannel {{file}},{{files[file]['log-file']}},{{files[file]['max-size']}},{{files[file]['action-on-max-size']}}
+{{files[file]['selectors']}} :omfile:${{file}}
+{% endfor %}
+{% if console %}
+## console logging
+{% for con in console %}
+{{console[con]['selectors']}} /dev/console
+{% endfor %}
+{% endif %}
+{% if hosts %}
+## remote logging
+{% for host in hosts %}
+{% if hosts[host]['proto'] == 'tcp' %}
+{% if hosts[host]['port'] %}
+{{hosts[host]['selectors']}} @@{{host}}:{{hosts[host]['port']}}
+{% else %}
+{{hosts[host]['selectors']}} @@{{host}}
+{% endif %}
+{% else %}
+{% if hosts[host]['port'] %}
+{{hosts[host]['selectors']}} @{{host}}:{{hosts[host]['port']}}
+{% else %}
+{{hosts[host]['selectors']}} @{{host}}
+{% endif %}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% if user %}
+{% for u in user %}
+{{user[u]['selectors']}} :omusrmsg:{{u}}
+{% endfor %}
+{% endif %}
diff --git a/data/templates/system-login/pam_radius_auth.conf.tmpl b/data/templates/system-login/pam_radius_auth.conf.tmpl
new file mode 100644
index 000000000..6cff67867
--- /dev/null
+++ b/data/templates/system-login/pam_radius_auth.conf.tmpl
@@ -0,0 +1,13 @@
+# Automatically generated by VyOS
+# RADIUS configuration file
+{%- if radius_server %}
+# server[:port] shared_secret timeout (s) source_ip
+{% for s in radius_server %}
+{%- if not s.disabled -%}
+{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if radius_source_address -%}{{ radius_source_address }}{% endif %}
+{% endif %}
+{%- endfor %}
+
+priv-lvl 15
+mapped_priv_user radius_priv_user
+{% endif %}
diff --git a/data/templates/tftp-server/default.tmpl b/data/templates/tftp-server/default.tmpl
new file mode 100644
index 000000000..18fee35d1
--- /dev/null
+++ b/data/templates/tftp-server/default.tmpl
@@ -0,0 +1,2 @@
+### Autogenerated by tftp_server.py ###
+DAEMON_ARGS="--listen --user tftp --address {% for a in listen-%}{{ a }}{% endfor %}{% if allow_upload %} --create --umask 000{% endif %} --secure {{ directory }}"
diff --git a/data/templates/vrf/vrf.conf.tmpl b/data/templates/vrf/vrf.conf.tmpl
new file mode 100644
index 000000000..761b0bb6f
--- /dev/null
+++ b/data/templates/vrf/vrf.conf.tmpl
@@ -0,0 +1,8 @@
+### Autogenerated by vrf.py ###
+#
+# Routing table ID to name mapping reference
+
+# id vrf name comment
+{% for vrf in vrf_add -%}
+{{ "%-10s" | format(vrf.table) }} {{ "%-16s" | format(vrf.name) }} # {{ vrf.description }}
+{% endfor -%}
diff --git a/data/templates/vrrp/daemon.tmpl b/data/templates/vrrp/daemon.tmpl
new file mode 100644
index 000000000..62d0ba63b
--- /dev/null
+++ b/data/templates/vrrp/daemon.tmpl
@@ -0,0 +1,5 @@
+# Autogenerated by VyOS
+# Options to pass to keepalived
+
+# DAEMON_ARGS are appended to the keepalived command-line
+DAEMON_ARGS="--snmp"
diff --git a/data/templates/vrrp/keepalived.conf.tmpl b/data/templates/vrrp/keepalived.conf.tmpl
new file mode 100644
index 000000000..08b821f70
--- /dev/null
+++ b/data/templates/vrrp/keepalived.conf.tmpl
@@ -0,0 +1,97 @@
+# Autogenerated by VyOS
+# Do not edit this file, all your changes will be lost
+# on next commit or reboot
+
+global_defs {
+ dynamic_interfaces
+ script_user root
+ notify_fifo /run/keepalived_notify_fifo
+ notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py
+}
+
+{% for group in groups -%}
+
+{% if group.health_check_script -%}
+vrrp_script healthcheck_{{ group.name }} {
+ script "{{ group.health_check_script }}"
+ interval {{ group.health_check_interval }}
+ fall {{ group.health_check_count }}
+ rise 1
+
+}
+{% endif %}
+
+vrrp_instance {{ group.name }} {
+ {% if group.description -%}
+ # {{ group.description }}
+ {% endif -%}
+
+ state BACKUP
+ interface {{ group.interface }}
+ virtual_router_id {{ group.vrid }}
+ priority {{ group.priority }}
+ advert_int {{ group.advertise_interval }}
+
+ {% if group.preempt -%}
+ preempt_delay {{ group.preempt_delay }}
+ {% else -%}
+ nopreempt
+ {% endif -%}
+
+ {% if group.peer_address -%}
+ unicast_peer { {{ group.peer_address }} }
+ {% endif -%}
+
+ {% if group.hello_source -%}
+ {%- if group.peer_address -%}
+ unicast_src_ip {{ group.hello_source }}
+ {%- else -%}
+ mcast_src_ip {{ group.hello_source }}
+ {%- endif %}
+ {% endif -%}
+
+ {% if group.use_vmac and group.peer_address -%}
+ use_vmac {{group.interface}}v{{group.vrid}}
+ vmac_xmit_base
+ {% elif group.use_vmac -%}
+ use_vmac {{group.interface}}v{{group.vrid}}
+ {% endif -%}
+
+ {% if group.auth_password -%}
+ authentication {
+ auth_pass "{{ group.auth_password }}"
+ auth_type {{ group.auth_type }}
+ }
+ {% endif -%}
+
+ virtual_ipaddress {
+ {% for addr in group.virtual_addresses -%}
+ {{ addr }}
+ {% endfor -%}
+ }
+
+ {% if group.health_check_script -%}
+ track_script {
+ healthcheck_{{ group.name }}
+ }
+ {% endif -%}
+}
+
+{% endfor -%}
+
+{% for sync_group in sync_groups -%}
+vrrp_sync_group {{ sync_group.name }} {
+ group {
+ {% for member in sync_group.members -%}
+ {{ member }}
+ {% endfor -%}
+ }
+
+ {% if sync_group.conntrack_sync -%}
+ notify_master "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh master {{ sync_group.name }}"
+ notify_backup "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh backup {{ sync_group.name }}"
+ notify_fault "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh fault {{ sync_group.name }}"
+ {% endif -%}
+}
+
+{% endfor -%}
diff --git a/data/templates/wifi/cfg80211.conf.tmpl b/data/templates/wifi/cfg80211.conf.tmpl
new file mode 100644
index 000000000..b21bacc1e
--- /dev/null
+++ b/data/templates/wifi/cfg80211.conf.tmpl
@@ -0,0 +1,3 @@
+{%- if regdom -%}
+options cfg80211 ieee80211_regdom={{ regdom }}
+{% endif %}
diff --git a/data/templates/wifi/crda.tmpl b/data/templates/wifi/crda.tmpl
new file mode 100644
index 000000000..750ad86ee
--- /dev/null
+++ b/data/templates/wifi/crda.tmpl
@@ -0,0 +1,3 @@
+{%- if regdom -%}
+REGDOMAIN={{ regdom }}
+{% endif %}
diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl
new file mode 100644
index 000000000..031fb6c90
--- /dev/null
+++ b/data/templates/wifi/hostapd.conf.tmpl
@@ -0,0 +1,728 @@
+### Autogenerated by interfaces-wireless.py ###
+{% if description %}
+# Description: {{ description }}
+# User-friendly description of device; up to 32 octets encoded in UTF-8
+device_name={{ description | truncate(32, True) }}
+{% endif %}
+
+# AP netdevice name (without 'ap' postfix, i.e., wlan0 uses wlan0ap for
+# management frames with the Host AP driver); wlan0 with many nl80211 drivers
+# Note: This attribute can be overridden by the values supplied with the '-i'
+# command line parameter.
+interface={{ intf }}
+
+# Driver interface type (hostap/wired/none/nl80211/bsd);
+# default: hostap). nl80211 is used with all Linux mac80211 drivers.
+# Use driver=none if building hostapd as a standalone RADIUS server that does
+# not control any wireless/wired driver.
+driver=nl80211
+
+# Levels (minimum value for logged events):
+# 0 = verbose debugging
+# 1 = debugging
+# 2 = informational messages
+# 3 = notification
+# 4 = warning
+logger_syslog=-1
+logger_syslog_level=0
+logger_stdout=-1
+logger_stdout_level=0
+
+{%- if country_code %}
+
+# Country code (ISO/IEC 3166-1). Used to set regulatory domain.
+# Set as needed to indicate country in which device is operating.
+# This can limit available channels and transmit power.
+country_code={{ country_code }}
+
+# Enable IEEE 802.11d. This advertises the country_code and the set of allowed
+# channels and transmit power levels based on the regulatory limits. The
+# country_code setting must be configured with the correct country for
+# IEEE 802.11d functions.
+ieee80211d=1
+{% endif %}
+
+{%- if ssid %}
+
+# SSID to be used in IEEE 802.11 management frames
+ssid={{ ssid }}
+{% endif %}
+
+{%- if channel %}
+
+# Channel number (IEEE 802.11)
+# (default: 0, i.e., not set)
+# Please note that some drivers do not use this value from hostapd and the
+# channel will need to be configured separately with iwconfig.
+#
+# If CONFIG_ACS build option is enabled, the channel can be selected
+# automatically at run time by setting channel=acs_survey or channel=0, both of
+# which will enable the ACS survey based algorithm.
+channel={{ channel }}
+{% endif %}
+
+{%- if mode %}
+
+# Operation mode (a = IEEE 802.11a (5 GHz), b = IEEE 802.11b (2.4 GHz),
+# g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used
+# with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this
+# needs to be set to hw_mode=a. For IEEE 802.11ax (HE) on 6 GHz this needs
+# to be set to hw_mode=a. When using ACS (see channel parameter), a
+# special value "any" can be used to indicate that any support band can be used.
+# This special case is currently supported only with drivers with which
+# offloaded ACS is used.
+{% if 'n' in mode -%}
+hw_mode=g
+ieee80211n=1
+{% elif 'ac' in mode -%}
+hw_mode=a
+ieee80211h=1
+ieee80211ac=1
+{% else -%}
+hw_mode={{ mode }}
+{% endif %}
+{% endif %}
+
+# ieee80211w: Whether management frame protection (MFP) is enabled
+# 0 = disabled (default)
+# 1 = optional
+# 2 = required
+{% if 'disabled' in mgmt_frame_protection -%}
+ieee80211w=0
+{% elif 'optional' in mgmt_frame_protection -%}
+ieee80211w=1
+{% elif 'required' in mgmt_frame_protection -%}
+ieee80211w=2
+{% endif %}
+
+# ht_capab: HT capabilities (list of flags)
+# LDPC coding capability: [LDPC] = supported
+# Supported channel width set: [HT40-] = both 20 MHz and 40 MHz with secondary
+# channel below the primary channel; [HT40+] = both 20 MHz and 40 MHz
+# with secondary channel above the primary channel
+# (20 MHz only if neither is set)
+# Note: There are limits on which channels can be used with HT40- and
+# HT40+. Following table shows the channels that may be available for
+# HT40- and HT40+ use per IEEE 802.11n Annex J:
+# freq HT40- HT40+
+# 2.4 GHz 5-13 1-7 (1-9 in Europe/Japan)
+# 5 GHz 40,48,56,64 36,44,52,60
+# (depending on the location, not all of these channels may be available
+# for use)
+# Please note that 40 MHz channels may switch their primary and secondary
+# channels if needed or creation of 40 MHz channel maybe rejected based
+# on overlapping BSSes. These changes are done automatically when hostapd
+# is setting up the 40 MHz channel.
+# Spatial Multiplexing (SM) Power Save: [SMPS-STATIC] or [SMPS-DYNAMIC]
+# (SMPS disabled if neither is set)
+# HT-greenfield: [GF] (disabled if not set)
+# Short GI for 20 MHz: [SHORT-GI-20] (disabled if not set)
+# Short GI for 40 MHz: [SHORT-GI-40] (disabled if not set)
+# Tx STBC: [TX-STBC] (disabled if not set)
+# Rx STBC: [RX-STBC1] (one spatial stream), [RX-STBC12] (one or two spatial
+# streams), or [RX-STBC123] (one, two, or three spatial streams); Rx STBC
+# disabled if none of these set
+# HT-delayed Block Ack: [DELAYED-BA] (disabled if not set)
+# Maximum A-MSDU length: [MAX-AMSDU-7935] for 7935 octets (3839 octets if not
+# set)
+# DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set)
+# 40 MHz intolerant [40-INTOLERANT] (not advertised if not set)
+# L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set)
+{% if cap_ht %}
+ht_capab=
+{%- endif -%}
+
+{%- if cap_ht_40mhz_incapable -%}
+[40-INTOLERANT]
+{%- endif -%}
+
+{%- if cap_ht_delayed_block_ack -%}
+[DELAYED-BA]
+{%- endif -%}
+
+{%- if cap_ht_dsss_cck_40 -%}
+[DSSS_CCK-40]
+{%- endif -%}
+
+{%- if cap_ht_greenfield -%}
+[GF]
+{%- endif -%}
+
+{%- if cap_ht_ldpc -%}
+[LDPC]
+{%- endif -%}
+
+{%- if cap_ht_lsig_protection -%}
+[LSIG-TXOP-PROT]
+{%- endif -%}
+
+{%- if cap_ht_max_amsdu -%}
+[MAX-AMSDU-{{ cap_ht_max_amsdu }}]
+{%- endif -%}
+
+{%- if cap_ht_smps -%}
+[SMPS-{{ cap_ht_smps | upper }}]
+{%- endif -%}
+
+{%- if cap_ht_chan_set_width -%}
+{%- for csw in cap_ht_chan_set_width -%}
+[{{ csw | upper }}]
+{%- endfor -%}
+{%- endif -%}
+
+{%- if cap_ht_short_gi -%}
+{%- for gi in cap_ht_short_gi -%}
+[SHORT-GI-{{ gi }}]
+{%- endfor -%}
+{%- endif -%}
+
+{%- if cap_ht_stbc_tx -%}
+[TX-STBC]
+{%- endif -%}
+{%- if cap_ht_stbc_rx -%}
+[RX-STBC{{ cap_ht_stbc_rx }}]
+{%- endif %}
+
+# Required for full HT and VHT functionality
+wme_enabled=1
+
+{% if cap_ht_powersave -%}
+# WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD]
+# Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver)
+uapsd_advertisement_enabled=1
+{%- endif %}
+
+{% if cap_req_ht -%}
+# Require stations to support HT PHY (reject association if they do not)
+require_ht=1
+{% endif %}
+
+{%- if cap_vht_chan_set_width -%}
+vht_oper_chwidth={{ cap_vht_chan_set_width }}
+{%- endif %}
+
+# vht_capab: VHT capabilities (list of flags)
+#
+# vht_max_mpdu_len: [MAX-MPDU-7991] [MAX-MPDU-11454]
+# Indicates maximum MPDU length
+# 0 = 3895 octets (default)
+# 1 = 7991 octets
+# 2 = 11454 octets
+# 3 = reserved
+#
+# supported_chan_width: [VHT160] [VHT160-80PLUS80]
+# Indicates supported Channel widths
+# 0 = 160 MHz & 80+80 channel widths are not supported (default)
+# 1 = 160 MHz channel width is supported
+# 2 = 160 MHz & 80+80 channel widths are supported
+# 3 = reserved
+#
+# Rx LDPC coding capability: [RXLDPC]
+# Indicates support for receiving LDPC coded pkts
+# 0 = Not supported (default)
+# 1 = Supported
+#
+# Short GI for 80 MHz: [SHORT-GI-80]
+# Indicates short GI support for reception of packets transmitted with TXVECTOR
+# params format equal to VHT and CBW = 80Mhz
+# 0 = Not supported (default)
+# 1 = Supported
+#
+# Short GI for 160 MHz: [SHORT-GI-160]
+# Indicates short GI support for reception of packets transmitted with TXVECTOR
+# params format equal to VHT and CBW = 160Mhz
+# 0 = Not supported (default)
+# 1 = Supported
+#
+# Tx STBC: [TX-STBC-2BY1]
+# Indicates support for the transmission of at least 2x1 STBC
+# 0 = Not supported (default)
+# 1 = Supported
+#
+# Rx STBC: [RX-STBC-1] [RX-STBC-12] [RX-STBC-123] [RX-STBC-1234]
+# Indicates support for the reception of PPDUs using STBC
+# 0 = Not supported (default)
+# 1 = support of one spatial stream
+# 2 = support of one and two spatial streams
+# 3 = support of one, two and three spatial streams
+# 4 = support of one, two, three and four spatial streams
+# 5,6,7 = reserved
+#
+# SU Beamformer Capable: [SU-BEAMFORMER]
+# Indicates support for operation as a single user beamformer
+# 0 = Not supported (default)
+# 1 = Supported
+#
+# SU Beamformee Capable: [SU-BEAMFORMEE]
+# Indicates support for operation as a single user beamformee
+# 0 = Not supported (default)
+# 1 = Supported
+#
+# Compressed Steering Number of Beamformer Antennas Supported:
+# [BF-ANTENNA-2] [BF-ANTENNA-3] [BF-ANTENNA-4]
+# Beamformee's capability indicating the maximum number of beamformer
+# antennas the beamformee can support when sending compressed beamforming
+# feedback
+# If SU beamformer capable, set to maximum value minus 1
+# else reserved (default)
+#
+# Number of Sounding Dimensions:
+# [SOUNDING-DIMENSION-2] [SOUNDING-DIMENSION-3] [SOUNDING-DIMENSION-4]
+# Beamformer's capability indicating the maximum value of the NUM_STS parameter
+# in the TXVECTOR of a VHT NDP
+# If SU beamformer capable, set to maximum value minus 1
+# else reserved (default)
+#
+# MU Beamformer Capable: [MU-BEAMFORMER]
+# Indicates support for operation as an MU beamformer
+# 0 = Not supported or sent by Non-AP STA (default)
+# 1 = Supported
+#
+# VHT TXOP PS: [VHT-TXOP-PS]
+# Indicates whether or not the AP supports VHT TXOP Power Save Mode
+# or whether or not the STA is in VHT TXOP Power Save mode
+# 0 = VHT AP doesn't support VHT TXOP PS mode (OR) VHT STA not in VHT TXOP PS
+# mode
+# 1 = VHT AP supports VHT TXOP PS mode (OR) VHT STA is in VHT TXOP power save
+# mode
+#
+# +HTC-VHT Capable: [HTC-VHT]
+# Indicates whether or not the STA supports receiving a VHT variant HT Control
+# field.
+# 0 = Not supported (default)
+# 1 = supported
+#
+# Maximum A-MPDU Length Exponent: [MAX-A-MPDU-LEN-EXP0]..[MAX-A-MPDU-LEN-EXP7]
+# Indicates the maximum length of A-MPDU pre-EOF padding that the STA can recv
+# This field is an integer in the range of 0 to 7.
+# The length defined by this field is equal to
+# 2 pow(13 + Maximum A-MPDU Length Exponent) -1 octets
+#
+# VHT Link Adaptation Capable: [VHT-LINK-ADAPT2] [VHT-LINK-ADAPT3]
+# Indicates whether or not the STA supports link adaptation using VHT variant
+# HT Control field
+# If +HTC-VHTcapable is 1
+# 0 = (no feedback) if the STA does not provide VHT MFB (default)
+# 1 = reserved
+# 2 = (Unsolicited) if the STA provides only unsolicited VHT MFB
+# 3 = (Both) if the STA can provide VHT MFB in response to VHT MRQ and if the
+# STA provides unsolicited VHT MFB
+# Reserved if +HTC-VHTcapable is 0
+#
+# Rx Antenna Pattern Consistency: [RX-ANTENNA-PATTERN]
+# Indicates the possibility of Rx antenna pattern change
+# 0 = Rx antenna pattern might change during the lifetime of an association
+# 1 = Rx antenna pattern does not change during the lifetime of an association
+#
+# Tx Antenna Pattern Consistency: [TX-ANTENNA-PATTERN]
+# Indicates the possibility of Tx antenna pattern change
+# 0 = Tx antenna pattern might change during the lifetime of an association
+# 1 = Tx antenna pattern does not change during the lifetime of an association
+{% if cap_vht %}
+vht_capab=
+{%- endif -%}
+
+{%- if cap_vht_max_mpdu -%}
+[MAX-MPDU-{{ cap_vht_max_mpdu }}]
+{%- endif -%}
+
+{%- if cap_vht_max_mpdu_exp -%}
+[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}]
+{%- endif -%}
+
+{%- if cap_vht_chan_set_width -%}
+{%- if '2' in cap_vht_chan_set_width -%}
+[VHT160]
+{%- elif '3' in cap_vht_chan_set_width -%}
+[VHT160-80PLUS80]
+{%- endif -%}
+{%- endif -%}
+
+{%- if cap_vht_stbc_tx -%}
+[TX-STBC-2BY1]
+{%- endif -%}
+
+{%- if cap_vht_stbc_rx -%}
+[RX-STBC-{{ cap_vht_stbc_rx }}]
+{%- endif -%}
+
+{%- if cap_vht_link_adaptation -%}
+{%- if 'unsolicited' in cap_vht_link_adaptation -%}
+[VHT-LINK-ADAPT2]
+{%- elif 'both' in cap_vht_link_adaptation -%}
+[VHT-LINK-ADAPT3]
+{%- endif -%}
+{%- endif -%}
+
+{%- if cap_vht_short_gi -%}
+{%- for gi in cap_vht_short_gi -%}
+[SHORT-GI-{{ gi }}]
+{%- endfor -%}
+{%- endif -%}
+
+{%- if cap_vht_ldpc -%}
+[RXLDPC]
+{%- endif -%}
+
+{%- if cap_vht_tx_powersave -%}
+[VHT-TXOP-PS]
+{%- endif -%}
+
+{%- if cap_vht_vht_cf -%}
+[HTC-VHT]
+{%- endif -%}
+
+{%- if cap_vht_beamform -%}
+{%- for beamform in cap_vht_beamform -%}
+{%- if 'single-user-beamformer' in beamform -%}
+[SU-BEAMFORMER]
+{%- elif 'single-user-beamformee' in beamform -%}
+[SU-BEAMFORMEE]
+{%- elif 'multi-user-beamformer' in beamform -%}
+[MU-BEAMFORMER]
+{%- elif 'multi-user-beamformee' in beamform -%}
+[MU-BEAMFORMEE]
+{%- endif -%}
+{%- endfor -%}
+{%- endif -%}
+
+{%- if cap_vht_antenna_fixed -%}
+[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN]
+{%- endif -%}
+
+{%- if cap_vht_antenna_cnt -%}
+{%- if cap_vht_antenna_cnt|int > 1 -%}
+{%- if cap_vht_beamform -%}
+{%- for beamform in cap_vht_beamform -%}
+{%- if 'single-user-beamformer' in beamform -%}
+{%- if cap_vht_antenna_cnt|int < 6 -%}
+[BF-ANTENNA-{{ cap_vht_antenna_cnt|int -1 }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt|int -1}}]
+{%- endif -%}
+{%- else -%}
+{%- if cap_vht_antenna_cnt|int < 5 -%}
+[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}]
+{%- endif -%}
+{%- endif -%}
+{%- endfor -%}
+{%- else -%}
+{%- if cap_vht_antenna_cnt|int < 5 -%}
+[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}]
+{%- endif -%}
+{%- endif -%}
+{%- endif -%}
+{%- endif %}
+
+# ieee80211n: Whether IEEE 802.11n (HT) is enabled
+# 0 = disabled (default)
+# 1 = enabled
+# Note: You will also need to enable WMM for full HT functionality.
+# Note: hw_mode=g (2.4 GHz) and hw_mode=a (5 GHz) is used to specify the band.
+{% if cap_req_vht -%}
+ieee80211n=0
+# Require stations to support VHT PHY (reject association if they do not)
+require_vht=1
+{% endif %}
+
+{% if cap_vht_center_freq_1 -%}
+# center freq = 5 GHz + (5 * index)
+# So index 42 gives center freq 5.210 GHz
+# which is channel 42 in 5G band
+vht_oper_centr_freq_seg0_idx={{ cap_vht_center_freq_1 }}
+{% endif %}
+
+{% if cap_vht_center_freq_2 -%}
+# center freq = 5 GHz + (5 * index)
+# So index 159 gives center freq 5.795 GHz
+# which is channel 159 in 5G band
+vht_oper_centr_freq_seg1_idx={{ cap_vht_center_freq_2 }}
+{% endif %}
+
+{% if disable_broadcast_ssid -%}
+# Send empty SSID in beacons and ignore probe request frames that do not
+# specify full SSID, i.e., require stations to know SSID.
+# default: disabled (0)
+# 1 = send empty (length=0) SSID in beacon and ignore probe request for
+# broadcast SSID
+# 2 = clear SSID (ASCII 0), but keep the original length (this may be required
+# with some clients that do not support empty SSID) and ignore probe
+# requests for broadcast SSID
+ignore_broadcast_ssid=1
+{% endif %}
+
+# Station MAC address -based authentication
+# Please note that this kind of access control requires a driver that uses
+# hostapd to take care of management frame processing and as such, this can be
+# used with driver=hostap or driver=nl80211, but not with driver=atheros.
+# 0 = accept unless in deny list
+# 1 = deny unless in accept list
+# 2 = use external RADIUS server (accept/deny lists are searched first)
+macaddr_acl=0
+
+{% if max_stations -%}
+# Maximum number of stations allowed in station table. New stations will be
+# rejected after the station table is full. IEEE 802.11 has a limit of 2007
+# different association IDs, so this number should not be larger than that.
+# (default: 2007)
+max_num_sta={{ max_stations }}
+{% endif %}
+
+{% if isolate_stations -%}
+# Client isolation can be used to prevent low-level bridging of frames between
+# associated stations in the BSS. By default, this bridging is allowed.
+ap_isolate=1
+{% endif %}
+
+{% if reduce_transmit_power -%}
+# Add Power Constraint element to Beacon and Probe Response frames
+# This config option adds Power Constraint element when applicable and Country
+# element is added. Power Constraint element is required by Transmit Power
+# Control. This can be used only with ieee80211d=1.
+# Valid values are 0..255.
+local_pwr_constraint={{ reduce_transmit_power }}
+{% endif %}
+
+{% if expunge_failing_stations -%}
+# Disassociate stations based on excessive transmission failures or other
+# indications of connection loss. This depends on the driver capabilities and
+# may not be available with all drivers.
+disassoc_low_ack=1
+{% endif %}
+
+{% if sec_wep -%}
+# IEEE 802.11 specifies two authentication algorithms. hostapd can be
+# configured to allow both of these or only one. Open system authentication
+# should be used with IEEE 802.1X.
+# Bit fields of allowed authentication algorithms:
+# bit 0 = Open System Authentication
+# bit 1 = Shared Key Authentication (requires WEP)
+auth_algs=2
+
+# WEP rekeying (disabled if key lengths are not set or are set to 0)
+# Key lengths for default/broadcast and individual/unicast keys:
+# 5 = 40-bit WEP (also known as 64-bit WEP with 40 secret bits)
+# 13 = 104-bit WEP (also known as 128-bit WEP with 104 secret bits)
+wep_key_len_broadcast=5
+wep_key_len_unicast=5
+
+# Static WEP key configuration
+#
+# The key number to use when transmitting.
+# It must be between 0 and 3, and the corresponding key must be set.
+# default: not set
+wep_default_key=0
+
+# The WEP keys to use.
+# A key may be a quoted string or unquoted hexadecimal digits.
+# The key length should be 5, 13, or 16 characters, or 10, 26, or 32
+# digits, depending on whether 40-bit (64-bit), 104-bit (128-bit), or
+# 128-bit (152-bit) WEP is used.
+# Only the default key must be supplied; the others are optional.
+{% if sec_wep_key -%}
+{% for key in sec_wep_key -%}
+wep_key{{ loop.index -1 }}={{ key}}
+{% endfor %}
+{%- endif %}
+
+{% elif sec_wpa -%}
+##### WPA/IEEE 802.11i configuration ##########################################
+
+# Enable WPA. Setting this variable configures the AP to require WPA (either
+# WPA-PSK or WPA-RADIUS/EAP based on other configuration). For WPA-PSK, either
+# wpa_psk or wpa_passphrase must be set and wpa_key_mgmt must include WPA-PSK.
+# Instead of wpa_psk / wpa_passphrase, wpa_psk_radius might suffice.
+# For WPA-RADIUS/EAP, ieee8021x must be set (but without dynamic WEP keys),
+# RADIUS authentication server must be configured, and WPA-EAP must be included
+# in wpa_key_mgmt.
+# This field is a bit field that can be used to enable WPA (IEEE 802.11i/D3.0)
+# and/or WPA2 (full IEEE 802.11i/RSN):
+# bit0 = WPA
+# bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled)
+{% if 'both' in sec_wpa_mode -%}
+wpa=3
+{%- elif 'wpa2' in sec_wpa_mode -%}
+wpa=2
+{%- elif 'wpa' in sec_wpa_mode -%}
+wpa=1
+{%- endif %}
+
+{% if sec_wpa_cipher -%}
+# Set of accepted cipher suites (encryption algorithms) for pairwise keys
+# (unicast packets). This is a space separated list of algorithms:
+# CCMP = AES in Counter mode with CBC-MAC (CCMP-128)
+# TKIP = Temporal Key Integrity Protocol
+# CCMP-256 = AES in Counter mode with CBC-MAC with 256-bit key
+# GCMP = Galois/counter mode protocol (GCMP-128)
+# GCMP-256 = Galois/counter mode protocol with 256-bit key
+# Group cipher suite (encryption algorithm for broadcast and multicast frames)
+# is automatically selected based on this configuration. If only CCMP is
+# allowed as the pairwise cipher, group cipher will also be CCMP. Otherwise,
+# TKIP will be used as the group cipher. The optional group_cipher parameter can
+# be used to override this automatic selection.
+{% if 'wpa2' in sec_wpa_mode -%}
+# Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value)
+rsn_pairwise={{ sec_wpa_cipher | join(" ") }}
+{% else -%}
+# Pairwise cipher for WPA (v1) (default: TKIP)
+wpa_pairwise={{ sec_wpa_cipher | join(" ") }}
+{%- endif -%}
+{% endif %}
+
+{% if sec_wpa_passphrase -%}
+# IEEE 802.11 specifies two authentication algorithms. hostapd can be
+# configured to allow both of these or only one. Open system authentication
+# should be used with IEEE 802.1X.
+# Bit fields of allowed authentication algorithms:
+# bit 0 = Open System Authentication
+# bit 1 = Shared Key Authentication (requires WEP)
+auth_algs=1
+
+# WPA pre-shared keys for WPA-PSK. This can be either entered as a 256-bit
+# secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase
+# (8..63 characters) that will be converted to PSK. This conversion uses SSID
+# so the PSK changes when ASCII passphrase is used and the SSID is changed.
+wpa_passphrase={{ sec_wpa_passphrase }}
+
+# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The
+# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be
+# added to enable SHA256-based stronger algorithms.
+# WPA-PSK = WPA-Personal / WPA2-Personal
+# WPA-PSK-SHA256 = WPA2-Personal using SHA256
+wpa_key_mgmt=WPA-PSK
+
+{% elif sec_wpa_radius -%}
+##### IEEE 802.1X-2004 related configuration ##################################
+# Require IEEE 802.1X authorization
+ieee8021x=1
+
+# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The
+# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be
+# added to enable SHA256-based stronger algorithms.
+# WPA-EAP = WPA-Enterprise / WPA2-Enterprise
+# WPA-EAP-SHA256 = WPA2-Enterprise using SHA256
+wpa_key_mgmt=WPA-EAP
+
+{% if sec_wpa_radius_source -%}
+# RADIUS client forced local IP address for the access point
+# Normally the local IP address is determined automatically based on configured
+# IP addresses, but this field can be used to force a specific address to be
+# used, e.g., when the device has multiple IP addresses.
+radius_client_addr={{ sec_wpa_radius_source }}
+
+# The own IP address of the access point (used as NAS-IP-Address)
+own_ip_addr={{ sec_wpa_radius_source }}
+{% else %}
+# The own IP address of the access point (used as NAS-IP-Address)
+own_ip_addr=127.0.0.1
+{% endif %}
+
+{% for radius in sec_wpa_radius -%}
+{%- if not radius.disabled -%}
+# RADIUS authentication server
+auth_server_addr={{ radius.server }}
+auth_server_port={{ radius.port }}
+auth_server_shared_secret={{ radius.key }}
+{% if radius.acc_port -%}
+# RADIUS accounting server
+acct_server_addr={{ radius.server }}
+acct_server_port={{ radius.acc_port }}
+acct_server_shared_secret={{ radius.key }}
+{% endif %}
+{% endif %}
+{% endfor %}
+
+{% endif %}
+
+{% else %}
+# Open system
+auth_algs=1
+{% endif %}
+
+# TX queue parameters (EDCF / bursting)
+# tx_queue_<queue name>_<param>
+# queues: data0, data1, data2, data3
+# (data0 is the highest priority queue)
+# parameters:
+# aifs: AIFS (default 2)
+# cwmin: cwMin (1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191,
+# 16383, 32767)
+# cwmax: cwMax (same values as cwMin, cwMax >= cwMin)
+# burst: maximum length (in milliseconds with precision of up to 0.1 ms) for
+# bursting
+#
+# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e):
+# These parameters are used by the access point when transmitting frames
+# to the clients.
+#
+# Low priority / AC_BK = background
+tx_queue_data3_aifs=7
+tx_queue_data3_cwmin=15
+tx_queue_data3_cwmax=1023
+tx_queue_data3_burst=0
+# Note: for IEEE 802.11b mode: cWmin=31 cWmax=1023 burst=0
+#
+# Normal priority / AC_BE = best effort
+tx_queue_data2_aifs=3
+tx_queue_data2_cwmin=15
+tx_queue_data2_cwmax=63
+tx_queue_data2_burst=0
+# Note: for IEEE 802.11b mode: cWmin=31 cWmax=127 burst=0
+#
+# High priority / AC_VI = video
+tx_queue_data1_aifs=1
+tx_queue_data1_cwmin=7
+tx_queue_data1_cwmax=15
+tx_queue_data1_burst=3.0
+# Note: for IEEE 802.11b mode: cWmin=15 cWmax=31 burst=6.0
+#
+# Highest priority / AC_VO = voice
+tx_queue_data0_aifs=1
+tx_queue_data0_cwmin=3
+tx_queue_data0_cwmax=7
+tx_queue_data0_burst=1.5
+
+# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e):
+# for 802.11a or 802.11g networks
+# These parameters are sent to WMM clients when they associate.
+# The parameters will be used by WMM clients for frames transmitted to the
+# access point.
+#
+# note - txop_limit is in units of 32microseconds
+# note - acm is admission control mandatory flag. 0 = admission control not
+# required, 1 = mandatory
+# note - Here cwMin and cmMax are in exponent form. The actual cw value used
+# will be (2^n)-1 where n is the value given here. The allowed range for these
+# wmm_ac_??_{cwmin,cwmax} is 0..15 with cwmax >= cwmin.
+#
+wmm_enabled=1
+
+# Low priority / AC_BK = background
+wmm_ac_bk_cwmin=4
+wmm_ac_bk_cwmax=10
+wmm_ac_bk_aifs=7
+wmm_ac_bk_txop_limit=0
+wmm_ac_bk_acm=0
+# Note: for IEEE 802.11b mode: cWmin=5 cWmax=10
+#
+# Normal priority / AC_BE = best effort
+wmm_ac_be_aifs=3
+wmm_ac_be_cwmin=4
+wmm_ac_be_cwmax=10
+wmm_ac_be_txop_limit=0
+wmm_ac_be_acm=0
+# Note: for IEEE 802.11b mode: cWmin=5 cWmax=7
+#
+# High priority / AC_VI = video
+wmm_ac_vi_aifs=2
+wmm_ac_vi_cwmin=3
+wmm_ac_vi_cwmax=4
+wmm_ac_vi_txop_limit=94
+wmm_ac_vi_acm=0
+# Note: for IEEE 802.11b mode: cWmin=4 cWmax=5 txop_limit=188
+#
+# Highest priority / AC_VO = voice
+wmm_ac_vo_aifs=2
+wmm_ac_vo_cwmin=2
+wmm_ac_vo_cwmax=3
+wmm_ac_vo_txop_limit=47
+wmm_ac_vo_acm=0
+
diff --git a/data/templates/wifi/wpa_supplicant.conf.tmpl b/data/templates/wifi/wpa_supplicant.conf.tmpl
new file mode 100644
index 000000000..2784883f1
--- /dev/null
+++ b/data/templates/wifi/wpa_supplicant.conf.tmpl
@@ -0,0 +1,9 @@
+# WPA supplicant config
+network={
+ ssid="{{ ssid }}"
+{%- if sec_wpa_passphrase %}
+ psk="{{ sec_wpa_passphrase }}"
+{% else %}
+ key_mgmt=NONE
+{% endif %}
+}
diff --git a/data/templates/wwan/chat.tmpl b/data/templates/wwan/chat.tmpl
new file mode 100644
index 000000000..a3395c057
--- /dev/null
+++ b/data/templates/wwan/chat.tmpl
@@ -0,0 +1,6 @@
+ABORT 'NO DIAL TONE' ABORT 'NO ANSWER' ABORT 'NO CARRIER' ABORT DELAYED
+'' AT
+OK ATZ
+OK 'AT+CGDCONT=1,"IP","{{ apn }}"'
+OK ATD*99#
+CONNECT ''
diff --git a/data/templates/wwan/ip-down.script.tmpl b/data/templates/wwan/ip-down.script.tmpl
new file mode 100644
index 000000000..194f8d863
--- /dev/null
+++ b/data/templates/wwan/ip-down.script.tmpl
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+tty=$2
+
+# Only applicable for Wireless Modems (WWAN)
+if [ -z "$(echo $tty | egrep "tty(USB|ACM)")" ]; then
+ exit 0
+fi
+
+# Determine if we are enslaved to a VRF, this is needed to properly insert
+# the default route
+VRF_NAME=""
+if [ -d /sys/class/net/{{ intf }}/upper_* ]; then
+ # Determine upper (VRF) interface
+ VRF=$(basename $(ls -d /sys/class/net/{{ intf }}/upper_*))
+ # Remove upper_ prefix from result string
+ VRF=${VRF#"upper_"}
+ # Populate variable to run in VR context
+ VRF_NAME=" -c vrf ${VRF_NAME} "
+fi
+
+# Remove default route to either default or VRF routing table
+vtysh -c "conf t" ${VRF_NAME} -c "no ip route 0.0.0.0/0 {{ intf }} {{ metric }}"
+
+DIALER_PID=$(cat /var/run/{{ intf }}.pid)
+logger -t pppd[$DIALER_PID] "removed default route via {{ intf }} metric {{ metric }}"
diff --git a/data/templates/wwan/ip-pre-up.script.tmpl b/data/templates/wwan/ip-pre-up.script.tmpl
new file mode 100644
index 000000000..7a17a1c71
--- /dev/null
+++ b/data/templates/wwan/ip-pre-up.script.tmpl
@@ -0,0 +1,23 @@
+#!/bin/sh
+# As WWAN is an "on demand" interface we need to re-configure it when it
+# becomes 'up'
+
+ipparam=$6
+
+# device name and metric are received using ipparam
+device=`echo "$ipparam"|awk '{ print $1 }'`
+
+if [ "$device" != "{{ intf }}" ]; then
+ exit
+fi
+
+# add some info to syslog
+DIALER_PID=$(cat /var/run/{{ intf }}.pid)
+logger -t pppd[$DIALER_PID] "executing $0"
+
+echo "{{ description }}" > /sys/class/net/{{ intf }}/ifalias
+
+{% if vrf -%}
+logger -t pppd[$DIALER_PID] "configuring interface {{ intf }} for VRF {{ vrf }}"
+ip link set dev {{ intf }} master {{ vrf }}
+{% endif %}
diff --git a/data/templates/wwan/ip-up.script.tmpl b/data/templates/wwan/ip-up.script.tmpl
new file mode 100644
index 000000000..89e42a23a
--- /dev/null
+++ b/data/templates/wwan/ip-up.script.tmpl
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+tty=$2
+
+# Only applicable for Wireless Modems (WWAN)
+if [ -z "$(echo $tty | egrep "tty(USB|ACM)")" ]; then
+ exit 0
+fi
+
+DIALER_PID=$(cat /var/run/{{ intf }}.pid)
+
+# Determine if we are enslaved to a VRF, this is needed to properly insert
+# the default route
+VRF_NAME=""
+if [ -d /sys/class/net/{{ intf }}/upper_* ]; then
+ # Determine upper (VRF) interface
+ VRF=$(basename $(ls -d /sys/class/net/{{ intf }}/upper_*))
+ # Remove upper_ prefix from result string
+ VRF=${VRF#"upper_"}
+ VRF_NAME="vrf ${VRF}"
+fi
+
+# Apply default route to either default or VRF routing table
+vtysh -c "conf t" -c "ip route 0.0.0.0/0 {{ intf }} ${VRF_NAME} {{ metric }}"
+logger -t pppd[$DIALER_PID] "added default route via {{ intf }} metric {{ metric }} ${VRF_NAME}"
diff --git a/data/templates/wwan/peer.tmpl b/data/templates/wwan/peer.tmpl
new file mode 100644
index 000000000..04ab4f844
--- /dev/null
+++ b/data/templates/wwan/peer.tmpl
@@ -0,0 +1,30 @@
+### Autogenerated by interfaces-wirelessmodem.py ###
+
+{% if description %}
+# {{ description }}
+{% endif %}
+ifname {{ intf }}
+ipparam {{ intf }}
+linkname {{ intf }}
+{% if name_server -%}
+usepeerdns
+{%- endif %}
+# physical device
+/dev/{{ device }}
+lcp-echo-failure 0
+115200
+debug
+logfile {{ logfile }}
+nodefaultroute
+ipcp-max-failure 4
+ipcp-accept-local
+ipcp-accept-remote
+noauth
+crtscts
+lock
+persist
+{% if on_demand -%}
+demand
+{%- endif %}
+
+connect '/usr/sbin/chat -v -t6 -f {{ chat_script }}'
diff --git a/debian/changelog b/debian/changelog
index 00ed8ea6d..80d8bf425 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+vyos-1x (1.3.0-17) unstable; urgency=medium
+
+ * vyos-1x-vmware: add dependency on vyos-1x
+
+ -- Christian Poessinger <christian@poessinger.com> Sat, 21 Mar 2020 20:47:22 +0100
+
vyos-1x (1.3.0-16) unstable; urgency=low
* [arp] moving 'show arp' to 'show protocols static arp'
diff --git a/debian/control b/debian/control
index fbcc3fa2e..7b95b2c75 100644
--- a/debian/control
+++ b/debian/control
@@ -29,8 +29,10 @@ Depends: python3,
python3-hurry.filesize,
python3-vici (>= 5.7.2),
python3-bottle,
+ python3-netaddr,
python3-zmq,
cron,
+ easy-rsa,
ipaddrcheck,
tcpdump,
tshark,
@@ -76,7 +78,9 @@ Depends: python3,
iperf,
iperf3,
frr,
+ radvd,
dbus,
+ usb-modeswitch,
hostapd (>= 0.6.8),
wpasupplicant (>= 0.6.7),
iw,
@@ -95,6 +99,7 @@ Architecture: amd64 i386
Depends:
${misc:Depends},
${shlibs:Depends},
+ vyos-1x,
open-vm-tools
Description: VyOS configuration scripts and data for VMware
Adds configuration files required for VyOS running on VMware hosts.
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
index eb19dafeb..5004d111f 100644
--- a/debian/vyos-1x.install
+++ b/debian/vyos-1x.install
@@ -1,6 +1,9 @@
etc/dhcp
etc/init.d
+etc/ppp
+etc/rsyslog.d
etc/systemd
+etc/udev
etc/vyos
lib/
opt/
diff --git a/interface-definitions/flow-accounting-conf.xml.in b/interface-definitions/flow-accounting-conf.xml.in
index 5cf866b8e..239269235 100644
--- a/interface-definitions/flow-accounting-conf.xml.in
+++ b/interface-definitions/flow-accounting-conf.xml.in
@@ -217,7 +217,7 @@
</valueHelp>
<valueHelp>
<format>10</format>
- <description>IPFIX</description>
+ <description>Internet Protocol Flow Information Export (IPFIX)</description>
</valueHelp>
</properties>
</leafNode>
diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in
index 4f940f7f6..9bb96f1f0 100644
--- a/interface-definitions/https.xml.in
+++ b/interface-definitions/https.xml.in
@@ -1,6 +1,7 @@
<?xml version="1.0"?>
<!-- HTTPS configuration -->
<interfaceDefinition>
+ <syntaxVersion component='https' version='2'></syntaxVersion>
<node name="service">
<children>
<node name="https" owner="${vyos_conf_scripts_dir}/https.py">
@@ -9,28 +10,37 @@
<priority>1001</priority>
</properties>
<children>
- <tagNode name="listen-address">
+ <tagNode name="virtual-host">
<properties>
- <help>Addresses to listen for HTTPS requests</help>
- <valueHelp>
- <format>ipv4</format>
- <description>HTTPS IPv4 address</description>
- </valueHelp>
- <valueHelp>
- <format>ipv6</format>
- <description>HTTPS IPv6 address</description>
- </valueHelp>
- <valueHelp>
- <format>'*'</format>
- <description>any</description>
- </valueHelp>
+ <help>Identifier for virtual host</help>
<constraint>
- <validator name="ipv4-address"/>
- <validator name="ipv6-address"/>
- <regex>\*$</regex>
+ <regex>[a-zA-Z0-9-_.:]{1,255}</regex>
</constraint>
+ <constraintErrorMessage>illegal characters in identifier or identifier longer than 255 characters</constraintErrorMessage>
</properties>
<children>
+ <leafNode name="listen-address">
+ <properties>
+ <help>Address to listen for HTTPS requests</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>HTTPS IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>HTTPS IPv6 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>'*'</format>
+ <description>any</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ <regex>\*$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
<leafNode name='listen-port'>
<properties>
<help>Port to listen for HTTPS requests; default 443</help>
@@ -45,7 +55,7 @@
</leafNode>
<leafNode name="server-name">
<properties>
- <help>Server names: exact, wildcard, regex, or '_' (any)</help>
+ <help>Server names: exact, wildcard, or regex</help>
<multi/>
</properties>
</leafNode>
@@ -103,6 +113,19 @@
</leafNode>
</children>
</node>
+ <node name="api-restrict">
+ <properties>
+ <help>Restrict api proxy to subset of virtual hosts</help>
+ </properties>
+ <children>
+ <leafNode name="virtual-host">
+ <properties>
+ <help>Restrict proxy to virtual host(s)</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
<node name="certificates">
<properties>
<help>TLS certificates</help>
diff --git a/interface-definitions/include/interface-description.xml.i b/interface-definitions/include/interface-description.xml.i
index 7a7a37871..961533e26 100644
--- a/interface-definitions/include/interface-description.xml.i
+++ b/interface-definitions/include/interface-description.xml.i
@@ -1,9 +1,9 @@
<leafNode name="description">
<properties>
- <help>Interface description</help>
+ <help>Interface specific description</help>
<constraint>
<regex>.{1,256}$</regex>
</constraint>
- <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage>
+ <constraintErrorMessage>Description too long (limit 256 characters)</constraintErrorMessage>
</properties>
</leafNode>
diff --git a/interface-definitions/include/interface-mtu-64-8024.xml.i b/interface-definitions/include/interface-mtu-64-8024.xml.i
new file mode 100644
index 000000000..e917c816f
--- /dev/null
+++ b/interface-definitions/include/interface-mtu-64-8024.xml.i
@@ -0,0 +1,13 @@
+<leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>64-8024</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 64-8024"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 64 and 8024</constraintErrorMessage>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-mtu-68-1500.xml.i b/interface-definitions/include/interface-mtu-68-1500.xml.i
new file mode 100644
index 000000000..81223c332
--- /dev/null
+++ b/interface-definitions/include/interface-mtu-68-1500.xml.i
@@ -0,0 +1,13 @@
+<leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>68-1500</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 68-1500"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 68 and 1500</constraintErrorMessage>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-vrf.xml.i b/interface-definitions/include/interface-vrf.xml.i
new file mode 100644
index 000000000..355e7f0f3
--- /dev/null
+++ b/interface-definitions/include/interface-vrf.xml.i
@@ -0,0 +1,12 @@
+<leafNode name="vrf">
+ <properties>
+ <help>VRF instance name</help>
+ <valueHelp>
+ <format>text</format>
+ <description>VRF instance name</description>
+ </valueHelp>
+ <completionHelp>
+ <path>vrf name</path>
+ </completionHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/ipv6-address.xml.i b/interface-definitions/include/ipv6-address.xml.i
new file mode 100644
index 000000000..507d5dcc1
--- /dev/null
+++ b/interface-definitions/include/ipv6-address.xml.i
@@ -0,0 +1,22 @@
+<node name="address">
+ <children>
+ <leafNode name="autoconf">
+ <properties>
+ <help>Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="eui64">
+ <properties>
+ <help>ssign IPv6 address using EUI-64 based on MAC address</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/ipv6-disable-forwarding.xml.i b/interface-definitions/include/ipv6-disable-forwarding.xml.i
new file mode 100644
index 000000000..3f90c7e34
--- /dev/null
+++ b/interface-definitions/include/ipv6-disable-forwarding.xml.i
@@ -0,0 +1,6 @@
+<leafNode name="disable-forwarding">
+ <properties>
+ <help>Disable IPv6 forwarding on this interface</help>
+ <valueless/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/ipv6-dup-addr-detect-transmits.xml.i b/interface-definitions/include/ipv6-dup-addr-detect-transmits.xml.i
new file mode 100644
index 000000000..728187560
--- /dev/null
+++ b/interface-definitions/include/ipv6-dup-addr-detect-transmits.xml.i
@@ -0,0 +1,16 @@
+<leafNode name="dup-addr-detect-transmits">
+ <properties>
+ <help>Number of NS messages to send while performing DAD (default: 1)</help>
+ <valueHelp>
+ <format>1-n</format>
+ <description>Number of NS messages to send while performing DAD</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0</format>
+ <description>Disable Duplicate Address Dectection (DAD)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--non-negative"/>
+ </constraint>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/port-number.xml.i b/interface-definitions/include/port-number.xml.i
new file mode 100644
index 000000000..78eb4b7af
--- /dev/null
+++ b/interface-definitions/include/port-number.xml.i
@@ -0,0 +1,12 @@
+<leafNode name="port">
+ <properties>
+ <help>Port number used to establish connection</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/radius-server.xml.i b/interface-definitions/include/radius-server.xml.i
new file mode 100644
index 000000000..047728233
--- /dev/null
+++ b/interface-definitions/include/radius-server.xml.i
@@ -0,0 +1,56 @@
+<node name="radius">
+ <properties>
+ <help>RADIUS based user authentication</help>
+ </properties>
+ <children>
+ <leafNode name="source-address">
+ <properties>
+ <help>RADIUS client source address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 source-address of RADIUS queries</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="server">
+ <properties>
+ <help>RADIUS server configuration</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>RADIUS server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Temporary disable this server</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="key">
+ <properties>
+ <help>Shared secret key</help>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Authentication port</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port (default: 1812)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/vif-s.xml.i b/interface-definitions/include/vif-s.xml.i
index 2120aa32d..ab2dcd955 100644
--- a/interface-definitions/include/vif-s.xml.i
+++ b/interface-definitions/include/vif-s.xml.i
@@ -58,6 +58,7 @@
#include <include/interface-disable.xml.i>
#include <include/interface-mac.xml.i>
#include <include/interface-mtu-68-9000.xml.i>
+ #include <include/interface-vrf.xml.i>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/include/vif.xml.i b/interface-definitions/include/vif.xml.i
index 85e901852..819534dc1 100644
--- a/interface-definitions/include/vif.xml.i
+++ b/interface-definitions/include/vif.xml.i
@@ -16,6 +16,7 @@
#include <include/dhcp-dhcpv6-options.xml.i>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
+ #include <include/interface-vrf.xml.i>
<leafNode name="egress-qos">
<properties>
<help>VLAN egress QoS</help>
@@ -50,6 +51,13 @@
#include <include/interface-enable-proxy-arp.xml.i>
</children>
</node>
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
#include <include/interface-mac.xml.i>
#include <include/interface-mtu-68-9000.xml.i>
</children>
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in
index 586a8437d..07a6abd30 100644
--- a/interface-definitions/interfaces-bonding.xml.in
+++ b/interface-definitions/interfaces-bonding.xml.in
@@ -7,7 +7,7 @@
<help>Bonding Interface/Link Aggregation</help>
<priority>320</priority>
<constraint>
- <regex>bond[0-9]+$</regex>
+ <regex>^bond[0-9]+$</regex>
</constraint>
<constraintErrorMessage>Bonding interface must be named bondN</constraintErrorMessage>
<valueHelp>
@@ -53,6 +53,7 @@
#include <include/dhcp-dhcpv6-options.xml.i>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
+ #include <include/interface-vrf.xml.i>
<leafNode name="hash-policy">
<properties>
<help>Bonding transmit hash policy</help>
@@ -88,6 +89,13 @@
#include <include/interface-proxy-arp-pvlan.xml.i>
</children>
</node>
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
#include <include/interface-mac.xml.i>
<leafNode name="mode">
<properties>
diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in
index e8285b16c..818bc9c0e 100644
--- a/interface-definitions/interfaces-bridge.xml.in
+++ b/interface-definitions/interfaces-bridge.xml.in
@@ -5,9 +5,9 @@
<tagNode name="bridge" owner="${vyos_conf_scripts_dir}/interfaces-bridge.py">
<properties>
<help>Bridge Interface</help>
- <priority>470</priority>
+ <priority>489</priority>
<constraint>
- <regex>br[0-9]+$</regex>
+ <regex>^br[0-9]+$</regex>
</constraint>
<constraintErrorMessage>Bridge interface must be named brN</constraintErrorMessage>
<valueHelp>
@@ -37,6 +37,7 @@
#include <include/dhcp-dhcpv6-options.xml.i>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
+ #include <include/interface-vrf.xml.i>
<leafNode name="forwarding-delay">
<properties>
<help>Forwarding delay</help>
@@ -85,6 +86,13 @@
#include <include/interface-disable-arp-filter.xml.i>
</children>
</node>
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
#include <include/interface-mac.xml.i>
<leafNode name="max-age">
<properties>
diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in
index 39809a610..135adfc10 100644
--- a/interface-definitions/interfaces-dummy.xml.in
+++ b/interface-definitions/interfaces-dummy.xml.in
@@ -7,7 +7,7 @@
<help>Dummy Interface</help>
<priority>300</priority>
<constraint>
- <regex>dum[0-9]+$</regex>
+ <regex>^dum[0-9]+$</regex>
</constraint>
<constraintErrorMessage>Dummy interface must be named dumN</constraintErrorMessage>
<valueHelp>
@@ -19,6 +19,7 @@
#include <include/address-ipv4-ipv6.xml.i>
#include <include/interface-description.xml.i>
#include <include/interface-disable.xml.i>
+ #include <include/interface-vrf.xml.i>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in
index 8f5d7355b..f8ec26d04 100644
--- a/interface-definitions/interfaces-ethernet.xml.in
+++ b/interface-definitions/interfaces-ethernet.xml.in
@@ -7,7 +7,7 @@
<help>Ethernet Interface</help>
<priority>318</priority>
<constraint>
- <regex>((eth|lan)[0-9]+|(eno|ens|enp|enx).+)$</regex>
+ <regex>^((eth|lan)[0-9]+|(eno|ens|enp|enx).+)$</regex>
</constraint>
<constraintErrorMessage>Invalid Ethernet interface name</constraintErrorMessage>
<valueHelp>
@@ -31,6 +31,7 @@
</leafNode>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
+ #include <include/interface-vrf.xml.i>
<leafNode name="duplex">
<properties>
<help>Duplex mode</help>
@@ -78,6 +79,13 @@
#include <include/interface-proxy-arp-pvlan.xml.i>
</children>
</node>
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
#include <include/interface-mac.xml.i>
#include <include/interface-mtu-68-9000.xml.i>
<node name="offload-options">
diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in
index a6406ffc9..31a3ebb7a 100644
--- a/interface-definitions/interfaces-geneve.xml.in
+++ b/interface-definitions/interfaces-geneve.xml.in
@@ -7,7 +7,7 @@
<help>Generic Network Virtualization Encapsulation (GENEVE) Interface</help>
<priority>460</priority>
<constraint>
- <regex>gnv[0-9]+$</regex>
+ <regex>^gnv[0-9]+$</regex>
</constraint>
<constraintErrorMessage>GENEVE interface must be named gnvN</constraintErrorMessage>
<valueHelp>
diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in
index a408e58c1..30dd9b604 100644
--- a/interface-definitions/interfaces-l2tpv3.xml.in
+++ b/interface-definitions/interfaces-l2tpv3.xml.in
@@ -5,9 +5,9 @@
<tagNode name="l2tpv3" owner="${vyos_conf_scripts_dir}/interfaces-l2tpv3.py">
<properties>
<help>Layer 2 Tunnel Protocol Version 3 (L2TPv3) Interface</help>
- <priority>800</priority>
+ <priority>485</priority>
<constraint>
- <regex>l2tpeth[0-9]+$</regex>
+ <regex>^l2tpeth[0-9]+$</regex>
</constraint>
<constraintErrorMessage>L2TPv3 interface must be named l2tpethN</constraintErrorMessage>
<valueHelp>
@@ -51,6 +51,13 @@
<constraintErrorMessage>Encapsulation must be UDP or IP</constraintErrorMessage>
</properties>
</leafNode>
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
<leafNode name="local-ip">
<properties>
<help>Local IP address for L2TPv3 tunnel</help>
diff --git a/interface-definitions/interfaces-loopback.xml.in b/interface-definitions/interfaces-loopback.xml.in
index ddbfad763..97d5bab90 100644
--- a/interface-definitions/interfaces-loopback.xml.in
+++ b/interface-definitions/interfaces-loopback.xml.in
@@ -7,7 +7,7 @@
<help>Loopback Interface</help>
<priority>300</priority>
<constraint>
- <regex>lo$</regex>
+ <regex>^lo$</regex>
</constraint>
<constraintErrorMessage>Loopback interface must be named lo</constraintErrorMessage>
<valueHelp>
diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in
index bc1a159a9..92bac3fab 100644
--- a/interface-definitions/interfaces-openvpn.xml.in
+++ b/interface-definitions/interfaces-openvpn.xml.in
@@ -7,7 +7,7 @@
<help>OpenVPN Tunnel Interface</help>
<priority>460</priority>
<constraint>
- <regex>vtun[0-9]+$</regex>
+ <regex>^vtun[0-9]+$</regex>
</constraint>
<constraintErrorMessage>OpenVPN tunnel interface must be named vtunN</constraintErrorMessage>
<valueHelp>
@@ -162,6 +162,13 @@
</leafNode>
</children>
</node>
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
<leafNode name="hash">
<properties>
<help>Hashing Algorithm</help>
@@ -611,6 +618,18 @@
</constraint>
</properties>
</leafNode>
+ <leafNode name="crypt-file">
+ <properties>
+ <help>File containing encryption key to authenticate control channel</help>
+ <valueHelp>
+ <format>file</format>
+ <description>File in /config/auth directory</description>
+ </valueHelp>
+ <constraint>
+ <validator name="file-exists" argument="--directory /config/auth"/>
+ </constraint>
+ </properties>
+ </leafNode>
<leafNode name="tls-version-min">
<properties>
<help>Specify the minimum required TLS version</help>
diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in
index b6b54c915..d69e0b42c 100644
--- a/interface-definitions/interfaces-pppoe.xml.in
+++ b/interface-definitions/interfaces-pppoe.xml.in
@@ -7,13 +7,12 @@
<help>Point-to-Point Protocol over Ethernet (PPPoE)</help>
<priority>321</priority>
<constraint>
- <regex>pppoe[0-9]+$</regex>
- <validator name="numeric" argument="--range 1-99"/>
+ <regex>^pppoe[0-9]+$</regex>
</constraint>
<constraintErrorMessage>PPPoE interface must be named pppoeN</constraintErrorMessage>
<valueHelp>
<format>pppoeN</format>
- <description>PPPoE interface name (1-15)</description>
+ <description>PPPoE dialer interface name</description>
</valueHelp>
</properties>
<children>
@@ -75,6 +74,7 @@
</leafNode>
#include <include/interface-description.xml.i>
#include <include/interface-disable.xml.i>
+ #include <include/interface-vrf.xml.i>
<leafNode name="idle-timeout">
<properties>
<help>Delay before disconnecting idle session (in seconds)</help>
diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in
index c2dea438a..c6e61d19a 100644
--- a/interface-definitions/interfaces-pseudo-ethernet.xml.in
+++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in
@@ -7,7 +7,7 @@
<help>Pseudo Ethernet</help>
<priority>319</priority>
<constraint>
- <regex>peth[0-9]+$</regex>
+ <regex>^peth[0-9]+$</regex>
</constraint>
<constraintErrorMessage>Pseudo Ethernet interface must be named pethN</constraintErrorMessage>
<valueHelp>
@@ -21,6 +21,7 @@
#include <include/dhcp-dhcpv6-options.xml.i>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
+ #include <include/interface-vrf.xml.i>
<node name="ip">
<children>
#include <include/interface-arp-cache-timeout.xml.i>
@@ -32,9 +33,16 @@
#include <include/interface-proxy-arp-pvlan.xml.i>
</children>
</node>
- <leafNode name="link">
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
+ <leafNode name="source-interface">
<properties>
- <help>Lower link device</help>
+ <help>Physical Interface used for this device</help>
<valueHelp>
<format>interface</format>
<description>Interface used for VXLAN underlay</description>
diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in
new file mode 100644
index 000000000..e1ac60319
--- /dev/null
+++ b/interface-definitions/interfaces-tunnel.xml.in
@@ -0,0 +1,280 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="tunnel" owner="${vyos_conf_scripts_dir}/interfaces-tunnel.py">
+ <properties>
+ <help>Tunnel interface</help>
+ <priority>380</priority>
+ <constraint>
+ <regex>^tun[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>tunnel interface must be named tunN</constraintErrorMessage>
+ <valueHelp>
+ <format>tunN</format>
+ <description>Tunnel interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/interface-description.xml.i>
+ #include <include/address-ipv4-ipv6.xml.i>
+ #include <include/interface-disable.xml.i>
+ #include <include/interface-disable-link-detect.xml.i>
+ #include <include/interface-vrf.xml.i>
+ #include <include/interface-mtu-64-8024.xml.i>
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
+ <leafNode name="local-ip">
+ <properties>
+ <help>Local IP address for this tunnel</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Local IPv4 address for this tunnel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Local IPv6 address for this tunnel [NOTICE: unavailable for mGRE tunnels]</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_local.py</script>
+ </completionHelp>
+ <constraint>
+ <!-- does it need fixing/changing to be more restrictive ? -->
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-ip">
+ <properties>
+ <help>Remote IP address for this tunnel</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Remote IPv4 address for this tunnel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Remote IPv6 address for this tunnel</description>
+ </valueHelp>
+ <constraint>
+ <!-- does it need fixing/changing to be more restrictive ? -->
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="6rd-prefix">
+ <properties>
+ <help>6rd network prefix</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="6rd-relay-prefix">
+ <properties>
+ <help>6rd relay prefix</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix of interface for 6rd</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="dhcp-interface">
+ <properties>
+ <help>dhcp interface</help>
+ <valueHelp>
+ <format>interface</format>
+ <description>DHCP interface that supplies the local IP address for this tunnel</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <constraint>
+ <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="encapsulation">
+ <properties>
+ <help>Encapsulation of this tunnel interface</help>
+ <completionHelp>
+ <list>gre gre-bridge ipip sit ipip6 ip6ip6 ip6gre</list>
+ </completionHelp>
+ <valueHelp>
+ <format>gre-bridge</format>
+ <description>Generic Routing Encapsulation bridge interface</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipip</format>
+ <description>IP in IP encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sit</format>
+ <description>Simple Internet Transition encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipip6</format>
+ <description>IP in IP6 encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ip6ip6</format>
+ <description>IP6 in IP6 encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ip6gre</format>
+ <description>GRE over IPv6 network</description>
+ </valueHelp>
+ <constraint>
+ <regex>(gre|gre-bridge|ipip|sit|ipip6|ip6ip6|ip6gre)</regex>
+ </constraint>
+ <constraintErrorMessage>Must be one of 'gre' 'gre-bridge' 'ipip' 'sit' 'ipip6' 'ip6ip6' 'ip6gre'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="multicast">
+ <properties>
+ <help>Multicast operation over tunnel</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable Multicast</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable Multicast (default)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Must be 'disable' or 'enable'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="parameters">
+ <properties>
+ <help>Tunnel parameters</help>
+ </properties>
+ <children>
+ <node name="ip">
+ <properties>
+ <help>IPv4 specific tunnel parameters</help>
+ </properties>
+ <children>
+ <leafNode name="ttl">
+ <properties>
+ <help>Time to live field</help>
+ <valueHelp>
+ <format>0-255</format>
+ <description>Time to live (default 255)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ <constraintErrorMessage>TTL must be between 0 and 255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="tos">
+ <properties>
+ <help>Type of Service (TOS)</help>
+ <valueHelp>
+ <format>0-99</format>
+ <description>Type of Service (TOS)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-99"/>
+ </constraint>
+ <constraintErrorMessage>TOS must be between 0 and 99</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="key">
+ <properties>
+ <help>Tunnel key</help>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>Tunnel key</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ <constraintErrorMessage>key must be between 0-4294967295</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="ipv6">
+ <properties>
+ <help>IPv6 specific tunnel parameters</help>
+ </properties>
+ <children>
+ <leafNode name="encaplimit">
+ <properties>
+ <help>Encaplimit field</help>
+ <valueHelp>
+ <format>0-255</format>
+ <description>Encaplimit (default 4)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ <constraintErrorMessage>key must be between 0-255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="flowlabel">
+ <properties>
+ <help>Flowlabel</help>
+ <valueHelp>
+ <format>0x0-0x0FFFFF</format>
+ <description>Tunnel key, 'inherit' or hex value</description>
+ </valueHelp>
+ <constraint>
+ <regex>(0x){0,1}(0?[0-9A-Fa-f]{1,5})</regex>
+ </constraint>
+ <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="hoplimit">
+ <properties>
+ <help>Hoplimit</help>
+ <valueHelp>
+ <format>0-255</format>
+ <description>Hoplimit (default 64)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ <constraintErrorMessage>hoplimit must be between 0-255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="tclass">
+ <properties>
+ <help>Traffic class (Tclass)</help>
+ <valueHelp>
+ <format>0x0-0x0FFFFF</format>
+ <description>Traffic class, 'inherit' or hex value</description>
+ </valueHelp>
+ <constraint>
+ <regex>(0x){0,1}(0?[0-9A-Fa-f]{1,2})</regex>
+ </constraint>
+ <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in
index 16cb2c169..fdde57525 100644
--- a/interface-definitions/interfaces-vxlan.xml.in
+++ b/interface-definitions/interfaces-vxlan.xml.in
@@ -7,7 +7,7 @@
<help>Virtual Extensible LAN (VXLAN) Interface</help>
<priority>460</priority>
<constraint>
- <regex>vxlan[0-9]+$</regex>
+ <regex>^vxlan[0-9]+$</regex>
</constraint>
<constraintErrorMessage>VXLAN interface must be named vxlanN</constraintErrorMessage>
<valueHelp>
@@ -45,9 +45,28 @@
#include <include/interface-enable-proxy-arp.xml.i>
</children>
</node>
- <leafNode name="link">
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
+ <leafNode name="source-address">
+ <properties>
+ <help>VXLAN source address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 source-address of VXLAN tunnel</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="source-interface">
<properties>
- <help>Underlay device of VXLAN interface</help>
+ <help>Physical Interface used for this connection</help>
<valueHelp>
<format>interface</format>
<description>Interface used for VXLAN underlay</description>
diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in
index dd4a73efd..9db608afb 100644
--- a/interface-definitions/interfaces-wireguard.xml.in
+++ b/interface-definitions/interfaces-wireguard.xml.in
@@ -7,7 +7,7 @@
<help>WireGuard Interface</help>
<priority>459</priority>
<constraint>
- <regex>wg[0-9]+$</regex>
+ <regex>^wg[0-9]+$</regex>
</constraint>
<constraintErrorMessage>WireGuard interface must be named wgN</constraintErrorMessage>
<valueHelp>
@@ -19,22 +19,9 @@
#include <include/address-ipv4-ipv6.xml.i>
#include <include/interface-description.xml.i>
#include <include/interface-disable.xml.i>
- <leafNode name="port">
- <properties>
- <help>Local port number to accept connections</help>
- <constraint>
- <validator name="numeric" argument="--range 1024-65535"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="mtu">
- <properties>
- <help>interface mtu size(default: 1420)</help>
- <constraint>
- <validator name="numeric" argument="--range 68-9000"/>
- </constraint>
- </properties>
- </leafNode>
+ #include <include/interface-vrf.xml.i>
+ #include <include/port-number.xml.i>
+ #include <include/interface-mtu-68-9000.xml.i>
<leafNode name="fwmark">
<properties>
<help>A 32-bit fwmark value set on all outgoing packets</help>
@@ -97,12 +84,19 @@
<multi/>
</properties>
</leafNode>
- <!-- eventually check format IP:port -->
- <leafNode name="endpoint">
+ <leafNode name="address">
<properties>
- <help>Remote endpoint (IP:port)</help>
+ <help>IP address of tunnel remote end</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IP address to listen for incoming connections</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
</properties>
</leafNode>
+ #include <include/port-number.xml.i>
<leafNode name="persistent-keepalive">
<properties>
<help>how often send keep alives in seconds</help>
diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in
index d6b257978..194669f77 100644
--- a/interface-definitions/interfaces-wireless.xml.in
+++ b/interface-definitions/interfaces-wireless.xml.in
@@ -7,7 +7,7 @@
<help>Wireless (WiFi/WLAN) Network Interface</help>
<priority>400</priority>
<constraint>
- <regex>wlan[0-9]+$</regex>
+ <regex>^wlan[0-9]+$</regex>
</constraint>
<constraintErrorMessage>Wireless interface must be named wlanN</constraintErrorMessage>
<valueHelp>
@@ -208,11 +208,11 @@
<properties>
<help>Number of antennas on this card</help>
<valueHelp>
- <format>1-9</format>
+ <format>1-8</format>
<description>Number of antennas for this card</description>
</valueHelp>
<constraint>
- <validator name="numeric" argument="--range 1-9"/>
+ <validator name="numeric" argument="--range 1-8"/>
</constraint>
</properties>
</leafNode>
@@ -320,7 +320,7 @@
<properties>
<help>VHT link adaptation capabilities</help>
<completionHelp>
- <list>single-user-beamformer single-user-beamformee multi-user-beamformer multi-user-beamformee</list>
+ <list>unsolicited both</list>
</completionHelp>
<valueHelp>
<format>unsolicited</format>
@@ -454,6 +454,7 @@
</leafNode>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
+ #include <include/interface-vrf.xml.i>
<leafNode name="expunge-failing-stations">
<properties>
<help>Disassociate stations based on excessive transmission failures</help>
@@ -468,6 +469,13 @@
#include <include/interface-enable-arp-ignore.xml.i>
</children>
</node>
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
<leafNode name="hw-id">
<properties>
<help>Media Access Control (MAC) address</help>
@@ -666,28 +674,10 @@
<constraintErrorMessage>Invalid WPA pass phrase, must be 8 to 63 printable characters!</constraintErrorMessage>
</properties>
</leafNode>
+ #include <include/radius-server.xml.i>
<node name="radius">
- <properties>
- <help>RADIUS specific configuration</help>
- </properties>
<children>
- <leafNode name="source-address">
- <properties>
- <help>RADIUS client forced local IP address</help>
- <valueHelp>
- <format>ipv4</format>
- <description>IPv4 address of RADIUS server</description>
- </valueHelp>
- </properties>
- </leafNode>
<tagNode name="server">
- <properties>
- <help>IP address of RADIUS server</help>
- <valueHelp>
- <format>ipv4</format>
- <description>IPv4 address of RADIUS server</description>
- </valueHelp>
- </properties>
<children>
<leafNode name="accounting">
<properties>
@@ -695,23 +685,6 @@
<valueless/>
</properties>
</leafNode>
- <leafNode name="port">
- <properties>
- <help>RADIUS server port (default: 1812)</help>
- <valueHelp>
- <format>1-65535</format>
- <description>RADIUS server port</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-65535"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="key">
- <properties>
- <help>RADIUS shared secret key</help>
- </properties>
- </leafNode>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/interfaces-wirelessmodem.xml.in b/interface-definitions/interfaces-wirelessmodem.xml.in
new file mode 100644
index 000000000..6bec34b56
--- /dev/null
+++ b/interface-definitions/interfaces-wirelessmodem.xml.in
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="wirelessmodem" owner="${vyos_conf_scripts_dir}/interfaces-wirelessmodem.py">
+ <properties>
+ <help>Wireless Modem (WWAN) Interface</help>
+ <priority>350</priority>
+ <constraint>
+ <regex>^wlm[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Wireless Modem interface must be named wlmN</constraintErrorMessage>
+ <valueHelp>
+ <format>wlmN</format>
+ <description>Wireless modem interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="apn">
+ <properties>
+ <help>Access Point Name (APN)</help>
+ </properties>
+ </leafNode>
+ <node name="backup">
+ <properties>
+ <help>Insert backup default route</help>
+ </properties>
+ <children>
+ <leafNode name="distance">
+ <properties>
+ <help>Distance backup default route</help>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Distance of the backup route (default: 10)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ <constraintErrorMessage>Must be between (1-255)</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/interface-description.xml.i>
+ #include <include/interface-disable.xml.i>
+ #include <include/interface-vrf.xml.i>
+ <leafNode name="device">
+ <properties>
+ <help>System device name (default: ttyUSB0)</help>
+ <valueHelp>
+ <format>ttyXXX</format>
+ <description>System TTY device name</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ #include <include/interface-disable-link-detect.xml.i>
+ #include <include/interface-mtu-68-9000.xml.i>
+ <node name="ipv6">
+ <children>
+ #include <include/ipv6-address.xml.i>
+ #include <include/ipv6-disable-forwarding.xml.i>
+ #include <include/ipv6-dup-addr-detect-transmits.xml.i>
+ </children>
+ </node>
+ <leafNode name="no-peer-dns">
+ <properties>
+ <help>Do not use peer supplied DNS server information</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ondemand">
+ <properties>
+ <help>Only dial when traffic is available</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/protocols-igmp.xml.in b/interface-definitions/protocols-igmp.xml.in
new file mode 100644
index 000000000..a9b11e1a3
--- /dev/null
+++ b/interface-definitions/protocols-igmp.xml.in
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<!-- Internet Group Management Protocol (IGMP) configuration -->
+<interfaceDefinition>
+ <node name="protocols">
+ <children>
+ <node name="igmp" owner="${vyos_conf_scripts_dir}/protocols_igmp.py">
+ <properties>
+ <help>Internet Group Management Protocol (IGMP)</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>IGMP interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="join">
+ <properties>
+ <help>IGMP join multicast group</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Multicast group address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="source">
+ <properties>
+ <help>Source address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Source address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="version">
+ <properties>
+ <help>IGMP version</help>
+ <valueHelp>
+ <format>2-3</format>
+ <description>IGMP version</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 2-3"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="query-interval">
+ <properties>
+ <help>IGMP host query interval</help>
+ <valueHelp>
+ <format>1-1800</format>
+ <description>Query interval in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-1800"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="query-max-response-time">
+ <properties>
+ <help>IGMP max query response time</help>
+ <valueHelp>
+ <format>10-250</format>
+ <description>Query response value in deci-seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 10-250"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/protocols-mpls.xml.in b/interface-definitions/protocols-mpls.xml.in
new file mode 100644
index 000000000..376323855
--- /dev/null
+++ b/interface-definitions/protocols-mpls.xml.in
@@ -0,0 +1,98 @@
+<?xml version="1.0"?>
+<!-- Multiprotocol Label Switching (MPLS) configuration -->
+<interfaceDefinition>
+ <node name="protocols">
+ <children>
+ <node name="mpls" owner="${vyos_conf_scripts_dir}/protocols_mpls.py">
+ <properties>
+ <help>Multiprotocol Label Switching (MPLS)</help>
+ <priority>299</priority>
+ </properties>
+ <children>
+ <node name="ldp">
+ <properties>
+ <help>LDP options</help>
+ </properties>
+ <children>
+ <leafNode name="router-id">
+ <properties>
+ <help>x.x.x.x Label Switch Router (LSR) id</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>LSR ipv4 id</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="neighbor">
+ <properties>
+ <help>LDP Id of neighbor</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>neighbor IPv4 id</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="password">
+ <properties>
+ <help>Peer password</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="discovery">
+ <properties>
+ <help>Discovery parameters</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Discovery parameters</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="transport-ipv4-address">
+ <properties>
+ <help>Transport ipv4 address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 bind as transport</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="transport-ipv6-address">
+ <properties>
+ <help>Transport ipv6 address</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 bind as transport</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="interface">
+ <properties>
+ <help>Listen interface for LDP</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition> \ No newline at end of file
diff --git a/interface-definitions/protocols-pim.xml.in b/interface-definitions/protocols-pim.xml.in
new file mode 100644
index 000000000..6152045a7
--- /dev/null
+++ b/interface-definitions/protocols-pim.xml.in
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<!-- Protocol Independent Multicast (PIM) configuration -->
+<interfaceDefinition>
+ <node name="protocols">
+ <children>
+ <node name="pim" owner="${vyos_conf_scripts_dir}/protocols_pim.py">
+ <properties>
+ <help>Protocol Independent Multicast (PIM)</help>
+ <priority>400</priority>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>PIM interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="dr-priority">
+ <properties>
+ <help>Designated Router Election Priority</help>
+ <valueHelp>
+ <format>1-4294967295</format>
+ <description>Value of the new DR Priority</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="hello">
+ <properties>
+ <help>Hello Interval</help>
+ <valueHelp>
+ <format>1-180</format>
+ <description>Hello Interval in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-180"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="rp">
+ <properties>
+ <help>Rendezvous Point</help>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Rendezvous Point address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Rendezvous Point address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="group">
+ <properties>
+ <help>Group Address range</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>Group Address range RFC 3171</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="keep-alive-timer">
+ <properties>
+ <help>Keep alive Timer</help>
+ <valueHelp>
+ <format>31-60000</format>
+ <description>Keep alive Timer in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 31-60000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/salt-minion.xml.in b/interface-definitions/salt-minion.xml.in
new file mode 100644
index 000000000..9aa60249a
--- /dev/null
+++ b/interface-definitions/salt-minion.xml.in
@@ -0,0 +1,85 @@
+<?xml version="1.0"?>
+<!--Salt-minion configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="salt-minion" owner="${vyos_conf_scripts_dir}/salt-minion.py">
+ <properties>
+ <help>Salt Minion</help>
+ <priority>500</priority>
+ </properties>
+ <children>
+ <leafNode name="hash_type">
+ <properties>
+ <help>The hash_type is the hash to use when discovering the hash of a file on the master server.</help>
+ </properties>
+ </leafNode>
+ <leafNode name="log_file">
+ <properties>
+ <help>The location of the minion log file.</help>
+ </properties>
+ </leafNode>
+ <leafNode name="log_level">
+ <properties>
+ <help>Log level</help>
+ <valueHelp>
+ <format>garbage</format>
+ <description>log garbage info</description>
+ </valueHelp>
+ <valueHelp>
+ <format>trace</format>
+ <description>log trace info</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>log debug info</description>
+ </valueHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>log info</description>
+ </valueHelp>
+ <valueHelp>
+ <format>warning</format>
+ <description>log warning info</description>
+ </valueHelp>
+ <valueHelp>
+ <format>error</format>
+ <description>log error info</description>
+ </valueHelp>
+ <valueHelp>
+ <format>critical</format>
+ <description>log critical info</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="master">
+ <properties>
+ <help>The hostname or IP address of the master.</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="id">
+ <properties>
+ <help>Explicitly declare the id for this minion to use.</help>
+ </properties>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>The user to run the Salt processes.</help>
+ </properties>
+ </leafNode>
+ <leafNode name="mine_interval">
+ <properties>
+ <help>The number of minutes between mine updates.</help>
+ </properties>
+ </leafNode>
+ <leafNode name="master-key">
+ <properties>
+ <help>Enables verification of the master-public-signature returned by the master in auth-replies.</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/service-router-advert.xml.in b/interface-definitions/service-router-advert.xml.in
new file mode 100644
index 000000000..bd63b15a3
--- /dev/null
+++ b/interface-definitions/service-router-advert.xml.in
@@ -0,0 +1,266 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="router-advert" owner="${vyos_conf_scripts_dir}/service-router-advert.py">
+ <properties>
+ <help>IPv6 Router Advertisements (RAs) service</help>
+ <priority>900</priority>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Interface to send DDNS updates for [REQUIRED]</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="hop-limit">
+ <properties>
+ <help>Set Hop Count field of the IP header for outgoing packets (default: 64)</help>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Value should represent current diameter of the Internet</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0</format>
+ <description>Unspecified (by this router)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ <constraintErrorMessage>Hop count must be between 0 and 255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="default-lifetime">
+ <properties>
+ <help>Lifetime associated with the default router in units of seconds</help>
+ <valueHelp>
+ <format>4-9000</format>
+ <description>Router Lifetime in seconds</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0</format>
+ <description>Not a default router</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-0 --range 4-9000"/>
+ </constraint>
+ <constraintErrorMessage>Default router livetime bust be 0 or between 4 and 9000</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="default-preference">
+ <properties>
+ <help>Preference associated with the default router,</help>
+ <completionHelp>
+ <list>low medium high</list>
+ </completionHelp>
+ <valueHelp>
+ <format>low</format>
+ <description>Default router has low preference</description>
+ </valueHelp>
+ <valueHelp>
+ <format>medium</format>
+ <description>Default router has medium preference (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>high</format>
+ <description>Default router has high preference</description>
+ </valueHelp>
+ <constraint>
+ <regex>(low|medium|high)</regex>
+ </constraint>
+ <constraintErrorMessage>Default preference must be low, medium or high</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="dnssl">
+ <properties>
+ <help>DNS search list</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="link-mtu">
+ <properties>
+ <help>Link MTU value placed in RAs, exluded in RAs if unset</help>
+ <valueHelp>
+ <format>1280-9000</format>
+ <description>Link MTU value in RAs</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1280-9000"/>
+ </constraint>
+ <constraintErrorMessage>Link MTU must be between 1280 and 9000</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="managed-flag">
+ <properties>
+ <help>Hosts use the administered (stateful) protocol for address autoconfiguration in addition to any addresses autoconfigured using SLAAC</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="interval">
+ <properties>
+ <help>Set interval between unsolicited multicast RAs</help>
+ </properties>
+ <children>
+ <leafNode name="max">
+ <properties>
+ <help>Maximum interval between unsolicited multicast RAs (default: 600)</help>
+ <valueHelp>
+ <format>4-1800</format>
+ <description>Maximum interval in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 4-1800"/>
+ </constraint>
+ <constraintErrorMessage>Maximum interval must be between 4 and 1800 seconds</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="min">
+ <properties>
+ <help>Minimum interval between unsolicited multicast RAs</help>
+ <valueHelp>
+ <format>3-1350</format>
+ <description>Minimum interval in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 3-1350"/>
+ </constraint>
+ <constraintErrorMessage>Minimum interval must be between 3 and 1350 seconds</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="name-server">
+ <properties>
+ <help>IPv6 address of recursive DNS server</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address of DNS name server</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="other-config-flag">
+ <properties>
+ <help>Hosts use the administered (stateful) protocol for autoconfiguration of other (non-address) information</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <tagNode name="prefix">
+ <properties>
+ <help>IPv6 prefix to be advertised in Router Advertisements (RAs)</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 prefix to be advertized</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="no-autonomous-flag">
+ <properties>
+ <help>Prefix can not be used for stateless address auto-configuration</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="no-on-link-flag">
+ <properties>
+ <help>Prefix can not be used for on-link determination</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="preferred-lifetime">
+ <properties>
+ <help>Time in seconds that the prefix will remain preferred (default 4 hours)</help>
+ <completionHelp>
+ <list>infinity</list>
+ </completionHelp>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>Time in seconds that the prefix will remain preferred</description>
+ </valueHelp>
+ <valueHelp>
+ <format>infinity</format>
+ <description>Prefix will remain preferred forever</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ <regex>(infinity)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="valid-lifetime">
+ <properties>
+ <help>Time in seconds that the prefix will remain valid (default: 30 days)</help>
+ <completionHelp>
+ <list>infinity</list>
+ </completionHelp>
+ <valueHelp>
+ <format>1-4294967295</format>
+ <description>Time in seconds that the prefix will remain valid</description>
+ </valueHelp>
+ <valueHelp>
+ <format>infinity</format>
+ <description>Prefix will remain preferred forever</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ <regex>(infinity)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="reachable-time">
+ <properties>
+ <help>Time, in milliseconds, that a node assumes a neighbor is reachable after having received a reachability confirmation</help>
+ <valueHelp>
+ <format>1-3600000</format>
+ <description>Reachable Time value in RAs (in milliseconds)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0</format>
+ <description>Reachable Time unspecified by this router</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-0 --range 1-3600000"/>
+ </constraint>
+ <constraintErrorMessage>Reachable time must be 0 or between 1 and 3600000 milliseconds</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="retrans-timer">
+ <properties>
+ <help>Time in milliseconds between retransmitted Neighbor Solicitation messages</help>
+ <valueHelp>
+ <format>1-4294967295</format>
+ <description>Minimum interval in milliseconds</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0</format>
+ <description>Time, in milliseconds, between retransmitted Neighbor Solicitation messages</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-0 --range 1-4294967295"/>
+ </constraint>
+ <constraintErrorMessage>Retransmit interval must be 0 or between 1 and 4294967295 milliseconds</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="no-send-advert">
+ <properties>
+ <help>Do not send router adverts</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in
index 3ed85b8d3..2499a192c 100644
--- a/interface-definitions/system-login.xml.in
+++ b/interface-definitions/system-login.xml.in
@@ -110,58 +110,11 @@
</leafNode>
</children>
</tagNode>
+ #include <include/radius-server.xml.i>
<node name="radius">
- <properties>
- <help>RADIUS based user authentication</help>
- </properties>
<children>
- <leafNode name="source-address">
- <properties>
- <help>RADIUS client source address</help>
- <valueHelp>
- <format>ipv4</format>
- <description>TFTP IPv4 listen address</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-address"/>
- </constraint>
- </properties>
- </leafNode>
<tagNode name="server">
- <properties>
- <help>RADIUS server configuration</help>
- <valueHelp>
- <format>ipv4</format>
- <description>RADIUS server IPv4 address</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-address"/>
- </constraint>
- </properties>
<children>
- <leafNode name="disable">
- <properties>
- <help>Temporary disable this server</help>
- <valueless/>
- </properties>
- </leafNode>
- <leafNode name="key">
- <properties>
- <help>Shared secret key</help>
- </properties>
- </leafNode>
- <leafNode name="port">
- <properties>
- <help>Authentication port</help>
- <valueHelp>
- <format>1-65535</format>
- <description>Numeric IP port (default: 1812)</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-65535"/>
- </constraint>
- </properties>
- </leafNode>
<leafNode name="timeout">
<properties>
<help>Session timeout</help>
diff --git a/interface-definitions/system-syslog.xml.in b/interface-definitions/system-syslog.xml.in
index 2079ec0ea..194cdb851 100644
--- a/interface-definitions/system-syslog.xml.in
+++ b/interface-definitions/system-syslog.xml.in
@@ -181,13 +181,17 @@
<properties>
<help>Logging to a remote host</help>
<constraint>
- <validator name="ip-address" />
- <regex>(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)</regex>
+ <validator name="ip-address"/>
+ <validator name="fqdn"/>
</constraint>
- <constraintErrorMessage>Invalid host FQDN or IP address</constraintErrorMessage>
+ <constraintErrorMessage>Invalid host (FQDN or IP address)</constraintErrorMessage>
<valueHelp>
- <format>x.x.x.x or host.domain.tld</format>
- <description>Remote host name or IP address</description>
+ <format>ipv4</format>
+ <description>Remote syslog server IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hostname</format>
+ <description>Remote syslog server FQDN</description>
</valueHelp>
</properties>
<children>
diff --git a/interface-definitions/l2tp-server.xml.in b/interface-definitions/vpn-l2tp.xml.in
index 7fc844054..7fc844054 100644
--- a/interface-definitions/l2tp-server.xml.in
+++ b/interface-definitions/vpn-l2tp.xml.in
diff --git a/interface-definitions/sstp.xml.in b/interface-definitions/vpn-sstp.xml.in
index 10b97b833..b026417b3 100644
--- a/interface-definitions/sstp.xml.in
+++ b/interface-definitions/vpn-sstp.xml.in
@@ -1,11 +1,11 @@
<?xml version="1.0"?>
<interfaceDefinition>
- <node name="service">
+ <node name="vpn">
<children>
- <node name="sstp-server" owner="${vyos_conf_scripts_dir}/accel_sstp.py">
+ <node name="sstp" owner="${vyos_conf_scripts_dir}/vpn_sstp.py">
<properties>
- <help>Secure Socket Tunneling Protocol (SSTP) Server</help>
- <priority>900</priority>
+ <help>Secure Socket Tunneling Protocol (SSTP) server</help>
+ <priority>901</priority>
</properties>
<children>
<node name="authentication">
@@ -113,85 +113,96 @@
<multi />
</properties>
</leafNode>
- <tagNode name="radius-server">
- <properties>
- <help>IP address of RADIUS server</help>
- <valueHelp>
- <format>ipv4</format>
- <description>IP address of RADIUS server</description>
- </valueHelp>
- </properties>
- <children>
- <leafNode name="secret">
- <properties>
- <help>Key for accessing the specified server</help>
- </properties>
- </leafNode>
- <leafNode name="req-limit">
- <properties>
- <help>Maximum number of simultaneous requests to server (default: unlimited)</help>
- </properties>
- </leafNode>
- <leafNode name="fail-time">
- <properties>
- <help>If server does not responds mark it as unavailable for this time (seconds)</help>
- </properties>
- </leafNode>
- </children>
- </tagNode>
- <node name="radius-settings">
- <properties>
- <help>RADIUS settings</help>
- </properties>
+ #include <include/radius-server.xml.i>
+ <node name="radius">
<children>
+ <tagNode name="server">
+ <children>
+ <leafNode name="fail-time">
+ <properties>
+ <help>Mark server unavailable for &lt;n&gt; seconds on failure</help>
+ <valueHelp>
+ <format>0-600</format>
+ <description>Fail time penalty</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-600"/>
+ </constraint>
+ <constraintErrorMessage>Fail time must be between 0 and 600 seconds</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
<leafNode name="timeout">
<properties>
- <help>Timeout to wait response from server (seconds)</help>
+ <help>Timeout in seconds to wait response from RADIUS server</help>
+ <valueHelp>
+ <format>1-60</format>
+ <description>Timeout in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-60"/>
+ </constraint>
+ <constraintErrorMessage>Timeout must be between 1 and 60 seconds</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="acct-timeout">
<properties>
- <help>Timeout to wait reply for Interim-Update packets. (default 3 seconds)</help>
+ <help>Timeout for Interim-Update packets, terminate session afterwards (default 3 seconds)</help>
+ <valueHelp>
+ <format>0-60</format>
+ <description>Timeout in seconds, 0 to keep active</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-60"/>
+ </constraint>
+ <constraintErrorMessage>Timeout must be between 0 and 60 seconds</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="max-try">
<properties>
- <help>Maximum number of tries to send Access-Request/Accounting-Request queries</help>
+ <help>Number of tries to send Access-Request/Accounting-Request queries</help>
+ <valueHelp>
+ <format>1-20</format>
+ <description>Maximum tries</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-20"/>
+ </constraint>
+ <constraintErrorMessage>Maximum tries must be between 1 and 20</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="nas-identifier">
<properties>
- <help>Value to send to RADIUS server in NAS-Identifier attribute and to be matched in DM/CoA requests.</help>
+ <help>NAS-Identifier attribute sent to RADIUS</help>
</properties>
</leafNode>
<leafNode name="nas-ip-address">
<properties>
- <help>Value to send to RADIUS server in NAS-IP-Address attribute and to be matched in DM/CoA requests. Also DM/CoA server will bind to that address.</help>
+ <help>NAS-IP-Address attribute sent to RADIUS</help>
<constraint>
<validator name="ipv4-address"/>
</constraint>
- <constraintErrorMessage>invalid IPv4 address</constraintErrorMessage>
<valueHelp>
<format>ipv4</format>
- <description>NAS-IP-Address Attribute Value</description>
+ <description>NAS-IP-Address attribute</description>
</valueHelp>
- </properties>
- </leafNode>
- <node name="dae-server">
+ </properties>
+ </leafNode>
+ <node name="dynamic-author">
<properties>
- <help>IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA)</help>
+ <help>Dynamic Authorization Extension/Change of Authorization server</help>
</properties>
<children>
- <leafNode name="ip-address">
+ <leafNode name="server">
<properties>
<help>IP address for Dynamic Authorization Extension server (DM/CoA)</help>
<constraint>
<validator name="ipv4-address"/>
</constraint>
- <constraintErrorMessage>invalid IPv4 address</constraintErrorMessage>
<valueHelp>
<format>ipv4</format>
- <description>Specifies IP address for Dynamic Authorization Extension server (DM/CoA)</description>
+ <description>IPv4 address for aynamic authorization server</description>
</valueHelp>
</properties>
</leafNode>
@@ -207,9 +218,9 @@
</constraint>
</properties>
</leafNode>
- <leafNode name="secret">
+ <leafNode name="key">
<properties>
- <help>Secret for Dynamic Authorization Extension server (DM/CoA)</help>
+ <help>Shared secret for Dynamic Authorization Extension server</help>
</properties>
</leafNode>
</children>
@@ -221,17 +232,17 @@
<children>
<leafNode name="attribute">
<properties>
- <help>Specifies which radius attribute contains rate information. (default is Filter-Id)</help>
+ <help>Specifies RADIUS attribute containing rate information (default 'Filter-Id')</help>
</properties>
</leafNode>
<leafNode name="vendor">
<properties>
- <help>Specifies the vendor dictionary. (dictionary needs to be in /usr/share/accel-ppp/radius)</help>
+ <help>Specifies vendor dictionary (needs to be in /usr/share/accel-ppp/radius)</help>
</properties>
</leafNode>
<leafNode name="enable">
<properties>
- <help>Enables Bandwidth shaping via RADIUS</help>
+ <help>Enable RADIUS bandwidth shaping</help>
<valueless />
</properties>
</leafNode>
@@ -241,42 +252,46 @@
</node>
</children>
</node>
- <node name="sstp-settings">
+ <node name="ssl">
<properties>
- <help>SSTP settings</help>
+ <help>SSL Certificate, SSL Key and CA (/config/user-data/sstp)</help>
</properties>
<children>
- <node name="ssl-certs">
+ <leafNode name="ca-cert-file">
<properties>
- <help>SSL Certificate, SSL Key and CA (/config/user-data/sstp)</help>
+ <help>Certificate Authority certificate</help>
+ <valueHelp>
+ <format>file</format>
+ <description>File in /config/auth directory</description>
+ </valueHelp>
+ <constraint>
+ <validator name="file-exists" argument="--directory /config/auth"/>
+ </constraint>
</properties>
- <children>
- <leafNode name="ca">
- <properties>
- <help>Certificate Authority certificate</help>
- <completionHelp>
- <script>if [ -e /config/user-data/sstp ]; then ls /config/user-data/sstp; fi</script>
- </completionHelp>
- </properties>
- </leafNode>
- <leafNode name="server-cert">
- <properties>
- <help>Server Certificate</help>
- <completionHelp>
- <script>if [ -e /config/user-data/sstp ]; then ls /config/user-data/sstp; fi</script>
- </completionHelp>
- </properties>
- </leafNode>
- <leafNode name="server-key">
- <properties>
- <help>Privat Key of the Server Certificate</help>
- <completionHelp>
- <script>if [ -e /config/user-data/sstp ]; then ls /config/user-data/sstp; fi</script>
- </completionHelp>
- </properties>
- </leafNode>
- </children>
- </node>
+ </leafNode>
+ <leafNode name="cert-file">
+ <properties>
+ <help>Server Certificate</help>
+ <completionHelp>
+ <script>ls /config</script>
+ </completionHelp>
+ <constraint>
+ <validator name="file-exists" argument="--directory /config/auth"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="key-file">
+ <properties>
+ <help>Privat Key of the Server Certificate</help>
+ <valueHelp>
+ <format>file</format>
+ <description>File in /config/auth directory</description>
+ </valueHelp>
+ <constraint>
+ <validator name="file-exists" argument="--directory /config/auth"/>
+ </constraint>
+ </properties>
+ </leafNode>
</children>
</node>
<node name="network-settings">
@@ -318,14 +333,9 @@
</leafNode>
</children>
</node>
- <node name="dns-server">
+ <leafNode name="name-server">
<properties>
<help>DNS servers propagated to clients</help>
- </properties>
- <children>
- <leafNode name="primary-dns">
- <properties>
- <help>Primary DNS Server</help>
<valueHelp>
<format>ipv4</format>
<description>IPv4 address</description>
@@ -333,30 +343,10 @@
<constraint>
<validator name="ipv4-address"/>
</constraint>
- </properties>
- </leafNode>
- <leafNode name="secondary-dns">
- <properties>
- <help>Secondary DNS Server</help>
- <valueHelp>
- <format>ipv4</format>
- <description>IPv4 address</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-address"/>
- </constraint>
- </properties>
- </leafNode>
- </children>
- </node>
- <leafNode name="mtu">
- <properties>
- <help>Maximum Transmission Unit (MTU)</help>
- <constraint>
- <validator name="numeric" argument="--range 128-16384"/>
- </constraint>
+ <multi/>
</properties>
</leafNode>
+ #include <include/interface-mtu-68-1500.xml.i>
</children>
</node>
<node name="ppp-settings">
diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in
new file mode 100644
index 000000000..7c75bf824
--- /dev/null
+++ b/interface-definitions/vrf.xml.in
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="vrf" owner="${vyos_conf_scripts_dir}/vrf.py">
+ <properties>
+ <help>Virtual Routing and Forwarding</help>
+ <!-- must be before any interface creation -->
+ <priority>210</priority>
+ </properties>
+ <children>
+ <leafNode name="bind-to-all">
+ <properties>
+ <help>Enable binding services to all VRFs</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <tagNode name="name">
+ <properties>
+ <help>VRF instance name</help>
+ <constraint>
+ <validator name="vrf-name"/>
+ </constraint>
+ <constraintErrorMessage>VRF instance name must be 16 characters or less and can not\nbe named as regular network interfaces</constraintErrorMessage>
+ <valueHelp>
+ <format>name</format>
+ <description>Instance name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="table">
+ <properties>
+ <help>Routing table associated with this instance</help>
+ <valueHelp>
+ <format>100-2147483647</format>
+ <description>Routing table ID</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 100-2147483647"/>
+ </constraint>
+ <constraintErrorMessage>VRF routing table must be in range from 100 to 2147483647</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ #include <include/interface-description.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/vrrp.xml.in b/interface-definitions/vrrp.xml.in
index 89d22f79f..120c7d218 100644
--- a/interface-definitions/vrrp.xml.in
+++ b/interface-definitions/vrrp.xml.in
@@ -254,6 +254,45 @@
</completionHelp>
</properties>
</leafNode>
+ <node name="transition-script">
+ <properties>
+ <help>VRRP transition scripts</help>
+ </properties>
+ <children>
+ <leafNode name="master">
+ <properties>
+ <help>Script to run on VRRP state transition to master</help>
+ <constraint>
+ <validator name="script"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="backup">
+ <properties>
+ <help>Script to run on VRRP state transition to backup</help>
+ <constraint>
+ <validator name="script"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="fault">
+ <properties>
+ <help>Script to run on VRRP state transition to fault</help>
+ <constraint>
+ <validator name="script"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="stop">
+ <properties>
+ <help>Script to run on VRRP state transition to stop</help>
+ <constraint>
+ <validator name="script"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
</children>
</tagNode>
</children>
diff --git a/op-mode-definitions/reset-ip-igmp.xml b/op-mode-definitions/reset-ip-igmp.xml
new file mode 100644
index 000000000..143553d33
--- /dev/null
+++ b/op-mode-definitions/reset-ip-igmp.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="reset">
+ <children>
+ <node name="ip">
+ <children>
+ <node name="igmp">
+ <properties>
+ <help>IGMP clear commands</help>
+ </properties>
+ <children>
+ <leafNode name="interfaces">
+ <properties>
+ <help>Reset IGMP interfaces</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "clear ip igmp interfaces"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/reset-ip-multicast.xml b/op-mode-definitions/reset-ip-multicast.xml
new file mode 100644
index 000000000..d610add16
--- /dev/null
+++ b/op-mode-definitions/reset-ip-multicast.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="reset">
+ <children>
+ <node name="ip">
+ <children>
+ <node name="multicast">
+ <properties>
+ <help>IP multicast routing table</help>
+ </properties>
+ <children>
+ <leafNode name="route">
+ <properties>
+ <help>Clear multicast routing table</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "clear ip mroute"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-pppoe.xml b/op-mode-definitions/show-interfaces-pppoe.xml
index 3acb14486..591ec8f5b 100644
--- a/op-mode-definitions/show-interfaces-pppoe.xml
+++ b/op-mode-definitions/show-interfaces-pppoe.xml
@@ -27,6 +27,15 @@
</leafNode>
</children>
</node>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show specified wirelessmodem interface statistics</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_pppoe_peers.sh</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/sbin/pppstats $4</command>
+ </leafNode>
</children>
</tagNode>
</children>
diff --git a/op-mode-definitions/show-interfaces-wirelessmodem.xml b/op-mode-definitions/show-interfaces-wirelessmodem.xml
new file mode 100644
index 000000000..681f54f3d
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-wirelessmodem.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="wirelessmodem">
+ <properties>
+ <help>Show Wireless Modem (WWAN) interface information</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_wlm_peers.sh</script>
+ </completionHelp>
+ </properties>
+ <command>${vyatta_bindir}/vyatta-show-interfaces.pl --intf="$4"</command>
+ <children>
+ <node name="log">
+ <properties>
+ <help>Show PPPoE logs</help>
+ </properties>
+ <command>cat /var/log/vyatta/ppp_$4.log</command>
+ <children>
+ <leafNode name="tail">
+ <properties>
+ <help>Watch PPPoE logs</help>
+ </properties>
+ <command>tail --follow=name /var/log/vyatta/ppp_$4.log</command>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show specified wirelessmodem interface statistics</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_wlm_peers.sh</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/sbin/pppstats $4</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ip-igmp.xml b/op-mode-definitions/show-ip-igmp.xml
new file mode 100644
index 000000000..b8f2f9107
--- /dev/null
+++ b/op-mode-definitions/show-ip-igmp.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ip">
+ <children>
+ <node name="igmp">
+ <properties>
+ <help>Show IGMP (Internet Group Management Protocol) information</help>
+ </properties>
+ <children>
+ <leafNode name="groups">
+ <properties>
+ <help>IGMP groups information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip igmp groups"</command>
+ </leafNode>
+ <leafNode name="interfaces">
+ <properties>
+ <help>IGMP interfaces information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip igmp interface"</command>
+ </leafNode>
+ <leafNode name="join">
+ <properties>
+ <help>IGMP static join information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip igmp join"</command>
+ </leafNode>
+ <leafNode name="sources">
+ <properties>
+ <help>IGMP sources information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip igmp sources"</command>
+ </leafNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>IGMP statistics</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip igmp statistics"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ip-multicast.xml b/op-mode-definitions/show-ip-multicast.xml
index 6ffe40436..5331d2e35 100644
--- a/op-mode-definitions/show-ip-multicast.xml
+++ b/op-mode-definitions/show-ip-multicast.xml
@@ -21,6 +21,18 @@
</properties>
<command>if ps -C igmpproxy &amp;&gt;/dev/null; then ${vyos_op_scripts_dir}/show_igmpproxy.py --mfc; else echo IGMP proxy not configured; fi</command>
</leafNode>
+ <leafNode name="summary">
+ <properties>
+ <help>IP multicast information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip multicast"</command>
+ </leafNode>
+ <leafNode name="route">
+ <properties>
+ <help>IP multicast routing table</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip mroute"</command>
+ </leafNode>
</children>
</node>
</children>
diff --git a/op-mode-definitions/show-ip-pim.xml b/op-mode-definitions/show-ip-pim.xml
new file mode 100644
index 000000000..3f4edc779
--- /dev/null
+++ b/op-mode-definitions/show-ip-pim.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ip">
+ <children>
+ <node name="pim">
+ <properties>
+ <help>Show PIM (Protocol Independent Multicast) information</help>
+ </properties>
+ <children>
+ <leafNode name="interfaces">
+ <properties>
+ <help>PIM interfaces information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip pim interface"</command>
+ </leafNode>
+ <leafNode name="join">
+ <properties>
+ <help>PIM join information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip pim join"</command>
+ </leafNode>
+ <leafNode name="neighbor">
+ <properties>
+ <help>PIM neighbor information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip pim neighbor"</command>
+ </leafNode>
+ <leafNode name="nexthop">
+ <properties>
+ <help>PIM cached nexthop rpf information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip pim nexthop"</command>
+ </leafNode>
+ <leafNode name="state">
+ <properties>
+ <help>PIM state information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip pim state"</command>
+ </leafNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>PIM statistics</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip pim statistics"</command>
+ </leafNode>
+ <leafNode name="rp">
+ <properties>
+ <help>PIM RP (Rendevous Point) information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip pim rp-info"</command>
+ </leafNode>
+ <leafNode name="rpf">
+ <properties>
+ <help>PIM cached source rpf information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip pim rpf"</command>
+ </leafNode>
+ <leafNode name="upstream">
+ <properties>
+ <help>PIM upstream information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip pim upstream"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-mpls.xml b/op-mode-definitions/show-mpls.xml
new file mode 100644
index 000000000..6610788c7
--- /dev/null
+++ b/op-mode-definitions/show-mpls.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="mpls">
+ <children>
+ <node name="ldp">
+ <properties>
+ <help>Label Distribution Protocol (LDP)</help>
+ </properties>
+ <children>
+ <node name="binding">
+ <properties>
+ <help>Label Information Base</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show mpls ldp binding"</command>
+ </node>
+ <node name="discovery">
+ <properties>
+ <help>Discovery hello information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show mpls ldp discovery"</command>
+ </node>
+ <node name="interface">
+ <properties>
+ <help>LDP interface information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show mpls ldp interface"</command>
+ </node>
+ <node name="neighbor">
+ <properties>
+ <help>LDP neighbor information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show mpls ldp neighbor"</command>
+ <children>
+ <node name="detail">
+ <properties>
+ <help>Show neighbor detail</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show mpls ldp neighbor detail"</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="pseudowire">
+ <properties>
+ <help>Show MPLS pseudowire interfaces</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show mpls pseudowires"</command>
+ </node>
+ <node name="table">
+ <properties>
+ <help>Show MPLS table</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show mpls table"</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ntp.xml b/op-mode-definitions/show-ntp.xml
index 48cee5bee..b7f0acdf8 100644
--- a/op-mode-definitions/show-ntp.xml
+++ b/op-mode-definitions/show-ntp.xml
@@ -6,7 +6,7 @@
<properties>
<help>Show peer status of NTP daemon</help>
</properties>
- <command>if ps -C ntpd &amp;&gt;/dev/null; then ntpdc -n -c peers; else echo NTP daemon disabled; fi</command>
+ <command>if ps -C ntpd &amp;&gt;/dev/null; then ntpq -n -c peers; else echo NTP daemon disabled; fi</command>
<children>
<tagNode name="server">
<properties>
@@ -21,7 +21,7 @@
<properties>
<help>Show NTP operational summary</help>
</properties>
- <command>if ps -C ntpd &amp;&gt;/dev/null; then ntpdc -n -c sysinfo; ntpdc -n -c kerninfo; else echo NTP daemon disabled; fi</command>
+ <command>if ps -C ntpd &amp;&gt;/dev/null; then ntpq -n -c sysinfo; ntpq -n -c kerninfo; else echo NTP daemon disabled; fi</command>
</node>
</children>
diff --git a/op-mode-definitions/show-raid.xml b/op-mode-definitions/show-raid.xml
index 000fd4610..8bf394552 100644
--- a/op-mode-definitions/show-raid.xml
+++ b/op-mode-definitions/show-raid.xml
@@ -4,7 +4,7 @@
<children>
<tagNode name="raid">
<properties>
- <help>Show statis of RAID set</help>
+ <help>Show status of RAID set</help>
<completionHelp>
<script>${vyos_completion_dir}/list_raidset.sh</script>
</completionHelp>
diff --git a/op-mode-definitions/show-system-info.xml b/op-mode-definitions/show-system-info.xml
index ade3829f2..61c947bbe 100644
--- a/op-mode-definitions/show-system-info.xml
+++ b/op-mode-definitions/show-system-info.xml
@@ -51,6 +51,13 @@
</children>
</node>
+ <leafNode name= "integrity">
+ <properties>
+ <help>Checks overall system integrity</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/system_integrity.py</command>
+ </leafNode>
+
<leafNode name="kernel-messages">
<properties>
<help>Show messages in kernel ring buffer</help>
diff --git a/op-mode-definitions/show-systemintegrity.xml b/op-mode-definitions/show-systemintegrity.xml
deleted file mode 100644
index 44b5faf68..000000000
--- a/op-mode-definitions/show-systemintegrity.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0"?>
-
-<interfaceDefinition>
- <node name="show">
- <children>
- <leafNode name= "system-integrity">
- <properties>
- <help>checks the integrity of the system</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/system_integrity.py</command>
- </leafNode>
- </children>
- </node>
-</interfaceDefinition>
diff --git a/op-mode-definitions/show-vrf.xml b/op-mode-definitions/show-vrf.xml
new file mode 100644
index 000000000..360153d8e
--- /dev/null
+++ b/op-mode-definitions/show-vrf.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="vrf">
+ <properties>
+ <help>Show VRF information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_vrf.py -e</command>
+ </node>
+ <tagNode name="vrf">
+ <properties>
+ <help>Show information on specific VRF instance</help>
+ <completionHelp>
+ <path>vrf name</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml
index 85f6047c1..d623fe103 100644
--- a/op-mode-definitions/traceroute.xml
+++ b/op-mode-definitions/traceroute.xml
@@ -14,7 +14,6 @@
</properties>
<command>/usr/bin/traceroute $2</command>
</tagNode>
-
<tagNode name="ipv4">
<properties>
<help>Track network path to &lt;hostname|IPv4 address&gt;</help>
@@ -24,7 +23,6 @@
</properties>
<command>/usr/bin/traceroute -4 $3</command>
</tagNode>
-
<tagNode name="ipv6">
<properties>
<help>Track network path to &lt;hostname|IPv6 address&gt;</help>
@@ -34,9 +32,45 @@
</properties>
<command>/usr/bin/traceroute -6 $3</command>
</tagNode>
+ <tagNode name="vrf">
+ <properties>
+ <help>Track network path to specified node via given VRF instance</help>
+ <completionHelp>
+ <path>vrf name</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="">
+ <properties>
+ <help>Track network path to specified node</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ip vrf exec "$3" traceroute "$4"</command>
+ </tagNode>
+ <tagNode name="ipv4">
+ <properties>
+ <help>Track network path to &lt;hostname|IPv4 address&gt;</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ip vrf exec "$3" traceroute -4 "$5"</command>
+ </tagNode>
+ <tagNode name="ipv6">
+ <properties>
+ <help>Track network path to &lt;hostname|IPv6 address&gt;</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ip vrf exec "$3" traceroute -6 "$5"</command>
+ </tagNode>
+ </children>
+ </tagNode>
</children>
</node>
-
<node name="monitor">
<children>
<tagNode name="traceroute">
diff --git a/python/setup.py b/python/setup.py
index 304ea5cb7..ac7d0b573 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -10,7 +10,7 @@ setup(
license = "LGPLv2+",
keywords = "vyos",
url = "http://www.vyos.io",
- packages=['vyos'],
+ packages=["vyos","vyos.ifconfig"],
long_description="VyOS configuration libraries",
classifiers=[
"Development Status :: 4 - Beta",
diff --git a/python/vyos/__init__.py b/python/vyos/__init__.py
index 9b5ed21c9..e3e14fdd8 100644
--- a/python/vyos/__init__.py
+++ b/python/vyos/__init__.py
@@ -1 +1 @@
-from .base import *
+from .base import ConfigError
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
new file mode 100644
index 000000000..664974d5f
--- /dev/null
+++ b/python/vyos/airbag.py
@@ -0,0 +1,169 @@
+# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import logging
+import logging.handlers
+from datetime import datetime
+
+from vyos.config import Config
+from vyos.version import get_version
+from vyos.util import run
+from vyos.util import debug
+
+
+# we allow to disable the extra logging
+DISABLE = False
+
+
+# emulate a file object
+class _IO(object):
+ def __init__(self, std, log):
+ self.std = std
+ self.log = log
+
+ def write(self, message):
+ self.std.write(message)
+ if DISABLE:
+ return
+ for line in message.split('\n'):
+ s = line.rstrip()
+ if s:
+ self.log(s)
+
+ def flush(self):
+ self.std.flush()
+
+ def close(self):
+ pass
+
+
+# The function which will be used to report information
+# to users when an exception is unhandled
+def bug_report(dtype, value, trace):
+ from traceback import format_exception
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ information = {
+ 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'version': get_version(),
+ 'trace': format_exception(dtype, value, trace),
+ 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED,
+ }
+
+ sys.stdout.write(INTRO.format(**information))
+ sys.stdout.flush()
+
+ sys.stderr.write(FAULT.format(**information))
+ sys.stderr.flush()
+
+
+# define an exception handler to be run when an exception
+# reach the end of __main__ and was not intercepted
+def intercepter(dtype, value, trace):
+ bug_report(dtype, value, trace)
+ # debug returns either '' or 'developer' if debuging is enabled
+ if debug('developer'):
+ import pdb
+ pdb.pm()
+
+
+def InterceptingLogger(address, _singleton=[False]):
+ skip = _singleton.pop()
+ _singleton.append(True)
+ if skip:
+ return
+
+ logger = logging.getLogger('VyOS')
+ logger.setLevel(logging.DEBUG)
+ handler = logging.handlers.SysLogHandler(address='/dev/log', facility='syslog')
+ logger.addHandler(handler)
+
+ # log to syslog any message sent to stderr
+ sys.stderr = _IO(sys.stderr, logger.critical)
+
+
+# lists as default arguments in function is normally dangerous
+# as they will keep any modification performed, unless this is
+# what you want to do (in that case to only run the code once)
+def InterceptingException(excepthook,_singleton=[False]):
+ skip = _singleton.pop()
+ _singleton.append(True)
+ if skip:
+ return
+
+ # install the handler to replace the default behaviour
+ # which just prints the exception trace on screen
+ sys.excepthook = excepthook
+
+
+# Do not attempt the extra logging for operational commands
+try:
+ # This fails during boot
+ insession = Config().in_session()
+except:
+ # we save info on boot to help debugging
+ insession = True
+
+
+# Installing the interception, it currently does not work when
+# running testing so we are checking that we are on the router
+# as otherwise it prevents dpkg-buildpackage to work
+if get_version() and insession:
+ InterceptingLogger('/run/systemd/journal/dev-log')
+ InterceptingException(intercepter)
+
+
+# Messages to print
+
+FAULT = """\
+Date: {date}
+VyOS image: {version}
+
+{trace}
+"""
+
+INTRO = """\
+VyOS had an issue completing a command.
+
+We are sorry that you encountered a problem with VyOS.
+There are a few things you can do to help us (and yourself):
+{instructions}
+
+PLEASE, when reporting, do include as much information as you can:
+- do not obfuscate any data (feel free to send us a private communication with
+ the extra information if your business policy is strict on information sharing)
+- and include all the information presented below
+
+"""
+
+COMMUNITY = """\
+- Make sure you are running the latest version of the code available at
+ https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso
+- Consult the forum to see how to handle this issue
+ https://forum.vyos.io
+- Join our community on slack where our users exchange help and advice
+ https://vyos.slack.com
+""".strip()
+
+SUPPORTED = """\
+- Make sure you are running the latest stable version of VyOS
+ the code is available at https://downloads.vyos.io/?dir=release/current
+- Contact us on our online help desk
+ https://support.vyos.io/
+""".strip()
diff --git a/python/vyos/authutils.py b/python/vyos/authutils.py
index 234294649..90a46ffb4 100644
--- a/python/vyos/authutils.py
+++ b/python/vyos/authutils.py
@@ -15,16 +15,14 @@
import re
-from subprocess import Popen, PIPE, STDOUT
+from vyos.util import cmd
def make_password_hash(password):
""" Makes a password hash for /etc/shadow using mkpasswd """
- mkpasswd = Popen(['mkpasswd', '--method=sha-512', '--stdin'], stdout=PIPE, stdin=PIPE, stderr=PIPE)
- hash = mkpasswd.communicate(input=password.encode(), timeout=5)[0].decode().strip()
-
- return hash
+ mkpassword = 'mkpasswd --method=sha-512 --stdin'
+ return cmd(mkpassword, input=password.encode(), timeout=5)
def split_ssh_public_key(key_string, defaultname=""):
""" Splits an SSH public key into its components """
diff --git a/python/vyos/config.py b/python/vyos/config.py
index 2342f7021..75055a603 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -238,6 +238,19 @@ class Config(object):
str: working configuration
"""
+ # show_config should be independent of CLI edit level.
+ # Set the CLI edit environment to the top level, and
+ # restore original on exit.
+ save_env = self.__session_env
+
+ env_str = self._run(self._make_command('getEditResetEnv', ''))
+ env_list = re.findall(r'([A-Z_]+)=\'([^;\s]+)\'', env_str)
+ root_env = os.environ
+ for k, v in env_list:
+ root_env[k] = v
+
+ self.__session_env = root_env
+
# FIXUP: by default, showConfig will give you a diff
# if there are uncommitted changes.
# The config parser obviously cannot work with diffs,
@@ -253,8 +266,10 @@ class Config(object):
path = " ".join(path)
try:
out = self._run(self._make_command('showConfig', path))
+ self.__session_env = save_env
return out
except VyOSError:
+ self.__session_env = save_env
return(default)
def get_config_dict(self, path=[], effective=False):
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 80e199907..24fe174d2 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -123,10 +123,15 @@ def vlan_to_dict(conf):
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
+ 'ip_proxy_arp': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'ingress_qos': '',
'ingress_qos_changed': False,
'mac': '',
- 'mtu': 1500
+ 'mtu': 1500,
+ 'vrf': ''
}
# retrieve configured interface addresses
if conf.exists('address'):
@@ -186,6 +191,22 @@ def vlan_to_dict(conf):
if conf.exists('ip enable-arp-ignore'):
vlan['ip_enable_arp_ignore'] = 1
+ # Enable Proxy ARP
+ if conf.exists('ip enable-proxy-arp'):
+ vlan['ip_proxy_arp'] = 1
+
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ vlan['ipv6_autoconf'] = 1
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ vlan['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ vlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# Media Access Control (MAC) address
if conf.exists('mac'):
vlan['mac'] = conf.return_value('mac')
@@ -194,6 +215,10 @@ def vlan_to_dict(conf):
if conf.exists('mtu'):
vlan['mtu'] = int(conf.return_value('mtu'))
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ vlan['vrf'] = conf.return_value('vrf')
+
# VLAN egress QoS
if conf.exists('egress-qos'):
vlan['egress_qos'] = conf.return_value('egress-qos')
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index d326b3b11..aaf08e726 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -29,6 +29,7 @@ SAVE_CONFIG = ['/opt/vyatta/sbin/vyatta-save-config.pl']
INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image']
REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del']
GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate']
+SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show']
# Default "commit via" string
APP = "vyos-http-api"
@@ -181,5 +182,10 @@ class ConfigSession(object):
return out
def generate(self, cmd):
- out = self.__run_command(GENERATE + cmd)
+ out = self.__run_command(GENERATE + cmd.split())
return out
+
+ def show(self, cmd):
+ out = self.__run_command(SHOW + cmd.split())
+ return out
+
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 0274f3573..a0b0eb3c1 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -24,58 +24,10 @@ def escape_backslash(string: str) -> str:
result = p.sub(r'\\\\', string)
return result
-def strip_comments(s):
- """ Split a config string into the config section and the trailing comments """
- INITIAL = 0
- IN_COMMENT = 1
-
- i = len(s) - 1
-
- state = INITIAL
-
- config_end = 0
-
- # Find the first character of the comments section at the end,
- # if it exists
- while (i >= 0):
- c = s[i]
-
- if (state == INITIAL) and re.match(r'\s', c):
- # Ignore whitespace
- if (i != 0):
- i -= 1
- else:
- config_end = 0
- break
- elif (state == INITIAL) and not re.match(r'(\s|\/)', c):
- # Assume there are no (more) trailing comments,
- # this is an end of a node: either a brace of the last character
- # of a leaf node value
- config_end = i + 1
- break
- elif (state == INITIAL) and (c == '/'):
- # A comment begins, or it's a stray slash
- if (s[i-1] == '*'):
- state = IN_COMMENT
- i -= 2
- else:
- raise ValueError("Invalid syntax: stray slash at character {0}".format(i + 1))
- elif (state == IN_COMMENT) and (c == '*'):
- # A comment ends here
- try:
- if (s[i-1] == '/'):
- state = INITIAL
- i -= 2
- except:
- raise ValueError("Invalid syntax: malformed commend end at character {0}".format(i + 1))
- elif (state == IN_COMMENT) and (c != '*'):
- # Ignore everything inside comments, including braces
- i -= 1
- else:
- # Shouldn't happen
- raise ValueError("Invalid syntax at character {0}: invalid character {1}".format(i + 1, c))
-
- return (s[0:config_end], s[config_end+1:])
+def extract_version(s):
+ """ Extract the version string from the config string """
+ t = re.split('(^//)', s, maxsplit=1, flags=re.MULTILINE)
+ return (s, ''.join(t[1:]))
def check_path(path):
# Necessary type checking
@@ -174,7 +126,7 @@ class ConfigTree(object):
self.__destroy = self.__lib.destroy
self.__destroy.argtypes = [c_void_p]
- config_section, comments_section = strip_comments(config_string)
+ config_section, version_section = extract_version(config_string)
config_section = escape_backslash(config_section)
config = self.__from_string(config_section.encode())
if config is None:
@@ -182,7 +134,7 @@ class ConfigTree(object):
raise ValueError("Failed to parse config: {0}".format(msg))
else:
self.__config = config
- self.__comments = comments_section
+ self.__version = version_section
def __del__(self):
if self.__config is not None:
@@ -193,7 +145,7 @@ class ConfigTree(object):
def to_string(self):
config_string = self.__to_string(self.__config).decode()
- config_string = "{0}\n{1}".format(config_string, self.__comments)
+ config_string = "{0}\n{1}".format(config_string, self.__version)
return config_string
def to_commands(self):
diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py
deleted file mode 100644
index 81867d086..000000000
--- a/python/vyos/ifconfig.py
+++ /dev/null
@@ -1,1920 +0,0 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-import jinja2
-import json
-import glob
-import time
-
-import vyos.interfaces
-
-from vyos.validate import *
-from vyos.config import Config
-from vyos import ConfigError
-
-from ipaddress import IPv4Network, IPv6Address
-from netifaces import ifaddresses, AF_INET, AF_INET6
-from subprocess import Popen, PIPE, STDOUT
-from time import sleep
-from os.path import isfile
-from tabulate import tabulate
-from hurry.filesize import size,alternative
-from datetime import timedelta
-
-dhclient_base = r'/var/lib/dhcp/dhclient_'
-dhcp_cfg = """
-# generated by ifconfig.py
-option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
-timeout 60;
-retry 300;
-
-interface "{{ intf }}" {
- send host-name "{{ hostname }}";
- {% if client_id -%}
- send dhcp-client-identifier "{{ client_id }}";
- {% endif -%}
- {% if vendor_class_id -%}
- send vendor-class-identifier "{{ vendor_class_id }}";
- {% endif -%}
- request subnet-mask, broadcast-address, routers, domain-name-servers,
- rfc3442-classless-static-routes, domain-name, interface-mtu;
- require subnet-mask;
-}
-
-"""
-
-dhcpv6_cfg = """
-# generated by ifconfig.py
-interface "{{ intf }}" {
- request routers, domain-name-servers, domain-name;
-}
-
-"""
-
-class Interface:
- def __init__(self, ifname, type=None):
- """
- This is the base interface class which supports basic IP/MAC address
- operations as well as DHCP(v6). Other interface which represent e.g.
- and ethernet bridge are implemented as derived classes adding all
- additional functionality.
-
- DEBUG:
- This class has embedded debugging (print) which can be enabled by
- creating the following file:
- vyos@vyos# touch /tmp/vyos.ifconfig.debug
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> i = Interface('eth0')
- """
- self._ifname = str(ifname)
-
- if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type:
- raise Exception('interface "{}" not found'.format(self._ifname))
-
- if not os.path.exists('/sys/class/net/{}'.format(self._ifname)):
- cmd = 'ip link add dev {} type {}'.format(self._ifname, type)
- self._cmd(cmd)
-
- # per interface DHCP config files
- self._dhcp_cfg_file = dhclient_base + self._ifname + '.conf'
- self._dhcp_pid_file = dhclient_base + self._ifname + '.pid'
- self._dhcp_lease_file = dhclient_base + self._ifname + '.leases'
-
- # per interface DHCPv6 config files
- self._dhcpv6_cfg_file = dhclient_base + self._ifname + '.v6conf'
- self._dhcpv6_pid_file = dhclient_base + self._ifname + '.v6pid'
- self._dhcpv6_lease_file = dhclient_base + self._ifname + '.v6leases'
-
- # DHCP options
- self._dhcp_options = {
- 'intf' : self._ifname,
- 'hostname' : '',
- 'client_id' : '',
- 'vendor_class_id' : ''
- }
-
- # DHCPv6 options
- self._dhcpv6_options = {
- 'intf' : self._ifname,
- 'dhcpv6_prm_only' : False,
- 'dhcpv6_temporary' : False
- }
-
- # list of assigned IP addresses
- self._addr = []
-
- def _debug_msg(self, msg):
- if os.path.isfile('/tmp/vyos.ifconfig.debug'):
- print('DEBUG/{:<6} {}'.format(self._ifname, msg))
-
- def _cmd(self, command):
- p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
- tmp = p.communicate()[0].strip()
- self._debug_msg("cmd '{}'".format(command))
- if tmp.decode():
- self._debug_msg("returned:\n{}".format(tmp.decode()))
-
- # do we need some error checking code here?
- return tmp.decode()
-
- def _read_sysfs(self, filename):
- """
- Provide a single primitive w/ error checking for reading from sysfs.
- """
- value = None
- with open(filename, 'r') as f:
- value = f.read().rstrip('\n')
-
- self._debug_msg("read '{}' < '{}'".format(value, filename))
- return value
-
- def _write_sysfs(self, filename, value):
- """
- Provide a single primitive w/ error checking for writing to sysfs.
- """
- self._debug_msg("write '{}' > '{}'".format(value, filename))
- with open(filename, 'w') as f:
- f.write(str(value))
-
- return None
-
- def remove(self):
- """
- Remove interface from operating system. Removing the interface
- deconfigures all assigned IP addresses and clear possible DHCP(v6)
- client processes.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> i = Interface('eth0')
- >>> i.remove()
- """
- # stop DHCP(v6) if running
- self._del_dhcp()
- self._del_dhcpv6()
-
- # remove all assigned IP addresses from interface - this is a bit redundant
- # as the kernel will remove all addresses on interface deletion, but we
- # can not delete ALL interfaces, see below
- for addr in self.get_addr():
- self.del_addr(addr)
-
- # Ethernet interfaces can not be removed
- if type(self) == type(EthernetIf(self._ifname)):
- return
-
- # NOTE (Improvement):
- # after interface removal no other commands should be allowed
- # to be called and instead should raise an Exception:
- cmd = 'ip link del dev {}'.format(self._ifname)
- return self._cmd(cmd)
-
- def get_mtu(self):
- """
- Get/set interface mtu in bytes.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').get_mtu()
- '1500'
- """
- return self._read_sysfs('/sys/class/net/{}/mtu'
- .format(self._ifname))
-
- def set_mtu(self, mtu):
- """
- Get/set interface mtu in bytes.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').set_mtu(1400)
- >>> Interface('eth0').get_mtu()
- '1400'
- """
- if mtu < 68 or mtu > 9000:
- raise ValueError('Invalid MTU size: "{}"'.format(mru))
-
- return self._write_sysfs('/sys/class/net/{}/mtu'
- .format(self._ifname), mtu)
-
- def set_mac(self, mac):
- """
- Set interface MAC (Media Access Contrl) address to given value.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').set_mac('00:50:ab:cd:ef:01')
- """
- # on interface removal (ethernet) an empty string is passed - ignore it
- if not mac:
- return None
-
- # a mac address consits out of 6 octets
- octets = len(mac.split(':'))
- if octets != 6:
- raise ValueError('wrong number of MAC octets: {} '.format(octets))
-
- # validate against the first mac address byte if it's a multicast
- # address
- if int(mac.split(':')[0], 16) & 1:
- raise ValueError('{} is a multicast MAC address'.format(mac))
-
- # overall mac address is not allowed to be 00:00:00:00:00:00
- if sum(int(i, 16) for i in mac.split(':')) == 0:
- raise ValueError('00:00:00:00:00:00 is not a valid MAC address')
-
- # check for VRRP mac address
- if mac.split(':')[0] == '0' and addr.split(':')[1] == '0' and mac.split(':')[2] == '94' and mac.split(':')[3] == '0' and mac.split(':')[4] == '1':
- raise ValueError('{} is a VRRP MAC address'.format(mac))
-
- # Assemble command executed on system. Unfortunately there is no way
- # of altering the MAC address via sysfs
- cmd = 'ip link set dev {} address {}'.format(self._ifname, mac)
- return self._cmd(cmd)
-
-
- def set_arp_cache_tmo(self, tmo):
- """
- Set ARP cache timeout value in seconds. Internal Kernel representation
- is in milliseconds.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').set_arp_cache_tmo(40)
- """
- return self._write_sysfs('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms'
- .format(self._ifname), (int(tmo) * 1000))
-
- def set_arp_filter(self, arp_filter):
- """
- Filter ARP requests
-
- 1 - Allows you to have multiple network interfaces on the same
- subnet, and have the ARPs for each interface be answered
- based on whether or not the kernel would route a packet from
- the ARP'd IP out that interface (therefore you must use source
- based routing for this to work). In other words it allows control
- of which cards (usually 1) will respond to an arp request.
-
- 0 - (default) The kernel can respond to arp requests with addresses
- from other interfaces. This may seem wrong but it usually makes
- sense, because it increases the chance of successful communication.
- IP addresses are owned by the complete host on Linux, not by
- particular interfaces. Only for more complex setups like load-
- balancing, does this behaviour cause problems.
- """
- if int(arp_filter) >= 0 and int(arp_filter) <= 1:
- return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_filter'
- .format(self._ifname), arp_filter)
- else:
- raise ValueError("Value out of range")
-
- def set_arp_accept(self, arp_accept):
- """
- Define behavior for gratuitous ARP frames who's IP is not
- already present in the ARP table:
- 0 - don't create new entries in the ARP table
- 1 - create new entries in the ARP table
-
- Both replies and requests type gratuitous arp will trigger the
- ARP table to be updated, if this setting is on.
-
- If the ARP table already contains the IP address of the
- gratuitous arp frame, the arp table will be updated regardless
- if this setting is on or off.
- """
- if int(arp_accept) >= 0 and int(arp_accept) <= 1:
- return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_accept'
- .format(self._ifname), arp_accept)
- else:
- raise ValueError("Value out of range")
-
- def set_arp_announce(self, arp_announce):
- """
- Define different restriction levels for announcing the local
- source IP address from IP packets in ARP requests sent on
- interface:
- 0 - (default) Use any local address, configured on any interface
- 1 - Try to avoid local addresses that are not in the target's
- subnet for this interface. This mode is useful when target
- hosts reachable via this interface require the source IP
- address in ARP requests to be part of their logical network
- configured on the receiving interface. When we generate the
- request we will check all our subnets that include the
- target IP and will preserve the source address if it is from
- such subnet.
-
- Increasing the restriction level gives more chance for
- receiving answer from the resolved target while decreasing
- the level announces more valid sender's information.
- """
- if int(arp_announce) >= 0 and int(arp_announce) <= 1:
- return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_announce'
- .format(self._ifname), arp_announce)
- else:
- raise ValueError("Value out of range")
-
- def set_arp_ignore(self, arp_ignore):
- """
- Define different modes for sending replies in response to received ARP
- requests that resolve local target IP addresses:
-
- 0 - (default): reply for any local target IP address, configured
- on any interface
- 1 - reply only if the target IP address is local address
- configured on the incoming interface
- """
- if int(arp_ignore) >= 0 and int(arp_ignore) <= 1:
- return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_ignore'
- .format(self._ifname), arp_ignore)
- else:
- raise ValueError("Value out of range")
-
- def set_link_detect(self, link_filter):
- """
- Configure kernel response in packets received on interfaces that are 'down'
-
- 0 - Allow packets to be received for the address on this interface
- even if interface is disabled or no carrier.
-
- 1 - Ignore packets received if interface associated with the incoming
- address is down.
-
- 2 - Ignore packets received if interface associated with the incoming
- address is down or has no carrier.
-
- Default value is 0. Note that some distributions enable it in startup
- scripts.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').set_link_detect(1)
- """
- if int(link_filter) >= 0 and int(link_filter) <= 2:
- return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter'
- .format(self._ifname), link_filter)
- else:
- raise ValueError("Value out of range")
-
- def set_alias(self, ifalias=None):
- """
- Set interface alias name used by e.g. SNMP
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').set_alias('VyOS upstream interface')
-
- to clear alias e.g. delete it use:
-
- >>> Interface('eth0').set_ifalias('')
- """
- if not ifalias:
- # clear interface alias
- ifalias = '\0'
-
- self._write_sysfs('/sys/class/net/{}/ifalias'
- .format(self._ifname), ifalias)
-
- def get_state(self):
- """
- Enable (up) / Disable (down) an interface
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').get_state()
- 'up'
- """
- cmd = 'ip -json link show dev {}'.format(self._ifname)
- tmp = self._cmd(cmd)
- out = json.loads(tmp)
- return out[0]['operstate'].lower()
-
- def set_state(self, state):
- """
- Enable (up) / Disable (down) an interface
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').set_state('down')
- >>> Interface('eth0').get_state()
- 'down'
- """
- if state not in ['up', 'down']:
- raise ValueError('state must be "up" or "down"')
-
- # Assemble command executed on system. Unfortunately there is no way
- # to up/down an interface via sysfs
- cmd = 'ip link set dev {} {}'.format(self._ifname, state)
- return self._cmd(cmd)
-
- def set_proxy_arp(self, enable):
- """
- Set per interface proxy ARP configuration
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').set_proxy_arp(1)
- """
- if int(enable) >= 0 and int(enable) <= 1:
- return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp'
- .format(self._ifname), enable)
- else:
- raise ValueError("Value out of range")
-
- def set_proxy_arp_pvlan(self, enable):
- """
- Private VLAN proxy arp.
- Basically allow proxy arp replies back to the same interface
- (from which the ARP request/solicitation was received).
-
- This is done to support (ethernet) switch features, like RFC
- 3069, where the individual ports are NOT allowed to
- communicate with each other, but they are allowed to talk to
- the upstream router. As described in RFC 3069, it is possible
- to allow these hosts to communicate through the upstream
- router by proxy_arp'ing. Don't need to be used together with
- proxy_arp.
-
- This technology is known by different names:
- In RFC 3069 it is called VLAN Aggregation.
- Cisco and Allied Telesyn call it Private VLAN.
- Hewlett-Packard call it Source-Port filtering or port-isolation.
- Ericsson call it MAC-Forced Forwarding (RFC Draft).
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').set_proxy_arp_pvlan(1)
- """
- if int(enable) >= 0 and int(enable) <= 1:
- return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp_pvlan'
- .format(self._ifname), enable)
- else:
- raise ValueError("Value out of range")
-
- def get_addr(self):
- """
- Retrieve assigned IPv4 and IPv6 addresses from given interface.
- This is done using the netifaces and ipaddress python modules.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').get_addrs()
- ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64']
- """
-
- ipv4 = []
- ipv6 = []
-
- if AF_INET in ifaddresses(self._ifname).keys():
- for v4_addr in ifaddresses(self._ifname)[AF_INET]:
- # we need to manually assemble a list of IPv4 address/prefix
- prefix = '/' + \
- str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen)
- ipv4.append(v4_addr['addr'] + prefix)
-
- if AF_INET6 in ifaddresses(self._ifname).keys():
- for v6_addr in ifaddresses(self._ifname)[AF_INET6]:
- # Note that currently expanded netmasks are not supported. That means
- # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not.
- # see https://docs.python.org/3/library/ipaddress.html
- bits = bin(
- int(v6_addr['netmask'].replace(':', ''), 16)).count('1')
- prefix = '/' + str(bits)
-
- # we alsoneed to remove the interface suffix on link local
- # addresses
- v6_addr['addr'] = v6_addr['addr'].split('%')[0]
- ipv6.append(v6_addr['addr'] + prefix)
-
- return ipv4 + ipv6
-
- def add_addr(self, addr):
- """
- Add IP(v6) address to interface. Address is only added if it is not
- already assigned to that interface.
-
- addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
- IPv4: add IPv4 address to interface
- IPv6: add IPv6 address to interface
- dhcp: start dhclient (IPv4) on interface
- dhcpv6: start dhclient (IPv6) on interface
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> j = Interface('eth0')
- >>> j.add_addr('192.0.2.1/24')
- >>> j.add_addr('2001:db8::ffff/64')
- >>> j.get_addr()
- ['192.0.2.1/24', '2001:db8::ffff/64']
- """
-
- # cache new IP address which is assigned to interface
- self._addr.append(addr)
-
- # we can not have both DHCP and static IPv4 addresses assigned to an interface
- if 'dhcp' in self._addr:
- for addr in self._addr:
- # do not change below 'if' ordering esle you will get an exception as:
- # ValueError: 'dhcp' does not appear to be an IPv4 or IPv6 address
- if addr != 'dhcp' and is_ipv4(addr):
- raise ConfigError("Can't configure both static IPv4 and DHCP address on the same interface")
-
- if addr == 'dhcp':
- self._set_dhcp()
- elif addr == 'dhcpv6':
- self._set_dhcpv6()
- else:
- if not is_intf_addr_assigned(self._ifname, addr):
- cmd = 'ip addr add "{}" dev "{}"'.format(addr, self._ifname)
- return self._cmd(cmd)
-
- def del_addr(self, addr):
- """
- Delete IP(v6) address to interface. Address is only added if it is
- assigned to that interface.
-
- addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
- IPv4: delete IPv4 address from interface
- IPv6: delete IPv6 address from interface
- dhcp: stop dhclient (IPv4) on interface
- dhcpv6: stop dhclient (IPv6) on interface
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> j = Interface('eth0')
- >>> j.add_addr('2001:db8::ffff/64')
- >>> j.add_addr('192.0.2.1/24')
- >>> j.get_addr()
- ['192.0.2.1/24', '2001:db8::ffff/64']
- >>> j.del_addr('192.0.2.1/24')
- >>> j.get_addr()
- ['2001:db8::ffff/64']
- """
- if addr == 'dhcp':
- self._del_dhcp()
- elif addr == 'dhcpv6':
- self._del_dhcpv6()
- else:
- if is_intf_addr_assigned(self._ifname, addr):
- cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname)
- return self._cmd(cmd)
-
-
- def get_dhcp_options(self):
- """
- Return dictionary with supported DHCP options.
-
- Dictionary should be altered and send back via set_dhcp_options()
- so those options are applied when DHCP is run.
- """
- return self._dhcp_options
-
- def set_dhcp_options(self, options):
- """
- Store new DHCP options used by next run of DHCP client.
- """
- self._dhcp_options = options
-
- def get_dhcpv6_options(self):
- """
- Return dictionary with supported DHCPv6 options.
-
- Dictionary should be altered and send back via set_dhcp_options()
- so those options are applied when DHCP is run.
- """
- return self._dhcpv6_options
-
- def set_dhcpv6_options(self, options):
- """
- Store new DHCP options used by next run of DHCP client.
- """
- self._dhcpv6_options = options
-
- # replace dhcpv4/v6 with systemd.networkd?
- def _set_dhcp(self):
- """
- Configure interface as DHCP client. The dhclient binary is automatically
- started in background!
-
- Example:
-
- >>> from vyos.ifconfig import Interface
- >>> j = Interface('eth0')
- >>> j.set_dhcp()
- """
-
- dhcp = self.get_dhcp_options()
- if not dhcp['hostname']:
- # read configured system hostname.
- # maybe change to vyos hostd client ???
- with open('/etc/hostname', 'r') as f:
- dhcp['hostname'] = f.read().rstrip('\n')
-
- # render DHCP configuration
- tmpl = jinja2.Template(dhcp_cfg)
- dhcp_text = tmpl.render(dhcp)
- with open(self._dhcp_cfg_file, 'w') as f:
- f.write(dhcp_text)
-
- cmd = 'start-stop-daemon --start --quiet --pidfile ' + \
- self._dhcp_pid_file
- cmd += ' --exec /sbin/dhclient --'
- # now pass arguments to dhclient binary
- cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format(
- self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname)
- return self._cmd(cmd)
-
-
- def _del_dhcp(self):
- """
- De-configure interface as DHCP clinet. All auto generated files like
- pid, config and lease will be removed.
-
- Example:
-
- >>> from vyos.ifconfig import Interface
- >>> j = Interface('eth0')
- >>> j.del_dhcp()
- """
- pid = 0
- if os.path.isfile(self._dhcp_pid_file):
- with open(self._dhcp_pid_file, 'r') as f:
- pid = int(f.read())
- else:
- self._debug_msg('No DHCP client PID found')
- return None
-
- # stop dhclient, we need to call dhclient and tell it should release the
- # aquired IP address. tcpdump tells me:
- # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000)
- # Client-IP 172.16.35.103
- # Client-Ethernet-Address 00:50:56:9d:11:df
- # Vendor-rfc1048 Extensions
- # Magic Cookie 0x63825363
- # DHCP-Message Option 53, length 1: Release
- # Server-ID Option 54, length 4: 172.16.35.254
- # Hostname Option 12, length 10: "vyos"
- #
- cmd = '/sbin/dhclient -cf {} -pf {} -lf {} -r {}'.format(
- self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname)
- self._cmd(cmd)
-
- # cleanup old config file
- if os.path.isfile(self._dhcp_cfg_file):
- os.remove(self._dhcp_cfg_file)
-
- # cleanup old pid file
- if os.path.isfile(self._dhcp_pid_file):
- os.remove(self._dhcp_pid_file)
-
- # cleanup old lease file
- if os.path.isfile(self._dhcp_lease_file):
- os.remove(self._dhcp_lease_file)
-
-
- def _set_dhcpv6(self):
- """
- Configure interface as DHCPv6 client. The dhclient binary is automatically
- started in background!
-
- Example:
-
- >>> from vyos.ifconfig import Interface
- >>> j = Interface('eth0')
- >>> j.set_dhcpv6()
- """
- dhcpv6 = self.get_dhcpv6_options()
-
- # better save then sorry .. should be checked in interface script
- # but if you missed it we are safe!
- if dhcpv6['dhcpv6_prm_only'] and dhcpv6['dhcpv6_temporary']:
- raise Exception('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- # render DHCP configuration
- tmpl = jinja2.Template(dhcpv6_cfg)
- dhcpv6_text = tmpl.render(dhcpv6)
- with open(self._dhcpv6_cfg_file, 'w') as f:
- f.write(dhcpv6_text)
-
- # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715
- #
- # wee need to wait for IPv6 DAD to finish once and interface is added
- # this suxx :-(
- sleep(5)
-
- # no longer accept router announcements on this interface
- self._write_sysfs('/proc/sys/net/ipv6/conf/{}/accept_ra'
- .format(self._ifname), 0)
-
- # assemble command-line to start DHCPv6 client (dhclient)
- cmd = 'start-stop-daemon --start --quiet --pidfile ' + \
- self._dhcpv6_pid_file
- cmd += ' --exec /sbin/dhclient --'
- # now pass arguments to dhclient binary
- cmd += ' -6 -nw -cf {} -pf {} -lf {}'.format(
- self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file)
-
- # add optional arguments
- if dhcpv6['dhcpv6_prm_only']:
- cmd += ' -S'
- if dhcpv6['dhcpv6_temporary']:
- cmd += ' -T'
-
- cmd += ' {}'.format(self._ifname)
- return self._cmd(cmd)
-
-
- def _del_dhcpv6(self):
- """
- De-configure interface as DHCPv6 clinet. All auto generated files like
- pid, config and lease will be removed.
-
- Example:
-
- >>> from vyos.ifconfig import Interface
- >>> j = Interface('eth0')
- >>> j.del_dhcpv6()
- """
- pid = 0
- if os.path.isfile(self._dhcpv6_pid_file):
- with open(self._dhcpv6_pid_file, 'r') as f:
- pid = int(f.read())
- else:
- self._debug_msg('No DHCPv6 client PID found')
- return None
-
- # stop dhclient
- cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format(self._dhcpv6_pid_file)
- self._cmd(cmd)
-
- # accept router announcements on this interface
- self._write_sysfs('/proc/sys/net/ipv6/conf/{}/accept_ra'
- .format(self._ifname), 1)
-
- # cleanup old config file
- if os.path.isfile(self._dhcpv6_cfg_file):
- os.remove(self._dhcpv6_cfg_file)
-
- # cleanup old pid file
- if os.path.isfile(self._dhcpv6_pid_file):
- os.remove(self._dhcpv6_pid_file)
-
- # cleanup old lease file
- if os.path.isfile(self._dhcpv6_lease_file):
- os.remove(self._dhcpv6_lease_file)
-
- def op_show_interface_stats(self):
- stats = self.get_interface_stats()
- rx = [['bytes','packets','errors','dropped','overrun','mcast'],[stats['rx_bytes'],stats['rx_packets'],stats['rx_errors'],stats['rx_dropped'],stats['rx_over_errors'],stats['multicast']]]
- tx = [['bytes','packets','errors','dropped','carrier','collisions'],[stats['tx_bytes'],stats['tx_packets'],stats['tx_errors'],stats['tx_dropped'],stats['tx_carrier_errors'],stats['collisions']]]
- output = "RX: \n"
- output += tabulate(rx,headers="firstrow",numalign="right",tablefmt="plain")
- output += "\n\nTX: \n"
- output += tabulate(tx,headers="firstrow",numalign="right",tablefmt="plain")
- print(' '.join(('\n'+output.lstrip()).splitlines(True)))
-
- def get_interface_stats(self):
- interface_stats = dict()
- devices = [f for f in glob.glob("/sys/class/net/**/statistics")]
- for dev_path in devices:
- metrics = [f for f in glob.glob(dev_path +"/**")]
- dev = re.findall(r"/sys/class/net/(.*)/statistics",dev_path)[0]
- dev_dict = dict()
- for metric_path in metrics:
- metric = metric_path.replace(dev_path+"/","")
- if isfile(metric_path):
- data = open(metric_path, 'r').read()[:-1]
- dev_dict[metric] = int(data)
- interface_stats[dev] = dev_dict
-
- return interface_stats[self._ifname]
-
-class LoopbackIf(Interface):
-
- """
- The loopback device is a special, virtual network interface that your router
- uses to communicate with itself.
- """
-
- def __init__(self, ifname):
- super().__init__(ifname, type='loopback')
-
- def remove(self):
- """
- Loopback interface can not be deleted from operating system. We can
- only remove all assigned IP addresses.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> i = LoopbackIf('lo').remove()
- """
- # remove all assigned IP addresses from interface
- for addr in self.get_addr():
- if addr in ["127.0.0.1/8", "::1/128"]:
- # Do not allow deletion of the default loopback addresses as
- # this will cause weird system behavior like snmp/ssh no longer
- # operating as expected, see https://phabricator.vyos.net/T2034.
- continue
-
- self.del_addr(addr)
-
-class DummyIf(Interface):
-
- """
- A dummy interface is entirely virtual like, for example, the loopback
- interface. The purpose of a dummy interface is to provide a device to route
- packets through without actually transmitting them.
- """
-
- def __init__(self, ifname):
- super().__init__(ifname, type='dummy')
-
-
-class STPIf(Interface):
- """
- A spanning-tree capable interface. This applies only to bridge port member
- interfaces!
- """
- def __init__(self, ifname):
- super().__init__(ifname)
-
- def set_path_cost(self, cost):
- """
- Set interface path cost, only relevant for STP enabled interfaces
-
- Example:
-
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').set_path_cost(4)
- """
- if not os.path.isfile('/sys/class/net/{}/brport/path_cost'
- .format(self._ifname)):
- raise TypeError('{} is not a bridge port member'.format(self._ifname))
-
- return self._write_sysfs('/sys/class/net/{}/brport/path_cost'
- .format(self._ifname), cost)
-
- def set_path_priority(self, priority):
- """
- Set interface path priority, only relevant for STP enabled interfaces
-
- Example:
-
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').set_path_priority(4)
- """
- if not os.path.isfile('/sys/class/net/{}/brport/priority'
- .format(self._ifname)):
- raise TypeError('{} is not a bridge port member'.format(self._ifname))
-
- return self._write_sysfs('/sys/class/net/{}/brport/priority'
- .format(self._ifname), priority)
-
-
-class BridgeIf(Interface):
-
- """
- A bridge is a way to connect two Ethernet segments together in a protocol
- independent way. Packets are forwarded based on Ethernet address, rather
- than IP address (like a router). Since forwarding is done at Layer 2, all
- protocols can go transparently through a bridge.
-
- The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard.
- """
-
- def __init__(self, ifname):
- super().__init__(ifname, type='bridge')
-
- def set_ageing_time(self, time):
- """
- Set bridge interface MAC address aging time in seconds. Internal kernel
- representation is in centiseconds. Kernel default is 300 seconds.
-
- Example:
- >>> from vyos.ifconfig import BridgeIf
- >>> BridgeIf('br0').ageing_time(2)
- """
- time = int(time) * 100
- return self._write_sysfs('/sys/class/net/{}/bridge/ageing_time'
- .format(self._ifname), time)
-
- def set_forward_delay(self, time):
- """
- Set bridge forwarding delay in seconds. Internal Kernel representation
- is in centiseconds.
-
- Example:
- >>> from vyos.ifconfig import BridgeIf
- >>> BridgeIf('br0').forward_delay(15)
- """
- return self._write_sysfs('/sys/class/net/{}/bridge/forward_delay'
- .format(self._ifname), (int(time) * 100))
-
- def set_hello_time(self, time):
- """
- Set bridge hello time in seconds. Internal Kernel representation
- is in centiseconds.
-
- Example:
- >>> from vyos.ifconfig import BridgeIf
- >>> BridgeIf('br0').set_hello_time(2)
- """
- return self._write_sysfs('/sys/class/net/{}/bridge/hello_time'
- .format(self._ifname), (int(time) * 100))
-
- def set_max_age(self, time):
- """
- Set bridge max message age in seconds. Internal Kernel representation
- is in centiseconds.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> BridgeIf('br0').set_max_age(30)
- """
- return self._write_sysfs('/sys/class/net/{}/bridge/max_age'
- .format(self._ifname), (int(time) * 100))
-
- def set_priority(self, priority):
- """
- Set bridge max aging time in seconds.
-
- Example:
- >>> from vyos.ifconfig import BridgeIf
- >>> BridgeIf('br0').set_priority(8192)
- """
- return self._write_sysfs('/sys/class/net/{}/bridge/priority'
- .format(self._ifname), priority)
-
- def set_stp(self, state):
- """
- Set bridge STP (Spanning Tree) state. 0 -> STP disabled, 1 -> STP enabled
-
- Example:
- >>> from vyos.ifconfig import BridgeIf
- >>> BridgeIf('br0').set_stp(1)
- """
-
- if int(state) >= 0 and int(state) <= 1:
- return self._write_sysfs('/sys/class/net/{}/bridge/stp_state'
- .format(self._ifname), state)
- else:
- raise ValueError("Value out of range")
-
-
- def set_multicast_querier(self, enable):
- """
- Sets whether the bridge actively runs a multicast querier or not. When a
- bridge receives a 'multicast host membership' query from another network
- host, that host is tracked based on the time that the query was received
- plus the multicast query interval time.
-
- Use enable=1 to enable or enable=0 to disable
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> BridgeIf('br0').set_multicast_querier(1)
- """
- if int(enable) >= 0 and int(enable) <= 1:
- return self._write_sysfs('/sys/class/net/{}/bridge/multicast_querier'
- .format(self._ifname), enable)
- else:
- raise ValueError("Value out of range")
-
-
- def add_port(self, interface):
- """
- Add physical interface to bridge (member port)
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> BridgeIf('br0').add_port('eth0')
- >>> BridgeIf('br0').add_port('eth1')
- """
- cmd = 'ip link set dev {} master {}'.format(interface, self._ifname)
- return self._cmd(cmd)
-
- def del_port(self, interface):
- """
- Remove member port from bridge instance.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> BridgeIf('br0').del_port('eth1')
- """
- cmd = 'ip link set dev {} nomaster'.format(interface)
- return self._cmd(cmd)
-
-class VLANIf(Interface):
- """
- This class handels the creation and removal of a VLAN interface. It serves
- as base class for BondIf and EthernetIf.
- """
- def __init__(self, ifname, type=None):
- super().__init__(ifname, type)
-
- def remove(self):
- """
- Remove interface from operating system. Removing the interface
- deconfigures all assigned IP addresses and clear possible DHCP(v6)
- client processes.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> i = Interface('eth0')
- >>> i.remove()
- """
- # Do we have sub interfaces (VLANs)? We apply a regex matching
- # subinterfaces (indicated by a .) of a parent interface.
- #
- # As interfaces need to be deleted "in order" starting from Q-in-Q
- # we delete them first.
- vlan_ifs = [f for f in os.listdir(r'/sys/class/net') \
- if re.match(self._ifname + r'(?:\.\d+)(?:\.\d+)', f)]
-
- for vlan in vlan_ifs:
- Interface(vlan).remove()
-
- # After deleting all Q-in-Q interfaces delete other VLAN interfaces
- # which probably acted as parent to Q-in-Q or have been regular 802.1q
- # interface.
- vlan_ifs = [f for f in os.listdir(r'/sys/class/net') \
- if re.match(self._ifname + r'(?:\.\d+)', f)]
-
- for vlan in vlan_ifs:
- Interface(vlan).remove()
-
- # All subinterfaces are now removed, continue on the physical interface
- super().remove()
-
-
- def add_vlan(self, vlan_id, ethertype='', ingress_qos='', egress_qos=''):
- """
- A virtual LAN (VLAN) is any broadcast domain that is partitioned and
- isolated in a computer network at the data link layer (OSI layer 2).
- Use this function to create a new VLAN interface on a given physical
- interface.
-
- This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto
- parameter is used to indicate VLAN type.
-
- A new object of type VLANIf is returned once the interface has been
- created.
-
- @param ethertype: If specified, create 802.1ad or 802.1q Q-in-Q VLAN
- interface
- @param ingress_qos: Defines a mapping of VLAN header prio field to the
- Linux internal packet priority on incoming frames.
- @param ingress_qos: Defines a mapping of Linux internal packet priority
- to VLAN header prio field but for outgoing frames.
-
- Example:
- >>> from vyos.ifconfig import VLANIf
- >>> i = VLANIf('eth0')
- >>> i.add_vlan(10)
- """
- vlan_ifname = self._ifname + '.' + str(vlan_id)
- if not os.path.exists('/sys/class/net/{}'.format(vlan_ifname)):
- self._vlan_id = int(vlan_id)
-
- if ethertype:
- self._ethertype = ethertype
- ethertype = 'proto {}'.format(ethertype)
-
- # Optional ingress QOS mapping
- opt_i = ''
- if ingress_qos:
- opt_i = 'ingress-qos-map ' + ingress_qos
- # Optional egress QOS mapping
- opt_e = ''
- if egress_qos:
- opt_e = 'egress-qos-map ' + egress_qos
-
- # create interface in the system
- cmd = 'ip link add link {intf} name {intf}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \
- .format(intf=self._ifname, vlan=self._vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i)
- self._cmd(cmd)
-
- # return new object mapping to the newly created interface
- # we can now work on this object for e.g. IP address setting
- # or interface description and so on
- return VLANIf(vlan_ifname)
-
-
- def del_vlan(self, vlan_id):
- """
- Remove VLAN interface from operating system. Removing the interface
- deconfigures all assigned IP addresses and clear possible DHCP(v6)
- client processes.
-
- Example:
- >>> from vyos.ifconfig import VLANIf
- >>> i = VLANIf('eth0.10')
- >>> i.del_vlan()
- """
- vlan_ifname = self._ifname + '.' + str(vlan_id)
- VLANIf(vlan_ifname).remove()
-
-
-class EthernetIf(VLANIf):
- """
- Abstraction of a Linux Ethernet Interface
- """
- def __init__(self, ifname):
- super().__init__(ifname)
-
- def get_driver_name(self):
- """
- Return the driver name used by NIC. Some NICs don't support all
- features e.g. changing link-speed, duplex
-
- Example:
- >>> from vyos.ifconfig import EthernetIf
- >>> i = EthernetIf('eth0')
- >>> i.get_driver_name()
- 'vmxnet3'
- """
- link = os.readlink('/sys/class/net/{}/device/driver/module'.format(self._ifname))
- return os.path.basename(link)
-
- def set_flow_control(self, enable):
- """
- Changes the pause parameters of the specified Ethernet device.
-
- @param enable: true -> enable pause frames, false -> disable pause frames
-
- Example:
- >>> from vyos.ifconfig import EthernetIf
- >>> i = EthernetIf('eth0')
- >>> i.set_flow_control(True)
- """
- if enable not in ['on', 'off']:
- raise ValueError("Value out of range")
-
- if self.get_driver_name() in ['vmxnet3', 'virtio_net']:
- self._debug_msg('{} driver does not support changing flow control settings!'
- .format(self.get_driver_name()))
- return
-
- # Get current flow control settings:
- cmd = '/sbin/ethtool --show-pause {0}'.format(self._ifname)
- tmp = self._cmd(cmd)
-
- # The above command returns - with tabs:
- #
- # Pause parameters for eth0:
- # Autonegotiate: on
- # RX: off
- # TX: off
- if re.search("Autonegotiate:\ton", tmp):
- if enable == "on":
- # flowcontrol is already enabled - no need to re-enable it again
- # this will prevent the interface from flapping as applying the
- # flow-control settings will take the interface down and bring
- # it back up every time.
- return
-
- # Assemble command executed on system. Unfortunately there is no way
- # to change this setting via sysfs
- cmd = '/sbin/ethtool --pause {0} autoneg {1} tx {1} rx {1}'.format(
- self._ifname, enable)
- try:
- # An exception will be thrown if the settings are not changed
- return self._cmd(cmd)
- except CalledProcessError:
- pass
-
-
- def set_speed_duplex(self, speed, duplex):
- """
- Set link speed in Mbit/s and duplex.
-
- @speed can be any link speed in MBit/s, e.g. 10, 100, 1000 auto
- @duplex can be half, full, auto
-
- Example:
- >>> from vyos.ifconfig import EthernetIf
- >>> i = EthernetIf('eth0')
- >>> i.set_speed_duplex('auto', 'auto')
- """
-
- if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000', '25000', '40000', '50000', '100000', '400000']:
- raise ValueError("Value out of range (speed)")
-
- if duplex not in ['auto', 'full', 'half']:
- raise ValueError("Value out of range (duplex)")
-
- if self.get_driver_name() in ['vmxnet3', 'virtio_net']:
- self._debug_msg('{} driver does not support changing speed/duplex settings!'
- .format(self.get_driver_name()))
- return
-
- # Get current speed and duplex settings:
- cmd = '/sbin/ethtool {0}'.format(self._ifname)
- tmp = self._cmd(cmd)
-
- if re.search("\tAuto-negotiation: on", tmp):
- if speed == 'auto' and duplex == 'auto':
- # bail out early as nothing is to change
- return
- else:
- # read in current speed and duplex settings
- cur_speed = 0
- cur_duplex = ''
- for line in tmp.splitlines():
- if line.lstrip().startswith("Speed:"):
- non_decimal = re.compile(r'[^\d.]+')
- cur_speed = non_decimal.sub('', line)
- continue
-
- if line.lstrip().startswith("Duplex:"):
- cur_duplex = line.split()[-1].lower()
- break
-
- if (cur_speed == speed) and (cur_duplex == duplex):
- # bail out early as nothing is to change
- return
-
- cmd = '/sbin/ethtool -s {}'.format(self._ifname)
- if speed == 'auto' or duplex == 'auto':
- cmd += ' autoneg on'
- else:
- cmd += ' speed {} duplex {} autoneg off'.format(speed, duplex)
-
- return self._cmd(cmd)
-
-
- def set_gro(self, state):
- """
- Example:
- >>> from vyos.ifconfig import EthernetIf
- >>> i = EthernetIf('eth0')
- >>> i.set_gro('on')
- """
- if state not in ['on', 'off']:
- raise ValueError('state must be "on" or "off"')
-
- cmd = '/sbin/ethtool -K {} gro {}'.format(self._ifname, state)
- return self._cmd(cmd)
-
-
- def set_gso(self, state):
- """
- Example:
- >>> from vyos.ifconfig import EthernetIf
- >>> i = EthernetIf('eth0')
- >>> i.set_gso('on')
- """
- if state not in ['on', 'off']:
- raise ValueError('state must be "on" or "off"')
-
- cmd = '/sbin/ethtool -K {} gso {}'.format(self._ifname, state)
- return self._cmd(cmd)
-
-
- def set_sg(self, state):
- """
- Example:
- >>> from vyos.ifconfig import EthernetIf
- >>> i = EthernetIf('eth0')
- >>> i.set_sg('on')
- """
- if state not in ['on', 'off']:
- raise ValueError('state must be "on" or "off"')
-
- cmd = '/sbin/ethtool -K {} sg {}'.format(self._ifname, state)
- return self._cmd(cmd)
-
-
- def set_tso(self, state):
- """
- Example:
- >>> from vyos.ifconfig import EthernetIf
- >>> i = EthernetIf('eth0')
- >>> i.set_tso('on')
- """
- if state not in ['on', 'off']:
- raise ValueError('state must be "on" or "off"')
-
- cmd = '/sbin/ethtool -K {} tso {}'.format(self._ifname, state)
- return self._cmd(cmd)
-
-
- def set_ufo(self, state):
- """
- Example:
- >>> from vyos.ifconfig import EthernetIf
- >>> i = EthernetIf('eth0')
- >>> i.set_udp_offload('on')
- """
- if state not in ['on', 'off']:
- raise ValueError('state must be "on" or "off"')
-
- cmd = '/sbin/ethtool -K {} ufo {}'.format(self._ifname, state)
- return self._cmd(cmd)
-
-class MACVLANIf(VLANIf):
- """
- Abstraction of a Linux MACvlan interface
- """
- def __init__(self, ifname, config=''):
- self._ifname = ifname
-
- if not os.path.exists('/sys/class/net/{}'.format(self._ifname)) and config:
- cmd = 'ip link add {intf} link {link} type macvlan mode {mode}' \
- .format(intf=self._ifname, link=config['link'], mode=config['mode'])
- self._cmd(cmd)
-
- super().__init__(ifname, type='macvlan')
-
- @staticmethod
- def get_config():
- """
- VXLAN interfaces require a configuration when they are added using
- iproute2. This static method will provide the configuration dictionary
- used by this class.
-
- Example:
- >> dict = MACVLANIf().get_config()
- """
- config = {
- 'address': '',
- 'link': 0,
- 'mode': ''
- }
- return config
-
- def set_mode(self, mode):
- """
- """
-
- cmd = 'ip link set dev {} type macvlan mode {}'.format(self._ifname, mode)
- return self._cmd(cmd)
-
-
-class BondIf(VLANIf):
- """
- The Linux bonding driver provides a method for aggregating multiple network
- interfaces into a single logical "bonded" interface. The behavior of the
- bonded interfaces depends upon the mode; generally speaking, modes provide
- either hot standby or load balancing services. Additionally, link integrity
- monitoring may be performed.
- """
- def __init__(self, ifname):
- super().__init__(ifname, type='bond')
-
- def remove(self):
- """
- Remove interface from operating system. Removing the interface
- deconfigures all assigned IP addresses and clear possible DHCP(v6)
- client processes.
- Example:
- >>> from vyos.ifconfig import Interface
- >>> i = Interface('eth0')
- >>> i.remove()
- """
- # when a bond member gets deleted, all members are placed in A/D state
- # even when they are enabled inside CLI. This will make the config
- # and system look async.
- slave_list = []
- for s in self.get_slaves():
- slave = {
- 'ifname' : s,
- 'state': Interface(s).get_state()
- }
- slave_list.append(slave)
-
- # remove bond master which places members in disabled state
- super().remove()
-
- # replicate previous interface state before bond destruction back to
- # physical interface
- for slave in slave_list:
- i = Interface(slave['ifname'])
- i.set_state(slave['state'])
-
-
- def set_hash_policy(self, mode):
- """
- Selects the transmit hash policy to use for slave selection in
- balance-xor, 802.3ad, and tlb modes. Possible values are: layer2,
- layer2+3, layer3+4, encap2+3, encap3+4.
-
- The default value is layer2
-
- Example:
- >>> from vyos.ifconfig import BondIf
- >>> BondIf('bond0').set_hash_policy('layer2+3')
- """
- if not mode in ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']:
- raise ValueError("Value out of range")
- return self._write_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy'
- .format(self._ifname), mode)
-
- def set_arp_interval(self, interval):
- """
- Specifies the ARP link monitoring frequency in milliseconds.
-
- The ARP monitor works by periodically checking the slave devices
- to determine whether they have sent or received traffic recently
- (the precise criteria depends upon the bonding mode, and the
- state of the slave). Regular traffic is generated via ARP probes
- issued for the addresses specified by the arp_ip_target option.
-
- If ARP monitoring is used in an etherchannel compatible mode
- (modes 0 and 2), the switch should be configured in a mode that
- evenly distributes packets across all links. If the switch is
- configured to distribute the packets in an XOR fashion, all
- replies from the ARP targets will be received on the same link
- which could cause the other team members to fail.
-
- value of 0 disables ARP monitoring. The default value is 0.
-
- Example:
- >>> from vyos.ifconfig import BondIf
- >>> BondIf('bond0').set_arp_interval('100')
- """
- if int(interval) == 0:
- """
- Specifies the MII link monitoring frequency in milliseconds.
- This determines how often the link state of each slave is
- inspected for link failures. A value of zero disables MII
- link monitoring. A value of 100 is a good starting point.
- """
- return self._write_sysfs('/sys/class/net/{}/bonding/miimon'
- .format(self._ifname), interval)
- else:
- return self._write_sysfs('/sys/class/net/{}/bonding/arp_interval'
- .format(self._ifname), interval)
-
- def get_arp_ip_target(self):
- """
- Specifies the IP addresses to use as ARP monitoring peers when
- arp_interval is > 0. These are the targets of the ARP request sent to
- determine the health of the link to the targets. Specify these values
- in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
- a comma. At least one IP address must be given for ARP monitoring to
- function. The maximum number of targets that can be specified is 16.
-
- The default value is no IP addresses.
-
- Example:
- >>> from vyos.ifconfig import BondIf
- >>> BondIf('bond0').get_arp_ip_target()
- '192.0.2.1'
- """
- return self._read_sysfs('/sys/class/net/{}/bonding/arp_ip_target'
- .format(self._ifname))
-
- def set_arp_ip_target(self, target):
- """
- Specifies the IP addresses to use as ARP monitoring peers when
- arp_interval is > 0. These are the targets of the ARP request sent to
- determine the health of the link to the targets. Specify these values
- in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
- a comma. At least one IP address must be given for ARP monitoring to
- function. The maximum number of targets that can be specified is 16.
-
- The default value is no IP addresses.
-
- Example:
- >>> from vyos.ifconfig import BondIf
- >>> BondIf('bond0').set_arp_ip_target('192.0.2.1')
- >>> BondIf('bond0').get_arp_ip_target()
- '192.0.2.1'
- """
- return self._write_sysfs('/sys/class/net/{}/bonding/arp_ip_target'
- .format(self._ifname), target)
-
- def add_port(self, interface):
- """
- Enslave physical interface to bond.
-
- Example:
- >>> from vyos.ifconfig import BondIf
- >>> BondIf('bond0').add_port('eth0')
- >>> BondIf('bond0').add_port('eth1')
- """
- # An interface can only be added to a bond if it is in 'down' state. If
- # interface is in 'up' state, the following Kernel error will be thrown:
- # bond0: eth1 is up - this may be due to an out of date ifenslave.
- Interface(interface).set_state('down')
-
- return self._write_sysfs('/sys/class/net/{}/bonding/slaves'
- .format(self._ifname), '+' + interface)
-
- def del_port(self, interface):
- """
- Remove physical port from bond
-
- Example:
- >>> from vyos.ifconfig import BondIf
- >>> BondIf('bond0').del_port('eth1')
- """
- return self._write_sysfs('/sys/class/net/{}/bonding/slaves'
- .format(self._ifname), '-' + interface)
-
- def get_slaves(self):
- """
- Return a list with all configured slave interfaces on this bond.
-
- Example:
- >>> from vyos.ifconfig import BondIf
- >>> BondIf('bond0').get_slaves()
- ['eth1', 'eth2']
- """
- enslaved_ifs = []
- # retrieve real enslaved interfaces from OS kernel
- sysfs_bond = '/sys/class/net/{}'.format(self._ifname)
- if os.path.isdir(sysfs_bond):
- for directory in os.listdir(sysfs_bond):
- if 'lower_' in directory:
- enslaved_ifs.append(directory.replace('lower_',''))
-
- return enslaved_ifs
-
-
- def set_primary(self, interface):
- """
- A string (eth0, eth2, etc) specifying which slave is the primary
- device. The specified device will always be the active slave while it
- is available. Only when the primary is off-line will alternate devices
- be used. This is useful when one slave is preferred over another, e.g.,
- when one slave has higher throughput than another.
-
- The primary option is only valid for active-backup, balance-tlb and
- balance-alb mode.
-
- Example:
- >>> from vyos.ifconfig import BondIf
- >>> BondIf('bond0').set_primary('eth2')
- """
- if not interface:
- # reset primary interface
- interface = '\0'
-
- return self._write_sysfs('/sys/class/net/{}/bonding/primary'
- .format(self._ifname), interface)
-
- def set_mode(self, mode):
- """
- Specifies one of the bonding policies. The default is balance-rr
- (round robin).
-
- Possible values are: balance-rr, active-backup, balance-xor,
- broadcast, 802.3ad, balance-tlb, balance-alb
-
- NOTE: the bonding mode can not be changed when the bond itself has
- slaves
-
- Example:
- >>> from vyos.ifconfig import BondIf
- >>> BondIf('bond0').set_mode('802.3ad')
- """
- if not mode in [
- 'balance-rr', 'active-backup', 'balance-xor', 'broadcast',
- '802.3ad', 'balance-tlb', 'balance-alb']:
- raise ValueError("Value out of range")
-
- return self._write_sysfs('/sys/class/net/{}/bonding/mode'
- .format(self._ifname), mode)
-
-class WireGuardIf(Interface):
- """
- Wireguard interface class, contains a comnfig dictionary since
- wireguard VPN is being comnfigured via the wg command rather than
- writing the config into a file. Otherwise if a pre-shared key is used
- (symetric enryption key), it would we exposed within multiple files.
- Currently it's only within the config.boot if the config was saved.
-
- Example:
- >>> from vyos.ifconfig import WireGuardIf as wg_if
- >>> wg_intfc = wg_if("wg01")
- >>> print (wg_intfc.wg_config)
- {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0,
- 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'}
- >>> wg_intfc.wg_config['keepalive'] = 100
- >>> print (wg_intfc.wg_config)
- {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0,
- 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'}
- """
-
- def __init__(self, ifname):
- super().__init__(ifname, type='wireguard')
-
- self.config = {
- 'port': 0,
- 'private-key': None,
- 'pubkey': None,
- 'psk': '/dev/null',
- 'allowed-ips': [],
- 'fwmark': 0x00,
- 'endpoint': None,
- 'keepalive': 0
- }
-
- def update(self):
- if not self.config['private-key']:
- raise ValueError("private key required")
- else:
- # fmask permission check?
- pass
-
- cmd = "wg set {} ".format(self._ifname)
- cmd += "listen-port {} ".format(self.config['port'])
- cmd += "fwmark {} ".format(str(self.config['fwmark']))
- cmd += "private-key {} ".format(self.config['private-key'])
- cmd += "peer {} ".format(self.config['pubkey'])
- cmd += " preshared-key {} ".format(self.config['psk'])
- cmd += " allowed-ips "
- for aip in self.config['allowed-ips']:
- if aip != self.config['allowed-ips'][-1]:
- cmd += aip + ","
- else:
- cmd += aip
- if self.config['endpoint']:
- cmd += " endpoint {}".format(self.config['endpoint'])
- cmd += " persistent-keepalive {}".format(self.config['keepalive'])
-
- self._cmd(cmd)
-
- # remove psk since it isn't required anymore and is saved in the cli
- # config only !!
- if self.config['psk'] != '/dev/null':
- if os.path.exists(self.config['psk']):
- os.remove(self.config['psk'])
-
-
- def remove_peer(self, peerkey):
- """
- Remove a peer of an interface, peers are identified by their public key.
- Giving it a readable name is a vyos feature, to remove a peer the pubkey
- and the interface is needed, to remove the entry.
- """
- cmd = "wg set {0} peer {1} remove".format(
- self._ifname, str(peerkey))
- return self._cmd(cmd)
-
- def op_show_interface(self):
- wgdump = vyos.interfaces.wireguard_dump().get(self._ifname,None)
-
- c = Config()
- c.set_level(["interfaces","wireguard",self._ifname])
- description = c.return_effective_value(["description"])
- ips = c.return_effective_values(["address"])
-
- print ("interface: {}".format(self._ifname))
- if (description):
- print (" description: {}".format(description))
-
- if (ips):
- print (" address: {}".format(", ".join(ips)))
- print (" public key: {}".format(wgdump['public_key']))
- print (" private key: (hidden)")
- print (" listening port: {}".format(wgdump['listen_port']))
- print ()
-
- for peer in c.list_effective_nodes(["peer"]):
- if wgdump['peers']:
- pubkey = c.return_effective_value(["peer",peer,"pubkey"])
- if pubkey in wgdump['peers']:
- wgpeer = wgdump['peers'][pubkey]
-
- print (" peer: {}".format(peer))
- print (" public key: {}".format(pubkey))
-
- """ figure out if the tunnel is recently active or not """
- status = "inactive"
- if (wgpeer['latest_handshake'] is None):
- """ no handshake ever """
- status = "inactive"
- else:
- if int(wgpeer['latest_handshake']) > 0:
- delta = timedelta(seconds=int(time.time() - wgpeer['latest_handshake']))
- print (" latest handshake: {}".format(delta))
- if (time.time() - int(wgpeer['latest_handshake']) < (60*5)):
- """ Five minutes and the tunnel is still active """
- status = "active"
- else:
- """ it's been longer than 5 minutes """
- status = "inactive"
- elif int(wgpeer['latest_handshake']) == 0:
- """ no handshake ever """
- status = "inactive"
- print (" status: {}".format(status))
-
- if wgpeer['endpoint'] is not None:
- print (" endpoint: {}".format(wgpeer['endpoint']))
-
- if wgpeer['allowed_ips'] is not None:
- print (" allowed ips: {}".format(",".join(wgpeer['allowed_ips']).replace(",",", ")))
-
- if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0:
- rx_size =size(wgpeer['transfer_rx'],system=alternative)
- tx_size =size(wgpeer['transfer_tx'],system=alternative)
- print (" transfer: {} received, {} sent".format(rx_size,tx_size))
-
- if wgpeer['persistent_keepalive'] is not None:
- print (" persistent keepalive: every {} seconds".format(wgpeer['persistent_keepalive']))
- print()
- super().op_show_interface_stats()
-
-
-class VXLANIf(Interface):
- """
- The VXLAN protocol is a tunnelling protocol designed to solve the
- problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the
- size of the identifier is expanded to 24 bits (16777216).
-
- VXLAN is described by IETF RFC 7348, and has been implemented by a
- number of vendors. The protocol runs over UDP using a single
- destination port. This document describes the Linux kernel tunnel
- device, there is also a separate implementation of VXLAN for
- Openvswitch.
-
- Unlike most tunnels, a VXLAN is a 1 to N network, not just point to
- point. A VXLAN device can learn the IP address of the other endpoint
- either dynamically in a manner similar to a learning bridge, or make
- use of statically-configured forwarding entries.
-
- For more information please refer to:
- https://www.kernel.org/doc/Documentation/networking/vxlan.txt
- """
- def __init__(self, ifname, config=''):
- if config:
- self._ifname = ifname
-
- if not os.path.exists('/sys/class/net/{}'.format(self._ifname)):
- # we assume that by default a multicast interface is created
- group = 'group {}'.format(config['group'])
-
- # if remote host is specified we ignore the multicast address
- if config['remote']:
- group = 'remote {}'.format(config['remote'])
-
- # an underlay device is not always specified
- dev = ''
- if config['dev']:
- dev = 'dev {}'.format(config['dev'])
-
- cmd = 'ip link add {intf} type vxlan id {vni} {grp_rem} {dev} dstport {port}' \
- .format(intf=self._ifname, vni=config['vni'], grp_rem=group, dev=dev, port=config['port'])
- self._cmd(cmd)
-
- super().__init__(ifname, type='vxlan')
-
- @staticmethod
- def get_config():
- """
- VXLAN interfaces require a configuration when they are added using
- iproute2. This static method will provide the configuration dictionary
- used by this class.
-
- Example:
- >> dict = VXLANIf().get_config()
- """
- config = {
- 'vni': 0,
- 'dev': '',
- 'group': '',
- 'port': 8472, # The Linux implementation of VXLAN pre-dates
- # the IANA's selection of a standard destination port
- 'remote': ''
- }
- return config
-
-class GeneveIf(Interface):
- """
- Geneve: Generic Network Virtualization Encapsulation
-
- For more information please refer to:
- https://tools.ietf.org/html/draft-gross-geneve-00
- https://www.redhat.com/en/blog/what-geneve
- https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#geneve
- https://lwn.net/Articles/644938/
- """
- def __init__(self, ifname, config=''):
- if config:
- self._ifname = ifname
- if not os.path.exists('/sys/class/net/{}'.format(self._ifname)):
- cmd = 'ip link add name {} type geneve id {} remote {}' \
- .format(self._ifname, config['vni'], config['remote'])
- self._cmd(cmd)
-
- # interface is always A/D down. It needs to be enabled explicitly
- self.set_state('down')
-
- super().__init__(ifname, type='geneve')
-
- @staticmethod
- def get_config():
- """
- GENEVE interfaces require a configuration when they are added using
- iproute2. This static method will provide the configuration dictionary
- used by this class.
-
- Example:
- >> dict = GeneveIf().get_config()
- """
- config = {
- 'vni': 0,
- 'remote': ''
- }
- return config
-
-class L2TPv3If(Interface):
- """
- The Linux bonding driver provides a method for aggregating multiple network
- interfaces into a single logical "bonded" interface. The behavior of the
- bonded interfaces depends upon the mode; generally speaking, modes provide
- either hot standby or load balancing services. Additionally, link integrity
- monitoring may be performed.
- """
- def __init__(self, ifname, config=''):
- self._config = {}
- if config:
- self._ifname = ifname
- self._config = config
- if not os.path.exists('/sys/class/net/{}'.format(self._ifname)):
- # create tunnel interface
- cmd = 'ip l2tp add tunnel tunnel_id {} '.format(config['tunnel_id'])
- cmd += 'peer_tunnel_id {} '.format(config['peer_tunnel_id'])
- cmd += 'udp_sport {} '.format(config['local_port'])
- cmd += 'udp_dport {} '.format(config['remote_port'])
- cmd += 'encap {} '.format(config['encapsulation'])
- cmd += 'local {} '.format(config['local_address'])
- cmd += 'remote {} '.format(config['remote_address'])
- self._cmd(cmd)
-
- # setup session
- cmd = 'ip l2tp add session name {} '.format(self._ifname)
- cmd += 'tunnel_id {} '.format(config['tunnel_id'])
- cmd += 'session_id {} '.format(config['session_id'])
- cmd += 'peer_session_id {} '.format(config['peer_session_id'])
- self._cmd(cmd)
-
- # interface is always A/D down. It needs to be enabled explicitly
- self.set_state('down')
-
- super().__init__(ifname, type='l2tp')
-
- def remove(self):
- """
- Remove interface from operating system. Removing the interface
- deconfigures all assigned IP addresses.
- Example:
- >>> from vyos.ifconfig import L2TPv3If
- >>> i = L2TPv3If('l2tpeth0')
- >>> i.remove()
- """
-
- if os.path.exists('/sys/class/net/{}'.format(self._ifname)):
- # interface is always A/D down. It needs to be enabled explicitly
- self.set_state('down')
-
- if self._config['tunnel_id'] and self._config['session_id']:
- cmd = 'ip l2tp del session tunnel_id {} '.format(self._config['tunnel_id'])
- cmd += 'session_id {} '.format(self._config['session_id'])
- self._cmd(cmd)
-
- if self._config['tunnel_id']:
- cmd = 'ip l2tp del tunnel tunnel_id {} '.format(self._config['tunnel_id'])
- self._cmd(cmd)
-
- @staticmethod
- def get_config():
- """
- L2TPv3 interfaces require a configuration when they are added using
- iproute2. This static method will provide the configuration dictionary
- used by this class.
-
- Example:
- >> dict = L2TPv3If().get_config()
- """
- config = {
- 'peer_tunnel_id': '',
- 'local_port': 0,
- 'remote_port': 0,
- 'encapsulation': 'udp',
- 'local_address': '',
- 'remote_address': '',
- 'session_id': '',
- 'tunnel_id': '',
- 'peer_session_id': ''
- }
- return config
diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py
new file mode 100644
index 000000000..1f9956af0
--- /dev/null
+++ b/python/vyos/ifconfig/__init__.py
@@ -0,0 +1,39 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos.ifconfig.interface import Interface
+
+from vyos.ifconfig.bond import BondIf
+from vyos.ifconfig.bridge import BridgeIf
+from vyos.ifconfig.dummy import DummyIf
+from vyos.ifconfig.ethernet import EthernetIf
+from vyos.ifconfig.geneve import GeneveIf
+from vyos.ifconfig.loopback import LoopbackIf
+from vyos.ifconfig.macvlan import MACVLANIf
+from vyos.ifconfig.vxlan import VXLANIf
+from vyos.ifconfig.wireguard import WireGuardIf
+from vyos.ifconfig.vtun import VTunIf
+from vyos.ifconfig.pppoe import PPPoEIf
+from vyos.ifconfig.tunnel import GREIf
+from vyos.ifconfig.tunnel import GRETapIf
+from vyos.ifconfig.tunnel import IP6GREIf
+from vyos.ifconfig.tunnel import IPIPIf
+from vyos.ifconfig.tunnel import IPIP6If
+from vyos.ifconfig.tunnel import IP6IP6If
+from vyos.ifconfig.tunnel import SitIf
+from vyos.ifconfig.tunnel import Sit6RDIf
+from vyos.ifconfig.wireless import WiFiIf
+from vyos.ifconfig.l2tpv3 import L2TPv3If
diff --git a/python/vyos/ifconfig/afi.py b/python/vyos/ifconfig/afi.py
new file mode 100644
index 000000000..fd263d220
--- /dev/null
+++ b/python/vyos/ifconfig/afi.py
@@ -0,0 +1,19 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+# https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml
+
+IP4 = 1
+IP6 = 2
diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py
new file mode 100644
index 000000000..47dd4ff34
--- /dev/null
+++ b/python/vyos/ifconfig/bond.py
@@ -0,0 +1,279 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.vlan import VLAN
+
+from vyos.validate import assert_list
+from vyos.validate import assert_positive
+
+
+@Interface.register
+@VLAN.enable
+class BondIf(Interface):
+ """
+ The Linux bonding driver provides a method for aggregating multiple network
+ interfaces into a single logical "bonded" interface. The behavior of the
+ bonded interfaces depends upon the mode; generally speaking, modes provide
+ either hot standby or load balancing services. Additionally, link integrity
+ monitoring may be performed.
+ """
+
+ default = {
+ 'type': 'bond',
+ }
+ definition = {
+ **Interface.definition,
+ ** {
+ 'section': 'bonding',
+ 'prefixes': ['bond', ],
+ 'broadcast': True,
+ 'bridgeable': True,
+ },
+ }
+
+ _sysfs_set = {**Interface._sysfs_set, **{
+ 'bond_hash_policy': {
+ 'validate': lambda v: assert_list(v, ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']),
+ 'location': '/sys/class/net/{ifname}/bonding/xmit_hash_policy',
+ },
+ 'bond_miimon': {
+ 'validate': assert_positive,
+ 'location': '/sys/class/net/{ifname}/bonding/miimon'
+ },
+ 'bond_arp_interval': {
+ 'validate': assert_positive,
+ 'location': '/sys/class/net/{ifname}/bonding/arp_interval'
+ },
+ 'bond_arp_ip_target': {
+ # XXX: no validation of the IP
+ 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target',
+ },
+ 'bond_add_port': {
+ 'location': '/sys/class/net/{ifname}/bonding/slaves',
+ },
+ 'bond_del_port': {
+ 'location': '/sys/class/net/{ifname}/bonding/slaves',
+ },
+ 'bond_primary': {
+ 'convert': lambda name: name if name else '\0',
+ 'location': '/sys/class/net/{ifname}/bonding/primary',
+ },
+ 'bond_mode': {
+ 'validate': lambda v: assert_list(v, ['balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb']),
+ 'location': '/sys/class/net/{ifname}/bonding/mode',
+ },
+ }}
+
+ _sysfs_get = {**Interface._sysfs_get, **{
+ 'bond_arp_ip_target': {
+ 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target',
+ }
+ }}
+
+ def remove(self):
+ """
+ Remove interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('eth0')
+ >>> i.remove()
+ """
+ # when a bond member gets deleted, all members are placed in A/D state
+ # even when they are enabled inside CLI. This will make the config
+ # and system look async.
+ slave_list = []
+ for s in self.get_slaves():
+ slave = {
+ 'ifname': s,
+ 'state': Interface(s).get_admin_state()
+ }
+ slave_list.append(slave)
+
+ # remove bond master which places members in disabled state
+ super().remove()
+
+ # replicate previous interface state before bond destruction back to
+ # physical interface
+ for slave in slave_list:
+ i = Interface(slave['ifname'])
+ i.set_admin_state(slave['state'])
+
+ def set_hash_policy(self, mode):
+ """
+ Selects the transmit hash policy to use for slave selection in
+ balance-xor, 802.3ad, and tlb modes. Possible values are: layer2,
+ layer2+3, layer3+4, encap2+3, encap3+4.
+
+ The default value is layer2
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_hash_policy('layer2+3')
+ """
+ self.set_interface('bond_hash_policy', mode)
+
+ def set_arp_interval(self, interval):
+ """
+ Specifies the ARP link monitoring frequency in milliseconds.
+
+ The ARP monitor works by periodically checking the slave devices
+ to determine whether they have sent or received traffic recently
+ (the precise criteria depends upon the bonding mode, and the
+ state of the slave). Regular traffic is generated via ARP probes
+ issued for the addresses specified by the arp_ip_target option.
+
+ If ARP monitoring is used in an etherchannel compatible mode
+ (modes 0 and 2), the switch should be configured in a mode that
+ evenly distributes packets across all links. If the switch is
+ configured to distribute the packets in an XOR fashion, all
+ replies from the ARP targets will be received on the same link
+ which could cause the other team members to fail.
+
+ value of 0 disables ARP monitoring. The default value is 0.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_arp_interval('100')
+ """
+ if int(interval) == 0:
+ """
+ Specifies the MII link monitoring frequency in milliseconds.
+ This determines how often the link state of each slave is
+ inspected for link failures. A value of zero disables MII
+ link monitoring. A value of 100 is a good starting point.
+ """
+ return self.set_interface('bond_miimon', interval)
+ else:
+ return self.set_interface('bond_arp_interval', interval)
+
+ def get_arp_ip_target(self):
+ """
+ Specifies the IP addresses to use as ARP monitoring peers when
+ arp_interval is > 0. These are the targets of the ARP request sent to
+ determine the health of the link to the targets. Specify these values
+ in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
+ a comma. At least one IP address must be given for ARP monitoring to
+ function. The maximum number of targets that can be specified is 16.
+
+ The default value is no IP addresses.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').get_arp_ip_target()
+ '192.0.2.1'
+ """
+ return self.get_interface('bond_arp_ip_target')
+
+ def set_arp_ip_target(self, target):
+ """
+ Specifies the IP addresses to use as ARP monitoring peers when
+ arp_interval is > 0. These are the targets of the ARP request sent to
+ determine the health of the link to the targets. Specify these values
+ in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
+ a comma. At least one IP address must be given for ARP monitoring to
+ function. The maximum number of targets that can be specified is 16.
+
+ The default value is no IP addresses.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_arp_ip_target('192.0.2.1')
+ >>> BondIf('bond0').get_arp_ip_target()
+ '192.0.2.1'
+ """
+ return self.set_interface('bond_arp_ip_target', target)
+
+ def add_port(self, interface):
+ """
+ Enslave physical interface to bond.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').add_port('eth0')
+ >>> BondIf('bond0').add_port('eth1')
+ """
+ # An interface can only be added to a bond if it is in 'down' state. If
+ # interface is in 'up' state, the following Kernel error will be thrown:
+ # bond0: eth1 is up - this may be due to an out of date ifenslave.
+ Interface(interface).set_admin_state('down')
+ return self.set_interface('bond_add_port', f'+{interface}')
+
+ def del_port(self, interface):
+ """
+ Remove physical port from bond
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').del_port('eth1')
+ """
+ return self.set_interface('bond_del_port', f'-{interface}')
+
+ def get_slaves(self):
+ """
+ Return a list with all configured slave interfaces on this bond.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').get_slaves()
+ ['eth1', 'eth2']
+ """
+ enslaved_ifs = []
+ # retrieve real enslaved interfaces from OS kernel
+ sysfs_bond = '/sys/class/net/{}'.format(self.config['ifname'])
+ if os.path.isdir(sysfs_bond):
+ for directory in os.listdir(sysfs_bond):
+ if 'lower_' in directory:
+ enslaved_ifs.append(directory.replace('lower_', ''))
+
+ return enslaved_ifs
+
+ def set_primary(self, interface):
+ """
+ A string (eth0, eth2, etc) specifying which slave is the primary
+ device. The specified device will always be the active slave while it
+ is available. Only when the primary is off-line will alternate devices
+ be used. This is useful when one slave is preferred over another, e.g.,
+ when one slave has higher throughput than another.
+
+ The primary option is only valid for active-backup, balance-tlb and
+ balance-alb mode.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_primary('eth2')
+ """
+ return self.set_interface('bond_primary', interface)
+
+ def set_mode(self, mode):
+ """
+ Specifies one of the bonding policies. The default is balance-rr
+ (round robin).
+
+ Possible values are: balance-rr, active-backup, balance-xor,
+ broadcast, 802.3ad, balance-tlb, balance-alb
+
+ NOTE: the bonding mode can not be changed when the bond itself has
+ slaves
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_mode('802.3ad')
+ """
+ return self.set_interface('bond_mode', mode)
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
new file mode 100644
index 000000000..44b92c1db
--- /dev/null
+++ b/python/vyos/ifconfig/bridge.py
@@ -0,0 +1,189 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos.ifconfig.interface import Interface
+
+from vyos.validate import assert_boolean
+from vyos.validate import assert_positive
+
+
+@Interface.register
+class BridgeIf(Interface):
+ """
+ A bridge is a way to connect two Ethernet segments together in a protocol
+ independent way. Packets are forwarded based on Ethernet address, rather
+ than IP address (like a router). Since forwarding is done at Layer 2, all
+ protocols can go transparently through a bridge.
+
+ The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard.
+ """
+
+ default = {
+ 'type': 'bridge',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'bridge',
+ 'prefixes': ['br', ],
+ 'broadcast': True,
+ },
+ }
+
+ _sysfs_set = {**Interface._sysfs_set, **{
+ 'ageing_time': {
+ 'validate': assert_positive,
+ 'convert': lambda t: int(t) * 100,
+ 'location': '/sys/class/net/{ifname}/bridge/ageing_time',
+ },
+ 'forward_delay': {
+ 'validate': assert_positive,
+ 'convert': lambda t: int(t) * 100,
+ 'location': '/sys/class/net/{ifname}/bridge/forward_delay',
+ },
+ 'hello_time': {
+ 'validate': assert_positive,
+ 'convert': lambda t: int(t) * 100,
+ 'location': '/sys/class/net/{ifname}/bridge/hello_time',
+ },
+ 'max_age': {
+ 'validate': assert_positive,
+ 'convert': lambda t: int(t) * 100,
+ 'location': '/sys/class/net/{ifname}/bridge/max_age',
+ },
+ 'priority': {
+ 'validate': assert_positive,
+ 'location': '/sys/class/net/{ifname}/bridge/priority',
+ },
+ 'stp': {
+ 'validate': assert_boolean,
+ 'location': '/sys/class/net/{ifname}/bridge/stp_state',
+ },
+ 'multicast_querier': {
+ 'validate': assert_boolean,
+ 'location': '/sys/class/net/{ifname}/bridge/multicast_querier',
+ },
+ }}
+
+ _command_set = {**Interface._command_set, **{
+ 'add_port': {
+ 'shellcmd': 'ip link set dev {value} master {ifname}',
+ },
+ 'del_port': {
+ 'shellcmd': 'ip link set dev {value} nomaster',
+ },
+ }}
+
+
+ def set_ageing_time(self, time):
+ """
+ Set bridge interface MAC address aging time in seconds. Internal kernel
+ representation is in centiseconds. Kernel default is 300 seconds.
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').ageing_time(2)
+ """
+ self.set_interface('ageing_time', time)
+
+ def set_forward_delay(self, time):
+ """
+ Set bridge forwarding delay in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').forward_delay(15)
+ """
+ self.set_interface('forward_delay', time)
+
+ def set_hello_time(self, time):
+ """
+ Set bridge hello time in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').set_hello_time(2)
+ """
+ self.set_interface('hello_time', time)
+
+ def set_max_age(self, time):
+ """
+ Set bridge max message age in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').set_max_age(30)
+ """
+ self.set_interface('max_age', time)
+
+ def set_priority(self, priority):
+ """
+ Set bridge max aging time in seconds.
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').set_priority(8192)
+ """
+ self.set_interface('priority', priority)
+
+ def set_stp(self, state):
+ """
+ Set bridge STP (Spanning Tree) state. 0 -> STP disabled, 1 -> STP enabled
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').set_stp(1)
+ """
+ self.set_interface('stp', state)
+
+ def set_multicast_querier(self, enable):
+ """
+ Sets whether the bridge actively runs a multicast querier or not. When a
+ bridge receives a 'multicast host membership' query from another network
+ host, that host is tracked based on the time that the query was received
+ plus the multicast query interval time.
+
+ Use enable=1 to enable or enable=0 to disable
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').set_multicast_querier(1)
+ """
+ self.set_interface('multicast_querier', enable)
+
+ def add_port(self, interface):
+ """
+ Add physical interface to bridge (member port)
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').add_port('eth0')
+ >>> BridgeIf('br0').add_port('eth1')
+ """
+ return self.set_interface('add_port', interface)
+
+ def del_port(self, interface):
+ """
+ Remove member port from bridge instance.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').del_port('eth1')
+ """
+ return self.set_interface('del_port', interface)
diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py
new file mode 100644
index 000000000..c7a2fa2d6
--- /dev/null
+++ b/python/vyos/ifconfig/control.py
@@ -0,0 +1,154 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+import os
+
+from vyos.util import debug, debug_msg
+from vyos.util import popen, cmd
+from vyos.ifconfig.register import Register
+
+
+class Control(Register):
+ _command_get = {}
+ _command_set = {}
+
+ def __init__(self, **kargs):
+ # some commands (such as operation comands - show interfaces, etc.)
+ # need to query the interface statistics. If the interface
+ # code is used and the debugging is enabled, the screen output
+ # will include both the command but also the debugging for that command
+ # to prevent this, debugging can be explicitely disabled
+
+ # if debug is not explicitely disabled the the config, enable it
+ self.debug = ''
+ if kargs.get('debug', True):
+ self.debug = debug('ifconfig')
+
+ def _debug_msg (self, message):
+ return debug_msg(message, self.debug)
+
+ def _popen(self, command):
+ return popen(command, self.debug)
+
+ def _cmd(self, command):
+ return cmd(command, self.debug)
+
+ def _get_command(self, config, name):
+ """
+ Using the defined names, set data write to sysfs.
+ """
+ cmd = self._command_get[name]['shellcmd'].format(**config)
+ return self._command_get[name].get('format', lambda _: _)(self._cmd(cmd))
+
+ def _set_command(self, config, name, value):
+ """
+ Using the defined names, set data write to sysfs.
+ """
+ # the code can pass int as int
+ value = str(value)
+
+ validate = self._command_set[name].get('validate', None)
+ if validate:
+ try:
+ validate(value)
+ except Exception as e:
+ raise e.__class__(f'Could not set {name}. {e}')
+
+ convert = self._command_set[name].get('convert', None)
+ if convert:
+ value = convert(value)
+
+ possible = self._command_set[name].get('possible', None)
+ if possible and not possible(config['ifname'], value):
+ return False
+
+ config = {**config, **{'value': value}}
+
+ cmd = self._command_set[name]['shellcmd'].format(**config)
+ return self._command_set[name].get('format', lambda _: _)(self._cmd(cmd))
+
+ _sysfs_get = {}
+ _sysfs_set = {}
+
+ def _read_sysfs(self, filename):
+ """
+ Provide a single primitive w/ error checking for reading from sysfs.
+ """
+ value = None
+ with open(filename, 'r') as f:
+ value = f.read().rstrip('\n')
+
+ self._debug_msg("read '{}' < '{}'".format(value, filename))
+ return value
+
+ def _write_sysfs(self, filename, value):
+ """
+ Provide a single primitive w/ error checking for writing to sysfs.
+ """
+ self._debug_msg("write '{}' > '{}'".format(value, filename))
+ if os.path.isfile(filename):
+ with open(filename, 'w') as f:
+ f.write(str(value))
+ return True
+ return False
+
+ def _get_sysfs(self, config, name):
+ """
+ Using the defined names, get data write from sysfs.
+ """
+ filename = self._sysfs_get[name]['location'].format(**config)
+ if not filename:
+ return None
+ return self._read_sysfs(filename)
+
+ def _set_sysfs(self, config, name, value):
+ """
+ Using the defined names, set data write to sysfs.
+ """
+ # the code can pass int as int
+ value = str(value)
+
+ validate = self._sysfs_set[name].get('validate', None)
+ if validate:
+ validate(value)
+
+ config = {**config, **{'value': value}}
+
+ convert = self._sysfs_set[name].get('convert', None)
+ if convert:
+ value = convert(value)
+
+ commited = self._write_sysfs(
+ self._sysfs_set[name]['location'].format(**config), value)
+ if not commited:
+ errmsg = self._sysfs_set.get('errormsg', '')
+ if errmsg:
+ raise TypeError(errmsg.format(**config))
+ return commited
+
+ def get_interface(self, name):
+ if name in self._sysfs_get:
+ return self._get_sysfs(self.config, name)
+ if name in self._command_get:
+ return self._get_command(self.config, name)
+ raise KeyError(f'{name} is not a attribute of the interface we can get')
+
+ def set_interface(self, name, value):
+ if name in self._sysfs_set:
+ return self._set_sysfs(self.config, name, value)
+ if name in self._command_set:
+ return self._set_command(self.config, name, value)
+ raise KeyError(f'{name} is not a attribute of the interface we can set')
diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py
new file mode 100644
index 000000000..8ec8263b5
--- /dev/null
+++ b/python/vyos/ifconfig/dhcp.py
@@ -0,0 +1,268 @@
+# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import jinja2
+
+from vyos.ifconfig.control import Control
+
+template_v4 = """
+# generated by ifconfig.py
+option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
+timeout 60;
+retry 300;
+
+interface "{{ intf }}" {
+ send host-name "{{ hostname }}";
+ {% if client_id -%}
+ send dhcp-client-identifier "{{ client_id }}";
+ {% endif -%}
+ {% if vendor_class_id -%}
+ send vendor-class-identifier "{{ vendor_class_id }}";
+ {% endif -%}
+ request subnet-mask, broadcast-address, routers, domain-name-servers,
+ rfc3442-classless-static-routes, domain-name, interface-mtu;
+ require subnet-mask;
+}
+
+"""
+
+template_v6 = """
+# generated by ifconfig.py
+interface "{{ intf }}" {
+ request routers, domain-name-servers, domain-name;
+}
+
+"""
+
+class DHCP (Control):
+ client_base = r'/var/lib/dhcp/dhclient_'
+
+ def __init__ (self, ifname, **kargs):
+ super().__init__(**kargs)
+
+ # per interface DHCP config files
+ self._dhcp = {
+ 4: {
+ 'ifname': ifname,
+ 'conf': self.client_base + ifname + '.conf',
+ 'pid': self.client_base + ifname + '.pid',
+ 'lease': self.client_base + ifname + '.leases',
+ 'options': {
+ 'intf': ifname,
+ 'hostname': '',
+ 'client_id': '',
+ 'vendor_class_id': ''
+ },
+ },
+ 6: {
+ 'ifname': ifname,
+ 'conf': self.client_base + ifname + '.v6conf',
+ 'pid': self.client_base + ifname + '.v6pid',
+ 'lease': self.client_base + ifname + '.v6leases',
+ 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
+ 'options': {
+ 'intf': ifname,
+ 'dhcpv6_prm_only': False,
+ 'dhcpv6_temporary': False
+ },
+ },
+ }
+
+ def get_dhcp_options(self):
+ """
+ Return dictionary with supported DHCP options.
+
+ Dictionary should be altered and send back via set_dhcp_options()
+ so those options are applied when DHCP is run.
+ """
+ return self._dhcp[4]['options']
+
+ def set_dhcp_options(self, options):
+ """
+ Store new DHCP options used by next run of DHCP client.
+ """
+ self._dhcp[4]['options'] = options
+
+ def get_dhcpv6_options(self):
+ """
+ Return dictionary with supported DHCPv6 options.
+
+ Dictionary should be altered and send back via set_dhcp_options()
+ so those options are applied when DHCP is run.
+ """
+ return self._dhcp[6]['options']
+
+ def set_dhcpv6_options(self, options):
+ """
+ Store new DHCP options used by next run of DHCP client.
+ """
+ self._dhcp[6]['options'] = options
+
+ # replace dhcpv4/v6 with systemd.networkd?
+ def _set_dhcp(self):
+ """
+ Configure interface as DHCP client. The dhclient binary is automatically
+ started in background!
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.set_dhcp()
+ """
+
+ dhcp = self.get_dhcp_options()
+ if not dhcp['hostname']:
+ # read configured system hostname.
+ # maybe change to vyos hostd client ???
+ with open('/etc/hostname', 'r') as f:
+ dhcp['hostname'] = f.read().rstrip('\n')
+
+ # render DHCP configuration
+ tmpl = jinja2.Template(template_v4)
+ dhcp_text = tmpl.render(dhcp)
+ with open(self._dhcp[4]['conf'], 'w') as f:
+ f.write(dhcp_text)
+
+ cmd = 'start-stop-daemon'
+ cmd += ' --start'
+ cmd += ' --oknodo'
+ cmd += ' --quiet'
+ cmd += ' --pidfile {pid}'
+ cmd += ' --exec /sbin/dhclient'
+ cmd += ' --'
+ # now pass arguments to dhclient binary
+ cmd += ' -4 -nw -cf {conf} -pf {pid} -lf {lease} {ifname}'
+ return self._cmd(cmd.format(**self._dhcp[4]))
+
+ def _del_dhcp(self):
+ """
+ De-configure interface as DHCP clinet. All auto generated files like
+ pid, config and lease will be removed.
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.del_dhcp()
+ """
+ if not os.path.isfile(self._dhcp[4]['pid']):
+ self._debug_msg('No DHCP client PID found')
+ return None
+
+ # with open(self._dhcp[4]['pid'], 'r') as f:
+ # pid = int(f.read())
+
+ # stop dhclient, we need to call dhclient and tell it should release the
+ # aquired IP address. tcpdump tells me:
+ # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000)
+ # Client-IP 172.16.35.103
+ # Client-Ethernet-Address 00:50:56:9d:11:df
+ # Vendor-rfc1048 Extensions
+ # Magic Cookie 0x63825363
+ # DHCP-Message Option 53, length 1: Release
+ # Server-ID Option 54, length 4: 172.16.35.254
+ # Hostname Option 12, length 10: "vyos"
+ #
+ cmd = '/sbin/dhclient -cf {conf} -pf {pid} -lf {lease} -r {ifname}'
+ self._cmd(cmd.format(**self._dhcp[4]))
+
+ # cleanup old config files
+ for name in ('conf', 'pid', 'lease'):
+ if os.path.isfile(self._dhcp[4][name]):
+ os.remove(self._dhcp[4][name])
+
+ def _set_dhcpv6(self):
+ """
+ Configure interface as DHCPv6 client. The dhclient binary is automatically
+ started in background!
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.set_dhcpv6()
+ """
+ dhcpv6 = self.get_dhcpv6_options()
+
+ # better save then sorry .. should be checked in interface script
+ # but if you missed it we are safe!
+ if dhcpv6['dhcpv6_prm_only'] and dhcpv6['dhcpv6_temporary']:
+ raise Exception(
+ 'DHCPv6 temporary and parameters-only options are mutually exclusive!')
+
+ # render DHCP configuration
+ tmpl = jinja2.Template(template_v6)
+ dhcpv6_text = tmpl.render(dhcpv6)
+ with open(self._dhcp[6]['conf'], 'w') as f:
+ f.write(dhcpv6_text)
+
+ # no longer accept router announcements on this interface
+ self._write_sysfs(self._dhcp[6]['accept_ra'], 0)
+
+ # assemble command-line to start DHCPv6 client (dhclient)
+ cmd = 'start-stop-daemon'
+ cmd += ' --start'
+ cmd += ' --oknodo'
+ cmd += ' --quiet'
+ cmd += ' --pidfile {pid}'
+ cmd += ' --exec /sbin/dhclient'
+ cmd += ' --'
+ # now pass arguments to dhclient binary
+ cmd += ' -6 -nw -cf {conf} -pf {pid} -lf {lease}'
+ # add optional arguments
+ if dhcpv6['dhcpv6_prm_only']:
+ cmd += ' -S'
+ if dhcpv6['dhcpv6_temporary']:
+ cmd += ' -T'
+ cmd += ' {ifname}'
+
+ return self._cmd(cmd.format(**self._dhcp[6]))
+
+ def _del_dhcpv6(self):
+ """
+ De-configure interface as DHCPv6 clinet. All auto generated files like
+ pid, config and lease will be removed.
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.del_dhcpv6()
+ """
+ if not os.path.isfile(self._dhcp[6]['pid']):
+ self._debug_msg('No DHCPv6 client PID found')
+ return None
+
+ # with open(self._dhcp[6]['pid'], 'r') as f:
+ # pid = int(f.read())
+
+ # stop dhclient
+ cmd = 'start-stop-daemon'
+ cmd += ' --start'
+ cmd += ' --oknodo'
+ cmd += ' --quiet'
+ cmd += ' --pidfile {pid}'
+ self._cmd(cmd.format(**self._dhcp[6]))
+
+ # accept router announcements on this interface
+ self._write_sysfs(self._dhcp[6]['accept_ra'], 1)
+
+ # cleanup old config files
+ for name in ('conf', 'pid', 'lease'):
+ if os.path.isfile(self._dhcp[6][name]):
+ os.remove(self._dhcp[6][name])
+
diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py
new file mode 100644
index 000000000..404c490c7
--- /dev/null
+++ b/python/vyos/ifconfig/dummy.py
@@ -0,0 +1,37 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class DummyIf(Interface):
+ """
+ A dummy interface is entirely virtual like, for example, the loopback
+ interface. The purpose of a dummy interface is to provide a device to route
+ packets through without actually transmitting them.
+ """
+
+ default = {
+ 'type': 'dummy',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'dummy',
+ 'prefixes': ['dum', ],
+ },
+ }
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
new file mode 100644
index 000000000..542de4f59
--- /dev/null
+++ b/python/vyos/ifconfig/ethernet.py
@@ -0,0 +1,257 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.vlan import VLAN
+from vyos.validate import assert_list
+from vyos.util import run
+
+
+@Interface.register
+@VLAN.enable
+class EthernetIf(Interface):
+ """
+ Abstraction of a Linux Ethernet Interface
+ """
+
+ default = {
+ 'type': 'ethernet',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'ethernet',
+ 'prefixes': ['lan', 'eth', 'eno', 'ens', 'enp', 'enx'],
+ 'bondable': True,
+ 'broadcast': True,
+ 'bridgeable': True,
+ }
+ }
+
+ @staticmethod
+ def feature(ifname, option, value):
+ run(f'/sbin/ethtool -K {ifname} {option} {value}','ifconfig')
+ return False
+
+ _command_set = {**Interface._command_set, **{
+ 'gro': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'possible': lambda i, v: EthernetIf.feature(i, 'gro', v),
+ # 'shellcmd': '/sbin/ethtool -K {ifname} gro {value}',
+ },
+ 'gso': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'possible': lambda i, v: EthernetIf.feature(i, 'gso', v),
+ # 'shellcmd': '/sbin/ethtool -K {ifname} gso {value}',
+ },
+ 'sg': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'possible': lambda i, v: EthernetIf.feature(i, 'sg', v),
+ # 'shellcmd': '/sbin/ethtool -K {ifname} sg {value}',
+ },
+ 'tso': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'possible': lambda i, v: EthernetIf.feature(i, 'tso', v),
+ # 'shellcmd': '/sbin/ethtool -K {ifname} tso {value}',
+ },
+ 'ufo': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'possible': lambda i, v: EthernetIf.feature(i, 'ufo', v),
+ # 'shellcmd': '/sbin/ethtool -K {ifname} ufo {value}',
+ },
+ }}
+
+ def _delete(self):
+ # Ethernet interfaces can not be removed
+ pass
+
+ def get_driver_name(self):
+ """
+ Return the driver name used by NIC. Some NICs don't support all
+ features e.g. changing link-speed, duplex
+
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.get_driver_name()
+ 'vmxnet3'
+ """
+ sysfs_file = '/sys/class/net/{}/device/driver/module'.format(
+ self.config['ifname'])
+ if os.path.exists(sysfs_file):
+ link = os.readlink(sysfs_file)
+ return os.path.basename(link)
+ else:
+ return None
+
+ def set_flow_control(self, enable):
+ """
+ Changes the pause parameters of the specified Ethernet device.
+
+ @param enable: true -> enable pause frames, false -> disable pause frames
+
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_flow_control(True)
+ """
+ ifname = self.config['ifname']
+
+ if enable not in ['on', 'off']:
+ raise ValueError("Value out of range")
+
+ if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']:
+ self._debug_msg('{} driver does not support changing flow control settings!'
+ .format(self.get_driver_name()))
+ return
+
+ # Get current flow control settings:
+ cmd = f'/sbin/ethtool --show-pause {ifname}'
+ output, code = self._popen(cmd)
+ if code == 76:
+ # the interface does not support it
+ return ''
+ if code:
+ # never fail here as it prevent vyos to boot
+ print(f'unexpected return code {code} from {cmd}')
+ return ''
+
+ # The above command returns - with tabs:
+ #
+ # Pause parameters for eth0:
+ # Autonegotiate: on
+ # RX: off
+ # TX: off
+ if re.search("Autonegotiate:\ton", output):
+ if enable == "on":
+ # flowcontrol is already enabled - no need to re-enable it again
+ # this will prevent the interface from flapping as applying the
+ # flow-control settings will take the interface down and bring
+ # it back up every time.
+ return ''
+
+ # Assemble command executed on system. Unfortunately there is no way
+ # to change this setting via sysfs
+ cmd = f'/sbin/ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}'
+ output, code = self._popen(cmd)
+ if code:
+ print(f'could not set flowcontrol for {ifname}')
+ return output
+
+ def set_speed_duplex(self, speed, duplex):
+ """
+ Set link speed in Mbit/s and duplex.
+
+ @speed can be any link speed in MBit/s, e.g. 10, 100, 1000 auto
+ @duplex can be half, full, auto
+
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_speed_duplex('auto', 'auto')
+ """
+
+ if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000', '25000', '40000', '50000', '100000', '400000']:
+ raise ValueError("Value out of range (speed)")
+
+ if duplex not in ['auto', 'full', 'half']:
+ raise ValueError("Value out of range (duplex)")
+
+ if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']:
+ self._debug_msg('{} driver does not support changing speed/duplex settings!'
+ .format(self.get_driver_name()))
+ return
+
+ # Get current speed and duplex settings:
+ cmd = '/sbin/ethtool {0}'.format(self.config['ifname'])
+ tmp = self._cmd(cmd)
+
+ if re.search("\tAuto-negotiation: on", tmp):
+ if speed == 'auto' and duplex == 'auto':
+ # bail out early as nothing is to change
+ return
+ else:
+ # read in current speed and duplex settings
+ cur_speed = 0
+ cur_duplex = ''
+ for line in tmp.splitlines():
+ if line.lstrip().startswith("Speed:"):
+ non_decimal = re.compile(r'[^\d.]+')
+ cur_speed = non_decimal.sub('', line)
+ continue
+
+ if line.lstrip().startswith("Duplex:"):
+ cur_duplex = line.split()[-1].lower()
+ break
+
+ if (cur_speed == speed) and (cur_duplex == duplex):
+ # bail out early as nothing is to change
+ return
+
+ cmd = '/sbin/ethtool -s {}'.format(self.config['ifname'])
+ if speed == 'auto' or duplex == 'auto':
+ cmd += ' autoneg on'
+ else:
+ cmd += ' speed {} duplex {} autoneg off'.format(speed, duplex)
+
+ return self._cmd(cmd)
+
+ def set_gro(self, state):
+ """
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_gro('on')
+ """
+ return self.set_interface('gro', state)
+
+ def set_gso(self, state):
+ """
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_gso('on')
+ """
+ return self.set_interface('gso', state)
+
+ def set_sg(self, state):
+ """
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_sg('on')
+ """
+ return self.set_interface('sg', state)
+
+ def set_tso(self, state):
+ """
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_tso('on')
+ """
+ return self.set_interface('tso', state)
+
+ def set_ufo(self, state):
+ """
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_udp_offload('on')
+ """
+ return self.set_interface('ufo', state)
diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py
new file mode 100644
index 000000000..0c1cdade9
--- /dev/null
+++ b/python/vyos/ifconfig/geneve.py
@@ -0,0 +1,64 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from copy import deepcopy
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class GeneveIf(Interface):
+ """
+ Geneve: Generic Network Virtualization Encapsulation
+
+ For more information please refer to:
+ https://tools.ietf.org/html/draft-gross-geneve-00
+ https://www.redhat.com/en/blog/what-geneve
+ https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#geneve
+ https://lwn.net/Articles/644938/
+ """
+
+ default = {
+ 'type': 'geneve',
+ 'vni': 0,
+ 'remote': '',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'geneve',
+ 'prefixes': ['gnv', ],
+ 'bridgeable': True,
+ }
+ }
+
+ def _create(self):
+ cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote}'.format(**self.config)
+ self._cmd(cmd)
+
+ # interface is always A/D down. It needs to be enabled explicitly
+ self.set_admin_state('down')
+
+ @classmethod
+ def get_config(cls):
+ """
+ GENEVE interfaces require a configuration when they are added using
+ iproute2. This static method will provide the configuration dictionary
+ used by this class.
+
+ Example:
+ >> dict = GeneveIf().get_config()
+ """
+ return deepcopy(cls.default)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
new file mode 100644
index 000000000..22c71a464
--- /dev/null
+++ b/python/vyos/ifconfig/interface.py
@@ -0,0 +1,738 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import json
+import glob
+import time
+from time import sleep
+from os.path import isfile
+from copy import deepcopy
+from datetime import timedelta
+
+from hurry.filesize import size, alternative
+from ipaddress import IPv4Network, IPv6Address, IPv6Network
+from netifaces import ifaddresses, AF_INET, AF_INET6
+from tabulate import tabulate
+
+from vyos.util import mac2eui64
+from vyos import ConfigError
+from vyos.ifconfig.dhcp import DHCP
+from vyos.validate import is_ipv4
+from vyos.validate import is_ipv6
+from vyos.validate import is_intf_addr_assigned
+from vyos.validate import assert_boolean
+from vyos.validate import assert_list
+from vyos.validate import assert_mac
+from vyos.validate import assert_mtu
+from vyos.validate import assert_positive
+from vyos.validate import assert_range
+
+
+class Interface(DHCP):
+ options = []
+ required = []
+ default = {
+ 'type': '',
+ 'debug': True,
+ 'create': True,
+ }
+ definition = {
+ 'section': '',
+ 'prefixes': [],
+ 'vlan': False,
+ 'bondable': False,
+ 'broadcast': False,
+ 'bridgeable': False,
+ }
+
+ _command_get = {
+ 'admin_state': {
+ 'shellcmd': 'ip -json link show dev {ifname}',
+ 'format': lambda j: 'up' if 'UP' in json.loads(j)[0]['flags'] else 'down',
+ }
+ }
+
+ _command_set = {
+ 'admin_state': {
+ 'validate': lambda v: assert_list(v, ['up', 'down']),
+ 'shellcmd': 'ip link set dev {ifname} {value}',
+ },
+ 'mac': {
+ 'validate': assert_mac,
+ 'shellcmd': 'ip link set dev {ifname} address {value}',
+ },
+ 'vrf': {
+ 'convert': lambda v: f'master {v}' if v else 'nomaster',
+ 'shellcmd': 'ip link set dev {ifname} {value}',
+ },
+ }
+
+ _sysfs_get = {
+ 'alias': {
+ 'location': '/sys/class/net/{ifname}/ifalias',
+ },
+ 'mac': {
+ 'location': '/sys/class/net/{ifname}/address',
+ },
+ 'mtu': {
+ 'location': '/sys/class/net/{ifname}/mtu',
+ },
+ 'oper_state':{
+ 'location': '/sys/class/net/{ifname}/operstate',
+ },
+ }
+
+ _sysfs_set = {
+ 'alias': {
+ 'convert': lambda name: name if name else '\0',
+ 'location': '/sys/class/net/{ifname}/ifalias',
+ },
+ 'mtu': {
+ 'validate': assert_mtu,
+ 'location': '/sys/class/net/{ifname}/mtu',
+ },
+ 'arp_cache_tmo': {
+ 'convert': lambda tmo: (int(tmo) * 1000),
+ 'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms',
+ },
+ 'arp_filter': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_filter',
+ },
+ 'arp_accept': {
+ 'validate': lambda arp: assert_range(arp,0,2),
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_accept',
+ },
+ 'arp_announce': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_announce',
+ },
+ 'arp_ignore': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore',
+ },
+ 'ipv6_autoconf': {
+ 'validate': lambda fwd: assert_range(fwd,0,2),
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf',
+ },
+ 'ipv6_forwarding': {
+ 'validate': lambda fwd: assert_range(fwd,0,2),
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/forwarding',
+ },
+ 'ipv6_dad_transmits': {
+ 'validate': assert_positive,
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/dad_transmits',
+ },
+ 'proxy_arp': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp',
+ },
+ 'proxy_arp_pvlan': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp_pvlan',
+ },
+ # link_detect vs link_filter name weirdness
+ 'link_detect': {
+ 'validate': lambda link: assert_range(link,0,3),
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/link_filter',
+ },
+ }
+
+ def __init__(self, ifname, **kargs):
+ """
+ This is the base interface class which supports basic IP/MAC address
+ operations as well as DHCP(v6). Other interface which represent e.g.
+ and ethernet bridge are implemented as derived classes adding all
+ additional functionality.
+
+ For creation you will need to provide the interface type, otherwise
+ the existing interface is used
+
+ DEBUG:
+ This class has embedded debugging (print) which can be enabled by
+ creating the following file:
+ vyos@vyos# touch /tmp/vyos.ifconfig.debug
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('eth0')
+ """
+
+ self.config = deepcopy(self.default)
+ for k in self.options:
+ if k in kargs:
+ self.config[k] = kargs[k]
+
+ # make sure the ifname is the first argument and not from the dict
+ self.config['ifname'] = ifname
+
+ # we must have updated config before initialising the Interface
+ super().__init__(ifname, **kargs)
+
+ if not os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])):
+ # Any instance of Interface, such as Interface('eth0')
+ # can be used safely to access the generic function in this class
+ # as 'type' is unset, the class can not be created
+ if not self.config['type']:
+ raise Exception('interface "{}" not found'.format(self.config['ifname']))
+
+ # Should an Instance of a child class (EthernetIf, DummyIf, ..)
+ # be required, then create should be set to False to not accidentally create it.
+ # In case a subclass does not define it, we use get to set the default to True
+ if self.config.get('create',True):
+ for k in self.required:
+ if k not in kargs:
+ name = self.default['type']
+ raise ConfigError(f'missing required option {k} for {name} {ifname} creation')
+
+ self._create()
+ # If we can not connect to the interface then let the caller know
+ # as the class could not be correctly initialised
+ else:
+ raise Exception('interface "{}" not found'.format(self.config['ifname']))
+
+ # list of assigned IP addresses
+ self._addr = []
+
+ def _create(self):
+ cmd = 'ip link add dev {ifname} type {type}'.format(**self.config)
+ self._cmd(cmd)
+
+ def remove(self):
+ """
+ Remove interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('eth0')
+ >>> i.remove()
+ """
+ # stop DHCP(v6) if running
+ self._del_dhcp()
+ self._del_dhcpv6()
+
+ # remove all assigned IP addresses from interface - this is a bit redundant
+ # as the kernel will remove all addresses on interface deletion, but we
+ # can not delete ALL interfaces, see below
+ for addr in self.get_addr():
+ self.del_addr(addr)
+
+ # ---------------------------------------------------------------------
+ # A code refactoring is required as this type check is present as
+ # Interface implement behaviour for one of it's sub-class.
+
+ # It is required as the current pattern for vlan is:
+ # Interface('name').remove() to delete an interface
+ # The code should be modified to have a class method called connect and
+ # have Interface.connect('name').remove()
+
+ # each subclass should register within Interface the pattern for that
+ # interface ie: (ethX, etc.) and use this to create an instance of
+ # the right class (EthernetIf, ...)
+
+ # Ethernet interfaces can not be removed
+
+ # Commented out as nowhere in the code do we call Interface()
+ # This would also cause an import loop
+ # if self.__class__ == EthernetIf:
+ # return
+
+ # ---------------------------------------------------------------------
+
+ self._delete()
+
+ def _delete(self):
+ # NOTE (Improvement):
+ # after interface removal no other commands should be allowed
+ # to be called and instead should raise an Exception:
+ cmd = 'ip link del dev {}'.format(self.config['ifname'])
+ return self._cmd(cmd)
+
+ def get_mtu(self):
+ """
+ Get/set interface mtu in bytes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_mtu()
+ '1500'
+ """
+ return self.get_interface('mtu')
+
+ def set_mtu(self, mtu):
+ """
+ Get/set interface mtu in bytes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_mtu(1400)
+ >>> Interface('eth0').get_mtu()
+ '1400'
+ """
+ return self.set_interface('mtu', mtu)
+
+ def get_mac(self):
+ """
+ Get current interface MAC (Media Access Contrl) address used.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_mac()
+ '00:50:ab:cd:ef:00'
+ """
+ return self.get_interface('mac')
+
+ def set_mac(self, mac):
+ """
+ Set interface MAC (Media Access Contrl) address to given value.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_mac('00:50:ab:cd:ef:01')
+ """
+
+ # If MAC is unchanged, bail out early
+ if mac == self.get_mac():
+ return None
+
+ # MAC address can only be changed if interface is in 'down' state
+ prev_state = self.get_admin_state()
+ if prev_state == 'up':
+ self.set_admin_state('down')
+
+ self.set_interface('mac', mac)
+
+ def set_vrf(self, vrf=''):
+ """
+ Add/Remove interface from given VRF instance.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_vrf('foo')
+ >>> Interface('eth0').set_vrf()
+ """
+ self.set_interface('vrf', vrf)
+
+ def set_arp_cache_tmo(self, tmo):
+ """
+ Set ARP cache timeout value in seconds. Internal Kernel representation
+ is in milliseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_arp_cache_tmo(40)
+ """
+ return self.set_interface('arp_cache_tmo', tmo)
+
+ def set_arp_filter(self, arp_filter):
+ """
+ Filter ARP requests
+
+ 1 - Allows you to have multiple network interfaces on the same
+ subnet, and have the ARPs for each interface be answered
+ based on whether or not the kernel would route a packet from
+ the ARP'd IP out that interface (therefore you must use source
+ based routing for this to work). In other words it allows control
+ of which cards (usually 1) will respond to an arp request.
+
+ 0 - (default) The kernel can respond to arp requests with addresses
+ from other interfaces. This may seem wrong but it usually makes
+ sense, because it increases the chance of successful communication.
+ IP addresses are owned by the complete host on Linux, not by
+ particular interfaces. Only for more complex setups like load-
+ balancing, does this behaviour cause problems.
+ """
+ return self.set_interface('arp_filter', arp_filter)
+
+ def set_arp_accept(self, arp_accept):
+ """
+ Define behavior for gratuitous ARP frames who's IP is not
+ already present in the ARP table:
+ 0 - don't create new entries in the ARP table
+ 1 - create new entries in the ARP table
+
+ Both replies and requests type gratuitous arp will trigger the
+ ARP table to be updated, if this setting is on.
+
+ If the ARP table already contains the IP address of the
+ gratuitous arp frame, the arp table will be updated regardless
+ if this setting is on or off.
+ """
+ return self.set_interface('arp_accept', arp_accept)
+
+ def set_arp_announce(self, arp_announce):
+ """
+ Define different restriction levels for announcing the local
+ source IP address from IP packets in ARP requests sent on
+ interface:
+ 0 - (default) Use any local address, configured on any interface
+ 1 - Try to avoid local addresses that are not in the target's
+ subnet for this interface. This mode is useful when target
+ hosts reachable via this interface require the source IP
+ address in ARP requests to be part of their logical network
+ configured on the receiving interface. When we generate the
+ request we will check all our subnets that include the
+ target IP and will preserve the source address if it is from
+ such subnet.
+
+ Increasing the restriction level gives more chance for
+ receiving answer from the resolved target while decreasing
+ the level announces more valid sender's information.
+ """
+ return self.set_interface('arp_announce', arp_announce)
+
+ def set_arp_ignore(self, arp_ignore):
+ """
+ Define different modes for sending replies in response to received ARP
+ requests that resolve local target IP addresses:
+
+ 0 - (default): reply for any local target IP address, configured
+ on any interface
+ 1 - reply only if the target IP address is local address
+ configured on the incoming interface
+ """
+ return self.set_interface('arp_ignore', arp_ignore)
+
+ def set_ipv6_autoconf(self, autoconf):
+ """
+ Autoconfigure addresses using Prefix Information in Router
+ Advertisements.
+ """
+ return self.set_interface('ipv6_autoconf', autoconf)
+
+ def set_ipv6_eui64_address(self, prefix):
+ """
+ Extended Unique Identifier (EUI), as per RFC2373, allows a host to
+ assign iteslf a unique IPv6 address based on a given IPv6 prefix.
+
+ If prefix is passed address is assigned, if prefix is '' address is
+ removed from interface.
+ """
+ # if prefix is an empty string convert it to None so mac2eui64 works
+ # as expected
+ if not prefix:
+ prefix = None
+
+ eui64 = mac2eui64(self.get_mac(), prefix)
+
+ if not prefix:
+ # if prefix is empty - thus removed - we need to walk through all
+ # interface IPv6 addresses and find the one with the calculated
+ # EUI-64 identifier. The address is then removed
+ for addr in self.get_addr():
+ addr_wo_prefix = addr.split('/')[0]
+ if is_ipv6(addr_wo_prefix):
+ if eui64 in IPv6Address(addr_wo_prefix).exploded:
+ self.del_addr(addr)
+
+ return None
+
+ # calculate and add EUI-64 IPv6 address
+ if IPv6Network(prefix):
+ # we also need to take the subnet length into account
+ prefix = prefix.split('/')[1]
+ eui64 = f'{eui64}/{prefix}'
+ self.add_addr(eui64 )
+
+ def set_ipv6_forwarding(self, forwarding):
+ """
+ Configure IPv6 interface-specific Host/Router behaviour.
+
+ False:
+
+ By default, Host behaviour is assumed. This means:
+
+ 1. IsRouter flag is not set in Neighbour Advertisements.
+ 2. If accept_ra is TRUE (default), transmit Router
+ Solicitations.
+ 3. If accept_ra is TRUE (default), accept Router
+ Advertisements (and do autoconfiguration).
+ 4. If accept_redirects is TRUE (default), accept Redirects.
+
+ True:
+
+ If local forwarding is enabled, Router behaviour is assumed.
+ This means exactly the reverse from the above:
+
+ 1. IsRouter flag is set in Neighbour Advertisements.
+ 2. Router Solicitations are not sent unless accept_ra is 2.
+ 3. Router Advertisements are ignored unless accept_ra is 2.
+ 4. Redirects are ignored.
+ """
+ return self.set_interface('ipv6_forwarding', forwarding)
+
+ def set_ipv6_dad_messages(self, dad):
+ """
+ The amount of Duplicate Address Detection probes to send.
+ Default: 1
+ """
+ return self.set_interface('ipv6_dad_transmits', dad)
+
+ def set_link_detect(self, link_filter):
+ """
+ Configure kernel response in packets received on interfaces that are 'down'
+
+ 0 - Allow packets to be received for the address on this interface
+ even if interface is disabled or no carrier.
+
+ 1 - Ignore packets received if interface associated with the incoming
+ address is down.
+
+ 2 - Ignore packets received if interface associated with the incoming
+ address is down or has no carrier.
+
+ Default value is 0. Note that some distributions enable it in startup
+ scripts.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_link_detect(1)
+ """
+ return self.set_interface('link_detect', link_filter)
+
+ def get_alias(self):
+ """
+ Get interface alias name used by e.g. SNMP
+
+ Example:
+ >>> Interface('eth0').get_alias()
+ 'interface description as set by user'
+ """
+ return self.get_interface('alias')
+
+ def set_alias(self, ifalias=''):
+ """
+ Set interface alias name used by e.g. SNMP
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_alias('VyOS upstream interface')
+
+ to clear alias e.g. delete it use:
+
+ >>> Interface('eth0').set_ifalias('')
+ """
+ self.set_interface('alias', ifalias)
+
+ def get_admin_state(self):
+ """
+ Get interface administrative state. Function will return 'up' or 'down'
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_admin_state()
+ 'up'
+ """
+ return self.get_interface('admin_state')
+
+ def set_admin_state(self, state):
+ """
+ Set interface administrative state to be 'up' or 'down'
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_admin_state('down')
+ >>> Interface('eth0').get_admin_state()
+ 'down'
+ """
+ return self.set_interface('admin_state', state)
+
+ def get_oper_state(self):
+ """
+ Get interface operational state
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_oper_sate()
+ 'up'
+ """
+ # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net
+ # "unknown", "notpresent", "down", "lowerlayerdown", "testing", "dormant", "up"
+ return self.get_interface('oper_state')
+
+ def set_proxy_arp(self, enable):
+ """
+ Set per interface proxy ARP configuration
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_proxy_arp(1)
+ """
+ self.set_interface('proxy_arp', enable)
+
+ def set_proxy_arp_pvlan(self, enable):
+ """
+ Private VLAN proxy arp.
+ Basically allow proxy arp replies back to the same interface
+ (from which the ARP request/solicitation was received).
+
+ This is done to support (ethernet) switch features, like RFC
+ 3069, where the individual ports are NOT allowed to
+ communicate with each other, but they are allowed to talk to
+ the upstream router. As described in RFC 3069, it is possible
+ to allow these hosts to communicate through the upstream
+ router by proxy_arp'ing. Don't need to be used together with
+ proxy_arp.
+
+ This technology is known by different names:
+ In RFC 3069 it is called VLAN Aggregation.
+ Cisco and Allied Telesyn call it Private VLAN.
+ Hewlett-Packard call it Source-Port filtering or port-isolation.
+ Ericsson call it MAC-Forced Forwarding (RFC Draft).
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_proxy_arp_pvlan(1)
+ """
+ self.set_interface('proxy_arp_pvlan', enable)
+
+ def get_addr(self):
+ """
+ Retrieve assigned IPv4 and IPv6 addresses from given interface.
+ This is done using the netifaces and ipaddress python modules.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_addrs()
+ ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64']
+ """
+
+ ipv4 = []
+ ipv6 = []
+
+ if AF_INET in ifaddresses(self.config['ifname']).keys():
+ for v4_addr in ifaddresses(self.config['ifname'])[AF_INET]:
+ # we need to manually assemble a list of IPv4 address/prefix
+ prefix = '/' + \
+ str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen)
+ ipv4.append(v4_addr['addr'] + prefix)
+
+ if AF_INET6 in ifaddresses(self.config['ifname']).keys():
+ for v6_addr in ifaddresses(self.config['ifname'])[AF_INET6]:
+ # Note that currently expanded netmasks are not supported. That means
+ # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not.
+ # see https://docs.python.org/3/library/ipaddress.html
+ bits = bin(
+ int(v6_addr['netmask'].replace(':', ''), 16)).count('1')
+ prefix = '/' + str(bits)
+
+ # we alsoneed to remove the interface suffix on link local
+ # addresses
+ v6_addr['addr'] = v6_addr['addr'].split('%')[0]
+ ipv6.append(v6_addr['addr'] + prefix)
+
+ return ipv4 + ipv6
+
+ def add_addr(self, addr):
+ """
+ Add IP(v6) address to interface. Address is only added if it is not
+ already assigned to that interface.
+
+ addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
+ IPv4: add IPv4 address to interface
+ IPv6: add IPv6 address to interface
+ dhcp: start dhclient (IPv4) on interface
+ dhcpv6: start dhclient (IPv6) on interface
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.add_addr('192.0.2.1/24')
+ >>> j.add_addr('2001:db8::ffff/64')
+ >>> j.get_addr()
+ ['192.0.2.1/24', '2001:db8::ffff/64']
+ """
+
+ # cache new IP address which is assigned to interface
+ self._addr.append(addr)
+
+ # we can not have both DHCP and static IPv4 addresses assigned to an interface
+ if 'dhcp' in self._addr:
+ for addr in self._addr:
+ # do not change below 'if' ordering esle you will get an exception as:
+ # ValueError: 'dhcp' does not appear to be an IPv4 or IPv6 address
+ if addr != 'dhcp' and is_ipv4(addr):
+ raise ConfigError("Can't configure both static IPv4 and DHCP address on the same interface")
+
+ if addr == 'dhcp':
+ self._set_dhcp()
+ elif addr == 'dhcpv6':
+ self._set_dhcpv6()
+ else:
+ if not is_intf_addr_assigned(self.config['ifname'], addr):
+ cmd = 'ip addr add "{}" dev "{}"'.format(addr, self.config['ifname'])
+ return self._cmd(cmd)
+
+ def del_addr(self, addr):
+ """
+ Delete IP(v6) address to interface. Address is only added if it is
+ assigned to that interface.
+
+ addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
+ IPv4: delete IPv4 address from interface
+ IPv6: delete IPv6 address from interface
+ dhcp: stop dhclient (IPv4) on interface
+ dhcpv6: stop dhclient (IPv6) on interface
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.add_addr('2001:db8::ffff/64')
+ >>> j.add_addr('192.0.2.1/24')
+ >>> j.get_addr()
+ ['192.0.2.1/24', '2001:db8::ffff/64']
+ >>> j.del_addr('192.0.2.1/24')
+ >>> j.get_addr()
+ ['2001:db8::ffff/64']
+ """
+ if addr == 'dhcp':
+ self._del_dhcp()
+ elif addr == 'dhcpv6':
+ self._del_dhcpv6()
+ else:
+ if is_intf_addr_assigned(self.config['ifname'], addr):
+ cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname'])
+ return self._cmd(cmd)
+
+ def op_show_interface_stats(self):
+ stats = self.get_interface_stats()
+ rx = [['bytes','packets','errors','dropped','overrun','mcast'],[stats['rx_bytes'],stats['rx_packets'],stats['rx_errors'],stats['rx_dropped'],stats['rx_over_errors'],stats['multicast']]]
+ tx = [['bytes','packets','errors','dropped','carrier','collisions'],[stats['tx_bytes'],stats['tx_packets'],stats['tx_errors'],stats['tx_dropped'],stats['tx_carrier_errors'],stats['collisions']]]
+ output = "RX: \n"
+ output += tabulate(rx,headers="firstrow",numalign="right",tablefmt="plain")
+ output += "\n\nTX: \n"
+ output += tabulate(tx,headers="firstrow",numalign="right",tablefmt="plain")
+ print(' '.join(('\n'+output.lstrip()).splitlines(True)))
+
+ def get_interface_stats(self):
+ interface_stats = dict()
+ devices = [f for f in glob.glob("/sys/class/net/**/statistics")]
+ for dev_path in devices:
+ metrics = [f for f in glob.glob(dev_path +"/**")]
+ dev = re.findall(r"/sys/class/net/(.*)/statistics",dev_path)[0]
+ dev_dict = dict()
+ for metric_path in metrics:
+ metric = metric_path.replace(dev_path+"/","")
+ if isfile(metric_path):
+ data = open(metric_path, 'r').read()[:-1]
+ dev_dict[metric] = int(data)
+ interface_stats[dev] = dev_dict
+
+ return interface_stats[self.config['ifname']]
+
diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py
new file mode 100644
index 000000000..34147eb38
--- /dev/null
+++ b/python/vyos/ifconfig/l2tpv3.py
@@ -0,0 +1,113 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+import os
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class L2TPv3If(Interface):
+ """
+ The Linux bonding driver provides a method for aggregating multiple network
+ interfaces into a single logical "bonded" interface. The behavior of the
+ bonded interfaces depends upon the mode; generally speaking, modes provide
+ either hot standby or load balancing services. Additionally, link integrity
+ monitoring may be performed.
+ """
+
+ default = {
+ 'type': 'l2tp',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'l2tpeth',
+ 'prefixes': ['l2tpeth', ],
+ 'bridgeable': True,
+ }
+ }
+ options = Interface.options + \
+ ['tunnel_id', 'peer_tunnel_id', 'local_port', 'remote_port',
+ 'encapsulation', 'local_address', 'remote_address', 'session_id',
+ 'peer_session_id']
+
+ def _create(self):
+ # create tunnel interface
+ cmd = 'ip l2tp add tunnel tunnel_id {tunnel_id}'
+ cmd += ' peer_tunnel_id {peer_tunnel_id}'
+ cmd += ' udp_sport {local_port}'
+ cmd += ' udp_dport {remote_port}'
+ cmd += ' encap {encapsulation}'
+ cmd += ' local {local_address}'
+ cmd += ' remote {remote_address}'
+ self._cmd(cmd.format(**self.config))
+
+ # setup session
+ cmd = 'ip l2tp add session name {ifname}'
+ cmd += ' tunnel_id {tunnel_id}'
+ cmd += ' session_id {session_id}'
+ cmd += ' peer_session_id {peer_session_id}'
+ self._cmd(cmd.format(**self.config))
+
+ # interface is always A/D down. It needs to be enabled explicitly
+ self.set_admin_state('down')
+
+ def remove(self):
+ """
+ Remove interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses.
+ Example:
+ >>> from vyos.ifconfig import L2TPv3If
+ >>> i = L2TPv3If('l2tpeth0')
+ >>> i.remove()
+ """
+
+ if os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])):
+ # interface is always A/D down. It needs to be enabled explicitly
+ self.set_admin_state('down')
+
+ if self.config['tunnel_id'] and self.config['session_id']:
+ cmd = 'ip l2tp del session tunnel_id {tunnel_id}'
+ cmd += ' session_id {session_id}'
+ self._cmd(cmd.format(**self.config))
+
+ if self.config['tunnel_id']:
+ cmd = 'ip l2tp del tunnel tunnel_id {tunnel_id}'
+ self._cmd(cmd.format(**self.config))
+
+ @staticmethod
+ def get_config():
+ """
+ L2TPv3 interfaces require a configuration when they are added using
+ iproute2. This static method will provide the configuration dictionary
+ used by this class.
+
+ Example:
+ >> dict = L2TPv3If().get_config()
+ """
+ config = {
+ 'peer_tunnel_id': '',
+ 'local_port': 0,
+ 'remote_port': 0,
+ 'encapsulation': 'udp',
+ 'local_address': '',
+ 'remote_address': '',
+ 'session_id': '',
+ 'tunnel_id': '',
+ 'peer_session_id': ''
+ }
+ return config
diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py
new file mode 100644
index 000000000..8e4438662
--- /dev/null
+++ b/python/vyos/ifconfig/loopback.py
@@ -0,0 +1,58 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class LoopbackIf(Interface):
+ """
+ The loopback device is a special, virtual network interface that your router
+ uses to communicate with itself.
+ """
+
+ default = {
+ 'type': 'loopback',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'loopback',
+ 'prefixes': ['lo', ],
+ 'bridgeable': True,
+ }
+ }
+
+ name = 'loopback'
+
+ def remove(self):
+ """
+ Loopback interface can not be deleted from operating system. We can
+ only remove all assigned IP addresses.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = LoopbackIf('lo').remove()
+ """
+ # remove all assigned IP addresses from interface
+ for addr in self.get_addr():
+ if addr in ["127.0.0.1/8", "::1/128"]:
+ # Do not allow deletion of the default loopback addresses as
+ # this will cause weird system behavior like snmp/ssh no longer
+ # operating as expected, see https://phabricator.vyos.net/T2034.
+ continue
+
+ self.del_addr(addr)
diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py
new file mode 100644
index 000000000..55b1a3e91
--- /dev/null
+++ b/python/vyos/ifconfig/macvlan.py
@@ -0,0 +1,67 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.vlan import VLAN
+
+
+@Interface.register
+@VLAN.enable
+class MACVLANIf(Interface):
+ """
+ Abstraction of a Linux MACvlan interface
+ """
+
+ default = {
+ 'type': 'macvlan',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'pseudo-ethernet',
+ 'prefixes': ['peth', ],
+ },
+ }
+ options = Interface.options + ['source_interface', 'mode']
+
+ def _create(self):
+ cmd = 'ip link add {ifname} link {source_interface} type macvlan mode {mode}'.format(
+ **self.config)
+ self._cmd(cmd)
+
+ @staticmethod
+ def get_config():
+ """
+ VXLAN interfaces require a configuration when they are added using
+ iproute2. This static method will provide the configuration dictionary
+ used by this class.
+
+ Example:
+ >> dict = MACVLANIf().get_config()
+ """
+ config = {
+ 'address': '',
+ 'source_interface': '',
+ 'mode': ''
+ }
+ return config
+
+ def set_mode(self, mode):
+ """
+ """
+ ifname = self.config['ifname']
+ cmd = f'ip link set dev {ifname} type macvlan mode {mode}'
+ return self._cmd(cmd)
diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py
new file mode 100644
index 000000000..7504408cf
--- /dev/null
+++ b/python/vyos/ifconfig/pppoe.py
@@ -0,0 +1,33 @@
+# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class PPPoEIf(Interface):
+ default = {
+ 'type': 'pppoe',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'pppoe',
+ 'prefixes': ['pppoe', ],
+ },
+ }
+
+ # The _create and _delete need to be moved from interface-ppoe to here
diff --git a/python/vyos/ifconfig/register.py b/python/vyos/ifconfig/register.py
new file mode 100644
index 000000000..c90782b70
--- /dev/null
+++ b/python/vyos/ifconfig/register.py
@@ -0,0 +1,95 @@
+# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import netifaces
+
+
+class Register:
+ # the known interface prefixes
+ _prefixes = {}
+
+ # class need to define: definition['prefixes']
+ # the interface prefixes declared by a class used to name interface with
+ # prefix[0-9]*(\.[0-9]+)?(\.[0-9]+)?, such as lo, eth0 or eth0.1.2
+
+ @classmethod
+ def register(cls, klass):
+ if not klass.definition.get('prefixes',[]):
+ raise RuntimeError(f'valid interface prefixes not defined for {klass.__name__}')
+
+ for ifprefix in klass.definition['prefixes']:
+ if ifprefix in cls._prefixes:
+ raise RuntimeError(f'only one class can be registered for prefix "{ifprefix}" type')
+ cls._prefixes[ifprefix] = klass
+
+ return klass
+
+ @classmethod
+ def _basename (cls, name, vlan):
+ # remove number from interface name
+ name = name.rstrip('0123456789')
+ name = name.rstrip('.')
+ if vlan:
+ name = name.rstrip('0123456789')
+ return name
+
+ @classmethod
+ def section(cls, name, vlan=True):
+ # return the name of a section an interface should be under
+ name = cls._basename(name, vlan)
+
+ # XXX: To leave as long as vti and input are not moved to vyos
+ if name == 'vti':
+ return 'vti'
+ if name == 'ifb':
+ return 'input'
+
+ if name in cls._prefixes:
+ return cls._prefixes[name].definition['section']
+ return ''
+
+ @classmethod
+ def klass(cls, name, vlan=True):
+ name = cls._basename(name, vlan)
+ if name in cls._prefixes:
+ return cls._prefixes[name]
+ raise ValueError(f'No type found for interface name: {name}')
+
+ @classmethod
+ def _listing (cls,section=''):
+ interfaces = netifaces.interfaces()
+
+ for ifname in interfaces:
+ # XXX: Temporary hack as vti and input are not yet moved from vyatta to vyos
+ if ifname.startswith('vti') or ifname.startswith('input'):
+ yield ifname
+ continue
+
+ ifsection = cls.section(ifname)
+ if not ifsection:
+ continue
+
+ if section and ifsection != section:
+ continue
+
+ yield ifname
+
+ @classmethod
+ def listing(cls, section=''):
+ return list(cls._listing(section))
+
+
+# XXX: TODO - limit name for VRF interfaces
+
diff --git a/python/vyos/ifconfig/stp.py b/python/vyos/ifconfig/stp.py
new file mode 100644
index 000000000..5e83206c2
--- /dev/null
+++ b/python/vyos/ifconfig/stp.py
@@ -0,0 +1,70 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos.ifconfig.interface import Interface
+
+from vyos.validate import assert_positive
+
+
+class STP:
+ """
+ A spanning-tree capable interface. This applies only to bridge port member
+ interfaces!
+ """
+
+ @classmethod
+ def enable (cls, adaptee):
+ adaptee._sysfs_set = {**adaptee._sysfs_set, **cls._sysfs_set}
+ adaptee.set_path_cost = cls.set_path_cost
+ adaptee.set_path_priority = cls.set_path_priority
+ return adaptee
+
+ _sysfs_set = {
+ 'path_cost': {
+ # XXX: we should set a maximum
+ 'validate': assert_positive,
+ 'location': '/sys/class/net/{ifname}/brport/path_cost',
+ 'errormsg': '{ifname} is not a bridge port member'
+ },
+ 'path_priority': {
+ # XXX: we should set a maximum
+ 'validate': assert_positive,
+ 'location': '/sys/class/net/{ifname}/brport/priority',
+ 'errormsg': '{ifname} is not a bridge port member'
+ },
+ }
+
+ def set_path_cost(self, cost):
+ """
+ Set interface path cost, only relevant for STP enabled interfaces
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_path_cost(4)
+ """
+ self.set_interface('path_cost', cost)
+
+ def set_path_priority(self, priority):
+ """
+ Set interface path priority, only relevant for STP enabled interfaces
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_path_priority(4)
+ """
+ self.set_interface('path_priority', priority)
diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py
new file mode 100644
index 000000000..05060669a
--- /dev/null
+++ b/python/vyos/ifconfig/tunnel.py
@@ -0,0 +1,324 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+# https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/
+# https://community.hetzner.com/tutorials/linux-setup-gre-tunnel
+
+
+from copy import deepcopy
+
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.afi import IP4, IP6
+from vyos.validate import assert_list
+
+def enable_to_on(value):
+ if value == 'enable':
+ return 'on'
+ if value == 'disable':
+ return 'off'
+ raise ValueError(f'expect enable or disable but got "{value}"')
+
+
+@Interface.register
+class _Tunnel(Interface):
+ """
+ _Tunnel: private base class for tunnels
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/tunnel.c
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/ip6tunnel.c
+ """
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'tunnel',
+ 'prefixes': ['tun',],
+ 'bridgeable': True,
+ },
+ }
+
+ # TODO: This is surely used for more than tunnels
+ # TODO: could be refactored elsewhere
+ _command_set = {**Interface._command_set, **{
+ 'multicast': {
+ 'validate': lambda v: assert_list(v, ['enable', 'disable']),
+ 'convert': enable_to_on,
+ 'shellcmd': 'ip link set dev {ifname} multicast {value}',
+ },
+ 'allmulticast': {
+ 'validate': lambda v: assert_list(v, ['enable', 'disable']),
+ 'convert': enable_to_on,
+ 'shellcmd': 'ip link set dev {ifname} allmulticast {value}',
+ },
+ }}
+
+ # use for "options" and "updates"
+ # If an key is only in the options list, it can only be set at creation time
+ # the create comand will only be make using the key in options
+
+ # If an option is in the updates list, it can be updated
+ # upon, the creation, all key not yet applied will be updated
+
+ # multicast/allmulticast can not be part of the create command
+
+ # options matrix:
+ # with ip = 4, we have multicast
+ # wiht ip = 6, nothing
+ # with tunnel = 4, we have tos, ttl, key
+ # with tunnel = 6, we have encaplimit, hoplimit, tclass, flowlabel
+
+ # TODO: For multicast, it is allowed on IP6IP6 and Sit6RD
+ # TODO: to match vyatta but it should be checked for correctness
+
+ updates = []
+
+ create = ''
+ change = ''
+ delete = ''
+
+ ip = [] # AFI of the families which can be used in the tunnel
+ tunnel = 0 # invalid - need to be set by subclasses
+
+ def __init__(self, ifname, **config):
+ self.config = deepcopy(config) if config else {}
+ super().__init__(ifname, **config)
+
+ def _create(self):
+ # add " option-name option-name-value ..." for all options set
+ options = " ".join(["{} {}".format(k, self.config[k])
+ for k in self.options if k in self.config and self.config[k]])
+ self._cmd('{} {}'.format(self.create.format(**self.config), options))
+ self.set_admin_state('down')
+
+ def _delete(self):
+ self.set_admin_state('down')
+ cmd = self.delete.format(**self.config)
+ return self._cmd(cmd)
+
+ def set_interface(self, option, value):
+ try:
+ return Interface.set_interface(self, option, value)
+ except Exception:
+ pass
+
+ if value == '':
+ # remove the value so that it is not used
+ self.config.pop(option, '')
+
+ if self.change:
+ self._cmd('{} {} {}'.format(
+ self.change.format(**self.config), option, value))
+ return True
+
+ @classmethod
+ def get_config(cls):
+ return dict(zip(cls.options, ['']*len(cls.options)))
+
+
+class GREIf(_Tunnel):
+ """
+ GRE: Generic Routing Encapsulation
+
+ For more information please refer to:
+ RFC1701, RFC1702, RFC2784
+ https://tools.ietf.org/html/rfc2784
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre.c
+ """
+
+ ip = [IP4, IP6]
+ tunnel = IP4
+
+ default = {'type': 'gre'}
+ required = ['local', ] # mGRE is a GRE without remote endpoint
+
+ options = ['local', 'remote', 'ttl', 'tos', 'key']
+ updates = ['local', 'remote', 'ttl', 'tos',
+ 'multicast', 'allmulticast']
+
+ create = 'ip tunnel add {ifname} mode {type}'
+ change = 'ip tunnel cha {ifname}'
+ delete = 'ip tunnel del {ifname}'
+
+
+# GreTap also called GRE Bridge
+class GRETapIf(_Tunnel):
+ """
+ GRETapIF: GreIF using TAP instead of TUN
+
+ https://en.wikipedia.org/wiki/TUN/TAP
+ """
+
+ # no multicast, ttl or tos for gretap
+
+ ip = [IP4, ]
+ tunnel = IP4
+
+ default = {'type': 'gretap'}
+ required = ['local', ]
+
+ options = ['local', 'remote', ]
+ updates = []
+
+ create = 'ip link add {ifname} type {type}'
+ change = ''
+ delete = 'ip link del {ifname}'
+
+
+class IP6GREIf(_Tunnel):
+ """
+ IP6Gre: IPv6 Support for Generic Routing Encapsulation (GRE)
+
+ For more information please refer to:
+ https://tools.ietf.org/html/rfc7676
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre6.c
+ """
+
+ ip = [IP4, IP6]
+ tunnel = IP6
+
+ default = {'type': 'ip6gre'}
+ required = ['local', 'remote']
+
+ options = ['local', 'remote', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel']
+ updates = ['local', 'remote', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel',
+ 'multicast', 'allmulticast']
+
+ create = 'ip tunnel add {ifname} mode {type}'
+ change = 'ip tunnel cha {ifname} mode {type}'
+ delete = 'ip tunnel del {ifname}'
+
+ # using "ip tunnel change" without using "mode" causes errors
+ # sudo ip tunnel add tun100 mode ip6gre local ::1 remote 1::1
+ # sudo ip tunnel cha tun100 hoplimit 100
+ # *** stack smashing detected ** *: < unknown > terminated
+ # sudo ip tunnel cha tun100 local: : 2
+ # Error: an IP address is expected rather than "::2"
+ # works if mode is explicit
+
+
+class IPIPIf(_Tunnel):
+ """
+ IPIP: IP Encapsulation within IP
+
+ For more information please refer to:
+ https://tools.ietf.org/html/rfc2003
+ """
+
+ # IPIP does not allow to pass multicast, unlike GRE
+ # but the interface itself can be set with multicast
+
+ ip = [IP4,]
+ tunnel = IP4
+
+ default = {'type': 'ipip'}
+ required = ['local', 'remote']
+
+ options = ['local', 'remote', 'ttl', 'tos', 'key']
+ updates = ['local', 'remote', 'ttl', 'tos',
+ 'multicast', 'allmulticast']
+
+ create = 'ip tunnel add {ifname} mode {type}'
+ change = 'ip tunnel cha {ifname}'
+ delete = 'ip tunnel del {ifname}'
+
+
+class IPIP6If(_Tunnel):
+ """
+ IPIP6: IPv4 over IPv6 tunnel
+
+ For more information please refer to:
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_ip6tnl.c
+ """
+
+ ip = [IP4,]
+ tunnel = IP6
+
+ default = {'type': 'ipip6'}
+ required = ['local', 'remote']
+
+ options = ['local', 'remote', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel']
+ updates = ['local', 'remote', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel',
+ 'multicast', 'allmulticast']
+
+ create = 'ip -6 tunnel add {ifname} mode {type}'
+ change = 'ip -6 tunnel cha {ifname}'
+ delete = 'ip -6 tunnel del {ifname}'
+
+
+class IP6IP6If(IPIP6If):
+ """
+ IP6IP6: IPv6 over IPv6 tunnel
+
+ For more information please refer to:
+ https://tools.ietf.org/html/rfc2473
+ """
+
+ ip = [IP6,]
+
+ default = {'type': 'ip6ip6'}
+
+
+class SitIf(_Tunnel):
+ """
+ Sit: Simple Internet Transition
+
+ For more information please refer to:
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_iptnl.c
+ """
+
+ ip = [IP6, IP4]
+ tunnel = IP4
+
+ default = {'type': 'sit'}
+ required = ['local', 'remote']
+
+ options = ['local', 'remote', 'ttl', 'tos', 'key']
+ updates = ['local', 'remote', 'ttl', 'tos',
+ 'multicast', 'allmulticast']
+
+ create = 'ip tunnel add {ifname} mode {type}'
+ change = 'ip tunnel cha {ifname}'
+ delete = 'ip tunnel del {ifname}'
+
+
+class Sit6RDIf(SitIf):
+ """
+ Sit6RDIf: Simple Internet Transition with 6RD
+
+ https://en.wikipedia.org/wiki/IPv6_rapid_deployment
+ """
+
+ ip = [IP6,]
+
+ required = ['remote', '6rd-prefix']
+
+ # TODO: check if key can really be used with 6RD
+ options = ['remote', 'ttl', 'tos', 'key', '6rd-prefix', '6rd-relay-prefix']
+ updates = ['remote', 'ttl', 'tos',
+ 'multicast', 'allmulticast']
+
+ def _create(self):
+ # do not call _Tunnel.create, building fully here
+
+ create = 'ip tunnel add {ifname} mode {type} remote {remote}'
+ self._cmd(create.format(**self.config))
+ self.set_interface('state','down')
+
+ set6rd = 'ip tunnel 6rd dev {ifname} 6rd-prefix {6rd-prefix}'
+ if '6rd-relay-prefix' in self.config:
+ set6rd += ' 6rd-relay-prefix {6rd-relay-prefix}'
+ self._cmd(set6rd.format(**self.config))
diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py
new file mode 100644
index 000000000..7b1e00d87
--- /dev/null
+++ b/python/vyos/ifconfig/vlan.py
@@ -0,0 +1,142 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+import os
+import re
+
+from vyos.ifconfig.interface import Interface
+
+
+# This is an internal implementation class
+class VLAN:
+ """
+ This class handels the creation and removal of a VLAN interface. It serves
+ as base class for BondIf and EthernetIf.
+ """
+
+ _novlan_remove = lambda : None
+
+ @classmethod
+ def enable (cls,adaptee):
+ adaptee._novlan_remove = adaptee.remove
+ adaptee.remove = cls.remove
+ adaptee.add_vlan = cls.add_vlan
+ adaptee.del_vlan = cls.del_vlan
+ adaptee.definition['vlan'] = True
+ return adaptee
+
+ def remove(self):
+ """
+ Remove interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('eth0')
+ >>> i.remove()
+ """
+ ifname = self.config['ifname']
+
+ # Do we have sub interfaces (VLANs)? We apply a regex matching
+ # subinterfaces (indicated by a .) of a parent interface.
+ #
+ # As interfaces need to be deleted "in order" starting from Q-in-Q
+ # we delete them first.
+ vlan_ifs = [f for f in os.listdir(r'/sys/class/net')
+ if re.match(ifname + r'(?:\.\d+)(?:\.\d+)', f)]
+
+ for vlan in vlan_ifs:
+ Interface(vlan).remove()
+
+ # After deleting all Q-in-Q interfaces delete other VLAN interfaces
+ # which probably acted as parent to Q-in-Q or have been regular 802.1q
+ # interface.
+ vlan_ifs = [f for f in os.listdir(r'/sys/class/net')
+ if re.match(ifname + r'(?:\.\d+)', f)]
+
+ for vlan in vlan_ifs:
+ # self.__class__ is already VLAN.enabled
+ self.__class__(vlan)._novlan_remove()
+
+ # All subinterfaces are now removed, continue on the physical interface
+ self._novlan_remove()
+
+ def add_vlan(self, vlan_id, ethertype='', ingress_qos='', egress_qos=''):
+ """
+ A virtual LAN (VLAN) is any broadcast domain that is partitioned and
+ isolated in a computer network at the data link layer (OSI layer 2).
+ Use this function to create a new VLAN interface on a given physical
+ interface.
+
+ This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto
+ parameter is used to indicate VLAN type.
+
+ A new object of type VLANIf is returned once the interface has been
+ created.
+
+ @param ethertype: If specified, create 802.1ad or 802.1q Q-in-Q VLAN
+ interface
+ @param ingress_qos: Defines a mapping of VLAN header prio field to the
+ Linux internal packet priority on incoming frames.
+ @param ingress_qos: Defines a mapping of Linux internal packet priority
+ to VLAN header prio field but for outgoing frames.
+
+ Example:
+ >>> from vyos.ifconfig import MACVLANIf
+ >>> i = MACVLANIf('eth0')
+ >>> i.add_vlan(10)
+ """
+ vlan_ifname = self.config['ifname'] + '.' + str(vlan_id)
+ if not os.path.exists(f'/sys/class/net/{vlan_ifname}'):
+ self._vlan_id = int(vlan_id)
+
+ if ethertype:
+ self._ethertype = ethertype
+ ethertype = 'proto {}'.format(ethertype)
+
+ # Optional ingress QOS mapping
+ opt_i = ''
+ if ingress_qos:
+ opt_i = 'ingress-qos-map ' + ingress_qos
+ # Optional egress QOS mapping
+ opt_e = ''
+ if egress_qos:
+ opt_e = 'egress-qos-map ' + egress_qos
+
+ # create interface in the system
+ cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \
+ .format(ifname=self.config['ifname'], vlan=self._vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i)
+ self._cmd(cmd)
+
+ # return new object mapping to the newly created interface
+ # we can now work on this object for e.g. IP address setting
+ # or interface description and so on
+ return self.__class__(vlan_ifname)
+
+ def del_vlan(self, vlan_id):
+ """
+ Remove VLAN interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+
+ Example:
+ >>> from vyos.ifconfig import MACVLANIf
+ >>> i = MACVLANIf('eth0.10')
+ >>> i.del_vlan()
+ """
+ ifname = self.config['ifname']
+ self.__class__(f'{ifname}.{vlan_id}')._novlan_remove()
diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py
new file mode 100644
index 000000000..07d39fcbb
--- /dev/null
+++ b/python/vyos/ifconfig/vtun.py
@@ -0,0 +1,34 @@
+# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class VTunIf(Interface):
+ default = {
+ 'type': 'vtun',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'openvpn',
+ 'prefixes': ['vtun', ],
+ 'bridgeable': True,
+ },
+ }
+
+ # The _create and _delete need to be moved from interface-ppoe to here
diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py
new file mode 100644
index 000000000..f47ae17cc
--- /dev/null
+++ b/python/vyos/ifconfig/vxlan.py
@@ -0,0 +1,106 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from copy import deepcopy
+
+from vyos import ConfigError
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class VXLANIf(Interface):
+ """
+ The VXLAN protocol is a tunnelling protocol designed to solve the
+ problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the
+ size of the identifier is expanded to 24 bits (16777216).
+
+ VXLAN is described by IETF RFC 7348, and has been implemented by a
+ number of vendors. The protocol runs over UDP using a single
+ destination port. This document describes the Linux kernel tunnel
+ device, there is also a separate implementation of VXLAN for
+ Openvswitch.
+
+ Unlike most tunnels, a VXLAN is a 1 to N network, not just point to
+ point. A VXLAN device can learn the IP address of the other endpoint
+ either dynamically in a manner similar to a learning bridge, or make
+ use of statically-configured forwarding entries.
+
+ For more information please refer to:
+ https://www.kernel.org/doc/Documentation/networking/vxlan.txt
+ """
+
+ default = {
+ 'type': 'vxlan',
+ 'group': '',
+ 'port': 8472, # The Linux implementation of VXLAN pre-dates
+ # the IANA's selection of a standard destination port
+ 'remote': '',
+ 'src_address': '',
+ 'src_interface': '',
+ 'vni': 0
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'vxlan',
+ 'prefixes': ['vxlan', ],
+ 'bridgeable': True,
+ }
+ }
+ options = ['group', 'remote', 'src_interface', 'port', 'vni', 'src_address']
+
+ mapping = {
+ 'ifname': 'add',
+ 'vni': 'id',
+ 'port': 'dstport',
+ 'src_address': 'nolearning local',
+ }
+
+ def _create(self):
+ cmdline = set()
+ if self.config['remote']:
+ cmdline = ('ifname', 'type', 'remote', 'src_interface', 'vni', 'port')
+
+ elif self.config['src_address']:
+ cmdline = ('ifname', 'type', 'src_address', 'vni', 'port')
+
+ elif self.config['group'] and self.config['src_interface']:
+ cmdline = ('ifname', 'type', 'group', 'src_interface', 'vni', 'port')
+
+ else:
+ ifname = self.config['ifname']
+ raise ConfigError(
+ f'VXLAN "{ifname}" is missing mandatory underlay interface for a multicast network.')
+
+ cmd = 'ip link'
+ for key in cmdline:
+ value = self.config.get(key, '')
+ if not value:
+ continue
+ cmd += ' {} {}'.format(self.mapping.get(key, key), value)
+
+ self._cmd(cmd)
+
+ @classmethod
+ def get_config(cls):
+ """
+ VXLAN interfaces require a configuration when they are added using
+ iproute2. This static method will provide the configuration dictionary
+ used by this class.
+
+ Example:
+ >> dict = VXLANIf().get_config()
+ """
+ return deepcopy(cls.default)
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
new file mode 100644
index 000000000..e2b8a5924
--- /dev/null
+++ b/python/vyos/ifconfig/wireguard.py
@@ -0,0 +1,222 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+import os
+import time
+from datetime import timedelta
+
+from vyos.config import Config
+from vyos.ifconfig.interface import Interface
+from hurry.filesize import size,alternative
+
+
+@Interface.register
+class WireGuardIf(Interface):
+ default = {
+ 'type': 'wireguard',
+ 'port': 0,
+ 'private-key': None,
+ 'pubkey': None,
+ 'psk': '/dev/null',
+ 'allowed-ips': [],
+ 'fwmark': 0x00,
+ 'endpoint': None,
+ 'keepalive': 0
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'wireguard',
+ 'prefixes': ['wg', ],
+ 'bridgeable': True,
+ }
+ }
+ options = ['port', 'private-key', 'pubkey', 'psk',
+ 'allowed-ips', 'fwmark', 'endpoint', 'keepalive']
+
+ """
+ Wireguard interface class, contains a comnfig dictionary since
+ wireguard VPN is being comnfigured via the wg command rather than
+ writing the config into a file. Otherwise if a pre-shared key is used
+ (symetric enryption key), it would we exposed within multiple files.
+ Currently it's only within the config.boot if the config was saved.
+
+ Example:
+ >>> from vyos.ifconfig import WireGuardIf as wg_if
+ >>> wg_intfc = wg_if("wg01")
+ >>> print (wg_intfc.wg_config)
+ {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0,
+ 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'}
+ >>> wg_intfc.wg_config['keepalive'] = 100
+ >>> print (wg_intfc.wg_config)
+ {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0,
+ 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'}
+ """
+
+ def update(self):
+ if not self.config['private-key']:
+ raise ValueError("private key required")
+ else:
+ # fmask permission check?
+ pass
+
+ cmd = "wg set {} ".format(self.config['ifname'])
+ cmd += "listen-port {} ".format(self.config['port'])
+ cmd += "fwmark {} ".format(str(self.config['fwmark']))
+ cmd += "private-key {} ".format(self.config['private-key'])
+ cmd += "peer {} ".format(self.config['pubkey'])
+ cmd += " preshared-key {} ".format(self.config['psk'])
+ cmd += " allowed-ips "
+ for aip in self.config['allowed-ips']:
+ if aip != self.config['allowed-ips'][-1]:
+ cmd += aip + ","
+ else:
+ cmd += aip
+ if self.config['endpoint']:
+ cmd += " endpoint {}".format(self.config['endpoint'])
+ cmd += " persistent-keepalive {}".format(self.config['keepalive'])
+
+ self._cmd(cmd)
+
+ # remove psk since it isn't required anymore and is saved in the cli
+ # config only !!
+ if self.config['psk'] != '/dev/null':
+ if os.path.exists(self.config['psk']):
+ os.remove(self.config['psk'])
+
+ def remove_peer(self, peerkey):
+ """
+ Remove a peer of an interface, peers are identified by their public key.
+ Giving it a readable name is a vyos feature, to remove a peer the pubkey
+ and the interface is needed, to remove the entry.
+ """
+ cmd = "wg set {0} peer {1} remove".format(
+ self.config['ifname'], str(peerkey))
+ return self._cmd(cmd)
+
+ def op_show_interface(self):
+ wgdump = self._dump().get(
+ self.config['ifname'], None)
+
+ c = Config()
+ c.set_level(["interfaces", "wireguard", self.config['ifname']])
+ description = c.return_effective_value(["description"])
+ ips = c.return_effective_values(["address"])
+
+ print ("interface: {}".format(self.config['ifname']))
+ if (description):
+ print (" description: {}".format(description))
+
+ if (ips):
+ print (" address: {}".format(", ".join(ips)))
+ print (" public key: {}".format(wgdump['public_key']))
+ print (" private key: (hidden)")
+ print (" listening port: {}".format(wgdump['listen_port']))
+ print ()
+
+ for peer in c.list_effective_nodes(["peer"]):
+ if wgdump['peers']:
+ pubkey = c.return_effective_value(["peer", peer, "pubkey"])
+ if pubkey in wgdump['peers']:
+ wgpeer = wgdump['peers'][pubkey]
+
+ print (" peer: {}".format(peer))
+ print (" public key: {}".format(pubkey))
+
+ """ figure out if the tunnel is recently active or not """
+ status = "inactive"
+ if (wgpeer['latest_handshake'] is None):
+ """ no handshake ever """
+ status = "inactive"
+ else:
+ if int(wgpeer['latest_handshake']) > 0:
+ delta = timedelta(seconds=int(
+ time.time() - wgpeer['latest_handshake']))
+ print (" latest handshake: {}".format(delta))
+ if (time.time() - int(wgpeer['latest_handshake']) < (60*5)):
+ """ Five minutes and the tunnel is still active """
+ status = "active"
+ else:
+ """ it's been longer than 5 minutes """
+ status = "inactive"
+ elif int(wgpeer['latest_handshake']) == 0:
+ """ no handshake ever """
+ status = "inactive"
+ print (" status: {}".format(status))
+
+ if wgpeer['endpoint'] is not None:
+ print (" endpoint: {}".format(wgpeer['endpoint']))
+
+ if wgpeer['allowed_ips'] is not None:
+ print (" allowed ips: {}".format(
+ ",".join(wgpeer['allowed_ips']).replace(",", ", ")))
+
+ if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0:
+ rx_size = size(
+ wgpeer['transfer_rx'], system=alternative)
+ tx_size = size(
+ wgpeer['transfer_tx'], system=alternative)
+ print (" transfer: {} received, {} sent".format(
+ rx_size, tx_size))
+
+ if wgpeer['persistent_keepalive'] is not None:
+ print (" persistent keepalive: every {} seconds".format(
+ wgpeer['persistent_keepalive']))
+ print()
+ super().op_show_interface_stats()
+
+ def _dump(self):
+ """Dump wireguard data in a python friendly way."""
+ last_device = None
+ output = {}
+
+ # Dump wireguard connection data
+ _f = self._cmd('wg show all dump')
+ for line in _f.split('\n'):
+ if not line:
+ # Skip empty lines and last line
+ continue
+ items = line.split('\t')
+
+ if last_device != items[0]:
+ # We are currently entering a new node
+ device, private_key, public_key, listen_port, fw_mark = items
+ last_device = device
+
+ output[device] = {
+ 'private_key': None if private_key == '(none)' else private_key,
+ 'public_key': None if public_key == '(none)' else public_key,
+ 'listen_port': int(listen_port),
+ 'fw_mark': None if fw_mark == 'off' else int(fw_mark),
+ 'peers': {},
+ }
+ else:
+ # We are entering a peer
+ device, public_key, preshared_key, endpoint, allowed_ips, latest_handshake, transfer_rx, transfer_tx, persistent_keepalive = items
+ if allowed_ips == '(none)':
+ allowed_ips = []
+ else:
+ allowed_ips = allowed_ips.split('\t')
+ output[device]['peers'][public_key] = {
+ 'preshared_key': None if preshared_key == '(none)' else preshared_key,
+ 'endpoint': None if endpoint == '(none)' else endpoint,
+ 'allowed_ips': allowed_ips,
+ 'latest_handshake': None if latest_handshake == '0' else int(latest_handshake),
+ 'transfer_rx': int(transfer_rx),
+ 'transfer_tx': int(transfer_tx),
+ 'persistent_keepalive': None if persistent_keepalive == 'off' else int(persistent_keepalive),
+ }
+ return output
diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py
new file mode 100644
index 000000000..946ae1642
--- /dev/null
+++ b/python/vyos/ifconfig/wireless.py
@@ -0,0 +1,82 @@
+# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.vlan import VLAN
+
+
+@Interface.register
+@VLAN.enable
+class WiFiIf(Interface):
+ """
+ Handle WIFI/WLAN interfaces.
+ """
+
+ default = {
+ 'type': 'wifi',
+ 'phy': 'phy0'
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'wireless',
+ 'prefixes': ['wlan', ],
+ 'bridgeable': True,
+ }
+ }
+ options = ['phy', 'op_mode']
+
+ def _create(self):
+ # all interfaces will be added in monitor mode
+ cmd = 'iw phy {phy} interface add {ifname} type monitor' \
+ .format(**self.config)
+ self._cmd(cmd)
+
+ # wireless interface is administratively down by default
+ self.set_admin_state('down')
+
+ def _delete(self):
+ cmd = 'iw dev {ifname} del' \
+ .format(**self.config)
+ self._cmd(cmd)
+
+ @staticmethod
+ def get_config():
+ """
+ WiFi interfaces require a configuration when they are added using
+ iw (type/phy). This static method will provide the configuration
+ ictionary used by this class.
+
+ Example:
+ >> conf = WiFiIf().get_config()
+ """
+ config = {
+ 'phy': 'phy0'
+ }
+ return config
+
+
+
+@Interface.register
+class WiFiModemIf(WiFiIf):
+ definition = {
+ **WiFiIf.definition,
+ **{
+ 'section': 'wirelessmodem',
+ 'prefixes': ['wlm', ],
+ }
+ }
diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py
index 8e09db95a..ed22646c1 100644
--- a/python/vyos/ifconfig_vlan.py
+++ b/python/vyos/ifconfig_vlan.py
@@ -13,7 +13,8 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-from vyos.ifconfig import VLANIf
+from netifaces import interfaces
+from vyos import ConfigError
def apply_vlan_config(vlan, config):
"""
@@ -21,7 +22,7 @@ def apply_vlan_config(vlan, config):
to a VLAN interface
"""
- if type(vlan) != type(VLANIf("lo")):
+ if not vlan.definition['vlan']:
raise TypeError()
# get DHCP config dictionary and update values
@@ -63,17 +64,29 @@ def apply_vlan_config(vlan, config):
vlan.set_arp_announce(config['ip_enable_arp_announce'])
# configure ARP ignore
vlan.set_arp_ignore(config['ip_enable_arp_ignore'])
+ # configure Proxy ARP
+ vlan.set_proxy_arp(config['ip_proxy_arp'])
+ # IPv6 address autoconfiguration
+ vlan.set_ipv6_autoconf(config['ipv6_autoconf'])
+ # IPv6 forwarding
+ vlan.set_ipv6_forwarding(config['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ vlan.set_ipv6_dad_messages(config['ipv6_dup_addr_detect'])
# Maximum Transmission Unit (MTU)
vlan.set_mtu(config['mtu'])
+
+ # assign/remove VRF
+ vlan.set_vrf(config['vrf'])
+
# Change VLAN interface MAC address
if config['mac']:
vlan.set_mac(config['mac'])
# enable/disable VLAN interface
if config['disable']:
- vlan.set_state('down')
+ vlan.set_admin_state('down')
else:
- vlan.set_state('up')
+ vlan.set_admin_state('up')
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -83,3 +96,46 @@ def apply_vlan_config(vlan, config):
for addr in config['address']:
vlan.add_addr(addr)
+def verify_vlan_config(config):
+ """
+ Generic function to verify VLAN config consistency. Instead of re-
+ implementing this function in multiple places use single source \o/
+ """
+
+ for vif in config['vif']:
+ # DHCPv6 parameters-only and temporary address are mutually exclusive
+ if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']:
+ raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+
+ vrf_name = vif['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
+ # e.g. wireless interface has no vif_s support
+ # thus we bail out eraly.
+ if 'vif_s' not in config.keys():
+ return
+
+ for vif_s in config['vif_s']:
+ for vif in config['vif']:
+ if vif['id'] == vif_s['id']:
+ raise ConfigError('Can not use identical ID on vif and vif-s interface')
+
+ # DHCPv6 parameters-only and temporary address are mutually exclusive
+ if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']:
+ raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+
+ vrf_name = vif_s['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
+ for vif_c in vif_s['vif_c']:
+ # DHCPv6 parameters-only and temporary address are mutually exclusive
+ if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']:
+ raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+
+ vrf_name = vif_c['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
+
diff --git a/python/vyos/interfaces.py b/python/vyos/interfaces.py
deleted file mode 100644
index 37c093aca..000000000
--- a/python/vyos/interfaces.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# Copyright 2018 VyOS maintainers and contributors <maintainers@vyos.io>
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-import re
-import json
-
-import subprocess
-import netifaces
-
-intf_type_data_file = '/usr/share/vyos/interface-types.json'
-
-def list_interfaces():
- interfaces = netifaces.interfaces()
-
- # Remove "fake" interfaces associated with drivers
- for i in ["dummy0", "ip6tnl0", "tunl0", "ip_vti0", "ip6_vti0"]:
- try:
- interfaces.remove(i)
- except ValueError:
- pass
-
- return interfaces
-
-def list_interfaces_of_type(typ):
- with open(intf_type_data_file, 'r') as f:
- types_data = json.load(f)
-
- all_intfs = list_interfaces()
- if not (typ in types_data.keys()):
- raise ValueError("Unknown interface type: {0}".format(typ))
- else:
- r = re.compile('^{0}\d+'.format(types_data[typ]))
- return list(filter(lambda i: re.match(r, i), all_intfs))
-
-def get_type_of_interface(intf):
- with open(intf_type_data_file, 'r') as f:
- types_data = json.load(f)
-
- for key,val in types_data.items():
- r = re.compile('^{0}\d+'.format(val))
- if re.match(r, intf):
- return key
-
- raise ValueError("No type found for interface name: {0}".format(intf))
-
-def wireguard_dump():
- """Dump wireguard data in a python friendly way."""
- last_device=None
- output = {}
-
- # Dump wireguard connection data
- _f = subprocess.check_output(["wg", "show", "all", "dump"]).decode()
- for line in _f.split('\n'):
- if not line:
- # Skip empty lines and last line
- continue
- items = line.split('\t')
-
- if last_device != items[0]:
- # We are currently entering a new node
- device, private_key, public_key, listen_port, fw_mark = items
- last_device = device
-
- output[device] = {
- 'private_key': None if private_key == '(none)' else private_key,
- 'public_key': None if public_key == '(none)' else public_key,
- 'listen_port': int(listen_port),
- 'fw_mark': None if fw_mark == 'off' else int(fw_mark),
- 'peers': {},
- }
- else:
- # We are entering a peer
- device, public_key, preshared_key, endpoint, allowed_ips, latest_handshake, transfer_rx, transfer_tx, persistent_keepalive = items
- if allowed_ips == '(none)':
- allowed_ips = []
- else:
- allowed_ips = allowed_ips.split('\t')
- output[device]['peers'][public_key] = {
- 'preshared_key': None if preshared_key == '(none)' else preshared_key,
- 'endpoint': None if endpoint == '(none)' else endpoint,
- 'allowed_ips': allowed_ips,
- 'latest_handshake': None if latest_handshake == '0' else int(latest_handshake),
- 'transfer_rx': int(transfer_rx),
- 'transfer_tx': int(transfer_tx),
- 'persistent_keepalive': None if persistent_keepalive == 'off' else int(persistent_keepalive),
- }
- return output
diff --git a/python/vyos/ioctl.py b/python/vyos/ioctl.py
index e57d261e4..cfa75aac6 100644
--- a/python/vyos/ioctl.py
+++ b/python/vyos/ioctl.py
@@ -13,9 +13,11 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+import sys
import os
-import fcntl, struct, sys
-from socket import *
+import socket
+import fcntl
+import struct
SIOCGIFFLAGS = 0x8913
@@ -28,7 +30,7 @@ def get_terminal_size():
def get_interface_flags(intf):
""" Pull the SIOCGIFFLAGS """
nullif = '\0'*256
- sock = socket(AF_INET, SOCK_DGRAM)
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
raw = fcntl.ioctl(sock.fileno(), SIOCGIFFLAGS, intf + nullif)
flags, = struct.unpack('H', raw[16:18])
return flags
diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py
index f05228041..9a5fdef2f 100644
--- a/python/vyos/migrator.py
+++ b/python/vyos/migrator.py
@@ -25,7 +25,7 @@ class MigratorError(Exception):
pass
class Migrator(object):
- def __init__(self, config_file, force=False, set_vintage=None):
+ def __init__(self, config_file, force=False, set_vintage='vyos'):
self._config_file = config_file
self._force = force
self._set_vintage = set_vintage
@@ -61,9 +61,6 @@ class Migrator(object):
if self._set_vintage:
self._config_file_vintage = self._set_vintage
- if not self._config_file_vintage:
- self._config_file_vintage = vyos.defaults.cfg_vintage
-
if self._config_file_vintage not in ['vyatta', 'vyos']:
raise MigratorError("Unknown vintage.")
@@ -204,16 +201,12 @@ class Migrator(object):
return self._changed
class VirtualMigrator(Migrator):
- def __init__(self, config_file, vintage='vyos'):
- super().__init__(config_file, set_vintage = vintage)
-
def run(self):
cfg_file = self._config_file
cfg_versions = self.read_config_file_versions()
if not cfg_versions:
- raise MigratorError("Config file has no version information;"
- " virtual migration not possible.")
+ return
if self.update_vintage():
self._changed = True
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
index f8a21f068..f918461d1 100644
--- a/python/vyos/remote.py
+++ b/python/vyos/remote.py
@@ -17,7 +17,8 @@ import sys
import os
import re
import fileinput
-import subprocess
+
+from vyos.util import cmd, DEVNULL
def check_and_add_host_key(host_name):
@@ -33,10 +34,8 @@ def check_and_add_host_key(host_name):
keyscan_cmd = 'ssh-keyscan -t rsa {} 2>/dev/null'.format(host_name)
try:
- host_key = subprocess.check_output(keyscan_cmd, shell=True,
- stderr=subprocess.DEVNULL,
- universal_newlines=True)
- except subprocess.CalledProcessError as err:
+ host_key = cmd(keyscan_cmd, shell=True, stderr=DEVNULL)
+ except OSError:
sys.exit("Can not get RSA host key")
# libssh2 (jessie; stretch) does not recognize ec host keys, and curl
@@ -64,10 +63,8 @@ def check_and_add_host_key(host_name):
fingerprint_cmd = 'ssh-keygen -lf /dev/stdin <<< "{}"'.format(host_key)
try:
- fingerprint = subprocess.check_output(fingerprint_cmd, shell=True,
- stderr=subprocess.DEVNULL,
- universal_newlines=True)
- except subprocess.CalledProcessError as err:
+ fingerprint = cmd(fingerprint_cmd, shell=True, stderr=DEVNULL)
+ except OSError:
sys.exit("Can not get RSA host key fingerprint.")
print("RSA host key fingerprint is {}".format(fingerprint.split()[1]))
@@ -128,9 +125,8 @@ def get_remote_config(remote_file):
# Try header first, and look for 'OK' or 'Moved' codes:
curl_cmd = 'curl {0} -q -I {1}'.format(redirect_opt, remote_file)
try:
- curl_output = subprocess.check_output(curl_cmd, shell=True,
- universal_newlines=True)
- except subprocess.CalledProcessError:
+ curl_output = cmd(curl_cmd, shell=True)
+ except OSError:
sys.exit(1)
return_vals = re.findall(r'^HTTP\/\d+\.?\d\s+(\d+)\s+(.*)$',
@@ -146,9 +142,6 @@ def get_remote_config(remote_file):
curl_cmd = 'curl {0} -# {1}'.format(redirect_opt, remote_file)
try:
- config_file = subprocess.check_output(curl_cmd, shell=True,
- universal_newlines=True)
- except subprocess.CalledProcessError:
- config_file = None
-
- return config_file
+ return cmd(curl_cmd, shell=True, stderr=None)
+ except OSError:
+ return None
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 67a602f7a..291ce64ea 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -15,15 +15,96 @@
import os
import re
-import getpass
-import grp
-import time
-import subprocess
import sys
+from subprocess import Popen, PIPE, STDOUT, DEVNULL
+
+def debug(flag):
+ # this is to force all new flags to be registered here so that
+ # they can be documented:
+ # - developer: the code will drop into PBD on un-handled exception
+ # - ifconfig: prints command and sysfs access on stdout for interface
+ if flag not in ['developer', 'ifconfig']:
+ return ''
+ return flag if os.path.isfile(f'/tmp/vyos.{flag}.debug') else ''
+
+
+def debug_msg(message, section=''):
+ if debug(section):
+ print(f'DEBUG/{section:<6} {message}')
+
+
+def popen(command, section='', shell=None, input=None, timeout=None, env=None,
+ universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None):
+ """ popen does not raise, returns the output and error code of command """
+ use_shell = shell
+ if shell is None:
+ use_shell = True if ' ' in command else False
+ p = Popen(
+ command,
+ stdout=stdout, stderr=stderr,
+ env=env, shell=use_shell,
+ universal_newlines=universal_newlines,
+ )
+ tmp = p.communicate(input, timeout)[0].strip()
+ debug_msg(f"cmd '{command}'", section)
+ decoded = tmp.decode(decode) if decode else tmp.decode()
+ if decoded:
+ debug_msg(f"returned:\n{decoded}", section)
+ return decoded, p.returncode
+
+
+def run(command, section='', shell=None, input=None, timeout=None, env=None,
+ universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None):
+ """ does not raise exception on error, returns error code """
+ _, code = popen(
+ command, section,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ universal_newlines=universal_newlines,
+ decode=decode,
+ )
+ return code
+
+
+def cmd(command, section='', shell=None, input=None, timeout=None, env=None,
+ universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None,
+ raising=None, message=''):
+ """ does raise exception, returns output of command """
+ decoded, code = popen(
+ command, section,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ universal_newlines=universal_newlines,
+ decode=decode,
+ )
+ if code != 0:
+ feedback = message + '\n' if message else ''
+ feedback += f'failed to run command: {command}\n'
+ feedback += f'returned: {decoded}\n'
+ feedback += f'exit code: {code}'
+ if raising is None:
+ # error code can be recovered with .errno
+ raise OSError(code, feedback)
+ else:
+ raise raising(feedback)
+ return decoded
-import psutil
-import vyos.defaults
+def call(command, section='', shell=None, input=None, timeout=None, env=None,
+ universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None):
+ """ does not raise exception on error, returns error code, print output """
+ out, code = popen(
+ command, section,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ universal_newlines=universal_newlines,
+ decode=decode,
+ )
+ print(out)
+ return code
def read_file(path):
@@ -32,6 +113,36 @@ def read_file(path):
data = f.read().strip()
return data
+
+def chown(path, user, group):
+ """ change file/directory owner """
+ from pwd import getpwnam
+ from grp import getgrnam
+
+ if os.path.exists(path):
+ uid = getpwnam(user).pw_uid
+ gid = getgrnam(group).gr_gid
+ os.chown(path, uid, gid)
+
+def chmod_750(path):
+ """ make file/directory only executable to user and group """
+ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP
+
+ if os.path.exists(path):
+ bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP
+ os.chmod(path, bitmask)
+
+
+def chmod_x(path):
+ """ make file executable """
+ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH
+
+ if os.path.exists(path):
+ bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \
+ S_IROTH | S_IXOTH
+ os.chmod(path, bitmask)
+
+
def colon_separated_to_dict(data_string, uniquekeys=False):
""" Converts a string containing newline-separated entries
of colon-separated key-value pairs into a dict.
@@ -80,11 +191,16 @@ def colon_separated_to_dict(data_string, uniquekeys=False):
return data
+
def process_running(pid_file):
""" Checks if a process with PID in pid_file is running """
+ from psutil import pid_exists
+ if not os.path.isfile(pid_file):
+ return False
with open(pid_file, 'r') as f:
pid = f.read().strip()
- return psutil.pid_exists(int(pid))
+ return pid_exists(int(pid))
+
def seconds_to_human(s, separator=""):
""" Converts number of seconds passed to a human-readable
@@ -125,10 +241,15 @@ def seconds_to_human(s, separator=""):
return result
+
def get_cfg_group_id():
- group_data = grp.getgrnam(vyos.defaults.cfg_group)
+ from grp import getgrnam
+ from vyos.defaults import cfg_group
+
+ group_data = getgrnam(cfg_group)
return group_data.gr_gid
+
def file_is_persistent(path):
if not re.match(r'^(/config|/opt/vyatta/etc/config)', os.path.dirname(path)):
warning = "Warning: file {0} is outside the /config directory\n".format(path)
@@ -137,6 +258,7 @@ def file_is_persistent(path):
else:
return (True, None)
+
def commit_in_progress():
""" Not to be used in normal op mode scripts! """
@@ -154,29 +276,34 @@ def commit_in_progress():
# Since this will be used in scripts that modify the config outside of the CLI
# framework, those knowingly have root permissions.
# For everything else, we add a safeguard.
- id = subprocess.check_output(['/usr/bin/id', '-u']).decode().strip()
- if id != '0':
+ from psutil import process_iter, NoSuchProcess
+ from vyos.defaults import commit_lock
+
+ idu = cmd('/usr/bin/id -u')
+ if idu != '0':
raise OSError("This functions needs root permissions to return correct results")
- for proc in psutil.process_iter():
+ for proc in process_iter():
try:
files = proc.open_files()
if files:
for f in files:
- if f.path == vyos.defaults.commit_lock:
+ if f.path == commit_lock:
return True
- except psutil.NoSuchProcess as err:
+ except NoSuchProcess as err:
# Process died before we could examine it
pass
# Default case
return False
+
def wait_for_commit_lock():
""" Not to be used in normal op mode scripts! """
-
+ from time import sleep
# Very synchronous approach to multiprocessing
while commit_in_progress():
- time.sleep(1)
+ sleep(1)
+
def ask_yes_no(question, default=False) -> bool:
"""Ask a yes/no question via input() and return their answer."""
@@ -196,6 +323,50 @@ def ask_yes_no(question, default=False) -> bool:
def is_admin() -> bool:
"""Look if current user is in sudo group"""
- current_user = getpass.getuser()
- (_, _, _, admin_group_members) = grp.getgrnam('sudo')
+ from getpass import getuser
+ from grp import getgrnam
+ current_user = getuser()
+ (_, _, _, admin_group_members) = getgrnam('sudo')
return current_user in admin_group_members
+
+
+def mac2eui64(mac, prefix=None):
+ """
+ Convert a MAC address to a EUI64 address or, with prefix provided, a full
+ IPv6 address.
+ Thankfully copied from https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3
+ """
+ from ipaddress import ip_network
+ # http://tools.ietf.org/html/rfc4291#section-2.5.1
+ eui64 = re.sub(r'[.:-]', '', mac).lower()
+ eui64 = eui64[0:6] + 'fffe' + eui64[6:]
+ eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:]
+
+ if prefix is None:
+ return ':'.join(re.findall(r'.{4}', eui64))
+ else:
+ try:
+ net = ip_network(prefix, strict=False)
+ euil = int('0x{0}'.format(eui64), 16)
+ return str(net[euil])
+ except: # pylint: disable=bare-except
+ return
+
+def is_bridge_member(interface):
+ """
+ Checks if passed interfaces is part of a bridge device or not.
+
+ Returns a tuple:
+ False, None -> Not part of a bridge
+ True, bridge-name -> If it is assigned to a bridge
+ """
+ from vyos.config import Config
+ c = Config()
+ base = ['interfaces', 'bridge']
+ for bridge in c.list_nodes(base):
+ members = c.list_nodes(base + [bridge, 'member', 'interface'])
+ if interface in members:
+ return (True, bridge)
+
+ return False, None
+
diff --git a/python/vyos/validate.py b/python/vyos/validate.py
index 33c495d91..9d413ffab 100644
--- a/python/vyos/validate.py
+++ b/python/vyos/validate.py
@@ -13,6 +13,7 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+import socket
import netifaces
import ipaddress
@@ -64,51 +65,61 @@ def is_ipv6_link_local(addr):
return False
+def _are_same_ip(one, two):
+ # compare the binary representation of the IP
+ f_one = socket.AF_INET if is_ipv4(one) else socket.AF_INET6
+ s_two = socket.AF_INET if is_ipv4(two) else socket.AF_INET6
+ return socket.inet_pton(f_one, one) == socket.inet_pton(f_one, two)
+
def is_intf_addr_assigned(intf, addr):
+ if '/' in addr:
+ ip,mask = addr.split('/')
+ return _is_intf_addr_assigned(intf, ip, mask)
+ return _is_intf_addr_assigned(intf, addr)
+
+def _is_intf_addr_assigned(intf, address, netmask=''):
"""
Verify if the given IPv4/IPv6 address is assigned to specific interface.
It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR
address 192.0.2.1/24.
"""
- # determine IP version (AF_INET or AF_INET6) depending on passed address
- addr_type = netifaces.AF_INET
- if is_ipv6(addr):
- addr_type = netifaces.AF_INET6
-
# check if the requested address type is configured at all
+ # {
+ # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
+ # 2: [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}],
+ # 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}]
+ # }
try:
- netifaces.ifaddresses(intf)
+ ifaces = netifaces.ifaddresses(intf)
except ValueError as e:
print(e)
return False
- if addr_type in netifaces.ifaddresses(intf).keys():
- # Check every IP address on this interface for a match
- for ip in netifaces.ifaddresses(intf)[addr_type]:
- # Check if it matches to the address requested
- # If passed address contains a '/' indicating a normalized IP
- # address we have to take this into account, too
- if r'/' in addr:
- prefixlen = ''
- if is_ipv6(addr):
- # Note that currently expanded netmasks are not supported. That means
- # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not.
- # see https://docs.python.org/3/library/ipaddress.html
- bits = bin( int(ip['netmask'].replace(':',''), 16) ).count('1')
- prefixlen = '/' + str(bits)
-
- else:
- prefixlen = '/' + str(ipaddress.IPv4Network('0.0.0.0/' + ip['netmask']).prefixlen)
-
- # construct temporary variable holding IPv6 address and netmask
- # in CIDR notation
- tmp = ip['addr'] + prefixlen
- if addr == tmp:
- return True
+ # determine IP version (AF_INET or AF_INET6) depending on passed address
+ addr_type = netifaces.AF_INET if is_ipv4(address) else netifaces.AF_INET6
- elif ip['addr'] == addr:
- return True
+ # Check every IP address on this interface for a match
+ for ip in ifaces.get(addr_type,[]):
+ # ip can have the interface name in the 'addr' field, we need to remove it
+ # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'}
+ ip_addr = ip['addr'].split('%')[0]
+
+ if not _are_same_ip(address, ip_addr):
+ continue
+
+ # we do not have a netmask to compare against, they are the same
+ if netmask == '':
+ return True
+
+ prefixlen = ''
+ if is_ipv4(ip_addr):
+ prefixlen = sum([bin(int(_)).count('1') for _ in ip['netmask'].split('.')])
+ else:
+ prefixlen = sum([bin(int(_,16)).count('1') for _ in ip['netmask'].split(':') if _])
+
+ if str(prefixlen) == netmask:
+ return True
return False
@@ -168,3 +179,64 @@ def is_subnet_connected(subnet, primary=False):
return True
return False
+
+
+def assert_boolean(b):
+ if int(b) not in (0, 1):
+ raise ValueError(f'Value {b} out of range')
+
+
+def assert_range(value, lower=0, count=3):
+ if int(value) not in range(lower,lower+count):
+ raise ValueError("Value out of range")
+
+
+def assert_list(s, l):
+ if s not in l:
+ o = ' or '.join([f'"{n}"' for n in l])
+ raise ValueError(f'state must be {o}, got {s}')
+
+
+def assert_number(n):
+ if not str(n).isnumeric():
+ raise ValueError(f'{n} must be a number')
+
+
+def assert_positive(n, smaller=0):
+ assert_number(n)
+ if int(n) < smaller:
+ raise ValueError(f'{n} is smaller than {limit}')
+
+
+def assert_mtu(mtu, min=68, max=9000):
+ assert_number(mtu)
+ if int(mtu) < min or int(mtu) > max:
+ raise ValueError(f'Invalid MTU size: "{mtu}"')
+
+
+def assert_mac(m):
+ split = m.split(':')
+ size = len(split)
+
+ # a mac address consits out of 6 octets
+ if size != 6:
+ raise ValueError(f'wrong number of MAC octets ({size}): {m}')
+
+ octets = []
+ try:
+ for octet in split:
+ octets.append(int(octet, 16))
+ except ValueError:
+ raise ValueError(f'invalid hex number "{octet}" in : {m}')
+
+ # validate against the first mac address byte if it's a multicast
+ # address
+ if octets[0] & 1:
+ raise ValueError(f'{m} is a multicast MAC address')
+
+ # overall mac address is not allowed to be 00:00:00:00:00:00
+ if sum(octets) == 0:
+ raise ValueError('00:00:00:00:00:00 is not a valid MAC address')
+
+ if octets[:5] == (0, 0, 94, 0, 1):
+ raise ValueError(f'{m} is a VRRP MAC address')
diff --git a/python/vyos/version.py b/python/vyos/version.py
index 383efbc1e..d51a940d6 100644
--- a/python/vyos/version.py
+++ b/python/vyos/version.py
@@ -44,7 +44,7 @@ def get_version_data(file=version_file):
file (str): path to the version file
Returns:
- dict: version data
+ dict: version data, if it can not be found and empty dict
The optional ``file`` argument comes in handy in upgrade scripts
that need to retrieve information from images other than the running image.
@@ -52,17 +52,20 @@ def get_version_data(file=version_file):
is an implementation detail and may change in the future, while the interface
of this module will stay the same.
"""
- with open(file, 'r') as f:
- version_data = json.load(f)
- return version_data
+ try:
+ with open(file, 'r') as f:
+ version_data = json.load(f)
+ return version_data
+ except FileNotFoundError:
+ return {}
def get_version(file=None):
"""
- Get the version number
+ Get the version number, or an empty string if it could not be determined
"""
version_data = None
if file:
version_data = get_version_data(file=file)
else:
version_data = get_version_data()
- return version_data["version"]
+ return version_data.get('version','')
diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates
index 0383c8982..689d19ece 100755
--- a/scripts/build-command-op-templates
+++ b/scripts/build-command-op-templates
@@ -111,7 +111,7 @@ def get_properties(p):
for i in lists:
comp_exprs.append("echo \"{0}\"".format(i.text))
for i in paths:
- comp_exprs.append("/bin/cli-shell-api listNodes {0}".format(i.text))
+ comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\"".format(i.text))
for i in scripts:
comp_exprs.append("{0}".format(i.text))
comp_help = " && ".join(comp_exprs)
diff --git a/scripts/build-command-templates b/scripts/build-command-templates
index dbf4ad9c5..c6534a6d8 100755
--- a/scripts/build-command-templates
+++ b/scripts/build-command-templates
@@ -264,6 +264,11 @@ def process_node(n, tmpl_dir):
if node_type == "tagNode":
props["tag"] = "True"
+ if node_type != "leafNode":
+ if "multi" in props:
+ raise ValueError("<multi/> tag is only allowed in <leafNode>")
+ if "valueless" in props:
+ raise ValueError("<valueless/> is only allowed in <leafNode>")
nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def")
if not os.path.exists(nodedef_path):
diff --git a/src/completion/list_dumpable_interfaces.py b/src/completion/list_dumpable_interfaces.py
index 53ee89633..101c92fbe 100755
--- a/src/completion/list_dumpable_interfaces.py
+++ b/src/completion/list_dumpable_interfaces.py
@@ -3,12 +3,10 @@
# Extract the list of interfaces available for traffic dumps from tcpdump -D
import re
-import subprocess
-if __name__ == '__main__':
- out = subprocess.check_output(['/usr/sbin/tcpdump', '-D']).decode().strip()
- out = out.split("\n")
+from vyos.util import cmd
+if __name__ == '__main__':
+ out = cmd('/usr/sbin/tcpdump -D').split('\n')
intfs = " ".join(map(lambda s: re.search(r'\d+\.(\S+)\s', s).group(1), out))
-
print(intfs)
diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py
index 8cd59917d..98b32797a 100755
--- a/src/completion/list_interfaces.py
+++ b/src/completion/list_interfaces.py
@@ -2,7 +2,7 @@
import sys
import argparse
-import vyos.interfaces
+from vyos.ifconfig import Interface
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
@@ -13,35 +13,39 @@ group.add_argument("-bo", "--bondable", action="store_true", help="List all bond
args = parser.parse_args()
+# XXX: Need to be rewritten using the data in the class definition
+# XXX: It can be done once vti and input are moved into vyos
+# XXX: We store for each class what type they are (broadcast, bridgeabe, ...)
+
if args.type:
try:
- interfaces = vyos.interfaces.list_interfaces_of_type(args.type)
+ interfaces = Interface.listing(args.type)
except ValueError as e:
print(e, file=sys.stderr)
print("")
elif args.broadcast:
- eth = vyos.interfaces.list_interfaces_of_type("ethernet")
- bridge = vyos.interfaces.list_interfaces_of_type("bridge")
- bond = vyos.interfaces.list_interfaces_of_type("bonding")
+ eth = Interface.listing("ethernet")
+ bridge = Interface.listing("bridge")
+ bond = Interface.listing("bonding")
interfaces = eth + bridge + bond
elif args.bridgeable:
- eth = vyos.interfaces.list_interfaces_of_type("ethernet")
- bond = vyos.interfaces.list_interfaces_of_type("bonding")
- l2tpv3 = vyos.interfaces.list_interfaces_of_type("l2tpv3")
- openvpn = vyos.interfaces.list_interfaces_of_type("openvpn")
- wireless = vyos.interfaces.list_interfaces_of_type("wireless")
- tunnel = vyos.interfaces.list_interfaces_of_type("tunnel")
- vxlan = vyos.interfaces.list_interfaces_of_type("vxlan")
- geneve = vyos.interfaces.list_interfaces_of_type("geneve")
+ eth = Interface.listing("ethernet")
+ bond = Interface.listing("bonding")
+ l2tpv3 = Interface.listing("l2tpv3")
+ openvpn = Interface.listing("openvpn")
+ wireless = Interface.listing("wireless")
+ tunnel = Interface.listing("tunnel")
+ vxlan = Interface.listing("vxlan")
+ geneve = Interface.listing("geneve")
interfaces = eth + bond + l2tpv3 + openvpn + vxlan + tunnel + wireless + geneve
elif args.bondable:
interfaces = []
- eth = vyos.interfaces.list_interfaces_of_type("ethernet")
+ eth = Interface.listing("ethernet")
# we need to filter out VLAN interfaces identified by a dot (.) in their name
for intf in eth:
@@ -49,6 +53,6 @@ elif args.bondable:
interfaces.append(intf)
else:
- interfaces = vyos.interfaces.list_interfaces()
+ interfaces = Interface.listing()
print(" ".join(interfaces))
diff --git a/src/completion/list_local.py b/src/completion/list_local.py
new file mode 100755
index 000000000..40cc95f1e
--- /dev/null
+++ b/src/completion/list_local.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+import json
+import argparse
+
+from vyos.util import cmd
+
+# [{"ifindex":1,"ifname":"lo","flags":["LOOPBACK","UP","LOWER_UP"],"mtu":65536,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"loopback","address":"00:00:00:00:00:00","broadcast":"00:00:00:00:00:00","addr_info":[{"family":"inet","local":"127.0.0.1","prefixlen":8,"scope":"host","label":"lo","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"::1","prefixlen":128,"scope":"host","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":2,"ifname":"eth0","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:fa:12:53","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet","local":"10.0.2.15","prefixlen":24,"broadcast":"10.0.2.255","scope":"global","label":"eth0","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"fe80::a00:27ff:fefa:1253","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":3,"ifname":"eth1","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:0d:25:dc","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fe0d:25dc","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":4,"ifname":"eth2","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:68:d0:b1","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fe68:d0b1","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":5,"ifname":"eth3","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:f0:17:c5","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fef0:17c5","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]}]
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ group = parser.add_mutually_exclusive_group()
+
+ out = cmd('ip -j address show')
+ data = json.loads(out)
+
+
+ interfaces = []
+ for interface in data:
+ if not 'addr_info' in interface:
+ continue
+ interfaces.extend(interface['addr_info'])
+
+ print(' '.join([interface['local'] for interface in interfaces if 'local' in interface]))
diff --git a/src/completion/list_openvpn_clients.py b/src/completion/list_openvpn_clients.py
index 828ce6b5e..17b0c7008 100755
--- a/src/completion/list_openvpn_clients.py
+++ b/src/completion/list_openvpn_clients.py
@@ -18,7 +18,7 @@ import os
import sys
import argparse
-from vyos.interfaces import list_interfaces_of_type
+from vyos.ifconfig import Interface
def get_client_from_interface(interface):
clients = []
@@ -50,7 +50,7 @@ if __name__ == "__main__":
if args.interface:
clients = get_client_from_interface(args.interface)
elif args.all:
- for interface in list_interfaces_of_type("openvpn"):
+ for interface in Interface.listing("openvpn"):
clients += get_client_from_interface(interface)
print(" ".join(clients))
diff --git a/src/completion/list_wlm_peers.sh b/src/completion/list_wlm_peers.sh
new file mode 100755
index 000000000..12dd00650
--- /dev/null
+++ b/src/completion/list_wlm_peers.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+if [ -d /etc/ppp/peers ]; then
+ cd /etc/ppp/peers
+ ls wlm*
+fi
diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py
index a7af9cc68..4ca5a858a 100755
--- a/src/conf_mode/accel_l2tp.py
+++ b/src/conf_mode/accel_l2tp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 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
@@ -13,20 +13,21 @@
#
# 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 sys
import os
import re
-import subprocess
import jinja2
import socket
import time
-import syslog as sl
+
+from jinja2 import FileSystemLoader, Environment
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import run
+
pidfile = r'/var/run/accel_l2tp.pid'
l2tp_cnf_dir = r'/etc/accel-ppp/l2tp'
@@ -34,526 +35,363 @@ chap_secrets = l2tp_cnf_dir + '/chap-secrets'
l2tp_conf = l2tp_cnf_dir + '/l2tp.config'
# accel-pppd -d -c /etc/accel-ppp/l2tp/l2tp.config -p /var/run/accel_l2tp.pid
-### config path creation
+# config path creation
if not os.path.exists(l2tp_cnf_dir):
- os.makedirs(l2tp_cnf_dir)
- sl.syslog(sl.LOG_NOTICE, l2tp_cnf_dir + " created")
-
-l2tp_config = '''
-### generated by accel_l2tp.py ###
-[modules]
-log_syslog
-l2tp
-chap-secrets
-{% for proto in authentication['auth_proto']: %}
-{{proto}}
-{% endfor%}
-{% if authentication['mode'] == 'radius' %}
-radius
-{% endif -%}
-ippool
-shaper
-ipv6pool
-ipv6_nd
-ipv6_dhcp
-
-[core]
-thread-count={{thread_cnt}}
-
-[log]
-syslog=accel-l2tp,daemon
-copy=1
-level=5
-
-{% if dns %}
-[dns]
-{% if dns[0] %}
-dns1={{dns[0]}}
-{% endif %}
-{% if dns[1] %}
-dns2={{dns[1]}}
-{% endif %}
-{% endif -%}
-
-{% if dnsv6 %}
-[ipv6-dns]
-{% for srv in dnsv6: %}
-{{srv}}
-{% endfor %}
-{% endif %}
-
-{% if wins %}
-[wins]
-{% if wins[0] %}
-wins1={{wins[0]}}
-{% endif %}
-{% if wins[1] %}
-wins2={{wins[1]}}
-{% endif %}
-{% endif -%}
-
-[l2tp]
-verbose=1
-ifname=l2tp%d
-ppp-max-mtu={{mtu}}
-mppe={{authentication['mppe']}}
-{% if outside_addr %}
-bind={{outside_addr}}
-{% endif %}
-{% if lns_shared_secret %}
-secret={{lns_shared_secret}}
-{% endif %}
-
-[client-ip-range]
-0.0.0.0/0
-
-{% if (client_ip_pool) or (client_ip_subnets) %}
-[ip-pool]
-{% if client_ip_pool %}
-{{client_ip_pool}}
-{% endif -%}
-{% if client_ip_subnets %}
-{% for sn in client_ip_subnets %}
-{{sn}}
-{% endfor -%}
-{% endif %}
-{% endif %}
-{% if gateway_address %}
-gw-ip-address={{gateway_address}}
-{% endif %}
-
-{% if authentication['mode'] == 'local' %}
-[chap-secrets]
-chap-secrets=/etc/accel-ppp/l2tp/chap-secrets
-{% if gateway_address %}
-gw-ip-address={{gateway_address}}
-{% endif %}
-{% endif %}
-
-[ppp]
-verbose=1
-check-ip=1
-single-session=replace
-{% if idle_timeout %}
-lcp-echo-timeout={{idle_timeout}}
-{% endif %}
-{% if ppp_options['lcp-echo-interval'] %}
-lcp-echo-interval={{ppp_options['lcp-echo-interval']}}
-{% else %}
-lcp-echo-interval=30
-{% endif %}
-{% if ppp_options['lcp-echo-failure'] %}
-lcp-echo-failure={{ppp_options['lcp-echo-failure']}}
-{% else %}
-lcp-echo-failure=3
-{% endif %}
-{% if ccp_disable %}
-ccp=0
-{% endif %}
-{% if client_ipv6_pool %}
-ipv6=allow
-{% endif %}
-
-{% if authentication['mode'] == 'radius' %}
-[radius]
-{% for rsrv in authentication['radiussrv']: %}
-server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\
-req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\
-fail-time={{authentication['radiussrv'][rsrv]['fail-time']}}
-{% endfor %}
-{% if authentication['radiusopt']['timeout'] %}
-timeout={{authentication['radiusopt']['timeout']}}
-{% endif %}
-{% if authentication['radiusopt']['acct-timeout'] %}
-acct-timeout={{authentication['radiusopt']['acct-timeout']}}
-{% endif %}
-{% if authentication['radiusopt']['max-try'] %}
-max-try={{authentication['radiusopt']['max-try']}}
-{% endif %}
-{% if authentication['radiusopt']['nas-id'] %}
-nas-identifier={{authentication['radiusopt']['nas-id']}}
-{% endif %}
-{% if authentication['radius_source_address'] %}
-nas-ip-address={{authentication['radius_source_address']}}
-{% endif -%}
-{% if authentication['radiusopt']['dae-srv'] %}
-dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\
-{{authentication['radiusopt']['dae-srv']['port']}},\
-{{authentication['radiusopt']['dae-srv']['secret']}}
-{% endif -%}
-gw-ip-address={{gateway_address}}
-verbose=1
-{% endif -%}
-
-{% if client_ipv6_pool %}
-[ipv6-pool]
-{% for prfx in client_ipv6_pool.prefix: %}
-{{prfx}}
-{% endfor %}
-{% for prfx in client_ipv6_pool.delegate_prefix: %}
-delegate={{prfx}}
-{% endfor %}
-{% endif %}
-
-{% if client_ipv6_pool['delegate_prefix'] %}
-[ipv6-dhcp]
-verbose=1
-{% endif %}
-
-{% if authentication['radiusopt']['shaper'] %}
-[shaper]
-verbose=1
-attr={{authentication['radiusopt']['shaper']['attr']}}
-{% if authentication['radiusopt']['shaper']['vendor'] %}
-vendor={{authentication['radiusopt']['shaper']['vendor']}}
-{% endif -%}
-{% endif %}
-
-[cli]
-tcp=127.0.0.1:2004
-sessions-columns=ifname,username,calling-sid,ip,{{ip6_column}}{{ip6_dp_column}}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime
-
-'''
-
-### l2tp chap secrets
-chap_secrets_conf = '''
-# username server password acceptable local IP addresses shaper
-{% for user in authentication['local-users'] %}
-{% if authentication['local-users'][user]['state'] == 'enabled' %}
-{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %}
-{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\
-{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}}
-{% else %}
-{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}
-{% endif %}
-{% endif %}
-{% endfor %}
-'''
+ os.makedirs(l2tp_cnf_dir)
###
# inline helper functions
###
# depending on hw and threads, daemon needs a little to start
# if it takes longer than 100 * 0.5 secs, exception is being raised
-# not sure if that's the best way to check it, but it worked so far quite well
+# not sure if that's the best way to check it, but it worked so far quite well
###
+
+
def chk_con():
- cnt = 0
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- while True:
- try:
- s.connect(("127.0.0.1", 2004))
- break
- except ConnectionRefusedError:
- time.sleep(0.5)
- cnt +=1
- if cnt == 100:
- raise("failed to start l2tp server")
- break
-
-### chap_secrets file if auth mode local
-def write_chap_secrets(c):
- tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True)
- chap_secrets_txt = tmpl.render(c)
- old_umask = os.umask(0o077)
- open(chap_secrets,'w').write(chap_secrets_txt)
- os.umask(old_umask)
- sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written')
-
-def accel_cmd(cmd=''):
- if not cmd:
- return None
- try:
- ret = subprocess.check_output(['/usr/bin/accel-cmd','-p','2004',cmd]).decode().strip()
- return ret
- except:
- return 1
+ cnt = 0
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ while True:
+ try:
+ s.connect(("127.0.0.1", 2004))
+ break
+ except ConnectionRefusedError:
+ time.sleep(0.5)
+ cnt += 1
+ if cnt == 100:
+ raise("failed to start l2tp server")
+ break
+
+
+def _accel_cmd(command):
+ return run(f'/usr/bin/accel-cmd -p 2004 {command}')
###
# inline helper functions end
###
+
def get_config():
- c = Config()
- if not c.exists('vpn l2tp remote-access '):
- return None
-
- c.set_level('vpn l2tp remote-access')
- config_data = {
- 'authentication' : {
- 'mode' : 'local',
- 'local-users' : {
+ c = Config()
+ if not c.exists('vpn l2tp remote-access '):
+ return None
+
+ c.set_level('vpn l2tp remote-access')
+ config_data = {
+ 'authentication': {
+ 'mode': 'local',
+ 'local-users': {
+ },
+ 'radiussrv': {},
+ 'radiusopt': {},
+ 'auth_proto': [],
+ 'mppe': 'prefer'
},
- 'radiussrv' : {},
- 'radiusopt' : {},
- 'auth_proto' : [],
- 'mppe' : 'prefer'
- },
- 'outside_addr' : '',
- 'gateway_address' : '10.255.255.0',
- 'dns' : [],
- 'dnsv6' : [],
- 'wins' : [],
- 'client_ip_pool' : None,
- 'client_ip_subnets' : [],
- 'client_ipv6_pool' : {},
- 'mtu' : '1436',
- 'ip6_column' : '',
- 'ip6_dp_column' : '',
- 'ppp_options' : {},
- }
-
- ### general options ###
-
- if c.exists('dns-servers server-1'):
- config_data['dns'].append( c.return_value('dns-servers server-1'))
- if c.exists('dns-servers server-2'):
- config_data['dns'].append( c.return_value('dns-servers server-2'))
- if c.exists('dnsv6-servers'):
- for dns6_server in c.return_values('dnsv6-servers'):
- config_data['dnsv6'].append(dns6_server)
- if c.exists('wins-servers server-1'):
- config_data['wins'].append( c.return_value('wins-servers server-1'))
- if c.exists('wins-servers server-2'):
- config_data['wins'].append( c.return_value('wins-servers server-2'))
- if c.exists('outside-address'):
- config_data['outside_addr'] = c.return_value('outside-address')
-
- ### auth local
- if c.exists('authentication mode local'):
- if c.exists('authentication local-users username'):
- for usr in c.list_nodes('authentication local-users username'):
- config_data['authentication']['local-users'].update(
- {
- usr : {
- 'passwd' : '',
- 'state' : 'enabled',
- 'ip' : '*',
- 'upload' : None,
- 'download' : None
- }
- }
- )
-
- if c.exists('authentication local-users username ' + usr + ' password'):
- config_data['authentication']['local-users'][usr]['passwd'] = c.return_value('authentication local-users username ' + usr + ' password')
- if c.exists('authentication local-users username ' + usr + ' disable'):
- config_data['authentication']['local-users'][usr]['state'] = 'disable'
- if c.exists('authentication local-users username ' + usr + ' static-ip'):
- config_data['authentication']['local-users'][usr]['ip'] = c.return_value('authentication local-users username ' + usr + ' static-ip')
- if c.exists('authentication local-users username ' + usr + ' rate-limit download'):
- config_data['authentication']['local-users'][usr]['download'] = c.return_value('authentication local-users username ' + usr + ' rate-limit download')
- if c.exists('authentication local-users username ' + usr + ' rate-limit upload'):
- config_data['authentication']['local-users'][usr]['upload'] = c.return_value('authentication local-users username ' + usr + ' rate-limit upload')
-
- ### authentication mode radius servers and settings
-
- if c.exists('authentication mode radius'):
- config_data['authentication']['mode'] = 'radius'
- rsrvs = c.list_nodes('authentication radius server')
- for rsrv in rsrvs:
- if c.return_value('authentication radius server ' + rsrv + ' fail-time') == None:
- ftime = '0'
- else:
- ftime = str(c.return_value('authentication radius server ' + rsrv + ' fail-time'))
- if c.return_value('authentication radius-server ' + rsrv + ' req-limit') == None:
- reql = '0'
- else:
- reql = str(c.return_value('authentication radius server ' + rsrv + ' req-limit'))
-
- config_data['authentication']['radiussrv'].update(
- {
- rsrv : {
- 'secret' : c.return_value('authentication radius server ' + rsrv + ' key'),
- 'fail-time' : ftime,
- 'req-limit' : reql
- }
- }
- )
- ### Source ip address feature
- if c.exists('authentication radius source-address'):
- config_data['authentication']['radius_source_address'] = c.return_value('authentication radius source-address')
-
- #### advanced radius-setting
- if c.exists('authentication radius acct-timeout'):
- config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius acct-timeout')
- if c.exists('authentication radius max-try'):
- config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius max-try')
- if c.exists('authentication radius timeout'):
- config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius timeout')
- if c.exists('authentication radius nas-identifier'):
- config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius nas-identifier')
- if c.exists('authentication radius dae-server'):
- # Set default dae-server port if not defined
- if c.exists('authentication radius dae-server port'):
- dae_server_port = c.return_value('authentication radius dae-server port')
- else:
- dae_server_port = "3799"
- config_data['authentication']['radiusopt'].update(
- {
- 'dae-srv' : {
- 'ip-addr' : c.return_value('authentication radius dae-server ip-address'),
- 'port' : dae_server_port,
- 'secret' : str(c.return_value('authentication radius dae-server secret'))
- }
- }
- )
- #### filter-id is the internal accel default if attribute is empty
- #### set here as default for visibility which may change in the future
- if c.exists('authentication radius rate-limit enable'):
- if not c.exists('authentication radius rate-limit attribute'):
- config_data['authentication']['radiusopt']['shaper'] = {
- 'attr' : 'Filter-Id'
- }
- else:
- config_data['authentication']['radiusopt']['shaper'] = {
- 'attr' : c.return_value('authentication radius rate-limit attribute')
- }
- if c.exists('authentication radius rate-limit vendor'):
- config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius rate-limit vendor')
-
- if c.exists('client-ip-pool'):
- if c.exists('client-ip-pool start') and c.exists('client-ip-pool stop'):
- config_data['client_ip_pool'] = c.return_value('client-ip-pool start') + '-' + re.search('[0-9]+$', c.return_value('client-ip-pool stop')).group(0)
-
- if c.exists('client-ip-pool subnet'):
- config_data['client_ip_subnets'] = c.return_values('client-ip-pool subnet')
-
- if c.exists('client-ipv6-pool prefix'):
- config_data['client_ipv6_pool']['prefix'] = c.return_values('client-ipv6-pool prefix')
- config_data['ip6_column'] = 'ip6,'
- if c.exists('client-ipv6-pool delegate-prefix'):
- config_data['client_ipv6_pool']['delegate_prefix'] = c.return_values('client-ipv6-pool delegate-prefix')
- config_data['ip6_dp_column'] = 'ip6-dp,'
-
- if c.exists('mtu'):
- config_data['mtu'] = c.return_value('mtu')
-
- ### gateway address
- if c.exists('gateway-address'):
- config_data['gateway_address'] = c.return_value('gateway-address')
- else:
- ### calculate gw-ip-address
- if c.exists('client-ip-pool start'):
- ### use start ip as gw-ip-address
- config_data['gateway_address'] = c.return_value('client-ip-pool start')
- elif c.exists('client-ip-pool subnet'):
- ### use first ip address from first defined pool
- lst_ip = re.findall("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", c.return_values('client-ip-pool subnet')[0])
- config_data['gateway_address'] = lst_ip[0]
-
- if c.exists('authentication require'):
- auth_mods = {'pap' : 'pap','chap' : 'auth_chap_md5', 'mschap' : 'auth_mschap_v1', 'mschap-v2' : 'auth_mschap_v2'}
- for proto in c.return_values('authentication require'):
- config_data['authentication']['auth_proto'].append(auth_mods[proto])
- else:
- config_data['authentication']['auth_proto'] = ['auth_mschap_v2']
-
- if c.exists('authentication mppe'):
- config_data['authentication']['mppe'] = c.return_value('authentication mppe')
-
- if c.exists('idle'):
- config_data['idle_timeout'] = c.return_value('idle')
-
- ### LNS secret
- if c.exists('lns shared-secret'):
- config_data['lns_shared_secret'] = c.return_value('lns shared-secret')
-
- if c.exists('ccp-disable'):
- config_data['ccp_disable'] = True
-
- ### ppp_options
- ppp_options = {}
- if c.exists('ppp-options'):
- if c.exists('ppp-options lcp-echo-failure'):
- ppp_options['lcp-echo-failure'] = c.return_value('ppp-options lcp-echo-failure')
- if c.exists('ppp-options lcp-echo-interval'):
- ppp_options['lcp-echo-interval'] = c.return_value('ppp-options lcp-echo-interval')
-
- if len(ppp_options) !=0:
- config_data['ppp_options'] = ppp_options
-
- return config_data
+ 'outside_addr': '',
+ 'gateway_address': '10.255.255.0',
+ 'dns': [],
+ 'dnsv6': [],
+ 'wins': [],
+ 'client_ip_pool': None,
+ 'client_ip_subnets': [],
+ 'client_ipv6_pool': {},
+ 'mtu': '1436',
+ 'ip6_column': '',
+ 'ip6_dp_column': '',
+ 'ppp_options': {},
+ }
+
+ ### general options ###
+
+ if c.exists('dns-servers server-1'):
+ config_data['dns'].append(c.return_value('dns-servers server-1'))
+ if c.exists('dns-servers server-2'):
+ config_data['dns'].append(c.return_value('dns-servers server-2'))
+ if c.exists('dnsv6-servers'):
+ for dns6_server in c.return_values('dnsv6-servers'):
+ config_data['dnsv6'].append(dns6_server)
+ if c.exists('wins-servers server-1'):
+ config_data['wins'].append(c.return_value('wins-servers server-1'))
+ if c.exists('wins-servers server-2'):
+ config_data['wins'].append(c.return_value('wins-servers server-2'))
+ if c.exists('outside-address'):
+ config_data['outside_addr'] = c.return_value('outside-address')
+
+ # auth local
+ if c.exists('authentication mode local'):
+ if c.exists('authentication local-users username'):
+ for usr in c.list_nodes('authentication local-users username'):
+ config_data['authentication']['local-users'].update(
+ {
+ usr: {
+ 'passwd': '',
+ 'state': 'enabled',
+ 'ip': '*',
+ 'upload': None,
+ 'download': None
+ }
+ }
+ )
+
+ if c.exists('authentication local-users username ' + usr + ' password'):
+ config_data['authentication']['local-users'][usr]['passwd'] = c.return_value(
+ 'authentication local-users username ' + usr + ' password')
+ if c.exists('authentication local-users username ' + usr + ' disable'):
+ config_data['authentication']['local-users'][usr]['state'] = 'disable'
+ if c.exists('authentication local-users username ' + usr + ' static-ip'):
+ config_data['authentication']['local-users'][usr]['ip'] = c.return_value(
+ 'authentication local-users username ' + usr + ' static-ip')
+ if c.exists('authentication local-users username ' + usr + ' rate-limit download'):
+ config_data['authentication']['local-users'][usr]['download'] = c.return_value(
+ 'authentication local-users username ' + usr + ' rate-limit download')
+ if c.exists('authentication local-users username ' + usr + ' rate-limit upload'):
+ config_data['authentication']['local-users'][usr]['upload'] = c.return_value(
+ 'authentication local-users username ' + usr + ' rate-limit upload')
+
+ # authentication mode radius servers and settings
+
+ if c.exists('authentication mode radius'):
+ config_data['authentication']['mode'] = 'radius'
+ rsrvs = c.list_nodes('authentication radius server')
+ for rsrv in rsrvs:
+ if c.return_value('authentication radius server ' + rsrv + ' fail-time') == None:
+ ftime = '0'
+ else:
+ ftime = str(c.return_value(
+ 'authentication radius server ' + rsrv + ' fail-time'))
+ if c.return_value('authentication radius-server ' + rsrv + ' req-limit') == None:
+ reql = '0'
+ else:
+ reql = str(c.return_value(
+ 'authentication radius server ' + rsrv + ' req-limit'))
+
+ config_data['authentication']['radiussrv'].update(
+ {
+ rsrv: {
+ 'secret': c.return_value('authentication radius server ' + rsrv + ' key'),
+ 'fail-time': ftime,
+ 'req-limit': reql
+ }
+ }
+ )
+ # Source ip address feature
+ if c.exists('authentication radius source-address'):
+ config_data['authentication']['radius_source_address'] = c.return_value(
+ 'authentication radius source-address')
+
+ # advanced radius-setting
+ if c.exists('authentication radius acct-timeout'):
+ config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value(
+ 'authentication radius acct-timeout')
+ if c.exists('authentication radius max-try'):
+ config_data['authentication']['radiusopt']['max-try'] = c.return_value(
+ 'authentication radius max-try')
+ if c.exists('authentication radius timeout'):
+ config_data['authentication']['radiusopt']['timeout'] = c.return_value(
+ 'authentication radius timeout')
+ if c.exists('authentication radius nas-identifier'):
+ config_data['authentication']['radiusopt']['nas-id'] = c.return_value(
+ 'authentication radius nas-identifier')
+ if c.exists('authentication radius dae-server'):
+ # Set default dae-server port if not defined
+ if c.exists('authentication radius dae-server port'):
+ dae_server_port = c.return_value(
+ 'authentication radius dae-server port')
+ else:
+ dae_server_port = "3799"
+ config_data['authentication']['radiusopt'].update(
+ {
+ 'dae-srv': {
+ 'ip-addr': c.return_value('authentication radius dae-server ip-address'),
+ 'port': dae_server_port,
+ 'secret': str(c.return_value('authentication radius dae-server secret'))
+ }
+ }
+ )
+ # filter-id is the internal accel default if attribute is empty
+ # set here as default for visibility which may change in the future
+ if c.exists('authentication radius rate-limit enable'):
+ if not c.exists('authentication radius rate-limit attribute'):
+ config_data['authentication']['radiusopt']['shaper'] = {
+ 'attr': 'Filter-Id'
+ }
+ else:
+ config_data['authentication']['radiusopt']['shaper'] = {
+ 'attr': c.return_value('authentication radius rate-limit attribute')
+ }
+ if c.exists('authentication radius rate-limit vendor'):
+ config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value(
+ 'authentication radius rate-limit vendor')
+
+ if c.exists('client-ip-pool'):
+ if c.exists('client-ip-pool start') and c.exists('client-ip-pool stop'):
+ config_data['client_ip_pool'] = c.return_value(
+ 'client-ip-pool start') + '-' + re.search('[0-9]+$', c.return_value('client-ip-pool stop')).group(0)
+
+ if c.exists('client-ip-pool subnet'):
+ config_data['client_ip_subnets'] = c.return_values(
+ 'client-ip-pool subnet')
+
+ if c.exists('client-ipv6-pool prefix'):
+ config_data['client_ipv6_pool']['prefix'] = c.return_values(
+ 'client-ipv6-pool prefix')
+ config_data['ip6_column'] = 'ip6,'
+ if c.exists('client-ipv6-pool delegate-prefix'):
+ config_data['client_ipv6_pool']['delegate_prefix'] = c.return_values(
+ 'client-ipv6-pool delegate-prefix')
+ config_data['ip6_dp_column'] = 'ip6-dp,'
+
+ if c.exists('mtu'):
+ config_data['mtu'] = c.return_value('mtu')
+
+ # gateway address
+ if c.exists('gateway-address'):
+ config_data['gateway_address'] = c.return_value('gateway-address')
+ else:
+ # calculate gw-ip-address
+ if c.exists('client-ip-pool start'):
+ # use start ip as gw-ip-address
+ config_data['gateway_address'] = c.return_value(
+ 'client-ip-pool start')
+ elif c.exists('client-ip-pool subnet'):
+ # use first ip address from first defined pool
+ lst_ip = re.findall("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", c.return_values(
+ 'client-ip-pool subnet')[0])
+ config_data['gateway_address'] = lst_ip[0]
+
+ if c.exists('authentication require'):
+ auth_mods = {'pap': 'pap', 'chap': 'auth_chap_md5',
+ 'mschap': 'auth_mschap_v1', 'mschap-v2': 'auth_mschap_v2'}
+ for proto in c.return_values('authentication require'):
+ config_data['authentication']['auth_proto'].append(
+ auth_mods[proto])
+ else:
+ config_data['authentication']['auth_proto'] = ['auth_mschap_v2']
+
+ if c.exists('authentication mppe'):
+ config_data['authentication']['mppe'] = c.return_value(
+ 'authentication mppe')
+
+ if c.exists('idle'):
+ config_data['idle_timeout'] = c.return_value('idle')
+
+ # LNS secret
+ if c.exists('lns shared-secret'):
+ config_data['lns_shared_secret'] = c.return_value('lns shared-secret')
+
+ if c.exists('ccp-disable'):
+ config_data['ccp_disable'] = True
+
+ # ppp_options
+ ppp_options = {}
+ if c.exists('ppp-options'):
+ if c.exists('ppp-options lcp-echo-failure'):
+ ppp_options['lcp-echo-failure'] = c.return_value(
+ 'ppp-options lcp-echo-failure')
+ if c.exists('ppp-options lcp-echo-interval'):
+ ppp_options['lcp-echo-interval'] = c.return_value(
+ 'ppp-options lcp-echo-interval')
+
+ if len(ppp_options) != 0:
+ config_data['ppp_options'] = ppp_options
+
+ return config_data
+
def verify(c):
- if c == None:
- return None
-
- if c['authentication']['mode'] == 'local':
- if not c['authentication']['local-users']:
- raise ConfigError('l2tp-server authentication local-users required')
- for usr in c['authentication']['local-users']:
- if not c['authentication']['local-users'][usr]['passwd']:
- raise ConfigError('user ' + usr + ' requires a password')
-
- if c['authentication']['mode'] == 'radius':
- if len(c['authentication']['radiussrv']) == 0:
- raise ConfigError('radius server required')
- for rsrv in c['authentication']['radiussrv']:
- if c['authentication']['radiussrv'][rsrv]['secret'] == None:
- raise ConfigError('radius server ' + rsrv + ' needs a secret configured')
-
- ### check for the existence of a client ip pool
- if not c['client_ip_pool'] and not c['client_ip_subnets']:
- raise ConfigError("set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool")
-
- ## check ipv6
- if 'delegate_prefix' in c['client_ipv6_pool'] and not 'prefix' in c['client_ipv6_pool']:
- raise ConfigError("\"set vpn l2tp remote-access client-ipv6-pool prefix\" required for delegate-prefix ")
-
- if len(c['dnsv6']) > 3:
- raise ConfigError("Maximum allowed dnsv6-servers addresses is 3")
+ if c == None:
+ return None
+
+ if c['authentication']['mode'] == 'local':
+ if not c['authentication']['local-users']:
+ raise ConfigError(
+ 'l2tp-server authentication local-users required')
+ for usr in c['authentication']['local-users']:
+ if not c['authentication']['local-users'][usr]['passwd']:
+ raise ConfigError('user ' + usr + ' requires a password')
+
+ if c['authentication']['mode'] == 'radius':
+ if len(c['authentication']['radiussrv']) == 0:
+ raise ConfigError('radius server required')
+ for rsrv in c['authentication']['radiussrv']:
+ if c['authentication']['radiussrv'][rsrv]['secret'] == None:
+ raise ConfigError('radius server ' + rsrv +
+ ' needs a secret configured')
+
+ # check for the existence of a client ip pool
+ if not c['client_ip_pool'] and not c['client_ip_subnets']:
+ raise ConfigError(
+ "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool")
+
+ # check ipv6
+ if 'delegate_prefix' in c['client_ipv6_pool'] and not 'prefix' in c['client_ipv6_pool']:
+ raise ConfigError(
+ "\"set vpn l2tp remote-access client-ipv6-pool prefix\" required for delegate-prefix ")
+
+ if len(c['dnsv6']) > 3:
+ raise ConfigError("Maximum allowed dnsv6-servers addresses is 3")
+
def generate(c):
- if c == None:
- return None
-
- ### accel-cmd reload doesn't work so any change results in a restart of the daemon
- try:
- if os.cpu_count() == 1:
- c['thread_cnt'] = 1
- else:
- c['thread_cnt'] = int(os.cpu_count()/2)
- except KeyError:
- if os.cpu_count() == 1:
- c['thread_cnt'] = 1
- else:
- c['thread_cnt'] = int(os.cpu_count()/2)
+ if c == None:
+ return None
- tmpl = jinja2.Template(l2tp_config, trim_blocks=True)
- config_text = tmpl.render(c)
- open(l2tp_conf,'w').write(config_text)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'l2tp')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
- if c['authentication']['local-users']:
- write_chap_secrets(c)
+ # accel-cmd reload doesn't work so any change results in a restart of the daemon
+ try:
+ if os.cpu_count() == 1:
+ c['thread_cnt'] = 1
+ else:
+ c['thread_cnt'] = int(os.cpu_count()/2)
+ except KeyError:
+ if os.cpu_count() == 1:
+ c['thread_cnt'] = 1
+ else:
+ c['thread_cnt'] = int(os.cpu_count()/2)
+
+ tmpl = env.get_template('l2tp.config.tmpl')
+ config_text = tmpl.render(c)
+ open(l2tp_conf, 'w').write(config_text)
+
+ if c['authentication']['local-users']:
+ tmpl = env.get_template('chap-secrets.tmpl')
+ chap_secrets_txt = tmpl.render(c)
+ old_umask = os.umask(0o077)
+ open(chap_secrets, 'w').write(chap_secrets_txt)
+ os.umask(old_umask)
+
+ return c
- return c
def apply(c):
- if c == None:
- if os.path.exists(pidfile):
- accel_cmd('shutdown hard')
- if os.path.exists(pidfile):
- os.remove(pidfile)
- return None
-
- if not os.path.exists(pidfile):
- ret = subprocess.call(['/usr/sbin/accel-pppd','-c',l2tp_conf,'-p',pidfile,'-d'])
- chk_con()
- if ret !=0 and os.path.exists(pidfile):
- os.remove(pidfile)
- raise ConfigError('accel-pppd failed to start')
- else:
- ### if gw ip changes, only restart doesn't work
- accel_cmd('restart')
- sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart")
+ if c == None:
+ if os.path.exists(pidfile):
+ _accel_cmd('shutdown hard')
+ if os.path.exists(pidfile):
+ os.remove(pidfile)
+ return None
+
+ if not os.path.exists(pidfile):
+ ret = run(f'/usr/sbin/accel-pppd -c {l2tp_conf} -p {pidfile} -d')
+ chk_con()
+ if ret != 0 and os.path.exists(pidfile):
+ os.remove(pidfile)
+ raise ConfigError('accel-pppd failed to start')
+ else:
+ # if gw ip changes, only restart doesn't work
+ _accel_cmd('restart')
+
if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/accel_sstp.py b/src/conf_mode/accel_sstp.py
deleted file mode 100755
index 1317a32db..000000000
--- a/src/conf_mode/accel_sstp.py
+++ /dev/null
@@ -1,469 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018 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 sys
-import os
-import re
-import subprocess
-import jinja2
-import socket
-import time
-import syslog as sl
-
-from vyos.config import Config
-from vyos import ConfigError
-
-pidfile = r'/var/run/accel_sstp.pid'
-sstp_cnf_dir = r'/etc/accel-ppp/sstp'
-chap_secrets = sstp_cnf_dir + '/chap-secrets'
-sstp_conf = sstp_cnf_dir + '/sstp.config'
-ssl_cert_dir = r'/config/user-data/sstp'
-
-### config path creation
-if not os.path.exists(sstp_cnf_dir):
- os.makedirs(sstp_cnf_dir)
- sl.syslog(sl.LOG_NOTICE, sstp_cnf_dir + " created")
-
-if not os.path.exists(ssl_cert_dir):
- os.makedirs(ssl_cert_dir)
- sl.syslog(sl.LOG_NOTICE, ssl_cert_dir + " created")
-
-sstp_config = '''
-### generated by accel_sstp.py ###
-[modules]
-log_syslog
-sstp
-ippool
-shaper
-{% if authentication['mode'] == 'local' %}
-chap-secrets
-{% endif -%}
-{% for proto in authentication['auth_proto'] %}
-{{proto}}
-{% endfor %}
-{% if authentication['mode'] == 'radius' %}
-radius
-{% endif %}
-
-[core]
-thread-count={{thread_cnt}}
-
-[common]
-single-session=replace
-
-[log]
-syslog=accel-sstp,daemon
-copy=1
-level=5
-
-[client-ip-range]
-disable
-
-[sstp]
-verbose=1
-accept=ssl
-{% if certs %}
-ssl-ca-file=/config/user-data/sstp/{{certs['ca']}}
-ssl-pemfile=/config/user-data/sstp/{{certs['server-cert']}}
-ssl-keyfile=/config/user-data/sstp/{{certs['server-key']}}
-{% endif %}
-
-{%if ip_pool %}
-[ip-pool]
-gw-ip-address={{gw}}
-{% for sn in ip_pool %}
-{{sn}}
-{% endfor %}
-{% endif %}
-
-{% if dnsv4 %}
-[dns]
-{% if dnsv4['primary'] %}
-dns1={{dnsv4['primary']}}
-{% endif -%}
-{% if dnsv4['secondary'] %}
-dns2={{dnsv4['secondary']}}
-{% endif -%}
-{% endif %}
-
-{% if authentication['mode'] == 'local' %}
-[chap-secrets]
-chap-secrets=/etc/accel-ppp/sstp/chap-secrets
-{% endif %}
-
-{%- if authentication['mode'] == 'radius' %}
-[radius]
-verbose=1
-{% for rsrv in authentication['radius-srv']: %}
-server={{rsrv}},{{authentication['radius-srv'][rsrv]['secret']}},\
-req-limit={{authentication['radius-srv'][rsrv]['req-limit']}},\
-fail-time={{authentication['radius-srv'][rsrv]['fail-time']}}
-{% endfor -%}
-{% if authentication['radiusopt']['acct-timeout'] %}
-acct-timeout={{authentication['radiusopt']['acct-timeout']}}
-{% endif -%}
-{% if authentication['radiusopt']['timeout'] %}
-timeout={{authentication['radiusopt']['timeout']}}
-{% endif -%}
-{% if authentication['radiusopt']['max-try'] %}
-max-try={{authentication['radiusopt']['max-try']}}
-{% endif -%}
-{% if authentication['radiusopt']['nas-id'] %}
-nas-identifier={{authentication['radiusopt']['nas-id']}}
-{% endif -%}
-{% if authentication['radiusopt']['nas-ip'] %}
-nas-ip-address={{authentication['radiusopt']['nas-ip']}}
-{% endif -%}
-{% if authentication['radiusopt']['dae-srv'] %}
-dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\
-{{authentication['radiusopt']['dae-srv']['port']}},\
-{{authentication['radiusopt']['dae-srv']['secret']}}
-{% endif -%}
-{% endif %}
-
-[ppp]
-verbose=1
-check-ip=1
-{% if mtu %}
-mtu={{mtu}}
-{% endif -%}
-{% if ppp['mppe'] %}
-mppe={{ppp['mppe']}}
-{% endif -%}
-{% if ppp['lcp-echo-interval'] %}
-lcp-echo-interval={{ppp['lcp-echo-interval']}}
-{% endif -%}
-{% if ppp['lcp-echo-failure'] %}
-lcp-echo-failure={{ppp['lcp-echo-failure']}}
-{% endif -%}
-{% if ppp['lcp-echo-timeout'] %}
-lcp-echo-timeout={{ppp['lcp-echo-timeout']}}
-{% endif %}
-
-{% if authentication['radiusopt']['shaper'] %}
-[shaper]
-verbose=1
-attr={{authentication['radiusopt']['shaper']['attr']}}
-{% if authentication['radiusopt']['shaper']['vendor'] %}
-vendor={{authentication['radiusopt']['shaper']['vendor']}}
-{% endif -%}
-{% endif %}
-
-[cli]
-tcp=127.0.0.1:2005
-'''
-
-### sstp chap secrets
-chap_secrets_conf = '''
-# username server password acceptable local IP addresses shaper
-{% for user in authentication['local-users'] %}
-{% if authentication['local-users'][user]['state'] == 'enabled' %}
-{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %}
-{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\
-{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}}
-{% else %}
-{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}
-{% endif %}
-{% endif %}
-{% endfor %}
-'''
-###
-# inline helper functions
-###
-# depending on hw and threads, daemon needs a little to start
-# if it takes longer than 100 * 0.5 secs, exception is being raised
-# not sure if that's the best way to check it, but it worked so far quite well
-###
-def chk_con():
- cnt = 0
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- while True:
- try:
- s.connect(("127.0.0.1", 2005))
- s.close()
- break
- except ConnectionRefusedError:
- time.sleep(0.5)
- cnt +=1
- if cnt == 100:
- raise("failed to start sstp server")
- break
-
-### chap_secrets file if auth mode local
-def write_chap_secrets(c):
- tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True)
- chap_secrets_txt = tmpl.render(c)
- old_umask = os.umask(0o077)
- open(chap_secrets,'w').write(chap_secrets_txt)
- os.umask(old_umask)
- sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written')
-
-def accel_cmd(cmd=''):
- if not cmd:
- return None
- try:
- ret = subprocess.check_output(['/usr/bin/accel-cmd','-p','2005',cmd]).decode().strip()
- return ret
- except:
- return 1
-
-#### check ig local-ip is in client pool subnet
-
-
-###
-# inline helper functions end
-###
-
-def get_config():
- c = Config()
- if not c.exists('service sstp-server'):
- return None
-
- c.set_level('service sstp-server')
-
- config_data = {
- 'authentication' : {
- 'local-users' : {
- },
- 'mode' : 'local',
- 'auth_proto' : [],
- 'radius-srv' : {},
- 'radiusopt' : {},
- 'dae-srv' : {}
- },
- 'certs' : {
- 'ca' : None,
- 'server-key' : None,
- 'server-cert' : None
- },
- 'ip_pool' : [],
- 'gw' : None,
- 'dnsv4' : {},
- 'mtu' : None,
- 'ppp' : {},
- }
-
- ### local auth
- if c.exists('authentication mode local'):
- if c.exists('authentication local-users'):
- for usr in c.list_nodes('authentication local-users username'):
- config_data['authentication']['local-users'].update(
- {
- usr : {
- 'passwd' : None,
- 'state' : 'enabled',
- 'ip' : '*',
- 'upload' : None,
- 'download' : None
- }
- }
- )
- if c.exists('authentication local-users username ' + usr + ' password'):
- config_data['authentication']['local-users'][usr]['passwd'] = c.return_value('authentication local-users username ' + usr + ' password')
- if c.exists('authentication local-users username ' + usr + ' disable'):
- config_data['authentication']['local-users'][usr]['state'] = 'disable'
- if c.exists('authentication local-users username ' + usr + ' static-ip'):
- config_data['authentication']['local-users'][usr]['ip'] = c.return_value('authentication local-users username ' + usr + ' static-ip')
- if c.exists('authentication local-users username ' + usr + ' rate-limit download'):
- config_data['authentication']['local-users'][usr]['download'] = c.return_value('authentication local-users username ' + usr + ' rate-limit download')
- if c.exists('authentication local-users username ' + usr + ' rate-limit upload'):
- config_data['authentication']['local-users'][usr]['upload'] = c.return_value('authentication local-users username ' + usr + ' rate-limit upload')
-
- if c.exists('authentication protocols'):
- auth_mods = {'pap' : 'pap','chap' : 'auth_chap_md5', 'mschap' : 'auth_mschap_v1', 'mschap-v2' : 'auth_mschap_v2'}
- for proto in c.return_values('authentication protocols'):
- config_data['authentication']['auth_proto'].append(auth_mods[proto])
- else:
- config_data['authentication']['auth_proto'] = ['auth_mschap_v2']
-
- #### RADIUS auth and settings
- if c.exists('authentication mode radius'):
- config_data['authentication']['mode'] = c.return_value('authentication mode')
- if c.exists('authentication radius-server'):
- for rsrv in c.list_nodes('authentication radius-server'):
- config_data['authentication']['radius-srv'][rsrv] = {}
- if c.exists('authentication radius-server ' + rsrv + ' secret'):
- config_data['authentication']['radius-srv'][rsrv]['secret'] = c.return_value('authentication radius-server ' + rsrv + ' secret')
- else:
- config_data['authentication']['radius-srv'][rsrv]['secret'] = None
- if c.exists('authentication radius-server ' + rsrv + ' fail-time'):
- config_data['authentication']['radius-srv'][rsrv]['fail-time'] = c.return_value('authentication radius-server ' + rsrv + ' fail-time')
- else:
- config_data['authentication']['radius-srv'][rsrv]['fail-time'] = 0
- if c.exists('authentication radius-server ' + rsrv + ' req-limit'):
- config_data['authentication']['radius-srv'][rsrv]['req-limit'] = c.return_value('authentication radius-server ' + rsrv + ' req-limit')
- else:
- config_data['authentication']['radius-srv'][rsrv]['req-limit'] = 0
-
- #### advanced radius-setting
- if c.exists('authentication radius-settings'):
- if c.exists('authentication radius-settings acct-timeout'):
- config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout')
- if c.exists('authentication radius-settings max-try'):
- config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius-settings max-try')
- if c.exists('authentication radius-settings timeout'):
- config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius-settings timeout')
- if c.exists('authentication radius-settings nas-identifier'):
- config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius-settings nas-identifier')
- if c.exists('authentication radius-settings nas-ip-address'):
- config_data['authentication']['radiusopt']['nas-ip'] = c.return_value('authentication radius-settings nas-ip-address')
- if c.exists('authentication radius-settings dae-server'):
- config_data['authentication']['radiusopt'].update(
- {
- 'dae-srv' : {
- 'ip-addr' : c.return_value('authentication radius-settings dae-server ip-address'),
- 'port' : c.return_value('authentication radius-settings dae-server port'),
- 'secret' : str(c.return_value('authentication radius-settings dae-server secret'))
- }
- }
- )
- if c.exists('authentication radius-settings rate-limit enable'):
- if not c.exists('authentication radius-settings rate-limit attribute'):
- config_data['authentication']['radiusopt']['shaper'] = { 'attr' : 'Filter-Id' }
- else:
- config_data['authentication']['radiusopt']['shaper'] = {
- 'attr' : c.return_value('authentication radius-settings rate-limit attribute')
- }
- if c.exists('authentication radius-settings rate-limit vendor'):
- config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius-settings rate-limit vendor')
-
- if c.exists('sstp-settings ssl-certs ca'):
- config_data['certs']['ca'] = c.return_value('sstp-settings ssl-certs ca')
- if c.exists('sstp-settings ssl-certs server-cert'):
- config_data['certs']['server-cert'] = c.return_value('sstp-settings ssl-certs server-cert')
- if c.exists('sstp-settings ssl-certs server-key'):
- config_data['certs']['server-key'] = c.return_value('sstp-settings ssl-certs server-key')
-
- if c.exists('network-settings client-ip-settings subnet'):
- config_data['ip_pool'] = c.return_values('network-settings client-ip-settings subnet')
- if c.exists('network-settings client-ip-settings gateway-address'):
- config_data['gw'] = c.return_value('network-settings client-ip-settings gateway-address')
- if c.exists('network-settings dns-server primary-dns'):
- config_data['dnsv4']['primary'] = c.return_value('network-settings dns-server primary-dns')
- if c.exists('network-settings dns-server secondary-dns'):
- config_data['dnsv4']['secondary'] = c.return_value('network-settings dns-server secondary-dns')
- if c.exists('network-settings mtu'):
- config_data['mtu'] = c.return_value('network-settings mtu')
-
- #### ppp
- if c.exists('ppp-settings mppe'):
- config_data['ppp']['mppe'] = c.return_value('ppp-settings mppe')
- if c.exists('ppp-settings lcp-echo-failure'):
- config_data['ppp']['lcp-echo-failure'] = c.return_value('ppp-settings lcp-echo-failure')
- if c.exists('ppp-settings lcp-echo-interval'):
- config_data['ppp']['lcp-echo-interval'] = c.return_value('ppp-settings lcp-echo-interval')
- if c.exists('ppp-settings lcp-echo-timeout'):
- config_data['ppp']['lcp-echo-timeout'] = c.return_value('ppp-settings lcp-echo-timeout')
-
- return config_data
-
-def verify(c):
- if c == None:
- return None
- ### vertify auth settings
- if c['authentication']['mode'] == 'local':
- if not c['authentication']['local-users']:
- raise ConfigError('sstp-server authentication local-users required')
-
- for usr in c['authentication']['local-users']:
- if not c['authentication']['local-users'][usr]['passwd']:
- raise ConfigError('user ' + usr + ' requires a password')
- ### if up/download is set, check that both have a value
- if c['authentication']['local-users'][usr]['upload']:
- if not c['authentication']['local-users'][usr]['download']:
- raise ConfigError('user ' + usr + ' requires download speed value')
- if c['authentication']['local-users'][usr]['download']:
- if not c['authentication']['local-users'][usr]['upload']:
- raise ConfigError('user ' + usr + ' requires upload speed value')
-
- if not c['certs']['ca'] or not c['certs']['server-key'] or not c['certs']['server-cert']:
- raise ConfigError('service sstp-server sstp-settings ssl-certs needs the ssl certificates set up')
- else:
- ssl_path = ssl_cert_dir + '/'
- if not os.path.exists(ssl_path + c['certs']['ca']):
- raise ConfigError('CA {0} doesn\'t exist'.format(ssl_path + c['certs']['ca']))
- if not os.path.exists(ssl_path + c['certs']['server-cert']):
- raise ConfigError('SSL Cert {0} doesn\'t exist'.format(ssl_path + c['certs']['server-cert']))
- if not os.path.exists(ssl_path + c['certs']['server-cert']):
- raise ConfigError('SSL Key {0} doesn\'t exist'.format(ssl_path + c['certs']['server-key']))
-
- if c['authentication']['mode'] == 'radius':
- if len(c['authentication']['radius-srv']) == 0:
- raise ConfigError('service sstp-server authentication radius-server needs a value')
- for rsrv in c['authentication']['radius-srv']:
- if c['authentication']['radius-srv'][rsrv]['secret'] == None:
- raise ConfigError('service sstp-server authentication radius-server {0} secret requires a value'.format(rsrv))
-
- if c['authentication']['mode'] == 'local':
- if not c['ip_pool']:
- print ("WARNING: service sstp-server network-settings client-ip-settings subnet requires a value")
- if not c['gw']:
- print ("WARNING: service sstp-server network-settings client-ip-settings gateway-address requires a value")
-
-def generate(c):
- if c == None:
- return None
-
- ### accel-cmd reload doesn't work so any change results in a restart of the daemon
- try:
- if os.cpu_count() == 1:
- c['thread_cnt'] = 1
- else:
- c['thread_cnt'] = int(os.cpu_count()/2)
- except KeyError:
- if os.cpu_count() == 1:
- c['thread_cnt'] = 1
- else:
- c['thread_cnt'] = int(os.cpu_count()/2)
-
- tmpl = jinja2.Template(sstp_config, trim_blocks=True)
- config_text = tmpl.render(c)
- open(sstp_conf,'w').write(config_text)
-
- if c['authentication']['local-users']:
- write_chap_secrets(c)
-
- return c
-
-def apply(c):
- if c == None:
- if os.path.exists(pidfile):
- accel_cmd('shutdown hard')
- if os.path.exists(pidfile):
- os.remove(pidfile)
- return None
-
- if not os.path.exists(pidfile):
- ret = subprocess.call(['/usr/sbin/accel-pppd','-c',sstp_conf,'-p',pidfile,'-d'])
- chk_con()
- if ret !=0 and os.path.exists(pidfile):
- os.remove(pidfile)
- raise ConfigError('accel-pppd failed to start')
- else:
- accel_cmd('restart')
- sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart")
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py
index aeca08432..fde7dc521 100755
--- a/src/conf_mode/arp.py
+++ b/src/conf_mode/arp.py
@@ -20,9 +20,9 @@ import sys
import os
import re
import syslog as sl
-import subprocess
from vyos.config import Config
+from vyos.util import call
from vyos import ConfigError
arp_cmd = '/usr/sbin/arp'
@@ -82,11 +82,12 @@ def generate(c):
def apply(c):
for ip_addr in c['remove']:
sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr)
- subprocess.call([arp_cmd + ' -d ' + ip_addr + ' >/dev/null 2>&1'], shell=True)
+ call(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1')
for ip_addr in c['update']:
sl.syslog(sl.LOG_NOTICE, "arp -s " + ip_addr + " " + c['update'][ip_addr])
- subprocess.call([arp_cmd + ' -s ' + ip_addr + ' ' + c['update'][ip_addr] ], shell=True)
+ updated = c['update'][ip_addr]
+ call(f'{arp_cmd} -s {ip_addr} {updated}')
if __name__ == '__main__':
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
index 8889e701c..8d4c4a89a 100755
--- a/src/conf_mode/bcast_relay.py
+++ b/src/conf_mode/bcast_relay.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2017 VyOS maintainers and contributors
+# Copyright (C) 2017-2020 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
@@ -13,42 +13,35 @@
#
# 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 sys
import os
import fnmatch
-import jinja2
+
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
config_file = r'/etc/default/udp-broadcast-relay'
-config_tmpl = """
-### Autogenerated by bcast_relay.py ###
-
-# UDP broadcast relay configuration for instance {{ id }}
-{%- if description %}
-# Comment: {{ description }}
-{% endif %}
-DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}"
-
-"""
-
default_config_data = {
'disabled': False,
'instances': []
}
def get_config():
- relay = default_config_data
+ relay = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('service broadcast-relay'):
+ base = ['service', 'broadcast-relay']
+
+ if not conf.exists(base):
return None
else:
- conf.set_level('service broadcast-relay')
+ conf.set_level(base)
# Service can be disabled by user
if conf.exists('disable'):
@@ -58,7 +51,7 @@ def get_config():
# Parse configuration of each individual instance
if conf.exists('id'):
for id in conf.list_nodes('id'):
- conf.set_level('service broadcast-relay id {0}'.format(id))
+ conf.set_level(base + ['id', id])
config = {
'id': id,
'disabled': False,
@@ -69,24 +62,24 @@ def get_config():
}
# Check if individual broadcast relay service is disabled
- if conf.exists('disable'):
+ if conf.exists(['disable']):
config['disabled'] = True
# Source IP of forwarded packets, if empty original senders address is used
- if conf.exists('address'):
- config['address'] = conf.return_value('address')
+ if conf.exists(['address']):
+ config['address'] = conf.return_value(['address'])
# A description for each individual broadcast relay service
- if conf.exists('description'):
- config['description'] = conf.return_value('description')
+ if conf.exists(['description']):
+ config['description'] = conf.return_value(['description'])
# UDP port to listen on for broadcast frames
- if conf.exists('port'):
- config['port'] = conf.return_value('port')
+ if conf.exists(['port']):
+ config['port'] = conf.return_value(['port'])
# Network interfaces to listen on for broadcast frames to be relayed
- if conf.exists('interface'):
- config['interfaces'] = conf.return_values('interface')
+ if conf.exists(['interface']):
+ config['interfaces'] = conf.return_values(['interface'])
relay['instances'].append(config)
@@ -119,6 +112,11 @@ def generate(relay):
if relay is None:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'bcast-relay')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
config_dir = os.path.dirname(config_file)
config_filename = os.path.basename(config_file)
active_configs = []
@@ -148,7 +146,7 @@ def generate(relay):
# configuration filename contains instance id
file = config_file + str(r['id'])
- tmpl = jinja2.Template(config_tmpl)
+ tmpl = env.get_template('udp-broadcast-relay.tmpl')
config_text = tmpl.render(r)
with open(file, 'w') as f:
f.write(config_text)
@@ -157,7 +155,7 @@ def generate(relay):
def apply(relay):
# first stop all running services
- os.system('sudo systemctl stop udp-broadcast-relay@{1..99}')
+ call('sudo systemctl stop udp-broadcast-relay@{1..99}')
if (relay is None) or relay['disabled']:
return None
@@ -167,7 +165,7 @@ def apply(relay):
# Don't start individual instance when it's disabled
if r['disabled']:
continue
- os.system('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id']))
+ call('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id']))
return None
@@ -179,4 +177,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
index a1af2575f..c92d6a4e1 100755
--- a/src/conf_mode/dhcp_relay.py
+++ b/src/conf_mode/dhcp_relay.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,39 +13,19 @@
#
# 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 sys
import os
-import jinja2
+
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
config_file = r'/etc/default/isc-dhcp-relay'
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by dhcp_relay.py ###
-
-# Defaults for isc-dhcp-relay initscript
-# sourced by /etc/init.d/isc-dhcp-relay
-
-#
-# This is a POSIX shell fragment
-#
-
-# What servers should the DHCP relay forward requests to?
-SERVERS="{{ server | join(' ') }}"
-
-# On what interfaces should the DHCP relay (dhrelay) serve DHCP requests?
-INTERFACES="{{ interface | join(' ') }}"
-
-# Additional options that are passed to the DHCP relay daemon?
-OPTIONS="-4 {{ options | join(' ') }}"
-"""
-
default_config_data = {
'interface': [],
'server': [],
@@ -57,23 +37,23 @@ default_config_data = {
def get_config():
relay = default_config_data
conf = Config()
- if not conf.exists('service dhcp-relay'):
+ if not conf.exists(['service', 'dhcp-relay']):
return None
else:
- conf.set_level('service dhcp-relay')
+ conf.set_level(['service', 'dhcp-relay'])
# Network interfaces to listen on
- if conf.exists('interface'):
- relay['interface'] = conf.return_values('interface')
+ if conf.exists(['interface']):
+ relay['interface'] = conf.return_values(['interface'])
# Servers equal to the address of the DHCP server(s)
- if conf.exists('server'):
- relay['server'] = conf.return_values('server')
+ if conf.exists(['server']):
+ relay['server'] = conf.return_values(['server'])
- conf.set_level('service dhcp-relay relay-options')
+ conf.set_level(['service', 'dhcp-relay', 'relay-options'])
- if conf.exists('hop-count'):
- count = '-c ' + conf.return_value('hop-count')
+ if conf.exists(['hop-count']):
+ count = '-c ' + conf.return_value(['hop-count'])
relay['options'].append(count)
# Specify the maximum packet size to send to a DHCPv4/BOOTP server.
@@ -81,8 +61,8 @@ def get_config():
# options while still fitting into the Ethernet MTU size.
#
# Available in DHCPv4 mode only:
- if conf.exists('max-size'):
- size = '-A ' + conf.return_value('max-size')
+ if conf.exists(['max-size']):
+ size = '-A ' + conf.return_value(['max-size'])
relay['options'].append(size)
# Control the handling of incoming DHCPv4 packets which already contain
@@ -94,8 +74,8 @@ def get_config():
# field; it may forward the packet unchanged; or, it may discard it.
#
# Available in DHCPv4 mode only:
- if conf.exists('relay-agents-packets'):
- pkt = '-a -m ' + conf.return_value('relay-agents-packets')
+ if conf.exists(['relay-agents-packets']):
+ pkt = '-a -m ' + conf.return_value(['relay-agents-packets'])
relay['options'].append(pkt)
return relay
@@ -119,7 +99,12 @@ def generate(relay):
if relay is None:
return None
- tmpl = jinja2.Template(config_tmpl)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcp-relay')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('config.tmpl')
config_text = tmpl.render(relay)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -128,10 +113,10 @@ def generate(relay):
def apply(relay):
if relay is not None:
- os.system('sudo systemctl restart isc-dhcp-relay.service')
+ call('sudo systemctl restart isc-dhcp-relay.service')
else:
# DHCP relay support is removed in the commit
- os.system('sudo systemctl stop isc-dhcp-relay.service')
+ call('sudo systemctl stop isc-dhcp-relay.service')
os.unlink(config_file)
return None
@@ -144,4 +129,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index bf86e484b..553247b88 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2019 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,232 +14,26 @@
# 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 sys
import os
-import jinja2
-import socket
-import struct
-
-import vyos.validate
from ipaddress import ip_address, ip_network
+from jinja2 import FileSystemLoader, Environment
+from socket import inet_ntoa
+from struct import pack
+from sys import exit
+
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.validate import is_subnet_connected
from vyos import ConfigError
+from vyos.util import call
+
config_file = r'/etc/dhcp/dhcpd.conf'
lease_file = r'/config/dhcpd.leases'
pid_file = r'/var/run/dhcpd.pid'
daemon_config_file = r'/etc/default/isc-dhcpv4-server'
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by dhcp_server.py ###
-
-# For options please consult the following website:
-# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
-#
-# log-facility local7;
-
-{% if hostfile_update %}
-on release {
- set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
- set ClientIp = binary-to-ascii(10, 8, ".",leased-address);
- set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6));
- set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
- execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain);
-}
-
-on expiry {
- set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
- set ClientIp = binary-to-ascii(10, 8, ".",leased-address);
- set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6));
- set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
- execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain);
-}
-{% endif %}
-{%- if host_decl_name %}
-use-host-decl-names on;
-{%- endif %}
-ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %};
-{% if static_route -%}
-option rfc3442-static-route code 121 = array of integer 8;
-option windows-static-route code 249 = array of integer 8;
-{%- endif %}
-{% if wpad -%}
-option wpad-url code 252 = text;
-{% endif %}
-
-{%- if global_parameters %}
-# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated
-{%- for param in global_parameters %}
-{{ param }}
-{%- endfor -%}
-{%- endif %}
-
-# Failover configuration
-{% for network in shared_network %}
-{%- if not network.disabled -%}
-{%- for subnet in network.subnet %}
-{%- if subnet.failover_name -%}
-failover peer "{{ subnet.failover_name }}" {
-{%- if subnet.failover_status == 'primary' %}
- primary;
- mclt 1800;
- split 128;
-{%- elif subnet.failover_status == 'secondary' %}
- secondary;
-{%- endif %}
- address {{ subnet.failover_local_addr }};
- port 520;
- peer address {{ subnet.failover_peer_addr }};
- peer port 520;
- max-response-delay 30;
- max-unacked-updates 10;
- load balance max seconds 3;
-}
-{% endif -%}
-{% endfor -%}
-{% endif -%}
-{% endfor %}
-
-# Shared network configration(s)
-{% for network in shared_network %}
-{%- if not network.disabled -%}
-shared-network {{ network.name }} {
- {%- if network.authoritative %}
- authoritative;
- {%- endif %}
- {%- if network.network_parameters %}
- # The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated
- {%- for param in network.network_parameters %}
- {{ param }}
- {%- endfor %}
- {%- endif %}
- {%- for subnet in network.subnet %}
- subnet {{ subnet.address }} netmask {{ subnet.netmask }} {
- {%- if subnet.dns_server %}
- option domain-name-servers {{ subnet.dns_server | join(', ') }};
- {%- endif %}
- {%- if subnet.domain_search %}
- option domain-search {{ subnet.domain_search | join(', ') }};
- {%- endif %}
- {%- if subnet.ntp_server %}
- option ntp-servers {{ subnet.ntp_server | join(', ') }};
- {%- endif %}
- {%- if subnet.pop_server %}
- option pop-server {{ subnet.pop_server | join(', ') }};
- {%- endif %}
- {%- if subnet.smtp_server %}
- option smtp-server {{ subnet.smtp_server | join(', ') }};
- {%- endif %}
- {%- if subnet.time_server %}
- option time-servers {{ subnet.time_server | join(', ') }};
- {%- endif %}
- {%- if subnet.wins_server %}
- option netbios-name-servers {{ subnet.wins_server | join(', ') }};
- {%- endif %}
- {%- if subnet.static_route %}
- option rfc3442-static-route {{ subnet.static_route }};
- option windows-static-route {{ subnet.static_route }};
- {%- endif %}
- {%- if subnet.ip_forwarding %}
- option ip-forwarding true;
- {%- endif -%}
- {%- if subnet.default_router %}
- option routers {{ subnet.default_router }};
- {%- endif -%}
- {%- if subnet.server_identifier %}
- option dhcp-server-identifier {{ subnet.server_identifier }};
- {%- endif -%}
- {%- if subnet.domain_name %}
- option domain-name "{{ subnet.domain_name }}";
- {%- endif -%}
- {%- if subnet.subnet_parameters %}
- # The following {{ subnet.subnet_parameters | length }} line(s) were added as subnet-parameters in the CLI and have not been validated
- {%- for param in subnet.subnet_parameters %}
- {{ param }}
- {%- endfor -%}
- {%- endif %}
- {%- if subnet.tftp_server %}
- option tftp-server-name "{{ subnet.tftp_server }}";
- {%- endif -%}
- {%- if subnet.bootfile_name %}
- option bootfile-name "{{ subnet.bootfile_name }}";
- filename "{{ subnet.bootfile_name }}";
- {%- endif -%}
- {%- if subnet.bootfile_server %}
- next-server {{ subnet.bootfile_server }};
- {%- endif -%}
- {%- if subnet.time_offset %}
- option time-offset {{ subnet.time_offset }};
- {%- endif -%}
- {%- if subnet.wpad_url %}
- option wpad-url "{{ subnet.wpad_url }}";
- {%- endif -%}
- {%- if subnet.client_prefix_length %}
- option subnet-mask {{ subnet.client_prefix_length }};
- {%- endif -%}
- {% if subnet.lease %}
- default-lease-time {{ subnet.lease }};
- max-lease-time {{ subnet.lease }};
- {%- endif -%}
- {%- for host in subnet.static_mapping %}
- {% if not host.disabled -%}
- host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} {
- {%- if host.ip_address %}
- fixed-address {{ host.ip_address }};
- {%- endif %}
- hardware ethernet {{ host.mac_address }};
- {%- if host.static_parameters %}
- # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated
- {%- for param in host.static_parameters %}
- {{ param }}
- {%- endfor -%}
- {%- endif %}
- }
- {%- endif %}
- {%- endfor %}
- {%- if subnet.failover_name %}
- pool {
- failover peer "{{ subnet.failover_name }}";
- deny dynamic bootp clients;
- {%- for range in subnet.range %}
- range {{ range.start }} {{ range.stop }};
- {%- endfor %}
- }
- {%- else %}
- {%- for range in subnet.range %}
- range {{ range.start }} {{ range.stop }};
- {%- endfor %}
- {%- endif %}
- }
- {%- endfor %}
- on commit {
- set shared-networkname = "{{ network.name }}";
- {% if hostfile_update -%}
- set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
- set ClientIp = binary-to-ascii(10, 8, ".", leased-address);
- set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6));
- set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
- execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain);
- {%- endif %}
- }
-}
-{%- endif %}
-{% endfor %}
-"""
-
-daemon_tmpl = """
-### Autogenerated by dhcp_server.py ###
-
-# sourced by /etc/init.d/isc-dhcpv4-server
-
-DHCPD_CONF={{ config_file }}
-DHCPD_PID={{ pid_file }}
-OPTIONS="-4 -lf {{ lease_file }}"
-INTERFACES=""
-"""
-
default_config_data = {
'lease_file': lease_file,
'disabled': False,
@@ -315,6 +109,26 @@ def dhcp_slice_range(exclude_list, range_list):
return output
+def dhcp_static_route(static_subnet, static_router):
+ # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
+ # Option format is:
+ # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
+ # where bytes with the value 0 are omitted.
+ net = ip_network(static_subnet)
+ # add netmask
+ string = str(net.prefixlen) + ','
+ # add network bytes
+ if net.prefixlen:
+ width = net.prefixlen // 8
+ if net.prefixlen % 8:
+ width += 1
+ string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ','
+
+ # add router bytes
+ string += ','.join(static_router.split('.'))
+
+ return string
+
def get_config():
dhcp = default_config_data
conf = Config()
@@ -395,6 +209,7 @@ def get_config():
'bootfile_server': '',
'client_prefix_length': '',
'default_router': '',
+ 'rfc3442_default_router': '',
'dns_server': [],
'domain_name': '',
'domain_search': [],
@@ -438,11 +253,12 @@ def get_config():
if conf.exists('client-prefix-length'):
# snippet borrowed from https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python
host_bits = 32 - int(conf.return_value('client-prefix-length'))
- subnet['client_prefix_length'] = socket.inet_ntoa(struct.pack('!I', (1 << 32) - (1 << host_bits)))
+ subnet['client_prefix_length'] = inet_ntoa(pack('!I', (1 << 32) - (1 << host_bits)))
# Default router IP address on the client's subnet
if conf.exists('default-router'):
subnet['default_router'] = conf.return_value('default-router')
+ subnet['rfc3442_default_router'] = dhcp_static_route("0.0.0.0/0", subnet['default_router'])
# Specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to
# the client. Servers should be listed in order of preference.
@@ -586,27 +402,7 @@ def get_config():
subnet['static_router'] = conf.return_value('static-route router')
if subnet['static_router'] and subnet['static_subnet']:
- # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
- # Option format is:
- # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
- # where bytes with the value 0 are omitted.
- net = ip_network(subnet['static_subnet'])
- # add netmask
- string = str(net.prefixlen) + ','
- # add network bytes
- bytes = str(net.network_address).split('.')
- for b in bytes:
- if b != '0':
- string += b + ','
-
- # add router bytes
- bytes = subnet['static_router'].split('.')
- for b in bytes:
- string += b
- if b is not bytes[-1]:
- string += ','
-
- subnet['static_route'] = string
+ subnet['static_route'] = dhcp_static_route(subnet['static_subnet'], subnet['static_router'])
# HACKS AND TRICKS
#
@@ -776,7 +572,7 @@ def verify(dhcp):
# There must be one subnet connected to a listen interface.
# This only counts if the network itself is not disabled!
if not network['disabled']:
- if vyos.validate.is_subnet_connected(subnet['network'], primary=True):
+ if is_subnet_connected(subnet['network'], primary=True):
listen_ok = True
# Subnets must be non overlapping
@@ -808,9 +604,13 @@ def generate(dhcp):
print('Warning: DHCP server will be deactivated because it is disabled')
return None
- tmpl = jinja2.Template(config_tmpl)
- config_text = tmpl.render(dhcp)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcp-server')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+ tmpl = env.get_template('dhcpd.conf.tmpl')
+ config_text = tmpl.render(dhcp)
# Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters
# we can pass to ISC DHCPd
config_text = config_text.replace("&quot;",'"')
@@ -818,7 +618,7 @@ def generate(dhcp):
with open(config_file, 'w') as f:
f.write(config_text)
- tmpl = jinja2.Template(daemon_tmpl)
+ tmpl = env.get_template('daemon.tmpl')
config_text = tmpl.render(dhcp)
with open(daemon_config_file, 'w') as f:
f.write(config_text)
@@ -828,7 +628,7 @@ def generate(dhcp):
def apply(dhcp):
if (dhcp is None) or dhcp['disabled']:
# DHCP server is removed in the commit
- os.system('sudo systemctl stop isc-dhcpv4-server.service')
+ call('sudo systemctl stop isc-dhcpv4-server.service')
if os.path.exists(config_file):
os.unlink(config_file)
if os.path.exists(daemon_config_file):
@@ -838,7 +638,7 @@ def apply(dhcp):
if not os.path.exists(lease_file):
os.mknod(lease_file)
- os.system('sudo systemctl restart isc-dhcpv4-server.service')
+ call('sudo systemctl restart isc-dhcpv4-server.service')
return None
@@ -850,4 +650,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
index ccabc901d..9355d9794 100755
--- a/src/conf_mode/dhcpv6_relay.py
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,26 +13,20 @@
#
# 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 sys
import os
-import jinja2
+
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
-config_file = r'/etc/default/isc-dhcpv6-relay'
-
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by dhcpv6_relay.py ###
-
-# Defaults for isc-dhcpv6-relay initscript sourced by /etc/init.d/isc-dhcpv6-relay
-OPTIONS="-6 -l {{ listen_addr | join(' -l ') }} -u {{ upstream_addr | join(' -u ') }} {{ options | join(' ') }}"
-"""
+config_file = r'/etc/default/isc-dhcpv6-relay'
default_config_data = {
'listen_addr': [],
@@ -41,7 +35,7 @@ default_config_data = {
}
def get_config():
- relay = default_config_data
+ relay = deepcopy(default_config_data)
conf = Config()
if not conf.exists('service dhcpv6-relay'):
return None
@@ -92,7 +86,12 @@ def generate(relay):
if relay is None:
return None
- tmpl = jinja2.Template(config_tmpl)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcpv6-relay')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('config.tmpl')
config_text = tmpl.render(relay)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -101,10 +100,10 @@ def generate(relay):
def apply(relay):
if relay is not None:
- os.system('sudo systemctl restart isc-dhcpv6-relay.service')
+ call('sudo systemctl restart isc-dhcpv6-relay.service')
else:
# DHCPv6 relay support is removed in the commit
- os.system('sudo systemctl stop isc-dhcpv6-relay.service')
+ call('sudo systemctl stop isc-dhcpv6-relay.service')
os.unlink(config_file)
return None
@@ -117,4 +116,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index 44a927789..950ca1ce2 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2019 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,119 +13,26 @@
#
# 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 sys
import os
import ipaddress
-import jinja2
-
-import vyos.validate
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.validate import is_subnet_connected
from vyos import ConfigError
+from vyos.util import call
+
config_file = r'/etc/dhcp/dhcpdv6.conf'
lease_file = r'/config/dhcpdv6.leases'
pid_file = r'/var/run/dhcpdv6.pid'
daemon_config_file = r'/etc/default/isc-dhcpv6-server'
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by dhcpv6_server.py ###
-
-# For options please consult the following website:
-# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
-
-log-facility local7;
-{%- if preference %}
-option dhcp6.preference {{ preference }};
-{%- endif %}
-
-# Shared network configration(s)
-{% for network in shared_network %}
-{%- if not network.disabled -%}
-shared-network {{ network.name }} {
- {%- for subnet in network.subnet %}
- subnet6 {{ subnet.network }} {
- {%- for range in subnet.range6_prefix %}
- range6 {{ range.prefix }}{{ " temporary" if range.temporary }};
- {%- endfor %}
- {%- for range in subnet.range6 %}
- range6 {{ range.start }} {{ range.stop }};
- {%- endfor %}
- {%- if subnet.domain_search %}
- option dhcp6.domain-search {{ subnet.domain_search | join(', ') }};
- {%- endif %}
- {%- if subnet.lease_def %}
- default-lease-time {{ subnet.lease_def }};
- {%- endif %}
- {%- if subnet.lease_max %}
- max-lease-time {{ subnet.lease_max }};
- {%- endif %}
- {%- if subnet.lease_min %}
- min-lease-time {{ subnet.lease_min }};
- {%- endif %}
- {%- if subnet.dns_server %}
- option dhcp6.name-servers {{ subnet.dns_server | join(', ') }};
- {%- endif %}
- {%- if subnet.nis_domain %}
- option dhcp6.nis-domain-name "{{ subnet.nis_domain }}";
- {%- endif %}
- {%- if subnet.nis_server %}
- option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }};
- {%- endif %}
- {%- if subnet.nisp_domain %}
- option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}";
- {%- endif %}
- {%- if subnet.nisp_server %}
- option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }};
- {%- endif %}
- {%- if subnet.sip_address %}
- option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }};
- {%- endif %}
- {%- if subnet.sip_hostname %}
- option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }};
- {%- endif %}
- {%- if subnet.sntp_server %}
- option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }};
- {%- endif %}
- {%- for host in subnet.static_mapping %}
- {% if not host.disabled -%}
- host {{ network.name }}_{{ host.name }} {
- {%- if host.client_identifier %}
- host-identifier option dhcp6.client-id {{ host.client_identifier }};
- {%- endif %}
- {%- if host.ipv6_address %}
- fixed-address6 {{ host.ipv6_address }};
- {%- endif %}
- }
- {%- endif %}
- {%- endfor %}
- }
- {%- endfor %}
- on commit {
- set shared-networkname = "{{ network.name }}";
- }
-}
-{%- endif %}
-{% endfor %}
-
-"""
-
-daemon_tmpl = """
-### Autogenerated by dhcpv6_server.py ###
-
-# sourced by /etc/init.d/isc-dhcpv6-server
-
-DHCPD_CONF={{ config_file }}
-DHCPD_PID={{ pid_file }}
-OPTIONS="-6 -lf {{ lease_file }}"
-INTERFACES=""
-"""
-
default_config_data = {
'lease_file': lease_file,
'preference': '',
@@ -134,7 +41,7 @@ default_config_data = {
}
def get_config():
- dhcpv6 = default_config_data
+ dhcpv6 = deepcopy(default_config_data)
conf = Config()
if not conf.exists('service dhcpv6-server'):
return None
@@ -409,7 +316,7 @@ def verify(dhcpv6):
# There must be one subnet connected to a listen interface if network is not disabled.
if not network['disabled']:
- if vyos.validate.is_subnet_connected(subnet['network']):
+ if is_subnet_connected(subnet['network']):
listen_ok = True
# DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping
@@ -437,12 +344,17 @@ def generate(dhcpv6):
print('Warning: DHCPv6 server will be deactivated because it is disabled')
return None
- tmpl = jinja2.Template(config_tmpl)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcpv6-server')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('dhcpdv6.conf.tmpl')
config_text = tmpl.render(dhcpv6)
with open(config_file, 'w') as f:
f.write(config_text)
- tmpl = jinja2.Template(daemon_tmpl)
+ tmpl = env.get_template('daemon.tmpl')
config_text = tmpl.render(dhcpv6)
with open(daemon_config_file, 'w') as f:
f.write(config_text)
@@ -452,7 +364,7 @@ def generate(dhcpv6):
def apply(dhcpv6):
if (dhcpv6 is None) or dhcpv6['disabled']:
# DHCP server is removed in the commit
- os.system('sudo systemctl stop isc-dhcpv6-server.service')
+ call('sudo systemctl stop isc-dhcpv6-server.service')
if os.path.exists(config_file):
os.unlink(config_file)
if os.path.exists(daemon_config_file):
@@ -462,7 +374,7 @@ def apply(dhcpv6):
if not os.path.exists(lease_file):
os.mknod(lease_file)
- os.system('sudo systemctl restart isc-dhcpv6-server.service')
+ call('sudo systemctl restart isc-dhcpv6-server.service')
return None
@@ -474,4 +386,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 38f3cb4de..4071c05c9 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,22 +13,20 @@
#
# 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 sys
import os
-
import argparse
-import jinja2
-import netifaces
-import vyos.util
-import vyos.hostsd_client
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.hostsd_client import Client as hostsd_client
+from vyos.util import wait_for_commit_lock
from vyos import ConfigError
-
+from vyos.util import call
parser = argparse.ArgumentParser()
parser.add_argument("--dhclient", action="store_true",
@@ -36,53 +34,6 @@ parser.add_argument("--dhclient", action="store_true",
config_file = r'/etc/powerdns/recursor.conf'
-# XXX: pdns recursor doesn't like whitespace near entry separators,
-# especially in the semicolon-separated lists of name servers.
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by dns_forwarding.py ###
-
-# Non-configurable defaults
-daemon=yes
-threads=1
-allow-from={{ allow_from | join(',') }}
-log-common-errors=yes
-non-local-bind=yes
-query-local-address=0.0.0.0
-query-local-address6=::
-
-# cache-size
-max-cache-entries={{ cache_size }}
-
-# negative TTL for NXDOMAIN
-max-negative-ttl={{ negative_ttl }}
-
-# ignore-hosts-file
-export-etc-hosts={{ export_hosts_file }}
-
-# listen-on
-local-address={{ listen_on | join(',') }}
-
-# dnssec
-dnssec={{ dnssec }}
-
-# forward-zones / recursion
-#
-# statement is only inserted if either one forwarding domain or nameserver is configured
-# if nothing is given at all, powerdns will act as a real recursor and resolve all requests by its own
-#
-{% if name_servers or domains %}forward-zones-recurse=
-{%- for d in domains %}
-{{ d.name }}={{ d.servers | join(";") }}
-{{- ", " if not loop.last -}}
-{%- endfor -%}
-{%- if name_servers -%}
-{%- if domains -%}, {% endif -%}.={{ name_servers | join(';') }}
-{% endif %}
-{% endif %}
-
-"""
-
default_config_data = {
'allow_from': [],
'cache_size': 10000,
@@ -96,74 +47,74 @@ default_config_data = {
def get_config(arguments):
- dns = default_config_data
+ dns = deepcopy(default_config_data)
conf = Config()
+ base = ['service', 'dns', 'forwarding']
if arguments.dhclient:
conf.exists = conf.exists_effective
conf.return_value = conf.return_effective_value
conf.return_values = conf.return_effective_values
- if not conf.exists('service dns forwarding'):
+ if not conf.exists(base):
return None
- conf.set_level('service dns forwarding')
+ conf.set_level(base)
- if conf.exists('allow-from'):
- dns['allow_from'] = conf.return_values('allow-from')
+ if conf.exists(['allow-from']):
+ dns['allow_from'] = conf.return_values(['allow-from'])
- if conf.exists('cache-size'):
- cache_size = conf.return_value('cache-size')
+ if conf.exists(['cache-size']):
+ cache_size = conf.return_value(['cache-size'])
dns['cache_size'] = cache_size
if conf.exists('negative-ttl'):
- negative_ttl = conf.return_value('negative-ttl')
+ negative_ttl = conf.return_value(['negative-ttl'])
dns['negative_ttl'] = negative_ttl
- if conf.exists('domain'):
- for node in conf.list_nodes('domain'):
- servers = conf.return_values("domain {0} server".format(node))
+ if conf.exists(['domain']):
+ for node in conf.list_nodes(['domain']):
+ servers = conf.return_values(['domain', node, 'server'])
domain = {
"name": node,
"servers": bracketize_ipv6_addrs(servers)
}
dns['domains'].append(domain)
- if conf.exists('ignore-hosts-file'):
+ if conf.exists(['ignore-hosts-file']):
dns['export_hosts_file'] = "no"
- if conf.exists('name-server'):
- name_servers = conf.return_values('name-server')
+ if conf.exists(['name-server']):
+ name_servers = conf.return_values(['name-server'])
dns['name_servers'] = dns['name_servers'] + name_servers
- if conf.exists('system'):
- conf.set_level('system')
+ if conf.exists(['system']):
+ conf.set_level(['system'])
system_name_servers = []
- system_name_servers = conf.return_values('name-server')
+ system_name_servers = conf.return_values(['name-server'])
if not system_name_servers:
- print(
- "DNS forwarding warning: No name-servers set under 'system name-server'\n")
+ print("DNS forwarding warning: No name-servers set under 'system name-server'\n")
else:
dns['name_servers'] = dns['name_servers'] + system_name_servers
- conf.set_level('service dns forwarding')
+ conf.set_level(base)
dns['name_servers'] = bracketize_ipv6_addrs(dns['name_servers'])
- if conf.exists('listen-address'):
- dns['listen_on'] = conf.return_values('listen-address')
+ if conf.exists(['listen-address']):
+ dns['listen_on'] = conf.return_values(['listen-address'])
- if conf.exists('dnssec'):
- dns['dnssec'] = conf.return_value('dnssec')
+ if conf.exists(['dnssec']):
+ dns['dnssec'] = conf.return_value(['dnssec'])
# Add name servers received from DHCP
- if conf.exists('dhcp'):
+ if conf.exists(['dhcp']):
interfaces = []
- interfaces = conf.return_values('dhcp')
- hc = vyos.hostsd_client.Client()
+ interfaces = conf.return_values(['dhcp'])
+ hc = hostsd_client()
for interface in interfaces:
- dhcp_resolvers = hc.get_name_servers("dhcp-{0}".format(interface))
- dhcpv6_resolvers = hc.get_name_servers("dhcpv6-{0}".format(interface))
+ dhcp_resolvers = hc.get_name_servers(f'dhcp-{interface}')
+ dhcpv6_resolvers = hc.get_name_servers(f'dhcpv6-{interface}')
if dhcp_resolvers:
dns['name_servers'] = dns['name_servers'] + dhcp_resolvers
@@ -202,7 +153,12 @@ def generate(dns):
if dns is None:
return None
- tmpl = jinja2.Template(config_tmpl, trim_blocks=True)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dns-forwarding')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
+ tmpl = env.get_template('recursor.conf.tmpl')
config_text = tmpl.render(dns)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -211,11 +167,11 @@ def generate(dns):
def apply(dns):
if dns is None:
# DNS forwarding is removed in the commit
- os.system("systemctl stop pdns-recursor")
+ call("systemctl stop pdns-recursor")
if os.path.isfile(config_file):
os.unlink(config_file)
else:
- os.system("systemctl restart pdns-recursor")
+ call("systemctl restart pdns-recursor")
if __name__ == '__main__':
args = parser.parse_args()
@@ -223,7 +179,7 @@ if __name__ == '__main__':
if args.dhclient:
# There's a big chance it was triggered by a commit still in progress
# so we need to wait until the new values are in the running config
- vyos.util.wait_for_commit_lock()
+ wait_for_commit_lock()
try:
c = get_config(args)
@@ -232,4 +188,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index 4663987b4..b54d76b06 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2019 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -15,68 +15,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import sys
-import jinja2
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
from stat import S_IRUSR, S_IWUSR
+
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
+
config_file = r'/etc/ddclient/ddclient.conf'
cache_file = r'/var/cache/ddclient/ddclient.cache'
pid_file = r'/var/run/ddclient/ddclient.pid'
-config_tmpl = """
-### Autogenerated by dynamic_dns.py ###
-daemon=1m
-syslog=yes
-ssl=yes
-pid={{ pid_file }}
-cache={{ cache_file }}
-
-{% for interface in interfaces -%}
-
-#
-# ddclient configuration for interface "{{ interface.interface }}":
-#
-{% if interface.web_url -%}
-use=web, web='{{ interface.web_url}}' {%- if interface.web_skip %}, web-skip='{{ interface.web_skip }}'{% endif %}
-{% else -%}
-use=if, if={{ interface.interface }}
-{% endif -%}
-
-{% for rfc in interface.rfc2136 -%}
-{% for record in rfc.record %}
-# RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }}
-server={{ rfc.server }}
-protocol=nsupdate
-password={{ rfc.keyfile }}
-ttl={{ rfc.ttl }}
-zone={{ rfc.zone }}
-{{ record }}
-{% endfor -%}
-{% endfor -%}
-
-{% for srv in interface.service %}
-{% for host in srv.host %}
-# DynDNS provider configuration for {{ host }}
-protocol={{ srv.protocol }},
-max-interval=28d,
-login={{ srv.login }},
-password='{{ srv.password }}',
-{% if srv.server -%}
-server={{ srv.server }},
-{% endif -%}
-{% if srv.zone -%}
-zone={{ srv.zone }},
-{% endif -%}
-{{ host }}
-{% endfor %}
-{% endfor %}
-
-{% endfor %}
-"""
-
# Mapping of service name to service protocol
default_service_protocol = {
'afraid': 'freedns',
@@ -100,7 +54,7 @@ default_config_data = {
}
def get_config():
- dyndns = default_config_data
+ dyndns = deepcopy(default_config_data)
conf = Config()
base_level = ['service', 'dns', 'dynamic']
@@ -198,8 +152,8 @@ def get_config():
node['service'].append(srv)
- # set config level back to top level
- conf.set_level(base_level)
+ # Set config back to appropriate level for these options
+ conf.set_level(base_level + ['interface', interface])
# Additional settings in CLI
if conf.exists(['use-web', 'skip']):
@@ -208,6 +162,9 @@ def get_config():
if conf.exists(['use-web', 'url']):
node['web_url'] = conf.return_value(['use-web', 'url'])
+ # set config level back to top level
+ conf.set_level(base_level)
+
dyndns['interfaces'].append(node)
return dyndns
@@ -269,6 +226,11 @@ def generate(dyndns):
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dynamic-dns')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
dirname = os.path.dirname(dyndns['pid_file'])
if not os.path.exists(dirname):
os.mkdir(dirname)
@@ -277,7 +239,7 @@ def generate(dyndns):
if not os.path.exists(dirname):
os.mkdir(dirname)
- tmpl = jinja2.Template(config_tmpl)
+ tmpl = env.get_template('ddclient.conf.tmpl')
config_text = tmpl.render(dyndns)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -295,11 +257,11 @@ def apply(dyndns):
os.unlink('/etc/ddclient.conf')
if dyndns['deleted']:
- os.system('/etc/init.d/ddclient stop')
+ call('/etc/init.d/ddclient stop')
if os.path.exists(dyndns['pid_file']):
os.unlink(dyndns['pid_file'])
else:
- os.system('/etc/init.d/ddclient restart')
+ call('/etc/init.d/ddclient restart')
return None
@@ -311,4 +273,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py
index 2be80cdbf..0b800f48f 100755
--- a/src/conf_mode/firewall_options.py
+++ b/src/conf_mode/firewall_options.py
@@ -21,6 +21,8 @@ import copy
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import call
+
default_config_data = {
'intf_opts': [],
@@ -85,19 +87,19 @@ def apply(tcp):
target = 'VYOS_FW_OPTIONS'
# always cleanup iptables
- os.system('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
- os.system('iptables --table mangle --flush {} >&/dev/null'.format(target))
- os.system('iptables --table mangle --delete-chain {} >&/dev/null'.format(target))
+ call('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
+ call('iptables --table mangle --flush {} >&/dev/null'.format(target))
+ call('iptables --table mangle --delete-chain {} >&/dev/null'.format(target))
# always cleanup ip6tables
- os.system('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
- os.system('ip6tables --table mangle --flush {} >&/dev/null'.format(target))
- os.system('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --flush {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target))
# Setup new iptables rules
if tcp['new_chain4']:
- os.system('iptables --table mangle --new-chain {} >&/dev/null'.format(target))
- os.system('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
+ call('iptables --table mangle --new-chain {} >&/dev/null'.format(target))
+ call('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
for opts in tcp['intf_opts']:
intf = opts['intf']
@@ -109,13 +111,13 @@ def apply(tcp):
# adjust TCP MSS per interface
if mss:
- os.system('iptables --table mangle --append {} --out-interface {} --protocol tcp ' \
+ call('iptables --table mangle --append {} --out-interface {} --protocol tcp '
'--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
# Setup new ip6tables rules
if tcp['new_chain6']:
- os.system('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target))
- os.system('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
for opts in tcp['intf_opts']:
intf = opts['intf']
@@ -127,8 +129,8 @@ def apply(tcp):
# adjust TCP MSS per interface
if mss:
- os.system('ip6tables --table mangle --append {} --out-interface {} --protocol tcp ' \
- '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
+ call('ip6tables --table mangle --append {} --out-interface {} --protocol tcp '
+ '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
return None
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 0bc50482c..1008f3fae 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,18 +13,21 @@
#
# 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 sys
+import os
import re
import ipaddress
-import subprocess
+from ipaddress import ip_address
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
+
+from vyos.ifconfig import Interface
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
-import vyos.interfaces
-from vyos.ifconfig import Interface
-from jinja2 import Template
+from vyos.util import cmd
+
# default values
default_sflow_server_port = 6343
@@ -37,78 +40,6 @@ uacctd_conf_path = '/etc/pmacct/uacctd.conf'
iptables_nflog_table = 'raw'
iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK'
-# pmacct config template
-uacct_conf_jinja = '''# Genereated from VyOS configuration
-daemonize: true
-promisc: false
-pidfile: /var/run/uacctd.pid
-uacctd_group: 2
-uacctd_nl_size: 2097152
-snaplen: {{ snaplen }}
-aggregate: in_iface,src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows
-plugin_pipe_size: {{ templatecfg['plugin_pipe_size'] }}
-plugin_buffer_size: {{ templatecfg['plugin_buffer_size'] }}
-{%- if templatecfg['syslog-facility'] != none %}
-syslog: {{ templatecfg['syslog-facility'] }}
-{%- endif %}
-{%- if templatecfg['disable-imt'] == none %}
-imt_path: /tmp/uacctd.pipe
-imt_mem_pools_number: 169
-{%- endif %}
-plugins:
-{%- if templatecfg['netflow']['servers'] != none -%}
- {% for server in templatecfg['netflow']['servers'] %}
- {%- if loop.last -%}nfprobe[nf_{{ server['address'] }}]{%- else %}nfprobe[nf_{{ server['address'] }}],{%- endif %}
- {%- endfor -%}
- {% set plugins_presented = true %}
-{%- endif %}
-{%- if templatecfg['sflow']['servers'] != none -%}
- {% if plugins_presented -%}
- {%- for server in templatecfg['sflow']['servers'] -%}
- ,sfprobe[sf_{{ server['address'] }}]
- {%- endfor %}
- {%- else %}
- {%- for server in templatecfg['sflow']['servers'] %}
- {%- if loop.last -%}sfprobe[sf_{{ server['address'] }}]{%- else %}sfprobe[sf_{{ server['address'] }}],{%- endif %}
- {%- endfor %}
- {%- endif -%}
- {% set plugins_presented = true %}
-{%- endif %}
-{%- if templatecfg['disable-imt'] == none %}
- {%- if plugins_presented -%},memory{%- else %}memory{%- endif %}
-{%- endif %}
-{%- if templatecfg['netflow']['servers'] != none %}
-{%- for server in templatecfg['netflow']['servers'] %}
-nfprobe_receiver[nf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }}
-nfprobe_version[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['version'] }}
-{%- if templatecfg['netflow']['engine-id'] != none %}
-nfprobe_engine[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['engine-id'] }}
-{%- endif %}
-{%- if templatecfg['netflow']['max-flows'] != none %}
-nfprobe_maxflows[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['max-flows'] }}
-{%- endif %}
-{%- if templatecfg['netflow']['sampling-rate'] != none %}
-sampling_rate[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['sampling-rate'] }}
-{%- endif %}
-{%- if templatecfg['netflow']['source-ip'] != none %}
-nfprobe_source_ip[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['source-ip'] }}
-{%- endif %}
-{%- if templatecfg['netflow']['timeout_string'] != '' %}
-nfprobe_timeouts[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['timeout_string'] }}
-{%- endif %}
-{%- endfor %}
-{%- endif %}
-{%- if templatecfg['sflow']['servers'] != none %}
-{%- for server in templatecfg['sflow']['servers'] %}
-sfprobe_receiver[sf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }}
-sfprobe_agentip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['agent-address'] }}
-{%- if templatecfg['sflow']['sampling-rate'] != none %}
-sampling_rate[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['sampling-rate'] }}
-{%- endif %}
-{%- endfor %}
-{% endif %}
-'''
-
# helper functions
# check if node exists and return True if this is true
def _node_exists(path):
@@ -129,7 +60,7 @@ def _sflow_default_agentip(config):
return config.return_value('protocols ospfv3 parameters router-id')
# if router-id was not found, use first available ip of any interface
- for iface in vyos.interfaces.list_interfaces():
+ for iface in Interface.listing():
for address in Interface(iface).get_addr():
# return an IP, if this is not loopback
regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
@@ -151,11 +82,7 @@ def _iptables_get_nflog():
for iptables_variant in ['iptables', 'ip6tables']:
# run iptables, save output and split it by lines
iptables_command = "sudo {0} -t {1} -S {2}".format(iptables_variant, iptables_nflog_table, iptables_nflog_chain)
- process = subprocess.Popen(iptables_command, stdout=subprocess.PIPE, shell=True, universal_newlines=True)
- stdout, stderr = process.communicate()
- if not process.returncode == 0:
- print("Failed to get flows list: command \"{}\" returned exit code: {}\nError: {}".format(command, process.returncode, stderr))
- sys.exit(1)
+ cmd(iptables_command, universal_newlines=True, message='Failed to get flows list')
iptables_out = stdout.splitlines()
# parse each line and add information to list
@@ -180,14 +107,21 @@ def _iptables_config(configured_ifaces):
# get currently configured interfaces with iptables rules
active_nflog_rules = _iptables_get_nflog()
-
+
# compare current active list with configured one and delete excessive interfaces, add missed
active_nflog_ifaces = []
for rule in active_nflog_rules:
- if rule['interface'] not in configured_ifaces:
- iptable_commands.append("sudo {0} -t {1} -D {2}".format(rule['iptables_variant'], rule['table'], rule['rule_definition']))
+ iptables = rule['iptables_variant']
+ interface = rule['interface']
+ if interface not in configured_ifaces:
+ table = rule['table']
+ rule = rule['rule_definition']
+ iptable_commands.append(f'sudo {iptables} -t {table} -D {rule}')
else:
- active_nflog_ifaces.append({ 'iface': rule['interface'], 'iptables_variant': rule['iptables_variant'] })
+ active_nflog_ifaces.append({
+ 'iface': interface,
+ 'iptables_variant': iptables,
+ })
# do not create new rules for already configured interfaces
for iface in active_nflog_ifaces:
@@ -196,14 +130,14 @@ def _iptables_config(configured_ifaces):
# create missed rules
for iface_extended in configured_ifaces_extended:
- rule_definition = "{0} -i {1} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {2} --nflog-threshold 100".format(iptables_nflog_chain, iface_extended['iface'], default_captured_packet_size)
- iptable_commands.append("sudo {0} -t {1} -I {2}".format(iface_extended['iptables_variant'], iptables_nflog_table, rule_definition))
+ iface = iface_extended['iface']
+ iptables = iface_extended['iptables_variant']
+ rule_definition = f'{iptables_nflog_chain} -i {iface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {default_captured_packet_size} --nflog-threshold 100'
+ iptable_commands.append(f'sudo {iptables} -t {iptables_nflog_table} -I {rule_definition}')
# change iptables
for command in iptable_commands:
- return_code = subprocess.call(command.split(' '))
- if not return_code == 0:
- raise ConfigError("Failed to run command: {}\nExit code {}".format(command, return_code))
+ cmd(command, raising=ConfigError)
def get_config():
@@ -300,7 +234,7 @@ def verify(config):
# check that all configured interfaces exists in the system
for iface in config['interfaces']:
- if not iface in vyos.interfaces.list_interfaces():
+ if not iface in Interface.listing():
# chnged from error to warning to allow adding dynamic interfaces and interface templates
# raise ConfigError("The {} interface is not presented in the system".format(iface))
print("Warning: the {} interface is not presented in the system".format(iface))
@@ -315,20 +249,20 @@ def verify(config):
sflow_collector_ipver = None
for sflow_collector in config['sflow']['servers']:
if sflow_collector_ipver:
- if sflow_collector_ipver != ipaddress.ip_address(sflow_collector['address']).version:
+ if sflow_collector_ipver != ip_address(sflow_collector['address']).version:
raise ConfigError("All sFlow servers must use the same IP protocol")
else:
- sflow_collector_ipver = ipaddress.ip_address(sflow_collector['address']).version
+ sflow_collector_ipver = ip_address(sflow_collector['address']).version
# check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa
for sflow_collector in config['sflow']['servers']:
- if ipaddress.ip_address(sflow_collector['address']).version != ipaddress.ip_address(config['sflow']['agent-address']).version:
+ if ip_address(sflow_collector['address']).version != ip_address(config['sflow']['agent-address']).version:
raise ConfigError("Different IP address versions cannot be mixed in \"sflow agent-address\" and \"sflow server\". You need to set manually the same IP version for \"agent-address\" as for all sFlow servers")
# check if configured sFlow agent-id exist in the system
agent_id_presented = None
- for iface in vyos.interfaces.list_interfaces():
+ for iface in Interface.listing():
for address in Interface(iface).get_addr():
# check an IP, if this is not loopback
regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
@@ -348,7 +282,7 @@ def verify(config):
# check if configured netflow source-ip exist in the system
if config['netflow']['source-ip']:
source_ip_presented = None
- for iface in vyos.interfaces.list_interfaces():
+ for iface in Interface.listing():
for address in Interface(iface).get_addr():
# check an IP
regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
@@ -379,7 +313,7 @@ def generate(config):
# skip all checks if flow-accounting was removed
if not config['flow-accounting-configured']:
return True
-
+
# Calculate all necessary values
if config['buffer-size']:
# circular queue size
@@ -400,13 +334,16 @@ def generate(config):
timeout_string = "{}:{}={}".format(timeout_string, timeout_type, timeout_value)
config['netflow']['timeout_string'] = timeout_string
- # Generate daemon configs
- uacct_conf_template = Template(uacct_conf_jinja)
- uacct_conf_file_data = uacct_conf_template.render(templatecfg = config, snaplen = default_captured_packet_size)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'netflow')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
- # save generated config to uacctd.conf
+ # Generate daemon configs
+ tmpl = env.get_template('uacctd.conf.tmpl')
+ config_text = tmpl.render(templatecfg = config, snaplen = default_captured_packet_size)
with open(uacctd_conf_path, 'w') as file:
- file.write(uacct_conf_file_data)
+ file.write(config_text)
def apply(config):
@@ -419,9 +356,7 @@ def apply(config):
command = '/usr/bin/sudo /bin/systemctl restart uacctd'
# run command to start or stop flow-accounting
- return_code = subprocess.call(command.split(' '))
- if not return_code == 0:
- raise ConfigError("Failed to start/stop flow-accounting: command {} returned exit code {}".format(command, return_code))
+ cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting')
# configure iptables rules for defined interfaces
if config['interfaces']:
@@ -437,4 +372,4 @@ if __name__ == '__main__':
apply(config)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index 47cf232e9..7c2f79abc 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -25,7 +25,6 @@ import re
import sys
import copy
import glob
-import subprocess
import argparse
import jinja2
@@ -34,6 +33,9 @@ import vyos.hostsd_client
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import cmd
+from vyos.util import call
+from vyos.util import run
default_config_data = {
@@ -156,21 +158,22 @@ def apply(config):
# rsyslog runs into a race condition at boot time with systemd
# restart rsyslog only if the hostname changed.
- hostname_old = subprocess.check_output(['hostnamectl', '--static']).decode().strip()
-
- os.system("hostnamectl set-hostname --static {0}".format(hostname_new))
+ hostname_old = cmd('hostnamectl --static')
+ call(f'hostnamectl set-hostname --static {hostname_new}')
# Restart services that use the hostname
if hostname_new != hostname_old:
- os.system("systemctl restart rsyslog.service")
+ call("systemctl restart rsyslog.service")
# If SNMP is running, restart it too
- if os.system("pgrep snmpd > /dev/null") == 0:
- os.system("systemctl restart snmpd.service")
+ ret = run("pgrep snmpd")
+ if ret == 0:
+ call("systemctl restart snmpd.service")
# restart pdns if it is used
- if os.system("/usr/bin/rec_control ping >/dev/null 2>&1") == 0:
- os.system("/etc/init.d/pdns-recursor restart >/dev/null")
+ ret = run('/usr/bin/rec_control ping')
+ if ret == 0:
+ call('/etc/init.d/pdns-recursor restart >/dev/null')
return None
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 0c2e029e9..26f4aea7f 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -18,13 +18,14 @@
import sys
import os
-import subprocess
import json
from copy import deepcopy
import vyos.defaults
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import cmd
+from vyos.util import call
config_file = '/etc/vyos/http-api.conf'
@@ -91,16 +92,12 @@ def generate(http_api):
def apply(http_api):
if http_api is not None:
- os.system('sudo systemctl restart vyos-http-api.service')
+ call('sudo systemctl restart vyos-http-api.service')
else:
- os.system('sudo systemctl stop vyos-http-api.service')
+ call('sudo systemctl stop vyos-http-api.service')
for dep in dependencies:
- cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
- try:
- subprocess.check_call(cmd, shell=True)
- except subprocess.CalledProcessError as err:
- raise ConfigError("{}.".format(err))
+ cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index 84d1a7691..da7193c9b 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 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
@@ -13,88 +13,26 @@
#
# 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 sys
import os
-import jinja2
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
import vyos.defaults
import vyos.certbot_util
+
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
-config_file = '/etc/nginx/sites-available/default'
-
-# Please be careful if you edit the template.
-config_tmpl = """
-
-### Autogenerated by https.py ###
-# Default server configuration
-#
-server {
- listen 80 default_server;
- listen [::]:80 default_server;
- server_name _;
- return 301 https://$server_name$request_uri;
-}
-
-{% for server in server_block_list %}
-server {
-
- # SSL configuration
- #
-{% if server.address == '*' %}
- listen {{ server.port }} ssl;
- listen [::]:{{ server.port }} ssl;
-{% else %}
- listen {{ server.address }}:{{ server.port }} ssl;
-{% endif %}
-
-{% for name in server.name %}
- server_name {{ name }};
-{% endfor %}
-
-{% if server.certbot %}
- ssl_certificate /etc/letsencrypt/live/{{ server.certbot_dir }}/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/{{ server.certbot_dir }}/privkey.pem;
- include /etc/letsencrypt/options-ssl-nginx.conf;
- ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
-{% elif server.vyos_cert %}
- include {{ server.vyos_cert.conf }};
-{% else %}
- #
- # Self signed certs generated by the ssl-cert package
- # Don't use them in a production server!
- #
- include snippets/snakeoil.conf;
-{% endif %}
-
- # proxy settings for HTTP API, if enabled; 503, if not
- location ~ /(retrieve|configure|config-file|image) {
-{% if server.api %}
- proxy_pass http://localhost:{{ server.api.port }};
- proxy_buffering off;
-{% else %}
- return 503;
-{% endif %}
- }
-
- error_page 501 502 503 =200 @50*_json;
-
- location @50*_json {
- default_type application/json;
- return 200 '{"error": "Start service in configuration mode: set service https api"}';
- }
-
-}
-{% endfor %}
-"""
+config_file = '/etc/nginx/sites-available/default'
default_server_block = {
+ 'id' : '',
'address' : '*',
'port' : '443',
'name' : ['_'],
@@ -111,22 +49,23 @@ def get_config():
else:
conf.set_level('service https')
- if conf.exists('listen-address'):
- for addr in conf.list_nodes('listen-address'):
- server_block = {'address' : addr}
- server_block['port'] = '443'
- server_block['name'] = ['_']
- if conf.exists('listen-address {0} listen-port'.format(addr)):
- port = conf.return_value('listen-address {0} listen-port'.format(addr))
+ if not conf.exists('virtual-host'):
+ server_block_list.append(default_server_block)
+ else:
+ for vhost in conf.list_nodes('virtual-host'):
+ server_block = deepcopy(default_server_block)
+ server_block['id'] = vhost
+ if conf.exists(f'virtual-host {vhost} listen-address'):
+ addr = conf.return_value(f'virtual-host {vhost} listen-address')
+ server_block['address'] = addr
+ if conf.exists(f'virtual-host {vhost} listen-port'):
+ port = conf.return_value(f'virtual-host {vhost} listen-port')
server_block['port'] = port
- if conf.exists('listen-address {0} server-name'.format(addr)):
- names = conf.return_values('listen-address {0} server-name'.format(addr))
+ if conf.exists(f'virtual-host {vhost} server-name'):
+ names = conf.return_values(f'virtual-host {vhost} server-name')
server_block['name'] = names[:]
server_block_list.append(server_block)
- if not server_block_list:
- server_block_list.append(default_server_block)
-
vyos_cert_data = {}
if conf.exists('certificates system-generated-certificate'):
vyos_cert_data = vyos.defaults.vyos_cert_data
@@ -149,17 +88,33 @@ def get_config():
# certbot organizes certificates by first domain
sb['certbot_dir'] = certbot_domains[0]
+ api_somewhere = False
api_data = {}
if conf.exists('api'):
+ api_somewhere = True
api_data = vyos.defaults.api_data
if conf.exists('api port'):
port = conf.return_value('api port')
api_data['port'] = port
- if api_data:
- for block in server_block_list:
- block['api'] = api_data
+ if conf.exists('api-restrict virtual-host'):
+ vhosts = conf.return_values('api-restrict virtual-host')
+ api_data['vhost'] = vhosts[:]
- https = {'server_block_list' : server_block_list, 'certbot': certbot}
+ if api_data:
+ # we do not want to include 'vhost' key as part of
+ # vyos.defaults.api_data, so check for key existence
+ vhost_list = api_data.get('vhost')
+ if vhost_list is None:
+ for block in server_block_list:
+ block['api'] = api_data
+ else:
+ for block in server_block_list:
+ if block['id'] in vhost_list:
+ block['api'] = api_data
+
+ https = {'server_block_list' : server_block_list,
+ 'api_somewhere': api_somewhere,
+ 'certbot': certbot}
return https
def verify(https):
@@ -170,7 +125,7 @@ def verify(https):
for sb in https['server_block_list']:
if sb['certbot']:
return None
- raise ConfigError("At least one 'listen-address x.x.x.x server-name' "
+ raise ConfigError("At least one 'virtual-host <id> server-name' "
"matching the 'certbot domain-name' is required.")
return None
@@ -178,10 +133,15 @@ def generate(https):
if https is None:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'https')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
if 'server_block_list' not in https or not https['server_block_list']:
https['server_block_list'] = [default_server_block]
- tmpl = jinja2.Template(config_tmpl, trim_blocks=True)
+ tmpl = env.get_template('nginx.default.tmpl')
config_text = tmpl.render(https)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -190,9 +150,9 @@ def generate(https):
def apply(https):
if https is not None:
- os.system('sudo systemctl restart nginx.service')
+ call('sudo systemctl restart nginx.service')
else:
- os.system('sudo systemctl stop nginx.service')
+ call('sudo systemctl stop nginx.service')
if __name__ == '__main__':
try:
@@ -202,4 +162,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py
index cd0704124..77e2bb150 100755
--- a/src/conf_mode/igmp_proxy.py
+++ b/src/conf_mode/igmp_proxy.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,59 +13,21 @@
#
# 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 sys
import os
-import jinja2
+
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
from netifaces import interfaces
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
-config_file = r'/etc/igmpproxy.conf'
-# Please be careful if you edit the template.
-config_tmpl = """
-########################################################
-#
-# autogenerated by igmp_proxy.py
-#
-# The configuration file must define one upstream
-# interface, and one or more downstream interfaces.
-#
-# If multicast traffic originates outside the
-# upstream subnet, the "altnet" option can be
-# used in order to define legal multicast sources.
-# (Se example...)
-#
-# The "quickleave" should be used to avoid saturation
-# of the upstream link. The option should only
-# be used if it's absolutely nessecary to
-# accurately imitate just one Client.
-#
-########################################################
-
-{% if not disable_quickleave -%}
-quickleave
-{% endif -%}
-
-{% for interface in interfaces %}
-# Configuration for {{ interface.name }} ({{ interface.role }} interface)
-{% if interface.role == 'disabled' -%}
-phyint {{ interface.name }} disabled
-{%- else -%}
-phyint {{ interface.name }} {{ interface.role }} ratelimit 0 threshold {{ interface.threshold }}
-{%- endif -%}
-{%- for subnet in interface.alt_subnet %}
- altnet {{ subnet }}
-{%- endfor %}
-{%- for subnet in interface.whitelist %}
- whitelist {{ subnet }}
-{%- endfor %}
-{% endfor %}
-"""
+config_file = r'/etc/igmpproxy.conf'
default_config_data = {
'disable': False,
@@ -74,23 +36,24 @@ default_config_data = {
}
def get_config():
- igmp_proxy = default_config_data
+ igmp_proxy = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('protocols igmp-proxy'):
+ base = ['protocols', 'igmp-proxy']
+ if not conf.exists(base):
return None
else:
- conf.set_level('protocols igmp-proxy')
+ conf.set_level(base)
# Network interfaces to listen on
- if conf.exists('disable'):
+ if conf.exists(['disable']):
igmp_proxy['disable'] = True
# Option to disable "quickleave"
- if conf.exists('disable-quickleave'):
+ if conf.exists(['disable-quickleave']):
igmp_proxy['disable_quickleave'] = True
- for intf in conf.list_nodes('interface'):
- conf.set_level('protocols igmp-proxy interface {0}'.format(intf))
+ for intf in conf.list_nodes(['interface']):
+ conf.set_level(base + ['interface', intf])
interface = {
'name': intf,
'alt_subnet': [],
@@ -99,17 +62,17 @@ def get_config():
'whitelist': []
}
- if conf.exists('alt-subnet'):
- interface['alt_subnet'] = conf.return_values('alt-subnet')
+ if conf.exists(['alt-subnet']):
+ interface['alt_subnet'] = conf.return_values(['alt-subnet'])
- if conf.exists('role'):
- interface['role'] = conf.return_value('role')
+ if conf.exists(['role']):
+ interface['role'] = conf.return_value(['role'])
- if conf.exists('threshold'):
- interface['threshold'] = conf.return_value('threshold')
+ if conf.exists(['threshold']):
+ interface['threshold'] = conf.return_value(['threshold'])
- if conf.exists('whitelist'):
- interface['whitelist'] = conf.return_values('whitelist')
+ if conf.exists(['whitelist']):
+ interface['whitelist'] = conf.return_values(['whitelist'])
# Append interface configuration to global configuration list
igmp_proxy['interfaces'].append(interface)
@@ -153,7 +116,12 @@ def generate(igmp_proxy):
print('Warning: IGMP Proxy will be deactivated because it is disabled')
return None
- tmpl = jinja2.Template(config_tmpl)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'igmp-proxy')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('igmpproxy.conf.tmpl')
config_text = tmpl.render(igmp_proxy)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -163,11 +131,11 @@ def generate(igmp_proxy):
def apply(igmp_proxy):
if igmp_proxy is None or igmp_proxy['disable']:
# IGMP Proxy support is removed in the commit
- os.system('sudo systemctl stop igmpproxy.service')
+ call('sudo systemctl stop igmpproxy.service')
if os.path.exists(config_file):
os.unlink(config_file)
else:
- os.system('sudo systemctl restart igmpproxy.service')
+ call('systemctl restart igmpproxy.service')
return None
@@ -179,4 +147,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/intel_qat.py b/src/conf_mode/intel_qat.py
index a1abd5e81..cc7d4a915 100755
--- a/src/conf_mode/intel_qat.py
+++ b/src/conf_mode/intel_qat.py
@@ -19,10 +19,10 @@
import sys
import os
import re
-import subprocess
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import popen, run
# Define for recovering
gl_ipsec_conf = None
@@ -49,13 +49,10 @@ def get_config():
# Control configured VPN service which can use QAT
def vpn_control(action):
+ # XXX: Should these commands report failure
if action == 'restore' and gl_ipsec_conf:
- ret = subprocess.Popen(['sudo', 'ipsec', 'start'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
- return
-
- ret = subprocess.Popen(['sudo', 'ipsec', action], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
+ return run('sudo ipsec start')
+ return run(f'sudo ipsec {action}')
def verify(c):
# Check if QAT service installed
@@ -66,10 +63,9 @@ def verify(c):
return
# Check if QAT device exist
- ret = subprocess.Popen(['sudo', 'lspci', '-nn'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
+ output, err = popen('sudo lspci -nn', decode='utf-8')
if not err:
- data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output.decode("utf-8"))
+ data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output)
#If QAT devices found
if not data:
print("\t No QAT acceleration device found")
@@ -82,17 +78,13 @@ def apply(c):
# Disable QAT service
if c['qat_conf'] == None:
- ret = subprocess.Popen(['sudo', '/etc/init.d/vyos-qat-utilities', 'stop'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
+ run('sudo /etc/init.d/vyos-qat-utilities stop')
if c['ipsec_conf']:
vpn_control('start')
-
return
# Run qat init.d script
- ret = subprocess.Popen(['sudo', '/etc/init.d/vyos-qat-utilities', 'start'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
-
+ run('sudo /etc/init.d/vyos-qat-utilities start')
if c['ipsec_conf']:
# Recovery VPN service
vpn_control('start')
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 6cdfb764c..32aa2826b 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -21,9 +21,11 @@ from sys import exit
from netifaces import interfaces
from vyos.ifconfig import BondIf
-from vyos.ifconfig_vlan import apply_vlan_config
+from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
from vyos.configdict import list_diff, vlan_to_dict
from vyos.config import Config
+from vyos.util import is_bridge_member
+from vyos.util import call
from vyos import ConfigError
default_config_data = {
@@ -48,6 +50,10 @@ default_config_data = {
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'intf': '',
'mac': '',
'mode': '802.3ad',
@@ -58,7 +64,8 @@ default_config_data = {
'vif_s': [],
'vif_s_remove': [],
'vif': [],
- 'vif_remove': []
+ 'vif_remove': [],
+ 'vrf': ''
}
@@ -85,7 +92,7 @@ def get_config():
if not os.path.isfile('/sys/class/net/bonding_masters'):
import syslog
syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module")
- if os.system('modprobe bonding max_bonds=0 miimon=250') != 0:
+ if call('modprobe bonding max_bonds=0 miimon=250') != 0:
syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module")
raise ConfigError("failed loading bonding kernel module")
@@ -93,10 +100,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- bond['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ bond['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# check if bond has been removed
cfg_base = 'interfaces bonding ' + bond['intf']
@@ -188,6 +195,22 @@ def get_config():
if conf.exists('ip proxy-arp-pvlan'):
bond['ip_proxy_arp_pvlan'] = 1
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ bond['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ bond['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ bond['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ bond['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# Media Access Control (MAC) address
if conf.exists('mac'):
bond['mac'] = conf.return_value('mac')
@@ -221,8 +244,10 @@ def get_config():
if conf.exists('primary'):
bond['primary'] = conf.return_value('primary')
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ bond['vrf'] = conf.return_value('vrf')
+
# get vif-s interfaces (currently effective) - to determine which vif-s
# interface is no longer present and needs to be removed
eff_intf = conf.list_effective_nodes('vif-s')
@@ -253,38 +278,30 @@ def get_config():
def verify(bond):
+ if bond['deleted']:
+ interface = bond['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
+ return None
+
if len (bond['arp_mon_tgt']) > 16:
raise ConfigError('The maximum number of targets that can be specified is 16')
if bond['primary']:
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
raise ConfigError('Mode dependency failed, primary not supported ' \
- 'in this mode.'.format())
-
- if bond['primary'] not in bond['member']:
- raise ConfigError('Interface "{}" is not part of the bond' \
- .format(bond['primary']))
-
-
- # DHCPv6 parameters-only and temporary address are mutually exclusive
- for vif_s in bond['vif_s']:
- if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+ 'in mode "{}"!'.format(bond['mode']))
- for vif_c in vif_s['vif_c']:
- if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- for vif in bond['vif']:
- if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
-
- for vif_s in bond['vif_s']:
- for vif in bond['vif']:
- if vif['id'] == vif_s['id']:
- raise ConfigError('Can not use identical ID on vif and vif-s interface')
+ vrf_name = bond['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ # use common function to verify VLAN configuration
+ verify_vlan_config(bond)
conf = Config()
for intf in bond['member']:
@@ -427,6 +444,14 @@ def apply(bond):
b.set_proxy_arp(bond['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan'])
+ # IPv6 address autoconfiguration
+ b.set_ipv6_autoconf(bond['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ b.set_ipv6_eui64_address(bond['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ b.set_ipv6_forwarding(bond['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ b.set_ipv6_dad_messages(bond['ipv6_dup_addr_detect'])
# Change interface MAC address
if bond['mac']:
@@ -442,7 +467,7 @@ def apply(bond):
# Some parameters can not be changed when the bond is up.
if bond['shutdown_required']:
# Disable bond prior changing of certain properties
- b.set_state('down')
+ b.set_admin_state('down')
# The bonding mode can not be changed when there are interfaces enslaved
# to this bond, thus we will free all interfaces from the bond first!
@@ -460,9 +485,9 @@ def apply(bond):
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not bond['disable']:
- b.set_state('up')
+ b.set_admin_state('up')
else:
- b.set_state('down')
+ b.set_admin_state('down')
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -472,6 +497,9 @@ def apply(bond):
for addr in bond['address']:
b.add_addr(addr)
+ # assign/remove VRF
+ b.set_vrf(bond['vrf'])
+
# remove no longer required service VLAN interfaces (vif-s)
for vif_s in bond['vif_s_remove']:
b.del_vlan(vif_s)
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index a3213f309..79247ee51 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -20,7 +20,8 @@ from copy import deepcopy
from sys import exit
from netifaces import interfaces
-from vyos.ifconfig import BridgeIf, STPIf
+from vyos.ifconfig import BridgeIf
+from vyos.ifconfig.stp import STP
from vyos.configdict import list_diff
from vyos.config import Config
from vyos import ConfigError
@@ -45,6 +46,10 @@ default_config_data = {
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'igmp_querier': 0,
'intf': '',
'mac' : '',
@@ -52,7 +57,8 @@ default_config_data = {
'member': [],
'member_remove': [],
'priority': 32768,
- 'stp': 0
+ 'stp': 0,
+ 'vrf': ''
}
def get_config():
@@ -60,10 +66,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if bridge has been removed
if not conf.exists('interfaces bridge ' + bridge['intf']):
@@ -150,6 +156,22 @@ def get_config():
if conf.exists('ip enable-arp-ignore'):
bridge['ip_enable_arp_ignore'] = 1
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ bridge['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ bridge['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ bridge['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ bridge['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# Media Access Control (MAC) address
if conf.exists('mac'):
bridge['mac'] = conf.return_value('mac')
@@ -191,12 +213,20 @@ def get_config():
if conf.exists('stp'):
bridge['stp'] = 1
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ bridge['vrf'] = conf.return_value('vrf')
+
return bridge
def verify(bridge):
if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+ vrf_name = bridge['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
conf = Config()
for br in conf.list_nodes('interfaces bridge'):
# it makes no sense to verify ourself in this case
@@ -213,6 +243,9 @@ def verify(bridge):
if intf['name'] not in interfaces():
raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf']))
+ if intf['name'] == 'lo':
+ raise ConfigError('Loopback interface "lo" can not be added to a bridge')
+
# bridge members are not allowed to be bond members, too
for intf in bridge['member']:
for bond in conf.list_nodes('interfaces bonding'):
@@ -233,7 +266,7 @@ def apply(bridge):
br.remove()
else:
# enable interface
- br.set_state('up')
+ br.set_admin_state('up')
# set ageing time
br.set_ageing_time(bridge['aging'])
# set bridge forward delay
@@ -248,6 +281,14 @@ def apply(bridge):
br.set_arp_announce(bridge['ip_enable_arp_announce'])
# configure ARP ignore
br.set_arp_ignore(bridge['ip_enable_arp_ignore'])
+ # IPv6 address autoconfiguration
+ br.set_ipv6_autoconf(bridge['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ br.set_ipv6_eui64_address(bridge['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ br.set_ipv6_forwarding(bridge['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ br.set_ipv6_dad_messages(bridge['ipv6_dup_addr_detect'])
# set max message age
br.set_max_age(bridge['max_age'])
# set bridge priority
@@ -286,6 +327,9 @@ def apply(bridge):
# store DHCPv6 config dictionary - used later on when addresses are aquired
br.set_dhcpv6_options(opt)
+ # assign/remove VRF
+ br.set_vrf(bridge['vrf'])
+
# Change interface MAC address
if bridge['mac']:
br.set_mac(bridge['mac'])
@@ -300,7 +344,7 @@ def apply(bridge):
# up/down interface
if bridge['disable']:
- br.set_state('down')
+ br.set_admin_state('down')
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -310,9 +354,10 @@ def apply(bridge):
for addr in bridge['address']:
br.add_addr(addr)
+ STPBridgeIf = STP.enable(BridgeIf)
# configure additional bridge member options
for member in bridge['member']:
- i = STPIf(member['name'])
+ i = STPBridgeIf(member['name'])
# configure ARP cache timeout
i.set_arp_cache_tmo(bridge['arp_cache_tmo'])
# ignore link state changes
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index eb0145f65..a256103af 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -18,10 +18,12 @@ import os
from copy import deepcopy
from sys import exit
+from netifaces import interfaces
from vyos.ifconfig import DummyIf
from vyos.configdict import list_diff
from vyos.config import Config
+from vyos.util import is_bridge_member
from vyos import ConfigError
default_config_data = {
@@ -30,7 +32,8 @@ default_config_data = {
'deleted': False,
'description': '',
'disable': False,
- 'intf': ''
+ 'intf': '',
+ 'vrf': ''
}
def get_config():
@@ -38,10 +41,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces dummy ' + dummy['intf']):
@@ -69,9 +72,27 @@ def get_config():
act_addr = conf.return_values('address')
dummy['address_remove'] = list_diff(eff_addr, act_addr)
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ dummy['vrf'] = conf.return_value('vrf')
+
return dummy
def verify(dummy):
+ if dummy['deleted']:
+ interface = dummy['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
+ return None
+
+ vrf_name = dummy['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
return None
def generate(dummy):
@@ -95,11 +116,14 @@ def apply(dummy):
for addr in dummy['address']:
d.add_addr(addr)
+ # assign/remove VRF
+ d.set_vrf(dummy['vrf'])
+
# disable interface on demand
if dummy['disable']:
- d.set_state('down')
+ d.set_admin_state('down')
else:
- d.set_state('up')
+ d.set_admin_state('up')
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index e4f6e5ff2..15e9b4185 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -16,11 +16,12 @@
import os
-from copy import deepcopy
from sys import exit
+from copy import deepcopy
+from netifaces import interfaces
from vyos.ifconfig import EthernetIf
-from vyos.ifconfig_vlan import apply_vlan_config
+from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
from vyos.configdict import list_diff, vlan_to_dict
from vyos.config import Config
from vyos import ConfigError
@@ -47,6 +48,10 @@ default_config_data = {
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'intf': '',
'mac': '',
'mtu': 1500,
@@ -59,7 +64,8 @@ default_config_data = {
'vif_s': [],
'vif_s_remove': [],
'vif': [],
- 'vif_remove': []
+ 'vif_remove': [],
+ 'vrf': ''
}
def get_config():
@@ -67,10 +73,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- eth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ eth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# check if ethernet interface has been removed
cfg_base = ['interfaces', 'ethernet', eth['intf']]
@@ -165,6 +171,22 @@ def get_config():
if conf.exists('ip proxy-arp-pvlan'):
eth['ip_proxy_arp_pvlan'] = 1
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ eth['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ eth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ eth['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ eth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# Media Access Control (MAC) address
if conf.exists('mac'):
eth['mac'] = conf.return_value('mac')
@@ -197,6 +219,10 @@ def get_config():
if conf.exists('speed'):
eth['speed'] = conf.return_value('speed')
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ eth['vrf'] = conf.return_value('vrf')
+
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
# get vif-s interfaces (currently effective) - to determine which vif-s
@@ -232,6 +258,9 @@ def verify(eth):
if eth['deleted']:
return None
+ if eth['intf'] not in interfaces():
+ raise ConfigError(f"Interface ethernet {eth['intf']} does not exist")
+
if eth['speed'] == 'auto':
if eth['duplex'] != 'auto':
raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too')
@@ -243,6 +272,10 @@ def verify(eth):
if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+ vrf_name = eth['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
conf = Config()
# some options can not be changed when interface is enslaved to a bond
for bond in conf.list_nodes('interfaces bonding'):
@@ -250,21 +283,10 @@ def verify(eth):
bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface')
if eth['intf'] in bond_member:
if eth['address']:
- raise ConfigError('Can not assign address to interface {} which is a member of {}').format(eth['intf'], bond)
-
- # DHCPv6 parameters-only and temporary address are mutually exclusive
- for vif_s in eth['vif_s']:
- if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- for vif_c in vif_s['vif_c']:
- if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- for vif in eth['vif']:
- if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+ raise ConfigError(f"Can not assign address to interface {eth['intf']} which is a member of {bond}")
+ # use common function to verify VLAN configuration
+ verify_vlan_config(eth)
return None
def generate(eth):
@@ -324,12 +346,20 @@ def apply(eth):
e.set_proxy_arp(eth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan'])
+ # IPv6 address autoconfiguration
+ e.set_ipv6_autoconf(eth['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ e.set_ipv6_eui64_address(eth['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ e.set_ipv6_forwarding(eth['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ e.set_ipv6_dad_messages(eth['ipv6_dup_addr_detect'])
# Change interface MAC address - re-set to real hardware address (hw-id)
# if custom mac is removed
if eth['mac']:
e.set_mac(eth['mac'])
- else:
+ elif eth['hw_id']:
e.set_mac(eth['hw_id'])
# Maximum Transmission Unit (MTU)
@@ -355,9 +385,9 @@ def apply(eth):
# Enable/Disable interface
if eth['disable']:
- e.set_state('down')
+ e.set_admin_state('down')
else:
- e.set_state('up')
+ e.set_admin_state('up')
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -367,6 +397,9 @@ def apply(eth):
for addr in eth['address']:
e.add_addr(addr)
+ # assign/remove VRF
+ e.set_vrf(eth['vrf'])
+
# remove no longer required service VLAN interfaces (vif-s)
for vif_s in eth['vif_s_remove']:
e.del_vlan(vif_s)
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index b0c381656..e47473d76 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -18,11 +18,12 @@ import os
from sys import exit
from copy import deepcopy
+from netifaces import interfaces
from vyos.config import Config
-from vyos.ifconfig import GeneveIf, Interface
+from vyos.ifconfig import GeneveIf
+from vyos.util import is_bridge_member
from vyos import ConfigError
-from netifaces import interfaces
default_config_data = {
'address': [],
@@ -42,10 +43,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces geneve ' + geneve['intf']):
@@ -92,7 +93,13 @@ def get_config():
def verify(geneve):
if geneve['deleted']:
- # bail out early
+ interface = geneve['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
if not geneve['remote']:
@@ -127,7 +134,7 @@ def apply(geneve):
conf['remote'] = geneve['remote']
# Finally create the new interface
- g = GeneveIf(geneve['intf'], config=conf)
+ g = GeneveIf(geneve['intf'], **conf)
# update interface description used e.g. by SNMP
g.set_alias(geneve['description'])
# Maximum Transfer Unit (MTU)
@@ -148,7 +155,7 @@ def apply(geneve):
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not geneve['disable']:
- g.set_state('up')
+ g.set_admin_state('up')
return None
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index ae49dadad..11ba9acdd 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -22,6 +22,8 @@ from copy import deepcopy
from vyos.config import Config
from vyos.ifconfig import L2TPv3If, Interface
from vyos import ConfigError
+from vyos.util import call
+from vyos.util import is_bridge_member
from netifaces import interfaces
default_config_data = {
@@ -33,6 +35,10 @@ default_config_data = {
'local_address': '',
'local_port': 5000,
'intf': '',
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'mtu': 1488,
'peer_session_id': '',
'peer_tunnel_id': '',
@@ -42,15 +48,22 @@ default_config_data = {
'tunnel_id': ''
}
+def check_kmod():
+ modules = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6']
+ for module in modules:
+ if not os.path.exists(f'/sys/module/{module}'):
+ if call(f'modprobe {module}') != 0:
+ raise ConfigError(f'Loading Kernel module {module} failed')
+
def get_config():
l2tpv3 = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
- try:
- l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']):
@@ -94,6 +107,22 @@ def get_config():
if conf.exists('local-ip'):
l2tpv3['local_address'] = conf.return_value('local-ip')
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ l2tpv3['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ l2tpv3['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ l2tpv3['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ l2tpv3['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
l2tpv3['mtu'] = int(conf.return_value('mtu'))
@@ -126,61 +155,41 @@ def get_config():
def verify(l2tpv3):
+ interface = l2tpv3['intf']
+
if l2tpv3['deleted']:
- # bail out early
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
if not l2tpv3['local_address']:
- raise ConfigError('Must configure the l2tpv3 local-ip for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}')
if not l2tpv3['remote_address']:
- raise ConfigError('Must configure the l2tpv3 remote-ip for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}')
if not l2tpv3['tunnel_id']:
- raise ConfigError('Must configure the l2tpv3 tunnel-id for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 tunnel-id for {interface}')
if not l2tpv3['peer_tunnel_id']:
- raise ConfigError('Must configure the l2tpv3 peer-tunnel-id for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 peer-tunnel-id for {interface}')
if not l2tpv3['session_id']:
- raise ConfigError('Must configure the l2tpv3 session-id for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 session-id for {interface}')
if not l2tpv3['peer_session_id']:
- raise ConfigError('Must configure the l2tpv3 peer-session-id for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}')
return None
def generate(l2tpv3):
- if l2tpv3['deleted']:
- # bail out early
- return None
-
- # initialize kernel module if not loaded
- if not os.path.isdir('/sys/module/l2tp_eth'):
- if os.system('modprobe l2tp_eth') != 0:
- raise ConfigError("failed loading l2tp_eth kernel module")
-
- if not os.path.isdir('/sys/module/l2tp_netlink'):
- if os.system('modprobe l2tp_netlink') != 0:
- raise ConfigError("failed loading l2tp_netlink kernel module")
-
- if not os.path.isdir('/sys/module/l2tp_ip'):
- if os.system('modprobe l2tp_ip') != 0:
- raise ConfigError("failed loading l2tp_ip kernel module")
-
- if l2tpv3['encapsulation'] == 'ip':
- if not os.path.isdir('/sys/module/l2tp_ip'):
- if os.system('modprobe l2tp_ip') != 0:
- raise ConfigError("failed loading l2tp_ip kernel module")
-
- if not os.path.isdir('/sys/module/l2tp_ip6 '):
- if os.system('modprobe l2tp_ip6 ') != 0:
- raise ConfigError("failed loading l2tp_ip6 kernel module")
-
return None
-
def apply(l2tpv3):
# L2TPv3 interface needs to be created/deleted on-block, instead of
# passing a ton of arguments, I just use a dict that is managed by
@@ -193,7 +202,7 @@ def apply(l2tpv3):
# always delete it first.
conf['session_id'] = l2tpv3['session_id']
conf['tunnel_id'] = l2tpv3['tunnel_id']
- l = L2TPv3If(l2tpv3['intf'], config=conf)
+ l = L2TPv3If(l2tpv3['intf'], **conf)
l.remove()
if not l2tpv3['deleted']:
@@ -208,11 +217,19 @@ def apply(l2tpv3):
conf['peer_session_id'] = l2tpv3['peer_session_id']
# Finally create the new interface
- l = L2TPv3If(l2tpv3['intf'], config=conf)
+ l = L2TPv3If(l2tpv3['intf'], **conf)
# update interface description used e.g. by SNMP
l.set_alias(l2tpv3['description'])
# Maximum Transfer Unit (MTU)
l.set_mtu(l2tpv3['mtu'])
+ # IPv6 address autoconfiguration
+ l.set_ipv6_autoconf(l2tpv3['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ l.set_ipv6_eui64_address(l2tpv3['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ l.set_ipv6_forwarding(l2tpv3['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ l.set_ipv6_dad_messages(l2tpv3['ipv6_dup_addr_detect'])
# Configure interface address(es) - no need to implicitly delete the
# old addresses as they have already been removed by deleting the
@@ -224,12 +241,13 @@ def apply(l2tpv3):
# we will only re-enable the interface if it is not administratively
# disabled
if not l2tpv3['disable']:
- l.set_state('up')
+ l.set_admin_state('up')
return None
if __name__ == '__main__':
try:
+ check_kmod()
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
index 10722d137..ddd18ae24 100755
--- a/src/conf_mode/interfaces-loopback.py
+++ b/src/conf_mode/interfaces-loopback.py
@@ -37,10 +37,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces loopback ' + loopback['intf']):
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 6bd269e97..f34e4f7fe 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 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,262 +17,27 @@
import os
import re
-from jinja2 import Template
+from jinja2 import FileSystemLoader, Environment
from copy import deepcopy
from sys import exit
from stat import S_IRUSR,S_IRWXU,S_IRGRP,S_IXGRP,S_IROTH,S_IXOTH
from grp import getgrnam
from ipaddress import ip_address,ip_network,IPv4Interface
from netifaces import interfaces
-from psutil import pid_exists
from pwd import getpwnam
-from subprocess import Popen, PIPE
from time import sleep
+from shutil import rmtree
-from vyos import ConfigError
from vyos.config import Config
-from vyos.ifconfig import Interface
+from vyos.defaults import directories as vyos_data_dir
+from vyos.ifconfig import VTunIf
+from vyos.util import process_running, cmd, is_bridge_member
from vyos.validate import is_addr_assigned
+from vyos import ConfigError
user = 'openvpn'
group = 'openvpn'
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by interfaces-openvpn.py ###
-#
-# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
-# for individual keyword definition
-
-{% if description %}
-# {{ description }}
-{% endif %}
-
-verb 3
-status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30
-writepid /var/run/openvpn/{{ intf }}.pid
-
-dev-type {{ type }}
-dev {{ intf }}
-user {{ uid }}
-group {{ gid }}
-persist-key
-iproute /usr/libexec/vyos/system/unpriv-ip
-
-proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %}
-
-{%- if local_host %}
-local {{ local_host }}
-{% endif %}
-
-{%- if local_port %}
-lport {{ local_port }}
-{% endif %}
-
-{%- if remote_port %}
-rport {{ remote_port }}
-{% endif %}
-
-{%- if remote_host %}
-{% for remote in remote_host -%}
-remote {{ remote }}
-{% endfor -%}
-{% endif %}
-
-{%- if shared_secret_file %}
-secret {{ shared_secret_file }}
-{% endif %}
-
-{%- if persistent_tunnel %}
-persist-tun
-{% endif %}
-
-{%- if mode %}
-{%- if 'client' in mode %}
-#
-# OpenVPN Client mode
-#
-client
-nobind
-{%- elif 'server' in mode %}
-#
-# OpenVPN Server mode
-#
-mode server
-tls-server
-keepalive {{ ping_interval }} {{ ping_restart }}
-management /tmp/openvpn-mgmt-intf unix
-
-{%- if server_topology %}
-topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %}
-{% endif %}
-
-{% for ns in server_dns_nameserver -%}
-push "dhcp-option DNS {{ ns }}"
-{% endfor -%}
-
-{% for route in server_push_route -%}
-push "route {{ route }}"
-{% endfor -%}
-
-{%- if server_domain %}
-push "dhcp-option DOMAIN {{ server_domain }}"
-{% endif %}
-
-{%- if server_max_conn %}
-max-clients {{ server_max_conn }}
-{% endif %}
-
-{%- if bridge_member %}
-server-bridge nogw
-{%- else %}
-server {{ server_subnet }}
-{% endif %}
-
-{%- if server_reject_unconfigured %}
-ccd-exclusive
-{% endif %}
-
-{%- else %}
-#
-# OpenVPN site-2-site mode
-#
-ping {{ ping_interval }}
-ping-restart {{ ping_restart }}
-
-{%- if local_address_subnet %}
-ifconfig {{ local_address }} {{ local_address_subnet }}
-{% elif remote_address %}
-ifconfig {{ local_address }} {{ remote_address }}
-{% endif %}
-
-{% endif %}
-{% endif %}
-
-{%- if tls_ca_cert %}
-ca {{ tls_ca_cert }}
-{% endif %}
-
-{%- if tls_cert %}
-cert {{ tls_cert }}
-{% endif %}
-
-{%- if tls_key %}
-key {{ tls_key }}
-{% endif %}
-
-{%- if tls_crl %}
-crl-verify {{ tls_crl }}
-{% endif %}
-
-{%- if tls_version_min %}
-tls-version-min {{tls_version_min}}
-{% endif %}
-
-{%- if tls_dh %}
-dh {{ tls_dh }}
-{% endif %}
-
-{%- if tls_auth %}
-tls-auth {{tls_auth}}
-{% endif %}
-
-{%- if 'active' in tls_role %}
-tls-client
-{%- elif 'passive' in tls_role %}
-tls-server
-{% endif %}
-
-{%- if redirect_gateway %}
-push "redirect-gateway {{ redirect_gateway }}"
-{% endif %}
-
-{%- if compress_lzo %}
-compress lzo
-{% endif %}
-
-{%- if hash %}
-auth {{ hash }}
-{% endif %}
-
-{%- if encryption %}
-{%- if 'des' in encryption %}
-cipher des-cbc
-{%- elif '3des' in encryption %}
-cipher des-ede3-cbc
-{%- elif 'bf128' in encryption %}
-cipher bf-cbc
-keysize 128
-{%- elif 'bf256' in encryption %}
-cipher bf-cbc
-keysize 25
-{%- elif 'aes128gcm' in encryption %}
-cipher aes-128-gcm
-{%- elif 'aes128' in encryption %}
-cipher aes-128-cbc
-{%- elif 'aes192gcm' in encryption %}
-cipher aes-192-gcm
-{%- elif 'aes192' in encryption %}
-cipher aes-192-cbc
-{%- elif 'aes256gcm' in encryption %}
-cipher aes-256-gcm
-{%- elif 'aes256' in encryption %}
-cipher aes-256-cbc
-{% endif %}
-{% endif %}
-
-{%- if ncp_ciphers %}
-ncp-ciphers {{ncp_ciphers}}
-{% endif %}
-{%- if disable_ncp %}
-ncp-disable
-{% endif %}
-
-{%- if auth %}
-auth-user-pass /tmp/openvpn-{{ intf }}-pw
-auth-retry nointeract
-{% endif %}
-
-{%- if client %}
-client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }}
-{% endif %}
-
-# DEPRECATED This option will be removed in OpenVPN 2.5
-# Until OpenVPN v2.3 the format of the X.509 Subject fields was formatted like this:
-# /C=US/L=Somewhere/CN=John Doe/emailAddress=john@example.com In addition the old
-# behaviour was to remap any character other than alphanumeric, underscore ('_'),
-# dash ('-'), dot ('.'), and slash ('/') to underscore ('_'). The X.509 Subject
-# string as returned by the tls_id environmental variable, could additionally
-# contain colon (':') or equal ('='). When using the --compat-names option, this
-# old formatting and remapping will be re-enabled again. This is purely implemented
-# for compatibility reasons when using older plug-ins or scripts which does not
-# handle the new formatting or UTF-8 characters.
-#
-# See https://phabricator.vyos.net/T1512
-compat-names
-
-{% for option in options -%}
-{{ option }}
-{% endfor -%}
-"""
-
-client_tmpl = """
-### Autogenerated by interfaces-openvpn.py ###
-
-ifconfig-push {{ ip }} {{ remote_netmask }}
-{% for route in push_route -%}
-push "route {{ route }}"
-{% endfor -%}
-
-{% for net in subnet -%}
-iroute {{ net }}
-{% endfor -%}
-
-{%- if disable %}
-disable
-{% endif %}
-"""
-
default_config_data = {
'address': [],
'auth_user': '',
@@ -287,6 +52,10 @@ default_config_data = {
'encryption': '',
'hash': '',
'intf': '',
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'ping_restart': '60',
'ping_interval': '10',
'local_address': '',
@@ -297,7 +66,7 @@ default_config_data = {
'ncp_ciphers': '',
'options': [],
'persistent_tunnel': False,
- 'protocol': '',
+ 'protocol': 'udp',
'redirect_gateway': '',
'remote_address': '',
'remote_host': [],
@@ -318,6 +87,7 @@ default_config_data = {
'tls_crl': '',
'tls_dh': '',
'tls_key': '',
+ 'tls_crypt': '',
'tls_role': '',
'tls_version_min': '',
'type': 'tun',
@@ -325,9 +95,6 @@ default_config_data = {
'gid': group,
}
-def subprocess_cmd(command):
- p = Popen(command, stdout=PIPE, shell=True)
- p.communicate()
def get_config_name(intf):
cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf)
@@ -360,7 +127,7 @@ def fixup_permission(filename, permission=S_IRUSR):
def checkCertHeader(header, filename):
"""
Verify if filename contains specified header.
- Returns True on success or on file not found to not trigger the exceptions
+ Returns True if match is found, False if no match or file is not found
"""
if not os.path.isfile(filename):
return False
@@ -370,17 +137,17 @@ def checkCertHeader(header, filename):
if re.match(header, line):
return True
- return True
+ return False
def get_config():
openvpn = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
- try:
- openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface instance has been removed
if not conf.exists('interfaces openvpn ' + openvpn['intf']):
@@ -482,10 +249,25 @@ def get_config():
if conf.exists('local-port'):
openvpn['local_port'] = conf.return_value('local-port')
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ openvpn['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ openvpn['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ openvpn['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ openvpn['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# OpenVPN operation mode
if conf.exists('mode'):
- mode = conf.return_value('mode')
- openvpn['mode'] = mode
+ openvpn['mode'] = conf.return_value('mode')
# Additional OpenVPN options
if conf.exists('openvpn-option'):
@@ -633,6 +415,11 @@ def get_config():
openvpn['tls_key'] = conf.return_value('tls key-file')
openvpn['tls'] = True
+ # File containing key to encrypt control channel packets
+ if conf.exists('tls crypt-file'):
+ openvpn['tls_crypt'] = conf.return_value('tls crypt-file')
+ openvpn['tls'] = True
+
# Role in TLS negotiation
if conf.exists('tls role'):
openvpn['tls_role'] = conf.return_value('tls role')
@@ -641,6 +428,7 @@ def get_config():
# Minimum required TLS version
if conf.exists('tls tls-version-min'):
openvpn['tls_version_min'] = conf.return_value('tls tls-version-min')
+ openvpn['tls'] = True
if conf.exists('shared-secret-key-file'):
openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file')
@@ -648,12 +436,25 @@ def get_config():
if conf.exists('use-lzo-compression'):
openvpn['compress_lzo'] = True
+ # Special case when using EC certificates:
+ # if key-file is EC and dh-file is unset, set tls_dh to 'none'
+ if not openvpn['tls_dh'] and openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ openvpn['tls_dh'] = 'none'
+
return openvpn
def verify(openvpn):
if openvpn['deleted']:
+ interface = openvpn['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
+
if not openvpn['mode']:
raise ConfigError('Must specify OpenVPN operation mode')
@@ -682,7 +483,7 @@ def verify(openvpn):
if not openvpn['remote_host']:
raise ConfigError('Must specify "remote-host" in client mode')
- if openvpn['tls_dh']:
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
raise ConfigError('Cannot specify "tls dh-file" in client mode')
#
@@ -732,8 +533,8 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-passive' and len(openvpn['remote_host']) > 1:
raise ConfigError('Cannot specify more than 1 "remote-host" with "tcp-passive"')
- if not openvpn['tls_dh']:
- raise ConfigError('Must specify "tls dh-file" in server mode')
+ if not openvpn['tls_dh'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode')
if not openvpn['server_subnet']:
if not openvpn['bridge_member']:
@@ -800,6 +601,9 @@ def verify(openvpn):
if not openvpn['tls_key']:
raise ConfigError('Must specify "tls key-file"')
+ if openvpn['tls_auth'] and openvpn['tls_crypt']:
+ raise ConfigError('TLS auth and crypt are mutually exclusive')
+
if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']):
raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert']))
@@ -812,14 +616,18 @@ def verify(openvpn):
raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert']))
if openvpn['tls_key']:
- if not checkCertHeader('-----BEGIN (?:RSA )?PRIVATE KEY-----', openvpn['tls_key']):
+ if not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', openvpn['tls_key']):
raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key']))
+ if openvpn['tls_crypt']:
+ if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_crypt']):
+ raise ConfigError('Specified TLS crypt-file "{}" is invalid'.format(openvpn['tls_crypt']))
+
if openvpn['tls_crl']:
if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']):
raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl']))
- if openvpn['tls_dh']:
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
if not checkCertHeader('-----BEGIN DH PARAMETERS-----', openvpn['tls_dh']):
raise ConfigError('Specified dh-file "{}" is not valid'.format(openvpn['tls_dh']))
@@ -832,7 +640,7 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
- if openvpn['tls_dh']:
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"')
elif openvpn['tls_role'] == 'passive':
@@ -842,6 +650,12 @@ def verify(openvpn):
if not openvpn['tls_dh']:
raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"')
+ if openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
+ print('Warning: using dh-file and EC keys simultaneously will lead to DH ciphers being used instead of ECDH')
+ else:
+ print('Diffie-Hellman prime file is unspecified, assuming ECDH')
+
#
# Auth user/pass
#
@@ -857,20 +671,27 @@ def verify(openvpn):
#
subnet = openvpn['server_subnet'].replace(' ', '/')
for client in openvpn['client']:
- if not ip_address(client['ip']) in ip_network(subnet):
+ if client['ip'] and not ip_address(client['ip']) in ip_network(subnet):
raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet))
-
-
return None
def generate(openvpn):
if openvpn['deleted'] or openvpn['disable']:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'openvpn')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
interface = openvpn['intf']
directory = os.path.dirname(get_config_name(interface))
+ # we can't know which clients were deleted, remove all client configs
+ if os.path.isdir(os.path.join(directory, 'ccd', interface)):
+ rmtree(os.path.join(directory, 'ccd', interface), ignore_errors=True)
+
# create config directory on demand
openvpn_mkdir(directory)
# create status directory on demand
@@ -892,6 +713,11 @@ def generate(openvpn):
fixup_permission(auth_file)
+ else:
+ # delete old auth file if present
+ if os.path.isfile('/tmp/openvpn-{}-pw'.format(interface)):
+ os.remove('/tmp/openvpn-{}-pw'.format(interface))
+
# get numeric uid/gid
uid = getpwnam(user).pw_uid
gid = getgrnam(group).gr_gid
@@ -899,19 +725,17 @@ def generate(openvpn):
# Generate client specific configuration
for client in openvpn['client']:
client_file = directory + '/ccd/' + interface + '/' + client['name']
- tmpl = Template(client_tmpl)
+ tmpl = env.get_template('client.conf.tmpl')
client_text = tmpl.render(client)
with open(client_file, 'w') as f:
f.write(client_text)
os.chown(client_file, uid, gid)
- tmpl = Template(config_tmpl)
+ tmpl = env.get_template('server.conf.tmpl')
config_text = tmpl.render(openvpn)
-
# we need to support quoting of raw parameters from OpenVPN CLI
# see https://phabricator.vyos.net/T1632
config_text = config_text.replace("&quot;",'"')
-
with open(get_config_name(interface), 'w') as f:
f.write(config_text)
os.chown(get_config_name(interface), uid, gid)
@@ -919,20 +743,18 @@ def generate(openvpn):
return None
def apply(openvpn):
- pid = 0
pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf'])
- if os.path.isfile(pidfile):
- pid = 0
- with open(pidfile, 'r') as f:
- pid = int(f.read())
# Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the
# service as the configuration is not re-read. Stop daemon only if it's
# running - it could have died or killed by someone evil
- if pid_exists(pid):
- cmd = 'start-stop-daemon --stop --quiet'
- cmd += ' --pidfile ' + pidfile
- subprocess_cmd(cmd)
+ if process_running(pidfile):
+ command = 'start-stop-daemon'
+ command += ' --stop '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ cmd(command)
# cleanup old PID file
if os.path.isfile(pidfile):
@@ -946,11 +768,12 @@ def apply(openvpn):
# cleanup client config dir
directory = os.path.dirname(get_config_name(openvpn['intf']))
- if os.path.isdir(directory + '/ccd/' + openvpn['intf']):
- try:
- os.remove(directory + '/ccd/' + openvpn['intf'] + '/*')
- except:
- pass
+ if os.path.isdir(os.path.join(directory, 'ccd', openvpn['intf'])):
+ rmtree(os.path.join(directory, 'ccd', openvpn['intf']), ignore_errors=True)
+
+ # cleanup auth file
+ if os.path.isfile('/tmp/openvpn-{}-pw'.format(openvpn['intf'])):
+ os.remove('/tmp/openvpn-{}-pw'.format(openvpn['intf']))
return None
@@ -962,16 +785,19 @@ def apply(openvpn):
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- cmd = 'start-stop-daemon --start --quiet'
- cmd += ' --pidfile ' + pidfile
- cmd += ' --exec /usr/sbin/openvpn'
+ command = 'start-stop-daemon'
+ command += ' --start '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ command += ' --exec /usr/sbin/openvpn'
# now pass arguments to openvpn binary
- cmd += ' --'
- cmd += ' --daemon openvpn-' + openvpn['intf']
- cmd += ' --config ' + get_config_name(openvpn['intf'])
+ command += ' --'
+ command += ' --daemon openvpn-' + openvpn['intf']
+ command += ' --config ' + get_config_name(openvpn['intf'])
# execute assembled command
- subprocess_cmd(cmd)
+ cmd(command)
# better late then sorry ... but we can only set interface alias after
# OpenVPN has been launched and created the interface
@@ -991,14 +817,25 @@ def apply(openvpn):
try:
# we need to catch the exception if the interface is not up due to
# reason stated above
- Interface(openvpn['intf']).set_alias(openvpn['description'])
+ o = VTunIf(openvpn['intf'])
+ # update interface description used e.g. within SNMP
+ o.set_alias(openvpn['description'])
+ # IPv6 address autoconfiguration
+ o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ o.set_ipv6_eui64_address(openvpn['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ o.set_ipv6_forwarding(openvpn['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ o.set_ipv6_dad_messages(openvpn['ipv6_dup_addr_detect'])
+
except:
pass
# TAP interface needs to be brought up explicitly
if openvpn['type'] == 'tap':
if not openvpn['disable']:
- Interface(openvpn['intf']).set_state('up')
+ VTunIf(openvpn['intf']).set_admin_state('up')
return None
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index 6acb45d5e..353a5a12c 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -18,85 +18,14 @@ import os
from sys import exit
from copy import deepcopy
-from jinja2 import Template
-from subprocess import Popen, PIPE
-from time import sleep
-from pwd import getpwnam
-from grp import getgrnam
+from jinja2 import FileSystemLoader, Environment
+from netifaces import interfaces
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos.ifconfig import Interface
+from vyos.util import chown, chmod_x, cmd
from vyos import ConfigError
-from netifaces import interfaces
-
-# Please be careful if you edit the template.
-config_pppoe_tmpl = """
-### Autogenerated by interfaces-pppoe.py ###
-
-{% if description %}
-# {{ description }}
-{% endif %}
-
-# Require peer to provide the local IP address if it is not
-# specified explicitly in the config file.
-noipdefault
-
-# Don't show the password in logfiles:
-hide-password
-
-# Standard Link Control Protocol (LCP) parameters:
-lcp-echo-interval 20
-lcp-echo-failure 3
-
-# RFC 2516, paragraph 7 mandates that the following options MUST NOT be
-# requested and MUST be rejected if requested by the peer:
-# Address-and-Control-Field-Compression (ACFC)
-noaccomp
-
-# Asynchronous-Control-Character-Map (ACCM)
-default-asyncmap
-
-# Override any connect script that may have been set in /etc/ppp/options.
-connect /bin/true
-
-# Don't try to authenticate the remote node
-noauth
-
-# Don't try to proxy ARP for the remote endpoint. User can set proxy
-# arp entries up manually if they wish. More importantly, having
-# the "proxyarp" parameter set disables the "defaultroute" option.
-noproxyarp
-
-plugin rp-pppoe.so
-{{ source_interface }}
-persist
-ifname {{ intf }}
-ipparam {{ intf }}
-debug
-logfile {{ logfile }}
-{% if 'auto' in default_route -%}
-defaultroute
-{% elif 'force' in default_route -%}
-defaultroute
-replacedefaultroute
-{% endif %}
-mtu {{ mtu }}
-mru {{ mtu }}
-user "{{ auth_username }}"
-password "{{ auth_password }}"
-{% if name_server -%}
-usepeerdns
-{% endif %}
-{% if ipv6_enable -%}
-+ipv6
-{% endif %}
-{% if service_name -%}
-rp_pppoe_service "{{ service_name }}"
-{% endif %}
-
-"""
-
-PPP_LOGFILE = '/var/log/vyatta/ppp_{}.log'
default_config_data = {
'access_concentrator': '',
@@ -105,7 +34,7 @@ default_config_data = {
'on_demand': False,
'default_route': 'auto',
'deleted': False,
- 'description': '',
+ 'description': '\0',
'disable': False,
'intf': '',
'idle_timeout': '',
@@ -117,24 +46,21 @@ default_config_data = {
'name_server': True,
'remote_address': '',
'service_name': '',
- 'source_interface': ''
+ 'source_interface': '',
+ 'vrf': ''
}
-def subprocess_cmd(command):
- p = Popen(command, stdout=PIPE, shell=True)
- p.communicate()
-
def get_config():
pppoe = deepcopy(default_config_data)
conf = Config()
base_path = ['interfaces', 'pppoe']
# determine tagNode instance
- try:
- pppoe['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- pppoe['logfile'] = PPP_LOGFILE.format(pppoe['intf'])
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ pppoe['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ pppoe['logfile'] = f"/var/log/vyatta/ppp_{pppoe['intf']}.log"
# Check if interface has been removed
if not conf.exists(base_path + [pppoe['intf']]):
@@ -190,7 +116,7 @@ def get_config():
# Physical Interface used for this PPPoE session
if conf.exists(['source-interface']):
- pppoe['source_interface'] = conf.return_value('source-interface')
+ pppoe['source_interface'] = conf.return_value(['source-interface'])
# Maximum Transmission Unit (MTU)
if conf.exists(['mtu']):
@@ -208,6 +134,10 @@ def get_config():
if conf.exists(['service-name']):
pppoe['service_name'] = conf.return_value(['service-name'])
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ pppoe['vrf'] = conf.return_value(['vrf'])
+
return pppoe
def verify(pppoe):
@@ -216,32 +146,90 @@ def verify(pppoe):
return None
if not pppoe['source_interface']:
- raise ConfigError('PPPoE source interface is missing')
+ raise ConfigError('PPPoE source interface missing')
+
+ if not pppoe['source_interface'] in interfaces():
+ raise ConfigError(f"PPPoE source interface {pppoe['source_interface']} does not exist")
+
+ vrf_name = pppoe['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF {vrf_name} does not exist')
- if pppoe['source_interface'] not in interfaces():
- raise ConfigError('PPPoE source interface does not exist')
+ if pppoe['on_demand'] and pppoe['vrf']:
+ raise ConfigError('On-demand dialing and VRF can not be used at the same time')
return None
def generate(pppoe):
- config_file_pppoe = '/etc/ppp/peers/{}'.format(pppoe['intf'])
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir["data"], "templates", "pppoe")
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
+ # set up configuration file path variables where our templates will be
+ # rendered into
+ intf = pppoe['intf']
+ config_pppoe = f'/etc/ppp/peers/{intf}'
+ script_pppoe_pre_up = f'/etc/ppp/ip-pre-up.d/1000-vyos-pppoe-{intf}'
+ script_pppoe_ip_up = f'/etc/ppp/ip-up.d/1000-vyos-pppoe-{intf}'
+ script_pppoe_ip_down = f'/etc/ppp/ip-down.d/1000-vyos-pppoe-{intf}'
+ script_pppoe_ipv6_up = f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{intf}'
+
+ config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up,
+ script_pppoe_ip_down, script_pppoe_ipv6_up]
+
+ # Ensure directories for config files exist - otherwise create them on demand
+ for file in config_files:
+ dirname = os.path.dirname(file)
+ if not os.path.isdir(dirname):
+ os.mkdir(dirname)
# Always hang-up PPPoE connection prior generating new configuration file
- cmd = 'systemctl stop ppp@{}.service'.format(pppoe['intf'])
- subprocess_cmd(cmd)
+ cmd(f'systemctl stop ppp@{intf}.service')
if pppoe['deleted']:
# Delete PPP configuration files
- if os.path.exists(config_file_pppoe):
- os.unlink(config_file_pppoe)
+ for file in config_files:
+ if os.path.exists(file):
+ os.unlink(file)
else:
# Create PPP configuration files
- tmpl = Template(config_pppoe_tmpl)
+ tmpl = env.get_template('peer.tmpl')
+ config_text = tmpl.render(pppoe)
+ with open(config_pppoe, 'w') as f:
+ f.write(config_text)
+
+ # Create script for ip-pre-up.d
+ tmpl = env.get_template('ip-pre-up.script.tmpl')
config_text = tmpl.render(pppoe)
- with open(config_file_pppoe, 'w') as f:
+ with open(script_pppoe_pre_up, 'w') as f:
f.write(config_text)
+ # Create script for ip-up.d
+ tmpl = env.get_template('ip-up.script.tmpl')
+ config_text = tmpl.render(pppoe)
+ with open(script_pppoe_ip_up, 'w') as f:
+ f.write(config_text)
+
+ # Create script for ip-down.d
+ tmpl = env.get_template('ip-down.script.tmpl')
+ config_text = tmpl.render(pppoe)
+ with open(script_pppoe_ip_down, 'w') as f:
+ f.write(config_text)
+
+ # Create script for ipv6-up.d
+ tmpl = env.get_template('ipv6-up.script.tmpl')
+ config_text = tmpl.render(pppoe)
+ with open(script_pppoe_ipv6_up, 'w') as f:
+ f.write(config_text)
+
+ # make generated script file executable
+ chmod_x(script_pppoe_pre_up)
+ chmod_x(script_pppoe_ip_up)
+ chmod_x(script_pppoe_ip_down)
+ chmod_x(script_pppoe_ipv6_up)
+
return None
def apply(pppoe):
@@ -250,33 +238,12 @@ def apply(pppoe):
return None
if not pppoe['disable']:
- # dial PPPoE connection
- cmd = 'systemctl start ppp@{}.service'.format(pppoe['intf'])
- subprocess_cmd(cmd)
+ # "dial" PPPoE connection
+ intf = pppoe['intf']
+ cmd(f'systemctl start ppp@{intf}.service')
# make logfile owned by root / vyattacfg
- if os.path.isfile(pppoe['logfile']):
- uid = getpwnam('root').pw_uid
- gid = getgrnam('vyattacfg').gr_gid
- os.chown(pppoe['logfile'], uid, gid)
-
- # better late then sorry ... but we can only set interface alias after
- # pppd has been launched and created the interface
- cnt = 0
- while pppoe['intf'] not in interfaces():
- cnt += 1
- if cnt == 50:
- break
-
- # sleep 250ms
- sleep(0.250)
-
- try:
- # we need to catch the exception if the interface is not up due to
- # reason stated above
- Interface(pppoe['intf']).set_alias(pppoe['description'])
- except:
- pass
+ chown(pppoe['logfile'], 'root', 'vyattacfg')
return None
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index 864e28936..ce3d472c4 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -21,8 +21,10 @@ from sys import exit
from netifaces import interfaces
from vyos.ifconfig import MACVLANIf
-from vyos.configdict import list_diff
+from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
+from vyos.configdict import list_diff, vlan_to_dict
from vyos.config import Config
+from vyos.util import is_bridge_member
from vyos import ConfigError
default_config_data = {
@@ -44,15 +46,20 @@ default_config_data = {
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'intf': '',
- 'link': '',
- 'link_changed': False,
+ 'source_interface': '',
+ 'source_interface_changed': False,
'mac': '',
'mode': 'private',
'vif_s': [],
'vif_s_remove': [],
'vif': [],
- 'vif_remove': []
+ 'vif_remove': [],
+ 'vrf': ''
}
def get_config():
@@ -60,11 +67,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- peth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+ peth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
cfg_base = ['interfaces', 'pseudo-ethernet', peth['intf']]
# Check if interface has been removed
@@ -144,12 +150,28 @@ def get_config():
if conf.exists(['ip', 'proxy-arp-pvlan']):
peth['ip_proxy_arp_pvlan'] = 1
- # Lower link device
- if conf.exists(['link']):
- peth['link'] = conf.return_value(['link'])
- tmp = conf.return_effective_value(['link'])
- if tmp != peth['link']:
- peth['link_changed'] = True
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ peth['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ peth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ peth['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ peth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
+ # Physical interface
+ if conf.exists(['source-interface']):
+ peth['source_interface'] = conf.return_value(['source-interface'])
+ tmp = conf.return_effective_value(['source-interface'])
+ if tmp != peth['source_interface']:
+ peth['source_interface_changed'] = True
# Media Access Control (MAC) address
if conf.exists(['mac']):
@@ -159,6 +181,10 @@ def get_config():
if conf.exists(['mode']):
peth['mode'] = conf.return_value(['mode'])
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ peth['vrf'] = conf.return_value('vrf')
+
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
# get vif-s interfaces (currently effective) - to determine which vif-s
@@ -192,11 +218,27 @@ def get_config():
def verify(peth):
if peth['deleted']:
+ interface = peth['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
- if not peth['link']:
+ if not peth['source_interface']:
raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf']))
+ if not peth['source_interface'] in interfaces():
+ raise ConfigError('Pseudo-ethernet source interface does not exist')
+
+ vrf_name = peth['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
+ # use common function to verify VLAN configuration
+ verify_vlan_config(peth)
return None
def generate(peth):
@@ -211,12 +253,12 @@ def apply(peth):
p.remove()
return None
- elif peth['link_changed']:
+ elif peth['source_interface_changed']:
# Check if MACVLAN interface already exists. Parameters like the
- # underlaying link device can not be changed on the fly and the
- # interface needs to be recreated from the bottom.
+ # underlaying source-interface device can not be changed on the fly
+ # and the interface needs to be recreated from the bottom.
#
- # link_changed also means - the interface was not present in the
+ # source_interface_changed also means - the interface was not present in the
# beginning and is newly created
if peth['intf'] in interfaces():
p = MACVLANIf(peth['intf'])
@@ -227,12 +269,12 @@ def apply(peth):
conf = deepcopy(MACVLANIf.get_config())
# Assign MACVLAN instance configuration parameters to config dict
- conf['link'] = peth['link']
+ conf['source_interface'] = peth['source_interface']
conf['mode'] = peth['mode']
# It is safe to "re-create" the interface always, there is a sanity check
# that the interface will only be create if its non existent
- p = MACVLANIf(peth['intf'], config=conf)
+ p = MACVLANIf(peth['intf'], **conf)
else:
p = MACVLANIf(peth['intf'])
@@ -282,6 +324,17 @@ def apply(peth):
p.set_proxy_arp(peth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan'])
+ # IPv6 address autoconfiguration
+ p.set_ipv6_autoconf(peth['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ p.set_ipv6_eui64_address(peth['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ p.set_ipv6_forwarding(peth['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ p.set_ipv6_dad_messages(peth['ipv6_dup_addr_detect'])
+
+ # assign/remove VRF
+ p.set_vrf(peth['vrf'])
# Change interface MAC address
if peth['mac']:
@@ -292,9 +345,9 @@ def apply(peth):
# Enable/Disable interface
if peth['disable']:
- p.set_state('down')
+ p.set_admin_state('down')
else:
- p.set_state('up')
+ p.set_admin_state('up')
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -304,6 +357,35 @@ def apply(peth):
for addr in peth['address']:
p.add_addr(addr)
+ # remove no longer required service VLAN interfaces (vif-s)
+ for vif_s in peth['vif_s_remove']:
+ p.del_vlan(vif_s)
+
+ # create service VLAN interfaces (vif-s)
+ for vif_s in peth['vif_s']:
+ s_vlan = p.add_vlan(vif_s['id'], ethertype=vif_s['ethertype'])
+ apply_vlan_config(s_vlan, vif_s)
+
+ # remove no longer required client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c in vif_s['vif_c_remove']:
+ s_vlan.del_vlan(vif_c)
+
+ # create client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c in vif_s['vif_c']:
+ c_vlan = s_vlan.add_vlan(vif_c['id'])
+ apply_vlan_config(c_vlan, vif_c)
+
+ # remove no longer required VLAN interfaces (vif)
+ for vif in peth['vif_remove']:
+ p.del_vlan(vif)
+
+ # create VLAN interfaces (vif)
+ for vif in peth['vif']:
+ vlan = p.add_vlan(vif['id'])
+ apply_vlan_config(vlan, vif)
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
new file mode 100755
index 000000000..28b1cf60f
--- /dev/null
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -0,0 +1,646 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 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 os
+import netifaces
+
+from sys import exit
+from copy import deepcopy
+
+from vyos.config import Config
+from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf
+from vyos.ifconfig.afi import IP4, IP6
+from vyos.configdict import list_diff
+from vyos.validate import is_ipv4, is_ipv6
+from vyos import ConfigError
+
+
+class FixedDict(dict):
+ """
+ FixedDict: A dictionnary not allowing new keys to be created after initialisation.
+
+ >>> f = FixedDict(**{'count':1})
+ >>> f['count'] = 2
+ >>> f['king'] = 3
+ File "...", line ..., in __setitem__
+ raise ConfigError(f'Option "{k}" has no defined default')
+ """
+ def __init__ (self, **options):
+ self._allowed = options.keys()
+ super().__init__(**options)
+
+ def __setitem__ (self, k, v):
+ """
+ __setitem__ is a builtin which is called by python when setting dict values:
+ >>> d = dict()
+ >>> d['key'] = 'value'
+ >>> d
+ {'key': 'value'}
+
+ is syntaxic sugar for
+
+ >>> d = dict()
+ >>> d.__setitem__('key','value')
+ >>> d
+ {'key': 'value'}
+ """
+ if k not in self._allowed:
+ raise ConfigError(f'Option "{k}" has no defined default')
+ super().__setitem__(k, v)
+
+
+class ConfigurationState(Config):
+ """
+ The current API require a dict to be generated by get_config()
+ which is then consumed by verify(), generate() and apply()
+
+ ConfiguartionState is an helper class wrapping Config and providing
+ an common API to this dictionary structure
+
+ Its to_dict() function return a dictionary containing three fields,
+ each a dict, called options, changes, actions.
+
+ options:
+
+ contains the configuration options for the dict and its value
+ {'options': {'commment': 'test'}} will be set if
+ 'set interface dummy dum1 description test' was used and
+ the key 'commment' is used to index the description info.
+
+ changes:
+
+ per key, let us know how the data was modified using one of the action
+ a special key called 'section' is used to indicate what happened to the
+ section. for example:
+
+ 'set interface dummy dum1 description test' when no interface was setup
+ will result in the following changes
+ {'changes': {'section': 'create', 'comment': 'create'}}
+
+ on an existing interface, depending if there was a description
+ 'set interface dummy dum1 description test' will result in one of
+ {'changes': {'comment': 'create'}} (not present before)
+ {'changes': {'comment': 'static'}} (unchanged)
+ {'changes': {'comment': 'modify'}} (changed from half)
+
+ and 'delete interface dummy dummy1 description' will result in:
+ {'changes': {'comment': 'delete'}}
+
+ actions:
+
+ for each action list the configuration key which were changes
+ in our example if we added the 'description' and added an IP we would have
+ {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}}
+
+ the actions are:
+ 'create': it did not exist previously and was created
+ 'modify': it did exist previously but its content changed
+ 'static': it did exist and did not change
+ 'delete': it was present but was removed from the configuration
+ 'absent': it was not and is not present
+ which for each field represent how it was modified since the last commit
+ """
+
+ def __init__ (self, section, default):
+ """
+ initialise the class for a given configuration path:
+
+ >>> conf = ConfigurationState('interfaces ethernet eth1')
+ all further references to get_value(s) and get_effective(s)
+ will be for this part of the configuration (eth1)
+ """
+ super().__init__()
+ self.section = section
+ self.default = deepcopy(default)
+ self.options = FixedDict(**default)
+ self.actions = {
+ 'create': [], # the key did not exist and was added
+ 'static': [], # the key exists and its value was not modfied
+ 'modify': [], # the key exists and its value was modified
+ 'absent': [], # the key is not present
+ 'delete': [], # the key was present and was deleted
+ }
+ self.changes = {}
+ if not self.exists(section):
+ self.changes['section'] = 'delete'
+ elif self.exists_effective(section):
+ self.changes['section'] = 'modify'
+ else:
+ self.changes['section'] = 'create'
+
+ def _act(self, section):
+ """
+ Returns for a given configuration field determine what happened to it
+
+ 'create': it did not exist previously and was created
+ 'modify': it did exist previously but its content changed
+ 'static': it did exist and did not change
+ 'delete': it was present but was removed from the configuration
+ 'absent': it was not and is not present
+ """
+ if self.exists(section):
+ if self.exists_effective(section):
+ if self.return_value(section) != self.return_effective_value(section):
+ return 'modify'
+ return 'static'
+ return 'create'
+ else:
+ if self.exists_effective(section):
+ return 'delete'
+ return 'absent'
+
+ def _action (self, name, key):
+ action = self._act(key)
+ self.changes[name] = action
+ self.actions[action].append(name)
+ return action
+
+ def _get(self, name, key, default, getter):
+ value = getter(key)
+ if not value:
+ if default:
+ self.options[name] = default
+ return
+ self.options[name] = self.default[name]
+ return
+ self.options[name] = value
+
+ def get_value(self, name, key, default=None):
+ """
+ >>> conf.get_value('comment', 'description')
+ will place the string of 'interface dummy description test'
+ into the dictionnary entry 'comment' using Config.return_value
+ (the data in the configuration to apply)
+ """
+ if self._action(name, key) in ('delete', 'absent'):
+ return
+ return self._get(name, key, default, self.return_value)
+
+ def get_values(self, name, key, default=None):
+ """
+ >>> conf.get_values('addresses-add', 'address')
+ will place a list made of the IP present in 'interface dummy dum1 address'
+ into the dictionnary entry 'addr' using Config.return_values
+ (the data in the configuration to apply)
+ """
+ if self._action(name, key) in ('delete', 'absent'):
+ return
+ return self._get(name, key, default, self.return_values)
+
+ def get_effective(self, name, key, default=None):
+ """
+ >>> conf.get_value('comment', 'description')
+ will place the string of 'interface dummy description test'
+ into the dictionnary entry 'comment' using Config.return_effective_value
+ (the data in the configuration to apply)
+ """
+ self._action(name, key)
+ return self._get(name, key, default, self.return_effective_value)
+
+ def get_effectives(self, name, key, default=None):
+ """
+ >>> conf.get_effectives('addresses-add', 'address')
+ will place a list made of the IP present in 'interface ethernet eth1 address'
+ into the dictionnary entry 'addresses-add' using Config.return_effectives_value
+ (the data in the un-modified configuration)
+ """
+ self._action(name, key)
+ return self._get(name, key, default, self.return_effectives_value)
+
+ def load(self, mapping):
+ """
+ load will take a dictionary defining how we wish the configuration
+ to be parsed and apply this definition to set the data.
+
+ >>> mapping = {
+ 'addresses-add' : ('address', True, None),
+ 'comment' : ('description', False, 'auto'),
+ }
+ >>> conf.load(mapping)
+
+ mapping is a dictionary where each key represents the name we wish
+ to have (such as 'addresses-add'), with a list a content representing
+ how the data should be parsed:
+ - the configuration section name
+ such as 'address' under 'interface ethernet eth1'
+ - boolean indicating if this data can have multiple values
+ for 'address', True, as multiple IPs can be set
+ for 'description', False, as it is a single string
+ - default represent the default value if absent from the configuration
+ 'None' indicate that no default should be set if the configuration
+ does not have the configuration section
+
+ """
+ for local_name, (config_name, multiple, default) in mapping.items():
+ if multiple:
+ self.get_values(local_name, config_name, default)
+ else:
+ self.get_value(local_name, config_name, default)
+
+ def remove_default (self,*options):
+ """
+ remove all the values which were not changed from the default
+ """
+ for option in options:
+ if self.exists(option) and self_return_value(option) != self.default[option]:
+ continue
+ del self.options[option]
+
+ def to_dict (self):
+ """
+ provide a dictionary with the generated data for the configuration
+ options: the configuration value for the key
+ changes: per key how they changed from the previous configuration
+ actions: per changes all the options which were changed
+ """
+ # as we have to use a dict() for the API for verify and apply the options
+ return {
+ 'options': self.options,
+ 'changes': self.changes,
+ 'actions': self.actions,
+ }
+
+default_config_data = {
+ # interface definition
+ 'vrf': '',
+ 'addresses-add': [],
+ 'addresses-del': [],
+ 'state': 'up',
+ 'dhcp-interface': '',
+ 'link_detect': 1,
+ 'ip': False,
+ 'ipv6': False,
+ 'nhrp': [],
+ 'ipv6_autoconf': 0,
+ 'ipv6_forwarding': 1,
+ 'ipv6_dad_transmits': 1,
+ # internal
+ 'tunnel': {},
+ # the following names are exactly matching the name
+ # for the ip command and must not be changed
+ 'ifname': '',
+ 'type': '',
+ 'alias': '',
+ 'mtu': '1476',
+ 'local': '',
+ 'remote': '',
+ 'multicast': 'disable',
+ 'allmulticast': 'disable',
+ 'ttl': '255',
+ 'tos': 'inherit',
+ 'key': '',
+ 'encaplimit': '4',
+ 'flowlabel': 'inherit',
+ 'hoplimit': '64',
+ 'tclass': 'inherit',
+ '6rd-prefix': '',
+ '6rd-relay-prefix': '',
+}
+
+# dict name -> config name, multiple values, default
+mapping = {
+ 'type': ('encapsulation', False, None),
+ 'alias': ('description', False, None),
+ 'mtu': ('mtu', False, None),
+ 'local': ('local-ip', False, None),
+ 'remote': ('remote-ip', False, None),
+ 'multicast': ('multicast', False, None),
+ 'ttl': ('parameters ip ttl', False, None),
+ 'tos': ('parameters ip tos', False, None),
+ 'key': ('parameters ip key', False, None),
+ 'encaplimit': ('parameters ipv6 encaplimit', False, None),
+ 'flowlabel': ('parameters ipv6 flowlabel', False, None),
+ 'hoplimit': ('parameters ipv6 hoplimit', False, None),
+ 'tclass': ('parameters ipv6 tclass', False, None),
+ '6rd-prefix': ('6rd-prefix', False, None),
+ '6rd-relay-prefix': ('6rd-relay-prefix', False, None),
+ 'dhcp-interface': ('dhcp-interface', False, None),
+ 'state': ('disable', False, 'down'),
+ 'link_detect': ('disable-link-detect', False, 2),
+ 'vrf': ('vrf', False, None),
+ 'addresses-add': ('address', True, None),
+ 'ipv6_autoconf': ('ipv6 address autoconf', False, 1),
+ 'ipv6_forwarding': ('ipv6 disable-forwarding', False, 0),
+ 'ipv6_dad_transmits:': ('ipv6 dup-addr-detect-transmits', False, None)
+}
+
+def get_class (options):
+ dispatch = {
+ 'gre': GREIf,
+ 'gre-bridge': GRETapIf,
+ 'ipip': IPIPIf,
+ 'ipip6': IPIP6If,
+ 'ip6ip6': IP6IP6If,
+ 'ip6gre': IP6GREIf,
+ 'sit': SitIf,
+ }
+
+ kls = dispatch[options['type']]
+ if options['type'] == 'gre' and not options['remote'] \
+ and not options['key'] and not options['multicast']:
+ # will use GreTapIf on GreIf deletion but it does not matter
+ return GRETapIf
+ elif options['type'] == 'sit' and options['6rd-prefix']:
+ # will use SitIf on Sit6RDIf deletion but it does not matter
+ return Sit6RDIf
+ return kls
+
+def get_interface_ip (ifname):
+ if not ifname:
+ return ''
+ try:
+ addrs = Interface(ifname).get_addr()
+ if addrs:
+ return addrs[0].split('/')[0]
+ except Exception:
+ return ''
+
+def get_afi (ip):
+ return IP6 if is_ipv6(ip) else IP4
+
+def ip_proto (afi):
+ return 6 if afi == IP6 else 4
+
+
+def get_config():
+ ifname = os.environ.get('VYOS_TAGNODE_VALUE','')
+ if not ifname:
+ raise ConfigError('Interface not specified')
+
+ conf = ConfigurationState('interfaces tunnel ' + ifname, default_config_data)
+ options = conf.options
+ changes = conf.changes
+ options['ifname'] = ifname
+
+ # set new configuration level
+ conf.set_level(conf.section)
+
+ if changes['section'] == 'delete':
+ conf.get_effective('type', mapping['type'][0])
+ conf.set_level('protocols nhrp tunnel')
+ options['nhrp'] = conf.list_nodes('')
+ return conf.to_dict()
+
+ # load all the configuration option according to the mapping
+ conf.load(mapping)
+
+ # remove default value if not set and not required
+ afi_local = get_afi(options['local'])
+ if afi_local == IP6:
+ conf.remove_default('ttl', 'tos', 'key')
+ if afi_local == IP4:
+ conf.remove_default('encaplimit', 'flowlabel', 'hoplimit', 'tclass')
+
+ # if the local-ip is not set, pick one from the interface !
+ # hopefully there is only one, otherwise it will not be very deterministic
+ # at time of writing the code currently returns ipv4 before ipv6 in the list
+
+ # XXX: There is no way to trigger an update of the interface source IP if
+ # XXX: the underlying interface IP address does change, I believe this
+ # XXX: limit/issue is present in vyatta too
+
+ if not options['local'] and options['dhcp-interface']:
+ # XXX: This behaviour changes from vyatta which would return 127.0.0.1 if
+ # XXX: the interface was not DHCP. As there is no easy way to find if an
+ # XXX: interface is using DHCP, and using this feature to get 127.0.0.1
+ # XXX: makes little sense, I feel the change in behaviour is acceptable
+ picked = get_interface_ip(options['dhcp-interface'])
+ if picked == '':
+ picked = '127.0.0.1'
+ print('Could not get an IP address from {dhcp-interface} using 127.0.0.1 instead')
+ options['local'] = picked
+ options['dhcp-interface'] = ''
+
+ # get interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed
+ # could be done within ConfigurationState
+ eff_addr = conf.return_effective_values('address')
+ options['addresses-del'] = list_diff(eff_addr, options['addresses-add'])
+
+ # allmulticast fate is linked to multicast
+ options['allmulticast'] = options['multicast']
+
+ # check that per encapsulation all local-remote pairs are unique
+ conf.set_level('interfaces tunnel')
+ ct = conf.get_config_dict()['tunnel']
+ options['tunnel'] = {}
+
+ for name in ct:
+ tunnel = ct[name]
+ encap = tunnel.get('encapsulation', '')
+ local = tunnel.get('local-ip', '')
+ if not local:
+ local = get_interface_ip(tunnel.get('dhcp-interface', ''))
+ remote = tunnel.get('remote-ip', '<unset>')
+ pair = f'{local}-{remote}'
+ options['tunnel'][encap][pair] = options['tunnel'].setdefault(encap, {}).get(pair, 0) + 1
+
+ return conf.to_dict()
+
+
+def verify(conf):
+ options = conf['options']
+ changes = conf['changes']
+ actions = conf['actions']
+
+ ifname = options['ifname']
+ iftype = options['type']
+
+ if changes['section'] == 'delete':
+ if ifname in options['nhrp']:
+ raise ConfigError(f'Can not delete interface tunnel {iftype} {ifname}, it is used by nhrp')
+ # done, bail out early
+ return None
+
+ # tunnel encapsulation checks
+
+ if not iftype:
+ raise ConfigError(f'Must provide an "encapsulation" for tunnel {iftype} {ifname}')
+
+ if changes['type'] in ('modify', 'delete'):
+ # TODO: we could now deal with encapsulation modification by deleting / recreating
+ raise ConfigError(f'Encapsulation can only be set at tunnel creation for tunnel {iftype} {ifname}')
+
+ if iftype != 'sit' and options['6rd-prefix']:
+ # XXX: should be able to remove this and let the definition catch it
+ print(f'6RD can only be configured for sit interfaces not tunnel {iftype} {ifname}')
+
+ # what are the tunnel options we can set / modified / deleted
+
+ kls = get_class(options)
+ valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf']
+
+ if changes['section'] == 'create':
+ valid.extend(['type',])
+ valid.extend([o for o in kls.options if o not in kls.updates])
+
+ for create in actions['create']:
+ if create not in valid:
+ raise ConfigError(f'Can not set "{create}" for tunnel {iftype} {ifname} at tunnel creation')
+
+ for modify in actions['modify']:
+ if modify not in valid:
+ raise ConfigError(f'Can not modify "{modify}" for tunnel {iftype} {ifname}. it must be set at tunnel creation')
+
+ for delete in actions['delete']:
+ if delete in kls.required:
+ raise ConfigError(f'Can not remove "{delete}", it is an mandatory option for tunnel {iftype} {ifname}')
+
+ # tunnel information
+
+ tun_local = options['local']
+ afi_local = get_afi(tun_local)
+ tun_remote = options['remote'] or tun_local
+ afi_remote = get_afi(tun_remote)
+ tun_ismgre = iftype == 'gre' and not options['remote']
+ tun_is6rd = iftype == 'sit' and options['6rd-prefix']
+
+ # incompatible options
+
+ if not tun_local and not options['dhcp-interface'] and not tun_is6rd:
+ raise ConfigError(f'Must configure either local-ip or dhcp-interface for tunnel {iftype} {ifname}')
+
+ if tun_local and options['dhcp-interface']:
+ raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}')
+
+ # tunnel endpoint
+
+ if afi_local != afi_remote:
+ raise ConfigError(f'IPv4/IPv6 mismatch between local-ip and remote-ip for tunnel {iftype} {ifname}')
+
+ if afi_local != kls.tunnel:
+ version = 4 if tun_local == IP4 else 6
+ raise ConfigError(f'Invalid IPv{version} local-ip for tunnel {iftype} {ifname}')
+
+ ipv4_count = len([ip for ip in options['addresses-add'] if is_ipv4(ip)])
+ ipv6_count = len([ip for ip in options['addresses-add'] if is_ipv6(ip)])
+
+ if tun_ismgre and afi_local == IP6:
+ raise ConfigError(f'Using an IPv6 address is forbidden for mGRE tunnels such as tunnel {iftype} {ifname}')
+
+ # check address family use
+ # checks are not enforced (but ip command failing) for backward compatibility
+
+ if ipv4_count and not IP4 in kls.ip:
+ print(f'Should not use IPv4 addresses on tunnel {iftype} {ifname}')
+
+ if ipv6_count and not IP6 in kls.ip:
+ print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}')
+
+ # tunnel encapsulation check
+
+ convert = {
+ (6, 4, 'gre'): 'ip6gre',
+ (6, 6, 'gre'): 'ip6gre',
+ (4, 6, 'ipip'): 'ipip6',
+ (6, 6, 'ipip'): 'ip6ip6',
+ }
+
+ iprotos = []
+ if ipv4_count:
+ iprotos.append(4)
+ if ipv6_count:
+ iprotos.append(6)
+
+ for iproto in iprotos:
+ replace = convert.get((kls.tunnel, iproto, iftype), '')
+ if replace:
+ raise ConfigError(
+ f'Using IPv6 address in local-ip or remote-ip is not possible with "encapsulation {iftype}". ' +
+ f'Use "encapsulation {replace}" for tunnel {iftype} {ifname} instead.'
+ )
+
+ # tunnel options
+
+ incompatible = []
+ if afi_local == IP6:
+ incompatible.extend(['ttl', 'tos', 'key',])
+ if afi_local == IP4:
+ incompatible.extend(['encaplimit', 'flowlabel', 'hoplimit', 'tclass'])
+
+ for option in incompatible:
+ if option in options:
+ # TODO: raise converted to print as not enforced by vyatta
+ # raise ConfigError(f'{option} is not valid for tunnel {iftype} {ifname}')
+ print(f'Using "{option}" is invalid for tunnel {iftype} {ifname}')
+
+ # duplicate tunnel pairs
+
+ pair = '{}-{}'.format(options['local'], options['remote'])
+ if options['tunnel'].get(iftype, {}).get(pair, 0) > 1:
+ raise ConfigError(f'More than one tunnel configured for with the same encapulation and IPs for tunnel {iftype} {ifname}')
+
+ return None
+
+
+def generate(gre):
+ return None
+
+def apply(conf):
+ options = conf['options']
+ changes = conf['changes']
+ actions = conf['actions']
+ kls = get_class(options)
+
+ # extract ifname as otherwise it is duplicated on the interface creation
+ ifname = options.pop('ifname')
+
+ # only the valid keys for creation of a Interface
+ config = dict((k, options[k]) for k in kls.options if options[k])
+
+ # setup or create the tunnel interface if it does not exist
+ tunnel = kls(ifname, **config)
+
+ if changes['section'] == 'delete':
+ tunnel.remove()
+ # The perl code was calling/opt/vyatta/sbin/vyatta-tunnel-cleanup
+ # which identified tunnels type which were not used anymore to remove them
+ # (ie: gre0, gretap0, etc.) The perl code did however nothing
+ # This feature is also not implemented yet
+ return
+
+ # A GRE interface without remote will be mGRE
+ # if the interface does not suppor the option, it skips the change
+ for option in tunnel.updates:
+ if changes['section'] in 'create' and option in tunnel.options:
+ # it was setup at creation
+ continue
+ tunnel.set_interface(option, options[option])
+
+ # set other interface properties
+ for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast',
+ 'vrf', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
+ tunnel.set_interface(option, options[option])
+
+ # Configure interface address(es)
+ for addr in options['addresses-del']:
+ tunnel.del_addr(addr)
+ for addr in options['addresses-add']:
+ tunnel.add_addr(addr)
+
+ # now bring it up (or not)
+ tunnel.set_admin_state(options['state'])
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index efdc21f89..6639a9b0d 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -18,11 +18,12 @@ import os
from sys import exit
from copy import deepcopy
+from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import VXLANIf, Interface
+from vyos.util import is_bridge_member
from vyos import ConfigError
-from netifaces import interfaces
default_config_data = {
'address': [],
@@ -37,7 +38,12 @@ default_config_data = {
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
- 'link': '',
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
+ 'source_address': '',
+ 'source_interface': '',
'mtu': 1450,
'remote': '',
'remote_port': 8472, # The Linux implementation of VXLAN pre-dates
@@ -50,10 +56,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces vxlan ' + vxlan['intf']):
@@ -103,9 +109,29 @@ def get_config():
if conf.exists('ip enable-proxy-arp'):
vxlan['ip_proxy_arp'] = 1
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ vxlan['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ vxlan['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ vxlan['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
+ # VXLAN source address
+ if conf.exists('source-address'):
+ vxlan['source_address'] = conf.return_value('source-address')
+
# VXLAN underlay interface
- if conf.exists('link'):
- vxlan['link'] = conf.return_value('link')
+ if conf.exists('source-interface'):
+ vxlan['source_interface'] = conf.return_value('source-interface')
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
@@ -128,25 +154,35 @@ def get_config():
def verify(vxlan):
if vxlan['deleted']:
- # bail out early
+ interface = vxlan['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
if vxlan['mtu'] < 1500:
print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
- if vxlan['group'] and not vxlan['link']:
- raise ConfigError('Multicast VXLAN requires an underlaying interface ')
+ if vxlan['group']:
+ if not vxlan['source_interface']:
+ raise ConfigError('Multicast VXLAN requires an underlaying interface ')
+
+ if not vxlan['source_interface'] in interfaces():
+ raise ConfigError('VXLAN source interface does not exist')
- if not (vxlan['group'] or vxlan['remote']):
- raise ConfigError('Group or remote must be configured')
+ if not (vxlan['group'] or vxlan['remote'] or vxlan['source_address']):
+ raise ConfigError('Group, remote or source-address must be configured')
if not vxlan['vni']:
raise ConfigError('Must configure VNI for VXLAN')
- if vxlan['link']:
+ if vxlan['source_interface']:
# VXLAN adds a 50 byte overhead - we need to check the underlaying MTU
# if our configured MTU is at least 50 bytes less
- underlay_mtu = int(Interface(vxlan['link']).get_mtu())
+ underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu())
if underlay_mtu < (vxlan['mtu'] + 50):
raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
'MTU is to small ({})'.format(underlay_mtu))
@@ -175,12 +211,13 @@ def apply(vxlan):
# Assign VXLAN instance configuration parameters to config dict
conf['vni'] = vxlan['vni']
conf['group'] = vxlan['group']
- conf['dev'] = vxlan['link']
+ conf['src_address'] = vxlan['source_address']
+ conf['src_interface'] = vxlan['source_interface']
conf['remote'] = vxlan['remote']
conf['port'] = vxlan['remote_port']
# Finally create the new interface
- v = VXLANIf(vxlan['intf'], config=conf)
+ v = VXLANIf(vxlan['intf'], **conf)
# update interface description used e.g. by SNMP
v.set_alias(vxlan['description'])
# Maximum Transfer Unit (MTU)
@@ -198,6 +235,14 @@ def apply(vxlan):
v.set_arp_ignore(vxlan['ip_enable_arp_ignore'])
# Enable proxy-arp on this interface
v.set_proxy_arp(vxlan['ip_proxy_arp'])
+ # IPv6 address autoconfiguration
+ v.set_ipv6_autoconf(vxlan['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ v.set_ipv6_eui64_address(vxlan['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ v.set_ipv6_forwarding(vxlan['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ v.set_ipv6_dad_messages(vxlan['ipv6_dup_addr_detect'])
# Configure interface address(es) - no need to implicitly delete the
# old addresses as they have already been removed by deleting the
@@ -209,7 +254,7 @@ def apply(vxlan):
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not vxlan['disable']:
- v.set_state('up')
+ v.set_admin_state('up')
return None
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index ff12a5172..8bf81c747 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,263 +13,284 @@
#
# 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 sys
import os
import re
-import subprocess
+
+from sys import exit
from copy import deepcopy
from netifaces import interfaces
-from vyos import ConfigError
from vyos.config import Config
from vyos.configdict import list_diff
from vyos.ifconfig import WireGuardIf
+from vyos.util import chown, is_bridge_member, chmod_750
+from vyos.util import call
+from vyos import ConfigError
kdir = r'/config/auth/wireguard'
+default_config_data = {
+ 'intfc': '',
+ 'address': [],
+ 'address_remove': [],
+ 'description': '',
+ 'lport': None,
+ 'deleted': False,
+ 'disable': False,
+ 'fwmark': 0x00,
+ 'mtu': 1420,
+ 'peer': [],
+ 'peer_remove': [], # stores public keys of peers to remove
+ 'pk': f'{kdir}/default/private.key',
+ 'vrf': ''
+}
def _check_kmod():
- if not os.path.exists('/sys/module/wireguard'):
- if os.system('sudo modprobe wireguard') != 0:
- raise ConfigError("modprobe wireguard failed")
+ modules = ['wireguard']
+ for module in modules:
+ if not os.path.exists(f'/sys/module/{module}'):
+ if call(f'modprobe {module}') != 0:
+ raise ConfigError(f'Loading Kernel module {module} failed')
def _migrate_default_keys():
- if os.path.exists('{}/private.key'.format(kdir)) and not os.path.exists('{}/default/private.key'.format(kdir)):
- old_umask = os.umask(0o027)
- location = '{}/default'.format(kdir)
- subprocess.call(['sudo mkdir -p ' + location], shell=True)
- subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True)
- subprocess.call(['sudo chmod 750 ' + location], shell=True)
- os.rename('{}/private.key'.format(kdir),
- '{}/private.key'.format(location))
- os.rename('{}/public.key'.format(kdir),
- '{}/public.key'.format(location))
- os.umask(old_umask)
+ if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'):
+ location = f'{kdir}/default'
+ if not os.path.exists(location):
+ os.makedirs(location)
+
+ chown(location, 'root', 'vyattacfg')
+ chmod_750(location)
+ os.rename(f'{kdir}/private.key', f'{location}/private.key')
+ os.rename(f'{kdir}/public.key', f'{location}/public.key')
def get_config():
- c = Config()
- if not c.exists(['interfaces', 'wireguard']):
- return None
+ conf = Config()
+ base = ['interfaces', 'wireguard']
+
+ # determine tagNode instance
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ wg = deepcopy(default_config_data)
+ wg['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Check if interface has been removed
+ if not conf.exists(base + [wg['intf']]):
+ wg['deleted'] = True
+ return wg
+
+ conf.set_level(base + [wg['intf']])
+
+ # retrieve configured interface addresses
+ if conf.exists(['address']):
+ wg['address'] = conf.return_values(['address'])
+
+ # get interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values(['address'])
+ wg['address_remove'] = list_diff(eff_addr, wg['address'])
+
+ # retrieve interface description
+ if conf.exists(['description']):
+ wg['description'] = conf.return_value(['description'])
+
+ # disable interface
+ if conf.exists(['disable']):
+ wg['disable'] = True
+
+ # local port to listen on
+ if conf.exists(['port']):
+ wg['lport'] = conf.return_value(['port'])
+
+ # fwmark value
+ if conf.exists(['fwmark']):
+ wg['fwmark'] = int(conf.return_value(['fwmark']))
+
+ # Maximum Transmission Unit (MTU)
+ if conf.exists('mtu'):
+ wg['mtu'] = int(conf.return_value(['mtu']))
+
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ wg['vrf'] = conf.return_value('vrf')
+
+ # private key
+ if conf.exists(['private-key']):
+ wg['pk'] = "{0}/{1}/private.key".format(
+ kdir, conf.return_value(['private-key']))
+
+ # peer removal, wg identifies peers by its pubkey
+ peer_eff = conf.list_effective_nodes(['peer'])
+ peer_rem = list_diff(peer_eff, conf.list_nodes(['peer']))
+ for peer in peer_rem:
+ wg['peer_remove'].append(
+ conf.return_effective_value(['peer', peer, 'pubkey']))
+
+ # peer settings
+ if conf.exists(['peer']):
+ for p in conf.list_nodes(['peer']):
+ # set new config level for this peer
+ conf.set_level(base + [wg['intf'], 'peer', p])
+ peer = {
+ 'allowed-ips': [],
+ 'address': '',
+ 'name': p,
+ 'persistent_keepalive': '',
+ 'port': '',
+ 'psk': '',
+ 'pubkey': ''
+ }
+
+ # peer allowed-ips
+ if conf.exists(['allowed-ips']):
+ peer['allowed-ips'] = conf.return_values(['allowed-ips'])
+
+ # peer address
+ if conf.exists(['address']):
+ peer['address'] = conf.return_value(['address'])
+
+ # peer port
+ if conf.exists(['port']):
+ peer['port'] = conf.return_value(['port'])
+
+ # persistent-keepalive
+ if conf.exists(['persistent-keepalive']):
+ peer['persistent_keepalive'] = conf.return_value(['persistent-keepalive'])
+
+ # preshared-key
+ if conf.exists(['preshared-key']):
+ peer['psk'] = conf.return_value(['preshared-key'])
+
+ # peer pubkeys
+ if conf.exists(['pubkey']):
+ key_eff = conf.return_effective_value(['pubkey'])
+ key_cfg = conf.return_value(['pubkey'])
+ peer['pubkey'] = key_cfg
+
+ # on a pubkey change we need to remove the pubkey first
+ # peers are identified by pubkey, so key update means
+ # peer removal and re-add
+ if key_eff != key_cfg and key_eff != None:
+ wg['peer_remove'].append(key_cfg)
+
+ # if a peer is disabled, we have to exec a remove for it's pubkey
+ if conf.exists(['disable']):
+ wg['peer_remove'].append(peer['pubkey'])
+ else:
+ wg['peer'].append(peer)
- dflt_cnf = {
- 'intfc': '',
- 'addr': [],
- 'addr_remove': [],
- 'descr': '',
- 'lport': None,
- 'delete': False,
- 'state': 'up',
- 'fwmark': 0x00,
- 'mtu': 1420,
- 'peer': {},
- 'peer_remove': [],
- 'pk': '{}/default/private.key'.format(kdir)
- }
-
- if os.getenv('VYOS_TAGNODE_VALUE'):
- ifname = str(os.environ['VYOS_TAGNODE_VALUE'])
- wg = deepcopy(dflt_cnf)
- wg['intfc'] = ifname
- wg['descr'] = ifname
- else:
- print("ERROR: VYOS_TAGNODE_VALUE undefined")
- sys.exit(1)
-
- c.set_level(['interfaces', 'wireguard'])
-
- # interface removal state
- if not c.exists(ifname) and c.exists_effective(ifname):
- wg['delete'] = True
-
- if not wg['delete']:
- c.set_level(['interfaces', 'wireguard', ifname])
- if c.exists(['address']):
- wg['addr'] = c.return_values(['address'])
-
- # determine addresses which need to be removed
- eff_addr = c.return_effective_values(['address'])
- wg['addr_remove'] = list_diff(eff_addr, wg['addr'])
-
- # ifalias description
- if c.exists(['description']):
- wg['descr'] = c.return_value(['description'])
-
- # link state
- if c.exists(['disable']):
- wg['state'] = 'down'
-
- # local port to listen on
- if c.exists(['port']):
- wg['lport'] = c.return_value(['port'])
-
- # fwmark value
- if c.exists(['fwmark']):
- wg['fwmark'] = c.return_value(['fwmark'])
-
- # mtu
- if c.exists('mtu'):
- wg['mtu'] = c.return_value('mtu')
-
- # private key
- if c.exists(['private-key']):
- wg['pk'] = "{0}/{1}/private.key".format(
- kdir, c.return_value(['private-key']))
-
- # peer removal, wg identifies peers by its pubkey
- peer_eff = c.list_effective_nodes(['peer'])
- peer_rem = list_diff(peer_eff, c.list_nodes(['peer']))
- for p in peer_rem:
- wg['peer_remove'].append(
- c.return_effective_value(['peer', p, 'pubkey']))
-
- # peer settings
- if c.exists(['peer']):
- for p in c.list_nodes(['peer']):
- if not c.exists(['peer', p, 'disable']):
- wg['peer'].update(
- {
- p: {
- 'allowed-ips': [],
- 'endpoint': '',
- 'pubkey': ''
- }
- }
- )
- # peer allowed-ips
- if c.exists(['peer', p, 'allowed-ips']):
- wg['peer'][p]['allowed-ips'] = c.return_values(
- ['peer', p, 'allowed-ips'])
- # peer endpoint
- if c.exists(['peer', p, 'endpoint']):
- wg['peer'][p]['endpoint'] = c.return_value(
- ['peer', p, 'endpoint'])
- # persistent-keepalive
- if c.exists(['peer', p, 'persistent-keepalive']):
- wg['peer'][p]['persistent-keepalive'] = c.return_value(
- ['peer', p, 'persistent-keepalive'])
- # preshared-key
- if c.exists(['peer', p, 'preshared-key']):
- wg['peer'][p]['psk'] = c.return_value(
- ['peer', p, 'preshared-key'])
- # peer pubkeys
- key_eff = c.return_effective_value(['peer', p, 'pubkey'])
- key_cfg = c.return_value(['peer', p, 'pubkey'])
- wg['peer'][p]['pubkey'] = key_cfg
-
- # on a pubkey change we need to remove the pubkey first
- # peers are identified by pubkey, so key update means
- # peer removal and re-add
- if key_eff != key_cfg and key_eff != None:
- wg['peer_remove'].append(key_cfg)
-
- # if a peer is disabled, we have to exec a remove for it's pubkey
- else:
- peer_key = c.return_value(['peer', p, 'pubkey'])
- wg['peer_remove'].append(peer_key)
return wg
-def verify(c):
- if not c:
- return None
+def verify(wg):
+ interface = wg['intf']
- if not os.path.exists(c['pk']):
- raise ConfigError(
- "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'")
-
- if not c['delete']:
- if not c['addr']:
- raise ConfigError("ERROR: IP address required")
- if not c['peer']:
- raise ConfigError("ERROR: peer required")
- for p in c['peer']:
- if not c['peer'][p]['allowed-ips']:
- raise ConfigError("ERROR: allowed-ips required for peer " + p)
- if not c['peer'][p]['pubkey']:
- raise ConfigError("peer pubkey required for peer " + p)
-
-
-def apply(c):
- # no wg configs left, remove all interface from system
- # maybe move it into ifconfig.py
- if not c:
- net_devs = os.listdir('/sys/class/net/')
- for dev in net_devs:
- if os.path.isdir('/sys/class/net/' + dev):
- buf = open('/sys/class/net/' + dev + '/uevent', 'r').read()
- if re.search("DEVTYPE=wireguard", buf, re.I | re.M):
- wg_intf = re.sub("INTERFACE=", "", re.search(
- "INTERFACE=.*", buf, re.I | re.M).group(0))
- subprocess.call(
- ['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True)
+ if wg['deleted']:
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
+ vrf_name = wg['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
+ if not os.path.exists(wg['pk']):
+ raise ConfigError('No keys found, generate them by executing:\n' \
+ '"run generate wireguard [keypair|named-keypairs]"')
+
+ if not wg['address']:
+ raise ConfigError(f'IP address required for interface "{interface}"!')
+
+ if not wg['peer']:
+ raise ConfigError(f'Peer required for interface "{interface}"!')
+
+ # run checks on individual configured WireGuard peer
+ for peer in wg['peer']:
+ peer_name = peer['name']
+ if not peer['allowed-ips']:
+ raise ConfigError(f'Peer allowed-ips required for peer "{peer_name}"!')
+
+ if not peer['pubkey']:
+ raise ConfigError(f'Peer public-key required for peer "{peer_name}"!')
+
+
+def apply(wg):
# init wg class
- intfc = WireGuardIf(c['intfc'])
+ w = WireGuardIf(wg['intf'])
# single interface removal
- if c['delete']:
- intfc.remove()
+ if wg['deleted']:
+ w.remove()
return None
- # remove IP addresses
- for ip in c['addr_remove']:
- intfc.del_addr(ip)
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in wg['address_remove']:
+ w.del_addr(addr)
+ for addr in wg['address']:
+ w.add_addr(addr)
- # add IP addresses
- for ip in c['addr']:
- intfc.add_addr(ip)
+ # Maximum Transmission Unit (MTU)
+ w.set_mtu(wg['mtu'])
- # interface mtu
- intfc.set_mtu(int(c['mtu']))
+ # update interface description used e.g. within SNMP
+ w.set_alias(wg['description'])
- # ifalias for snmp from description
- intfc.set_alias(str(c['descr']))
+ # assign/remove VRF
+ w.set_vrf(wg['vrf'])
# remove peers
- if c['peer_remove']:
- for pkey in c['peer_remove']:
- intfc.remove_peer(pkey)
+ for pub_key in wg['peer_remove']:
+ w.remove_peer(pub_key)
# peer pubkey
# setting up the wg interface
- intfc.config['private-key'] = c['pk']
- for p in c['peer']:
+ w.config['private-key'] = c['pk']
+
+ for peer in wg['peer']:
# peer pubkey
- intfc.config['pubkey'] = str(c['peer'][p]['pubkey'])
+ w.config['pubkey'] = peer['pubkey']
# peer allowed-ips
- intfc.config['allowed-ips'] = c['peer'][p]['allowed-ips']
+ w.config['allowed-ips'] = peer['allowed-ips']
# local listen port
- if c['lport']:
- intfc.config['port'] = c['lport']
+ if wg['lport']:
+ w.config['port'] = wg['lport']
# fwmark
if c['fwmark']:
- intfc.config['fwmark'] = c['fwmark']
+ w.config['fwmark'] = wg['fwmark']
+
# endpoint
- if c['peer'][p]['endpoint']:
- intfc.config['endpoint'] = c['peer'][p]['endpoint']
+ if peer['address'] and peer['port']:
+ w.config['endpoint'] = '{}:{}'.format(
+ peer['address'], peer['port'])
# persistent-keepalive
- if 'persistent-keepalive' in c['peer'][p]:
- intfc.config['keepalive'] = c['peer'][p]['persistent-keepalive']
+ if peer['persistent_keepalive']:
+ w.config['keepalive'] = peer['persistent_keepalive']
# maybe move it into ifconfig.py
# preshared-key - needs to be read from a file
- if 'psk' in c['peer'][p]:
+ if peer['psk']:
psk_file = '/config/auth/wireguard/psk'
- old_umask = os.umask(0o077)
- open(psk_file, 'w').write(str(c['peer'][p]['psk']))
- os.umask(old_umask)
- intfc.config['psk'] = psk_file
- intfc.update()
+ with open(psk_file, 'w') as f:
+ f.write(peer['psk'])
+ w.config['psk'] = psk_file
- # interface state
- intfc.set_state(c['state'])
+ w.update()
+
+ # Enable/Disable interface
+ if wg['disable']:
+ w.set_admin_state('down')
+ else:
+ w.set_admin_state('up')
return None
@@ -282,4 +303,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index 098aa8d97..138f27755 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -15,756 +15,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+from sys import exit
+from re import findall
-from jinja2 import Template
from copy import deepcopy
-from sys import exit
-from stat import S_IRWXU,S_IRGRP,S_IXGRP,S_IROTH,S_IXOTH
-from pwd import getpwnam
-from grp import getgrnam
+from jinja2 import FileSystemLoader, Environment
-from subprocess import Popen, PIPE
-from psutil import pid_exists
+from netifaces import interfaces
+from netaddr import EUI, mac_unix_expanded
-from vyos.ifconfig import EthernetIf
-from vyos.ifconfig_vlan import apply_vlan_config
-from vyos.configdict import list_diff, vlan_to_dict
from vyos.config import Config
+from vyos.configdict import list_diff, vlan_to_dict
+from vyos.defaults import directories as vyos_data_dir
+from vyos.ifconfig import WiFiIf
+from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
+from vyos.util import process_running, chmod_x, chown, run, is_bridge_member
from vyos import ConfigError
user = 'root'
group = 'vyattacfg'
-# Please be careful if you edit the template.
-config_hostapd_tmpl = """
-### Autogenerated by interfaces-wireless.py ###
-{% if description %}
-# Description: {{ description }}
-# User-friendly description of device; up to 32 octets encoded in UTF-8
-device_name={{ description | truncate(32, True) }}
-{% endif %}
-
-# AP netdevice name (without 'ap' postfix, i.e., wlan0 uses wlan0ap for
-# management frames with the Host AP driver); wlan0 with many nl80211 drivers
-# Note: This attribute can be overridden by the values supplied with the '-i'
-# command line parameter.
-interface={{ intf }}
-
-# Driver interface type (hostap/wired/none/nl80211/bsd);
-# default: hostap). nl80211 is used with all Linux mac80211 drivers.
-# Use driver=none if building hostapd as a standalone RADIUS server that does
-# not control any wireless/wired driver.
-driver=nl80211
-
-#
-# What about bridge?
-# bridge=br0
-# wds_sta=1
-#
-
-# Levels (minimum value for logged events):
-# 0 = verbose debugging
-# 1 = debugging
-# 2 = informational messages
-# 3 = notification
-# 4 = warning
-logger_syslog=-1
-logger_syslog_level=0
-logger_stdout=-1
-logger_stdout_level=0
-
-{%- if country_code %}
-
-# Country code (ISO/IEC 3166-1). Used to set regulatory domain.
-# Set as needed to indicate country in which device is operating.
-# This can limit available channels and transmit power.
-country_code={{ country_code }}
-
-# Enable IEEE 802.11d. This advertises the country_code and the set of allowed
-# channels and transmit power levels based on the regulatory limits. The
-# country_code setting must be configured with the correct country for
-# IEEE 802.11d functions.
-ieee80211d=1
-{% endif %}
-
-{%- if ssid %}
-
-# SSID to be used in IEEE 802.11 management frames
-ssid={{ ssid }}
-{% endif %}
-
-{%- if channel %}
-
-# Channel number (IEEE 802.11)
-# (default: 0, i.e., not set)
-# Please note that some drivers do not use this value from hostapd and the
-# channel will need to be configured separately with iwconfig.
-#
-# If CONFIG_ACS build option is enabled, the channel can be selected
-# automatically at run time by setting channel=acs_survey or channel=0, both of
-# which will enable the ACS survey based algorithm.
-channel={{ channel }}
-{% endif %}
-
-{%- if mode %}
-
-# Operation mode (a = IEEE 802.11a (5 GHz), b = IEEE 802.11b (2.4 GHz),
-# g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used
-# with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this
-# needs to be set to hw_mode=a. For IEEE 802.11ax (HE) on 6 GHz this needs
-# to be set to hw_mode=a. When using ACS (see channel parameter), a
-# special value "any" can be used to indicate that any support band can be used.
-# This special case is currently supported only with drivers with which
-# offloaded ACS is used.
-{% if 'n' in mode -%}
-hw_mode=g
-ieee80211n=1
-{% elif 'ac' in mode -%}
-hw_mode=a
-ieee80211h=1
-ieee80211ac=1
-{% else -%}
-hw_mode={{ mode }}
-{% endif %}
-{% endif %}
-
-# ieee80211w: Whether management frame protection (MFP) is enabled
-# 0 = disabled (default)
-# 1 = optional
-# 2 = required
-{% if 'disabled' in mgmt_frame_protection -%}
-ieee80211w=0
-{% elif 'optional' in mgmt_frame_protection -%}
-ieee80211w=1
-{% elif 'required' in mgmt_frame_protection -%}
-ieee80211w=2
-{% endif %}
-
-# ht_capab: HT capabilities (list of flags)
-# LDPC coding capability: [LDPC] = supported
-# Supported channel width set: [HT40-] = both 20 MHz and 40 MHz with secondary
-# channel below the primary channel; [HT40+] = both 20 MHz and 40 MHz
-# with secondary channel above the primary channel
-# (20 MHz only if neither is set)
-# Note: There are limits on which channels can be used with HT40- and
-# HT40+. Following table shows the channels that may be available for
-# HT40- and HT40+ use per IEEE 802.11n Annex J:
-# freq HT40- HT40+
-# 2.4 GHz 5-13 1-7 (1-9 in Europe/Japan)
-# 5 GHz 40,48,56,64 36,44,52,60
-# (depending on the location, not all of these channels may be available
-# for use)
-# Please note that 40 MHz channels may switch their primary and secondary
-# channels if needed or creation of 40 MHz channel maybe rejected based
-# on overlapping BSSes. These changes are done automatically when hostapd
-# is setting up the 40 MHz channel.
-# Spatial Multiplexing (SM) Power Save: [SMPS-STATIC] or [SMPS-DYNAMIC]
-# (SMPS disabled if neither is set)
-# HT-greenfield: [GF] (disabled if not set)
-# Short GI for 20 MHz: [SHORT-GI-20] (disabled if not set)
-# Short GI for 40 MHz: [SHORT-GI-40] (disabled if not set)
-# Tx STBC: [TX-STBC] (disabled if not set)
-# Rx STBC: [RX-STBC1] (one spatial stream), [RX-STBC12] (one or two spatial
-# streams), or [RX-STBC123] (one, two, or three spatial streams); Rx STBC
-# disabled if none of these set
-# HT-delayed Block Ack: [DELAYED-BA] (disabled if not set)
-# Maximum A-MSDU length: [MAX-AMSDU-7935] for 7935 octets (3839 octets if not
-# set)
-# DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set)
-# 40 MHz intolerant [40-INTOLERANT] (not advertised if not set)
-# L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set)
-{% if cap_ht %}
-ht_capab=
-{%- endif -%}
-
-{%- if cap_ht_40mhz_incapable -%}
-[40-INTOLERANT]
-{%- endif -%}
-
-{%- if cap_ht_delayed_block_ack -%}
-[DELAYED-BA]
-{%- endif -%}
-
-{%- if cap_ht_dsss_cck_40 -%}
-[DSSS_CCK-40]
-{%- endif -%}
-
-{%- if cap_ht_greenfield -%}
-[GF]
-{%- endif -%}
-
-{%- if cap_ht_ldpc -%}
-[LDPC]
-{%- endif -%}
-
-{%- if cap_ht_lsig_protection -%}
-[LSIG-TXOP-PROT]
-{%- endif -%}
-
-{%- if cap_ht_max_amsdu -%}
-[MAX-AMSDU-{{ cap_ht_max_amsdu }}]
-{%- endif -%}
-
-{%- if cap_ht_smps -%}
-[SMPS-{{ cap_ht_smps | upper }}]
-{%- endif -%}
-
-{%- if cap_ht_chan_set_width -%}
-{%- for csw in cap_ht_chan_set_width -%}
-[{{ csw | upper }}]
-{%- endfor -%}
-{%- endif -%}
-
-{%- if cap_ht_short_gi -%}
-{%- for gi in cap_ht_short_gi -%}
-[SHORT-GI-{{ gi }}]
-{%- endfor -%}
-{%- endif -%}
-
-{%- if cap_ht_stbc_tx -%}
-[TX-STBC]
-{%- endif -%}
-{%- if cap_ht_stbc_rx -%}
-[RX-STBC{{ cap_ht_stbc_rx }}]
-{%- endif %}
-
-# Required for full HT and VHT functionality
-wme_enabled=1
-
-{% if cap_ht_powersave -%}
-# WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD]
-# Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver)
-uapsd_advertisement_enabled=1
-{%- endif %}
-
-{% if cap_req_ht -%}
-# Require stations to support HT PHY (reject association if they do not)
-require_ht=1
-{% endif %}
-
-# vht_capab: VHT capabilities (list of flags)
-#
-# vht_max_mpdu_len: [MAX-MPDU-7991] [MAX-MPDU-11454]
-# Indicates maximum MPDU length
-# 0 = 3895 octets (default)
-# 1 = 7991 octets
-# 2 = 11454 octets
-# 3 = reserved
-#
-# supported_chan_width: [VHT160] [VHT160-80PLUS80]
-# Indicates supported Channel widths
-# 0 = 160 MHz & 80+80 channel widths are not supported (default)
-# 1 = 160 MHz channel width is supported
-# 2 = 160 MHz & 80+80 channel widths are supported
-# 3 = reserved
-#
-# Rx LDPC coding capability: [RXLDPC]
-# Indicates support for receiving LDPC coded pkts
-# 0 = Not supported (default)
-# 1 = Supported
-#
-# Short GI for 80 MHz: [SHORT-GI-80]
-# Indicates short GI support for reception of packets transmitted with TXVECTOR
-# params format equal to VHT and CBW = 80Mhz
-# 0 = Not supported (default)
-# 1 = Supported
-#
-# Short GI for 160 MHz: [SHORT-GI-160]
-# Indicates short GI support for reception of packets transmitted with TXVECTOR
-# params format equal to VHT and CBW = 160Mhz
-# 0 = Not supported (default)
-# 1 = Supported
-#
-# Tx STBC: [TX-STBC-2BY1]
-# Indicates support for the transmission of at least 2x1 STBC
-# 0 = Not supported (default)
-# 1 = Supported
-#
-# Rx STBC: [RX-STBC-1] [RX-STBC-12] [RX-STBC-123] [RX-STBC-1234]
-# Indicates support for the reception of PPDUs using STBC
-# 0 = Not supported (default)
-# 1 = support of one spatial stream
-# 2 = support of one and two spatial streams
-# 3 = support of one, two and three spatial streams
-# 4 = support of one, two, three and four spatial streams
-# 5,6,7 = reserved
-#
-# SU Beamformer Capable: [SU-BEAMFORMER]
-# Indicates support for operation as a single user beamformer
-# 0 = Not supported (default)
-# 1 = Supported
-#
-# SU Beamformee Capable: [SU-BEAMFORMEE]
-# Indicates support for operation as a single user beamformee
-# 0 = Not supported (default)
-# 1 = Supported
-#
-# Compressed Steering Number of Beamformer Antennas Supported:
-# [BF-ANTENNA-2] [BF-ANTENNA-3] [BF-ANTENNA-4]
-# Beamformee's capability indicating the maximum number of beamformer
-# antennas the beamformee can support when sending compressed beamforming
-# feedback
-# If SU beamformer capable, set to maximum value minus 1
-# else reserved (default)
-#
-# Number of Sounding Dimensions:
-# [SOUNDING-DIMENSION-2] [SOUNDING-DIMENSION-3] [SOUNDING-DIMENSION-4]
-# Beamformer's capability indicating the maximum value of the NUM_STS parameter
-# in the TXVECTOR of a VHT NDP
-# If SU beamformer capable, set to maximum value minus 1
-# else reserved (default)
-#
-# MU Beamformer Capable: [MU-BEAMFORMER]
-# Indicates support for operation as an MU beamformer
-# 0 = Not supported or sent by Non-AP STA (default)
-# 1 = Supported
-#
-# VHT TXOP PS: [VHT-TXOP-PS]
-# Indicates whether or not the AP supports VHT TXOP Power Save Mode
-# or whether or not the STA is in VHT TXOP Power Save mode
-# 0 = VHT AP doesn't support VHT TXOP PS mode (OR) VHT STA not in VHT TXOP PS
-# mode
-# 1 = VHT AP supports VHT TXOP PS mode (OR) VHT STA is in VHT TXOP power save
-# mode
-#
-# +HTC-VHT Capable: [HTC-VHT]
-# Indicates whether or not the STA supports receiving a VHT variant HT Control
-# field.
-# 0 = Not supported (default)
-# 1 = supported
-#
-# Maximum A-MPDU Length Exponent: [MAX-A-MPDU-LEN-EXP0]..[MAX-A-MPDU-LEN-EXP7]
-# Indicates the maximum length of A-MPDU pre-EOF padding that the STA can recv
-# This field is an integer in the range of 0 to 7.
-# The length defined by this field is equal to
-# 2 pow(13 + Maximum A-MPDU Length Exponent) -1 octets
-#
-# VHT Link Adaptation Capable: [VHT-LINK-ADAPT2] [VHT-LINK-ADAPT3]
-# Indicates whether or not the STA supports link adaptation using VHT variant
-# HT Control field
-# If +HTC-VHTcapable is 1
-# 0 = (no feedback) if the STA does not provide VHT MFB (default)
-# 1 = reserved
-# 2 = (Unsolicited) if the STA provides only unsolicited VHT MFB
-# 3 = (Both) if the STA can provide VHT MFB in response to VHT MRQ and if the
-# STA provides unsolicited VHT MFB
-# Reserved if +HTC-VHTcapable is 0
-#
-# Rx Antenna Pattern Consistency: [RX-ANTENNA-PATTERN]
-# Indicates the possibility of Rx antenna pattern change
-# 0 = Rx antenna pattern might change during the lifetime of an association
-# 1 = Rx antenna pattern does not change during the lifetime of an association
-#
-# Tx Antenna Pattern Consistency: [TX-ANTENNA-PATTERN]
-# Indicates the possibility of Tx antenna pattern change
-# 0 = Tx antenna pattern might change during the lifetime of an association
-# 1 = Tx antenna pattern does not change during the lifetime of an association
-{% if cap_vht %}
-vht_capab=
-{%- endif -%}
-
-{%- if cap_vht_max_mpdu -%}
-[MAX-MPDU-{{ cap_vht_max_mpdu }}]
-{%- endif -%}
-
-{%- if cap_vht_max_mpdu_exp -%}
-[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}]
-{%- endif -%}
-
-{%- if cap_vht_chan_set_width -%}
-[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}]
-{%- endif -%}
-
-{%- if cap_vht_chan_set_width -%}
-{%- if '2' in cap_vht_chan_set_width -%}
-[VHT160]
-{%- elif '3' in cap_vht_chan_set_width -%}
-[VHT160-80PLUS80]
-{%- endif -%}
-{%- endif -%}
-
-{%- if cap_vht_stbc_tx -%}
-[TX-STBC-2BY1]
-{%- endif -%}
-
-{%- if cap_vht_stbc_rx -%}
-[RX-STBC-{{ cap_vht_stbc_rx }}]
-{%- endif -%}
-
-{%- if cap_vht_link_adaptation -%}
-{%- if 'unsolicited' in cap_vht_link_adaptation -%}
-[VHT-LINK-ADAPT2]
-{%- elif 'both' in cap_vht_link_adaptation -%}
-[VHT-LINK-ADAPT3]
-{%- endif -%}
-{%- endif -%}
-
-{%- if cap_vht_short_gi -%}
-{%- for gi in cap_vht_short_gi -%}
-[SHORT-GI-{{ gi }}]
-{%- endfor -%}
-{%- endif -%}
-
-{%- if cap_vht_ldpc -%}
-[RXLDPC]
-{%- endif -%}
-
-{%- if cap_vht_tx_powersave -%}
-[VHT-TXOP-PS]
-{%- endif -%}
-
-{%- if cap_vht_vht_cf -%}
-[HTC-VHT]
-{%- endif -%}
-
-{%- if cap_vht_beamform -%}
-{%- for beamform in cap_vht_beamform -%}
-{%- if 'single-user-beamformer' in beamform -%}
-[SU-BEAMFORMER]
-{%- elif 'single-user-beamformee' in beamform -%}
-[SU-BEAMFORMEE]
-{%- elif 'multi-user-beamformer' in beamform -%}
-[MU-BEAMFORMER]
-{%- elif 'multi-user-beamformee' in beamform -%}
-[MU-BEAMFORMEE]
-{%- endif -%}
-{%- endfor -%}
-{%- endif -%}
-
-{%- if cap_vht_antenna_fixed -%}
-[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN]
-{%- endif -%}
-
-{%- if cap_vht_antenna_cnt -%}
-{%- for beamform in cap_vht_beamform -%}
-{%- if 'single-user-beamformer' in beamform -%}
-[BF-ANTENNA-{{ cap_vht_antenna_cnt|int -1 }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt|int -1}}]
-{%- else -%}
-[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}]
-{%- endif -%}
-{%- endfor -%}
-{%- endif %}
-
-# ieee80211n: Whether IEEE 802.11n (HT) is enabled
-# 0 = disabled (default)
-# 1 = enabled
-# Note: You will also need to enable WMM for full HT functionality.
-# Note: hw_mode=g (2.4 GHz) and hw_mode=a (5 GHz) is used to specify the band.
-{% if cap_req_vht -%}
-ieee80211n=0
-# Require stations to support VHT PHY (reject association if they do not)
-require_vht=1
-{% endif %}
-
-{% if cap_vht_center_freq_1 -%}
-# center freq = 5 GHz + (5 * index)
-# So index 42 gives center freq 5.210 GHz
-# which is channel 42 in 5G band
-vht_oper_centr_freq_seg0_idx={{ cap_vht_center_freq_1 }}
-{% endif %}
-
-{% if cap_vht_center_freq_2 -%}
-# center freq = 5 GHz + (5 * index)
-# So index 159 gives center freq 5.795 GHz
-# which is channel 159 in 5G band
-vht_oper_centr_freq_seg1_idx={{ cap_vht_center_freq_2 }}
-{% endif %}
-
-{% if disable_broadcast_ssid -%}
-# Send empty SSID in beacons and ignore probe request frames that do not
-# specify full SSID, i.e., require stations to know SSID.
-# default: disabled (0)
-# 1 = send empty (length=0) SSID in beacon and ignore probe request for
-# broadcast SSID
-# 2 = clear SSID (ASCII 0), but keep the original length (this may be required
-# with some clients that do not support empty SSID) and ignore probe
-# requests for broadcast SSID
-ignore_broadcast_ssid=1
-{% endif %}
-
-# Station MAC address -based authentication
-# Please note that this kind of access control requires a driver that uses
-# hostapd to take care of management frame processing and as such, this can be
-# used with driver=hostap or driver=nl80211, but not with driver=atheros.
-# 0 = accept unless in deny list
-# 1 = deny unless in accept list
-# 2 = use external RADIUS server (accept/deny lists are searched first)
-macaddr_acl=0
-
-{% if max_stations -%}
-# Maximum number of stations allowed in station table. New stations will be
-# rejected after the station table is full. IEEE 802.11 has a limit of 2007
-# different association IDs, so this number should not be larger than that.
-# (default: 2007)
-max_num_sta={{ max_stations }}
-{% endif %}
-
-{% if isolate_stations -%}
-# Client isolation can be used to prevent low-level bridging of frames between
-# associated stations in the BSS. By default, this bridging is allowed.
-ap_isolate=1
-{% endif %}
-
-{% if reduce_transmit_power -%}
-# Add Power Constraint element to Beacon and Probe Response frames
-# This config option adds Power Constraint element when applicable and Country
-# element is added. Power Constraint element is required by Transmit Power
-# Control. This can be used only with ieee80211d=1.
-# Valid values are 0..255.
-local_pwr_constraint={{ reduce_transmit_power }}
-{% endif %}
-
-{% if expunge_failing_stations -%}
-# Disassociate stations based on excessive transmission failures or other
-# indications of connection loss. This depends on the driver capabilities and
-# may not be available with all drivers.
-disassoc_low_ack=1
-{% endif %}
-
-{% if sec_wep -%}
-# IEEE 802.11 specifies two authentication algorithms. hostapd can be
-# configured to allow both of these or only one. Open system authentication
-# should be used with IEEE 802.1X.
-# Bit fields of allowed authentication algorithms:
-# bit 0 = Open System Authentication
-# bit 1 = Shared Key Authentication (requires WEP)
-auth_algs=2
-
-# WEP rekeying (disabled if key lengths are not set or are set to 0)
-# Key lengths for default/broadcast and individual/unicast keys:
-# 5 = 40-bit WEP (also known as 64-bit WEP with 40 secret bits)
-# 13 = 104-bit WEP (also known as 128-bit WEP with 104 secret bits)
-wep_key_len_broadcast=5
-wep_key_len_unicast=5
-
-# Static WEP key configuration
-#
-# The key number to use when transmitting.
-# It must be between 0 and 3, and the corresponding key must be set.
-# default: not set
-wep_default_key=0
-
-# The WEP keys to use.
-# A key may be a quoted string or unquoted hexadecimal digits.
-# The key length should be 5, 13, or 16 characters, or 10, 26, or 32
-# digits, depending on whether 40-bit (64-bit), 104-bit (128-bit), or
-# 128-bit (152-bit) WEP is used.
-# Only the default key must be supplied; the others are optional.
-{% if sec_wep_key -%}
-{% for key in sec_wep_key -%}
-wep_key{{ loop.index -1 }}={{ key}}
-{% endfor %}
-{%- endif %}
-
-{% elif sec_wpa -%}
-##### WPA/IEEE 802.11i configuration ##########################################
-
-# Enable WPA. Setting this variable configures the AP to require WPA (either
-# WPA-PSK or WPA-RADIUS/EAP based on other configuration). For WPA-PSK, either
-# wpa_psk or wpa_passphrase must be set and wpa_key_mgmt must include WPA-PSK.
-# Instead of wpa_psk / wpa_passphrase, wpa_psk_radius might suffice.
-# For WPA-RADIUS/EAP, ieee8021x must be set (but without dynamic WEP keys),
-# RADIUS authentication server must be configured, and WPA-EAP must be included
-# in wpa_key_mgmt.
-# This field is a bit field that can be used to enable WPA (IEEE 802.11i/D3.0)
-# and/or WPA2 (full IEEE 802.11i/RSN):
-# bit0 = WPA
-# bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled)
-{% if 'both' in sec_wpa_mode -%}
-wpa=3
-{%- elif 'wpa2' in sec_wpa_mode -%}
-wpa=2
-{%- elif 'wpa' in sec_wpa_mode -%}
-wpa=1
-{%- endif %}
-
-{% if sec_wpa_cipher -%}
-# Set of accepted cipher suites (encryption algorithms) for pairwise keys
-# (unicast packets). This is a space separated list of algorithms:
-# CCMP = AES in Counter mode with CBC-MAC (CCMP-128)
-# TKIP = Temporal Key Integrity Protocol
-# CCMP-256 = AES in Counter mode with CBC-MAC with 256-bit key
-# GCMP = Galois/counter mode protocol (GCMP-128)
-# GCMP-256 = Galois/counter mode protocol with 256-bit key
-# Group cipher suite (encryption algorithm for broadcast and multicast frames)
-# is automatically selected based on this configuration. If only CCMP is
-# allowed as the pairwise cipher, group cipher will also be CCMP. Otherwise,
-# TKIP will be used as the group cipher. The optional group_cipher parameter can
-# be used to override this automatic selection.
-{% if 'wpa2' in sec_wpa_mode -%}
-# Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value)
-rsn_pairwise={{ sec_wpa_cipher | join(" ") }}
-{% else -%}
-# Pairwise cipher for WPA (v1) (default: TKIP)
-wpa_pairwise={{ sec_wpa_cipher | join(" ") }}
-{%- endif -%}
-{% endif %}
-
-{% if sec_wpa_passphrase -%}
-# IEEE 802.11 specifies two authentication algorithms. hostapd can be
-# configured to allow both of these or only one. Open system authentication
-# should be used with IEEE 802.1X.
-# Bit fields of allowed authentication algorithms:
-# bit 0 = Open System Authentication
-# bit 1 = Shared Key Authentication (requires WEP)
-auth_algs=1
-
-# WPA pre-shared keys for WPA-PSK. This can be either entered as a 256-bit
-# secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase
-# (8..63 characters) that will be converted to PSK. This conversion uses SSID
-# so the PSK changes when ASCII passphrase is used and the SSID is changed.
-wpa_passphrase={{ sec_wpa_passphrase }}
-
-# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The
-# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be
-# added to enable SHA256-based stronger algorithms.
-# WPA-PSK = WPA-Personal / WPA2-Personal
-# WPA-PSK-SHA256 = WPA2-Personal using SHA256
-wpa_key_mgmt=WPA-PSK
-
-{% elif sec_wpa_radius -%}
-##### IEEE 802.1X-2004 related configuration ##################################
-# Require IEEE 802.1X authorization
-ieee8021x=1
-
-# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The
-# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be
-# added to enable SHA256-based stronger algorithms.
-# WPA-EAP = WPA-Enterprise / WPA2-Enterprise
-# WPA-EAP-SHA256 = WPA2-Enterprise using SHA256
-wpa_key_mgmt=WPA-EAP
-
-{% if sec_wpa_radius_source -%}
-# RADIUS client forced local IP address for the access point
-# Normally the local IP address is determined automatically based on configured
-# IP addresses, but this field can be used to force a specific address to be
-# used, e.g., when the device has multiple IP addresses.
-radius_client_addr={{ sec_wpa_radius_source }}
-{% endif %}
-
-{% for radius in sec_wpa_radius -%}
-# RADIUS authentication server
-auth_server_addr={{ radius.server }}
-auth_server_port={{ radius.port }}
-auth_server_shared_secret={{ radius.key }}
-{% if radius.acc_port -%}
-# RADIUS accounting server
-acct_server_addr={{ radius.server }}
-acct_server_port={{ radius.acc_port }}
-acct_server_shared_secret={{ radius.key }}
-{% endif %}
-{% endfor %}
-
-{% endif %}
-
-{% else %}
-# Open system
-auth_algs=1
-{% endif %}
-
-# TX queue parameters (EDCF / bursting)
-# tx_queue_<queue name>_<param>
-# queues: data0, data1, data2, data3
-# (data0 is the highest priority queue)
-# parameters:
-# aifs: AIFS (default 2)
-# cwmin: cwMin (1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191,
-# 16383, 32767)
-# cwmax: cwMax (same values as cwMin, cwMax >= cwMin)
-# burst: maximum length (in milliseconds with precision of up to 0.1 ms) for
-# bursting
-#
-# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e):
-# These parameters are used by the access point when transmitting frames
-# to the clients.
-#
-# Low priority / AC_BK = background
-tx_queue_data3_aifs=7
-tx_queue_data3_cwmin=15
-tx_queue_data3_cwmax=1023
-tx_queue_data3_burst=0
-# Note: for IEEE 802.11b mode: cWmin=31 cWmax=1023 burst=0
-#
-# Normal priority / AC_BE = best effort
-tx_queue_data2_aifs=3
-tx_queue_data2_cwmin=15
-tx_queue_data2_cwmax=63
-tx_queue_data2_burst=0
-# Note: for IEEE 802.11b mode: cWmin=31 cWmax=127 burst=0
-#
-# High priority / AC_VI = video
-tx_queue_data1_aifs=1
-tx_queue_data1_cwmin=7
-tx_queue_data1_cwmax=15
-tx_queue_data1_burst=3.0
-# Note: for IEEE 802.11b mode: cWmin=15 cWmax=31 burst=6.0
-#
-# Highest priority / AC_VO = voice
-tx_queue_data0_aifs=1
-tx_queue_data0_cwmin=3
-tx_queue_data0_cwmax=7
-tx_queue_data0_burst=1.5
-
-# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e):
-# for 802.11a or 802.11g networks
-# These parameters are sent to WMM clients when they associate.
-# The parameters will be used by WMM clients for frames transmitted to the
-# access point.
-#
-# note - txop_limit is in units of 32microseconds
-# note - acm is admission control mandatory flag. 0 = admission control not
-# required, 1 = mandatory
-# note - Here cwMin and cmMax are in exponent form. The actual cw value used
-# will be (2^n)-1 where n is the value given here. The allowed range for these
-# wmm_ac_??_{cwmin,cwmax} is 0..15 with cwmax >= cwmin.
-#
-wmm_enabled=1
-
-# Low priority / AC_BK = background
-wmm_ac_bk_cwmin=4
-wmm_ac_bk_cwmax=10
-wmm_ac_bk_aifs=7
-wmm_ac_bk_txop_limit=0
-wmm_ac_bk_acm=0
-# Note: for IEEE 802.11b mode: cWmin=5 cWmax=10
-#
-# Normal priority / AC_BE = best effort
-wmm_ac_be_aifs=3
-wmm_ac_be_cwmin=4
-wmm_ac_be_cwmax=10
-wmm_ac_be_txop_limit=0
-wmm_ac_be_acm=0
-# Note: for IEEE 802.11b mode: cWmin=5 cWmax=7
-#
-# High priority / AC_VI = video
-wmm_ac_vi_aifs=2
-wmm_ac_vi_cwmin=3
-wmm_ac_vi_cwmax=4
-wmm_ac_vi_txop_limit=94
-wmm_ac_vi_acm=0
-# Note: for IEEE 802.11b mode: cWmin=4 cWmax=5 txop_limit=188
-#
-# Highest priority / AC_VO = voice
-wmm_ac_vo_aifs=2
-wmm_ac_vo_cwmin=2
-wmm_ac_vo_cwmax=3
-wmm_ac_vo_txop_limit=47
-wmm_ac_vo_acm=0
-
-"""
-
-# Please be careful if you edit the template.
-config_wpa_suppl_tmpl = """
-# WPA supplicant config
-network={
- ssid="{{ ssid }}"
-{%- if sec_wpa_passphrase %}
- psk="{{ sec_wpa_passphrase }}"
-{% endif %}
-}
-
-"""
-
default_config_data = {
'address': [],
'address_remove': [],
@@ -792,7 +62,7 @@ default_config_data = {
'cap_vht_center_freq_2' : '',
'cap_vht_chan_set_width' : '',
'cap_vht_ldpc' : False,
- 'cap_vht_link_adaptation' : False,
+ 'cap_vht_link_adaptation' : '',
'cap_vht_max_mpdu_exp' : '',
'cap_vht_max_mpdu' : '',
'cap_vht_short_gi' : [],
@@ -820,6 +90,10 @@ default_config_data = {
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'mac' : '',
'max_stations' : '',
'mgmt_frame_protection' : 'disabled',
@@ -834,9 +108,10 @@ default_config_data = {
'sec_wpa_passphrase' : '',
'sec_wpa_radius' : [],
'ssid' : '',
- 'type' : 'monitor',
+ 'op_mode' : 'monitor',
'vif': [],
- 'vif_remove': []
+ 'vif_remove': [],
+ 'vrf': ''
}
def get_conf_file(conf_type, intf):
@@ -845,11 +120,8 @@ def get_conf_file(conf_type, intf):
# create directory on demand
if not os.path.exists(cfg_dir):
os.mkdir(cfg_dir)
- # fix permissions - corresponds to mode 755
- os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH)
- uid = getpwnam(user).pw_uid
- gid = getgrnam(group).gr_gid
- os.chown(cfg_dir, uid, gid)
+ chmod_x(cfg_dir)
+ chown(cfg_dir, user, group)
cfg_file = cfg_dir + r'/{}.cfg'.format(intf)
return cfg_file
@@ -860,11 +132,8 @@ def get_pid(conf_type, intf):
# create directory on demand
if not os.path.exists(cfg_dir):
os.mkdir(cfg_dir)
- # fix permissions - corresponds to mode 755
- os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH)
- uid = getpwnam(user).pw_uid
- gid = getgrnam(group).gr_gid
- os.chown(cfg_dir, uid, gid)
+ chmod_x(cfg_dir)
+ chown(cfg_dir, user, group)
cfg_file = cfg_dir + r'/{}.pid'.format(intf)
return cfg_file
@@ -876,28 +145,22 @@ def get_wpa_suppl_config_name(intf):
# create directory on demand
if not os.path.exists(cfg_dir):
os.mkdir(cfg_dir)
- # fix permissions - corresponds to mode 755
- os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH)
- uid = getpwnam(user).pw_uid
- gid = getgrnam(group).gr_gid
- os.chown(cfg_dir, uid, gid)
+ chmod_x(cfg_dir)
+ chown(cfg_dir, user, group)
cfg_file = cfg_dir + r'/{}.cfg'.format(intf)
return cfg_file
-def subprocess_cmd(command):
- p = Popen(command, stdout=PIPE, shell=True)
- p.communicate()
def get_config():
wifi = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
- try:
- wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# check if wireless interface has been removed
cfg_base = 'interfaces wireless ' + wifi['intf']
@@ -1031,7 +294,7 @@ def get_config():
# VHT link adaptation capabilities
if conf.exists('capabilities vht link-adaptation'):
wifi['cap_vht'] = True
- wifi['cap_vht_link_adaptation'] = True
+ wifi['cap_vht_link_adaptation'] = conf.return_value('capabilities vht link-adaptation')
# Set the maximum length of A-MPDU pre-EOF padding that the station can receive
if conf.exists('capabilities vht max-mpdu-exp'):
@@ -1128,10 +391,30 @@ def get_config():
if conf.exists('ip enable-arp-announce'):
wifi['ip_enable_arp_announce'] = 1
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ wifi['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ wifi['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
# ARP enable ignore
if conf.exists('ip enable-arp-ignore'):
wifi['ip_enable_arp_ignore'] = 1
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ wifi['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ wifi['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
+ # Wireless physical device
+ if conf.exists('physical-device'):
+ wifi['phy'] = conf.return_value('physical-device')
+
# Media Access Control (MAC) address
if conf.exists('mac'):
wifi['mac'] = conf.return_value('mac')
@@ -1148,9 +431,9 @@ def get_config():
if conf.exists('mode'):
wifi['mode'] = conf.return_value('mode')
- # Wireless physical device
- if conf.exists('phy'):
- wifi['phy'] = conf.return_value('phy')
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ wifi['vrf'] = conf.return_value('vrf')
# Transmission power reduction in dBm
if conf.exists('reduce-transmit-power'):
@@ -1204,6 +487,7 @@ def get_config():
radius = {
'server' : server,
'acc_port' : '',
+ 'disabled': False,
'port' : 1812,
'key' : ''
}
@@ -1216,6 +500,10 @@ def get_config():
if conf.exists('accounting'):
radius['acc_port'] = radius['port'] + 1
+ # Check if RADIUS server was temporary disabled
+ if conf.exists(['disable']):
+ radius['disabled'] = True
+
# RADIUS server shared-secret
if conf.exists('key'):
radius['key'] = conf.return_value('key')
@@ -1232,7 +520,11 @@ def get_config():
# Wireless device type for this interface
if conf.exists('type'):
- wifi['type'] = conf.return_value('type')
+ tmp = conf.return_value('type')
+ if tmp == 'access-point':
+ tmp = 'ap'
+
+ wifi['op_mode'] = tmp
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
@@ -1248,6 +540,9 @@ def get_config():
conf.set_level(cfg_base + ' vif ' + vif)
wifi['vif'].append(vlan_to_dict(conf))
+ # disable interface
+ if conf.exists('disable'):
+ wifi['disable'] = True
# retrieve configured regulatory domain
conf.set_level('system')
@@ -1259,12 +554,23 @@ def get_config():
def verify(wifi):
if wifi['deleted']:
+ interface = wifi['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
- if wifi['type'] != 'monitor' and not wifi['ssid']:
+
+ if wifi['op_mode'] != 'monitor' and not wifi['ssid']:
raise ConfigError('SSID must be set for {}'.format(wifi['intf']))
- if wifi['type'] == 'access-point':
+ if not wifi['phy']:
+ raise ConfigError('You must specify physical-device')
+
+ if wifi['op_mode'] == 'ap':
c = Config()
if not c.exists('system wifi-regulatory-domain'):
raise ConfigError('Wireless regulatory domain is mandatory,\n' \
@@ -1273,7 +579,6 @@ def verify(wifi):
if not wifi['channel']:
raise ConfigError('Channel must be set for {}'.format(wifi['intf']))
-
if len(wifi['sec_wep_key']) > 4:
raise ConfigError('No more then 4 WEP keys configurable')
@@ -1283,6 +588,10 @@ def verify(wifi):
if wifi['cap_vht_beamform'] and wifi['cap_vht_antenna_cnt'] == 1:
raise ConfigError('Cannot use beam forming with just one antenna!')
+ if wifi['cap_vht_beamform'] == 'single-user-beamformer' and wifi['cap_vht_antenna_cnt'] < 3:
+ # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705
+ raise ConfigError('Single-user beam former requires at least 3 antennas!')
+
if wifi['sec_wep'] and (len(wifi['sec_wep_key']) == 0):
raise ConfigError('Missing WEP keys')
@@ -1293,35 +602,55 @@ def verify(wifi):
if not radius['key']:
raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server']))
+ vrf_name = wifi['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ # use common function to verify VLAN configuration
+ verify_vlan_config(wifi)
+
+ conf = Config()
+ # Only one wireless interface per phy can be in station mode
+ base = ['interfaces', 'wireless']
+ for phy in os.listdir('/sys/class/ieee80211'):
+ stations = []
+ for wlan in conf.list_nodes(base):
+ # the following node is mandatory
+ if conf.exists(base + [wlan, 'physical-device', phy]):
+ tmp = conf.return_value(base + [wlan, 'type'])
+ if tmp == 'station':
+ stations.append(wlan)
+
+ if len(stations) > 1:
+ raise ConfigError('Only one station per wireless physical interface possible!')
return None
def generate(wifi):
- pid = 0
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir["data"], "templates", "wifi")
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
# always stop hostapd service first before reconfiguring it
pidfile = get_pid('hostapd', wifi['intf'])
- if os.path.isfile(pidfile):
- pid = 0
- with open(pidfile, 'r') as f:
- pid = int(f.read())
-
- if pid_exists(pid):
- cmd = 'start-stop-daemon --stop --quiet'
- cmd += ' --pidfile ' + pidfile
- subprocess_cmd(cmd)
+ if process_running(pidfile):
+ command = 'start-stop-daemon'
+ command += ' --stop '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ run(command)
# always stop wpa_supplicant service first before reconfiguring it
pidfile = get_pid('wpa_supplicant', wifi['intf'])
- if os.path.isfile(pidfile):
- pid = 0
- with open(pidfile, 'r') as f:
- pid = int(f.read())
-
- if pid_exists(pid):
- cmd = 'start-stop-daemon --stop --quiet'
- cmd += ' --pidfile ' + pidfile
- subprocess_cmd(cmd)
+ if process_running(pidfile):
+ command = 'start-stop-daemon'
+ command += ' --stop '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ run(command)
# Delete config files if interface is removed
if wifi['deleted']:
@@ -1333,15 +662,37 @@ def generate(wifi):
return None
+ if not wifi['mac']:
+ # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd
+ # generate locally administered MAC address from used phy interface
+ with open('/sys/class/ieee80211/{}/addresses'.format(wifi['phy']), 'r') as f:
+ # some PHYs tend to have multiple interfaces and thus supply multiple MAC
+ # addresses - we only need the first one for our calculation
+ tmp = f.readline().rstrip()
+ tmp = EUI(tmp).value
+ # mask last nibble from the MAC address
+ tmp &= 0xfffffffffff0
+ # set locally administered bit in MAC address
+ tmp |= 0x020000000000
+ # we now need to add an offset to our MAC address indicating this
+ # subinterfaces index
+ tmp += int(findall(r'\d+', wifi['intf'])[0])
+
+ # convert integer to "real" MAC address representation
+ mac = EUI(hex(tmp).split('x')[-1])
+ # change dialect to use : as delimiter instead of -
+ mac.dialect = mac_unix_expanded
+ wifi['mac'] = str(mac)
+
# render appropriate new config files depending on access-point or station mode
- if wifi['type'] == 'access-point':
- tmpl = Template(config_hostapd_tmpl)
+ if wifi['op_mode'] == 'ap':
+ tmpl = env.get_template('hostapd.conf.tmpl')
config_text = tmpl.render(wifi)
with open(get_conf_file('hostapd', wifi['intf']), 'w') as f:
f.write(config_text)
- elif wifi['type'] == 'station':
- tmpl = Template(config_wpa_suppl_tmpl)
+ elif wifi['op_mode'] == 'station':
+ tmpl = env.get_template('wpa_supplicant.conf.tmpl')
config_text = tmpl.render(wifi)
with open(get_conf_file('wpa_supplicant', wifi['intf']), 'w') as f:
f.write(config_text)
@@ -1349,13 +700,24 @@ def generate(wifi):
return None
def apply(wifi):
- w = EthernetIf(wifi['intf'])
if wifi['deleted']:
+ w = WiFiIf(wifi['intf'])
# delete interface
w.remove()
else:
- # Some parts e.g. MAC address can't be changed when interface is up
- w.set_state('down')
+ # WiFi interface needs to be created on-block (e.g. mode or physical
+ # interface) instead of passing a ton of arguments, I just use a dict
+ # that is managed by vyos.ifconfig
+ conf = deepcopy(WiFiIf.get_config())
+
+ # Assign WiFi instance configuration parameters to config dict
+ conf['phy'] = wifi['phy']
+
+ # Finally create the new interface
+ w = WiFiIf(wifi['intf'], **conf)
+
+ # assign/remove VRF
+ w.set_vrf(wifi['vrf'])
# update interface description used e.g. within SNMP
w.set_alias(wifi['description'])
@@ -1394,7 +756,7 @@ def apply(wifi):
# if custom mac is removed
if wifi['mac']:
w.set_mac(wifi['mac'])
- else:
+ elif wifi['hw_id']:
w.set_mac(wifi['hw_id'])
# configure ARP filter configuration
@@ -1405,10 +767,14 @@ def apply(wifi):
w.set_arp_announce(wifi['ip_enable_arp_announce'])
# configure ARP ignore
w.set_arp_ignore(wifi['ip_enable_arp_ignore'])
-
- # enable interface
- if not wifi['disable']:
- w.set_state('up')
+ # IPv6 address autoconfiguration
+ w.set_ipv6_autoconf(wifi['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ w.set_ipv6_eui64_address(wifi['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ w.set_ipv6_forwarding(wifi['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ w.set_ipv6_dad_messages(wifi['ipv6_dup_addr_detect'])
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -1437,30 +803,46 @@ def apply(wifi):
vlan = e.add_vlan(vif['id'])
apply_vlan_config(vlan, vif)
- # Physical interface is now configured. Proceed by starting hostapd or
- # wpa_supplicant daemon. When type is monitor we can just skip this.
- if wifi['type'] == 'access-point':
- cmd = 'start-stop-daemon --start --quiet'
- cmd += ' --exec /usr/sbin/hostapd'
- # now pass arguments to hostapd binary
- cmd += ' -- -B'
- cmd += ' -P {}'.format(get_pid('hostapd', wifi['intf']))
- cmd += ' {}'.format(get_conf_file('hostapd', wifi['intf']))
-
- # execute assembled command
- subprocess_cmd(cmd)
-
- elif wifi['type'] == 'station':
- cmd = 'start-stop-daemon --start --quiet'
- cmd += ' --exec /sbin/wpa_supplicant'
- # now pass arguments to hostapd binary
- cmd += ' -- -s -B -D nl80211'
- cmd += ' -P {}'.format(get_pid('wpa_supplicant', wifi['intf']))
- cmd += ' -i {}'.format(wifi['intf'])
- cmd += ' -c {}'.format(get_conf_file('wpa_supplicant', wifi['intf']))
-
- # execute assembled command
- subprocess_cmd(cmd)
+ # Enable/Disable interface - interface is always placed in
+ # administrative down state in WiFiIf class
+ if not wifi['disable']:
+ w.set_admin_state('up')
+
+ # Physical interface is now configured. Proceed by starting hostapd or
+ # wpa_supplicant daemon. When type is monitor we can just skip this.
+ if wifi['op_mode'] == 'ap':
+ command = 'start-stop-daemon'
+ command += ' --start '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + get_pid('hostapd', wifi['intf'])
+ command += ' --exec /usr/sbin/hostapd'
+ # now pass arguments to hostapd binary
+ command += ' -- '
+ command += ' -B'
+ command += ' -P ' + get_pid('hostapd', wifi['intf'])
+ command += ' ' + get_conf_file('hostapd', wifi['intf'])
+
+ # execute assembled command
+ run(command)
+
+ elif wifi['op_mode'] == 'station':
+ command = 'start-stop-daemon'
+ command += ' --start '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + get_pid('hostapd', wifi['intf'])
+ command += ' --exec /sbin/wpa_supplicant'
+ # now pass arguments to hostapd binary
+ command += ' -- '
+ command += ' -s -B -D nl80211'
+ command += ' -P ' + get_pid('wpa_supplicant', wifi['intf'])
+ command += ' -i ' + wifi['intf']
+ command += ' -c ' + \
+ get_conf_file('wpa_supplicant', wifi['intf'])
+
+ # execute assembled command
+ run(command)
return None
diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
new file mode 100755
index 000000000..c44a993c4
--- /dev/null
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -0,0 +1,237 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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 os
+
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.util import chown, chmod_x, is_bridge_member
+from vyos.util import cmd
+from vyos.util import call
+from vyos import ConfigError
+
+default_config_data = {
+ 'address': [],
+ 'apn': '',
+ 'chat_script': '',
+ 'deleted': False,
+ 'description': '',
+ 'device': 'ttyUSB0',
+ 'disable': False,
+ 'disable_link_detect': 1,
+ 'on_demand': False,
+ 'logfile': '',
+ 'metric': '10',
+ 'mtu': '1500',
+ 'name_server': True,
+ 'intf': '',
+ 'vrf': ''
+}
+
+def check_kmod():
+ modules = ['option', 'usb_wwan', 'usbserial']
+ for module in modules:
+ if not os.path.exists(f'/sys/module/{module}'):
+ if call(f'modprobe {module}') != 0:
+ raise ConfigError(f'Loading Kernel module {module} failed')
+
+def get_config():
+ wwan = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ wwan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log"
+ wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}"
+
+ # Check if interface has been removed
+ if not conf.exists('interfaces wirelessmodem ' + wwan['intf']):
+ wwan['deleted'] = True
+ return wwan
+
+ # set new configuration level
+ conf.set_level('interfaces wirelessmodem ' + wwan['intf'])
+
+ # get metrick for backup default route
+ if conf.exists(['apn']):
+ wwan['apn'] = conf.return_value(['apn'])
+
+ # get metrick for backup default route
+ if conf.exists(['backup', 'distance']):
+ wwan['metric'] = conf.return_value(['backup', 'distance'])
+
+ # Retrieve interface description
+ if conf.exists(['description']):
+ wwan['description'] = conf.return_value(['description'])
+
+ # System device name
+ if conf.exists(['device']):
+ wwan['device'] = conf.return_value(['device'])
+
+ # disable interface
+ if conf.exists('disable'):
+ wwan['disable'] = True
+
+ # ignore link state changes
+ if conf.exists('disable-link-detect'):
+ wwan['disable_link_detect'] = 2
+
+ # Do not use DNS servers provided by the peer
+ if conf.exists(['mtu']):
+ wwan['mtu'] = conf.return_value(['mtu'])
+
+ # Do not use DNS servers provided by the peer
+ if conf.exists(['no-peer-dns']):
+ wwan['name_server'] = False
+
+ # Access concentrator name (only connect to this concentrator)
+ if conf.exists(['ondemand']):
+ wwan['on_demand'] = True
+
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ wwan['vrf'] = conf.return_value(['vrf'])
+
+ return wwan
+
+def verify(wwan):
+ if wwan['deleted']:
+ interface = wwan['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
+ return None
+
+ if not wwan['apn']:
+ raise ConfigError(f"APN for {wwan['intf']} not configured")
+
+ # we can not use isfile() here as Linux device files are no regular files
+ # thus the check will return False
+ if not os.path.exists(f"/dev/{wwan['device']}"):
+ raise ConfigError(f"Device {wwan['device']} does not exist")
+
+ vrf_name = wwan['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF {vrf_name} does not exist')
+
+ return None
+
+def generate(wwan):
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'wwan')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ # set up configuration file path variables where our templates will be
+ # rendered into
+ intf = wwan['intf']
+ config_wwan = f'/etc/ppp/peers/{intf}'
+ config_wwan_chat = wwan['chat_script']
+ script_wwan_pre_up = f'/etc/ppp/ip-pre-up.d/1010-vyos-wwan-{intf}'
+ script_wwan_ip_up = f'/etc/ppp/ip-up.d/1010-vyos-wwan-{intf}'
+ script_wwan_ip_down = f'/etc/ppp/ip-down.d/1010-vyos-wwan-{intf}'
+
+ config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up,
+ script_wwan_ip_up, script_wwan_ip_down]
+
+ # Ensure directories for config files exist - otherwise create them on demand
+ for file in config_files:
+ dirname = os.path.dirname(file)
+ if not os.path.isdir(dirname):
+ os.mkdir(dirname)
+
+ # Always hang-up WWAN connection prior generating new configuration file
+ cmd(f'systemctl stop ppp@{intf}.service')
+
+ if wwan['deleted']:
+ # Delete PPP configuration files
+ for file in config_files:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ else:
+ # Create PPP configuration files
+ tmpl = env.get_template('peer.tmpl')
+ config_text = tmpl.render(wwan)
+ with open(config_wwan, 'w') as f:
+ f.write(config_text)
+
+ # Create PPP chat script
+ tmpl = env.get_template('chat.tmpl')
+ config_text = tmpl.render(wwan)
+ with open(config_wwan_chat, 'w') as f:
+ f.write(config_text)
+
+ # Create script for ip-pre-up.d
+ tmpl = env.get_template('ip-pre-up.script.tmpl')
+ config_text = tmpl.render(wwan)
+ with open(script_wwan_pre_up, 'w') as f:
+ f.write(config_text)
+
+ # Create script for ip-up.d
+ tmpl = env.get_template('ip-up.script.tmpl')
+ config_text = tmpl.render(wwan)
+ with open(script_wwan_ip_up, 'w') as f:
+ f.write(config_text)
+
+ # Create script for ip-down.d
+ tmpl = env.get_template('ip-down.script.tmpl')
+ config_text = tmpl.render(wwan)
+ with open(script_wwan_ip_down, 'w') as f:
+ f.write(config_text)
+
+ # make generated script file executable
+ chmod_x(script_wwan_pre_up)
+ chmod_x(script_wwan_ip_up)
+ chmod_x(script_wwan_ip_down)
+
+ return None
+
+def apply(wwan):
+ if wwan['deleted']:
+ # bail out early
+ return None
+
+ if not wwan['disable']:
+ # "dial" WWAN connection
+ intf = wwan['intf']
+ cmd(f'systemctl start ppp@{intf}.service')
+ # make logfile owned by root / vyattacfg
+ chown(wwan['logfile'], 'root', 'vyattacfg')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ check_kmod()
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index e80c6caf0..dc04e9131 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,21 +13,18 @@
#
# 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 sys
import re
import os
-import jinja2
-import syslog as sl
-import time
-import vyos.config
-import vyos.defaults
+from time import sleep
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
-
+from vyos.util import call
ra_conn_name = "remote-access"
charon_conf_file = "/etc/strongswan.d/charon.conf"
@@ -42,55 +39,8 @@ delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###"
delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###"
charon_pidfile = "/var/run/charon.pid"
-l2pt_ipsec_conf = '''
-{{delim_ipsec_l2tp_begin}}
-include {{ipsec_ra_conn_file}}
-{{delim_ipsec_l2tp_end}}
-'''
-
-l2pt_ipsec_secrets_conf = '''
-{{delim_ipsec_l2tp_begin}}
-{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %}
-{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}"
-{% elif ipsec_l2tp_auth_mode == 'x509' %}
-: RSA {{server_key_file_copied}}
-{% endif%}
-{{delim_ipsec_l2tp_end}}
-'''
-
-l2tp_ipsec_ra_conn_conf = '''
-{{delim_ipsec_l2tp_begin}}
-conn {{ra_conn_name}}
- type=transport
- left={{outside_addr}}
- leftsubnet=%dynamic[/1701]
- rightsubnet=%dynamic
- mark_in=%unique
- auto=add
- ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024!
- dpddelay=15
- dpdtimeout=45
- dpdaction=clear
- esp=aes256-sha1,3des-sha1!
- rekey=no
-{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %}
- authby=secret
- leftauth=psk
- rightauth=psk
-{% elif ipsec_l2tp_auth_mode == 'x509' %}
- authby=rsasig
- leftrsasigkey=%cert
- rightrsasigkey=%cert
- rightca=%same
- leftcert={{server_cert_file_copied}}
-{% endif %}
- ikelifetime={{ipsec_l2tp_ike_lifetime}}
- keylife={{ipsec_l2tp_lifetime}}
-{{delim_ipsec_l2tp_end}}
-'''
-
def get_config():
- config = vyos.config.Config()
+ config = Config()
data = {"install_routes": "yes"}
if config.exists("vpn ipsec options disable-route-autoinstall"):
@@ -146,44 +96,12 @@ def get_config():
return data
-### ipsec secret l2tp
-def write_ipsec_secrets(c):
- tmpl = jinja2.Template(l2pt_ipsec_secrets_conf, trim_blocks=True)
- l2pt_ipsec_secrets_txt = tmpl.render(c)
- old_umask = os.umask(0o077)
- open(ipsec_secrets_flie,'w').write(l2pt_ipsec_secrets_txt)
- os.umask(old_umask)
- sl.syslog(sl.LOG_NOTICE, ipsec_secrets_flie + ' written')
-
-### ipsec remote access connection config
-def write_ipsec_ra_conn(c):
- tmpl = jinja2.Template(l2tp_ipsec_ra_conn_conf, trim_blocks=True)
- ipsec_ra_conn_txt = tmpl.render(c)
- old_umask = os.umask(0o077)
-
- # Create tunnels directory if does not exist
- if not os.path.exists(ipsec_ra_conn_dir):
- os.makedirs(ipsec_ra_conn_dir)
- sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_dir + " created")
-
- open(ipsec_ra_conn_file,'w').write(ipsec_ra_conn_txt)
- os.umask(old_umask)
- sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_file + ' written')
### Remove config from file by delimiter
def remove_confs(delim_begin, delim_end, conf_file):
- os.system("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file)
+ call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file)
-### Append "include /path/to/ra_conn" to ipsec conf file
-def append_ipsec_conf(c):
- tmpl = jinja2.Template(l2pt_ipsec_conf, trim_blocks=True)
- l2pt_ipsec_conf_txt = tmpl.render(c)
- old_umask = os.umask(0o077)
- open(ipsec_conf_flie,'a').write(l2pt_ipsec_conf_txt)
- os.umask(old_umask)
- sl.syslog(sl.LOG_NOTICE, ipsec_conf_flie + ' written')
-
### Checking certificate storage and notice if certificate not in /config directory
def check_cert_file_store(cert_name, file_path, dts_path):
if not re.search('^\/config\/.+', file_path):
@@ -194,11 +112,9 @@ def check_cert_file_store(cert_name, file_path, dts_path):
else:
### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/
# todo make check
- ret = os.system('cp -f '+file_path+' '+dts_path)
+ ret = call('cp -f '+file_path+' '+dts_path)
if ret:
raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path)
- else:
- sl.syslog(sl.LOG_NOTICE, file_path + ' copied to '+dts_path)
def verify(data):
# l2tp ipsec check
@@ -231,22 +147,45 @@ def verify(data):
raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.")
def generate(data):
- tmpl_path = os.path.join(vyos.defaults.directories["data"], "templates", "ipsec")
- fs_loader = jinja2.FileSystemLoader(tmpl_path)
- env = jinja2.Environment(loader=fs_loader)
-
-
- charon_conf_tmpl = env.get_template("charon.tmpl")
- charon_conf = charon_conf_tmpl.render(data)
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ipsec')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+ tmpl = env.get_template('charon.tmpl')
+ config_text = tmpl.render(data)
with open(charon_conf_file, 'w') as f:
- f.write(charon_conf)
+ f.write(config_text)
if data["ipsec_l2tp"]:
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie)
- write_ipsec_secrets(data)
- write_ipsec_ra_conn(data)
- append_ipsec_conf(data)
+
+ tmpl = env.get_template('ipsec.secrets.tmpl')
+ l2pt_ipsec_secrets_txt = tmpl.render(c)
+ old_umask = os.umask(0o077)
+ with open(ipsec_secrets_flie,'w') as f:
+ f.write(l2pt_ipsec_secrets_txt)
+ os.umask(old_umask)
+
+ tmpl = env.get_template('remote-access.tmpl')
+ ipsec_ra_conn_txt = tmpl.render(c)
+ old_umask = os.umask(0o077)
+
+ # Create tunnels directory if does not exist
+ if not os.path.exists(ipsec_ra_conn_dir):
+ os.makedirs(ipsec_ra_conn_dir)
+
+ with open(ipsec_ra_conn_file,'w') as f:
+ f.write(ipsec_ra_conn_txt)
+ os.umask(old_umask)
+
+
+ tmpl = env.get_template('ipsec.conf.tmpl')
+ l2pt_ipsec_conf_txt = tmpl.render(c)
+ old_umask = os.umask(0o077)
+ with open(ipsec_conf_flie,'a') as f:
+ f.write(l2pt_ipsec_conf_txt)
+ os.umask(old_umask)
+
else:
if os.path.exists(ipsec_ra_conn_file):
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file)
@@ -254,15 +193,15 @@ def generate(data):
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie)
def restart_ipsec():
- os.system("ipsec restart >&/dev/null")
+ call('ipsec restart >&/dev/null')
# counter for apply swanctl config
counter = 10
while counter <= 10:
if os.path.exists(charon_pidfile):
- os.system("swanctl -q >&/dev/null")
+ call('swanctl -q >&/dev/null')
break
counter -=1
- time.sleep(1)
+ sleep(1)
if counter == 0:
raise ConfigError('VPN configuration error: IPSec is not running.')
@@ -278,4 +217,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py
index c657098e1..4b365a566 100755
--- a/src/conf_mode/le_cert.py
+++ b/src/conf_mode/le_cert.py
@@ -18,11 +18,13 @@
import sys
import os
-import subprocess
import vyos.defaults
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import cmd
+from vyos.util import call
+
vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
@@ -45,9 +47,9 @@ def request_certbot(cert):
certbot_cmd = 'certbot certonly -n --nginx --agree-tos --no-eff-email --expand {0} {1}'.format(email_flag, domain_flag)
- completed = subprocess.run(certbot_cmd, shell=True)
-
- return completed.returncode
+ cmd(certbot_cmd,
+ raising=ConfigError,
+ message="The certbot request failed for the specified domains.")
def get_config():
conf = Config()
@@ -84,28 +86,21 @@ def generate(cert):
# certbot will attempt to reload nginx, even with 'certonly';
# start nginx if not active
- ret = os.system('systemctl is-active --quiet nginx.ervice')
+ ret = call('systemctl is-active --quiet nginx.ervice')
if ret:
- os.system('sudo systemctl start nginx.service')
+ call('sudo systemctl start nginx.service')
- ret = request_certbot(cert)
- if ret:
- raise ConfigError("The certbot request failed for the"
- " specified domains.")
+ request_certbot(cert)
def apply(cert):
if cert is not None:
- os.system('sudo systemctl restart certbot.timer')
+ call('sudo systemctl restart certbot.timer')
else:
- os.system('sudo systemctl stop certbot.timer')
+ call('sudo systemctl stop certbot.timer')
return None
for dep in dependencies:
- cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
- try:
- subprocess.check_call(cmd, shell=True)
- except subprocess.CalledProcessError as err:
- raise ConfigError(str(err))
+ cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py
index b72916ab8..ec59c68d0 100755
--- a/src/conf_mode/lldp.py
+++ b/src/conf_mode/lldp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2017-2019 VyOS maintainers and contributors
+# Copyright (C) 2017-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,47 +14,24 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import re
-import sys
import os
-import jinja2
+import re
from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
+
from vyos.config import Config
from vyos.validate import is_addr_assigned,is_loopback_addr
+from vyos.defaults import directories as vyos_data_dir
+from vyos.version import get_version_data
from vyos import ConfigError
+from vyos.util import call
-# Please be careful if you edit the template.
-config_file = "/etc/default/lldpd"
-lldp_tmpl = """
-### Autogenerated by lldp.py ###
-DAEMON_ARGS="-M 4{% if options.snmp %} -x{% endif %}{% if options.cdp %} -c{% endif %}{% if options.edp %} -e{% endif %}{% if options.fdp %} -f{% endif %}{% if options.sonmp %} -s{% endif %}"
-
-"""
+config_file = "/etc/default/lldpd"
vyos_config_file = "/etc/lldpd.d/01-vyos.conf"
-vyos_tmpl = """
-### Autogenerated by lldp.py ###
-
-configure system platform VyOS
-configure system description "VyOS {{ options.description }}"
-{% if options.listen_on -%}
-configure system interface pattern "{{ ( options.listen_on | select('equalto','all') | map('replace','all','*') | list + options.listen_on | select('equalto','!all') | map('replace','!all','!*') | list + options.listen_on | reject('equalto','all') | reject('equalto','!all') | list ) | unique | join(",") }}"
-{%- endif %}
-{% if options.mgmt_addr -%}
-configure system ip management pattern {{ options.mgmt_addr | join(",") }}
-{%- endif %}
-{%- for loc in location -%}
-{%- if loc.elin %}
-configure ports {{ loc.name }} med location elin "{{ loc.elin }}"
-{%- endif %}
-{%- if loc.coordinate_based %}
-configure ports {{ loc.name }} med location coordinate {% if loc.coordinate_based.latitude %}latitude {{ loc.coordinate_based.latitude }}{% endif %} {% if loc.coordinate_based.longitude %}longitude {{ loc.coordinate_based.longitude }}{% endif %} {% if loc.coordinate_based.altitude %}altitude {{ loc.coordinate_based.altitude }} m{% endif %} {% if loc.coordinate_based.datum %}datum {{ loc.coordinate_based.datum }}{% endif %}
-{%- endif %}
-
-
-{% endfor %}
-"""
+base = ['service', 'lldp']
default_config_data = {
"options": '',
@@ -64,7 +41,7 @@ default_config_data = {
def get_options(config):
options = {}
- config.set_level('service lldp')
+ config.set_level(base)
options['listen_vlan'] = config.exists('listen-vlan')
options['mgmt_addr'] = []
@@ -84,30 +61,31 @@ def get_options(config):
if snmp:
config.set_level('')
options["sys_snmp"] = config.exists('service snmp')
- config.set_level('service lldp')
+ config.set_level(base)
- config.set_level('service lldp legacy-protocols')
+ config.set_level(base + ['legacy-protocols'])
options['cdp'] = config.exists('cdp')
options['edp'] = config.exists('edp')
options['fdp'] = config.exists('fdp')
options['sonmp'] = config.exists('sonmp')
# start with an unknown version information
- options['description'] = 'unknown'
+ version_data = get_version_data()
+ options['description'] = version_data['version']
options['listen_on'] = []
return options
def get_interface_list(config):
- config.set_level('service lldp')
- intfs_names = config.list_nodes('interface')
+ config.set_level(base)
+ intfs_names = config.list_nodes(['interface'])
if len(intfs_names) < 0:
return 0
interface_list = []
for name in intfs_names:
- config.set_level('service lldp interface {0}'.format(name))
- disable = config.exists('disable')
+ config.set_level(base + ['interface', name])
+ disable = config.exists(['disable'])
intf = {
'name': name,
'disable': disable
@@ -117,10 +95,10 @@ def get_interface_list(config):
def get_location_intf(config, name):
- path = 'service lldp interface {0}'.format(name)
+ path = base + ['interface', name]
config.set_level(path)
- config.set_level('{} location'.format(path))
+ config.set_level(path + ['location'])
elin = ''
coordinate_based = {}
@@ -128,18 +106,18 @@ def get_location_intf(config, name):
elin = config.return_value('elin')
if config.exists('coordinate-based'):
- config.set_level('{} location coordinate-based'.format(path))
+ config.set_level(path + ['location', 'coordinate-based'])
- coordinate_based['latitude'] = config.return_value('latitude')
- coordinate_based['longitude'] = config.return_value('longitude')
+ coordinate_based['latitude'] = config.return_value(['latitude'])
+ coordinate_based['longitude'] = config.return_value(['longitude'])
coordinate_based['altitude'] = '0'
- if config.exists('altitude'):
- coordinate_based['altitude'] = config.return_value('altitude')
+ if config.exists(['altitude']):
+ coordinate_based['altitude'] = config.return_value(['altitude'])
coordinate_based['datum'] = 'WGS84'
- if config.exists('datum'):
- coordinate_based['datum'] = config.return_value('datum')
+ if config.exists(['datum']):
+ coordinate_based['datum'] = config.return_value(['datum'])
intf = {
'name': name,
@@ -151,8 +129,8 @@ def get_location_intf(config, name):
def get_location(config):
- config.set_level('service lldp')
- intfs_names = config.list_nodes('interface')
+ config.set_level(base)
+ intfs_names = config.list_nodes(['interface'])
if len(intfs_names) < 0:
return 0
@@ -170,7 +148,7 @@ def get_location(config):
def get_config():
lldp = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('service lldp'):
+ if not conf.exists(base):
return None
else:
lldp['options'] = get_options(conf)
@@ -232,10 +210,10 @@ def generate(lldp):
if lldp is None:
return
- with open('/opt/vyatta/etc/version', 'r') as f:
- tmp = f.read()
- lldp['options']['description'] = tmp.split()[1]
-
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'lldp')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
# generate listen on interfaces
for intf in lldp['interface_list']:
@@ -248,13 +226,13 @@ def generate(lldp):
lldp['options']['listen_on'].append(tmp)
# generate /etc/default/lldpd
- tmpl = jinja2.Template(lldp_tmpl)
+ tmpl = env.get_template('lldpd.tmpl')
config_text = tmpl.render(lldp)
with open(config_file, 'w') as f:
f.write(config_text)
# generate /etc/lldpd.d/01-vyos.conf
- tmpl = jinja2.Template(vyos_tmpl)
+ tmpl = env.get_template('vyos.conf.tmpl')
config_text = tmpl.render(lldp)
with open(vyos_config_file, 'w') as f:
f.write(config_text)
@@ -263,10 +241,10 @@ def generate(lldp):
def apply(lldp):
if lldp:
# start/restart lldp service
- os.system('sudo systemctl restart lldpd.service')
+ call('sudo systemctl restart lldpd.service')
else:
# LLDP service has been terminated
- os.system('sudo systemctl stop lldpd.service')
+ call('sudo systemctl stop lldpd.service')
os.unlink(config_file)
os.unlink(vyos_config_file)
@@ -278,5 +256,5 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/mdns_repeater.py
index cef735c0d..9230aaf61 100755
--- a/src/conf_mode/mdns_repeater.py
+++ b/src/conf_mode/mdns_repeater.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2017 VyOS maintainers and contributors
+# Copyright (C) 2017-2020 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
@@ -13,23 +13,21 @@
#
# 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 sys
import os
-import jinja2
-import netifaces
+
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
+from netifaces import ifaddresses, AF_INET
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
-config_file = r'/etc/default/mdns-repeater'
-config_tmpl = """
-### Autogenerated by mdns_repeater.py ###
-DAEMON_ARGS="{{ interfaces | join(' ') }}"
-"""
+config_file = r'/etc/default/mdns-repeater'
default_config_data = {
'disabled': False,
@@ -37,21 +35,22 @@ default_config_data = {
}
def get_config():
- mdns = default_config_data
+ mdns = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('service mdns repeater'):
+ base = ['service', 'mdns', 'repeater']
+ if not conf.exists(base):
return None
else:
- conf.set_level('service mdns repeater')
+ conf.set_level(base)
# Service can be disabled by user
- if conf.exists('disable'):
+ if conf.exists(['disable']):
mdns['disabled'] = True
return mdns
# Interface to repeat mDNS advertisements
- if conf.exists('interface'):
- mdns['interfaces'] = conf.return_values('interface')
+ if conf.exists(['interface']):
+ mdns['interfaces'] = conf.return_values(['interface'])
return mdns
@@ -69,8 +68,8 @@ def verify(mdns):
# For mdns-repeater to work it is essential that the interfaces has
# an IPv4 address assigned
for interface in mdns['interfaces']:
- if netifaces.AF_INET in netifaces.ifaddresses(interface).keys():
- if len(netifaces.ifaddresses(interface)[netifaces.AF_INET]) < 1:
+ if AF_INET in ifaddresses(interface).keys():
+ if len(ifaddresses(interface)[AF_INET]) < 1:
raise ConfigError('mDNS repeater requires an IPv6 address configured on interface %s!'.format(interface))
return None
@@ -83,7 +82,12 @@ def generate(mdns):
print('Warning: mDNS repeater will be deactivated because it is disabled')
return None
- tmpl = jinja2.Template(config_tmpl)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'mdns-repeater')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('mdns-repeater.tmpl')
config_text = tmpl.render(mdns)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -92,11 +96,11 @@ def generate(mdns):
def apply(mdns):
if (mdns is None) or mdns['disabled']:
- os.system('sudo systemctl stop mdns-repeater')
+ call('sudo systemctl stop mdns-repeater')
if os.path.exists(config_file):
os.unlink(config_file)
else:
- os.system('sudo systemctl restart mdns-repeater')
+ call('sudo systemctl restart mdns-repeater')
return None
@@ -108,4 +112,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index 8f32e6e81..75328dfd7 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,64 +13,21 @@
#
# 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 sys
import os
-import jinja2
-import ipaddress
-import copy
+from copy import deepcopy
+from ipaddress import ip_network
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
-config_file = r'/etc/ntp.conf'
-
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by ntp.py ###
-#
-# Non-configurable defaults
-#
-driftfile /var/lib/ntp/ntp.drift
-# By default, only allow ntpd to query time sources, ignore any incoming requests
-restrict default noquery nopeer notrap nomodify
-# Local users have unrestricted access, allowing reconfiguration via ntpdc
-restrict 127.0.0.1
-restrict -6 ::1
-
-# Do not listen on any interface address by default
-interface ignore wildcard
-#
-# Configurable section
-#
-
-{% if servers -%}
-{% for s in servers -%}
-# Server configuration for: {{ s.name }}
-server {{ s.name }} iburst {{ s.options | join(" ") }}
-{% endfor -%}
-{% endif %}
-
-{% if allowed_networks -%}
-{% for n in allowed_networks -%}
-# Client configuration for network: {{ n.network }}
-restrict {{ n.address }} mask {{ n.netmask }} nomodify notrap nopeer
-
-{% endfor -%}
-{% endif %}
-
-{% if listen_address -%}
-# NTP should listen on configured addresses only
-{% for a in listen_address -%}
-interface listen {{ a }}
-{% endfor -%}
-{% endif %}
-
-"""
+config_file = r'/etc/ntp.conf'
default_config_data = {
'servers': [],
@@ -79,7 +36,7 @@ default_config_data = {
}
def get_config():
- ntp = copy.deepcopy(default_config_data)
+ ntp = deepcopy(default_config_data)
conf = Config()
if not conf.exists('system ntp'):
return None
@@ -89,7 +46,7 @@ def get_config():
if conf.exists('allow-clients address'):
networks = conf.return_values('allow-clients address')
for n in networks:
- addr = ipaddress.ip_network(n)
+ addr = ip_network(n)
net = {
"network" : n,
"address" : addr.network_address,
@@ -131,7 +88,7 @@ def verify(ntp):
for n in ntp['allowed_networks']:
try:
- addr = ipaddress.ip_network( n['network'] )
+ addr = ip_network( n['network'] )
break
except ValueError:
raise ConfigError("{0} does not appear to be a valid IPv4 or IPv6 network, check host bits!".format(n['network']))
@@ -143,7 +100,12 @@ def generate(ntp):
if ntp is None:
return None
- tmpl = jinja2.Template(config_tmpl)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ntp')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('ntp.conf.tmpl')
config_text = tmpl.render(ntp)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -152,10 +114,10 @@ def generate(ntp):
def apply(ntp):
if ntp is not None:
- os.system('sudo systemctl restart ntp.service')
+ call('sudo systemctl restart ntp.service')
else:
# NTP support is removed in the commit
- os.system('sudo systemctl stop ntp.service')
+ call('sudo systemctl stop ntp.service')
os.unlink(config_file)
return None
@@ -168,4 +130,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index 58f5b5a0e..cf4db5f54 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 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
@@ -13,38 +13,21 @@
#
# 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 sys
-import jinja2
-import copy
import os
-import vyos.validate
-from vyos import ConfigError
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
+
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.validate import is_ipv6_link_local, is_ipv6
+from vyos import ConfigError
+from vyos.util import call
-config_file = r'/tmp/bfd.frr'
-# Please be careful if you edit the template.
-config_tmpl = """
-!
-bfd
-{% for peer in old_peers -%}
- no peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %}
-{% endfor -%}
-!
-{% for peer in new_peers -%}
- peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %}
- detect-multiplier {{ peer.multiplier }}
- receive-interval {{ peer.rx_interval }}
- transmit-interval {{ peer.tx_interval }}
- {% if peer.echo_mode %}echo-mode{% endif %}
- {% if peer.echo_interval != '' %}echo-interval {{ peer.echo_interval }}{% endif %}
- {% if not peer.shutdown %}no {% endif %}shutdown
-{% endfor -%}
-!
-"""
+config_file = r'/tmp/bfd.frr'
default_config_data = {
'new_peers': [],
@@ -132,7 +115,7 @@ def get_bfd_peer_config(peer, conf_mode="proposed"):
return bfd_peer
def get_config():
- bfd = copy.deepcopy(default_config_data)
+ bfd = deepcopy(default_config_data)
conf = Config()
if not (conf.exists('protocols bfd') or conf.exists_effective('protocols bfd')):
return None
@@ -164,12 +147,12 @@ def verify(bfd):
for peer in bfd['new_peers']:
# IPv6 link local peers require an explicit local address/interface
- if vyos.validate.is_ipv6_link_local(peer['remote']):
+ if is_ipv6_link_local(peer['remote']):
if not (peer['src_if'] and peer['src_addr']):
raise ConfigError('BFD IPv6 link-local peers require explicit local address and interface setting')
# IPv6 peers require an explicit local address
- if vyos.validate.is_ipv6(peer['remote']):
+ if is_ipv6(peer['remote']):
if not peer['src_addr']:
raise ConfigError('BFD IPv6 peers require explicit local address setting')
@@ -208,18 +191,23 @@ def generate(bfd):
if bfd is None:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'frr-bfd')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('bfd.frr.tmpl')
+ config_text = tmpl.render(bfd)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
return None
def apply(bfd):
if bfd is None:
return None
- tmpl = jinja2.Template(config_tmpl)
- config_text = tmpl.render(bfd)
- with open(config_file, 'w') as f:
- f.write(config_text)
-
- os.system("sudo vtysh -d bfdd -f " + config_file)
+ call("vtysh -d bfdd -f " + config_file)
if os.path.exists(config_file):
os.remove(config_file)
@@ -233,4 +221,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py
new file mode 100755
index 000000000..141b1950d
--- /dev/null
+++ b/src/conf_mode/protocols_igmp.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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 os
+
+from ipaddress import IPv4Address
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
+
+from vyos import ConfigError
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.util import call
+
+
+config_file = r'/tmp/igmp.frr'
+
+def get_config():
+ conf = Config()
+ igmp_conf = {
+ 'igmp_conf' : False,
+ 'old_ifaces' : {},
+ 'ifaces' : {}
+ }
+ if not (conf.exists('protocols igmp') or conf.exists_effective('protocols igmp')):
+ return None
+
+ if conf.exists('protocols igmp'):
+ igmp_conf['igmp_conf'] = True
+
+ conf.set_level('protocols igmp')
+
+ # # Get interfaces
+ for iface in conf.list_effective_nodes('interface'):
+ igmp_conf['old_ifaces'].update({
+ iface : {
+ 'version' : conf.return_effective_value('interface {0} version'.format(iface)),
+ 'query_interval' : conf.return_effective_value('interface {0} query-interval'.format(iface)),
+ 'query_max_resp_time' : conf.return_effective_value('interface {0} query-max-response-time'.format(iface)),
+ 'gr_join' : {}
+ }
+ })
+ for gr_join in conf.list_effective_nodes('interface {0} join'.format(iface)):
+ igmp_conf['old_ifaces'][iface]['gr_join'][gr_join] = conf.return_effective_values('interface {0} join {1} source'.format(iface, gr_join))
+
+ for iface in conf.list_nodes('interface'):
+ igmp_conf['ifaces'].update({
+ iface : {
+ 'version' : conf.return_value('interface {0} version'.format(iface)),
+ 'query_interval' : conf.return_value('interface {0} query-interval'.format(iface)),
+ 'query_max_resp_time' : conf.return_value('interface {0} query-max-response-time'.format(iface)),
+ 'gr_join' : {}
+ }
+ })
+ for gr_join in conf.list_nodes('interface {0} join'.format(iface)):
+ igmp_conf['ifaces'][iface]['gr_join'][gr_join] = conf.return_values('interface {0} join {1} source'.format(iface, gr_join))
+
+ return igmp_conf
+
+def verify(igmp):
+ if igmp is None:
+ return None
+
+ if igmp['igmp_conf']:
+ # Check interfaces
+ if not igmp['ifaces']:
+ raise ConfigError(f"IGMP require defined interfaces!")
+ # Check, is this multicast group
+ for intfc in igmp['ifaces']:
+ for gr_addr in igmp['ifaces'][intfc]['gr_join']:
+ if IPv4Address(gr_addr) < IPv4Address('224.0.0.0'):
+ raise ConfigError(gr_addr + " not a multicast group")
+
+def generate(igmp):
+ if igmp is None:
+ return None
+
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'igmp')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('igmp.frr.tmpl')
+ config_text = tmpl.render(igmp)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(igmp):
+ if igmp is None:
+ return None
+
+ if os.path.exists(config_file):
+ call("sudo vtysh -d pimd -f " + config_file)
+ os.remove(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
new file mode 100755
index 000000000..b5753aea8
--- /dev/null
+++ b/src/conf_mode/protocols_mpls.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 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 os
+
+from jinja2 import FileSystemLoader, Environment
+
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos import ConfigError
+from vyos.util import call
+
+
+config_file = r'/tmp/ldpd.frr'
+
+def sysctl(name, value):
+ call('sysctl -wq {}={}'.format(name, value))
+
+def get_config():
+ conf = Config()
+ mpls_conf = {
+ 'router_id' : None,
+ 'mpls_ldp' : False,
+ 'old_ldp' : {
+ 'interfaces' : [],
+ 'neighbors' : {},
+ 'd_transp_ipv4' : None,
+ 'd_transp_ipv6' : None
+ },
+ 'ldp' : {
+ 'interfaces' : [],
+ 'neighbors' : {},
+ 'd_transp_ipv4' : None,
+ 'd_transp_ipv6' : None
+ }
+ }
+ if not (conf.exists('protocols mpls') or conf.exists_effective('protocols mpls')):
+ return None
+
+ if conf.exists('protocols mpls ldp'):
+ mpls_conf['mpls_ldp'] = True
+
+ conf.set_level('protocols mpls ldp')
+
+ # Get router-id
+ if conf.exists_effective('router-id'):
+ mpls_conf['old_router_id'] = conf.return_effective_value('router-id')
+ if conf.exists('router-id'):
+ mpls_conf['router_id'] = conf.return_value('router-id')
+
+ # Get discovery transport-ipv4-address
+ if conf.exists_effective('discovery transport-ipv4-address'):
+ mpls_conf['old_ldp']['d_transp_ipv4'] = conf.return_effective_value('discovery transport-ipv4-address')
+
+ if conf.exists('discovery transport-ipv4-address'):
+ mpls_conf['ldp']['d_transp_ipv4'] = conf.return_value('discovery transport-ipv4-address')
+
+ # Get discovery transport-ipv6-address
+ if conf.exists_effective('discovery transport-ipv6-address'):
+ mpls_conf['old_ldp']['d_transp_ipv6'] = conf.return_effective_value('discovery transport-ipv6-address')
+
+ if conf.exists('discovery transport-ipv6-address'):
+ mpls_conf['ldp']['d_transp_ipv6'] = conf.return_value('discovery transport-ipv6-address')
+
+ # Get interfaces
+ if conf.exists_effective('interface'):
+ mpls_conf['old_ldp']['interfaces'] = conf.return_effective_values('interface')
+
+ if conf.exists('interface'):
+ mpls_conf['ldp']['interfaces'] = conf.return_values('interface')
+
+ # Get neighbors
+ for neighbor in conf.list_effective_nodes('neighbor'):
+ mpls_conf['old_ldp']['neighbors'].update({
+ neighbor : {
+ 'password' : conf.return_effective_value('neighbor {0} password'.format(neighbor))
+ }
+ })
+
+ for neighbor in conf.list_nodes('neighbor'):
+ mpls_conf['ldp']['neighbors'].update({
+ neighbor : {
+ 'password' : conf.return_value('neighbor {0} password'.format(neighbor))
+ }
+ })
+
+ return mpls_conf
+
+def operate_mpls_on_intfc(interfaces, action):
+ rp_filter = 0
+ if action == 1:
+ rp_filter = 2
+ for iface in interfaces:
+ sysctl('net.mpls.conf.{0}.input'.format(iface), action)
+ # Operate rp filter
+ sysctl('net.ipv4.conf.{0}.rp_filter'.format(iface), rp_filter)
+
+def verify(mpls):
+ if mpls is None:
+ return None
+
+ if mpls['mpls_ldp']:
+ # Requre router-id
+ if not mpls['router_id']:
+ raise ConfigError(f"MPLS ldp router-id is mandatory!")
+
+ # Requre discovery transport-address
+ if not mpls['ldp']['d_transp_ipv4'] and not mpls['ldp']['d_transp_ipv6']:
+ raise ConfigError(f"MPLS ldp discovery transport address is mandatory!")
+
+ # Requre interface
+ if not mpls['ldp']['interfaces']:
+ raise ConfigError(f"MPLS ldp interface is mandatory!")
+
+def generate(mpls):
+ if mpls is None:
+ return None
+
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'mpls')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('ldpd.frr.tmpl')
+ config_text = tmpl.render(mpls)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(mpls):
+ if mpls is None:
+ return None
+
+ # Set number of entries in the platform label table
+ if mpls['mpls_ldp']:
+ sysctl('net.mpls.platform_labels', '1048575')
+ else:
+ sysctl('net.mpls.platform_labels', '0')
+
+ # Do not copy IP TTL to MPLS header
+ sysctl('net.mpls.ip_ttl_propagate', '0')
+
+ # Allow mpls on interfaces
+ operate_mpls_on_intfc(mpls['ldp']['interfaces'], 1)
+
+ # Disable mpls on deleted interfaces
+ diactive_ifaces = set(mpls['old_ldp']['interfaces']).difference(mpls['ldp']['interfaces'])
+ operate_mpls_on_intfc(diactive_ifaces, 0)
+
+ if os.path.exists(config_file):
+ call("sudo vtysh -d ldpd -f " + config_file)
+ os.remove(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py
new file mode 100755
index 000000000..44fc9293b
--- /dev/null
+++ b/src/conf_mode/protocols_pim.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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 os
+
+from ipaddress import IPv4Address
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
+
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos import ConfigError
+from vyos.util import call
+
+
+config_file = r'/tmp/pimd.frr'
+
+def get_config():
+ conf = Config()
+ pim_conf = {
+ 'pim_conf' : False,
+ 'old_pim' : {
+ 'ifaces' : {},
+ 'rp' : {}
+ },
+ 'pim' : {
+ 'ifaces' : {},
+ 'rp' : {}
+ }
+ }
+ if not (conf.exists('protocols pim') or conf.exists_effective('protocols pim')):
+ return None
+
+ if conf.exists('protocols pim'):
+ pim_conf['pim_conf'] = True
+
+ conf.set_level('protocols pim')
+
+ # Get interfaces
+ for iface in conf.list_effective_nodes('interface'):
+ pim_conf['old_pim']['ifaces'].update({
+ iface : {
+ 'hello' : conf.return_effective_value('interface {0} hello'.format(iface)),
+ 'dr_prio' : conf.return_effective_value('interface {0} dr-priority'.format(iface))
+ }
+ })
+
+ for iface in conf.list_nodes('interface'):
+ pim_conf['pim']['ifaces'].update({
+ iface : {
+ 'hello' : conf.return_value('interface {0} hello'.format(iface)),
+ 'dr_prio' : conf.return_value('interface {0} dr-priority'.format(iface)),
+ }
+ })
+
+ conf.set_level('protocols pim rp')
+
+ # Get RPs addresses
+ for rp_addr in conf.list_effective_nodes('address'):
+ pim_conf['old_pim']['rp'][rp_addr] = conf.return_effective_values('address {0} group'.format(rp_addr))
+
+ for rp_addr in conf.list_nodes('address'):
+ pim_conf['pim']['rp'][rp_addr] = conf.return_values('address {0} group'.format(rp_addr))
+
+ # Get RP keep-alive-timer
+ if conf.exists_effective('rp keep-alive-timer'):
+ pim_conf['old_pim']['rp_keep_alive'] = conf.return_effective_value('rp keep-alive-timer')
+ if conf.exists('rp keep-alive-timer'):
+ pim_conf['pim']['rp_keep_alive'] = conf.return_value('rp keep-alive-timer')
+
+ return pim_conf
+
+def verify(pim):
+ if pim is None:
+ return None
+
+ if pim['pim_conf']:
+ # Check interfaces
+ if not pim['pim']['ifaces']:
+ raise ConfigError(f"PIM require defined interfaces!")
+
+ if not pim['pim']['rp']:
+ raise ConfigError(f"RP address required")
+
+ # Check unique multicast groups
+ uniq_groups = []
+ for rp_addr in pim['pim']['rp']:
+ if not pim['pim']['rp'][rp_addr]:
+ raise ConfigError(f"Group should be specified for RP " + rp_addr)
+ for group in pim['pim']['rp'][rp_addr]:
+ if (group in uniq_groups):
+ raise ConfigError(f"Group range " + group + " specified cannot exact match another")
+
+ # Check, is this multicast group
+ gr_addr = group.split('/')
+ if IPv4Address(gr_addr[0]) < IPv4Address('224.0.0.0'):
+ raise ConfigError(group + " not a multicast group")
+
+ uniq_groups.extend(pim['pim']['rp'][rp_addr])
+
+def generate(pim):
+ if pim is None:
+ return None
+
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pim')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('pimd.frr.tmpl')
+ config_text = tmpl.render(pim)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(pim):
+ if pim is None:
+ return None
+
+ if os.path.exists(config_file):
+ call("vtysh -d pimd -f " + config_file)
+ os.remove(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py
new file mode 100755
index 000000000..bfc3a707e
--- /dev/null
+++ b/src/conf_mode/salt-minion.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 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 os
+
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
+from pwd import getpwnam
+from socket import gethostname
+from sys import exit
+from urllib3 import PoolManager
+
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos import ConfigError
+from vyos.util import call
+
+
+config_file = r'/etc/salt/minion'
+
+default_config_data = {
+ 'hash_type': 'sha256',
+ 'log_file': '/var/log/salt/minion',
+ 'log_level': 'warning',
+ 'master' : 'salt',
+ 'user': 'minion',
+ 'salt_id': gethostname(),
+ 'mine_interval': '60',
+ 'verify_master_pubkey_sign': 'false'
+}
+
+def get_config():
+ salt = deepcopy(default_config_data)
+ conf = Config()
+ if not conf.exists('service salt-minion'):
+ return None
+ else:
+ conf.set_level('service salt-minion')
+
+ if conf.exists('hash_type'):
+ salt['hash_type'] = conf.return_value('hash_type')
+
+ if conf.exists('log_file'):
+ salt['log_file'] = conf.return_value('log_file')
+
+ if conf.exists('log_level'):
+ salt['log_level'] = conf.return_value('log_level')
+
+ if conf.exists('master'):
+ master = conf.return_values('master')
+ salt['master'] = master
+
+ if conf.exists('id'):
+ salt['salt_id'] = conf.return_value('id')
+
+ if conf.exists('user'):
+ salt['user'] = conf.return_value('user')
+
+ if conf.exists('mine_interval'):
+ salt['mine_interval'] = conf.return_value('mine_interval')
+
+ salt['master-key'] = None
+ if conf.exists('master-key'):
+ salt['master-key'] = conf.return_value('master-key')
+ salt['verify_master_pubkey_sign'] = 'true'
+
+ return salt
+
+def generate(salt):
+ paths = ['/etc/salt/','/var/run/salt','/opt/vyatta/etc/config/salt/']
+ directory = '/opt/vyatta/etc/config/salt/pki/minion'
+ uid = getpwnam(salt['user']).pw_uid
+ http = PoolManager()
+
+ if salt is None:
+ return None
+
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'salt-minion')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+
+ tmpl = env.get_template('minion.tmpl')
+ config_text = tmpl.render(salt)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ path = "/etc/salt/"
+ for path in paths:
+ for root, dirs, files in os.walk(path):
+ for usgr in dirs:
+ os.chown(os.path.join(root, usgr), uid, 100)
+ for usgr in files:
+ os.chown(os.path.join(root, usgr), uid, 100)
+
+ if not os.path.exists('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'):
+ if not salt['master-key'] is None:
+ r = http.request('GET', salt['master-key'], preload_content=False)
+
+ with open('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub', 'wb') as out:
+ while True:
+ data = r.read(1024)
+ if not data:
+ break
+ out.write(data)
+
+ r.release_conn()
+
+ return None
+
+def apply(salt):
+ if salt is not None:
+ call("sudo systemctl restart salt-minion")
+ else:
+ # Salt access is removed in the commit
+ call("sudo systemctl stop salt-minion")
+ os.unlink(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/service-ipoe.py b/src/conf_mode/service-ipoe.py
index bd7a898d0..5bd4aea2e 100755
--- a/src/conf_mode/service-ipoe.py
+++ b/src/conf_mode/service-ipoe.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,20 +13,19 @@
#
# 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 sys
import os
import re
-import time
-import socket
-import subprocess
-import jinja2
-import syslog as sl
+
+from jinja2 import FileSystemLoader, Environment
+from socket import socket, AF_INET, SOCK_STREAM
+from sys import exit
+from time import sleep
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import run
ipoe_cnf_dir = r'/etc/accel-ppp/ipoe'
ipoe_cnf = ipoe_cnf_dir + r'/ipoe.config'
@@ -37,157 +36,8 @@ cmd_port = r'2002'
chap_secrets = ipoe_cnf_dir + '/chap-secrets'
## accel-pppd -d -c /etc/accel-ppp/pppoe/pppoe.config -p /var/run/accel_pppoe.pid
-ipoe_config = '''
-### generated by ipoe.py ###
-[modules]
-log_syslog
-ipoe
-shaper
-ipv6pool
-ipv6_nd
-ipv6_dhcp
-{% if auth['mech'] == 'radius' %}
-radius
-{% endif -%}
-ippool
-{% if auth['mech'] == 'local' %}
-chap-secrets
-{% endif %}
-
-[core]
-thread-count={{thread_cnt}}
-
-[log]
-syslog=accel-ipoe,daemon
-copy=1
-level=5
-
-[ipoe]
-verbose=1
-{% for intfc in interfaces %}
-{% if interfaces[intfc]['vlan_mon'] %}
-interface=re:{{intfc}}\.\d+,\
-{% else %}
-interface={{intfc}},\
-{% endif %}
-shared={{interfaces[intfc]['shared']}},\
-mode={{interfaces[intfc]['mode']}},\
-ifcfg={{interfaces[intfc]['ifcfg']}},\
-range={{interfaces[intfc]['range']}},\
-start={{interfaces[intfc]['sess_start']}},\
-ipv6=1
-{% endfor %}
-{% if auth['mech'] == 'noauth' %}
-noauth=1
-{% endif %}
-{% if auth['mech'] == 'local' %}
-username=ifname
-password=csid
-{% endif %}
-
-{%- for intfc in interfaces %}
-{% if (interfaces[intfc]['shared'] == '0') and (interfaces[intfc]['vlan_mon']) %}
-vlan-mon={{intfc}},{{interfaces[intfc]['vlan_mon']|join(',')}}
-{% endif %}
-{% endfor %}
-
-{% if (dns['server1']) or (dns['server2']) %}
-[dns]
-{% if dns['server1'] %}
-dns1={{dns['server1']}}
-{% endif -%}
-{% if dns['server2'] %}
-dns2={{dns['server2']}}
-{% endif -%}
-{% endif -%}
-
-{% if (dnsv6['server1']) or (dnsv6['server2']) or (dnsv6['server3']) %}
-[dnsv6]
-dns={{dnsv6['server1']}}
-dns={{dnsv6['server2']}}
-dns={{dnsv6['server3']}}
-{% endif %}
-
-[ipv6-nd]
-verbose=1
-
-[ipv6-dhcp]
-verbose=1
-
-{% if ipv6['prfx'] %}
-[ipv6-pool]
-{% for prfx in ipv6['prfx'] %}
-{{prfx}}
-{% endfor %}
-{% for pd in ipv6['pd'] %}
-delegate={{pd}}
-{% endfor %}
-{% endif %}
-
-{% if auth['mech'] == 'local' %}
-[chap-secrets]
-chap-secrets=/etc/accel-ppp/ipoe/chap-secrets
-{% endif %}
-
-{% if auth['mech'] == 'radius' %}
-[radius]
-verbose=1
-{% for srv in auth['radius'] %}
-server={{srv}},{{auth['radius'][srv]['secret']}},\
-req-limit={{auth['radius'][srv]['req-limit']}},\
-fail-time={{auth['radius'][srv]['fail-time']}}
-{% endfor %}
-{% if auth['radsettings']['dae-server']['ip-address'] %}
-dae-server={{auth['radsettings']['dae-server']['ip-address']}}:\
-{{auth['radsettings']['dae-server']['port']}},\
-{{auth['radsettings']['dae-server']['secret']}}
-{% endif -%}
-{% if auth['radsettings']['acct-timeout'] %}
-acct-timeout={{auth['radsettings']['acct-timeout']}}
-{% endif -%}
-{% if auth['radsettings']['max-try'] %}
-max-try={{auth['radsettings']['max-try']}}
-{% endif -%}
-{% if auth['radsettings']['timeout'] %}
-timeout={{auth['radsettings']['timeout']}}
-{% endif -%}
-{% if auth['radsettings']['nas-ip-address'] %}
-nas-ip-address={{auth['radsettings']['nas-ip-address']}}
-{% endif -%}
-{% if auth['radsettings']['nas-identifier'] %}
-nas-identifier={{auth['radsettings']['nas-identifier']}}
-{% endif -%}
-{% endif %}
-
-[cli]
-tcp=127.0.0.1:2002
-'''
-
-# chap secrets
-chap_secrets_conf = '''
-# username server password acceptable local IP addresses shaper
-{% for aifc in auth['auth_if'] %}
-{% for mac in auth['auth_if'][aifc] %}
-{% if (auth['auth_if'][aifc][mac]['up']) and (auth['auth_if'][aifc][mac]['down']) %}
-{% if auth['auth_if'][aifc][mac]['vlan'] %}
-{{aifc}}.{{auth['auth_if'][aifc][mac]['vlan']}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}}
-{% else %}
-{{aifc}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}}
-{% endif %}
-{% else %}
-{% if auth['auth_if'][aifc][mac]['vlan'] %}
-{{aifc}}.{{auth['auth_if'][aifc][mac]['vlan']}}\t*\t{{mac.lower()}}\t*
-{% else %}
-{{aifc}}\t*\t{{mac.lower()}}\t*
-{% endif %}
-{% endif %}
-{% endfor %}
-{% endfor %}
-'''
-
if not os.path.exists(ipoe_cnf_dir):
os.makedirs(ipoe_cnf_dir)
- sl.syslog(sl.LOG_NOTICE, ipoe_cnf_dir + " created")
def _get_cpu():
@@ -201,40 +51,21 @@ def _get_cpu():
def _chk_con():
cnt = 0
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s = socket(AF_INET, SOCK_STREAM)
while True:
try:
s.connect(("127.0.0.1", int(cmd_port)))
break
except ConnectionRefusedError:
- time.sleep(0.5)
+ sleep(0.5)
cnt += 1
if cnt == 100:
raise("failed to start pppoe server")
break
-def _accel_cmd(cmd=''):
- if not cmd:
- return None
- try:
- ret = subprocess.check_output(
- ['/usr/bin/accel-cmd', '-p', cmd_port, cmd]).decode().strip()
- return ret
- except:
- return 1
-
-# chap_secrets file if auth mode local
-
-
-def _gen_chap_secrets(c):
-
- tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True)
- chap_secrets_txt = tmpl.render(c)
- old_umask = os.umask(0o077)
- open(chap_secrets, 'w').write(chap_secrets_txt)
- os.umask(old_umask)
- sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written')
+def _accel_cmd(command):
+ return run('/usr/bin/accel-cmd -p {cmd_port} {command}')
##### Inline functions end ####
@@ -388,14 +219,25 @@ def generate(c):
if c == None or not c:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ipoe-server')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
c['thread_cnt'] = _get_cpu()
if c['auth']['mech'] == 'local':
- _gen_chap_secrets(c)
-
- tmpl = jinja2.Template(ipoe_config, trim_blocks=True)
+ tmpl = env.get_template('chap-secrets.tmpl')
+ chap_secrets_txt = tmpl.render(c)
+ old_umask = os.umask(0o077)
+ with open(chap_secrets, 'w') as f:
+ f.write(chap_secrets_txt)
+ os.umask(old_umask)
+
+ tmpl = env.get_template('ipoe.config.tmpl')
config_text = tmpl.render(c)
- open(ipoe_cnf, 'w').write(config_text)
+ with open(ipoe_cnf, 'w') as f:
+ f.write(config_text)
return c
@@ -457,15 +299,13 @@ def apply(c):
return None
if not os.path.exists(pidfile):
- ret = subprocess.call(
- ['/usr/sbin/accel-pppd', '-c', ipoe_cnf, '-p', pidfile, '-d'])
+ ret = run(f'/usr/sbin/accel-pppd -c {ipoe_cnf} -p {pidfile} -d')
_chk_con()
if ret != 0 and os.path.exists(pidfile):
os.remove(pidfile)
raise ConfigError('accel-pppd failed to start')
else:
_accel_cmd('restart')
- sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart")
if __name__ == '__main__':
@@ -476,4 +316,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/service-pppoe.py b/src/conf_mode/service-pppoe.py
index 22250d18b..d3fc82406 100755
--- a/src/conf_mode/service-pppoe.py
+++ b/src/conf_mode/service-pppoe.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,20 +13,19 @@
#
# 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 sys
import os
import re
-import subprocess
-import jinja2
-import socket
-import time
-import syslog as sl
+
+from jinja2 import FileSystemLoader, Environment
+from socket import socket, AF_INET, SOCK_STREAM
+from sys import exit
+from time import sleep
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import run
pidfile = r'/var/run/accel_pppoe.pid'
pppoe_cnf_dir = r'/etc/accel-ppp/pppoe'
@@ -38,291 +37,28 @@ pppoe_conf = pppoe_cnf_dir + '/pppoe.config'
# config path creation
if not os.path.exists(pppoe_cnf_dir):
os.makedirs(pppoe_cnf_dir)
- sl.syslog(sl.LOG_NOTICE, pppoe_cnf_dir + " created")
-
-pppoe_config = '''
-### generated by accel_pppoe.py ###
-[modules]
-log_syslog
-pppoe
-{% if authentication['mode'] == 'radius' %}
-radius
-{% endif %}
-ippool
-{% if ppp_options['ipv6'] != 'deny' %}
-ipv6pool
-ipv6_nd
-ipv6_dhcp
-{% endif %}
-chap-secrets
-auth_pap
-auth_chap_md5
-auth_mschap_v1
-auth_mschap_v2
-#pppd_compat
-shaper
-{% if snmp == 'enable' or snmp == 'enable-ma' %}
-net-snmp
-{% endif %}
-{% if limits %}
-connlimit
-{% endif %}
-
-[core]
-thread-count={{thread_cnt}}
-
-[log]
-syslog=accel-pppoe,daemon
-copy=1
-level=5
-
-{% if snmp == 'enable-ma' %}
-[snmp]
-master=1
-{% endif -%}
-
-[client-ip-range]
-disable
-
-{% if ppp_gw %}
-[ip-pool]
-gw-ip-address={{ppp_gw}}
-{% if client_ip_pool %}
-{{client_ip_pool}}
-{% endif -%}
-
-{% if client_ip_subnets %}
-{% for sn in client_ip_subnets %}
-{{sn}}
-{% endfor %}
-{% endif %}
-{% endif -%}
-
-{% if client_ipv6_pool %}
-[ipv6-pool]
-{% for prfx in client_ipv6_pool['prefix']: %}
-{{prfx}}
-{% endfor %}
-{% for prfx in client_ipv6_pool['delegate-prefix']: %}
-delegate={{prfx}}
-{% endfor %}
-{% endif %}
-
-{% if dns %}
-[dns]
-{% if dns[0] %}
-dns1={{dns[0]}}
-{% endif -%}
-{% if dns[1] %}
-dns2={{dns[1]}}
-{% endif -%}
-{% endif %}
-
-{% if dnsv6 %}
-[ipv6-dns]
-{% for srv in dnsv6: %}
-{{srv}}
-{% endfor %}
-{% endif %}
-
-{% if wins %}
-[wins]
-{% if wins[0] %}
-wins1={{wins[0]}}
-{% endif %}
-{% if wins[1] %}
-wins2={{wins[1]}}
-{% endif -%}
-{% endif -%}
-
-{% if authentication['mode'] == 'local' %}
-[chap-secrets]
-chap-secrets=/etc/accel-ppp/pppoe/chap-secrets
-{% endif -%}
-
-{% if authentication['mode'] == 'radius' %}
-[radius]
-{% for rsrv in authentication['radiussrv']: %}
-server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\
-req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\
-fail-time={{authentication['radiussrv'][rsrv]['fail-time']}}
-{% endfor %}
-{% if authentication['radiusopt']['timeout'] %}
-timeout={{authentication['radiusopt']['timeout']}}
-{% endif %}
-{% if authentication['radiusopt']['acct-timeout'] %}
-acct-timeout={{authentication['radiusopt']['acct-timeout']}}
-{% endif %}
-{% if authentication['radiusopt']['max-try'] %}
-max-try={{authentication['radiusopt']['max-try']}}
-{% endif %}
-{% if authentication['radiusopt']['nas-id'] %}
-nas-identifier={{authentication['radiusopt']['nas-id']}}
-{% endif %}
-{% if authentication['radiusopt']['nas-ip'] %}
-nas-ip-address={{authentication['radiusopt']['nas-ip']}}
-{% endif -%}
-{% if authentication['radiusopt']['dae-srv'] %}
-dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\
-{{authentication['radiusopt']['dae-srv']['port']}},\
-{{authentication['radiusopt']['dae-srv']['secret']}}
-{% endif -%}
-gw-ip-address={{ppp_gw}}
-verbose=1
-
-{% if authentication['radiusopt']['shaper'] %}
-[shaper]
-verbose=1
-attr={{authentication['radiusopt']['shaper']['attr']}}
-{% if authentication['radiusopt']['shaper']['vendor'] %}
-vendor={{authentication['radiusopt']['shaper']['vendor']}}
-{% endif -%}
-{% endif -%}
-{% endif %}
-
-[ppp]
-verbose=1
-check-ip=1
-{% if not sesscrtl == 'disable' %}
-single-session={{sesscrtl}}
-{% endif -%}
-{% if ppp_options['ccp'] %}
-ccp=1
-{% endif %}
-{% if ppp_options['min-mtu'] %}
-min-mtu={{ppp_options['min-mtu']}}
-{% else %}
-min-mtu={{mtu}}
-{% endif %}
-{% if ppp_options['mru'] %}
-mru={{ppp_options['mru']}}
-{% endif %}
-{% if ppp_options['mppe'] %}
-mppe={{ppp_options['mppe']}}
-{% else %}
-mppe=prefer
-{% endif %}
-{% if ppp_options['lcp-echo-interval'] %}
-lcp-echo-interval={{ppp_options['lcp-echo-interval']}}
-{% else %}
-lcp-echo-interval=30
-{% endif %}
-{% if ppp_options['lcp-echo-timeout'] %}
-lcp-echo-timeout={{ppp_options['lcp-echo-timeout']}}
-{% endif %}
-{% if ppp_options['lcp-echo-failure'] %}
-lcp-echo-failure={{ppp_options['lcp-echo-failure']}}
-{% else %}
-lcp-echo-failure=3
-{% endif %}
-{% if ppp_options['ipv4'] %}
-ipv4={{ppp_options['ipv4']}}
-{% endif %}
-{% if client_ipv6_pool %}
-ipv6=allow
-{% endif %}
-
-{% if ppp_options['ipv6'] %}
-ipv6={{ppp_options['ipv6']}}
-{% if ppp_options['ipv6-intf-id'] %}
-ipv6-intf-id={{ppp_options['ipv6-intf-id']}}
-{% endif %}
-{% if ppp_options['ipv6-peer-intf-id'] %}
-ipv6-peer-intf-id={{ppp_options['ipv6-peer-intf-id']}}
-{% endif %}
-{% if ppp_options['ipv6-accept-peer-intf-id'] %}
-ipv6-accept-peer-intf-id={{ppp_options['ipv6-accept-peer-intf-id']}}
-{% endif %}
-{% endif %}
-mtu={{mtu}}
-
-[pppoe]
-verbose=1
-{% if concentrator %}
-ac-name={{concentrator}}
-{% endif %}
-{% if interface %}
-{% for int in interface %}
-interface={{int}}
-{% if interface[int]['vlans'] %}
-vlan-mon={{int}},{{interface[int]['vlans']|join(',')}}
-interface=re:{{int}}\.\d+
-{% endif %}
-{% endfor -%}
-{% endif -%}
-
-{% if svc_name %}
-service-name={{svc_name|join(',')}}
-{% endif -%}
-
-{% if pado_delay %}
-pado-delay={{pado_delay}}
-{% endif %}
-
-{% if limits %}
-[connlimit]
-limit={{limits['conn-limit']}}
-burst={{limits['burst']}}
-timeout={{limits['timeout']}}
-{% endif %}
-
-[cli]
-tcp=127.0.0.1:2001
-'''
-
-# pppoe chap secrets
-chap_secrets_conf = '''
-# username server password acceptable local IP addresses shaper
-{% for user in authentication['local-users'] %}
-{% if authentication['local-users'][user]['state'] == 'enabled' %}
-{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %}
-{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\
-{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}}
-{% else %}
-{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}
-{% endif %}
-{% endif %}
-{% endfor %}
-'''
+
#
# depending on hw and threads, daemon needs a little to start
# if it takes longer than 100 * 0.5 secs, exception is being raised
# not sure if that's the best way to check it, but it worked so far quite well
#
-
-
def _chk_con():
cnt = 0
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s = socket(AF_INET, SOCK_STREAM)
while True:
try:
s.connect(("127.0.0.1", 2001))
break
except ConnectionRefusedError:
- time.sleep(0.5)
+ sleep(0.5)
cnt += 1
if cnt == 100:
raise("failed to start pppoe server")
-def _write_chap_secrets(c):
- tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True)
- chap_secrets_txt = tmpl.render(c)
- old_umask = os.umask(0o077)
- open(chap_secrets, 'w').write(chap_secrets_txt)
- os.umask(old_umask)
- sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written')
-
-
-def _accel_cmd(cmd=''):
- if not cmd:
- return None
- try:
- ret = subprocess.check_output(
- ['/usr/bin/accel-cmd', cmd]).decode().strip()
- return ret
- except:
- return 1
+def _accel_cmd(command):
+ return run(f'/usr/bin/accel-cmd {command}')
def get_config():
@@ -640,6 +376,11 @@ def generate(c):
if c == None:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pppoe-server')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
# accel-cmd reload doesn't work so any change results in a restart of the
# daemon
try:
@@ -653,12 +394,18 @@ def generate(c):
else:
c['thread_cnt'] = int(os.cpu_count() / 2)
- tmpl = jinja2.Template(pppoe_config, trim_blocks=True)
+ tmpl = env.get_template('pppoe.config.tmpl')
config_text = tmpl.render(c)
- open(pppoe_conf, 'w').write(config_text)
+ with open(pppoe_conf, 'w') as f:
+ f.write(config_text)
if c['authentication']['local-users']:
- _write_chap_secrets(c)
+ tmpl = env.get_template('chap-secrets.tmpl')
+ chap_secrets_txt = tmpl.render(c)
+ old_umask = os.umask(0o077)
+ with open(chap_secrets, 'w') as f:
+ f.write(chap_secrets_txt)
+ os.umask(old_umask)
return c
@@ -672,15 +419,13 @@ def apply(c):
return None
if not os.path.exists(pidfile):
- ret = subprocess.call(
- ['/usr/sbin/accel-pppd', '-c', pppoe_conf, '-p', pidfile, '-d'])
+ ret = run(f'/usr/sbin/accel-pppd -c {pppoe_conf} -p {pidfile} -d')
_chk_con()
if ret != 0 and os.path.exists(pidfile):
os.remove(pidfile)
raise ConfigError('accel-pppd failed to start')
else:
_accel_cmd('restart')
- sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart")
if __name__ == '__main__':
@@ -691,4 +436,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/service-router-advert.py b/src/conf_mode/service-router-advert.py
new file mode 100755
index 000000000..75a324260
--- /dev/null
+++ b/src/conf_mode/service-router-advert.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2019 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 os
+
+from jinja2 import FileSystemLoader, Environment
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from sys import exit
+
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos import ConfigError
+from vyos.util import call
+
+
+config_file = r'/etc/radvd.conf'
+
+default_config_data = {
+ 'interfaces': []
+}
+
+def get_config():
+ rtradv = default_config_data
+ conf = Config()
+ base_level = ['service', 'router-advert']
+
+ if not conf.exists(base_level):
+ return rtradv
+
+ for interface in conf.list_nodes(base_level + ['interface']):
+ intf = {
+ 'name': interface,
+ 'hop_limit' : '64',
+ 'default_lifetime': '',
+ 'default_preference': 'medium',
+ 'dnssl': [],
+ 'link_mtu': '',
+ 'managed_flag': 'off',
+ 'interval_max': '600',
+ 'interval_min': '',
+ 'name_server': [],
+ 'other_config_flag': 'off',
+ 'prefixes' : [],
+ 'reachable_time': '0',
+ 'retrans_timer': '0',
+ 'send_advert': 'on'
+ }
+
+ # set config level first to reduce boilerplate code
+ conf.set_level(base_level + ['interface', interface])
+
+ if conf.exists(['hop-limit']):
+ intf['hop_limit'] = conf.return_value(['hop-limit'])
+
+ if conf.exists(['default-lifetim']):
+ intf['default_lifetime'] = conf.return_value(['default-lifetim'])
+
+ if conf.exists(['default-preference']):
+ intf['default_preference'] = conf.return_value(['default-preference'])
+
+ if conf.exists(['dnssl']):
+ intf['dnssl'] = conf.return_values(['dnssl'])
+
+ if conf.exists(['link-mtu']):
+ intf['link_mtu'] = conf.return_value(['link-mtu'])
+
+ if conf.exists(['managed-flag']):
+ intf['managed_flag'] = 'on'
+
+ if conf.exists(['interval', 'max']):
+ intf['interval_max'] = conf.return_value(['interval', 'max'])
+
+ if conf.exists(['interval', 'min']):
+ intf['interval_min'] = conf.return_value(['interval', 'min'])
+
+ if conf.exists(['name-server']):
+ intf['name_server'] = conf.return_values(['name-server'])
+
+ if conf.exists(['other-config-flag']):
+ intf['other_config_flag'] = 'on'
+
+ if conf.exists(['reachable-time']):
+ intf['reachable_time'] = conf.return_value(['reachable-time'])
+
+ if conf.exists(['retrans-timer']):
+ intf['retrans_timer'] = conf.return_value(['retrans-timer'])
+
+ if conf.exists(['no-send-advert']):
+ intf['send_advert'] = 'off'
+
+ for prefix in conf.list_nodes(['prefix']):
+ tmp = {
+ 'prefix' : prefix,
+ 'autonomous_flag' : 'on',
+ 'on_link' : 'on',
+ 'preferred_lifetime': '14400',
+ 'valid_lifetime' : '2592000'
+
+ }
+
+ # set config level first to reduce boilerplate code
+ conf.set_level(base_level + ['interface', interface, 'prefix', prefix])
+
+ if conf.exists(['no-autonomous-flag']):
+ tmp['autonomous_flag'] = 'off'
+
+ if conf.exists(['no-on-link-flag']):
+ tmp['on_link'] = 'off'
+
+ if conf.exists(['preferred-lifetime']):
+ tmp['preferred_lifetime'] = conf.return_value(['preferred-lifetime'])
+
+ if conf.exists(['valid-lifetime']):
+ tmp['valid_lifetime'] = conf.return_value(['valid-lifetime'])
+
+ intf['prefixes'].append(tmp)
+
+ rtradv['interfaces'].append(intf)
+
+ return rtradv
+
+def verify(rtradv):
+ return None
+
+def generate(rtradv):
+ if not rtradv['interfaces']:
+ return None
+
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'router-advert')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
+ tmpl = env.get_template('radvd.conf.tmpl')
+ config_text = tmpl.render(rtradv)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ # adjust file permissions of new configuration file
+ if os.path.exists(config_file):
+ os.chmod(config_file, S_IRUSR | S_IWUSR | S_IRGRP)
+
+ return None
+
+def apply(rtradv):
+ if not rtradv['interfaces']:
+ # bail out early - looks like removal from running config
+ call('systemctl stop radvd.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+
+ return None
+
+ call('systemctl restart radvd.service')
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index ac94afb1a..4a69e8742 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -15,18 +15,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import jinja2
-
-import vyos.version
-import vyos.validate
from binascii import hexlify
from time import sleep
from stat import S_IRWXU, S_IXGRP, S_IXOTH, S_IROTH, S_IRGRP
from sys import exit
+from jinja2 import FileSystemLoader, Environment
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.validate import is_ipv4, is_addr_assigned
+from vyos.version import get_version_data
from vyos import ConfigError
+from vyos.util import call
+
config_file_client = r'/etc/snmp/snmp.conf'
config_file_daemon = r'/etc/snmp/snmpd.conf'
@@ -43,171 +45,10 @@ OIDs = {
'none': '.1.3.6.1.6.3.10.1.2.1'
}
-# SNMP template (/etc/snmp/snmp.conf) - be careful if you edit the template.
-client_config_tmpl = """
-### Autogenerated by snmp.py ###
-{% if trap_source -%}
-clientaddr {{ trap_source }}
-{% endif %}
-
-"""
-
-# SNMP template (/usr/share/snmp/snmpd.conf) - be careful if you edit the template.
-access_config_tmpl = """
-### Autogenerated by snmp.py ###
-{%- for u in v3_users %}
-{{ u.mode }}user {{ u.name }}
-{%- endfor %}
-
-rwuser {{ vyos_user }}
-
-"""
-
-# SNMP template (/var/lib/snmp/snmpd.conf) - be careful if you edit the template.
-user_config_tmpl = """
-### Autogenerated by snmp.py ###
-# user
-{%- for u in v3_users %}
-{%- if u.authOID == 'none' %}
-createUser {{ u.name }}
-{%- elif u.authPassword %}
-createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }}
-{%- else %}
-usmUser 1 3 {{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x
-{%- endif %}
-{%- endfor %}
-
-createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES
-{%- if v3_engineid %}
-oldEngineID {{ v3_engineid }}
-{%- endif %}
-"""
-
-# SNMP template (/etc/snmp/snmpd.conf) - be careful if you edit the template.
-daemon_config_tmpl = """
-### Autogenerated by snmp.py ###
-
-# non configurable defaults
-sysObjectID 1.3.6.1.4.1.44641
-sysServices 14
-master agentx
-agentXPerms 0777 0777
-pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias
-smuxpeer .1.3.6.1.2.1.83
-smuxpeer .1.3.6.1.2.1.157
-smuxsocket localhost
-
-# linkUp/Down configure the Event MIB tables to monitor
-# the ifTable for network interfaces being taken up or down
-# for making internal queries to retrieve any necessary information
-iquerySecName {{ vyos_user }}
-
-# Modified from the default linkUpDownNotification
-# to include more OIDs and poll more frequently
-notificationEvent linkUpTrap linkUp ifIndex ifDescr ifType ifAdminStatus ifOperStatus
-notificationEvent linkDownTrap linkDown ifIndex ifDescr ifType ifAdminStatus ifOperStatus
-monitor -r 10 -e linkUpTrap "Generate linkUp" ifOperStatus != 2
-monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2
-
-########################
-# configurable section #
-########################
-
-# Default system description is VyOS version
-sysDescr VyOS {{ version }}
-
-{% if description %}
-# Description
-SysDescr {{ description }}
-{%- endif %}
-
-# Listen
-agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161,udp6:161{% endif %}
-
-# SNMP communities
-{%- for c in communities %}
-
-{%- if c.network_v4 %}
-{%- for network in c.network_v4 %}
-{{ c.authorization }}community {{ c.name }} {{ network }}
-{%- endfor %}
-{%- elif not c.has_source %}
-{{ c.authorization }}community {{ c.name }}
-{%- endif %}
-
-{%- if c.network_v6 %}
-{%- for network in c.network_v6 %}
-{{ c.authorization }}community6 {{ c.name }} {{ network }}
-{%- endfor %}
-{%- elif not c.has_source %}
-{{ c.authorization }}community6 {{ c.name }}
-{%- endif %}
-
-{%- endfor %}
-
-{% if contact %}
-# system contact information
-SysContact {{ contact }}
-{%- endif %}
-
-{% if location %}
-# system location information
-SysLocation {{ location }}
-{%- endif %}
-
-{% if smux_peers -%}
-# additional smux peers
-{%- for sp in smux_peers %}
-smuxpeer {{ sp }}
-{%- endfor %}
-{%- endif %}
-
-{% if trap_targets -%}
-# if there is a problem - tell someone!
-{%- for t in trap_targets %}
-trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }}
-{%- endfor %}
-{%- endif %}
-
-{%- if v3_enabled %}
-#
-# SNMPv3 stuff goes here
-#
-# views
-{%- for v in v3_views %}
-{%- for oid in v.oids %}
-view {{ v.name }} included .{{ oid.oid }}
-{%- endfor %}
-{%- endfor %}
-
-# access
-# context sec.model sec.level match read write notif
-{%- for g in v3_groups %}
-access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none
-{%- endfor %}
-
-# trap-target
-{%- for t in v3_traps %}
-trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ v3_engineid }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }}
-{%- endfor %}
-
-# group
-{%- for u in v3_users %}
-group {{ u.group }} usm {{ u.name }}
-{% endfor %}
-{%- endif %}
-
-{% if script_ext %}
-# extension scripts
-{%- for ext in script_ext|sort(attribute='name') %}
-extend {{ ext.name }} {{ ext.script }}
-{%- endfor %}
-{% endif %}
-"""
-
default_config_data = {
'listen_on': [],
'listen_address': [],
+ 'ipv6_enabled': 'True',
'communities': [],
'smux_peers': [],
'location' : '',
@@ -237,9 +78,12 @@ def get_config():
if not conf.exists('service snmp'):
return None
else:
+ if conf.exists('system ipv6 disable'):
+ snmp['ipv6_enabled'] = False
+
conf.set_level('service snmp')
- version_data = vyos.version.get_version_data()
+ version_data = get_version_data()
snmp['version'] = version_data['version']
# create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx'
@@ -263,7 +107,7 @@ def get_config():
# Subnet of SNMP client(s) allowed to contact system
if conf.exists('community {0} network'.format(name)):
for addr in conf.return_values('community {0} network'.format(name)):
- if vyos.validate.is_ipv4(addr):
+ if is_ipv4(addr):
community['network_v4'].append(addr)
else:
community['network_v6'].append(addr)
@@ -271,7 +115,7 @@ def get_config():
# IP address of SNMP client allowed to contact system
if conf.exists('community {0} client'.format(name)):
for addr in conf.return_values('community {0} client'.format(name)):
- if vyos.validate.is_ipv4(addr):
+ if is_ipv4(addr):
community['network_v4'].append(addr)
else:
community['network_v6'].append(addr)
@@ -554,16 +398,16 @@ def verify(snmp):
addr = listen[0]
port = listen[1]
- if vyos.validate.is_ipv4(addr):
+ if is_ipv4(addr):
# example: udp:127.0.0.1:161
listen = 'udp:' + addr + ':' + port
- else:
+ elif snmp['ipv6_enabled']:
# example: udp6:[::1]:161
listen = 'udp6:' + '[' + addr + ']' + ':' + port
# We only wan't to configure addresses that exist on the system.
# Hint the user if they don't exist
- if vyos.validate.is_addr_assigned(addr):
+ if is_addr_assigned(addr):
snmp['listen_on'].append(listen)
else:
print('WARNING: SNMP listen address {0} not configured!'.format(addr))
@@ -665,35 +509,40 @@ def generate(snmp):
#
# As we are manipulating the snmpd user database we have to stop it first!
# This is even save if service is going to be removed
- os.system("systemctl stop snmpd.service")
- rmfile(config_file_client)
- rmfile(config_file_daemon)
- rmfile(config_file_access)
- rmfile(config_file_user)
+ call('systemctl stop snmpd.service')
+ config_files = [config_file_client, config_file_daemon, config_file_access,
+ config_file_user]
+ for file in config_files:
+ rmfile(file)
if snmp is None:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'snmp')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
# Write client config file
- tmpl = jinja2.Template(client_config_tmpl)
+ tmpl = env.get_template('etc.snmp.conf.tmpl')
config_text = tmpl.render(snmp)
with open(config_file_client, 'w') as f:
f.write(config_text)
# Write server config file
- tmpl = jinja2.Template(daemon_config_tmpl)
+ tmpl = env.get_template('etc.snmpd.conf.tmpl')
config_text = tmpl.render(snmp)
with open(config_file_daemon, 'w') as f:
f.write(config_text)
# Write access rights config file
- tmpl = jinja2.Template(access_config_tmpl)
+ tmpl = env.get_template('usr.snmpd.conf.tmpl')
config_text = tmpl.render(snmp)
with open(config_file_access, 'w') as f:
f.write(config_text)
# Write access rights config file
- tmpl = jinja2.Template(user_config_tmpl)
+ tmpl = env.get_template('var.snmpd.conf.tmpl')
config_text = tmpl.render(snmp)
with open(config_file_user, 'w') as f:
f.write(config_text)
@@ -705,7 +554,7 @@ def apply(snmp):
return None
# start SNMP daemon
- os.system("systemctl restart snmpd.service")
+ call("systemctl restart snmpd.service")
# Passwords are not available immediately in the configuration file,
# after daemon startup - we wait until they have been processed by
@@ -746,15 +595,15 @@ def apply(snmp):
# Now update the running configuration
#
- # Currently when executing os.system() the environment does not
+ # Currently when executing call() the environment does not
# have the vyos_libexec_dir variable set, see Phabricator T685.
- os.system('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw']))
- os.system('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw']))
- os.system('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user']))
- os.system('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user']))
+ call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw']))
+ call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw']))
+ call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user']))
+ call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user']))
# Enable AgentX in FRR
- os.system('vtysh -c "configure terminal" -c "agentx" >/dev/null')
+ call('vtysh -c "configure terminal" -c "agentx" >/dev/null')
return None
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index 9fe22bfee..a6cdb7ccc 100755
--- a/src/conf_mode/ssh.py
+++ b/src/conf_mode/ssh.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,148 +13,18 @@
#
# 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 sys
import os
-
-import jinja2
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
-config_file = r'/etc/ssh/sshd_config'
-# Please be careful if you edit the template.
-config_tmpl = """
-
-### Autogenerated by ssh.py ###
-
-# Non-configurable defaults
-Protocol 2
-HostKey /etc/ssh/ssh_host_rsa_key
-HostKey /etc/ssh/ssh_host_dsa_key
-HostKey /etc/ssh/ssh_host_ecdsa_key
-HostKey /etc/ssh/ssh_host_ed25519_key
-SyslogFacility AUTH
-LoginGraceTime 120
-StrictModes yes
-PubkeyAuthentication yes
-IgnoreRhosts yes
-HostbasedAuthentication no
-PermitEmptyPasswords no
-ChallengeResponseAuthentication no
-X11Forwarding yes
-X11DisplayOffset 10
-PrintMotd no
-PrintLastLog yes
-TCPKeepAlive yes
-Banner /etc/issue.net
-Subsystem sftp /usr/lib/openssh/sftp-server
-UsePAM yes
-HostKey /etc/ssh/ssh_host_rsa_key
-
-# Specifies whether sshd should look up the remote host name,
-# and to check that the resolved host name for the remote IP
-# address maps back to the very same IP address.
-UseDNS {{ host_validation }}
-
-# Specifies the port number that sshd listens on. The default is 22.
-# Multiple options of this type are permitted.
-{% if mport|length != 0 %}
-{% for p in mport %}
-Port {{ p }}
-{% endfor %}
-{% else %}
-Port {{ port }}
-{% endif %}
-
-# Gives the verbosity level that is used when logging messages from sshd
-LogLevel {{ log_level }}
-
-# Specifies whether root can log in using ssh
-PermitRootLogin no
-
-# Specifies whether password authentication is allowed
-PasswordAuthentication {{ password_authentication }}
-
-{% if listen_on %}
-# Specifies the local addresses sshd should listen on
-{% for a in listen_on %}
-ListenAddress {{ a }}
-{% endfor %}
-{{ "\n" }}
-{% endif %}
-
-{%- if ciphers %}
-# Specifies the ciphers allowed. Multiple ciphers must be comma-separated.
-#
-# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/
-Ciphers {{ ciphers | join(",") }}
-{{ "\n" }}
-{% endif %}
-
-{%- if mac %}
-# Specifies the available MAC (message authentication code) algorithms. The MAC
-# algorithm is used for data integrity protection. Multiple algorithms must be
-# comma-separated.
-#
-# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/
-MACs {{ mac | join(",") }}
-{{ "\n" }}
-{% endif %}
-
-{%- if key_exchange %}
-# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must
-# be comma-separated.
-#
-# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/
-KexAlgorithms {{ key_exchange | join(",") }}
-{{ "\n" }}
-{% endif %}
-
-{%- if allow_users %}
-# This keyword can be followed by a list of user name patterns, separated by spaces.
-# If specified, login is allowed only for user names that match one of the patterns.
-# Only user names are valid, a numerical user ID is not recognized.
-AllowUsers {{ allow_users | join(" ") }}
-{{ "\n" }}
-{% endif %}
-
-{%- if allow_groups %}
-# This keyword can be followed by a list of group name patterns, separated by spaces.
-# If specified, login is allowed only for users whose primary group or supplementary
-# group list matches one of the patterns. Only group names are valid, a numerical group
-# ID is not recognized.
-AllowGroups {{ allow_groups | join(" ") }}
-{{ "\n" }}
-{% endif %}
-
-{%- if deny_users %}
-# This keyword can be followed by a list of user name patterns, separated by spaces.
-# Login is disallowed for user names that match one of the patterns. Only user names
-# are valid, a numerical user ID is not recognized.
-DenyUsers {{ deny_users | join(" ") }}
-{{ "\n" }}
-{% endif %}
-
-{%- if deny_groups %}
-# This keyword can be followed by a list of group name patterns, separated by spaces.
-# Login is disallowed for users whose primary group or supplementary group list matches
-# one of the patterns. Only group names are valid, a numerical group ID is not recognized.
-DenyGroups {{ deny_groups | join(" ") }}
-{{ "\n" }}
-{% endif %}
-
-{%- if client_keepalive %}
-# Sets a timeout interval in seconds after which if no data has been received from the client,
-# sshd will send a message through the encrypted channel to request a response from the client.
-# The default is 0, indicating that these messages will not be sent to the client.
-# This option applies to protocol version 2 only.
-ClientAliveInterval {{ client_keepalive }}
-{% endif %}
-"""
+config_file = r'/etc/ssh/sshd_config'
default_config_data = {
'port' : '22',
@@ -250,7 +120,12 @@ def generate(ssh):
if ssh is None:
return None
- tmpl = jinja2.Template(config_tmpl, trim_blocks=True)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ssh')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
+ tmpl = env.get_template('sshd_config.tmpl')
config_text = tmpl.render(ssh)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -258,10 +133,10 @@ def generate(ssh):
def apply(ssh):
if ssh is not None and 'port' in ssh.keys():
- os.system("sudo systemctl restart ssh.service")
+ call("sudo systemctl restart ssh.service")
else:
# SSH access is removed in the commit
- os.system("sudo systemctl stop ssh.service")
+ call("sudo systemctl stop ssh.service")
if os.path.isfile(config_file):
os.unlink(config_file)
@@ -275,4 +150,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 335507411..8a1ac8411 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.py
@@ -20,6 +20,8 @@ from sys import exit
from copy import deepcopy
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import call
+
default_config_data = {
'arp_table': 8192,
@@ -29,7 +31,7 @@ default_config_data = {
}
def sysctl(name, value):
- os.system('sysctl -wq {}={}'.format(name, value))
+ call('sysctl -wq {}={}'.format(name, value))
def get_config():
ip_opt = deepcopy(default_config_data)
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index bd28ec357..04a063564 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -21,6 +21,8 @@ from sys import exit
from copy import deepcopy
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import call
+
ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf'
@@ -35,7 +37,7 @@ default_config_data = {
}
def sysctl(name, value):
- os.system('sysctl -wq {}={}'.format(name, value))
+ call('sysctl -wq {}={}'.format(name, value))
def get_config():
ip_opt = deepcopy(default_config_data)
diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py
index e66d409bb..5a34a0b06 100755
--- a/src/conf_mode/system-login-banner.py
+++ b/src/conf_mode/system-login-banner.py
@@ -16,6 +16,7 @@
from sys import exit
from vyos.config import Config
+from vyos import ConfigError
motd="""
The programs included with the Debian GNU/Linux system are free software;
@@ -24,6 +25,7 @@ individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
+
"""
PRELOGIN_FILE = r'/etc/issue'
@@ -31,8 +33,8 @@ PRELOGIN_NET_FILE = r'/etc/issue.net'
POSTLOGIN_FILE = r'/etc/motd'
default_config_data = {
- 'issue': 'Welcome to VyOS - \n \l',
- 'issue_net': 'Welcome to VyOS',
+ 'issue': 'Welcome to VyOS - \n \l\n',
+ 'issue_net': 'Welcome to VyOS\n',
'motd': motd
}
@@ -49,15 +51,29 @@ def get_config():
# Post-Login banner
if conf.exists(['post-login']):
tmp = conf.return_value(['post-login'])
- tmp = tmp.replace('\\n','\n')
- tmp = tmp.replace('\\t','\t')
+ # post-login banner can be empty as well
+ if tmp:
+ tmp = tmp.replace('\\n','\n')
+ tmp = tmp.replace('\\t','\t')
+ # always add newline character
+ tmp += '\n'
+ else:
+ tmp = ''
+
banner['motd'] = tmp
# Pre-Login banner
if conf.exists(['pre-login']):
tmp = conf.return_value(['pre-login'])
- tmp = tmp.replace('\\n','\n')
- tmp = tmp.replace('\\t','\t')
+ # pre-login banner can be empty as well
+ if tmp:
+ tmp = tmp.replace('\\n','\n')
+ tmp = tmp.replace('\\t','\t')
+ # always add newline character
+ tmp += '\n'
+ else:
+ tmp = ''
+
banner['issue'] = banner['issue_net'] = tmp
return banner
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 23152fee0..43732cfae 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -14,36 +14,22 @@
# 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 sys
import os
-import jinja2
+from jinja2 import FileSystemLoader, Environment
+from psutil import users
from pwd import getpwall, getpwnam
from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP
-from subprocess import Popen, PIPE, STDOUT
-from psutil import users
+from sys import exit
from vyos.config import Config
from vyos.configdict import list_diff
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import cmd
+from vyos.util import call
radius_config_file = "/etc/pam_radius_auth.conf"
-radius_config_tmpl = """
-# Automatically generated by VyOS
-# RADIUS configuration file
-{%- if radius_server %}
-# server[:port] shared_secret timeout (s) source_ip
-{% for s in radius_server %}
-{%- if not s.disabled -%}
-{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if radius_source_address -%}{{ radius_source_address }}{% endif %}
-{% endif %}
-{%- endfor %}
-
-priv-lvl 15
-mapped_priv_user radius_priv_user
-{% endif %}
-
-"""
default_config_data = {
'deleted': False,
@@ -67,10 +53,7 @@ def get_local_users():
def get_crypt_pw(password):
- command = '/usr/bin/mkpasswd --method=sha-512 {}'.format(password)
- p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
- tmp = p.communicate()[0].strip()
- return tmp.decode()
+ return cmd(f'/usr/bin/mkpasswd --method=sha-512 {password}')
def get_config():
@@ -196,6 +179,14 @@ def verify(login):
if cur_user in login['del_users']:
raise ConfigError('Attempting to delete current user: {}'.format(cur_user))
+ for user in login['add_users']:
+ for key in user['public_keys']:
+ if not key['type']:
+ raise ConfigError('SSH public key type missing for "{}"!'.format(key['name']))
+
+ if not key['key']:
+ raise ConfigError('SSH public key for id "{}" missing!'.format(key['name']))
+
# At lease one RADIUS server must not be disabled
if len(login['radius_server']) > 0:
fail = True
@@ -221,7 +212,12 @@ def generate(login):
os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}' >/dev/null".format(user['name'], user['password_encrypted']))
if len(login['radius_server']) > 0:
- tmpl = jinja2.Template(radius_config_tmpl)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'system-login')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('pam_radius_auth.conf.tmpl')
config_text = tmpl.render(login)
with open(radius_config_file, 'w') as f:
f.write(config_text)
@@ -240,40 +236,44 @@ def apply(login):
for user in login['add_users']:
# make new user using vyatta shell and make home directory (-m),
# default group of 100 (users)
- cmd = "useradd -m -N"
+ command = "useradd -m -N"
# check if user already exists:
if user['name'] in get_local_users():
# update existing account
- cmd = "usermod"
+ command = "usermod"
# we need to use '' quotes when passing formatted data to the shell
# else it will not work as some data parts are lost in translation
- cmd += " -p '{}'".format(user['password_encrypted'])
- cmd += " -s /bin/vbash"
+ command += " -p '{}'".format(user['password_encrypted'])
+ command += " -s /bin/vbash"
if user['full_name']:
- cmd += " -c '{}'".format(user['full_name'])
+ command += " -c '{}'".format(user['full_name'])
if user['home_dir']:
- cmd += " -d '{}'".format(user['home_dir'])
+ command += " -d '{}'".format(user['home_dir'])
- cmd += " -G frrvty,vyattacfg,sudo,adm,dip,disk"
- cmd += " {}".format(user['name'])
+ command += " -G frrvty,vyattacfg,sudo,adm,dip,disk"
+ command += " {}".format(user['name'])
try:
- os.system(cmd)
+ call(command)
uid = getpwnam(user['name']).pw_uid
gid = getpwnam(user['name']).pw_gid
+ # we should not rely on the home dir value stored in user['home_dir']
+ # as if a crazy user will choose username root or any other system
+ # user this will fail. should be deny using root at all?
+ home_dir = getpwnam(user['name']).pw_dir
# install ssh keys
- key_dir = '{}/.ssh'.format(user['home_dir'])
- if not os.path.isdir(key_dir):
- os.mkdir(key_dir)
- os.chown(key_dir, uid, gid)
- os.chmod(key_dir, S_IRWXU | S_IRGRP | S_IXGRP)
-
- key_file = key_dir + '/authorized_keys';
- with open(key_file, 'w') as f:
+ ssh_key_dir = home_dir + '/.ssh'
+ if not os.path.isdir(ssh_key_dir):
+ os.mkdir(ssh_key_dir)
+ os.chown(ssh_key_dir, uid, gid)
+ os.chmod(ssh_key_dir, S_IRWXU | S_IRGRP | S_IXGRP)
+
+ ssh_key_file = ssh_key_dir + '/authorized_keys';
+ with open(ssh_key_file, 'w') as f:
f.write("# Automatically generated by VyOS\n")
f.write("# Do not edit, all changes will be lost\n")
@@ -285,8 +285,8 @@ def apply(login):
line += '{} {} {}\n'.format(id['type'], id['key'], id['name'])
f.write(line)
- os.chown(key_file, uid, gid)
- os.chmod(key_file, S_IRUSR | S_IWUSR)
+ os.chown(ssh_key_file, uid, gid)
+ os.chmod(ssh_key_file, S_IRUSR | S_IWUSR)
except Exception as e:
raise ConfigError('Adding user "{}" raised an exception: {}'.format(user['name'], e))
@@ -296,10 +296,10 @@ def apply(login):
# Logout user if he is logged in
if user in list(set([tmp[0] for tmp in users()])):
print('{} is logged in, forcing logout'.format(user))
- os.system('pkill -HUP -u {}'.format(user))
+ call('pkill -HUP -u {}'.format(user))
# Remove user account but leave home directory to be safe
- os.system('userdel -r {} 2>/dev/null'.format(user))
+ call('userdel -r {} 2>/dev/null'.format(user))
except Exception as e:
raise ConfigError('Deleting user "{}" raised an exception: {}'.format(user, e))
@@ -313,7 +313,7 @@ def apply(login):
os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius")
# Make NSS system aware of RADIUS, too
- cmd = "sed -i -e \'/\smapname/b\' \
+ command = "sed -i -e \'/\smapname/b\' \
-e \'/^passwd:/s/\s\s*/&mapuid /\' \
-e \'/^passwd:.*#/s/#.*/mapname &/\' \
-e \'/^passwd:[^#]*$/s/$/ mapname &/\' \
@@ -321,7 +321,7 @@ def apply(login):
-e \'/^group:[^#]*$/s/: */&mapname /\' \
/etc/nsswitch.conf"
- os.system(cmd)
+ call(command)
except Exception as e:
raise ConfigError('RADIUS configuration failed: {}'.format(e))
@@ -331,13 +331,13 @@ def apply(login):
# Disable RADIUS in PAM
os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius")
- cmd = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \
+ command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \
-e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \
-e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \
-e \'s/[ \t]*$//\' \
/etc/nsswitch.conf"
- os.system(cmd)
+ call(command)
except Exception as e:
raise ConfigError('Removing RADIUS configuration failed'.format(e))
@@ -352,4 +352,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/system-options.py b/src/conf_mode/system-options.py
index a893e98b3..b3dbc82fb 100755
--- a/src/conf_mode/system-options.py
+++ b/src/conf_mode/system-options.py
@@ -20,6 +20,7 @@ from sys import exit
from copy import deepcopy
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import run
systemd_ctrl_alt_del = '/lib/systemd/system/ctrl-alt-del.target'
@@ -51,9 +52,9 @@ def generate(opt):
def apply(opt):
# Beep action
if opt['beep_if_fully_booted']:
- os.system('systemctl enable vyos-beep.service >/dev/null 2>&1')
+ run('systemctl enable vyos-beep.service')
else:
- os.system('systemctl disable vyos-beep.service >/dev/null 2>&1')
+ run('systemctl disable vyos-beep.service')
# Ctrl-Alt-Delete action
if opt['ctrl_alt_del'] == 'ignore':
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
index 2d47cc061..25b9b5bed 100755
--- a/src/conf_mode/system-syslog.py
+++ b/src/conf_mode/system-syslog.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,84 +13,17 @@
#
# 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 sys
import os
import re
-import subprocess
-import jinja2
+
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
-
-# config templates
-
-# /etc/rsyslog.d/vyos-rsyslog.conf ###
-configs = '''
-## generated by syslog.py ##
-## file based logging
-{% if files['global']['marker'] -%}
-$ModLoad immark
-{% if files['global']['marker-interval'] %}
-$MarkMessagePeriod {{files['global']['marker-interval']}}
-{% endif %}
-{% endif -%}
-{% if files['global']['preserver_fqdn'] -%}
-$PreserveFQDN on
-{% endif -%}
-{% for file in files %}
-$outchannel {{file}},{{files[file]['log-file']}},{{files[file]['max-size']}},{{files[file]['action-on-max-size']}}
-{{files[file]['selectors']}} :omfile:${{file}}
-{% endfor %}
-{% if console %}
-## console logging
-{% for con in console %}
-{{console[con]['selectors']}} /dev/console
-{% endfor %}
-{% endif %}
-{% if hosts %}
-## remote logging
-{% for host in hosts %}
-{% if hosts[host]['proto'] == 'tcp' %}
-{% if hosts[host]['port'] %}
-{{hosts[host]['selectors']}} @@{{host}}:{{hosts[host]['port']}}
-{% else %}
-{{hosts[host]['selectors']}} @@{{host}}
-{% endif %}
-{% else %}
-{% if hosts[host]['port'] %}
-{{hosts[host]['selectors']}} @{{host}}:{{hosts[host]['port']}}
-{% else %}
-{{hosts[host]['selectors']}} @{{host}}
-{% endif %}
-{% endif %}
-{% endfor %}
-{% endif %}
-{% if user %}
-{% for u in user %}
-{{user[u]['selectors']}} :omusrmsg:{{u}}
-{% endfor %}
-{% endif %}
-'''
-
-logrotate_configs = '''
-{% for file in files %}
-{{files[file]['log-file']}} {
- missingok
- notifempty
- create
- rotate {{files[file]['max-files']}}
- size={{files[file]['max-size']//1024}}k
- postrotate
- invoke-rc.d rsyslog rotate > /dev/null
- endscript
-}
-{% endfor %}
-'''
-# config templates end
-
+from vyos.util import run
def get_config():
c = Config()
@@ -259,14 +192,19 @@ def generate(c):
if c == None:
return None
- tmpl = jinja2.Template(configs, trim_blocks=True)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'syslog')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
+ tmpl = env.get_template('rsyslog.conf.tmpl')
config_text = tmpl.render(c)
with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f:
f.write(config_text)
# eventually write for each file its own logrotate file, since size is
# defined it shouldn't matter
- tmpl = jinja2.Template(logrotate_configs, trim_blocks=True)
+ tmpl = env.get_template('logrotate.tmpl')
config_text = tmpl.render(c)
with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f:
f.write(config_text)
@@ -315,10 +253,8 @@ def verify(c):
def apply(c):
if not c:
- subprocess.call(['sudo', 'systemctl', 'stop', 'syslog'])
- return 0
-
- subprocess.call(['sudo', 'systemctl', 'restart', 'syslog'])
+ return run('systemctl stop syslog')
+ return run('systemctl restart syslog')
if __name__ == '__main__':
try:
@@ -328,4 +264,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/system-timezone.py b/src/conf_mode/system-timezone.py
index d715bd27e..25b949a79 100755
--- a/src/conf_mode/system-timezone.py
+++ b/src/conf_mode/system-timezone.py
@@ -20,6 +20,8 @@ import os
from copy import deepcopy
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import call
+
default_config_data = {
'name': 'UTC'
@@ -40,9 +42,7 @@ def generate(tz):
pass
def apply(tz):
- cmd = '/usr/bin/timedatectl set-timezone {}'.format(tz['name'])
- os.system(cmd)
- pass
+ call('/usr/bin/timedatectl set-timezone {}'.format(tz['name']))
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py
index 01dc92a20..943c42274 100755
--- a/src/conf_mode/system-wifi-regdom.py
+++ b/src/conf_mode/system-wifi-regdom.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 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
@@ -15,52 +15,34 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import jinja2
from copy import deepcopy
from sys import exit
+from jinja2 import FileSystemLoader, Environment
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
config_80211_file='/etc/modprobe.d/cfg80211.conf'
config_crda_file='/etc/default/crda'
-# Please be careful if you edit the template.
-config_80211_tmpl = """
-{%- if regdom -%}
-options cfg80211 ieee80211_regdom={{ regdom }}
-{% endif %}
-"""
-
-# Please be careful if you edit the template.
-config_crda_tmpl = """
-{%- if regdom -%}
-REGDOMAIN={{ regdom }}
-{% endif %}
-"""
-
default_config_data = {
'regdom' : '',
'deleted' : False
}
-
def get_config():
regdom = deepcopy(default_config_data)
conf = Config()
-
- # set new configuration level
- conf.set_level('system')
+ base = ['system', 'wifi-regulatory-domain']
# Check if interface has been removed
- if not conf.exists('wifi-regulatory-domain'):
+ if not conf.exists(base):
regdom['deleted'] = True
return regdom
-
- # retrieve configured regulatory domain
- if conf.exists('wifi-regulatory-domain'):
- regdom['regdom'] = conf.return_value('wifi-regulatory-domain')
+ else:
+ regdom['regdom'] = conf.return_value(base)
return regdom
@@ -85,12 +67,17 @@ def generate(regdom):
return None
- tmpl = jinja2.Template(config_80211_tmpl)
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'wifi')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('cfg80211.conf.tmpl')
config_text = tmpl.render(regdom)
with open(config_80211_file, 'w') as f:
f.write(config_text)
- tmpl = jinja2.Template(config_crda_tmpl)
+ tmpl = env.get_template('crda.tmpl')
config_text = tmpl.render(regdom)
with open(config_crda_file, 'w') as f:
f.write(config_text)
diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py
index ff7cad0c9..7a7246783 100755
--- a/src/conf_mode/tftp_server.py
+++ b/src/conf_mode/tftp_server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,31 +13,24 @@
#
# 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 sys
import os
import stat
import pwd
-import copy
-import glob
-import jinja2
-import vyos.validate
+from copy import deepcopy
+from glob import glob
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.validate import is_ipv4, is_addr_assigned
from vyos import ConfigError
+from vyos.util import call
-config_file = r'/etc/default/tftpd'
-
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by tftp_server.py ###
-DAEMON_ARGS="--listen --user tftp --address {% for a in listen-%}{{ a }}{% endfor %}{% if allow_upload %} --create --umask 000{% endif %} --secure {{ directory }}"
-
-"""
+config_file = r'/etc/default/tftpd'
default_config_data = {
'directory': '',
@@ -47,23 +40,25 @@ default_config_data = {
}
def get_config():
- tftpd = copy.deepcopy(default_config_data)
+ tftpd = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('service tftp-server'):
+ base = ['service', 'tftp-server']
+ if not conf.exists(base):
return None
else:
- conf.set_level('service tftp-server')
+ conf.set_level(base)
- if conf.exists('directory'):
- tftpd['directory'] = conf.return_value('directory')
+ if conf.exists(['directory']):
+ tftpd['directory'] = conf.return_value(['directory'])
- if conf.exists('allow-upload'):
+ if conf.exists(['allow-upload']):
tftpd['allow_upload'] = True
- if conf.exists('port'):
- tftpd['port'] = conf.return_value('port')
+ if conf.exists(['port']):
+ tftpd['port'] = conf.return_value(['port'])
- tftpd['listen'] = conf.return_values('listen-address')
+ if conf.exists(['listen-address']):
+ tftpd['listen'] = conf.return_values(['listen-address'])
return tftpd
@@ -80,7 +75,7 @@ def verify(tftpd):
raise ConfigError('TFTP server listen address must be configured!')
for addr in tftpd['listen']:
- if not vyos.validate.is_addr_assigned(addr):
+ if not is_addr_assigned(addr):
print('WARNING: TFTP server listen address {0} not assigned to any interface!'.format(addr))
return None
@@ -88,22 +83,27 @@ def verify(tftpd):
def generate(tftpd):
# cleanup any available configuration file
# files will be recreated on demand
- for i in glob.glob(config_file + '*'):
+ for i in glob(config_file + '*'):
os.unlink(i)
# bail out early - looks like removal from running config
if tftpd is None:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'tftp-server')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
idx = 0
for listen in tftpd['listen']:
- config = copy.deepcopy(tftpd)
- if vyos.validate.is_ipv4(listen):
+ config = deepcopy(tftpd)
+ if is_ipv4(listen):
config['listen'] = [listen + ":" + tftpd['port'] + " -4"]
else:
config['listen'] = ["[" + listen + "]" + tftpd['port'] + " -6"]
- tmpl = jinja2.Template(config_tmpl)
+ tmpl = env.get_template('default.tmpl')
config_text = tmpl.render(config)
file = config_file + str(idx)
with open(file, 'w') as f:
@@ -115,7 +115,7 @@ def generate(tftpd):
def apply(tftpd):
# stop all services first - then we will decide
- os.system('sudo systemctl stop tftpd@{0..20}')
+ call('systemctl stop tftpd@{0..20}')
# bail out early - e.g. service deletion
if tftpd is None:
@@ -140,7 +140,7 @@ def apply(tftpd):
idx = 0
for listen in tftpd['listen']:
- os.system('sudo systemctl restart tftpd@{0}.service'.format(idx))
+ call('systemctl restart tftpd@{0}.service'.format(idx))
idx = idx + 1
return None
@@ -153,4 +153,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/vpn-pptp.py b/src/conf_mode/vpn-pptp.py
index 355adf715..45b2c4b40 100755
--- a/src/conf_mode/vpn-pptp.py
+++ b/src/conf_mode/vpn-pptp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,20 +13,19 @@
#
# 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 sys
import os
import re
-import subprocess
-import jinja2
-import socket
-import time
-import syslog as sl
+
+from jinja2 import FileSystemLoader, Environment
+from socket import socket, AF_INET, SOCK_STREAM
+from sys import exit
+from time import sleep
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import run
pidfile = r'/var/run/accel_pptp.pid'
pptp_cnf_dir = r'/etc/accel-ppp/pptp'
@@ -36,143 +35,24 @@ pptp_conf = pptp_cnf_dir + '/pptp.config'
# config path creation
if not os.path.exists(pptp_cnf_dir):
os.makedirs(pptp_cnf_dir)
- sl.syslog(sl.LOG_NOTICE, pptp_cnf_dir + " created")
-
-pptp_config = '''
-### generated by accel_pptp.py ###
-[modules]
-log_syslog
-pptp
-ippool
-chap-secrets
-{% if authentication['auth_proto'] %}
-{{ authentication['auth_proto'] }}
-{% else %}
-auth_mschap_v2
-{% endif %}
-{% if authentication['mode'] == 'radius' %}
-radius
-{% endif -%}
-
-[core]
-thread-count={{thread_cnt}}
-
-[log]
-syslog=accel-pptp,daemon
-copy=1
-level=5
-
-{% if dns %}
-[dns]
-{% if dns[0] %}
-dns1={{dns[0]}}
-{% endif %}
-{% if dns[1] %}
-dns2={{dns[1]}}
-{% endif %}
-{% endif %}
-
-{% if wins %}
-[wins]
-{% if wins[0] %}
-wins1={{wins[0]}}
-{% endif %}
-{% if wins[1] %}
-wins2={{wins[1]}}
-{% endif %}
-{% endif %}
-
-[pptp]
-ifname=pptp%d
-{% if outside_addr %}
-bind={{outside_addr}}
-{% endif %}
-verbose=1
-ppp-max-mtu={{mtu}}
-mppe={{authentication['mppe']}}
-echo-interval=10
-echo-failure=3
-
-
-[client-ip-range]
-0.0.0.0/0
-
-[ip-pool]
-tunnel={{client_ip_pool}}
-gw-ip-address={{gw_ip}}
-
-{% if authentication['mode'] == 'local' %}
-[chap-secrets]
-chap-secrets=/etc/accel-ppp/pptp/chap-secrets
-{% endif %}
-
-[ppp]
-verbose=5
-check-ip=1
-single-session=replace
-
-{% if authentication['mode'] == 'radius' %}
-[radius]
-{% for rsrv in authentication['radiussrv']: %}
-server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\
-req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\
-fail-time={{authentication['radiussrv'][rsrv]['fail-time']}}
-{% endfor %}
-timeout=30
-acct-timeout=30
-max-try=3
-{%endif %}
-
-[cli]
-tcp=127.0.0.1:2003
-'''
-
-# pptp chap secrets
-chap_secrets_conf = '''
-# username server password acceptable local IP addresses
-{% for user in authentication['local-users'] %}
-{% if authentication['local-users'][user]['state'] == 'enabled' %}
-{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}
-{% endif %}
-{% endfor %}
-'''
-
def _chk_con():
cnt = 0
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s = socket(AF_INET, SOCK_STREAM)
while True:
try:
s.connect(("127.0.0.1", 2003))
break
except ConnectionRefusedError:
- time.sleep(0.5)
+ sleep(0.5)
cnt += 1
if cnt == 100:
raise("failed to start pptp server")
break
-# chap_secrets file if auth mode local
-
-
-def _write_chap_secrets(c):
- tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True)
- chap_secrets_txt = tmpl.render(c)
- old_umask = os.umask(0o077)
- open(chap_secrets, 'w').write(chap_secrets_txt)
- os.umask(old_umask)
- sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written')
-
-def _accel_cmd(cmd=''):
- if not cmd:
- return None
- try:
- ret = subprocess.check_output(
- ['/usr/bin/accel-cmd', '-p', '2003', cmd]).decode().strip()
- return ret
- except:
- return 1
+def _accel_cmd(command):
+ return run('/usr/bin/accel-cmd -p 2003 {command}')
###
# inline helper functions end
@@ -326,6 +206,11 @@ def generate(c):
if c == None:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pptp')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
# accel-cmd reload doesn't work so any change results in a restart of the daemon
try:
if os.cpu_count() == 1:
@@ -338,12 +223,18 @@ def generate(c):
else:
c['thread_cnt'] = int(os.cpu_count()/2)
- tmpl = jinja2.Template(pptp_config, trim_blocks=True)
+ tmpl = env.get_template('pptp.config.tmpl')
config_text = tmpl.render(c)
- open(pptp_conf, 'w').write(config_text)
+ with open(pptp_conf, 'w') as f:
+ f.write(config_text)
if c['authentication']['local-users']:
- _write_chap_secrets(c)
+ tmpl = env.get_template('chap-secrets.tmpl')
+ chap_secrets_txt = tmpl.render(c)
+ old_umask = os.umask(0o077)
+ with open(chap_secrets, 'w') as f:
+ f.write(chap_secrets_txt)
+ os.umask(old_umask)
return c
@@ -357,8 +248,7 @@ def apply(c):
return None
if not os.path.exists(pidfile):
- ret = subprocess.call(
- ['/usr/sbin/accel-pppd', '-c', pptp_conf, '-p', pidfile, '-d'])
+ ret = run(f'/usr/sbin/accel-pppd -c {pptp_conf} -p {pidfile} -d')
_chk_con()
if ret != 0 and os.path.exists(pidfile):
os.remove(pidfile)
@@ -366,8 +256,6 @@ def apply(c):
else:
# if gw ip changes, only restart doesn't work
_accel_cmd('restart')
- sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart")
-
if __name__ == '__main__':
try:
@@ -377,4 +265,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
new file mode 100755
index 000000000..ca0844c50
--- /dev/null
+++ b/src/conf_mode/vpn_sstp.py
@@ -0,0 +1,402 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 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 os
+
+from time import sleep
+from sys import exit
+from socket import socket, AF_INET, SOCK_STREAM
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from jinja2 import FileSystemLoader, Environment
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.defaults import directories as vyos_data_dir
+from vyos.util import process_running
+from vyos.util import process_running, cmd, run
+
+pidfile = r'/var/run/accel_sstp.pid'
+sstp_cnf_dir = r'/etc/accel-ppp/sstp'
+chap_secrets = sstp_cnf_dir + '/chap-secrets'
+sstp_conf = sstp_cnf_dir + '/sstp.config'
+
+# config path creation
+if not os.path.exists(sstp_cnf_dir):
+ os.makedirs(sstp_cnf_dir)
+
+def chk_con():
+ cnt = 0
+ s = socket(AF_INET, SOCK_STREAM)
+ while True:
+ try:
+ s.connect(("127.0.0.1", 2005))
+ s.close()
+ break
+ except ConnectionRefusedError:
+ sleep(0.5)
+ cnt += 1
+ if cnt == 100:
+ raise("failed to start sstp server")
+ break
+
+
+def _accel_cmd(command):
+ return run(f'/usr/bin/accel-cmd -p 2005 {command}')
+
+default_config_data = {
+ 'local_users' : [],
+ 'auth_mode' : 'local',
+ 'auth_proto' : [],
+ 'radius_server' : [],
+ 'radius_acct_tmo' : '3',
+ 'radius_max_try' : '3',
+ 'radius_timeout' : '3',
+ 'radius_nas_id' : '',
+ 'radius_nas_ip' : '',
+ 'radius_source_address' : '',
+ 'radius_shaper_attr' : '',
+ 'radius_shaper_vendor': '',
+ 'radius_dynamic_author' : '',
+ 'ssl_ca' : '',
+ 'ssl_cert' : '',
+ 'ssl_key' : '',
+ 'client_ip_pool' : [],
+ 'dnsv4' : [],
+ 'mtu' : '',
+ 'ppp_mppe' : '',
+ 'ppp_echo_failure' : '',
+ 'ppp_echo_interval' : '',
+ 'ppp_echo_timeout' : '',
+ 'thread_cnt' : ''
+}
+
+def get_config():
+ sstp = deepcopy(default_config_data)
+ base_path = ['vpn', 'sstp']
+ conf = Config()
+ if not conf.exists(base_path):
+ return None
+
+ conf.set_level(base_path)
+
+ cpu = int(os.cpu_count()/2)
+ if cpu < 1:
+ cpu = 1
+ sstp['thread_cnt'] = cpu
+
+ if conf.exists(['authentication', 'mode']):
+ sstp['auth_mode'] = conf.return_value(['authentication', 'mode'])
+
+ #
+ # local auth
+ if conf.exists(['authentication', 'local-users']):
+ for username in conf.list_nodes(['authentication', 'local-users', 'username']):
+ user = {
+ 'name' : username,
+ 'password' : '',
+ 'state' : 'enabled',
+ 'ip' : '*',
+ 'upload' : None,
+ 'download' : None
+ }
+
+ conf.set_level(base_path + ['authentication', 'local-users', 'username', username])
+
+ if conf.exists(['password']):
+ user['password'] = conf.return_value(['password'])
+
+ if conf.exists(['disable']):
+ user['state'] = 'disable'
+
+ if conf.exists(['static-ip']):
+ user['ip'] = conf.return_value(['static-ip'])
+
+ if conf.exists(['rate-limit', 'download']):
+ user['download'] = conf.return_value(['rate-limit', 'download'])
+
+ if conf.exists(['rate-limit', 'upload']):
+ user['upload'] = conf.return_value(['rate-limit', 'upload'])
+
+ sstp['local_users'].append(user)
+
+ #
+ # RADIUS auth and settings
+ conf.set_level(base_path + ['authentication', 'radius'])
+ if conf.exists(['server']):
+ for server in conf.list_nodes(['server']):
+ radius = {
+ 'server' : server,
+ 'key' : '',
+ 'fail_time' : 0,
+ 'port' : '1812'
+ }
+
+ conf.set_level(base_path + ['authentication', 'radius', 'server', server])
+
+ if conf.exists(['fail-time']):
+ radius['fail-time'] = conf.return_value(['fail-time'])
+
+ if conf.exists(['port']):
+ radius['port'] = conf.return_value(['port'])
+
+ if conf.exists(['key']):
+ radius['key'] = conf.return_value(['key'])
+
+ if not conf.exists(['disable']):
+ sstp['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+
+ if conf.exists(['acct-timeout']):
+ sstp['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ sstp['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ sstp['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ sstp['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ sstp['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ sstp['radius_source_address'] = conf.return_value(['source-address'])
+
+ # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA)
+ if conf.exists(['dynamic-author']):
+ dae = {
+ 'port' : '',
+ 'server' : '',
+ 'key' : ''
+ }
+
+ if conf.exists(['dynamic-author', 'server']):
+ dae['server'] = conf.return_value(['dynamic-author', 'server'])
+
+ if conf.exists(['dynamic-author', 'port']):
+ dae['port'] = conf.return_value(['dynamic-author', 'port'])
+
+ if conf.exists(['dynamic-author', 'key']):
+ dae['key'] = conf.return_value(['dynamic-author', 'key'])
+
+ sstp['radius_dynamic_author'] = dae
+
+ if conf.exists(['rate-limit', 'enable']):
+ sstp['radius_shaper_attr'] = 'Filter-Id'
+ c_attr = ['rate-limit', 'enable', 'attribute']
+ if conf.exists(c_attr):
+ sstp['radius_shaper_attr'] = conf.return_value(c_attr)
+
+ c_vendor = ['rate-limit', 'enable', 'vendor']
+ if conf.exists(c_vendor):
+ sstp['radius_shaper_vendor'] = conf.return_value(c_vendor)
+
+ #
+ # authentication protocols
+ conf.set_level(base_path + ['authentication'])
+ if conf.exists(['protocols']):
+ auth_mods = {
+ 'pap': 'auth_pap',
+ 'chap': 'auth_chap_md5',
+ 'mschap': 'auth_mschap_v1',
+ 'mschap-v2': 'auth_mschap_v2'
+ }
+
+ for proto in conf.return_values(['protocols']):
+ sstp['auth_proto'].append(auth_mods[proto])
+
+ else:
+ sstp['auth_proto'] = ['auth_mschap_v2']
+
+ #
+ # read in SSL certs
+ conf.set_level(base_path + ['ssl'])
+ if conf.exists(['ca-cert-file']):
+ sstp['ssl_ca'] = conf.return_value(['ca-cert-file'])
+
+ if conf.exists(['cert-file']):
+ sstp['ssl_cert'] = conf.return_value(['cert-file'])
+
+ if conf.exists(['key-file']):
+ sstp['ssl_key'] = conf.return_value(['key-file'])
+
+
+ #
+ # read in client ip pool settings
+ conf.set_level(base_path + ['network-settings', 'client-ip-settings'])
+ if conf.exists(['subnet']):
+ sstp['client_ip_pool'] = conf.return_values(['subnet'])
+
+ if conf.exists(['gateway-address']):
+ sstp['client_gateway'] = conf.return_value(['gateway-address'])
+
+ #
+ # read in network settings
+ conf.set_level(base_path + ['network-settings'])
+ if conf.exists(['name-server']):
+ sstp['dnsv4'] = conf.return_values(['name-server'])
+
+ if conf.exists(['mtu']):
+ sstp['mtu'] = conf.return_value(['mtu'])
+
+ #
+ # read in PPP stuff
+ conf.set_level(base_path + ['ppp-settings'])
+ if conf.exists('mppe'):
+ sstp['ppp_mppe'] = conf.return_value('ppp-settings mppe')
+
+ if conf.exists(['lcp-echo-failure']):
+ sstp['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure'])
+
+ if conf.exists(['lcp-echo-interval']):
+ sstp['ppp_echo_interval'] = conf.return_value(['lcp-echo-interval'])
+
+ if conf.exists(['lcp-echo-timeout']):
+ sstp['ppp_echo_timeout'] = conf.return_value(['lcp-echo-timeout'])
+
+ return sstp
+
+
+def verify(sstp):
+ if sstp is None:
+ return None
+
+ # vertify auth settings
+ if sstp['auth_mode'] == 'local':
+ if not sstp['local_users']:
+ raise ConfigError('sstp-server authentication local-users required')
+
+ for user in sstp['local_users']:
+ if not user['password']:
+ raise ConfigError(f"Password required for user {user['name']}")
+
+ # if up/download is set, check that both have a value
+ if user['upload'] and not user['download']:
+ raise ConfigError(f"Download speed value required for user {user['name']}")
+
+ if user['download'] and not user['upload']:
+ raise ConfigError(f"Upload speed value required for user {user['name']}")
+
+ if not sstp['client_ip_pool']:
+ raise ConfigError("Client IP subnet required")
+
+ if not sstp['client_gateway']:
+ raise ConfigError("Client gateway IP address required")
+
+ if len(sstp['dnsv4']) > 2:
+ raise ConfigError("Only 2 DNS name-servers can be configured")
+
+ if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']:
+ raise ConfigError('One or more SSL certificates missing')
+
+ if not os.path.exists(sstp['ssl_ca']):
+ raise ConfigError(f"CA cert file {sstp['ssl_ca']} does not exist")
+
+ if not os.path.exists(sstp['ssl_cert']):
+ raise ConfigError(f"SSL cert file {sstp['ssl_cert']} does not exist")
+
+ if not os.path.exists(sstp['ssl_key']):
+ raise ConfigError(f"SSL key file {sstp['ssl_key']} does not exist")
+
+ if sstp['auth_mode'] == 'radius':
+ if len(sstp['radius_server']) == 0:
+ raise ConfigError("RADIUS authentication requires at least one server")
+
+ for radius in sstp['radius_server']:
+ if not radius['key']:
+ raise ConfigError(f"Missing RADIUS secret for server {{ radius['key'] }}")
+
+def generate(sstp):
+ if sstp is None:
+ return None
+
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'sstp')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
+ # accel-cmd reload doesn't work so any change results in a restart of the daemon
+ tmpl = env.get_template('sstp.config.tmpl')
+ config_text = tmpl.render(sstp)
+ with open(sstp_conf, 'w') as f:
+ f.write(config_text)
+
+ if sstp['local_users']:
+ tmpl = env.get_template('chap-secrets.tmpl')
+ config_text = tmpl.render(sstp)
+ with open(chap_secrets, 'w') as f:
+ f.write(config_text)
+
+ os.chmod(chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+ else:
+ if os.path.exists(chap_secrets):
+ os.unlink(chap_secrets)
+
+ return sstp
+
+def apply(sstp):
+ if sstp is None:
+ if process_running(pidfile):
+ command = 'start-stop-daemon'
+ command += ' --stop '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ cmd(command)
+
+ if os.path.exists(pidfile):
+ os.remove(pidfile)
+
+ return None
+
+ if not process_running(pidfile):
+ if os.path.exists(pidfile):
+ os.remove(pidfile)
+
+ command = 'start-stop-daemon'
+ command += ' --start '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ command += ' --exec /usr/sbin/accel-pppd'
+ # now pass arguments to accel-pppd binary
+ command += ' --'
+ command += ' -c ' + sstp_conf
+ command += ' -p ' + pidfile
+ command += ' -d'
+ cmd(command)
+
+ chk_con()
+
+ else:
+ _accel_cmd('restart')
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
new file mode 100755
index 000000000..586424c09
--- /dev/null
+++ b/src/conf_mode/vrf.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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 os
+
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
+from json import loads
+
+from vyos.config import Config
+from vyos.configdict import list_diff
+from vyos.defaults import directories as vyos_data_dir
+from vyos.ifconfig import Interface
+from vyos.util import read_file, cmd
+from vyos import ConfigError
+
+config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf'
+
+default_config_data = {
+ 'bind_to_all': '0',
+ 'deleted': False,
+ 'vrf_add': [],
+ 'vrf_existing': [],
+ 'vrf_remove': []
+}
+
+def _cmd(command):
+ cmd(command, raising=ConfigError, message='Error changing VRF')
+
+def list_rules():
+ command = 'ip -j -4 rule show'
+ answer = loads(cmd(command))
+ return [_ for _ in answer if _]
+
+def vrf_interfaces(c, match):
+ matched = []
+ old_level = c.get_level()
+ c.set_level(['interfaces'])
+ section = c.get_config_dict([])
+ for type in section:
+ interfaces = section[type]
+ for name in interfaces:
+ interface = interfaces[name]
+ if 'vrf' in interface:
+ v = interface.get('vrf', '')
+ if v == match:
+ matched.append(name)
+
+ c.set_level(old_level)
+ return matched
+
+def vrf_routing(c, match):
+ matched = []
+ old_level = c.get_level()
+ c.set_level(['protocols', 'vrf'])
+ if match in c.list_nodes([]):
+ matched.append(match)
+
+ c.set_level(old_level)
+ return matched
+
+
+def get_config():
+ conf = Config()
+ vrf_config = deepcopy(default_config_data)
+
+ cfg_base = ['vrf']
+ if not conf.exists(cfg_base):
+ # get all currently effetive VRFs and mark them for deletion
+ vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name'])
+ else:
+ # set configuration level base
+ conf.set_level(cfg_base)
+
+ # Should services be allowed to bind to all VRFs?
+ if conf.exists(['bind-to-all']):
+ vrf_config['bind_to_all'] = '1'
+
+ # Determine vrf interfaces (currently effective) - to determine which
+ # vrf interface is no longer present and needs to be removed
+ eff_vrf = conf.list_effective_nodes(['name'])
+ act_vrf = conf.list_nodes(['name'])
+ vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf)
+
+ # read in individual VRF definition and build up
+ # configuration
+ for name in conf.list_nodes(['name']):
+ vrf_inst = {
+ 'description' : '',
+ 'members': [],
+ 'name' : name,
+ 'table' : '',
+ 'table_mod': False
+ }
+ conf.set_level(cfg_base + ['name', name])
+
+ if conf.exists(['table']):
+ # VRF table can't be changed on demand, thus we need to read in the
+ # current and the effective routing table number
+ act_table = conf.return_value(['table'])
+ eff_table = conf.return_effective_value(['table'])
+ vrf_inst['table'] = act_table
+ if eff_table and eff_table != act_table:
+ vrf_inst['table_mod'] = True
+
+ if conf.exists(['description']):
+ vrf_inst['description'] = conf.return_value(['description'])
+
+ # append individual VRF configuration to global configuration list
+ vrf_config['vrf_add'].append(vrf_inst)
+
+ # set configuration level base
+ conf.set_level(cfg_base)
+
+ # check VRFs which need to be removed as they are not allowed to have
+ # interfaces attached
+ tmp = []
+ for name in vrf_config['vrf_remove']:
+ vrf_inst = {
+ 'interfaces': [],
+ 'name': name,
+ 'routes': []
+ }
+
+ # find member interfaces of this particulat VRF
+ vrf_inst['interfaces'] = vrf_interfaces(conf, name)
+
+ # find routing protocols used by this VRF
+ vrf_inst['routes'] = vrf_routing(conf, name)
+
+ # append individual VRF configuration to temporary configuration list
+ tmp.append(vrf_inst)
+
+ # replace values in vrf_remove with list of dictionaries
+ # as we need it in verify() - we can't delete a VRF with members attached
+ vrf_config['vrf_remove'] = tmp
+ return vrf_config
+
+def verify(vrf_config):
+ # ensure VRF is not assigned to any interface
+ for vrf in vrf_config['vrf_remove']:
+ if len(vrf['interfaces']) > 0:
+ raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active member interfaces!")
+
+ if len(vrf['routes']) > 0:
+ raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active routing protocols!")
+
+ table_ids = []
+ for vrf in vrf_config['vrf_add']:
+ # table id is mandatory
+ if not vrf['table']:
+ raise ConfigError(f"VRF {vrf['name']} table id is mandatory!")
+
+ # routing table id can't be changed - OS restriction
+ if vrf['table_mod']:
+ raise ConfigError(f"VRF {vrf['name']} table id modification is not possible!")
+
+ # VRf routing table ID must be unique on the system
+ if vrf['table'] in table_ids:
+ raise ConfigError(f"VRF {vrf['name']} table id {vrf['table']} is not unique!")
+
+ table_ids.append(vrf['table'])
+
+ return None
+
+def generate(vrf_config):
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'vrf')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('vrf.conf.tmpl')
+ config_text = tmpl.render(vrf_config)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(vrf_config):
+ # Documentation
+ #
+ # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt
+ # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF)
+ # - https://github.com/Mellanox/mlxsw/wiki/L3-Tunneling
+ # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf
+ # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf
+
+ # set the default VRF global behaviour
+ bind_all = vrf_config['bind_to_all']
+ if read_file('/proc/sys/net/ipv4/tcp_l3mdev_accept') != bind_all:
+ _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}')
+ _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}')
+
+ for vrf in vrf_config['vrf_remove']:
+ name = vrf['name']
+ if os.path.isdir(f'/sys/class/net/{name}'):
+ _cmd(f'sudo ip -4 route del vrf {name} unreachable default metric 4278198272')
+ _cmd(f'sudo ip -6 route del vrf {name} unreachable default metric 4278198272')
+ _cmd(f'ip link delete dev {name}')
+
+ for vrf in vrf_config['vrf_add']:
+ name = vrf['name']
+ table = vrf['table']
+
+ if not os.path.isdir(f'/sys/class/net/{name}'):
+ # For each VRF apart from your default context create a VRF
+ # interface with a separate routing table
+ _cmd(f'ip link add {name} type vrf table {table}')
+ # Start VRf
+ _cmd(f'ip link set dev {name} up')
+ # The kernel Documentation/networking/vrf.txt also recommends
+ # adding unreachable routes to the VRF routing tables so that routes
+ # afterwards are taken.
+ _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272')
+ _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272')
+
+ # set VRF description for e.g. SNMP monitoring
+ Interface(name).set_alias(vrf['description'])
+
+ # Linux routing uses rules to find tables - routing targets are then
+ # looked up in those tables. If the lookup got a matching route, the
+ # process ends.
+ #
+ # TL;DR; first table with a matching entry wins!
+ #
+ # You can see your routing table lookup rules using "ip rule", sadly the
+ # local lookup is hit before any VRF lookup. Pinging an addresses from the
+ # VRF will usually find a hit in the local table, and never reach the VRF
+ # routing table - this is usually not what you want. Thus we will
+ # re-arrange the tables and move the local lookup furhter down once VRFs
+ # are enabled.
+
+ # get current preference on local table
+ local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0]
+
+ # change preference when VRFs are enabled and local lookup table is default
+ if not local_pref and vrf_config['vrf_add']:
+ for af in ['-4', '-6']:
+ _cmd(f'ip {af} rule add pref 32765 table local')
+ _cmd(f'ip {af} rule del pref 0')
+
+ # return to default lookup preference when no VRF is configured
+ if not vrf_config['vrf_add']:
+ for af in ['-4', '-6']:
+ _cmd(f'ip {af} rule add pref 0 table local')
+ _cmd(f'ip {af} rule del pref 32765')
+
+ # clean out l3mdev-table rule if present
+ if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]:
+ _cmd(f'ip {af} rule del pref 1000')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py
index a09e55a2f..3f1b73385 100755
--- a/src/conf_mode/vrrp.py
+++ b/src/conf_mode/vrrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,145 +13,25 @@
#
# 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 os
-import sys
-import subprocess
-import ipaddress
-import jinja2
+from sys import exit
+from ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address
+from jinja2 import FileSystemLoader, Environment
+from json import dumps
+from pathlib import Path
import vyos.config
import vyos.keepalived
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
daemon_file = "/etc/default/keepalived"
config_file = "/etc/keepalived/keepalived.conf"
-
-config_tmpl = """
-# Autogenerated by VyOS
-# Do not edit this file, all your changes will be lost
-# on next commit or reboot
-
-global_defs {
- dynamic_interfaces
- script_user root
-}
-
-{% for group in groups -%}
-
-{% if group.health_check_script -%}
-vrrp_script healthcheck_{{ group.name }} {
- script "{{ group.health_check_script }}"
- interval {{ group.health_check_interval }}
- fall {{ group.health_check_count }}
- rise 1
-
-}
-{% endif %}
-
-vrrp_instance {{ group.name }} {
- {% if group.description -%}
- # {{ group.description }}
- {% endif -%}
-
- state BACKUP
- interface {{ group.interface }}
- virtual_router_id {{ group.vrid }}
- priority {{ group.priority }}
- advert_int {{ group.advertise_interval }}
-
- {% if group.preempt -%}
- preempt_delay {{ group.preempt_delay }}
- {% else -%}
- nopreempt
- {% endif -%}
-
- {% if group.peer_address -%}
- unicast_peer { {{ group.peer_address }} }
- {% endif -%}
-
- {% if group.hello_source -%}
- {%- if group.peer_address -%}
- unicast_src_ip {{ group.hello_source }}
- {%- else -%}
- mcast_src_ip {{ group.hello_source }}
- {%- endif %}
- {% endif -%}
-
- {% if group.use_vmac and group.peer_address -%}
- use_vmac {{group.interface}}v{{group.vrid}}
- vmac_xmit_base
- {% elif group.use_vmac -%}
- use_vmac {{group.interface}}v{{group.vrid}}
- {% endif -%}
-
- {% if group.auth_password -%}
- authentication {
- auth_pass "{{ group.auth_password }}"
- auth_type {{ group.auth_type }}
- }
- {% endif -%}
-
- virtual_ipaddress {
- {% for addr in group.virtual_addresses -%}
- {{ addr }}
- {% endfor -%}
- }
-
- {% if group.health_check_script -%}
- track_script {
- healthcheck_{{ group.name }}
- }
- {% endif -%}
-
- {% if group.master_script -%}
- notify_master "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state master --group {{ group.name }} --interface {{ group.interface }} {{ group.master_script }}"
- {% endif -%}
-
- {% if group.backup_script -%}
- notify_backup "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state backup --group {{ group.name }} --interface {{ group.interface }} {{ group.backup_script }}"
- {% endif -%}
-
- {% if group.fault_script -%}
- notify_fault "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state fault --group {{ group.name }} --interface {{ group.interface }} {{ group.fault_script }}"
- {% endif -%}
-
- {% if group.stop_script -%}
- notify_stop "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state stop --group {{ group.name }} --interface {{ group.interface }} {{ group.stop_script }}"
- {% endif -%}
-}
-
-{% endfor -%}
-
-{% for sync_group in sync_groups -%}
-vrrp_sync_group {{ sync_group.name }} {
- group {
- {% for member in sync_group.members -%}
- {{ member }}
- {% endfor -%}
- }
-
- {% if sync_group.conntrack_sync -%}
- notify_master "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh master {{ sync_group.name }}"
- notify_backup "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh backup {{ sync_group.name }}"
- notify_fault "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh fault {{ sync_group.name }}"
- {% endif -%}
-}
-
-{% endfor -%}
-
-"""
-
-daemon_tmpl = """
-# Autogenerated by VyOS
-# Options to pass to keepalived
-
-# DAEMON_ARGS are appended to the keepalived command-line
-DAEMON_ARGS="--snmp"
-"""
+config_dict_path = "/run/keepalived_config.dict"
def get_config():
vrrp_groups = []
@@ -220,7 +100,7 @@ def get_config():
vrrp_groups.append(group)
- config.set_level("")
+ config.set_level("")
# Get the sync group used for conntrack-sync
conntrack_sync_group = None
@@ -238,10 +118,21 @@ def get_config():
if conntrack_sync_group == sync_group_name:
sync_group["conntrack_sync"] = True
+ # add transition script configuration
+ sync_group["master_script"] = config.return_value("transition-script master")
+ sync_group["backup_script"] = config.return_value("transition-script backup")
+ sync_group["fault_script"] = config.return_value("transition-script fault")
+ sync_group["stop_script"] = config.return_value("transition-script stop")
+
sync_groups.append(sync_group)
+ # create a file with dict with proposed configuration
+ with open("{}.temp".format(config_dict_path), 'w') as dict_file:
+ dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups}))
+
return (vrrp_groups, sync_groups)
+
def verify(data):
vrrp_groups, sync_groups = data
@@ -262,31 +153,31 @@ def verify(data):
# XXX: filter on map object is destructive, so we force it to list.
# Additionally, filter objects always evaluate to True, empty or not,
# so we force them to lists as well.
- vaddrs = list(map(lambda i: ipaddress.ip_interface(i), group["virtual_addresses"]))
- vaddrs4 = list(filter(lambda x: isinstance(x, ipaddress.IPv4Interface), vaddrs))
- vaddrs6 = list(filter(lambda x: isinstance(x, ipaddress.IPv6Interface), vaddrs))
+ vaddrs = list(map(lambda i: ip_interface(i), group["virtual_addresses"]))
+ vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs))
+ vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs))
if vaddrs4 and vaddrs6:
raise ConfigError("VRRP group {0} mixes IPv4 and IPv6 virtual addresses, this is not allowed. Create separate groups for IPv4 and IPv6".format(group["name"]))
if vaddrs4:
if group["hello_source"]:
- hsa = ipaddress.ip_address(group["hello_source"])
- if isinstance(hsa, ipaddress.IPv6Address):
+ hsa = ip_address(group["hello_source"])
+ if isinstance(hsa, IPv6Address):
raise ConfigError("VRRP group {0} uses IPv4 but its hello-source-address is IPv6".format(group["name"]))
if group["peer_address"]:
- pa = ipaddress.ip_address(group["peer_address"])
- if isinstance(pa, ipaddress.IPv6Address):
+ pa = ip_address(group["peer_address"])
+ if isinstance(pa, IPv6Address):
raise ConfigError("VRRP group {0} uses IPv4 but its peer-address is IPv6".format(group["name"]))
if vaddrs6:
if group["hello_source"]:
- hsa = ipaddress.ip_address(group["hello_source"])
- if isinstance(hsa, ipaddress.IPv4Address):
+ hsa = ip_address(group["hello_source"])
+ if isinstance(hsa, IPv4Address):
raise ConfigError("VRRP group {0} uses IPv6 but its hello-source-address is IPv4".format(group["name"]))
if group["peer_address"]:
- pa = ipaddress.ip_address(group["peer_address"])
- if isinstance(pa, ipaddress.IPv4Address):
+ pa = ip_address(group["peer_address"])
+ if isinstance(pa, IPv4Address):
raise ConfigError("VRRP group {0} uses IPv6 but its peer-address is IPv4".format(group["name"]))
# Disallow same VRID on multiple interfaces
@@ -308,7 +199,13 @@ def verify(data):
if not (m in vrrp_group_names):
raise ConfigError("VRRP sync-group {0} refers to VRRP group {1}, but group {1} does not exist".format(sync_group["name"], m))
+
def generate(data):
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'vrrp')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
vrrp_groups, sync_groups = data
# Remove disabled groups from the sync group member lists
@@ -318,34 +215,44 @@ def generate(data):
if g["disable"]:
print("Warning: ignoring disabled VRRP group {0} in sync-group {1}".format(g["name"], sync_group["name"]))
# Filter out disabled groups
- vrrp_groups = list(filter(lambda x: x["disable"] != True, vrrp_groups))
+ vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups))
- tmpl = jinja2.Template(config_tmpl)
+ tmpl = env.get_template('keepalived.conf.tmpl')
config_text = tmpl.render({"groups": vrrp_groups, "sync_groups": sync_groups})
with open(config_file, 'w') as f:
f.write(config_text)
+ tmpl = env.get_template('daemon.tmpl')
+ config_text = tmpl.render()
with open(daemon_file, 'w') as f:
- f.write(daemon_tmpl)
+ f.write(config_text)
return None
+
def apply(data):
vrrp_groups, sync_groups = data
if vrrp_groups:
+ # safely rename a temporary file with configuration dict
+ try:
+ dict_file = Path("{}.temp".format(config_dict_path))
+ dict_file.rename(Path(config_dict_path))
+ except Exception as err:
+ print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err))
+
if not vyos.keepalived.vrrp_running():
print("Starting the VRRP process")
- ret = subprocess.call("sudo systemctl restart keepalived.service", shell=True)
+ ret = call("sudo systemctl restart keepalived.service")
else:
print("Reloading the VRRP process")
- ret = subprocess.call("sudo systemctl reload keepalived.service", shell=True)
+ ret = call("sudo systemctl reload keepalived.service")
if ret != 0:
raise ConfigError("keepalived failed to start")
else:
# VRRP is removed in the commit
print("Stopping the VRRP process")
- subprocess.call("sudo systemctl stop keepalived.service", shell=True)
+ call("sudo systemctl stop keepalived.service")
os.unlink(config_file)
return None
@@ -359,4 +266,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print("VRRP error: {0}".format(str(e)))
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py
index 4a44573ca..8b8953cb7 100755
--- a/src/conf_mode/vyos_cert.py
+++ b/src/conf_mode/vyos_cert.py
@@ -18,7 +18,6 @@
import sys
import os
-import subprocess
import tempfile
import pathlib
import ssl
@@ -26,6 +25,7 @@ import ssl
import vyos.defaults
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import cmd
vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
@@ -49,16 +49,16 @@ def status_self_signed(cert_data):
# check if certificate is 1/2 past lifetime, with openssl -checkend
end_days = int(cert_data['lifetime'])
end_seconds = int(0.5*60*60*24*end_days)
- checkend_cmd = ('openssl x509 -checkend {end} -noout -in {crt}'
- ''.format(end=end_seconds, **cert_data))
+ checkend_cmd = 'openssl x509 -checkend {end} -noout -in {crt}'.format(end=end_seconds, **cert_data)
try:
- subprocess.check_call(checkend_cmd, shell=True)
+ cmd(checkend_cmd, message='Called process error')
return True
- except subprocess.CalledProcessError as err:
- if err.returncode == 1:
+ except OSError as err:
+ if err.errno == 1:
return False
- else:
- print("Called process error: {}.".format(err))
+ print(err)
+ # XXX: This seems wrong to continue on failure
+ # implicitely returning None
def generate_self_signed(cert_data):
san_config = None
@@ -86,9 +86,10 @@ def generate_self_signed(cert_data):
''.format(**cert_data))
try:
- subprocess.check_call(openssl_req_cmd, shell=True)
- except subprocess.CalledProcessError as err:
- print("Called process error: {}.".format(err))
+ cmd(openssl_req_cmd, message='Called process error')
+ except OSError as err:
+ print(err)
+ # XXX: seems wrong to ignore the failure
os.chmod('{key}'.format(**cert_data), 0o400)
@@ -126,11 +127,8 @@ def generate(vyos_cert):
def apply(vyos_cert):
for dep in dependencies:
- cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
- try:
- subprocess.check_call(cmd, shell=True)
- except subprocess.CalledProcessError as err:
- raise ConfigError("{}.".format(err))
+ command = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
+ cmd(command, raising=ConfigError)
if __name__ == '__main__':
try:
diff --git a/src/etc/init.d/isc-dhcpv4-server b/src/etc/init.d/isc-dhcpv4-server
index 377634a13..94a1020ac 100755
--- a/src/etc/init.d/isc-dhcpv4-server
+++ b/src/etc/init.d/isc-dhcpv4-server
@@ -72,7 +72,7 @@ case "$1" in
start)
test_config
log_daemon_msg "Starting $DESC" "$NAME"
- start-stop-daemon --start --quiet --pidfile "$DHCPD_PID" \
+ start-stop-daemon --start --oknodo --quiet --pidfile "$DHCPD_PID" \
--exec /usr/sbin/dhcpd -- \
-q $OPTIONS -cf "$DHCPD_CONF" -pf "$DHCPD_PID" $INTERFACES
sleep 2
@@ -87,7 +87,7 @@ case "$1" in
;;
stop)
log_daemon_msg "Stopping $DESC" "$NAME"
- start-stop-daemon --stop --quiet --pidfile "$DHCPD_PID"
+ start-stop-daemon --stop --oknodo --quiet --pidfile "$DHCPD_PID"
log_end_msg $?
rm -f "$DHCPD_PID"
;;
diff --git a/src/etc/init.d/isc-dhcpv6-relay b/src/etc/init.d/isc-dhcpv6-relay
index 5a8ce620c..e553eafd1 100755
--- a/src/etc/init.d/isc-dhcpv6-relay
+++ b/src/etc/init.d/isc-dhcpv6-relay
@@ -31,11 +31,11 @@ DHCRELAYPID=/var/run/dhcv6relay.pid
case "$1" in
start)
- start-stop-daemon --start --quiet --pidfile $DHCRELAYPID \
+ start-stop-daemon --start --oknodo --quiet --pidfile $DHCRELAYPID \
--exec /usr/sbin/dhcrelay -- -q $OPTIONS -pf $DHCRELAYPID
;;
stop)
- start-stop-daemon --stop --quiet --pidfile $DHCRELAYPID
+ start-stop-daemon --stop --oknodo --quiet --pidfile $DHCRELAYPID
;;
restart | force-reload)
$0 stop
diff --git a/src/etc/init.d/isc-dhcpv6-server b/src/etc/init.d/isc-dhcpv6-server
index 55f59b68e..f6b27cb4a 100755
--- a/src/etc/init.d/isc-dhcpv6-server
+++ b/src/etc/init.d/isc-dhcpv6-server
@@ -72,7 +72,7 @@ case "$1" in
start)
test_config
log_daemon_msg "Starting $DESC" "$NAME"
- start-stop-daemon --start --quiet --pidfile "$DHCPD_PID" \
+ start-stop-daemon --start --oknodo --quiet --pidfile "$DHCPD_PID" \
--exec /usr/sbin/dhcpd -- \
-q $OPTIONS -cf "$DHCPD_CONF" -pf "$DHCPD_PID" $INTERFACES
sleep 2
@@ -87,7 +87,7 @@ case "$1" in
;;
stop)
log_daemon_msg "Stopping $DESC" "$NAME"
- start-stop-daemon --stop --quiet --pidfile "$DHCPD_PID"
+ start-stop-daemon --stop --oknodo --quiet --pidfile "$DHCPD_PID"
log_end_msg $?
rm -f "$DHCPD_PID"
;;
diff --git a/src/etc/ppp/ip-pre-up b/src/etc/ppp/ip-pre-up
new file mode 100755
index 000000000..05840650b
--- /dev/null
+++ b/src/etc/ppp/ip-pre-up
@@ -0,0 +1,51 @@
+#!/bin/sh
+#
+# This script is run by the pppd when the link is created.
+# It uses run-parts to run scripts in /etc/ppp/ip-pre-up.d, to
+# change name, setup firewall,etc you should create script(s) there.
+#
+# Be aware that other packages may include /etc/ppp/ip-pre-up.d scripts (named
+# after that package), so choose local script names with that in mind.
+#
+# This script is called with the following arguments:
+# Arg Name Example
+# $1 Interface name ppp0
+# $2 The tty ttyS1
+# $3 The link speed 38400
+# $4 Local IP number 12.34.56.78
+# $5 Peer IP number 12.34.56.99
+# $6 Optional ``ipparam'' value foo
+
+# The environment is cleared before executing this script
+# so the path must be reset
+PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin
+export PATH
+
+# These variables are for the use of the scripts run by run-parts
+PPP_IFACE="$1"
+PPP_TTY="$2"
+PPP_SPEED="$3"
+PPP_LOCAL="$4"
+PPP_REMOTE="$5"
+PPP_IPPARAM="$6"
+export PPP_IFACE PPP_TTY PPP_SPEED PPP_LOCAL PPP_REMOTE PPP_IPPARAM
+
+# as an additional convenience, $PPP_TTYNAME is set to the tty name,
+# stripped of /dev/ (if present) for easier matching.
+PPP_TTYNAME=`/usr/bin/basename "$2"`
+export PPP_TTYNAME
+
+# If /var/log/ppp-ipupdown.log exists use it for logging.
+if [ -e /var/log/ppp-ipupdown.log ]; then
+ exec > /var/log/ppp-ipupdown.log 2>&1
+ echo $0 $*
+ echo
+fi
+
+# This script can be used to override the .d files supplied by other packages.
+if [ -x /etc/ppp/ip-pre-up.local ]; then
+ exec /etc/ppp/ip-pre-up.local "$*"
+fi
+
+run-parts /etc/ppp/ip-pre-up.d \
+ --arg="$1" --arg="$2" --arg="$3" --arg="$4" --arg="$5" --arg="$6"
diff --git a/src/etc/rsyslog.d/01-auth.conf b/src/etc/rsyslog.d/01-auth.conf
new file mode 100644
index 000000000..cc64099d6
--- /dev/null
+++ b/src/etc/rsyslog.d/01-auth.conf
@@ -0,0 +1,14 @@
+# The lines below cause all listed daemons/processes to be logged into
+# /var/log/auth.log, then drops the message so it does not also go to the
+# regular syslog so that messages are not duplicated
+
+$outchannel auth_log,/var/log/auth.log
+if $programname == 'CRON' or
+ $programname == 'sudo' or
+ $programname == 'su'
+ then :omfile:$auth_log
+
+if $programname == 'CRON' or
+ $programname == 'sudo' or
+ $programname == 'su'
+ then stop
diff --git a/src/etc/udev/rules.d/99-vyos-wwan.rules b/src/etc/udev/rules.d/99-vyos-wwan.rules
new file mode 100644
index 000000000..67f30a3dd
--- /dev/null
+++ b/src/etc/udev/rules.d/99-vyos-wwan.rules
@@ -0,0 +1,11 @@
+ACTION!="add|change", GOTO="mbim_to_qmi_rules_end"
+
+SUBSYSTEM!="usb", GOTO="mbim_to_qmi_rules_end"
+
+# ignore any device with only one configuration
+ATTR{bNumConfigurations}=="1", GOTO="mbim_to_qmi_rules_end"
+
+# force Sierra Wireless MC7710 to configuration #1
+ATTR{idVendor}=="1199",ATTR{idProduct}=="68a2",ATTR{bConfigurationValue}="1"
+
+LABEL="mbim_to_qmi_rules_end"
diff --git a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
index 72da317f5..dc751c45c 100755
--- a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
+++ b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
@@ -15,11 +15,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
-import subprocess
import syslog as sl
from vyos.config import Config
-from vyos.util import vyos
+from vyos import ConfigError
+from vyos.util import run
+
def get_config():
c = Config()
@@ -28,7 +29,7 @@ def get_config():
# skip interfaces that are disabled or is configured for dhcp
check_disable = "interfaces ethernet {} disable".format(intf)
check_dhcp = "interfaces ethernet {} address dhcp".format(intf)
- if c.exists_effective(check_disable) or c.exists_effective(check_dhcp):
+ if c.exists_effective(check_disable):
continue
# get addresses configured on the interface
@@ -38,23 +39,28 @@ def get_config():
interfaces[intf] = [addr.strip("'") for addr in intf_addresses]
return interfaces
+
def apply(config):
for intf, addresses in config.items():
# bring the interface up
cmd = ["ip", "link", "set", "dev", intf, "up"]
sl.syslog(sl.LOG_NOTICE, " ".join(cmd))
- subprocess.call(cmd)
+ run(cmd)
# add configured addresses to interface
for addr in addresses:
- cmd = ["ip", "address", "add", addr, "dev", intf]
+ if addr == "dhcp":
+ cmd = ["dhclient", intf]
+ else:
+ cmd = ["ip", "address", "add", addr, "dev", intf]
sl.syslog(sl.LOG_NOTICE, " ".join(cmd))
- subprocess.call(cmd)
+ run(cmd)
+
if __name__ == '__main__':
try:
config = get_config()
apply(config)
- except vyos.ConfigError as e:
+ except ConfigError as e:
print(e)
sys.exit(1)
diff --git a/src/helpers/run-config-migration.py b/src/helpers/run-config-migration.py
index a57a19cdf..cc7166c22 100755
--- a/src/helpers/run-config-migration.py
+++ b/src/helpers/run-config-migration.py
@@ -19,7 +19,8 @@ import os
import sys
import argparse
import datetime
-import subprocess
+
+from vyos.util import cmd
from vyos.migrator import Migrator, VirtualMigrator
def main():
@@ -61,23 +62,25 @@ def main():
'{0:%Y-%m-%d-%H%M%S}'.format(datetime.datetime.now()),
'pre-migration'])
- try:
- subprocess.check_call(['cp', '-p', config_file_name,
- backup_file_name])
- except subprocess.CalledProcessError as err:
- print("Called process error: {}.".format(err))
- sys.exit(1)
+ cmd(f'cp -p {config_file_name} {backup_file_name}')
if not virtual:
- migration = Migrator(config_file_name, force=force_on,
- set_vintage=vintage)
+ virtual_migration = VirtualMigrator(config_file_name)
+ virtual_migration.run()
+
+ migration = Migrator(config_file_name, force=force_on)
+ migration.run()
+
+ if not migration.config_changed():
+ os.remove(backup_file_name)
else:
- migration = VirtualMigrator(config_file_name)
+ virtual_migration = VirtualMigrator(config_file_name,
+ set_vintage=vintage)
- migration.run()
+ virtual_migration.run()
- if not migration._changed:
- os.remove(backup_file_name)
+ if not virtual_migration.config_changed():
+ os.remove(backup_file_name)
if __name__ == '__main__':
main()
diff --git a/src/helpers/validate-value.py b/src/helpers/validate-value.py
index 36f996d38..a58ba61d1 100755
--- a/src/helpers/validate-value.py
+++ b/src/helpers/validate-value.py
@@ -5,6 +5,8 @@ import os
import sys
import argparse
+from vyos.util import call
+
parser = argparse.ArgumentParser()
parser.add_argument('--regex', action='append')
parser.add_argument('--exec', action='append')
@@ -31,7 +33,7 @@ try:
cmd = "{0} {1}".format(cmd, args.value)
if debug:
print(cmd)
- res = os.system(cmd)
+ res = call(cmd)
if res == 0:
sys.exit(0)
except Exception as exn:
diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py
index 7c81a4c3c..c5bf22f10 100755
--- a/src/helpers/vyos-boot-config-loader.py
+++ b/src/helpers/vyos-boot-config-loader.py
@@ -20,19 +20,21 @@ import os
import sys
import pwd
import grp
-import subprocess
import traceback
from datetime import datetime
from vyos.defaults import directories
from vyos.configsession import ConfigSession, ConfigSessionError
from vyos.configtree import ConfigTree
+from vyos.util import cmd
STATUS_FILE = '/tmp/vyos-config-status'
TRACE_FILE = '/tmp/boot-config-trace'
CFG_GROUP = 'vyattacfg'
+trace_config = False
+
if 'log' in directories:
LOG_DIR = directories['log']
else:
@@ -45,6 +47,9 @@ try:
cmdline = f.read()
if 'vyos-debug' in cmdline:
os.environ['VYOS_DEBUG'] = 'yes'
+ if 'vyos-config-debug' in cmdline:
+ os.environ['VYOS_DEBUG'] = 'yes'
+ trace_config = True
except Exception as e:
print('{0}'.format(e))
@@ -97,12 +102,7 @@ def failsafe(config_file_name):
'authentication',
'encrypted-password'])
- cmd = ("useradd -s /bin/bash -G 'users,sudo' -m -N -p '{0}' "
- "vyos".format(passwd))
- try:
- subprocess.check_call(cmd, shell=True)
- except subprocess.CalledProcessError as e:
- sys.exit("{0}".format(e))
+ cmd(f"useradd -s /bin/bash -G 'users,sudo' -m -N -p '{passwd}' vyos")
if __name__ == '__main__':
if len(sys.argv) < 2:
@@ -130,8 +130,9 @@ if __name__ == '__main__':
config_file = f.read()
except Exception:
write_config_status(1)
- failsafe(default_file_name)
- trace_to_file(TRACE_FILE)
+ if trace_config:
+ failsafe(default_file_name)
+ trace_to_file(TRACE_FILE)
sys.exit(1)
try:
@@ -146,8 +147,9 @@ if __name__ == '__main__':
# If here, there is no use doing session.discard, as we have no
# recoverable config environment, and will only throw an error
write_config_status(1)
- failsafe(default_file_name)
- trace_to_file(TRACE_FILE)
+ if trace_config:
+ failsafe(default_file_name)
+ trace_to_file(TRACE_FILE)
sys.exit(1)
time_elapsed_load = time_end_load - time_begin_load
diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py
index 495eb5d40..097d28d85 100755
--- a/src/helpers/vyos-bridge-sync.py
+++ b/src/helpers/vyos-bridge-sync.py
@@ -21,16 +21,12 @@
# to the bridge automatically once it's available
import argparse
-import subprocess
-
from sys import exit
from time import sleep
+
from vyos.config import Config
+from vyos.util import cmd, run
-def subprocess_cmd(command):
- process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True)
- proc_stdout = process.communicate()[0].strip()
- pass
if __name__ == '__main__':
parser = argparse.ArgumentParser()
@@ -45,9 +41,11 @@ if __name__ == '__main__':
for bridge in conf.list_nodes('interfaces bridge'):
for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)):
if args.interface == member_if:
- cmd = 'brctl addif "{}" "{}"'.format(bridge, args.interface)
+ command = 'brctl addif "{}" "{}"'.format(bridge, args.interface)
# let interfaces etc. settle - especially required for OpenVPN bridged interfaces
sleep(4)
- subprocess_cmd(cmd)
+ # XXX: This is ignoring any issue, should be cmd but kept as it
+ # XXX: during the migration to not cause any regression
+ run(command)
exit(0)
diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py
index 4e6d67efa..a9fa15778 100755
--- a/src/helpers/vyos-load-config.py
+++ b/src/helpers/vyos-load-config.py
@@ -28,27 +28,21 @@ import tempfile
import vyos.defaults
import vyos.remote
from vyos.config import Config, VyOSError
-from vyos.migrator import Migrator, MigratorError
-
-system_config_file = 'config.boot'
+from vyos.migrator import Migrator, VirtualMigrator, MigratorError
class LoadConfig(Config):
"""A subclass for calling 'loadFile'.
This does not belong in config.py, and only has a single caller.
"""
- def load_config(self, file_path):
- cmd = [self._cli_shell_api, 'loadFile', file_path]
- self._run(cmd)
+ def load_config(self, path):
+ return self._run(['/bin/cli-shell-api','loadFile',path])
-if len(sys.argv) > 1:
- file_name = sys.argv[1]
-else:
- file_name = system_config_file
+file_name = sys.argv[1] if len(sys.argv) > 1 else 'config.boot'
configdir = vyos.defaults.directories['config']
-
protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp']
+
if any(x in file_name for x in protocols):
config_file = vyos.remote.get_remote_config(file_name)
if not config_file:
@@ -73,6 +67,12 @@ with tempfile.NamedTemporaryFile() as fp:
with open(fp.name, 'w') as fd:
fd.write(config_file)
+ virtual_migration = VirtualMigrator(fp.name)
+ try:
+ virtual_migration.run()
+ except MigratorError as err:
+ sys.exit('{}'.format(err))
+
migration = Migrator(fp.name)
try:
migration.run()
diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py
index 7ae62cfb3..14df2734b 100755
--- a/src/helpers/vyos-merge-config.py
+++ b/src/helpers/vyos-merge-config.py
@@ -17,13 +17,13 @@
import sys
import os
-import subprocess
import tempfile
import vyos.defaults
import vyos.remote
-import vyos.migrator
from vyos.config import Config
from vyos.configtree import ConfigTree
+from vyos.migrator import Migrator, VirtualMigrator
+from vyos.util import cmd, DEVNULL
if (len(sys.argv) < 2):
@@ -61,19 +61,20 @@ with tempfile.NamedTemporaryFile() as file_to_migrate:
with open(file_to_migrate.name, 'w') as fd:
fd.write(config_file)
- migration = vyos.migrator.Migrator(file_to_migrate.name)
+ virtual_migration = VirtualMigrator(file_to_migrate.name)
+ virtual_migration.run()
+
+ migration = Migrator(file_to_migrate.name)
migration.run()
- if migration.config_changed():
+
+ if virtual_migration.config_changed() or migration.config_changed():
with open(file_to_migrate.name, 'r') as fd:
config_file = fd.read()
merge_config_tree = ConfigTree(config_file)
effective_config = Config()
-
-output_effective_config = effective_config.show_config()
-
-effective_config_tree = ConfigTree(output_effective_config)
+effective_config_tree = effective_config._running_config
effective_cmds = effective_config_tree.to_commands()
merge_cmds = merge_config_tree.to_commands()
@@ -98,13 +99,11 @@ if (len(sys.argv) > 2):
if path:
add_cmds = [ cmd for cmd in add_cmds if path in cmd ]
-for cmd in add_cmds:
- cmd = "/opt/vyatta/sbin/my_" + cmd
-
+for add in add_cmds:
try:
- subprocess.check_call(cmd, shell=True)
- except subprocess.CalledProcessError as err:
- print("Called process error: {}.".format(err))
+ cmd(f'/opt/vyatta/sbin/my_{add}', shell=True, stderr=DEVNULL)
+ except OSError as err:
+ print(err)
if effective_config.session_changed():
print("Merge complete. Use 'commit' to make changes effective.")
diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2
index 31ba5573f..9a50b6aa3 100755
--- a/src/migration-scripts/dns-forwarding/1-to-2
+++ b/src/migration-scripts/dns-forwarding/1-to-2
@@ -23,8 +23,8 @@
import sys
from ipaddress import ip_interface
+from vyos.ifconfig import Interface
from vyos.configtree import ConfigTree
-from vyos.interfaces import get_type_of_interface
if (len(sys.argv) < 1):
print("Must specify file name!")
@@ -41,7 +41,10 @@ base = ['service', 'dns', 'forwarding']
if not config.exists(base):
# Nothing to do
sys.exit(0)
+
else:
+ # XXX: we can remove the else and un-indent this whole block
+
if config.exists(base + ['listen-on']):
listen_intf = config.return_values(base + ['listen-on'])
# Delete node with abandoned command
@@ -60,7 +63,10 @@ else:
# this is a QinQ VLAN interface
intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2]
- path = ['interfaces', get_type_of_interface(intf), intf, 'address']
+ section = Interface.section(intf)
+ if not section:
+ raise ValueError(f'Invalid interface name {intf}')
+ path = ['interfaces', section, intf, 'address']
# retrieve corresponding interface addresses in CIDR format
# those need to be converted in pure IP addresses without network information
diff --git a/src/migration-scripts/https/0-to-1 b/src/migration-scripts/https/0-to-1
new file mode 100755
index 000000000..23809f5ad
--- /dev/null
+++ b/src/migration-scripts/https/0-to-1
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# * Move server block directives under 'virtual-host' tag node, instead of
+# relying on 'listen-address' tag node
+
+import sys
+
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 2):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+old_base = ['service', 'https', 'listen-address']
+if not config.exists(old_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ new_base = ['service', 'https', 'virtual-host']
+ config.set(new_base)
+ config.set_tag(new_base)
+
+ index = 0
+ for addr in config.list_nodes(old_base):
+ tag_name = f'vhost{index}'
+ config.set(new_base + [tag_name])
+ config.set(new_base + [tag_name, 'listen-address'], value=addr)
+
+ if config.exists(old_base + [addr, 'listen-port']):
+ port = config.return_value(old_base + [addr, 'listen-port'])
+ config.set(new_base + [tag_name, 'listen-port'], value=port)
+
+ if config.exists(old_base + [addr, 'server-name']):
+ names = config.return_values(old_base + [addr, 'server-name'])
+ for name in names:
+ config.set(new_base + [tag_name, 'server-name'], value=name,
+ replace=False)
+
+ index += 1
+
+ config.delete(old_base)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/https/1-to-2 b/src/migration-scripts/https/1-to-2
new file mode 100755
index 000000000..b1cf37ea6
--- /dev/null
+++ b/src/migration-scripts/https/1-to-2
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# * Move 'api virtual-host' list to 'api-restrict virtual-host' so it
+# is owned by https.py instead of http-api.py
+
+import sys
+
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 2):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+old_base = ['service', 'https', 'api', 'virtual-host']
+if not config.exists(old_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ new_base = ['service', 'https', 'api-restrict', 'virtual-host']
+ config.set(new_base)
+
+ names = config.return_values(old_base)
+ for name in names:
+ config.set(new_base, value=name, replace=False)
+
+ config.delete(old_base)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/interfaces/5-to-6 b/src/migration-scripts/interfaces/5-to-6
new file mode 100755
index 000000000..85a1994c6
--- /dev/null
+++ b/src/migration-scripts/interfaces/5-to-6
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+# Migrate IPv6 router advertisments from a nested interface configuration to
+# a denested "service router-advert"
+
+import sys
+from vyos.configtree import ConfigTree
+
+def copy_rtradv(c, old_base, interface):
+ base = ['service', 'router-advert', 'interface']
+
+ if c.exists(old_base):
+ if not c.exists(base):
+ c.set(base)
+ c.set_tag(base)
+
+ # take the old node as a whole and copy it to new new path,
+ # additional migrations will be done afterwards
+ new_base = base + [interface]
+ c.copy(old_base, new_base)
+ c.delete(old_base)
+
+ # cur-hop-limit has been renamed to hop-limit
+ if c.exists(new_base + ['cur-hop-limit']):
+ c.rename(new_base + ['cur-hop-limit'], 'hop-limit')
+
+ bool_cleanup = ['managed-flag', 'other-config-flag']
+ for bool in bool_cleanup:
+ if c.exists(new_base + [bool]):
+ tmp = c.return_value(new_base + [bool])
+ c.delete(new_base + [bool])
+ if tmp == 'true':
+ c.set(new_base + [bool])
+
+ # max/min interval moved to subnode
+ intervals = ['max-interval', 'min-interval']
+ for interval in intervals:
+ if c.exists(new_base + [interval]):
+ tmp = c.return_value(new_base + [interval])
+ c.delete(new_base + [interval])
+ min_max = interval.split('-')[0]
+ c.set(new_base + ['interval', min_max], value=tmp)
+
+ # cleanup boolean nodes in individual prefix
+ prefix_base = new_base + ['prefix']
+ if c.exists(prefix_base):
+ for prefix in config.list_nodes(prefix_base):
+ bool_cleanup = ['autonomous-flag', 'on-link-flag']
+ for bool in bool_cleanup:
+ if c.exists(prefix_base + [prefix, bool]):
+ tmp = c.return_value(prefix_base + [prefix, bool])
+ c.delete(prefix_base + [prefix, bool])
+ if tmp == 'true':
+ c.set(prefix_base + [prefix, bool])
+
+ # router advertisement can be individually disabled per interface
+ # the node has been renamed from send-advert {true | false} to no-send-advert
+ if c.exists(new_base + ['send-advert']):
+ tmp = c.return_value(new_base + ['send-advert'])
+ c.delete(new_base + ['send-advert'])
+ if tmp == 'false':
+ c.set(new_base + ['no-send-advert'])
+
+ # link-mtu advertisement was formerly disabled by setting its value to 0
+ # ... this makes less sense - if it should not be send, just do not
+ # configure it
+ if c.exists(new_base + ['link-mtu']):
+ tmp = c.return_value(new_base + ['link-mtu'])
+ if tmp == '0':
+ c.delete(new_base + ['link-mtu'])
+
+if __name__ == '__main__':
+ if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+ file_name = sys.argv[1]
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+
+ config = ConfigTree(config_file)
+
+ # list all individual interface types like dummy, ethernet and so on
+ for if_type in config.list_nodes(['interfaces']):
+ base_if_type = ['interfaces', if_type]
+
+ # for every individual interface we need to check if there is an
+ # ipv6 ra configured ... and also for every VIF (VLAN) interface
+ for intf in config.list_nodes(base_if_type):
+ old_base = base_if_type + [intf, 'ipv6', 'router-advert']
+ copy_rtradv(config, old_base, intf)
+
+ vif_base = base_if_type + [intf, 'vif']
+ if config.exists(vif_base):
+ for vif in config.list_nodes(vif_base):
+ old_base = vif_base + [vif, 'ipv6', 'router-advert']
+ vlan_name = f'{intf}.{vif}'
+ copy_rtradv(config, old_base, vlan_name)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/interfaces/6-to-7 b/src/migration-scripts/interfaces/6-to-7
new file mode 100755
index 000000000..220c7e601
--- /dev/null
+++ b/src/migration-scripts/interfaces/6-to-7
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+# Remove network provider name from CLI and rather use provider APN from CLI
+
+import sys
+from vyos.configtree import ConfigTree
+
+if __name__ == '__main__':
+ if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+ file_name = sys.argv[1]
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+
+ config = ConfigTree(config_file)
+ base = ['interfaces', 'wirelessmodem']
+
+ if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+ # list all individual wwan/wireless modem interfaces
+ for i in config.list_nodes(base):
+ iface = base + [i]
+
+ # only three carries have been supported in the past, thus
+ # this will be fairly simple \o/ - and only one (AT&T) did
+ # configure an APN
+ if config.exists(iface + ['network']):
+ network = config.return_value(iface + ['network'])
+ if network == "att":
+ apn = 'isp.cingular'
+ config.set(iface + ['apn'], value=apn)
+
+ config.delete(iface + ['network'])
+
+ # synchronize DNS configuration with PPPoE interfaces to have a
+ # uniform CLI experience
+ if config.exists(iface + ['no-dns']):
+ config.rename(iface + ['no-dns'], 'no-peer-dns')
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/interfaces/7-to-8 b/src/migration-scripts/interfaces/7-to-8
new file mode 100755
index 000000000..8830ffdc7
--- /dev/null
+++ b/src/migration-scripts/interfaces/7-to-8
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+# Split WireGuard endpoint into address / port nodes to make use of common
+# validators
+
+from sys import exit, argv
+from vyos.configtree import ConfigTree
+
+if __name__ == '__main__':
+ if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+ file_name = argv[1]
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+
+ config = ConfigTree(config_file)
+ base = ['interfaces', 'wireguard']
+
+ if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+ # list all individual wireguard interface isntance
+ for i in config.list_nodes(base):
+ iface = base + [i]
+ for peer in config.list_nodes(iface + ['peer']):
+ base_peer = iface + ['peer', peer]
+ if config.exists(base_peer + ['endpoint']):
+ endpoint = config.return_value(base_peer + ['endpoint'])
+ address = endpoint.split(':')[0]
+ port = endpoint.split(':')[1]
+ # delete old node
+ config.delete(base_peer + ['endpoint'])
+ # setup new nodes
+ config.set(base_peer + ['address'], value=address)
+ config.set(base_peer + ['port'], value=port)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/interfaces/8-to-9 b/src/migration-scripts/interfaces/8-to-9
new file mode 100755
index 000000000..e0b9dd375
--- /dev/null
+++ b/src/migration-scripts/interfaces/8-to-9
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+# Rename link nodes to source-interface for the following interface types:
+# - vxlan
+# - pseudo ethernet
+
+from sys import exit, argv
+from vyos.configtree import ConfigTree
+
+if __name__ == '__main__':
+ if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+ file_name = argv[1]
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+
+ config = ConfigTree(config_file)
+
+ for if_type in ['vxlan', 'pseudo-ethernet']:
+ base = ['interfaces', if_type]
+ if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+ # list all individual interface isntance
+ for i in config.list_nodes(base):
+ iface = base + [i]
+ if config.exists(iface + ['link']):
+ config.rename(iface + ['link'], 'source-interface')
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/quagga/3-to-4 b/src/migration-scripts/quagga/3-to-4
index f8c87ce8c..be3528391 100755
--- a/src/migration-scripts/quagga/3-to-4
+++ b/src/migration-scripts/quagga/3-to-4
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2020 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
@@ -16,6 +16,13 @@
#
#
+# Between 1.2.3 and 1.2.4, FRR added per-neighbor enforce-first-as option.
+# Unfortunately they also removed the global enforce-first-as option,
+# which broke all old configs that used to have it.
+#
+# To emulate the effect of the original option, we insert it in every neighbor
+# if the config used to have the original global option
+
import sys
from vyos.configtree import ConfigTree
@@ -45,11 +52,16 @@ else:
# There's actually no BGP, just its empty shell
sys.exit(0)
- # Check if BGP scan-time parameter exist
- scan_time_param = ['protocols', 'bgp', asn, 'parameters', 'scan-time']
- if config.exists(scan_time_param):
- # Delete BGP scan-time parameter
- config.delete(scan_time_param)
+ # Check if BGP enforce-first-as option is set
+ enforce_first_as_path = ['protocols', 'bgp', asn, 'parameters', 'enforce-first-as']
+ if config.exists(enforce_first_as_path):
+ # Delete the obsolete option
+ config.delete(enforce_first_as_path)
+
+ # Now insert it in every peer
+ peers = config.list_nodes(['protocols', 'bgp', asn, 'neighbor'])
+ for p in peers:
+ config.set(['protocols', 'bgp', asn, 'neighbor', p, 'enforce-first-as'])
else:
# Do nothing
sys.exit(0)
@@ -61,3 +73,4 @@ else:
except OSError as e:
print("Failed to save the modified config: {}".format(e))
sys.exit(1)
+
diff --git a/src/migration-scripts/quagga/4-to-5 b/src/migration-scripts/quagga/4-to-5
new file mode 100755
index 000000000..f8c87ce8c
--- /dev/null
+++ b/src/migration-scripts/quagga/4-to-5
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 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 sys
+
+from vyos.configtree import ConfigTree
+
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+if not config.exists(['protocols', 'bgp']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Check if BGP is actually configured and obtain the ASN
+ asn_list = config.list_nodes(['protocols', 'bgp'])
+ if asn_list:
+ # There's always just one BGP node, if any
+ asn = asn_list[0]
+ else:
+ # There's actually no BGP, just its empty shell
+ sys.exit(0)
+
+ # Check if BGP scan-time parameter exist
+ scan_time_param = ['protocols', 'bgp', asn, 'parameters', 'scan-time']
+ if config.exists(scan_time_param):
+ # Delete BGP scan-time parameter
+ config.delete(scan_time_param)
+ else:
+ # Do nothing
+ sys.exit(0)
+
+ # Save a new configuration file
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/quagga/5-to-6 b/src/migration-scripts/quagga/5-to-6
new file mode 100755
index 000000000..a71851942
--- /dev/null
+++ b/src/migration-scripts/quagga/5-to-6
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+# * Remove parameter 'disable-network-import-check' which, as implemented,
+# had no effect on boot.
+
+import sys
+
+from vyos.configtree import ConfigTree
+
+
+if (len(sys.argv) < 2):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+if not config.exists(['protocols', 'bgp']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Check if BGP is actually configured and obtain the ASN
+ asn_list = config.list_nodes(['protocols', 'bgp'])
+ if asn_list:
+ # There's always just one BGP node, if any
+ asn = asn_list[0]
+ else:
+ # There's actually no BGP, just its empty shell
+ sys.exit(0)
+
+ # Check if BGP parameter disable-network-import-check exists
+ param = ['protocols', 'bgp', asn, 'parameters', 'disable-network-import-check']
+ if config.exists(param):
+ # Delete parameter
+ config.delete(param)
+ else:
+ # Do nothing
+ sys.exit(0)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/sstp/0-to-1 b/src/migration-scripts/sstp/0-to-1
new file mode 100755
index 000000000..0e8dd1c4b
--- /dev/null
+++ b/src/migration-scripts/sstp/0-to-1
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+
+# - migrate from "service sstp-server" to "vpn sstp"
+# - remove primary/secondary identifier from nameserver
+# - migrate RADIUS configuration to a more uniform syntax accross the system
+# - authentication radius-server x.x.x.x to authentication radius server x.x.x.x
+# - authentication radius-settings to authentication radius
+# - do not migrate radius server req-limit, use default of unlimited
+# - migrate SSL certificate path
+
+import os
+import sys
+
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+old_base = ['service', 'sstp-server']
+if not config.exists(old_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # ensure new base path exists
+ if not config.exists(['vpn']):
+ config.set(['vpn'])
+
+ new_base = ['vpn', 'sstp']
+ # copy entire tree
+ config.copy(old_base, new_base)
+ config.delete(old_base)
+
+ # migrate DNS servers
+ dns_base = new_base + ['network-settings', 'dns-server']
+ if config.exists(dns_base):
+ if config.exists(dns_base + ['primary-dns']):
+ dns = config.return_value(dns_base + ['primary-dns'])
+ config.set(new_base + ['network-settings', 'name-server'], value=dns, replace=False)
+
+ if config.exists(dns_base + ['secondary-dns']):
+ dns = config.return_value(dns_base + ['secondary-dns'])
+ config.set(new_base + ['network-settings', 'name-server'], value=dns, replace=False)
+
+ config.delete(dns_base)
+
+
+ # migrate radius options - copy subtree
+ # thus must happen before migration of the individual RADIUS servers
+ old_options = new_base + ['authentication', 'radius-settings']
+ if config.exists(old_options):
+ new_options = new_base + ['authentication', 'radius']
+ config.copy(old_options, new_options)
+ config.delete(old_options)
+
+ # migrate radius dynamic author / change of authorisation server
+ dae_old = new_base + ['authentication', 'radius', 'dae-server']
+ if config.exists(dae_old):
+ config.rename(dae_old, 'dynamic-author')
+ dae_new = new_base + ['authentication', 'radius', 'dynamic-author']
+
+ if config.exists(dae_new + ['ip-address']):
+ config.rename(dae_new + ['ip-address'], 'server')
+
+ if config.exists(dae_new + ['secret']):
+ config.rename(dae_new + ['secret'], 'key')
+
+
+ # migrate radius server
+ radius_server = new_base + ['authentication', 'radius-server']
+ if config.exists(radius_server):
+ for server in config.list_nodes(radius_server):
+ base = radius_server + [server]
+ new = new_base + ['authentication', 'radius', 'server', server]
+
+ # convert secret to key
+ if config.exists(base + ['secret']):
+ tmp = config.return_value(base + ['secret'])
+ config.set(new + ['key'], value=tmp)
+
+ if config.exists(base + ['fail-time']):
+ tmp = config.return_value(base + ['fail-time'])
+ config.set(new + ['fail-time'], value=tmp)
+
+ config.set_tag(new_base + ['authentication', 'radius', 'server'])
+ config.delete(radius_server)
+
+ # migrate SSL certificates
+ old_ssl = new_base + ['sstp-settings', 'ssl-certs']
+ new_ssl = new_base + ['ssl']
+ config.copy(old_ssl, new_ssl)
+ config.delete(old_ssl)
+
+ if config.exists(new_ssl + ['ca']):
+ config.rename(new_ssl + ['ca'], 'ca-cert-file')
+
+ if config.exists(new_ssl + ['server-cert']):
+ config.rename(new_ssl + ['server-cert'], 'cert-file')
+
+ if config.exists(new_ssl + ['server-key']):
+ config.rename(new_ssl + ['server-key'], 'key-file')
+
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/sstp/1-to-2 b/src/migration-scripts/sstp/1-to-2
new file mode 100755
index 000000000..94cb04831
--- /dev/null
+++ b/src/migration-scripts/sstp/1-to-2
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+# - migrate relative path SSL certificate to absolute path, as certs are only
+# allowed to stored in /config/user-data/sstp/ this is pretty straight
+# forward move. Delete certificates from source directory
+
+import os
+import sys
+
+from shutil import copy2
+from stat import S_IRUSR, S_IWUSR, S_IRGRP, S_IROTH
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base_path = ['vpn', 'sstp', 'ssl']
+if not config.exists(base_path):
+ # Nothing to do
+ sys.exit(0)
+else:
+ cert_path_old ='/config/user-data/sstp/'
+ cert_path_new ='/config/auth/sstp/'
+
+ if not os.path.isdir(cert_path_new):
+ os.mkdir(cert_path_new)
+
+ #
+ # migrate ca-cert-file to new path
+ if config.exists(base_path + ['ca-cert-file']):
+ tmp = config.return_value(base_path + ['ca-cert-file'])
+ cert_old = cert_path_old + tmp
+ cert_new = cert_path_new + tmp
+
+ if os.path.isfile(cert_old):
+ # adjust file permissions on source file,
+ # permissions will be copied by copy2()
+ os.chmod(cert_old, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
+ copy2(cert_old, cert_path_new)
+ # delete old certificate file
+ os.unlink(cert_old)
+
+ config.set(base_path + ['ca-cert-file'], value=cert_new, replace=True)
+
+ #
+ # migrate cert-file to new path
+ if config.exists(base_path + ['cert-file']):
+ tmp = config.return_value(base_path + ['cert-file'])
+ cert_old = cert_path_old + tmp
+ cert_new = cert_path_new + tmp
+
+ if os.path.isfile(cert_old):
+ # adjust file permissions on source file,
+ # permissions will be copied by copy2()
+ os.chmod(cert_old, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
+ copy2(cert_old, cert_path_new)
+ # delete old certificate file
+ os.unlink(cert_old)
+
+ config.set(base_path + ['cert-file'], value=cert_new, replace=True)
+
+ #
+ # migrate key-file to new path
+ if config.exists(base_path + ['key-file']):
+ tmp = config.return_value(base_path + ['key-file'])
+ cert_old = cert_path_old + tmp
+ cert_new = cert_path_new + tmp
+
+ if os.path.isfile(cert_old):
+ # adjust file permissions on source file,
+ # permissions will be copied by copy2()
+ os.chmod(cert_old, S_IRUSR | S_IWUSR)
+ copy2(cert_old, cert_path_new)
+ # delete old certificate file
+ os.unlink(cert_old)
+
+ config.set(base_path + ['key-file'], value=cert_new, replace=True)
+
+ #
+ # check if old certificate directory exists but is empty
+ if os.path.isdir(cert_path_old) and not os.listdir(cert_path_old):
+ os.rmdir(cert_path_old)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/system/12-to-13 b/src/migration-scripts/system/12-to-13
index 5f7413d46..5b068f4fc 100755
--- a/src/migration-scripts/system/12-to-13
+++ b/src/migration-scripts/system/12-to-13
@@ -12,12 +12,8 @@ import re
import sys
from vyos.configtree import ConfigTree
-from subprocess import Popen, PIPE, STDOUT
+from vyos.util import cmd
-def _cmd(cmd):
- p = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True)
- tmp = p.communicate()[0].strip()
- return tmp.decode()
if (len(sys.argv) < 1):
print("Must specify file name!")
@@ -37,8 +33,11 @@ else:
tz = config.return_value(tz_base)
# retrieve all valid timezones
- tz_data = _cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::')
- tz_data = tz_data.split('\n')
+ try:
+ tz_datas = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::')
+ except OSError:
+ tz_datas = ''
+ tz_data = tz_datas.split('\n')
if re.match(r'[Ll][Oo][Ss].+', tz):
tz = 'America/Los_Angeles'
diff --git a/src/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py
index 0e52b9086..423694187 100755
--- a/src/op_mode/clear_conntrack.py
+++ b/src/op_mode/clear_conntrack.py
@@ -14,13 +14,13 @@
# 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 subprocess
import sys
from vyos.util import ask_yes_no
+from vyos.util import cmd, DEVNULL
if not ask_yes_no("This will clear all currently tracked and expected connections. Continue?"):
sys.exit(1)
else:
- subprocess.check_call(['/usr/sbin/conntrack -F'], shell=True, stderr=subprocess.DEVNULL)
- subprocess.check_call(['/usr/sbin/conntrack -F expect'], shell=True, stderr=subprocess.DEVNULL)
+ cmd('/usr/sbin/conntrack -F', stderr=DEVNULL)
+ cmd('/usr/sbin/conntrack -F expect', stderr=DEVNULL)
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py
index a22615096..b191f630d 100755
--- a/src/op_mode/connect_disconnect.py
+++ b/src/op_mode/connect_disconnect.py
@@ -21,6 +21,9 @@ from sys import exit
from psutil import process_iter
from time import strftime, localtime, time
+from vyos.util import call
+
+
PPP_LOGFILE = '/var/log/vyatta/ppp_{}.log'
def check_interface(interface):
@@ -56,8 +59,7 @@ def connect(interface):
tm = strftime("%a %d %b %Y %I:%M:%S %p %Z", localtime(time()))
with open(PPP_LOGFILE.format(interface), 'a') as f:
f.write('{}: user {} started PPP daemon for {} by connect command\n'.format(tm, user, interface))
- cmd = 'umask 0; setsid sh -c "nohup /usr/sbin/pppd call {0} > /tmp/{0}.log 2>&1 &"'.format(interface)
- os.system(cmd)
+ call('umask 0; setsid sh -c "nohup /usr/sbin/pppd call {0} > /tmp/{0}.log 2>&1 &"'.format(interface))
def disconnect(interface):
@@ -75,8 +77,7 @@ def disconnect(interface):
tm = strftime("%a %d %b %Y %I:%M:%S %p %Z", localtime(time()))
with open(PPP_LOGFILE.format(interface), 'a') as f:
f.write('{}: user {} stopped PPP daemon for {} by disconnect command\n'.format(tm, user, interface))
- cmd = '/usr/bin/poff "{}"'.format(interface)
- os.system(cmd)
+ call('/usr/bin/poff "{}"'.format(interface))
def main():
parser = argparse.ArgumentParser()
diff --git a/src/op_mode/dns_forwarding_reset.py b/src/op_mode/dns_forwarding_reset.py
index da4fba3a2..8e2ee546c 100755
--- a/src/op_mode/dns_forwarding_reset.py
+++ b/src/op_mode/dns_forwarding_reset.py
@@ -21,10 +21,11 @@
import os
-import sys
import argparse
-import vyos.config
+from sys import exit
+from vyos.config import Config
+from vyos.util import call
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--all", action="store_true", help="Reset all cache")
@@ -34,16 +35,18 @@ if __name__ == '__main__':
args = parser.parse_args()
# Do nothing if service is not configured
- c = vyos.config.Config()
- if not c.exists_effective('service dns forwarding'):
+ c = Config()
+ if not c.exists_effective(['service', 'dns', 'forwarding']):
print("DNS forwarding is not configured")
- sys.exit(0)
+ exit(0)
if args.all:
- os.system("rec_control wipe-cache \'.$\'")
- sys.exit(1)
+ call("rec_control wipe-cache \'.$\'")
+ exit(0)
+
elif args.domain:
- os.system("rec_control wipe-cache \'{0}$\'".format(args.domain))
+ call("rec_control wipe-cache \'{0}$\'".format(args.domain))
+
else:
parser.print_help()
- sys.exit(1)
+ exit(1)
diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py
index f626244a8..c400a72cd 100755
--- a/src/op_mode/dns_forwarding_statistics.py
+++ b/src/op_mode/dns_forwarding_statistics.py
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
-import subprocess
import jinja2
import sys
from vyos.config import Config
+from vyos.config import cmd
PDNS_CMD='/usr/bin/rec_control'
@@ -26,8 +26,8 @@ if __name__ == '__main__':
data = {}
- data['cache_entries'] = subprocess.check_output([PDNS_CMD, 'get cache-entries']).decode()
- data['cache_size'] = "{0:.2f}".format( int(subprocess.check_output([PDNS_CMD, 'get cache-bytes']).decode()) / 1024 )
+ data['cache_entries'] = cmd(f'{PDNS_CMD} get cache-entries')
+ data['cache_size'] = "{0:.2f}".format( int(cmd(f'{PDNS_CMD} get cache-bytes')) / 1024 )
tmpl = jinja2.Template(OUT_TMPL_SRC)
print(tmpl.render(data))
diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py
index 0d457e247..405dd9f04 100755
--- a/src/op_mode/dynamic_dns.py
+++ b/src/op_mode/dynamic_dns.py
@@ -21,6 +21,8 @@ import sys
import time
from vyos.config import Config
+from vyos.util import call
+
cache_file = r'/var/cache/ddclient/ddclient.cache'
@@ -84,9 +86,9 @@ def show_status():
def update_ddns():
- os.system('systemctl stop ddclient')
+ call('systemctl stop ddclient')
os.remove(cache_file)
- os.system('systemctl start ddclient')
+ call('systemctl start ddclient')
def main():
diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py
index a39eaf871..7f3ad7476 100755
--- a/src/op_mode/flow_accounting_op.py
+++ b/src/op_mode/flow_accounting_op.py
@@ -19,10 +19,11 @@ import sys
import argparse
import re
import ipaddress
-import subprocess
import os.path
from tabulate import tabulate
+from vyos.util import cmd, run
+
# some default values
uacctd_pidfile = '/var/run/uacctd.pid'
uacctd_pipefile = '/tmp/uacctd.pipe'
@@ -69,26 +70,16 @@ def _is_host(host):
# check if flow-accounting running
def _uacctd_running():
- command = "/usr/bin/sudo /bin/systemctl status uacctd > /dev/null"
- return_code = subprocess.call(command, shell=True)
- if not return_code == 0:
- return False
-
- # return True if all checks were passed
- return True
+ command = '/usr/bin/sudo /bin/systemctl status uacctd > /dev/null'
+ return run(command) == 0
# get list of interfaces
def _get_ifaces_dict():
# run command to get ifaces list
- command = "/bin/ip link show"
- process = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, universal_newlines=True)
- stdout, stderr = process.communicate()
- if not process.returncode == 0:
- print("Failed to get interfaces list: command \"{}\" returned exit code: {}".format(command, process.returncode))
- sys.exit(1)
+ out = cmd('/bin/ip link show', universal_newlines=True)
# read output
- ifaces_out = stdout.splitlines()
+ ifaces_out = out.splitlines()
# make a dictionary with interfaces and indexes
ifaces_dict = {}
@@ -103,15 +94,12 @@ def _get_ifaces_dict():
# get list of flows
def _get_flows_list():
# run command to get flows list
- command = "/usr/bin/pmacct -s -O json -T flows -p {}".format(uacctd_pipefile)
- process = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, universal_newlines=True)
- stdout, stderr = process.communicate()
- if not process.returncode == 0:
- print("Failed to get flows list: command \"{}\" returned exit code: {}\nError: {}".format(command, process.returncode, stderr))
- sys.exit(1)
+ out = cmd(f'/usr/bin/pmacct -s -O json -T flows -p {uacctd_pipefile}',
+ universal_newlines=True,
+ message='Failed to get flows list')
# read output
- flows_out = stdout.splitlines()
+ flows_out = out.splitlines()
# make a list with flows
flows_list = []
@@ -208,21 +196,15 @@ if not _uacctd_running():
# restart pmacct daemon
if cmd_args.action == 'restart':
# run command to restart flow-accounting
- command = '/usr/bin/sudo /bin/systemctl restart uacctd'
- return_code = subprocess.call(command.split(' '))
- if not return_code == 0:
- print("Failed to restart flow-accounting: command \"{}\" returned exit code: {}".format(command, return_code))
- sys.exit(1)
+ cmd('/usr/bin/sudo /bin/systemctl restart uacctd',
+ message='Failed to restart flow-accounting')
# clear in-memory collected flows
if cmd_args.action == 'clear':
_check_imt()
# run command to clear flows
- command = "/usr/bin/pmacct -e -p {}".format(uacctd_pipefile)
- return_code = subprocess.call(command.split(' '))
- if not return_code == 0:
- print("Failed to clear flows: command \"{}\" returned exit code: {}".format(command, return_code))
- sys.exit(1)
+ cmd(f'/usr/bin/pmacct -e -p {uacctd_pipefile}',
+ message='Failed to clear flows')
# show table with flows
if cmd_args.action == 'show':
diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py
index 5a3b250ee..df4486bce 100755
--- a/src/op_mode/format_disk.py
+++ b/src/op_mode/format_disk.py
@@ -17,13 +17,14 @@
import argparse
import os
import re
-import subprocess
import sys
from datetime import datetime
from time import sleep
from vyos.util import is_admin, ask_yes_no
-
+from vyos.util import call
+from vyos.util import cmd
+from vyos.util import DEVNULL
def list_disks():
disks = set()
@@ -37,10 +38,7 @@ def list_disks():
def is_busy(disk: str):
"""Check if given disk device is busy by re-reading it's partition table"""
-
- cmd = 'sudo blockdev --rereadpt /dev/{}'.format(disk)
- status = subprocess.call([cmd], shell=True, stderr=subprocess.DEVNULL)
- return status != 0
+ return call(f'sudo blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0
def backup_partitions(disk: str):
@@ -49,8 +47,7 @@ def backup_partitions(disk: str):
device_path = '/dev/' + disk
backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M')
backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts)
- cmd = 'sudo /sbin/sfdisk -d {} > {}'.format(device_path, backup_file)
- subprocess.check_call([cmd], shell=True)
+ cmd(f'sudo /sbin/sfdisk -d {device_path} > {backup_file}')
def list_partitions(disk: str):
@@ -68,13 +65,11 @@ def list_partitions(disk: str):
def delete_partition(disk: str, partition_idx: int):
- cmd = 'sudo /sbin/parted /dev/{} rm {}'.format(disk, partition_idx)
- subprocess.check_call([cmd], shell=True)
+ cmd(f'sudo /sbin/parted /dev/{disk} rm {partition_idx}')
def format_disk_like(target: str, proto: str):
- cmd = 'sudo /sbin/sfdisk -d /dev/{} | sudo /sbin/sfdisk --force /dev/{}'.format(proto, target)
- subprocess.check_call([cmd], shell=True)
+ cmd(f'sudo /sbin/sfdisk -d /dev/{proto} | sudo /sbin/sfdisk --force /dev/{target}')
if __name__ == '__main__':
diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py
index f205919b8..f65d383c0 100755
--- a/src/op_mode/generate_ssh_server_key.py
+++ b/src/op_mode/generate_ssh_server_key.py
@@ -14,14 +14,14 @@
# 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 subprocess
import sys
from vyos.util import ask_yes_no
+from vyos.util import cmd
if not ask_yes_no('Do you really want to remove the existing SSH host keys?'):
sys.exit(0)
-else:
- subprocess.check_call(['sudo rm -v /etc/ssh/ssh_host_*'], shell=True)
- subprocess.check_call(['sudo dpkg-reconfigure openssh-server'], shell=True)
- subprocess.check_call(['sudo systemctl restart ssh'], shell=True)
+
+cmd('sudo rm -v /etc/ssh/ssh_host_*')
+cmd('sudo dpkg-reconfigure openssh-server')
+cmd('sudo systemctl restart ssh')
diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py
index 4d8fdbc99..5d48e3210 100755
--- a/src/op_mode/lldp_op.py
+++ b/src/op_mode/lldp_op.py
@@ -20,9 +20,11 @@ import jinja2
from xml.dom import minidom
from sys import exit
-from subprocess import Popen, PIPE, STDOUT
from tabulate import tabulate
+from vyos.util import popen
+from vyos.config import Config
+
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces")
parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface")
@@ -40,9 +42,8 @@ Device ID Local Proto Cap Platform Port ID
def _get_neighbors():
command = '/usr/sbin/lldpcli -f xml show neighbors'
- p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
- tmp = p.communicate()[0].strip()
- return tmp.decode()
+ out,_ = popen(command)
+ return out
def extract_neighbor(neighbor):
"""
@@ -141,6 +142,11 @@ if __name__ == '__main__':
args = parser.parse_args()
tmp = { 'neighbors' : [] }
+ c = Config()
+ if not c.exists_effective(['service', 'lldp']):
+ print('Service LLDP is not configured')
+ exit(0)
+
if args.all:
neighbors = minidom.parseString(_get_neighbors())
for neighbor in neighbors.getElementsByTagName('interface'):
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index 54fc12be3..0f3619411 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -17,12 +17,13 @@
import os
import sys
import argparse
-import subprocess
import re
from datetime import datetime, timedelta, time as type_time, date as type_date
-from subprocess import check_output, CalledProcessError, STDOUT
from vyos.util import ask_yes_no
+from vyos.util import cmd
+from vyos.util import call
+from vyos.util import run
systemd_sched_file = "/run/systemd/shutdown/scheduled"
@@ -45,23 +46,20 @@ def parse_date(s):
return None
def get_shutdown_status():
- try:
- if os.path.exists(systemd_sched_file):
- # Get scheduled from systemd file
- with open(systemd_sched_file, 'r') as f:
- data = f.read().rstrip('\n')
- r_data = {}
- for line in data.splitlines():
- tmp_split = line.split("=")
- if tmp_split[0] == "USEC":
- # Convert USEC to human readable format
- r_data['DATETIME'] = datetime.utcfromtimestamp(int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S')
- else:
- r_data[tmp_split[0]] = tmp_split[1]
- return r_data
- return None
- except CalledProcessError:
- return None
+ if os.path.exists(systemd_sched_file):
+ # Get scheduled from systemd file
+ with open(systemd_sched_file, 'r') as f:
+ data = f.read().rstrip('\n')
+ r_data = {}
+ for line in data.splitlines():
+ tmp_split = line.split("=")
+ if tmp_split[0] == "USEC":
+ # Convert USEC to human readable format
+ r_data['DATETIME'] = datetime.utcfromtimestamp(int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S')
+ else:
+ r_data[tmp_split[0]] = tmp_split[1]
+ return r_data
+ return None
def check_shutdown():
output = get_shutdown_status()
@@ -76,13 +74,13 @@ def check_shutdown():
def cancel_shutdown():
output = get_shutdown_status()
if output and 'MODE' in output:
+ timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
try:
- timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- cmd = check_output(["/sbin/shutdown","-c","--no-wall"])
- message = "Scheduled %s has been cancelled %s" % (output['MODE'], timenow)
- os.system("wall %s" % message)
- except CalledProcessError as e:
+ cmd('/sbin/shutdown -c --no-wall')
+ except OSError as e:
sys.exit("Could not cancel a reboot or poweroff: %s" % e)
+ message = "Scheduled %s has been cancelled %s" % (output['MODE'], timenow)
+ run(f'wall {message}')
else:
print("Reboot or poweroff is not scheduled")
@@ -99,14 +97,14 @@ def execute_shutdown(time, reboot = True, ask=True):
chk_vyatta_based_reboots()
###
- cmd = check_output(["/sbin/shutdown",action,"now"],stderr=STDOUT)
- print(cmd.decode().split(",",1)[0])
+ out = cmd(f'/sbin/shutdown {action} now')
+ print(out.split(",",1)[0])
return
elif len(time) == 1:
# Assume the argument is just time
ts = parse_time(time[0])
if ts:
- cmd = check_output(["/sbin/shutdown", action, time[0]], stderr=STDOUT)
+ cmd(f'/sbin/shutdown {action} {time[0]}')
else:
sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
elif len(time) == 2:
@@ -117,7 +115,7 @@ def execute_shutdown(time, reboot = True, ask=True):
t = datetime.combine(ds, ts)
td = t - datetime.now()
t2 = 1 + int(td.total_seconds())//60 # Get total minutes
- cmd = check_output(["/sbin/shutdown", action, str(t2)], stderr=STDOUT)
+ cmd('/sbin/shutdown {action} {t2}')
else:
if not ts:
sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
@@ -136,7 +134,7 @@ def chk_vyatta_based_reboots():
if os.path.exists(f):
jid = open(f).read().strip()
if jid != 0:
- subprocess.call(['sudo', 'atrm', jid])
+ call(f'sudo atrm {jid}')
os.remove(f)
def main():
diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py
index 7043ac261..618cad5ea 100755
--- a/src/op_mode/reset_openvpn.py
+++ b/src/op_mode/reset_openvpn.py
@@ -17,10 +17,9 @@
import sys
import os
-from psutil import pid_exists
-from subprocess import Popen, PIPE
from time import sleep
from netifaces import interfaces
+from vyos.util import process_running, cmd
def get_config_name(intf):
cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf)
@@ -30,9 +29,6 @@ def get_pid_file(intf):
pid_file = r'/var/run/openvpn/{}.pid'.format(intf)
return pid_file
-def subprocess_cmd(command):
- p = Popen(command, stdout=PIPE, shell=True)
- p.communicate()
if __name__ == '__main__':
if (len(sys.argv) < 1):
@@ -42,15 +38,13 @@ if __name__ == '__main__':
interface = sys.argv[1]
if os.path.isfile(get_config_name(interface)):
pidfile = '/var/run/openvpn/{}.pid'.format(interface)
- if os.path.isfile(pidfile):
- pid = 0
- with open(pidfile, 'r') as f:
- pid = int(f.read())
-
- if pid_exists(pid):
- cmd = 'start-stop-daemon --stop --quiet'
- cmd += ' --pidfile ' + pidfile
- subprocess_cmd(cmd)
+ if process_running(pidfile):
+ command = 'start-stop-daemon'
+ command += ' --stop'
+ command += ' --oknodo'
+ command += ' --quiet'
+ command += ' --pidfile ' + pidfile
+ cmd(command)
# When stopping OpenVPN we need to wait for the 'old' interface to
# vanish from the Kernel, if it is not gone, OpenVPN will report:
@@ -59,14 +53,18 @@ if __name__ == '__main__':
sleep(0.250) # 250ms
# re-start OpenVPN process
- cmd = 'start-stop-daemon --start --quiet'
- cmd += ' --pidfile ' + get_pid_file(interface)
- cmd += ' --exec /usr/sbin/openvpn'
+ command = 'start-stop-daemon'
+ command += ' --start'
+ command += ' --oknodo'
+ command += ' --quiet'
+ command += ' --pidfile ' + get_pid_file(interface)
+ command += ' --exec /usr/sbin/openvpn'
# now pass arguments to openvpn binary
- cmd += ' --'
- cmd += ' --config ' + get_config_name(interface)
+ command += ' --'
+ command += ' --daemon openvpn-' + interface
+ command += ' --config ' + get_config_name(interface)
- subprocess_cmd(cmd)
+ cmd(command)
else:
print("OpenVPN interface {} does not exist!".format(interface))
sys.exit(1)
diff --git a/src/op_mode/reset_vpn.py b/src/op_mode/reset_vpn.py
index 52677b58d..8962df212 100755
--- a/src/op_mode/reset_vpn.py
+++ b/src/op_mode/reset_vpn.py
@@ -16,52 +16,54 @@
# import os
import sys
-import subprocess
import argparse
#import re
-pptp_cmd = ["/usr/bin/accel-cmd", "-p 2003"]
-l2tp_cmd = ["/usr/bin/accel-cmd", "-p 2004"]
+from vyos.util import run
+from vyos.util import DEVNULL
+
+pptp_base = '/usr/bin/accel-cmd -p 2003 terminate {} {}'
+l2tp_base = '/usr/bin/accel-cmd -p 2004 terminate {} {}'
def terminate_sessions(username='', interface='', protocol=''):
if username:
if username == "all_users":
if protocol == "pptp":
- pptp_cmd.append("terminate all")
- subprocess.call(pptp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ pptp_cmd = pptp_base.format('all','')
+ run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL)
return
elif protocol == "l2tp":
- l2tp_cmd.append("terminate all")
- subprocess.call(l2tp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ l2tp_cmd = l2tp_base.format('all', '')
+ run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL)
return
else:
- pptp_cmd.append("terminate all")
- subprocess.call(pptp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
- l2tp_cmd.append("terminate all")
- subprocess.call(l2tp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ pptp_cmd = pptp_base.format('all', '')
+ run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL)
+ l2tp_cmd = l2tp_base.format('all', '')
+ run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL)
return
if protocol == "pptp":
- pptp_cmd.append("terminate username {0}".format(username))
- subprocess.call(pptp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ pptp_cmd = pptp_base.format('username', username)
+ run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL)
return
elif protocol == "l2tp":
- l2tp_cmd.append("terminate username {0}".format(username))
- subprocess.call(l2tp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ l2tp_cmd = l2tp_base.format('username', username)
+ run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL)
return
else:
- pptp_cmd.append("terminate username {0}".format(username))
- subprocess.call(pptp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ pptp_cmd = pptp_base.format('username', username)
+ run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL)
l2tp_cmd.append("terminate username {0}".format(username))
- subprocess.call(l2tp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL)
return
# rewrite `terminate by interface` if pptp will have pptp%d interface naming
if interface:
- pptp_cmd.append("terminate if {0}".format(interface))
- subprocess.call(pptp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
- l2tp_cmd.append("terminate if {0}".format(interface))
- subprocess.call(l2tp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ pptp_cmd = pptp_base.format('if', interface)
+ run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL)
+ l2tp_cmd = l2tp_base.format('if', interface)
+ run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL)
def main():
diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py
index ab02d1eb3..66dc435b3 100755
--- a/src/op_mode/restart_dhcp_relay.py
+++ b/src/op_mode/restart_dhcp_relay.py
@@ -23,6 +23,8 @@ import argparse
import os
import vyos.config
+from vyos.util import call
+
parser = argparse.ArgumentParser()
parser.add_argument("--ipv4", action="store_true", help="Restart IPv4 DHCP relay")
@@ -37,7 +39,7 @@ if __name__ == '__main__':
if not c.exists_effective('service dhcp-relay'):
print("DHCP relay service not configured")
else:
- os.system('sudo systemctl restart isc-dhcp-relay.service')
+ call('sudo systemctl restart isc-dhcp-relay.service')
sys.exit(0)
elif args.ipv6:
@@ -45,7 +47,7 @@ if __name__ == '__main__':
if not c.exists_effective('service dhcpv6-relay'):
print("DHCPv6 relay service not configured")
else:
- os.system('sudo systemctl restart isc-dhcpv6-relay.service')
+ call('sudo systemctl restart isc-dhcpv6-relay.service')
sys.exit(0)
else:
diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py
index da6407e23..d1b66b33f 100755
--- a/src/op_mode/restart_frr.py
+++ b/src/op_mode/restart_frr.py
@@ -17,12 +17,13 @@
import sys
import argparse
-import subprocess
import logging
from logging.handlers import SysLogHandler
from pathlib import Path
import psutil
+from vyos.util import call
+
# some default values
watchfrr = '/usr/lib/frr/watchfrr.sh'
vtysh = '/usr/bin/vtysh'
@@ -86,7 +87,7 @@ def _write_config():
Path(frrconfig_tmp).mkdir(parents=False, exist_ok=True)
# save frr.conf to it
command = "{} -n -w --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp)
- return_code = subprocess.call(command, shell=True)
+ return_code = call(command)
if not return_code == 0:
logger.error("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code))
return False
@@ -108,7 +109,7 @@ def _cleanup():
# check if daemon is running
def _daemon_check(daemon):
command = "{} print_status {}".format(watchfrr, daemon)
- return_code = subprocess.call(command, shell=True)
+ return_code = call(command)
if not return_code == 0:
logger.error("Daemon \"{}\" is not running".format(daemon))
return False
@@ -119,7 +120,7 @@ def _daemon_check(daemon):
# restart daemon
def _daemon_restart(daemon):
command = "{} restart {}".format(watchfrr, daemon)
- return_code = subprocess.call(command, shell=True)
+ return_code = call(command)
if not return_code == 0:
logger.error("Failed to restart daemon \"{}\"".format(daemon))
return False
@@ -135,7 +136,7 @@ def _reload_config(daemon):
else:
command = "{} -n -b --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp)
- return_code = subprocess.call(command, shell=True)
+ return_code = call(command)
if not return_code == 0:
logger.error("Failed to reinstall configuration")
return False
diff --git a/src/op_mode/show_acceleration.py b/src/op_mode/show_acceleration.py
index 3ba0e85dd..6d44b0f66 100755
--- a/src/op_mode/show_acceleration.py
+++ b/src/op_mode/show_acceleration.py
@@ -19,14 +19,16 @@ import sys
import os
import re
import argparse
-import subprocess
+
from vyos.config import Config
+from vyos.util import popen
+from vyos.util import call
+
def detect_qat_dev():
- ret = subprocess.Popen(['sudo', 'lspci', '-nn'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
+ output, err = popen('sudo lspci -nn', decode='utf-8')
if not err:
- data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output.decode("utf-8"))
+ data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output)
#If QAT devices found
if data:
return
@@ -42,15 +44,12 @@ def show_qat_status():
sys.exit(1)
# Show QAT service
- os.system('sudo /etc/init.d/vyos-qat-utilities status')
+ call('sudo /etc/init.d/vyos-qat-utilities status')
# Return QAT devices
def get_qat_devices():
- ret = subprocess.Popen(['sudo', '/etc/init.d/vyos-qat-utilities', 'status'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
+ data_st, err = popen('sudo /etc/init.d/vyos-qat-utilities status', decode='utf-8')
if not err:
- #print(output)
- data_st = output.decode("utf-8")
elm_lst = re.findall('qat_dev\d', data_st)
print('\n'.join(elm_lst))
@@ -58,11 +57,10 @@ def get_qat_devices():
def get_qat_proc_path(qat_dev):
q_type = ""
q_bsf = ""
- ret = subprocess.Popen(['sudo', '/etc/init.d/vyos-qat-utilities', 'status'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
+ output, err = popen('sudo /etc/init.d/vyos-qat-utilities status', decode='utf-8')
if not err:
# Parse QAT service output
- data_st = output.decode("utf-8").split("\n")
+ data_st = output.split("\n")
for elm_str in range(len(data_st)):
if re.search(qat_dev, data_st[elm_str]):
elm_list = data_st[elm_str].split(", ")
@@ -97,22 +95,22 @@ args = parser.parse_args()
if args.hw:
detect_qat_dev()
# Show availible Intel QAT devices
- os.system('sudo lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'')
+ call('sudo lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'')
elif args.flow and args.dev:
check_qat_if_conf()
- os.system('sudo cat '+get_qat_proc_path(args.dev)+"fw_counters")
+ call('sudo cat '+get_qat_proc_path(args.dev)+"fw_counters")
elif args.interrupts:
check_qat_if_conf()
# Delete _dev from args.dev
- os.system('sudo cat /proc/interrupts | grep qat')
+ call('sudo cat /proc/interrupts | grep qat')
elif args.status:
check_qat_if_conf()
show_qat_status()
elif args.conf and args.dev:
check_qat_if_conf()
- os.system('sudo cat '+get_qat_proc_path(args.dev)+"dev_cfg")
+ call('sudo cat '+get_qat_proc_path(args.dev)+"dev_cfg")
elif args.dev_list:
get_qat_devices()
else:
parser.print_help()
- sys.exit(1) \ No newline at end of file
+ sys.exit(1)
diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py
index f801ba753..a79033f69 100755
--- a/src/op_mode/show_dhcp.py
+++ b/src/op_mode/show_dhcp.py
@@ -24,9 +24,12 @@ import collections
import os
from datetime import datetime
-from vyos.config import Config
from isc_dhcp_leases import Lease, IscDhcpLeases
+from vyos.config import Config
+from vyos.util import call
+
+
lease_file = "/config/dhcpd.leases"
pool_key = "shared-networkname"
@@ -190,7 +193,7 @@ if __name__ == '__main__':
sys.exit(0)
# if dhcp server is down, inactive leases may still be shown as active, so warn the user.
- if os.system('systemctl -q is-active isc-dhcpv4-server.service') != 0:
+ if call('systemctl -q is-active isc-dhcpv4-server.service') != 0:
print("WARNING: DHCP server is configured but not started. Data may be stale.")
if args.leases:
diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py
index ae63af39b..18baa5517 100755
--- a/src/op_mode/show_dhcpv6.py
+++ b/src/op_mode/show_dhcpv6.py
@@ -24,9 +24,11 @@ import collections
import os
from datetime import datetime
-from vyos.config import Config
from isc_dhcp_leases import Lease, IscDhcpLeases
+from vyos.config import Config
+from vyos.util import call
+
lease_file = "/config/dhcpdv6.leases"
pool_key = "shared-networkname"
@@ -177,7 +179,7 @@ if __name__ == '__main__':
sys.exit(0)
# if dhcp server is down, inactive leases may still be shown as active, so warn the user.
- if os.system('systemctl -q is-active isc-dhcpv6-server.service') != 0:
+ if call('systemctl -q is-active isc-dhcpv6-server.service') != 0:
print("WARNING: DHCPv6 server is configured but not started. Data may be stale.")
if args.leases:
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
index 06b90296f..32918ddce 100755
--- a/src/op_mode/show_openvpn.py
+++ b/src/op_mode/show_openvpn.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
+import os
import jinja2
import argparse
@@ -63,6 +64,9 @@ def get_status(mode, interface):
'clients': [],
}
+ if not os.path.exists(status_file):
+ return data
+
with open(status_file, 'r') as f:
lines = f.readlines()
for line_no, line in enumerate(lines):
diff --git a/src/op_mode/show_vpn_ra.py b/src/op_mode/show_vpn_ra.py
index cf6119c2f..2323193b1 100755
--- a/src/op_mode/show_vpn_ra.py
+++ b/src/op_mode/show_vpn_ra.py
@@ -17,8 +17,8 @@
import os
import sys
import re
-import subprocess
-# from subprocess import Popen, PIPE
+
+from vyos.util import popen
# chech connection to pptp and l2tp daemon
def get_sessions():
@@ -31,18 +31,16 @@ def get_sessions():
len_def_header = 170
# Check pptp
- ret = subprocess.Popen(pptp_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
- if not err and len(output.decode("utf-8")) > len_def_header and not re.search(err_pattern, output.decode("utf-8")):
- print(output.decode("utf-8"))
+ output, err = popen(pptp_cmd, decode='utf-8')
+ if not err and len(output) > len_def_header and not re.search(err_pattern, output):
+ print(output)
else:
absent_pptp = True
# Check l2tp
- ret = subprocess.Popen(l2tp_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
- if not err and len(output.decode("utf-8")) > len_def_header and not re.search(err_pattern, output.decode("utf-8")):
- print(output.decode("utf-8"))
+ output, err = popen(l2tp_cmd, decode='utf-8')
+ if not err and len(output) > len_def_header and not re.search(err_pattern, output):
+ print(output)
else:
absent_l2tp = True
diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py
new file mode 100755
index 000000000..b6bb73d01
--- /dev/null
+++ b/src/op_mode/show_vrf.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import jinja2
+from json import loads
+
+from vyos.util import cmd
+
+vrf_out_tmpl = """
+VRF name state mac address flags interfaces
+-------- ----- ----------- ----- ----------
+{%- for v in vrf %}
+{{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}}
+{%- endfor %}
+
+"""
+
+def list_vrfs():
+ command = 'ip -j -br link show type vrf'
+ answer = loads(cmd(command))
+ return [_ for _ in answer if _]
+
+def list_vrf_members(vrf):
+ command = f'ip -j -br link show master {vrf}'
+ answer = loads(cmd(command))
+ return [_ for _ in answer if _]
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("-e", "--extensive", action="store_true",
+ help="provide detailed vrf informatio")
+parser.add_argument('interface', metavar='I', type=str, nargs='?',
+ help='interface to display')
+
+args = parser.parse_args()
+
+if args.extensive:
+ data = { 'vrf': [] }
+ for vrf in list_vrfs():
+ name = vrf['ifname']
+ if args.interface and name != args.interface:
+ continue
+
+ vrf['members'] = []
+ for member in list_vrf_members(name):
+ vrf['members'].append(member['ifname'])
+ data['vrf'].append(vrf)
+
+ tmpl = jinja2.Template(vrf_out_tmpl)
+ print(tmpl.render(data))
+
+else:
+ print(" ".join([vrf['ifname'] for vrf in list_vrfs()]))
diff --git a/src/op_mode/show_wireless.py b/src/op_mode/show_wireless.py
index aff882559..b5ee3aee1 100755
--- a/src/op_mode/show_wireless.py
+++ b/src/op_mode/show_wireless.py
@@ -19,19 +19,15 @@ import re
from sys import exit
from copy import deepcopy
-from subprocess import Popen, PIPE, STDOUT
from vyos.config import Config
+from vyos.util import popen
parser = argparse.ArgumentParser()
parser.add_argument("-s", "--scan", help="Scan for Wireless APs on given interface, e.g. 'wlan0'")
parser.add_argument("-b", "--brief", action="store_true", help="Show wireless configuration")
parser.add_argument("-c", "--stations", help="Show wireless clients connected on interface, e.g. 'wlan0'")
-def _cmd(command):
- p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
- tmp = p.communicate()[0].strip()
- return tmp.decode()
def show_brief():
config = Config()
@@ -57,7 +53,8 @@ def show_brief():
return interfaces
def ssid_scan(intf):
- tmp = _cmd('/sbin/iw dev {} scan ap-force'.format(intf))
+ # XXX: This ignores errors
+ tmp, _ = popen(f'/sbin/iw dev {intf} scan ap-force')
networks = []
data = {
'ssid': '',
@@ -89,7 +86,8 @@ def ssid_scan(intf):
return networks
def show_clients(intf):
- tmp = _cmd('/sbin/iw dev {} station dump'.format(intf))
+ # XXX: This ignores errors
+ tmp, _ = popen(f'/sbin/iw dev {intf} station dump')
clients = []
data = {
'mac': '',
diff --git a/src/op_mode/snmp.py b/src/op_mode/snmp.py
index e08441f0e..5fae67881 100755
--- a/src/op_mode/snmp.py
+++ b/src/op_mode/snmp.py
@@ -24,6 +24,7 @@ import sys
import argparse
from vyos.config import Config
+from vyos.util import call
config_file_daemon = r'/etc/snmp/snmpd.conf'
@@ -53,7 +54,7 @@ def show_all():
def show_community(c, h):
print('Status of SNMP community {0} on {1}'.format(c, h), flush=True)
- os.system('/usr/bin/snmpstatus -t1 -v1 -c {0} {1}'.format(c, h))
+ call('/usr/bin/snmpstatus -t1 -v1 -c {0} {1}'.format(c, h))
if __name__ == '__main__':
args = parser.parse_args()
diff --git a/src/op_mode/snmp_ifmib.py b/src/op_mode/snmp_ifmib.py
index 3a0e0d4b2..2479936bd 100755
--- a/src/op_mode/snmp_ifmib.py
+++ b/src/op_mode/snmp_ifmib.py
@@ -22,36 +22,24 @@
import sys
import argparse
import netifaces
-import subprocess
from vyos.config import Config
+from vyos.util import popen
parser = argparse.ArgumentParser(description='Retrieve SNMP interfaces information')
parser.add_argument('--ifindex', action='store', nargs='?', const='all', help='Show interface index')
parser.add_argument('--ifalias', action='store', nargs='?', const='all', help='Show interface aliase')
parser.add_argument('--ifdescr', action='store', nargs='?', const='all', help='Show interface description')
-def show_ifindex(i):
- proc = subprocess.Popen(['/bin/ip', 'link', 'show', i], stdout=subprocess.PIPE)
- (out, err) = proc.communicate()
- # convert output to string
- string = out.decode("utf-8")
-
- index = 'ifIndex = ' + string.split(':')[0]
+def show_ifindex(intf):
+ out, err = popen(f'/bin/ip link show {intf}', decode='utf-8')
+ index = 'ifIndex = ' + out.split(':')[0]
return index.replace('\n', '')
-def show_ifalias(i):
- proc = subprocess.Popen(['/bin/ip', 'link', 'show', i], stdout=subprocess.PIPE)
- (out, err) = proc.communicate()
- # convert output to string
- string = out.decode("utf-8")
-
- if 'alias' in string:
- alias = 'ifAlias = ' + string.split('alias')[1].lstrip()
- else:
- alias = 'ifAlias = ' + i
-
- return alias.replace('\n', '')
+def show_ifalias(intf):
+ out, err = popen(f'/bin/ip link show {intf}', decode='utf-8')
+ alias = out.split('alias')[1].lstrip() if 'alias' in out else intf
+ return 'ifAlias = ' + alias.replace('\n', '')
def show_ifdescr(i):
ven_id = ''
@@ -74,14 +62,13 @@ def show_ifdescr(i):
return ret
device = str(ven_id) + ':' + str(dev_id)
- proc = subprocess.Popen(['/usr/bin/lspci', '-mm', '-d', device], stdout=subprocess.PIPE)
- (out, err) = proc.communicate()
+ out, err = popen(f'/usr/bin/lspci -mm -d {device}', decode='utf-8')
vendor = ""
device = ""
# convert output to string
- string = out.decode("utf-8").split('"')
+ string = out.split('"')
if len(string) > 3:
vendor = string[3]
diff --git a/src/op_mode/system_integrity.py b/src/op_mode/system_integrity.py
index 886d94f16..c0e3d1095 100755
--- a/src/op_mode/system_integrity.py
+++ b/src/op_mode/system_integrity.py
@@ -18,18 +18,19 @@
import sys
import os
-import subprocess
import re
import itertools
from datetime import datetime, timedelta
+from vyos.util import cmd
+
verf = r'/usr/libexec/vyos/op_mode/version.py'
def get_sys_build_version():
if not os.path.exists(verf):
return None
- a = subprocess.check_output(['/usr/libexec/vyos/op_mode/version.py']).decode()
+ a = cmd('/usr/libexec/vyos/op_mode/version.py')
if re.search('^Built on:.+',a, re.M) == None:
return None
diff --git a/src/op_mode/version.py b/src/op_mode/version.py
index 5aff0f767..fe6ecbae5 100755
--- a/src/op_mode/version.py
+++ b/src/op_mode/version.py
@@ -22,7 +22,6 @@
import os
import sys
-import subprocess
import argparse
import json
@@ -31,6 +30,10 @@ import pystache
import vyos.version
import vyos.limericks
+from vyos.util import cmd
+from vyos.util import call
+from vyos.util import run
+
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--all", action="store_true", help="Include individual package versions")
@@ -73,15 +76,15 @@ if __name__ == '__main__':
version_data = vyos.version.get_version_data()
# Get system architecture (well, kernel architecture rather)
- version_data['system_arch'] = subprocess.check_output('uname -m', shell=True).decode().strip()
+ version_data['system_arch'] = cmd('uname -m')
# Get hypervisor name, if any
system_type = "bare metal"
try:
- hypervisor = subprocess.check_output('hvinfo 2>/dev/null', shell=True).decode().strip()
+ hypervisor = cmd('hvinfo 2>/dev/null')
system_type = "{0} guest".format(hypervisor)
- except subprocess.CalledProcessError:
+ except OSError:
# hvinfo returns 1 if it cannot detect any hypervisor
pass
version_data['system_type'] = system_type
@@ -93,9 +96,9 @@ if __name__ == '__main__':
# while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot
# from an installed image
boot_via = "installed image"
- if subprocess.call(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""", shell=True) == 0:
+ if run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0:
boot_via = "livecd"
- elif subprocess.call(""" grep '^overlay /' /proc/mounts >/dev/null """, shell=True) != 0:
+ elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0:
boot_via = "legacy non-image installation"
version_data['boot_via'] = boot_via
@@ -118,7 +121,7 @@ if __name__ == '__main__':
if args.all:
print("Package versions:")
- os.system("dpkg -l")
+ call("dpkg -l")
if args.funny:
print(vyos.limericks.get_random())
diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py
index 8d1369823..8a993f92c 100755
--- a/src/op_mode/vrrp.py
+++ b/src/op_mode/vrrp.py
@@ -18,12 +18,28 @@
import sys
import time
import argparse
-
+import json
import tabulate
import vyos.keepalived
import vyos.util
+config_dict_path = '/run/keepalived_config.dict'
+
+
+# get disabled instances from a config
+def vrrp_get_disabled():
+ # read the dictionary file with configuration
+ with open(config_dict_path, 'r') as dict_file:
+ vrrp_config_dict = json.load(dict_file)
+ vrrp_disabled = []
+ # add disabled groups to the list
+ for vrrp_group in vrrp_config_dict['vrrp_groups']:
+ if vrrp_group['disable']:
+ vrrp_disabled.append([vrrp_group['name'], vrrp_group['interface'], vrrp_group['vrid'], 'DISABLED', ''])
+ # return list with disabled instances
+ return vrrp_disabled
+
def print_summary():
try:
@@ -54,10 +70,13 @@ def print_summary():
row = [name, interface, vrid, state, ltrans_time]
groups.append(row)
+ # add to the active list disabled instances
+ groups.extend(vrrp_get_disabled())
headers = ["Name", "Interface", "VRID", "State", "Last Transition"]
output = tabulate.tabulate(groups, headers)
print(output)
+
def print_statistics():
try:
vyos.keepalived.force_stats_dump()
@@ -69,6 +88,7 @@ def print_statistics():
print("VRRP statistics are not available")
sys.exit(1)
+
def print_state_data():
try:
vyos.keepalived.force_state_data_dump()
@@ -80,6 +100,7 @@ def print_state_data():
print("VRRP information is not available")
sys.exit(1)
+
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-s", "--summary", action="store_true", help="Print VRRP summary")
diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py
index 38c061cf4..1b90f4fa7 100755
--- a/src/op_mode/wireguard.py
+++ b/src/op_mode/wireguard.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,14 +13,11 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#
import argparse
import os
import sys
import shutil
-import subprocess
import syslog as sl
import re
@@ -28,6 +25,7 @@ from vyos.ifconfig import WireGuardIf
from vyos import ConfigError
from vyos.config import Config
+from vyos.util import cmd, run
dir = r'/config/auth/wireguard'
psk = dir + '/preshared.key'
@@ -36,16 +34,14 @@ def check_kmod():
""" check if kmod is loaded, if not load it """
if not os.path.exists('/sys/module/wireguard'):
sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
- if os.system('sudo modprobe wireguard') != 0:
+ if run('sudo modprobe wireguard') != 0:
sl.syslog(sl.LOG_ERR, "modprobe wireguard failed")
raise ConfigError("modprobe wireguard failed")
def generate_keypair(pk, pub):
""" generates a keypair which is stored in /config/auth/wireguard """
old_umask = os.umask(0o027)
- ret = subprocess.call(
- ['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True)
- if ret != 0:
+ if run(f'wg genkey | tee {pk} | wg pubkey > {pub}') != 0:
raise ConfigError("wireguard key-pair generation failed")
else:
sl.syslog(
@@ -69,9 +65,9 @@ def genkey(location):
else:
""" if keypair is bing executed from a running iso """
if not os.path.exists(location):
- subprocess.call(['sudo mkdir -p ' + location], shell=True)
- subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True)
- subprocess.call(['sudo chmod 750 ' + location], shell=True)
+ run(f'sudo mkdir -p {location}')
+ run(f'sudo chgrp vyattacfg {location}')
+ run(f'sudo chmod 750 {location}')
generate_keypair(pk, pub)
os.umask(old_umask)
@@ -90,10 +86,11 @@ def genpsk():
it's stored only in the cli config
"""
- subprocess.call(['wg genpsk'], shell=True)
+ psk = cmd('wg genpsk')
+ print(psk)
def list_key_dirs():
- """ lists all dirs under /config/auth/wireguard """
+ """ lists all dirs under /config/auth/wireguard """
if os.path.exists(dir):
nks = next(os.walk(dir))[1]
for nk in nks:
@@ -150,7 +147,7 @@ if __name__ == '__main__':
if args.listkdir:
list_key_dirs()
if args.showinterface:
- intf = WireGuardIf(args.showinterface)
+ intf = WireGuardIf(args.showinterface, create=False, debug=False)
intf.op_show_interface()
if args.delkdir:
if args.location:
diff --git a/src/pam-configs/radius b/src/pam-configs/radius
index 0d9fbbb03..0e2c71e38 100644
--- a/src/pam-configs/radius
+++ b/src/pam-configs/radius
@@ -3,12 +3,18 @@ Default: yes
Priority: 257
Auth-Type: Primary
Auth:
+ [default=ignore success=1] pam_succeed_if.so uid eq 1001 quiet
+ [default=ignore success=ignore] pam_succeed_if.so uid eq 1002 quiet
[authinfo_unavail=ignore success=end default=ignore] pam_radius_auth.so
Account-Type: Primary
Account:
+ [default=ignore success=1] pam_succeed_if.so uid eq 1001 quiet
+ [default=ignore success=ignore] pam_succeed_if.so uid eq 1002 quiet
[authinfo_unavail=ignore success=end perm_denied=bad default=ignore] pam_radius_auth.so
Session-Type: Additional
Session:
+ [default=ignore success=1] pam_succeed_if.so uid eq 1001 quiet
+ [default=ignore success=ignore] pam_succeed_if.so uid eq 1002 quiet
[authinfo_unavail=ignore success=ok default=ignore] pam_radius_auth.so
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index da6e5b118..b5ad8b159 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -308,6 +308,49 @@ def config_file_op():
return success(res)
+@app.route('/generate', method='POST')
+@auth_required
+def generate_op():
+ session = app.config['vyos_session']
+
+ command = bottle.request.forms.get("data")
+ command = json.loads(command)
+
+ try:
+ cmd = command['cmd']
+ res = session.generate(cmd)
+ except KeyError:
+ return error(400, "Missing required field \"cmd\"")
+ except VyOSError as e:
+ return error(400, str(e))
+ except Exception as e:
+ print(traceback.format_exc(), file=sys.stderr)
+ return error(500, "An internal error occured. Check the logs for details.")
+
+ return success(res)
+
+@app.route('/show', method='POST')
+@auth_required
+def show_op():
+ session = app.config['vyos_session']
+
+ command = bottle.request.forms.get("data")
+ command = json.loads(command)
+
+ try:
+ cmd = command['cmd']
+ res = session.show(cmd)
+ except KeyError:
+ return error(400, "Missing required field \"cmd\"")
+ except VyOSError as e:
+ return error(400, str(e))
+ except Exception as e:
+ print(traceback.format_exc(), file=sys.stderr)
+ return error(500, "An internal error occured. Check the logs for details.")
+
+ return success(res)
+
+
if __name__ == '__main__':
# systemd's user and group options don't work, do it by hand here,
# else no one else will be able to commit
diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
new file mode 100755
index 000000000..2778deaab
--- /dev/null
+++ b/src/system/keepalived-fifo.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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 os
+import time
+import signal
+import argparse
+import threading
+import re
+import json
+from pathlib import Path
+from queue import Queue
+import logging
+from logging.handlers import SysLogHandler
+
+from vyos.util import cmd
+
+# configure logging
+logger = logging.getLogger(__name__)
+logs_format = logging.Formatter('%(filename)s: %(message)s')
+logs_handler_syslog = SysLogHandler('/dev/log')
+logs_handler_syslog.setFormatter(logs_format)
+logger.addHandler(logs_handler_syslog)
+logger.setLevel(logging.DEBUG)
+
+
+# class for all operations
+class KeepalivedFifo:
+ # init - read command arguments
+ def __init__(self):
+ logger.info("Starting FIFO pipe for Keepalived")
+ # define program arguments
+ cmd_args_parser = argparse.ArgumentParser(description='Create FIFO pipe for keepalived and process notify events', add_help=False)
+ cmd_args_parser.add_argument('PIPE', help='path to the FIFO pipe')
+ # parse arguments
+ cmd_args = cmd_args_parser.parse_args()
+ self._config_load()
+ self.pipe_path = cmd_args.PIPE
+
+ # create queue for messages and events for syncronization
+ self.message_queue = Queue(maxsize=100)
+ self.stopme = threading.Event()
+ self.message_event = threading.Event()
+
+ # load configuration
+ def _config_load(self):
+ try:
+ # read the dictionary file with configuration
+ with open('/run/keepalived_config.dict', 'r') as dict_file:
+ vrrp_config_dict = json.load(dict_file)
+ self.vrrp_config = {'vrrp_groups': {}, 'sync_groups': {}}
+ # save VRRP instances to the new dictionary
+ for vrrp_group in vrrp_config_dict['vrrp_groups']:
+ self.vrrp_config['vrrp_groups'][vrrp_group['name']] = {
+ 'STOP': vrrp_group.get('stop_script'),
+ 'FAULT': vrrp_group.get('fault_script'),
+ 'BACKUP': vrrp_group.get('backup_script'),
+ 'MASTER': vrrp_group.get('master_script')
+ }
+ # save VRRP sync groups to the new dictionary
+ for sync_group in vrrp_config_dict['sync_groups']:
+ self.vrrp_config['sync_groups'][sync_group['name']] = {
+ 'STOP': sync_group.get('stop_script'),
+ 'FAULT': sync_group.get('fault_script'),
+ 'BACKUP': sync_group.get('backup_script'),
+ 'MASTER': sync_group.get('master_script')
+ }
+ logger.debug("Loaded configuration: {}".format(self.vrrp_config))
+ except Exception as err:
+ logger.error("Unable to load configuration: {}".format(err))
+
+ # run command
+ def _run_command(self, command):
+ logger.debug("Running the command: {}".format(command))
+ try:
+ cmd(command, universal_newlines=True)
+ except OSError as err:
+ logger.error(f'Unable to execute command "{command}": {err}')
+
+ # create FIFO pipe
+ def pipe_create(self):
+ if Path(self.pipe_path).exists():
+ logger.info("PIPE already exist: {}".format(self.pipe_path))
+ else:
+ os.mkfifo(self.pipe_path)
+
+ # process message from pipe
+ def pipe_process(self):
+ logger.debug("Message processing start")
+ regex_notify = re.compile(r'^(?P<type>\w+) "(?P<name>[\w-]+)" (?P<state>\w+) (?P<priority>\d+)$', re.MULTILINE)
+ while self.stopme.is_set() is False:
+ # wait for a new message event from pipe_wait
+ self.message_event.wait()
+ try:
+ # clear mesage event flag
+ self.message_event.clear()
+ # get all messages from queue and try to process them
+ while self.message_queue.empty() is not True:
+ message = self.message_queue.get()
+ logger.debug("Received message: {}".format(message))
+ notify_message = regex_notify.search(message)
+ # try to process a message if it looks valid
+ if notify_message:
+ n_type = notify_message.group('type')
+ n_name = notify_message.group('name')
+ n_state = notify_message.group('state')
+ logger.info("{} {} changed state to {}".format(n_type, n_name, n_state))
+ # check and run commands for VRRP instances
+ if n_type == 'INSTANCE':
+ if n_name in self.vrrp_config['vrrp_groups'] and n_state in self.vrrp_config['vrrp_groups'][n_name]:
+ n_script = self.vrrp_config['vrrp_groups'][n_name].get(n_state)
+ if n_script:
+ self._run_command(n_script)
+ # check and run commands for VRRP sync groups
+ # currently, this is not available in VyOS CLI
+ if n_type == 'GROUP':
+ if n_name in self.vrrp_config['sync_groups'] and n_state in self.vrrp_config['sync_groups'][n_name]:
+ n_script = self.vrrp_config['sync_groups'][n_name].get(n_state)
+ if n_script:
+ self._run_command(n_script)
+ # mark task in queue as done
+ self.message_queue.task_done()
+ except Exception as err:
+ logger.error("Error processing message: {}".format(err))
+ logger.debug("Terminating messages processing thread")
+
+ # wait for messages
+ def pipe_wait(self):
+ logger.debug("Message reading start")
+ self.pipe_read = os.open(self.pipe_path, os.O_RDONLY | os.O_NONBLOCK)
+ while self.stopme.is_set() is False:
+ # sleep a bit to not produce 100% CPU load
+ time.sleep(0.1)
+ try:
+ # try to read a message from PIPE
+ message = os.read(self.pipe_read, 500)
+ if message:
+ # split PIPE content by lines and put them into queue
+ for line in message.decode().strip().splitlines():
+ self.message_queue.put(line)
+ # set new message flag to start processing
+ self.message_event.set()
+ except Exception as err:
+ # ignore the "Resource temporarily unavailable" error
+ if err.errno != 11:
+ logger.error("Error receiving message: {}".format(err))
+
+ logger.debug("Closing FIFO pipe")
+ os.close(self.pipe_read)
+
+
+# handle SIGTERM signal to allow finish all messages processing
+def sigterm_handle(signum, frame):
+ logger.info("Ending processing: Received SIGTERM signal")
+ fifo.stopme.set()
+ thread_wait_message.join()
+ fifo.message_event.set()
+ thread_process_message.join()
+
+
+signal.signal(signal.SIGTERM, sigterm_handle)
+
+# init our class
+fifo = KeepalivedFifo()
+# try to create PIPE if it is not exist yet
+# It looks like keepalived do it before the script will be running, but if we
+# will decide to run this not from keepalived config, then we may get in
+# trouble. So it is betteer to leave this here.
+fifo.pipe_create()
+# create and run dedicated threads for reading and processing messages
+thread_wait_message = threading.Thread(target=fifo.pipe_wait)
+thread_process_message = threading.Thread(target=fifo.pipe_process)
+thread_wait_message.start()
+thread_process_message.start()
diff --git a/src/system/vrrp-script-wrapper.py b/src/system/vrrp-script-wrapper.py
deleted file mode 100755
index c28ecba55..000000000
--- a/src/system/vrrp-script-wrapper.py
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018 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 os
-import sys
-import subprocess
-import argparse
-import syslog
-
-import vyos.util
-
-
-parser = argparse.ArgumentParser()
-parser.add_argument("-t", "--state", type=str, help="VRRP state")
-parser.add_argument("-g", "--group", type=str, help="VRRP group")
-parser.add_argument("-i", "--interface", type=str, help="Network interface")
-parser.add_argument("script", nargs='+')
-
-syslog.openlog('vyos-vrrp-wrapper')
-
-args = parser.parse_args()
-if not args.script or not args.state or not args.group \
- or not args.interface:
- parser.print_usage()
- sys.exit(1)
-
-# Fixup: the reason we take multiple "script" arguments is that people may want
-# to pass arguments to the script
-args.script = " ".join(args.script)
-
-exitcode = 0
-# Change the process GID to the config owners group to avoid screwing up
-# running config permissions
-os.setgid(vyos.util.get_cfg_group_id())
-syslog.syslog(syslog.LOG_NOTICE, 'Running transition script {0} for VRRP group {1}'.format(args.script, args.group))
-try:
- ret = subprocess.call("%s %s %s %s" % ( args.script, args.state, args.interface, args.group), shell=True)
- if ret != 0:
- syslog.syslog(syslog.LOG_ERR, "Transition script {0} failed, exit status: {1}".format(args.script, ret))
- exitcode = ret
-except Exception as e:
- syslog.syslog(syslog.LOG_ERR, "Failed to execute transition script {0}: {1}".format(args.script, e))
- exitcode = 1
-
-if exitcode == 0:
- syslog.syslog(syslog.LOG_NOTICE, "Transition script {0} executed successfully".format(args.script))
-
-syslog.closelog()
-sys.exit(exitcode)
diff --git a/src/validators/fqdn b/src/validators/fqdn
new file mode 100755
index 000000000..9f4ed764f
--- /dev/null
+++ b/src/validators/fqdn
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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 re import match
+from sys import argv,exit
+
+if len(argv) == 2:
+ # pattern copied from: https://www.regextester.com/103452
+ pattern = "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)"
+ if match(pattern, argv[1]):
+ exit(0)
+ else:
+ exit(1)
+
diff --git a/src/validators/mac-address b/src/validators/mac-address
index d6ec6d712..435920b84 100755
--- a/src/validators/mac-address
+++ b/src/validators/mac-address
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 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
@@ -13,16 +13,14 @@
#
# 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 sys
import re
+from sys import exit, argv
-if len(sys.argv) == 2:
- pattern = "^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$"
- if re.match(pattern, sys.argv[1]):
- sys.exit(0)
- else:
- sys.exit(1)
+if len(argv) == 2:
+ pattern = "^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$"
+ if re.match(pattern, argv[1]):
+ exit(0)
+ else:
+ exit(1)
diff --git a/src/validators/timezone b/src/validators/timezone
index d7a8f64c4..ec845e755 100755
--- a/src/validators/timezone
+++ b/src/validators/timezone
@@ -17,12 +17,8 @@
import argparse
from sys import exit
-from subprocess import Popen, PIPE, STDOUT
-def _cmd(cmd):
- p = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True)
- tmp = p.communicate()[0].strip()
- return tmp.decode()
+from vyos.util import cmd
parser = argparse.ArgumentParser()
parser.add_argument("--validate", action="store", help="Check if timezone is valid")
@@ -31,7 +27,7 @@ if __name__ == '__main__':
args = parser.parse_args()
if args.validate:
- tz_data = _cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::')
+ tz_data = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::')
tz_data = tz_data.split('\n')
# if timezone can't be found in list it's invalid
if args.validate not in tz_data:
diff --git a/src/validators/vrf-name b/src/validators/vrf-name
new file mode 100755
index 000000000..11c453f4d
--- /dev/null
+++ b/src/validators/vrf-name
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+from sys import exit, argv
+
+if len(argv) == 2:
+ len = len(argv[1])
+ # VRF instance name must be 16 characters or less, python range needs to be
+ # extended by one
+ if not len in range(1, 17):
+ exit(1)
+
+ # Treat loopback interface "lo" explicitly. Adding "lo" explicitly to the
+ # following regex pattern would deny any VRF name starting with lo - thuse
+ # local-vrf would be illegal - and that we do not want.
+ if argv[1] == "lo":
+ exit(1)
+
+ # VRF instances should not be named after regular interface names like bond0,
+ # br10 and so on - this can cause a lot of confusion/trouble
+ pattern = "^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|" \
+ "vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wlm)\d+(\.\d+(v.+)?)?$).*$"
+ if re.match(pattern, argv[1]):
+ exit(0)
+
+exit(1)
diff --git a/tests/data/config.valid b/tests/data/config.valid
index a21c6a4d1..1fbdd1505 100644
--- a/tests/data/config.valid
+++ b/tests/data/config.valid
@@ -35,5 +35,5 @@ empty-node {
trailing-leaf-node-without-value
-/* Trailing commend */
-/* Another trailing comment */
+// Trailing comment
+// Another trailing comment