summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore142
-rw-r--r--CONTRIBUTING.md12
-rw-r--r--LICENSE.GPL339
-rw-r--r--LICENSE.LGPL502
-rw-r--r--Makefile145
-rw-r--r--Pipfile17
-rw-r--r--Pipfile.lock248
-rw-r--r--README.md66
-rw-r--r--data/templates/accel-ppp/chap-secrets.ipoe.tmpl18
-rw-r--r--data/templates/accel-ppp/chap-secrets.tmpl10
-rw-r--r--data/templates/accel-ppp/ipoe.config.tmpl111
-rw-r--r--data/templates/accel-ppp/l2tp.config.tmpl148
-rw-r--r--data/templates/accel-ppp/pppoe.config.tmpl204
-rw-r--r--data/templates/accel-ppp/pptp.config.tmpl89
-rw-r--r--data/templates/accel-ppp/sstp.config.tmpl146
-rw-r--r--data/templates/bcast-relay/udp-broadcast-relay.tmpl7
-rw-r--r--data/templates/conserver/conserver.conf.tmpl37
-rw-r--r--data/templates/dhcp-client/daemon-options.tmpl4
-rw-r--r--data/templates/dhcp-client/ipv4.tmpl19
-rw-r--r--data/templates/dhcp-client/ipv6.tmpl57
-rw-r--r--data/templates/dhcp-relay/config.tmpl4
-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/dhcpdv6.conf.tmpl81
-rw-r--r--data/templates/dns-forwarding/recursor.conf.lua.tmpl9
-rw-r--r--data/templates/dns-forwarding/recursor.conf.tmpl33
-rw-r--r--data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl28
-rw-r--r--data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl24
-rw-r--r--data/templates/dynamic-dns/ddclient.conf.tmpl46
-rw-r--r--data/templates/firewall/nftables-nat.tmpl157
-rw-r--r--data/templates/frr/bfd.frr.tmpl16
-rw-r--r--data/templates/frr/bgp.frr.tmpl1
-rw-r--r--data/templates/frr/igmp.frr.tmpl41
-rw-r--r--data/templates/frr/ldpd.frr.tmpl70
-rw-r--r--data/templates/frr/pimd.frr.tmpl34
-rw-r--r--data/templates/frr/rip.frr.tmpl143
-rw-r--r--data/templates/frr/static_mcast.frr.tmpl20
-rw-r--r--data/templates/getty/serial-getty.service.tmpl37
-rw-r--r--data/templates/https/nginx.default.tmpl69
-rw-r--r--data/templates/ids/fastnetmon.tmpl60
-rw-r--r--data/templates/ids/fastnetmon_networks_list.tmpl7
-rw-r--r--data/templates/igmp-proxy/igmpproxy.conf.tmpl37
-rw-r--r--data/templates/ipsec/charon.tmpl342
-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/lcd/LCDd.conf.tmpl132
-rw-r--r--data/templates/lcd/lcdproc.conf.tmpl60
-rw-r--r--data/templates/lldp/lldpd.tmpl3
-rw-r--r--data/templates/lldp/vyos.conf.tmpl20
-rw-r--r--data/templates/macsec/wpa_supplicant.conf.tmpl89
-rw-r--r--data/templates/mdns-repeater/mdns-repeater.tmpl2
-rw-r--r--data/templates/netflow/uacctd.conf.tmpl69
-rw-r--r--data/templates/ntp/ntp.conf.tmpl47
-rw-r--r--data/templates/ntp/override.conf.tmpl11
-rw-r--r--data/templates/ocserv/ocserv_config.tmpl82
-rw-r--r--data/templates/ocserv/ocserv_passwd.tmpl6
-rw-r--r--data/templates/ocserv/radius_conf.tmpl22
-rw-r--r--data/templates/ocserv/radius_servers.tmpl7
-rw-r--r--data/templates/openvpn/client.conf.tmpl35
-rw-r--r--data/templates/openvpn/server.conf.tmpl262
-rw-r--r--data/templates/pppoe/ip-down.script.tmpl36
-rw-r--r--data/templates/pppoe/ip-pre-up.script.tmpl18
-rw-r--r--data/templates/pppoe/ip-up.script.tmpl49
-rw-r--r--data/templates/pppoe/ipv6-up.script.tmpl83
-rw-r--r--data/templates/pppoe/peer.tmpl76
-rw-r--r--data/templates/router-advert/radvd.conf.tmpl47
-rw-r--r--data/templates/rsyslog/rsyslog.conf59
-rw-r--r--data/templates/salt-minion/minion.tmpl59
-rw-r--r--data/templates/snmp/etc.snmp.conf.tmpl4
-rw-r--r--data/templates/snmp/etc.snmpd.conf.tmpl115
-rw-r--r--data/templates/snmp/override.conf.tmpl13
-rw-r--r--data/templates/snmp/usr.snmpd.conf.tmpl6
-rw-r--r--data/templates/snmp/var.snmpd.conf.tmpl14
-rw-r--r--data/templates/ssh/override.conf.tmpl11
-rw-r--r--data/templates/ssh/sshd_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.tmpl16
-rw-r--r--data/templates/system/curlrc.tmpl8
-rw-r--r--data/templates/system/ssh_config.tmpl3
-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/vyos-hostsd/hosts.tmpl26
-rw-r--r--data/templates/vyos-hostsd/resolv.conf.tmpl26
-rw-r--r--data/templates/wifi/cfg80211.conf.tmpl1
-rw-r--r--data/templates/wifi/crda.tmpl1
-rw-r--r--data/templates/wifi/hostapd.conf.tmpl687
-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.tmpl27
-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.tmpl27
-rw-r--r--debian/changelog11
-rw-r--r--debian/control129
-rw-r--r--debian/copyright12
-rw-r--r--debian/lintian-overrides6
-rwxr-xr-xdebian/rules92
-rw-r--r--debian/vyos-1x-smoketest.install2
-rw-r--r--debian/vyos-1x-vmware.install1
-rw-r--r--debian/vyos-1x.install23
-rw-r--r--debian/vyos-1x.postinst32
-rw-r--r--interface-definitions/arp.xml.in37
-rw-r--r--interface-definitions/bcast-relay.xml.in80
-rw-r--r--interface-definitions/cron.xml.in75
-rw-r--r--interface-definitions/dhcp-relay.xml.in98
-rw-r--r--interface-definitions/dhcp-server.xml.in467
-rw-r--r--interface-definitions/dhcpv6-relay.xml.in80
-rw-r--r--interface-definitions/dhcpv6-server.xml.in344
-rw-r--r--interface-definitions/dns-domain-name.xml.in117
-rw-r--r--interface-definitions/dns-dynamic.xml.in242
-rw-r--r--interface-definitions/dns-forwarding.xml.in189
-rw-r--r--interface-definitions/firewall-options.xml.in55
-rw-r--r--interface-definitions/flow-accounting-conf.xml.in431
-rw-r--r--interface-definitions/https.xml.in174
-rw-r--r--interface-definitions/igmp-proxy.xml.in100
-rw-r--r--interface-definitions/include/accel-auth-mode.xml.i19
-rw-r--r--interface-definitions/include/accel-client-ipv6-pool.xml.in59
-rw-r--r--interface-definitions/include/accel-name-server.xml.in18
-rw-r--r--interface-definitions/include/accel-radius-additions.xml.in125
-rw-r--r--interface-definitions/include/accel-wins-server.xml.i13
-rw-r--r--interface-definitions/include/address-ipv4-ipv6-dhcp.xml.i29
-rw-r--r--interface-definitions/include/address-ipv4-ipv6.xml.i17
-rw-r--r--interface-definitions/include/bgp-afi-aggregate-address.xml.i12
-rw-r--r--interface-definitions/include/bgp-afi-redistribute-metric-route-map.xml.i17
-rw-r--r--interface-definitions/include/bgp-neighbor-afi-ipv4-unicast.xml.i285
-rw-r--r--interface-definitions/include/bgp-neighbor-afi-ipv6-unicast.xml.i322
-rw-r--r--interface-definitions/include/bgp-peer-group-afi-ipv4-unicast.xml.i301
-rw-r--r--interface-definitions/include/bgp-peer-group-afi-ipv6-unicast.xml.i317
-rw-r--r--interface-definitions/include/dhcp-options.xml.i22
-rw-r--r--interface-definitions/include/dhcpv6-options.xml.i86
-rw-r--r--interface-definitions/include/interface-arp-cache-timeout.xml.i14
-rw-r--r--interface-definitions/include/interface-description.xml.i9
-rw-r--r--interface-definitions/include/interface-disable-arp-filter.xml.i6
-rw-r--r--interface-definitions/include/interface-disable-link-detect.xml.i6
-rw-r--r--interface-definitions/include/interface-disable.xml.i6
-rw-r--r--interface-definitions/include/interface-enable-arp-accept.xml.i6
-rw-r--r--interface-definitions/include/interface-enable-arp-announce.xml.i6
-rw-r--r--interface-definitions/include/interface-enable-arp-ignore.xml.i6
-rw-r--r--interface-definitions/include/interface-enable-proxy-arp.xml.i6
-rw-r--r--interface-definitions/include/interface-hw-id.xml.i12
-rw-r--r--interface-definitions/include/interface-ipv4.xml.i11
-rw-r--r--interface-definitions/include/interface-ipv6.xml.i10
-rw-r--r--interface-definitions/include/interface-mac.xml.i12
-rw-r--r--interface-definitions/include/interface-mtu-1200-9000.xml.i14
-rw-r--r--interface-definitions/include/interface-mtu-1450-9000.xml.i14
-rw-r--r--interface-definitions/include/interface-mtu-64-8024.xml.i14
-rw-r--r--interface-definitions/include/interface-mtu-68-1500.xml.i14
-rw-r--r--interface-definitions/include/interface-mtu-68-9000.xml.i14
-rw-r--r--interface-definitions/include/interface-proxy-arp-pvlan.xml.i6
-rw-r--r--interface-definitions/include/interface-vrf.xml.i12
-rw-r--r--interface-definitions/include/ipv6-address.xml.i29
-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/isis-redistribute-ipv4.xml.i82
-rw-r--r--interface-definitions/include/nat-address.xml.i37
-rw-r--r--interface-definitions/include/nat-interface.xml.i9
-rw-r--r--interface-definitions/include/nat-port.xml.i17
-rw-r--r--interface-definitions/include/nat-rule.xml.i303
-rw-r--r--interface-definitions/include/nat-translation-port.xml.i13
-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/rip-redistribute.xml.i24
-rw-r--r--interface-definitions/include/source-address-ipv4-ipv6.xml.i17
-rw-r--r--interface-definitions/include/source-interface-ethernet.xml.i12
-rw-r--r--interface-definitions/include/source-interface.xml.i12
-rw-r--r--interface-definitions/include/vif-s.xml.i67
-rw-r--r--interface-definitions/include/vif.xml.i65
-rw-r--r--interface-definitions/intel_qat.xml.in21
-rw-r--r--interface-definitions/interfaces-bonding.xml.in174
-rw-r--r--interface-definitions/interfaces-bridge.xml.in184
-rw-r--r--interface-definitions/interfaces-dummy.xml.in27
-rw-r--r--interface-definitions/interfaces-ethernet.xml.in277
-rw-r--r--interface-definitions/interfaces-geneve.xml.in60
-rw-r--r--interface-definitions/interfaces-l2tpv3.xml.in161
-rw-r--r--interface-definitions/interfaces-loopback.xml.in25
-rw-r--r--interface-definitions/interfaces-macsec.xml.in116
-rw-r--r--interface-definitions/interfaces-openvpn.xml.in808
-rw-r--r--interface-definitions/interfaces-pppoe.xml.in164
-rw-r--r--interface-definitions/interfaces-pseudo-ethernet.xml.in82
-rw-r--r--interface-definitions/interfaces-tunnel.xml.in283
-rw-r--r--interface-definitions/interfaces-vxlan.xml.in114
-rw-r--r--interface-definitions/interfaces-wireguard.xml.in124
-rw-r--r--interface-definitions/interfaces-wireless.xml.in800
-rw-r--r--interface-definitions/interfaces-wirelessmodem.xml.in93
-rw-r--r--interface-definitions/ipsec-settings.xml.in24
-rw-r--r--interface-definitions/lldp.xml.in191
-rw-r--r--interface-definitions/nat.xml.in180
-rw-r--r--interface-definitions/ntp.xml.in84
-rw-r--r--interface-definitions/protocols-bfd.xml.in140
-rw-r--r--interface-definitions/protocols-bgp.xml.in1205
-rw-r--r--interface-definitions/protocols-igmp.xml.in88
-rw-r--r--interface-definitions/protocols-isis.xml.in552
-rw-r--r--interface-definitions/protocols-mpls.xml.in122
-rw-r--r--interface-definitions/protocols-multicast.xml.in95
-rw-r--r--interface-definitions/protocols-pim.xml.in96
-rw-r--r--interface-definitions/protocols-rip.xml.in406
-rw-r--r--interface-definitions/salt-minion.xml.in67
-rw-r--r--interface-definitions/service-ids-ddos-protection.xml.in118
-rw-r--r--interface-definitions/service_console-server.xml.in93
-rw-r--r--interface-definitions/service_ipoe-server.xml.in208
-rw-r--r--interface-definitions/service_mdns-repeater.xml.in37
-rw-r--r--interface-definitions/service_pppoe-server.xml.in491
-rw-r--r--interface-definitions/service_router-advert.xml.in273
-rw-r--r--interface-definitions/snmp.xml.in631
-rw-r--r--interface-definitions/ssh.xml.in207
-rw-r--r--interface-definitions/system-console.xml.in90
-rw-r--r--interface-definitions/system-ip.xml.in58
-rw-r--r--interface-definitions/system-ipv6.xml.in64
-rw-r--r--interface-definitions/system-lcd.xml.in66
-rw-r--r--interface-definitions/system-login-banner.xml.in32
-rw-r--r--interface-definitions/system-login.xml.in152
-rw-r--r--interface-definitions/system-options.xml.in68
-rw-r--r--interface-definitions/system-proxy.xml.in43
-rw-r--r--interface-definitions/system-syslog.xml.in949
-rw-r--r--interface-definitions/system-time-zone.xml.in19
-rw-r--r--interface-definitions/tftp-server.xml.in57
-rw-r--r--interface-definitions/vpn_anyconnect.xml.in258
-rw-r--r--interface-definitions/vpn_l2tp.xml.in457
-rw-r--r--interface-definitions/vpn_pptp.xml.in165
-rw-r--r--interface-definitions/vpn_sstp.xml.in273
-rw-r--r--interface-definitions/vrf.xml.in47
-rw-r--r--interface-definitions/vrrp.xml.in302
-rw-r--r--op-mode-definitions/add-system-image.xml62
-rw-r--r--op-mode-definitions/anyconnect.xml20
-rw-r--r--op-mode-definitions/configure.xml24
-rw-r--r--op-mode-definitions/connect.xml29
-rw-r--r--op-mode-definitions/date.xml64
-rw-r--r--op-mode-definitions/dhcp.xml203
-rw-r--r--op-mode-definitions/disconnect.xml20
-rw-r--r--op-mode-definitions/disks.xml50
-rw-r--r--op-mode-definitions/dns-dynamic.xml75
-rw-r--r--op-mode-definitions/dns-forwarding.xml94
-rw-r--r--op-mode-definitions/flow-accounting-op.xml81
-rw-r--r--op-mode-definitions/force-arp.xml79
-rw-r--r--op-mode-definitions/force-ipv6-nd.xml33
-rw-r--r--op-mode-definitions/force-ipv6-rd.xml34
-rw-r--r--op-mode-definitions/generate-macsec-key.xml26
-rw-r--r--op-mode-definitions/generate-ssh-server-key.xml16
-rw-r--r--op-mode-definitions/igmp-proxy.xml13
-rw-r--r--op-mode-definitions/ipoe-server.xml81
-rw-r--r--op-mode-definitions/ipv4-route.xml87
-rw-r--r--op-mode-definitions/ipv6-route.xml133
-rw-r--r--op-mode-definitions/l2tp-server.xml26
-rw-r--r--op-mode-definitions/lldp.xml37
-rw-r--r--op-mode-definitions/monitor-bandwidth-test.xml29
-rw-r--r--op-mode-definitions/monitor-bandwidth.xml23
-rw-r--r--op-mode-definitions/monitor-log.xml13
-rw-r--r--op-mode-definitions/monitor-ndp.xml44
-rw-r--r--op-mode-definitions/nat.xml98
-rw-r--r--op-mode-definitions/openvpn.xml140
-rw-r--r--op-mode-definitions/ping.xml23
-rw-r--r--op-mode-definitions/poweroff.xml52
-rw-r--r--op-mode-definitions/pppoe-server.xml104
-rw-r--r--op-mode-definitions/pptp-server.xml26
-rw-r--r--op-mode-definitions/reboot.xml52
-rw-r--r--op-mode-definitions/reset-conntrack.xml16
-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/reset-vpn.xml96
-rw-r--r--op-mode-definitions/restart-frr.xml63
-rw-r--r--op-mode-definitions/show-acceleration.xml63
-rw-r--r--op-mode-definitions/show-bridge.xml36
-rw-r--r--op-mode-definitions/show-configuration.xml37
-rw-r--r--op-mode-definitions/show-console-server.xml36
-rw-r--r--op-mode-definitions/show-environment.xml21
-rw-r--r--op-mode-definitions/show-hardware.xml94
-rw-r--r--op-mode-definitions/show-history.xml31
-rw-r--r--op-mode-definitions/show-host.xml44
-rw-r--r--op-mode-definitions/show-interfaces-bonding.xml59
-rw-r--r--op-mode-definitions/show-interfaces-bridge.xml42
-rw-r--r--op-mode-definitions/show-interfaces-dummy.xml42
-rw-r--r--op-mode-definitions/show-interfaces-ethernet.xml91
-rw-r--r--op-mode-definitions/show-interfaces-input.xml42
-rw-r--r--op-mode-definitions/show-interfaces-l2tpv3.xml42
-rw-r--r--op-mode-definitions/show-interfaces-loopback.xml42
-rw-r--r--op-mode-definitions/show-interfaces-macsec.xml29
-rw-r--r--op-mode-definitions/show-interfaces-pppoe.xml51
-rw-r--r--op-mode-definitions/show-interfaces-pseudo-ethernet.xml42
-rw-r--r--op-mode-definitions/show-interfaces-tunnel.xml42
-rw-r--r--op-mode-definitions/show-interfaces-vti.xml42
-rw-r--r--op-mode-definitions/show-interfaces-vxlan.xml42
-rw-r--r--op-mode-definitions/show-interfaces-wirelessmodem.xml51
-rw-r--r--op-mode-definitions/show-interfaces.xml27
-rw-r--r--op-mode-definitions/show-ip-access-paths-prefix-community-lists.xml116
-rw-r--r--op-mode-definitions/show-ip-bgp.xml342
-rw-r--r--op-mode-definitions/show-ip-igmp.xml48
-rw-r--r--op-mode-definitions/show-ip-multicast.xml42
-rw-r--r--op-mode-definitions/show-ip-ospf.xml579
-rw-r--r--op-mode-definitions/show-ip-pim.xml72
-rw-r--r--op-mode-definitions/show-ip-ports.xml17
-rw-r--r--op-mode-definitions/show-ip-rip.xml28
-rw-r--r--op-mode-definitions/show-ip-route.xml160
-rw-r--r--op-mode-definitions/show-ipv6-bgp.xml24
-rw-r--r--op-mode-definitions/show-license.xml13
-rw-r--r--op-mode-definitions/show-log.xml218
-rw-r--r--op-mode-definitions/show-login.xml33
-rw-r--r--op-mode-definitions/show-monitoring.xml13
-rw-r--r--op-mode-definitions/show-mpls.xml65
-rw-r--r--op-mode-definitions/show-ntp.xml31
-rw-r--r--op-mode-definitions/show-poweroff.xml13
-rw-r--r--op-mode-definitions/show-protocols-bfd.xml49
-rw-r--r--op-mode-definitions/show-protocols-static.xml32
-rw-r--r--op-mode-definitions/show-raid.xml16
-rw-r--r--op-mode-definitions/show-reboot.xml13
-rw-r--r--op-mode-definitions/show-route-map.xml22
-rw-r--r--op-mode-definitions/show-rpki.xml32
-rw-r--r--op-mode-definitions/show-system.xml183
-rw-r--r--op-mode-definitions/show-table.xml13
-rw-r--r--op-mode-definitions/show-users.xml30
-rw-r--r--op-mode-definitions/show-version.xml33
-rw-r--r--op-mode-definitions/show-vpn.xml20
-rw-r--r--op-mode-definitions/show-vrf.xml30
-rw-r--r--op-mode-definitions/snmp.xml111
-rw-r--r--op-mode-definitions/sstp-server.xml26
-rw-r--r--op-mode-definitions/telnet.xml30
-rw-r--r--op-mode-definitions/terminal.xml122
-rw-r--r--op-mode-definitions/traceroute.xml227
-rw-r--r--op-mode-definitions/traffic-dump.xml45
-rw-r--r--op-mode-definitions/vrrp.xml37
-rw-r--r--op-mode-definitions/wake-on-lan.xml26
-rw-r--r--op-mode-definitions/wireguard.xml138
-rw-r--r--op-mode-definitions/wireless.xml119
-rw-r--r--python/setup.py27
-rw-r--r--python/vyos/__init__.py1
-rw-r--r--python/vyos/airbag.py181
-rw-r--r--python/vyos/authutils.py41
-rw-r--r--python/vyos/base.py18
-rw-r--r--python/vyos/certbot_util.py58
-rw-r--r--python/vyos/component_versions.py57
-rw-r--r--python/vyos/config.py454
-rw-r--r--python/vyos/configdict.py314
-rw-r--r--python/vyos/configdiff.py249
-rw-r--r--python/vyos/configsession.py191
-rw-r--r--python/vyos/configsource.py318
-rw-r--r--python/vyos/configtree.py283
-rw-r--r--python/vyos/configverify.py139
-rw-r--r--python/vyos/debug.py205
-rw-r--r--python/vyos/defaults.py53
-rw-r--r--python/vyos/dicts.py53
-rw-r--r--python/vyos/formatversions.py109
-rw-r--r--python/vyos/frr.py288
-rw-r--r--python/vyos/hostsd_client.py119
-rw-r--r--python/vyos/ifconfig/__init__.py44
-rw-r--r--python/vyos/ifconfig/afi.py19
-rw-r--r--python/vyos/ifconfig/bond.py383
-rw-r--r--python/vyos/ifconfig/bridge.py263
-rw-r--r--python/vyos/ifconfig/control.py185
-rw-r--r--python/vyos/ifconfig/dummy.py56
-rw-r--r--python/vyos/ifconfig/ethernet.py309
-rw-r--r--python/vyos/ifconfig/geneve.py85
-rw-r--r--python/vyos/ifconfig/input.py31
-rw-r--r--python/vyos/ifconfig/interface.py1067
-rw-r--r--python/vyos/ifconfig/l2tpv3.py113
-rw-r--r--python/vyos/ifconfig/loopback.py89
-rw-r--r--python/vyos/ifconfig/macsec.py92
-rw-r--r--python/vyos/ifconfig/macvlan.py89
-rw-r--r--python/vyos/ifconfig/operational.py179
-rw-r--r--python/vyos/ifconfig/pppoe.py41
-rw-r--r--python/vyos/ifconfig/section.py189
-rw-r--r--python/vyos/ifconfig/stp.py70
-rw-r--r--python/vyos/ifconfig/tunnel.py338
-rw-r--r--python/vyos/ifconfig/vlan.py142
-rw-r--r--python/vyos/ifconfig/vrrp.py151
-rw-r--r--python/vyos/ifconfig/vti.py31
-rw-r--r--python/vyos/ifconfig/vtun.py44
-rw-r--r--python/vyos/ifconfig/vxlan.py130
-rw-r--r--python/vyos/ifconfig/wireguard.py247
-rw-r--r--python/vyos/ifconfig/wireless.py102
-rw-r--r--python/vyos/iflag.py38
-rw-r--r--python/vyos/initialsetup.py72
-rw-r--r--python/vyos/ioctl.py36
-rw-r--r--python/vyos/limericks.py72
-rw-r--r--python/vyos/logger.py143
-rw-r--r--python/vyos/migrator.py220
-rw-r--r--python/vyos/remote.py143
-rw-r--r--python/vyos/snmpv3_hashgen.py50
-rw-r--r--python/vyos/systemversions.py63
-rw-r--r--python/vyos/template.py143
-rw-r--r--python/vyos/util.py688
-rw-r--r--python/vyos/validate.py331
-rw-r--r--python/vyos/version.py107
-rw-r--r--python/vyos/xml/.gitignore1
-rw-r--r--python/vyos/xml/__init__.py59
-rw-r--r--python/vyos/xml/cache/__init__.py0
-rw-r--r--python/vyos/xml/definition.py330
-rwxr-xr-xpython/vyos/xml/generate.py70
-rw-r--r--python/vyos/xml/kw.py83
-rw-r--r--python/vyos/xml/load.py290
-rw-r--r--python/vyos/xml/test_xml.py279
-rw-r--r--schema/interface_definition.rnc175
-rw-r--r--schema/interface_definition.rng307
-rw-r--r--schema/op-mode-definition.rnc107
-rw-r--r--schema/op-mode-definition.rng168
-rwxr-xr-xscripts/build-command-op-templates225
-rwxr-xr-xscripts/build-command-templates306
-rwxr-xr-xscripts/build-component-versions47
-rwxr-xr-xscripts/import-conf-mode-commands255
-rwxr-xr-xsmoketest/bin/vyos-smoketest (renamed from bin/vyos-smoketest)0
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py (renamed from scripts/cli/base_interfaces_test.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bonding.py (renamed from scripts/cli/test_interfaces_bonding.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bridge.py (renamed from scripts/cli/test_interfaces_bridge.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_dummy.py (renamed from scripts/cli/test_interfaces_dummy.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_ethernet.py (renamed from scripts/cli/test_interfaces_ethernet.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_geneve.py (renamed from scripts/cli/test_interfaces_geneve.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_l2tpv3.py (renamed from scripts/cli/test_interfaces_l2tpv3.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_loopback.py (renamed from scripts/cli/test_interfaces_loopback.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_macsec.py (renamed from scripts/cli/test_interfaces_macsec.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_pppoe.py (renamed from scripts/cli/test_interfaces_pppoe.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_pseudo_ethernet.py (renamed from scripts/cli/test_interfaces_pseudo_ethernet.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_tunnel.py (renamed from scripts/cli/test_interfaces_tunnel.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_vxlan.py (renamed from scripts/cli/test_interfaces_vxlan.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireguard.py (renamed from scripts/cli/test_interfaces_wireguard.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireless.py (renamed from scripts/cli/test_interfaces_wireless.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wirelessmodem.py (renamed from scripts/cli/test_interfaces_wirelessmodem.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_nat.py (renamed from scripts/cli/test_nat.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_service_bcast-relay.py (renamed from scripts/cli/test_service_bcast-relay.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_service_dns_dynamic.py (renamed from scripts/cli/test_service_dns_dynamic.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_service_mdns-repeater.py (renamed from scripts/cli/test_service_mdns-repeater.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_service_pppoe-server.py (renamed from scripts/cli/test_service_pppoe-server.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_service_router-advert.py (renamed from scripts/cli/test_service_router-advert.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_service_snmp.py (renamed from scripts/cli/test_service_snmp.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_service_ssh.py (renamed from scripts/cli/test_service_ssh.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_system_lcd.py (renamed from scripts/cli/test_system_lcd.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py (renamed from scripts/cli/test_system_login.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_system_nameserver.py (renamed from scripts/cli/test_system_nameserver.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_system_ntp.py (renamed from scripts/cli/test_system_ntp.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_anyconnect.py (renamed from scripts/cli/test_vpn_anyconnect.py)0
-rwxr-xr-xsmoketest/scripts/cli/test_vrf.py (renamed from scripts/cli/test_vrf.py)0
-rwxr-xr-xsmoketest/scripts/system/test_module_load.py (renamed from scripts/system/test_module_load.py)0
-rw-r--r--sonar-project.properties21
-rw-r--r--sphinx/Makefile177
-rw-r--r--sphinx/source/conf.py261
-rw-r--r--sphinx/source/index.rst22
-rwxr-xr-xsrc/completion/list_disks.py36
-rwxr-xr-xsrc/completion/list_dumpable_interfaces.py12
-rwxr-xr-xsrc/completion/list_interfaces.py42
-rwxr-xr-xsrc/completion/list_ipoe.py16
-rwxr-xr-xsrc/completion/list_local.py24
-rwxr-xr-xsrc/completion/list_ntp_servers.sh4
-rwxr-xr-xsrc/completion/list_openvpn_clients.py57
-rwxr-xr-xsrc/completion/list_raidset.sh3
-rwxr-xr-xsrc/completion/list_wireless_phys.sh5
-rwxr-xr-xsrc/conf_mode/arp.py104
-rwxr-xr-xsrc/conf_mode/bcast_relay.py107
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py126
-rwxr-xr-xsrc/conf_mode/dhcp_server.py625
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py112
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py386
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py216
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py249
-rwxr-xr-xsrc/conf_mode/firewall_options.py147
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py371
-rwxr-xr-xsrc/conf_mode/host_name.py175
-rwxr-xr-xsrc/conf_mode/http-api.py113
-rwxr-xr-xsrc/conf_mode/https.py185
-rwxr-xr-xsrc/conf_mode/igmp_proxy.py142
-rwxr-xr-xsrc/conf_mode/intel_qat.py103
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py197
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py139
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py73
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py89
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py96
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py127
-rwxr-xr-xsrc/conf_mode/interfaces-loopback.py61
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py130
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py1116
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py132
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py123
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py718
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py120
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py115
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py260
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py127
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py227
-rwxr-xr-xsrc/conf_mode/le_cert.py115
-rwxr-xr-xsrc/conf_mode/lldp.py249
-rwxr-xr-xsrc/conf_mode/nat.py274
-rwxr-xr-xsrc/conf_mode/ntp.py82
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py216
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py102
-rwxr-xr-xsrc/conf_mode/protocols_igmp.py113
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py187
-rwxr-xr-xsrc/conf_mode/protocols_pim.py140
-rwxr-xr-xsrc/conf_mode/protocols_rip.py317
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py117
-rwxr-xr-xsrc/conf_mode/salt-minion.py123
-rwxr-xr-xsrc/conf_mode/service_console-server.py103
-rwxr-xr-xsrc/conf_mode/service_ids_fastnetmon.py89
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py309
-rwxr-xr-xsrc/conf_mode/service_mdns-repeater.py89
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py473
-rwxr-xr-xsrc/conf_mode/service_router-advert.py117
-rwxr-xr-xsrc/conf_mode/snmp.py581
-rwxr-xr-xsrc/conf_mode/ssh.py94
-rwxr-xr-xsrc/conf_mode/system-ip.py85
-rwxr-xr-xsrc/conf_mode/system-ipv6.py113
-rwxr-xr-xsrc/conf_mode/system-login-banner.py110
-rwxr-xr-xsrc/conf_mode/system-login.py403
-rwxr-xr-xsrc/conf_mode/system-options.py109
-rwxr-xr-xsrc/conf_mode/system-proxy.py95
-rwxr-xr-xsrc/conf_mode/system-syslog.py259
-rwxr-xr-xsrc/conf_mode/system-timezone.py57
-rwxr-xr-xsrc/conf_mode/system-wifi-regdom.py87
-rwxr-xr-xsrc/conf_mode/system_console.py141
-rwxr-xr-xsrc/conf_mode/system_lcd.py88
-rwxr-xr-xsrc/conf_mode/task_scheduler.py150
-rwxr-xr-xsrc/conf_mode/tftp_server.py149
-rwxr-xr-xsrc/conf_mode/vpn_anyconnect.py135
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py381
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py286
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py387
-rwxr-xr-xsrc/conf_mode/vrf.py269
-rwxr-xr-xsrc/conf_mode/vrrp.py256
-rwxr-xr-xsrc/conf_mode/vyos_cert.py144
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/01-vyos-logging20
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient27
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper87
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf44
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/05-vyos-mtureplace38
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup104
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442148
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook44
-rwxr-xr-xsrc/etc/ppp/ip-pre-up51
-rw-r--r--src/etc/rsyslog.d/01-auth.conf14
-rw-r--r--src/etc/sysctl.d/31-vyos-addr_gen_mode.conf14
-rw-r--r--src/etc/systemd/system/LCDd.service.d/override.conf8
-rw-r--r--src/etc/systemd/system/conserver-server.service.d/override.conf10
-rw-r--r--src/etc/systemd/system/hostapd@.service.d/override.conf10
-rw-r--r--src/etc/systemd/system/keepalived.service.d/override.conf2
-rw-r--r--src/etc/systemd/system/ocserv.service.d/override.conf14
-rw-r--r--src/etc/systemd/system/openvpn@.service.d/override.conf9
-rw-r--r--src/etc/systemd/system/pdns-recursor.service.d/override.conf8
-rw-r--r--src/etc/systemd/system/radvd.service.d/override.conf17
-rw-r--r--src/etc/systemd/system/wpa_supplicant@.service.d/override.conf10
-rw-r--r--src/etc/udev/rules.d/90-vyos-serial.rules28
-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.py66
-rwxr-xr-xsrc/helpers/run-config-migration.py86
-rwxr-xr-xsrc/helpers/system-versions-foot.py39
-rwxr-xr-xsrc/helpers/vyos-boot-config-loader.py178
-rwxr-xr-xsrc/helpers/vyos-bridge-sync.py51
-rwxr-xr-xsrc/helpers/vyos-load-config.py90
-rwxr-xr-xsrc/helpers/vyos-merge-config.py111
-rwxr-xr-xsrc/helpers/vyos-sudo.py33
-rwxr-xr-xsrc/migration-scripts/config-management/0-to-131
-rwxr-xr-xsrc/migration-scripts/dhcp-relay/1-to-235
-rwxr-xr-xsrc/migration-scripts/dhcp-server/4-to-5122
-rwxr-xr-xsrc/migration-scripts/dhcpv6-server/0-to-161
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/0-to-150
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/1-to-283
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/2-to-351
-rwxr-xr-xsrc/migration-scripts/https/0-to-169
-rwxr-xr-xsrc/migration-scripts/https/1-to-254
-rwxr-xr-xsrc/migration-scripts/interfaces/0-to-1118
-rwxr-xr-xsrc/migration-scripts/interfaces/1-to-263
-rwxr-xr-xsrc/migration-scripts/interfaces/10-to-1155
-rwxr-xr-xsrc/migration-scripts/interfaces/11-to-1258
-rwxr-xr-xsrc/migration-scripts/interfaces/2-to-343
-rwxr-xr-xsrc/migration-scripts/interfaces/3-to-497
-rwxr-xr-xsrc/migration-scripts/interfaces/4-to-5112
-rwxr-xr-xsrc/migration-scripts/interfaces/5-to-6123
-rwxr-xr-xsrc/migration-scripts/interfaces/6-to-763
-rwxr-xr-xsrc/migration-scripts/interfaces/7-to-876
-rwxr-xr-xsrc/migration-scripts/interfaces/8-to-952
-rwxr-xr-xsrc/migration-scripts/interfaces/9-to-1064
-rwxr-xr-xsrc/migration-scripts/ipoe-server/0-to-1133
-rwxr-xr-xsrc/migration-scripts/ipsec/4-to-533
-rwxr-xr-xsrc/migration-scripts/l2tp/0-to-160
-rwxr-xr-xsrc/migration-scripts/l2tp/1-to-233
-rwxr-xr-xsrc/migration-scripts/l2tp/2-to-3111
-rwxr-xr-xsrc/migration-scripts/lldp/0-to-135
-rwxr-xr-xsrc/migration-scripts/nat/4-to-558
-rwxr-xr-xsrc/migration-scripts/ntp/0-to-136
-rwxr-xr-xsrc/migration-scripts/pppoe-server/0-to-137
-rwxr-xr-xsrc/migration-scripts/pppoe-server/1-to-238
-rwxr-xr-xsrc/migration-scripts/pppoe-server/2-to-3141
-rwxr-xr-xsrc/migration-scripts/pppoe-server/3-to-454
-rwxr-xr-xsrc/migration-scripts/pptp/0-to-159
-rwxr-xr-xsrc/migration-scripts/pptp/1-to-271
-rwxr-xr-xsrc/migration-scripts/quagga/2-to-3203
-rwxr-xr-xsrc/migration-scripts/quagga/3-to-476
-rwxr-xr-xsrc/migration-scripts/quagga/4-to-563
-rwxr-xr-xsrc/migration-scripts/quagga/5-to-663
-rwxr-xr-xsrc/migration-scripts/salt/0-to-158
-rwxr-xr-xsrc/migration-scripts/snmp/0-to-156
-rwxr-xr-xsrc/migration-scripts/snmp/1-to-289
-rwxr-xr-xsrc/migration-scripts/ssh/0-to-132
-rwxr-xr-xsrc/migration-scripts/ssh/1-to-255
-rwxr-xr-xsrc/migration-scripts/sstp/0-to-1130
-rwxr-xr-xsrc/migration-scripts/sstp/1-to-2110
-rwxr-xr-xsrc/migration-scripts/system/10-to-1171
-rwxr-xr-xsrc/migration-scripts/system/11-to-1247
-rwxr-xr-xsrc/migration-scripts/system/12-to-1370
-rwxr-xr-xsrc/migration-scripts/system/13-to-1440
-rwxr-xr-xsrc/migration-scripts/system/14-to-1537
-rwxr-xr-xsrc/migration-scripts/system/15-to-1655
-rwxr-xr-xsrc/migration-scripts/system/16-to-1776
-rwxr-xr-xsrc/migration-scripts/system/17-to-18105
-rwxr-xr-xsrc/migration-scripts/system/6-to-748
-rwxr-xr-xsrc/migration-scripts/system/7-to-845
-rwxr-xr-xsrc/migration-scripts/system/8-to-932
-rwxr-xr-xsrc/migration-scripts/system/9-to-1036
-rwxr-xr-xsrc/migration-scripts/vrrp/1-to-2270
-rwxr-xr-xsrc/migration-scripts/webproxy/1-to-239
-rwxr-xr-xsrc/op_mode/anyconnect-control.py67
-rwxr-xr-xsrc/op_mode/clear_conntrack.py26
-rwxr-xr-xsrc/op_mode/connect_disconnect.py87
-rwxr-xr-xsrc/op_mode/cpu_summary.py36
-rwxr-xr-xsrc/op_mode/dns_forwarding_reset.py54
-rwxr-xr-xsrc/op_mode/dns_forwarding_restart.sh8
-rwxr-xr-xsrc/op_mode/dns_forwarding_statistics.py32
-rwxr-xr-xsrc/op_mode/dynamic_dns.py104
-rwxr-xr-xsrc/op_mode/flow_accounting_op.py252
-rwxr-xr-xsrc/op_mode/format_disk.py143
-rwxr-xr-xsrc/op_mode/generate_ssh_server_key.py26
-rwxr-xr-xsrc/op_mode/ipoe-control.py65
-rwxr-xr-xsrc/op_mode/lldp_op.py125
-rwxr-xr-xsrc/op_mode/maya_date.py208
-rwxr-xr-xsrc/op_mode/ping.py230
-rwxr-xr-xsrc/op_mode/powerctrl.py193
-rwxr-xr-xsrc/op_mode/ppp-server-ctrl.py71
-rwxr-xr-xsrc/op_mode/reset_openvpn.py31
-rwxr-xr-xsrc/op_mode/reset_vpn.py72
-rwxr-xr-xsrc/op_mode/restart_dhcp_relay.py55
-rwxr-xr-xsrc/op_mode/restart_frr.py197
-rwxr-xr-xsrc/op_mode/show_acceleration.py116
-rwxr-xr-xsrc/op_mode/show_configuration_files.sh10
-rwxr-xr-xsrc/op_mode/show_cpu.py61
-rwxr-xr-xsrc/op_mode/show_current_user.sh18
-rwxr-xr-xsrc/op_mode/show_dhcp.py264
-rwxr-xr-xsrc/op_mode/show_dhcpv6.py219
-rwxr-xr-xsrc/op_mode/show_disk_format.sh8
-rwxr-xr-xsrc/op_mode/show_igmpproxy.py241
-rwxr-xr-xsrc/op_mode/show_interfaces.py304
-rwxr-xr-xsrc/op_mode/show_ipsec_sa.py111
-rwxr-xr-xsrc/op_mode/show_nat_statistics.py63
-rwxr-xr-xsrc/op_mode/show_nat_translations.py200
-rwxr-xr-xsrc/op_mode/show_openvpn.py178
-rwxr-xr-xsrc/op_mode/show_raid.sh17
-rwxr-xr-xsrc/op_mode/show_ram.sh33
-rwxr-xr-xsrc/op_mode/show_sensors.py27
-rwxr-xr-xsrc/op_mode/show_usb_serial.py57
-rwxr-xr-xsrc/op_mode/show_users.py111
-rwxr-xr-xsrc/op_mode/show_version.py73
-rwxr-xr-xsrc/op_mode/show_vpn_ra.py56
-rwxr-xr-xsrc/op_mode/show_vrf.py67
-rwxr-xr-xsrc/op_mode/show_wireless.py156
-rwxr-xr-xsrc/op_mode/snmp.py78
-rwxr-xr-xsrc/op_mode/snmp_ifmib.py121
-rwxr-xr-xsrc/op_mode/snmp_v3.py180
-rwxr-xr-xsrc/op_mode/snmp_v3_showcerts.sh8
-rwxr-xr-xsrc/op_mode/system_integrity.py70
-rwxr-xr-xsrc/op_mode/toggle_help_binding.sh25
-rwxr-xr-xsrc/op_mode/vrrp.py55
-rwxr-xr-xsrc/op_mode/wireguard.py159
-rw-r--r--src/pam-configs/radius20
-rwxr-xr-xsrc/services/vyos-hostsd618
-rwxr-xr-xsrc/services/vyos-http-api-server400
-rwxr-xr-xsrc/system/keepalived-fifo.py188
-rwxr-xr-xsrc/system/normalize-ip43
-rwxr-xr-xsrc/system/on-dhcp-event.sh54
-rwxr-xr-xsrc/system/post-upgrade3
-rwxr-xr-xsrc/system/unpriv-ip2
-rw-r--r--src/systemd/accel-ppp@.service16
-rw-r--r--src/systemd/ddclient.service14
-rw-r--r--src/systemd/dhclient@.service18
-rw-r--r--src/systemd/dhcp6c@.service17
-rw-r--r--src/systemd/dropbear@.service14
-rw-r--r--src/systemd/dropbearkey.service11
-rw-r--r--src/systemd/isc-dhcp-relay.service20
-rw-r--r--src/systemd/isc-dhcp-relay6.service20
-rw-r--r--src/systemd/isc-dhcp-server.service24
-rw-r--r--src/systemd/isc-dhcp-server6.service24
-rw-r--r--src/systemd/lcdproc.service13
-rw-r--r--src/systemd/ppp@.service11
-rw-r--r--src/systemd/tftpd@.service14
-rw-r--r--src/systemd/vyos-beep.service11
-rw-r--r--src/systemd/vyos-hostsd.service34
-rw-r--r--src/systemd/vyos-http-api.service24
-rw-r--r--src/systemd/wpa_supplicant-macsec@.service17
-rw-r--r--src/tests/helper.py27
-rw-r--r--src/tests/test_config_parser.py61
-rw-r--r--src/tests/test_initial_setup.py105
-rw-r--r--src/tests/test_task_scheduler.py130
-rw-r--r--src/tests/test_util.py34
-rw-r--r--src/utils/initial-setup40
-rwxr-xr-xsrc/utils/vyos-config-file-query100
-rwxr-xr-xsrc/utils/vyos-config-to-commands29
-rwxr-xr-xsrc/utils/vyos-config-to-json40
-rwxr-xr-xsrc/utils/vyos-hostsd-client165
-rwxr-xr-xsrc/validators/dotted-decimal33
-rwxr-xr-xsrc/validators/file-exists61
-rwxr-xr-xsrc/validators/fqdn30
-rwxr-xr-xsrc/validators/interface-address3
-rwxr-xr-xsrc/validators/ip-address3
-rwxr-xr-xsrc/validators/ip-cidr3
-rwxr-xr-xsrc/validators/ip-host3
-rwxr-xr-xsrc/validators/ip-prefix3
-rwxr-xr-xsrc/validators/ip-protocol41
-rwxr-xr-xsrc/validators/ipv4-address3
-rwxr-xr-xsrc/validators/ipv4-address-exclude7
-rwxr-xr-xsrc/validators/ipv4-host3
-rwxr-xr-xsrc/validators/ipv4-prefix3
-rwxr-xr-xsrc/validators/ipv4-prefix-exclude7
-rwxr-xr-xsrc/validators/ipv4-range33
-rwxr-xr-xsrc/validators/ipv4-range-exclude7
-rwxr-xr-xsrc/validators/ipv63
-rwxr-xr-xsrc/validators/ipv6-address3
-rwxr-xr-xsrc/validators/ipv6-host3
-rwxr-xr-xsrc/validators/ipv6-prefix3
-rwxr-xr-xsrc/validators/mac-address29
-rwxr-xr-xsrc/validators/script45
-rwxr-xr-xsrc/validators/timezone33
-rwxr-xr-xsrc/validators/vrf-name41
-rwxr-xr-xsrc/validators/wireless-phy25
-rw-r--r--test-requirements.txt5
-rw-r--r--tests/data/config.boot.default40
-rw-r--r--tests/data/config.valid39
-rw-r--r--tests/data/interface-definitions/test-op.xml21
-rw-r--r--tests/data/interface-definitions/test.xml24
724 files changed, 71528 insertions, 41 deletions
diff --git a/.gitignore b/.gitignore
index e57e2953a..e86ef4036 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,139 @@
-debian/.debhelper/
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+.idea/
+.idea
+.idea/*
+*.iml
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# dotenv
+.env
+
+# virtualenv
+.venv
+venv/
+ENV/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+# Autogenerated files
+templates-cfg/*
+templates-op/*
+tests/templates/*
+
+# Debian packaging
debian/files
-debian/vyos-smoketest.debhelper.log
-debian/vyos-smoketest.substvars
-debian/vyos-smoketest/
+debian/tmp
+debian/debhelper-build-stamp
+debian/.debhelper/
+debian/vyos-1x
+debian/vyos-1x-vmware
+debian/vyos-1x-smoketest
+debian/*.postinst.debhelper
+debian/*.prerm.debhelper
+debian/*.substvars
+
+# Sonar Cloud
+.scannerwork
+/.vs
+
+# SlickEdit
+*.vpj
+*.vpw
+*.vpwhist
+*.vtg
+
+# VIM
+*.swp
+
+# vyos-1x JSON version
+data/component-versions.json
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..d8177a5f5
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,12 @@
+# Contributing to VyOS
+
+You wan't to help us improve VyOS? This is awesome. We accept any kind of Pull
+Requests on GitHub. To make the life of the maintainers and you as future
+contributor (or maybe maintainer) much easier we have come up with some basic
+rules. Instead of copy/pasting or maintaining two instances of how to contribute
+to VyOS you can find the entire process documented in our online documentation:
+https://docs.vyos.io/en/latest/contributing/development.html
+
+Also this guide might not be complete so any PR is much appreciated.
+
+It might also worth browsing our blog: https://blog.vyos.io
diff --git a/LICENSE.GPL b/LICENSE.GPL
new file mode 100644
index 000000000..23cb79033
--- /dev/null
+++ b/LICENSE.GPL
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {description}
+ Copyright (C) {year} {fullname}
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ 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, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ {signature of Ty Coon}, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/LICENSE.LGPL b/LICENSE.LGPL
new file mode 100644
index 000000000..4362b4915
--- /dev/null
+++ b/LICENSE.LGPL
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/Makefile b/Makefile
index e3d0d706c..5b7e4da63 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,148 @@
+TMPL_DIR := templates-cfg
+OP_TMPL_DIR := templates-op
+BUILD_DIR := build
+DATA_DIR := data
+CFLAGS :=
+
+src = $(wildcard interface-definitions/*.xml.in)
+obj = $(src:.xml.in=.xml)
+
+%.xml: %.xml.in
+ @echo Generating $(BUILD_DIR)/$@ from $<
+ # -ansi This turns off certain features of GCC that are incompatible
+ # with ISO C90. Without this regexes containing '/' as in an URL
+ # won't work
+ # -x c By default GCC guesses the input language from its file extension,
+ # thus XML is unknown. Force it to C language
+ # -E Stop after the preprocessing stage
+ # -undef Do not predefine any system-specific or GCC-specific macros.
+ # -nostdinc Do not search the standard system directories for header files
+ # -P Inhibit generation of linemarkers in the output from the
+ # preprocessor
+ @$(CC) -x c-header -E -undef -nostdinc -P -I$(CURDIR)/interface-definitions -o $(BUILD_DIR)/$@ -c $<
+
+$(BUILD_DIR):
+ install -d -m 0755 $(BUILD_DIR)/interface-definitions
+ install -d -m 0755 $(BUILD_DIR)/op-mode-definitions
+
+.PHONY: interface_definitions
+.ONESHELL:
+interface_definitions: $(BUILD_DIR) $(obj)
+ mkdir -p $(TMPL_DIR)
+
+ find $(BUILD_DIR)/interface-definitions -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-templates {} $(CURDIR)/schema/interface_definition.rng $(TMPL_DIR) || exit 1
+
+ # XXX: delete top level node.def's that now live in other packages
+ 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/ip/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/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 -rf $(TMPL_DIR)/protocols/nbgp
+ rm -rf $(TMPL_DIR)/protocols/isis
+ rm -f $(TMPL_DIR)/protocols/static/node.def
+ rm -f $(TMPL_DIR)/system/node.def
+ rm -f $(TMPL_DIR)/vpn/node.def
+ rm -f $(TMPL_DIR)/vpn/ipsec/node.def
+
+.PHONY: op_mode_definitions
+.ONESHELL:
+op_mode_definitions:
+ mkdir -p $(OP_TMPL_DIR)
+
+ find $(CURDIR)/op-mode-definitions/ -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-op-templates {} $(CURDIR)/schema/op-mode-definition.rng $(OP_TMPL_DIR) || exit 1
+
+ # XXX: delete top level op mode node.def's that now live in other packages
+ rm -f $(OP_TMPL_DIR)/add/node.def
+ rm -f $(OP_TMPL_DIR)/clear/node.def
+ rm -f $(OP_TMPL_DIR)/clear/interfaces/node.def
+ rm -f $(OP_TMPL_DIR)/set/node.def
+ rm -f $(OP_TMPL_DIR)/show/node.def
+ rm -f $(OP_TMPL_DIR)/show/interfaces/node.def
+ rm -f $(OP_TMPL_DIR)/show/ipv6/node.def
+ rm -f $(OP_TMPL_DIR)/show/ipv6/bgp/node.def
+ rm -f $(OP_TMPL_DIR)/show/ipv6/route/node.def
+ rm -f $(OP_TMPL_DIR)/restart/node.def
+ rm -f $(OP_TMPL_DIR)/monitor/node.def
+ rm -f $(OP_TMPL_DIR)/generate/node.def
+ rm -f $(OP_TMPL_DIR)/show/system/node.def
+ rm -f $(OP_TMPL_DIR)/show/vpn/node.def
+ rm -f $(OP_TMPL_DIR)/delete/node.def
+ rm -f $(OP_TMPL_DIR)/reset/vpn/node.def
+
+ # XXX: ping must be able to recursivly call itself as the
+ # options are provided from the script itself
+ ln -s ../node.tag $(OP_TMPL_DIR)/ping/node.tag/node.tag/
+
+.PHONY: component_versions
+.ONESHELL:
+component_versions: $(BUILD_DIR) $(obj)
+ $(CURDIR)/scripts/build-component-versions $(BUILD_DIR)/interface-definitions $(DATA_DIR)
+
.PHONY: all
+all: clean interface_definitions op_mode_definitions component_versions
-all:
- # Install is just xcopy
+.PHONY: clean
+clean:
+ rm -rf $(BUILD_DIR)
+ rm -rf $(TMPL_DIR)
+ rm -rf $(OP_TMPL_DIR)
+
+.PHONY: test
+test:
+ set -e; python3 -m compileall -q .
+ PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators,src/tests --verbose
+
+.PHONY: sonar
+sonar:
+ sonar-scanner -X -Dsonar.login=${SONAR_TOKEN}
+
+.PHONY: docs
+.ONESHELL:
+docs:
+ sphinx-apidoc -o sphinx/source/ python/
+ cd sphinx/
+ PYTHONPATH=../python make html
deb:
dpkg-buildpackage -uc -us -tc -b
+
+.PHONY: schema
+schema:
+ trang -I rnc -O rng schema/interface_definition.rnc schema/interface_definition.rng
+ trang -I rnc -O rng schema/op-mode-definition.rnc schema/op-mode-definition.rng
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 000000000..5bb9fb73d
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,17 @@
+[[source]]
+name = "pypi"
+url = "https://pypi.org/simple"
+verify_ssl = true
+
+[dev-packages]
+lxml = "*"
+pylint = "*"
+nose = "*"
+coverage = "*"
+
+[packages]
+vyos = {file = "./python"}
+jinja2 = "*"
+
+[requires]
+python_version = "3.6"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 000000000..eae5ea9fe
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,248 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "5564acff86bcf8ef717129935c288ffed2631f1d795d1c44d0af36779593b89b"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.6"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "jinja2": {
+ "hashes": [
+ "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
+ "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
+ ],
+ "index": "pypi",
+ "version": "==2.10.1"
+ },
+ "markupsafe": {
+ "hashes": [
+ "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
+ "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
+ "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
+ "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
+ "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
+ "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
+ "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
+ "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
+ "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
+ "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
+ "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
+ "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
+ "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
+ "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
+ "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
+ "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
+ "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
+ "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
+ "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
+ "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
+ "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
+ "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
+ "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
+ "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
+ "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
+ "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
+ "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
+ "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
+ ],
+ "version": "==1.1.0"
+ },
+ "vyos": {
+ "file": "./python"
+ }
+ },
+ "develop": {
+ "astroid": {
+ "hashes": [
+ "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22",
+ "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e"
+ ],
+ "version": "==2.1.0"
+ },
+ "coverage": {
+ "hashes": [
+ "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f",
+ "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe",
+ "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d",
+ "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0",
+ "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607",
+ "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d",
+ "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b",
+ "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3",
+ "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e",
+ "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815",
+ "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36",
+ "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1",
+ "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14",
+ "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c",
+ "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794",
+ "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b",
+ "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840",
+ "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd",
+ "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82",
+ "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952",
+ "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389",
+ "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f",
+ "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4",
+ "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da",
+ "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647",
+ "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d",
+ "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42",
+ "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478",
+ "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b",
+ "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb",
+ "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9"
+ ],
+ "index": "pypi",
+ "version": "==4.5.2"
+ },
+ "isort": {
+ "hashes": [
+ "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
+ "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
+ "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
+ ],
+ "version": "==4.3.4"
+ },
+ "lazy-object-proxy": {
+ "hashes": [
+ "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33",
+ "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39",
+ "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019",
+ "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088",
+ "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b",
+ "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e",
+ "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6",
+ "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b",
+ "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5",
+ "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff",
+ "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd",
+ "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7",
+ "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff",
+ "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d",
+ "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2",
+ "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35",
+ "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4",
+ "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514",
+ "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252",
+ "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109",
+ "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f",
+ "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c",
+ "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92",
+ "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577",
+ "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d",
+ "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d",
+ "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f",
+ "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a",
+ "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b"
+ ],
+ "version": "==1.3.1"
+ },
+ "lxml": {
+ "hashes": [
+ "sha256:0dd6589fa75d369ba06d2b5f38dae107f76ea127f212f6a7bee134f6df2d1d21",
+ "sha256:1afbac344aa68c29e81ab56c1a9411c3663157b5aee5065b7fa030b398d4f7e0",
+ "sha256:1baad9d073692421ad5dbbd81430aba6c7f5fdc347f03537ae046ddf2c9b2297",
+ "sha256:1d8736421a2358becd3edf20260e41a06a0bf08a560480d3a5734a6bcbacf591",
+ "sha256:1e1d9bddc5afaddf0de76246d3f2152f961697ad7439c559f179002682c45801",
+ "sha256:1f179dc8b2643715f020f4d119d5529b02cd794c1c8f305868b73b8674d2a03f",
+ "sha256:241fb7bdf97cb1df1edfa8f0bcdfd80525d4023dac4523a241907c8b2f44e541",
+ "sha256:2f9765ee5acd3dbdcdc0d0c79309e01f7c16bc8d39b49250bf88de7b46daaf58",
+ "sha256:312e1e1b1c3ce0c67e0b8105317323e12807955e8186872affb667dbd67971f6",
+ "sha256:3273db1a8055ca70257fd3691c6d2c216544e1a70b673543e15cc077d8e9c730",
+ "sha256:34dfaa8c02891f9a246b17a732ca3e99c5e42802416628e740a5d1cb2f50ff49",
+ "sha256:3aa3f5288af349a0f3a96448ebf2e57e17332d99f4f30b02093b7948bd9f94cc",
+ "sha256:51102e160b9d83c1cc435162d90b8e3c8c93b28d18d87b60c56522d332d26879",
+ "sha256:56115fc2e2a4140e8994eb9585119a1ae9223b506826089a3ba753a62bd194a6",
+ "sha256:69d83de14dbe8fe51dccfd36f88bf0b40f5debeac763edf9f8325180190eba6e",
+ "sha256:99fdce94aeaa3ccbdfcb1e23b34273605c5853aa92ec23d84c84765178662c6c",
+ "sha256:a7c0cd5b8a20f3093ee4a67374ccb3b8a126743b15a4d759e2a1bf098faac2b2",
+ "sha256:abe12886554634ed95416a46701a917784cb2b4c77bfacac6916681d49bbf83d",
+ "sha256:b4f67b5183bd5f9bafaeb76ad119e977ba570d2b0e61202f534ac9b5c33b4485",
+ "sha256:bdd7c1658475cc1b867b36d5c4ed4bc316be8d3368abe03d348ba906a1f83b0e",
+ "sha256:c6f24149a19f611a415a51b9bc5f17b6c2f698e0d6b41ffb3fa9f24d35d05d73",
+ "sha256:d1e111b3ab98613115a208c1017f266478b0ab224a67bc8eac670fa0bad7d488",
+ "sha256:d6520aa965773bbab6cb7a791d5895b00d02cf9adc93ac2bf4edb9ac1a6addc5",
+ "sha256:dd185cde2ccad7b649593b0cda72021bc8a91667417001dbaf24cd746ecb7c11",
+ "sha256:de2e5b0828a9d285f909b5d2e9d43f1cf6cf21fe65bc7660bdaa1780c7b58298",
+ "sha256:f726444b8e909c4f41b4fde416e1071cf28fa84634bfb4befdf400933b6463af"
+ ],
+ "index": "pypi",
+ "version": "==4.3.0"
+ },
+ "mccabe": {
+ "hashes": [
+ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
+ "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
+ ],
+ "version": "==0.6.1"
+ },
+ "nose": {
+ "hashes": [
+ "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac",
+ "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a",
+ "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"
+ ],
+ "index": "pypi",
+ "version": "==1.3.7"
+ },
+ "pylint": {
+ "hashes": [
+ "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492",
+ "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c"
+ ],
+ "index": "pypi",
+ "version": "==2.2.2"
+ },
+ "six": {
+ "hashes": [
+ "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
+ "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
+ ],
+ "version": "==1.12.0"
+ },
+ "typed-ast": {
+ "hashes": [
+ "sha256:023625bfa9359e29bd6e24cac2a4503495b49761d48a5f1e38333fc4ac4d93fe",
+ "sha256:07591f7a5fdff50e2e566c4c1e9df545c75d21e27d98d18cb405727ed0ef329c",
+ "sha256:153e526b0f4ffbfada72d0bb5ffe8574ba02803d2f3a9c605c8cf99dfedd72a2",
+ "sha256:3ad2bdcd46a4a1518d7376e9f5016d17718a9ed3c6a3f09203d832f6c165de4a",
+ "sha256:3ea98c84df53ada97ee1c5159bb3bc784bd734231235a1ede14c8ae0775049f7",
+ "sha256:51a7141ccd076fa561af107cfb7a8b6d06a008d92451a1ac7e73149d18e9a827",
+ "sha256:52c93cd10e6c24e7ac97e8615da9f224fd75c61770515cb323316c30830ddb33",
+ "sha256:6344c84baeda3d7b33e157f0b292e4dd53d05ddb57a63f738178c01cac4635c9",
+ "sha256:64699ca1b3bd5070bdeb043e6d43bc1d0cebe08008548f4a6bee782b0ecce032",
+ "sha256:74903f2e56bbffe29282ef8a5487d207d10be0f8513b41aff787d954a4cf91c9",
+ "sha256:7891710dba83c29ee2bd51ecaa82f60f6bede40271af781110c08be134207bf2",
+ "sha256:91976c56224e26c256a0de0f76d2004ab885a29423737684b4f7ebdd2f46dde2",
+ "sha256:9bad678a576ecc71f25eba9f1e3fd8d01c28c12a2834850b458428b3e855f062",
+ "sha256:b4726339a4c180a8b6ad9d8b50d2b6dc247e1b79b38fe2290549c98e82e4fd15",
+ "sha256:ba36f6aa3f8933edf94ea35826daf92cbb3ec248b89eccdc053d4a815d285357",
+ "sha256:bbc96bde544fd19e9ef168e4dfa5c3dfe704bfa78128fa76f361d64d6b0f731a",
+ "sha256:c0c927f1e44469056f7f2dada266c79b577da378bbde3f6d2ada726d131e4824",
+ "sha256:c0f9a3708008aa59f560fa1bd22385e05b79b8e38e0721a15a8402b089243442",
+ "sha256:f0bf6f36ff9c5643004171f11d2fdc745aa3953c5aacf2536a0685db9ceb3fb1",
+ "sha256:f5be39a0146be663cbf210a4d95c3c58b2d7df7b043c9047c5448e358f0550a2",
+ "sha256:fcd198bf19d9213e5cbf2cde2b9ef20a9856e716f76f9476157f90ae6de06cc6"
+ ],
+ "markers": "python_version < '3.7' and implementation_name == 'cpython'",
+ "version": "==1.2.0"
+ },
+ "wrapt": {
+ "hashes": [
+ "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533"
+ ],
+ "version": "==1.11.1"
+ }
+ }
+}
diff --git a/README.md b/README.md
index d02557393..896f28b3d 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,63 @@
-vyos-smoketest
-==============
+# vyos-1x: VyOS 1.2.0+ configuration scripts and data
-This is a set of scripts and test data for sanity checking VyOS builds.
+[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=vyos%3Avyos-1x&metric=coverage)](https://sonarcloud.io/component_measures?id=vyos%3Avyos-1x&metric=coverage)
+[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_shield)
-The main entry point is /usr/bin/vyos-smoketest
+VyOS 1.1.x had its codebase split into way too many submodules for no good reason, which made it hard
+to navigate or write meaningful changelogs. As the code undergoes rewrite in the new style in VyOS 1.2.0+,
+we consolidate the rewritten code in this package.
-It will try to check for common things that break such as kernel modules not loading,
-and print a test report.
+If you just want to build a VyOS image, the repository you want is [vyos-build](https://github.com/vyos/vyos-build).
+If you also want to contribute to VyOS, read on.
-It also comes with a huge reference config that has almost every feature set.
+## Package layout
+
+```
+interface-definitions # Configuration interface (i.e. conf mode command) definitions
+op-mode-definitions # Operational command definitions
+src
+ conf_mode/ # Configuration mode scripts
+ op_mode/ # Operational mode scripts
+ completion/ # Completion helpers
+ validators/ # Value validators
+ helpers/ # Misc helpers
+ migration-scripts # Migration scripts
+ tests/ # Unit tests
+
+python/ # Python modules
+
+scripts/ # Build-time scripts
+schema/ # XML schemas
+```
+
+## Interface/command definitions
+
+Raw node.def files for the old backend are no longer written by hand or generated by custom sciprts.
+They are all now produced from a unified XML format that supports a strict subset of the old backend
+features. In particular, it intentionally does not support embedded shell scripts, default values,
+and value "types", instead delegating those tasks to external scripts.
+
+Configuration interface definitions must conform to the schema found in schema/interface_definition.rng
+and operational command definitions must conform to schema/op-mode-definition.rng
+Schema checks are performed at build time, so a package with malformed interface definitions will not build.
+
+## Configuration scripts
+
+The guidelines in a nutshell:
+
+* Use separate functions for retrieving configuration data, validating it, and generating taret config
+* Use a template processor when the format is more complex than just one line (jinja2 and pystache are acceptable options)
+
+## Tests
+
+Tests are executed at build time, you can also execute them by hand with:
+
+```
+pipenv install
+pipenv shell
+make test
+```
+
+
+## License
+[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_large) \ No newline at end of file
diff --git a/data/templates/accel-ppp/chap-secrets.ipoe.tmpl b/data/templates/accel-ppp/chap-secrets.ipoe.tmpl
new file mode 100644
index 000000000..a7d899354
--- /dev/null
+++ b/data/templates/accel-ppp/chap-secrets.ipoe.tmpl
@@ -0,0 +1,18 @@
+# username server password acceptable local IP addresses shaper
+{% for interface in auth_interfaces -%}
+{% for mac in interface.mac -%}
+{% if mac.rate_upload and mac.rate_download -%}
+{% if mac.vlan_id -%}
+{{ interface.name }}.{{ mac.vlan_id }} * {{ mac.address | lower }} * {{ mac.rate_download }}/{{ mac.rate_upload }}
+{% else -%}
+{{ interface.name }} * {{ mac.address | lower }} * {{ mac.rate_download }}/{{ mac.rate_upload }}
+{% endif -%}
+{% else -%}
+{% if mac.vlan_id -%}
+{{ interface.name }}.{{ mac.vlan_id }} * {{ mac.address | lower }} *
+{% else -%}
+{{ interface.name }} * {{ mac.address | lower }} *
+{% endif -%}
+{% endif -%}
+{% endfor -%}
+{% endfor -%}
diff --git a/data/templates/accel-ppp/chap-secrets.tmpl b/data/templates/accel-ppp/chap-secrets.tmpl
new file mode 100644
index 000000000..dd00d7bd0
--- /dev/null
+++ b/data/templates/accel-ppp/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/accel-ppp/ipoe.config.tmpl b/data/templates/accel-ppp/ipoe.config.tmpl
new file mode 100644
index 000000000..fca520efa
--- /dev/null
+++ b/data/templates/accel-ppp/ipoe.config.tmpl
@@ -0,0 +1,111 @@
+### generated by ipoe.py ###
+[modules]
+log_syslog
+ipoe
+shaper
+ipv6pool
+ipv6_nd
+ipv6_dhcp
+ippool
+{% if auth_mode == 'radius' %}
+radius
+{% elif auth_mode == 'local' %}
+chap-secrets
+{% endif %}
+
+[core]
+thread-count={{ thread_cnt }}
+
+[log]
+syslog=accel-ipoe,daemon
+copy=1
+level=5
+
+[ipoe]
+verbose=1
+{% for interface in interfaces %}
+{% if interface.vlan_mon %}
+interface=re:{{ interface.name }}\.\d+,{% else %}interface={{ interface.name }},{% endif %}shared={{ interface.shared }},mode={{ interface.mode }},ifcfg={{ interface.ifcfg }},range={{ interface.range }},start={{ interface.sess_start }},ipv6=1
+{% endfor %}
+{% if auth_mode == 'noauth' %}
+noauth=1
+{% elif auth_mode == 'local' %}
+username=ifname
+password=csid
+{% endif %}
+
+{%- for interface in interfaces %}
+{% if (interface.shared == '0') and (interface.vlan_mon) %}
+vlan-mon={{ interface.name }},{{ interface.vlan_mon | join(',') }}
+{% endif %}
+{% endfor %}
+
+{% if dnsv4 %}
+[dns]
+{% for dns in dnsv4 -%}
+dns{{ loop.index }}={{ dns }}
+{% endfor -%}
+{% endif %}
+
+{% if dnsv6 %}
+[ipv6-dns]
+{% for dns in dnsv6 -%}
+{{ dns }}
+{% endfor -%}
+{% endif %}
+
+[ipv6-nd]
+verbose=1
+
+[ipv6-dhcp]
+verbose=1
+
+{% if client_ipv6_pool %}
+[ipv6-pool]
+{% for p in client_ipv6_pool %}
+{{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% for p in client_ipv6_delegate_prefix %}
+delegate={{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% endif %}
+
+{% if auth_mode == 'local' %}
+[chap-secrets]
+chap-secrets={{ chap_secrets_file }}
+{% elif auth_mode == 'radius' %}
+[radius]
+verbose=1
+{% for r in radius_server %}
+server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }}
+{% endfor -%}
+
+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 -%}
+
+{% if radius_shaper_attr %}
+[shaper]
+verbose=1
+attr={{ radius_shaper_attr }}
+{% if radius_shaper_vendor %}
+vendor={{ radius_shaper_vendor }}
+{% endif -%}
+{% endif -%}
+{% endif %}
+
+[cli]
+tcp=127.0.0.1:2002
diff --git a/data/templates/accel-ppp/l2tp.config.tmpl b/data/templates/accel-ppp/l2tp.config.tmpl
new file mode 100644
index 000000000..b9131684d
--- /dev/null
+++ b/data/templates/accel-ppp/l2tp.config.tmpl
@@ -0,0 +1,148 @@
+### generated by accel_l2tp.py ###
+[modules]
+log_syslog
+l2tp
+chap-secrets
+{% for proto in auth_proto: %}
+{{proto}}
+{% endfor%}
+
+{% if auth_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 dnsv4 %}
+[dns]
+{% for dns in dnsv4 -%}
+dns{{ loop.index }}={{ dns }}
+{% endfor -%}
+{% endif %}
+
+{% if dnsv6 %}
+[ipv6-dns]
+{% for dns in dnsv6 -%}
+{{ dns }}
+{% endfor -%}
+{% endif %}
+
+{% if wins %}
+[wins]
+{% for server in wins -%}
+wins{{ loop.index }}={{ server }}
+{% endfor -%}
+{% endif %}
+
+[l2tp]
+verbose=1
+ifname=l2tp%d
+ppp-max-mtu={{ mtu }}
+mppe={{ ppp_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 auth_mode == 'local' %}
+[chap-secrets]
+chap-secrets={{ chap_secrets_file }}
+{% elif auth_mode == 'radius' %}
+[radius]
+verbose=1
+{% for r in radius_server %}
+server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }}
+{% endfor -%}
+
+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 -%}
+{% endif %}
+{% if gateway_address %}
+gw-ip-address={{ gateway_address }}
+{% endif %}
+
+[ppp]
+verbose=1
+check-ip=1
+single-session=replace
+lcp-echo-timeout={{ ppp_echo_timeout }}
+lcp-echo-interval={{ ppp_echo_interval }}
+lcp-echo-failure={{ ppp_echo_failure }}
+{% if ccp_disable %}
+ccp=0
+{% endif %}
+{% if client_ipv6_pool %}
+ipv6=allow
+{% endif %}
+
+
+{% if client_ipv6_pool %}
+[ipv6-pool]
+{% for p in client_ipv6_pool %}
+{{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% for p in client_ipv6_delegate_prefix %}
+delegate={{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% endif %}
+
+{% if client_ipv6_delegate_prefix %}
+[ipv6-dhcp]
+verbose=1
+{% 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:2004
+sessions-columns=ifname,username,calling-sid,ip,{{ ip6_column | join(',') }}{{ ',' if ip6_column }}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime
diff --git a/data/templates/accel-ppp/pppoe.config.tmpl b/data/templates/accel-ppp/pppoe.config.tmpl
new file mode 100644
index 000000000..5ad628fde
--- /dev/null
+++ b/data/templates/accel-ppp/pppoe.config.tmpl
@@ -0,0 +1,204 @@
+### generated by accel_pppoe.py ###
+[modules]
+log_syslog
+pppoe
+{% if auth_mode == 'radius' %}
+radius
+{% endif %}
+chap-secrets
+ippool
+{% if ppp_ipv6 != 'deny' %}
+ipv6pool
+ipv6_nd
+ipv6_dhcp
+{% endif %}
+{% for proto in auth_proto: %}
+{{proto}}
+{% endfor%}
+shaper
+{% if snmp %}
+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 subnet in client_ip_subnets %}
+{{ subnet }}
+{% endfor %}
+{% endif %}
+{% endif %}
+
+{% if client_ipv6_pool %}
+[ipv6-nd]
+AdvAutonomousFlag=1
+
+[ipv6-pool]
+{% for p in client_ipv6_pool %}
+{{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% for p in client_ipv6_delegate_prefix %}
+delegate={{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% endif %}
+
+{% if dnsv4 %}
+[dns]
+{% for dns in dnsv4 -%}
+dns{{ loop.index }}={{ dns }}
+{% endfor -%}
+{% endif %}
+
+{% if dnsv6 %}
+[ipv6-dns]
+{% for dns in dnsv6 -%}
+{{ dns }}
+{% endfor -%}
+{% endif %}
+
+{% if wins %}
+[wins]
+{% for server in wins -%}
+wins{{ loop.index }}={{ server }}
+{% endfor -%}
+{% endif %}
+
+{% if auth_mode == 'local' %}
+[chap-secrets]
+chap-secrets={{ chap_secrets_file }}
+{% elif auth_mode == 'radius' %}
+[radius]
+verbose=1
+{% for r in radius_server %}
+server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }}
+{% endfor -%}
+
+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 %}
+{% if ppp_gw %}
+gw-ip-address={{ ppp_gw }}
+{% endif %}
+
+{% if sesscrtl != 'disable' %}
+[common]
+single-session={{ sesscrtl }}
+{% endif %}
+
+[ppp]
+verbose=1
+check-ip=1
+{% if ppp_ccp %}
+ccp=1
+{% else %}
+ccp=0
+{% endif %}
+{% if ppp_min_mtu %}
+min-mtu={{ ppp_min_mtu }}
+{% else %}
+min-mtu={{ mtu }}
+{% endif %}
+{% if ppp_mru %}
+mru={{ ppp_mru }}
+{% endif %}
+mppe={{ ppp_mppe }}
+lcp-echo-interval={{ ppp_echo_interval }}
+lcp-echo-timeout={{ ppp_echo_timeout }}
+lcp-echo-failure={{ ppp_echo_failure }}
+{% if ppp_ipv4 %}
+ipv4={{ ppp_ipv4 }}
+{% endif %}
+{% if client_ipv6_pool %}
+ipv6=allow
+{% endif %}
+
+{% if ppp_ipv6 %}
+ipv6={{ ppp_ipv6 }}
+{% if ppp_ipv6_intf_id %}
+ipv6-intf-id={{ ppp_ipv6_intf_id }}
+{% endif %}
+{% if ppp_ipv6_peer_intf_id %}
+ipv6-peer-intf-id={{ ppp_ipv6_peer_intf_id }}
+{% endif %}
+{% if ppp_ipv6_accept_peer_intf_id %}
+ipv6-accept-peer-intf-id={{ ppp_ipv6_accept_peer_intf_id }}
+{% endif %}
+{% endif %}
+mtu={{ mtu }}
+
+[pppoe]
+verbose=1
+ac-name={{ concentrator }}
+
+{% if interfaces %}
+{% for interface in interfaces %}
+interface={{ interface.name }}
+{% if interface.vlans %}
+vlan-mon={{ interface.name }},{{ interface.vlans | join(',') }}
+interface=re:{{ interface.name }}\.\d+
+{% endif %}
+{% endfor -%}
+{% endif -%}
+
+{% if svc_name %}
+service-name={{ svc_name|join(',') }}
+{% endif -%}
+
+{% if pado_delay %}
+pado-delay={{ pado_delay }}
+{% endif %}
+
+{% if limits_burst or limits_connections or limits_connections %}
+[connlimit]
+{% if limits_connections %}
+limit={{ limits_connections }}
+{% endif %}
+{% if limits_burst %}
+burst={{ limits_burst }}
+{% endif %}
+{% if limits_timeout %}
+timeout={{ limits_timeout }}
+{% endif %}
+{% endif %}
+
+[cli]
+tcp=127.0.0.1:2001
diff --git a/data/templates/accel-ppp/pptp.config.tmpl b/data/templates/accel-ppp/pptp.config.tmpl
new file mode 100644
index 000000000..e0f2c6da9
--- /dev/null
+++ b/data/templates/accel-ppp/pptp.config.tmpl
@@ -0,0 +1,89 @@
+### generated by accel_pptp.py ###
+[modules]
+log_syslog
+pptp
+ippool
+{% if auth_mode == 'local' %}
+chap-secrets
+{% elif auth_mode == 'radius' %}
+radius
+{% endif -%}
+{% for proto in auth_proto %}
+{{proto}}
+{% endfor %}
+
+[core]
+thread-count={{ thread_cnt }}
+
+[log]
+syslog=accel-pptp,daemon
+copy=1
+level=5
+
+{% if dnsv4 %}
+[dns]
+{% for dns in dnsv4 -%}
+dns{{ loop.index }}={{ dns }}
+{% endfor -%}
+{% endif %}
+
+{% if wins %}
+[wins]
+{% for server in wins -%}
+wins{{ loop.index }}={{ server }}
+{% endfor -%}
+{% endif %}
+
+
+[pptp]
+ifname=pptp%d
+{% if outside_addr %}
+bind={{ outside_addr }}
+{% endif %}
+verbose=1
+ppp-max-mtu={{mtu}}
+mppe={{ ppp_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 }}
+
+[ppp]
+verbose=5
+check-ip=1
+single-session=replace
+
+{% if auth_mode == 'local' %}
+[chap-secrets]
+chap-secrets={{ chap_secrets_file }}
+{% elif auth_mode == 'radius' %}
+[radius]
+verbose=1
+{% for r in radius_server %}
+server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }}
+{% endfor -%}
+
+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 -%}
+{% endif %}
+
+[cli]
+tcp=127.0.0.1:2003
+
diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl
new file mode 100644
index 000000000..c9e4a1d7d
--- /dev/null
+++ b/data/templates/accel-ppp/sstp.config.tmpl
@@ -0,0 +1,146 @@
+### generated by vpn_sstp.py ###
+[modules]
+log_syslog
+sstp
+shaper
+{% if auth_mode == 'local' %}
+chap-secrets
+{% elif auth_mode == 'radius' %}
+radius
+{% endif -%}
+ippool
+ipv6pool
+ipv6_nd
+ipv6_dhcp
+
+{% 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
+ifname=sstp%d
+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 dnsv6 %}
+[ipv6-dns]
+{% for dns in dnsv6 -%}
+{{ dns }}
+{% endfor -%}
+{% endif %}
+
+
+{% if auth_mode == 'local' %}
+[chap-secrets]
+chap-secrets={{ chap_secrets_file }}
+{% elif auth_mode == 'radius' %}
+[radius]
+verbose=1
+{% for r in radius_server %}
+server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }}
+{% endfor -%}
+
+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 %}
+{% if client_gateway %}
+gw-ip-address={{ client_gateway }}
+{% endif %}
+
+[ppp]
+verbose=1
+check-ip=1
+{% if mtu %}
+mtu={{ mtu }}
+{% endif -%}
+{% if client_ipv6_pool %}
+ipv6=allow
+{% 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 client_ipv6_pool %}
+[ipv6-pool]
+{% for p in client_ipv6_pool %}
+{{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% for p in client_ipv6_delegate_prefix %}
+delegate={{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% endif %}
+
+{% if client_ipv6_delegate_prefix %}
+[ipv6-dhcp]
+verbose=1
+{% 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/bcast-relay/udp-broadcast-relay.tmpl b/data/templates/bcast-relay/udp-broadcast-relay.tmpl
new file mode 100644
index 000000000..d0c7d8bf9
--- /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="{{ '-s ' + address if address is defined }} {{ instance }} {{ port }} {{ interface | join(' ') }}"
diff --git a/data/templates/conserver/conserver.conf.tmpl b/data/templates/conserver/conserver.conf.tmpl
new file mode 100644
index 000000000..4e7b5d8d7
--- /dev/null
+++ b/data/templates/conserver/conserver.conf.tmpl
@@ -0,0 +1,37 @@
+### Autogenerated by service_console-server.py ###
+
+# See https://www.conserver.com/docs/conserver.cf.man.html for additional options
+
+config * {
+ primaryport 3109;
+ daemonmode false;
+}
+
+default * {
+ motd "VyOS Console Server";
+ rw *;
+}
+
+##
+## list of consoles we serve
+##
+{% for key, value in device.items() %}
+{# Depending on our USB serial console we could require a path adjustment #}
+{% set path = '/dev' if key.startswith('ttyS') else '/dev/serial/by-bus' %}
+console {{ key }} {
+ master localhost;
+ type device;
+ device {{ path }}/{{ key }};
+ baud {{ value.speed }};
+ parity {{ value.parity }};
+ options {{ "!" if value.stop_bits == "1" }}cstopb;
+}
+{% endfor %}
+
+##
+## list of clients we allow
+##
+access * {
+ trusted localhost;
+ allowed localhost;
+}
diff --git a/data/templates/dhcp-client/daemon-options.tmpl b/data/templates/dhcp-client/daemon-options.tmpl
new file mode 100644
index 000000000..290aefa49
--- /dev/null
+++ b/data/templates/dhcp-client/daemon-options.tmpl
@@ -0,0 +1,4 @@
+### Autogenerated by interface.py ###
+
+DHCLIENT_OPTS="-nw -cf /var/lib/dhcp/dhclient_{{ifname}}.conf -pf /var/lib/dhcp/dhclient_{{ifname}}.pid -lf /var/lib/dhcp/dhclient_{{ifname}}.leases {{ifname}}"
+
diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl
new file mode 100644
index 000000000..8a44a9761
--- /dev/null
+++ b/data/templates/dhcp-client/ipv4.tmpl
@@ -0,0 +1,19 @@
+### Autogenerated by interface.py ###
+
+option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
+timeout 60;
+retry 300;
+
+interface "{{ ifname }}" {
+ send host-name "{{ dhcp_options.host_name }}";
+{% if dhcp_options.client_id is defined and dhcp_options.client_id is not none %}
+ send dhcp-client-identifier "{{ dhcp_options.client_id }}";
+{% endif %}
+{% if dhcp_options.vendor_class_id is defined and dhcp_options.vendor_class_id is not none %}
+ send vendor-class-identifier "{{ dhcp_options.vendor_class_id }}";
+{% endif %}
+ request subnet-mask, broadcast-address, routers, domain-name-servers,
+ rfc3442-classless-static-routes, domain-name, interface-mtu;
+ require subnet-mask;
+}
+
diff --git a/data/templates/dhcp-client/ipv6.tmpl b/data/templates/dhcp-client/ipv6.tmpl
new file mode 100644
index 000000000..68f668117
--- /dev/null
+++ b/data/templates/dhcp-client/ipv6.tmpl
@@ -0,0 +1,57 @@
+### Autogenerated by interface.py ###
+
+# man https://www.unix.com/man-page/debian/5/dhcp6c.conf/
+interface {{ ifname }} {
+{% if address is defined and 'dhcpv6' in address %}
+ request domain-name-servers;
+ request domain-name;
+{% if dhcpv6_options is defined and dhcpv6_options.parameters_only is defined %}
+ information-only;
+{% endif %}
+{% if dhcpv6_options is not defined or dhcpv6_options.temporary is not defined %}
+ send ia-na 0; # non-temporary address
+{% endif %}
+{% if dhcpv6_options is defined and dhcpv6_options.rapid_commit is defined %}
+ send rapid-commit; # wait for immediate reply instead of advertisements
+{% endif %}
+{% endif %}
+{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %}
+{% for pd in dhcpv6_options.pd %}
+ send ia-pd {{ pd }}; # prefix delegation #{{ pd }}
+{% endfor %}
+{% endif %}
+};
+
+{% if address is defined and 'dhcpv6' in address %}
+{% if dhcpv6_options is not defined or dhcpv6_options.temporary is not defined %}
+id-assoc na 0 {
+ # Identity association for non temporary address
+};
+{% endif %}
+{% endif %}
+
+{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %}
+{% for pd in dhcpv6_options.pd %}
+id-assoc pd {{ pd }} {
+{# length got a default value #}
+ prefix ::/{{ dhcpv6_options.pd[pd].length }} infinity;
+{% set sla_len = 64 - dhcpv6_options.pd[pd].length|int %}
+{% set count = namespace(value=0) %}
+{% for interface in dhcpv6_options.pd[pd].interface if dhcpv6_options.pd[pd].interface is defined %}
+ prefix-interface {{ interface }} {
+ sla-len {{ sla_len }};
+{% if dhcpv6_options.pd[pd].interface[interface].sla_id is defined and dhcpv6_options.pd[pd].interface[interface].sla_id is not none %}
+ sla-id {{ dhcpv6_options.pd[pd].interface[interface].sla_id }};
+{% else %}
+ sla-id {{ count.value }};
+{% endif %}
+{% if dhcpv6_options.pd[pd].interface[interface].address is defined and dhcpv6_options.pd[pd].interface[interface].address is not none %}
+ ifid {{ dhcpv6_options.pd[pd].interface[interface].address }};
+{% endif %}
+ };
+{% set count.value = count.value + 1 %}
+{% endfor %}
+};
+{% endfor %}
+{% endif %}
+
diff --git a/data/templates/dhcp-relay/config.tmpl b/data/templates/dhcp-relay/config.tmpl
new file mode 100644
index 000000000..b223807cf
--- /dev/null
+++ b/data/templates/dhcp-relay/config.tmpl
@@ -0,0 +1,4 @@
+### Autogenerated by dhcp_relay.py ###
+
+# Defaults for isc-dhcp-relay6.service
+OPTIONS="{{ options | join(' ') }} -i {{ interface | join(' -i ') }} {{ server | join(' ') }}"
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..55035ae6c
--- /dev/null
+++ b/data/templates/dhcpv6-relay/config.tmpl
@@ -0,0 +1,4 @@
+### Autogenerated by dhcpv6_relay.py ###
+
+# Defaults for isc-dhcp-relay6.service
+OPTIONS="-l {{ listen_addr | join(' -l ') }} -u {{ upstream_addr | join(' -u ') }} {{ options | join(' ') }}"
diff --git a/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl
new file mode 100644
index 000000000..ff7822b0d
--- /dev/null
+++ b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl
@@ -0,0 +1,81 @@
+### 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 prefix in subnet.prefix_delegation %}
+ prefix6 {{ prefix.start }} {{ prefix.stop }} /{{ prefix.length }};
+ {%- endfor %}
+ {%- 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.lua.tmpl b/data/templates/dns-forwarding/recursor.conf.lua.tmpl
new file mode 100644
index 000000000..e2506238d
--- /dev/null
+++ b/data/templates/dns-forwarding/recursor.conf.lua.tmpl
@@ -0,0 +1,9 @@
+-- Autogenerated by VyOS (dns_forwarding.py) --
+-- Do not edit, your changes will get overwritten --
+
+-- Load DNSSEC root keys from dns-root-data package.
+dofile("/usr/share/pdns-recursor/lua-config/rootkeys.lua")
+
+-- Load lua from vyos-hostsd --
+dofile("recursor.vyos-hostsd.conf.lua")
+
diff --git a/data/templates/dns-forwarding/recursor.conf.tmpl b/data/templates/dns-forwarding/recursor.conf.tmpl
new file mode 100644
index 000000000..d233b8abc
--- /dev/null
+++ b/data/templates/dns-forwarding/recursor.conf.tmpl
@@ -0,0 +1,33 @@
+### 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=::
+lua-config-file=recursor.conf.lua
+
+# 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-address
+local-address={{ listen_address | join(',') }}
+
+# dnssec
+dnssec={{ dnssec }}
+
+forward-zones-file=recursor.forward-zones.conf
+
diff --git a/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl b/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl
new file mode 100644
index 000000000..de5eaee00
--- /dev/null
+++ b/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl
@@ -0,0 +1,28 @@
+# Autogenerated by VyOS (vyos-hostsd)
+# Do not edit, your changes will get overwritten
+
+# dot zone (catch-all): '+' indicates recursion is desired
+# (same as forward-zones-recurse)
+{#- the code below ensures the order of nameservers is determined first by #}
+{#- the order of tags, then by the order of nameservers within that tag #}
+{%- set n = namespace(dot_zone_ns='') %}
+{%- for tag in name_server_tags_recursor %}
+{%- set ns = '' %}
+{%- if tag in name_servers %}
+{%- set ns = ns + name_servers[tag]|join(', ') %}
+{%- set n.dot_zone_ns = (n.dot_zone_ns, ns)|join(', ') if n.dot_zone_ns != '' else ns %}
+{%- endif %}
+# {{ tag }}: {{ ns }}
+{%- endfor %}
+
+{%- if n.dot_zone_ns %}
++.={{ n.dot_zone_ns }}
+{%- endif %}
+
+{% if forward_zones -%}
+# zones added via 'service dns forwarding domain'
+{%- for zone, zonedata in forward_zones.items() %}
+{% if zonedata['recursion-desired'] %}+{% endif %}{{ zone }}={{ zonedata['nslist']|join(', ') }}
+{%- endfor %}
+{%- endif %}
+
diff --git a/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl
new file mode 100644
index 000000000..b0d99d9ae
--- /dev/null
+++ b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl
@@ -0,0 +1,24 @@
+-- Autogenerated by VyOS (vyos-hostsd) --
+-- Do not edit, your changes will get overwritten --
+
+{% if hosts -%}
+-- from 'system static-host-mapping' and DHCP server
+{%- for tag, taghosts in hosts.items() %}
+{%- for host, hostprops in taghosts.items() %}
+addNTA("{{ host }}.", "{{ tag }}")
+{%- for a in hostprops['aliases'] %}
+addNTA("{{ a }}.", "{{ tag }} alias")
+{%- endfor %}
+{%- endfor %}
+{%- endfor %}
+{%- endif %}
+
+{% if forward_zones -%}
+-- from 'service dns forwarding domain'
+{%- for zone, zonedata in forward_zones.items() %}
+{%- if zonedata['addNTA'] %}
+addNTA("{{ zone }}", "static")
+{%- endif %}
+{%- endfor %}
+{%- 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..9c7219230
--- /dev/null
+++ b/data/templates/dynamic-dns/ddclient.conf.tmpl
@@ -0,0 +1,46 @@
+### Autogenerated by dynamic_dns.py ###
+daemon=1m
+syslog=yes
+ssl=yes
+
+{% 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/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl
new file mode 100644
index 000000000..0c29f536b
--- /dev/null
+++ b/data/templates/firewall/nftables-nat.tmpl
@@ -0,0 +1,157 @@
+#!/usr/sbin/nft -f
+
+# Start with clean NAT table
+flush table nat
+
+{% if helper_functions == 'remove' %}
+{# NAT if going to be disabled - remove rules and targets from nftables #}
+
+{% set base_command = "delete rule ip raw" %}
+{{ base_command }} PREROUTING handle {{ pre_ct_ignore }}
+{{ base_command }} OUTPUT handle {{ out_ct_ignore }}
+{{ base_command }} PREROUTING handle {{ pre_ct_conntrack }}
+{{ base_command }} OUTPUT handle {{ out_ct_conntrack }}
+
+delete chain ip raw NAT_CONNTRACK
+
+{% elif helper_functions == 'add' %}
+{# NAT if enabled - add targets to nftables #}
+add chain ip raw NAT_CONNTRACK
+add rule ip raw NAT_CONNTRACK counter accept
+
+{% set base_command = "add rule ip raw" %}
+
+{{ base_command }} PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER
+{{ base_command }} OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER
+{{ base_command }} PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK
+{{ base_command }} OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK
+{% endif %}
+
+{% macro nat_rule(rule, chain) %}
+{% set src_addr = "ip saddr " + rule.source_address if rule.source_address %}
+{% set dst_addr = "ip daddr " + rule.dest_address if rule.dest_address %}
+
+{# negated port groups need special treatment, move != in front of { } group #}
+{% if rule.source_port.startswith('!=') %}
+{% set src_port = "sport != { " + rule.source_port.replace('!=','') +" }" if rule.source_port %}
+{% else %}
+{% set src_port = "sport { " + rule.source_port +" }" if rule.source_port %}
+{% endif %}
+
+{# negated port groups need special treatment, move != in front of { } group #}
+{% if rule.dest_port.startswith('!=') %}
+{% set dst_port = "dport != { " + rule.dest_port.replace('!=','') +" }" if rule.dest_port %}
+{% else %}
+{% set dst_port = "dport { " + rule.dest_port +" }" if rule.dest_port %}
+{% endif %}
+
+{% set comment = "DST-NAT-" + rule.number %}
+
+{% if chain == "PREROUTING" %}
+{% set interface = " iifname \"" + rule.interface_in + "\"" if rule.interface_in is defined and rule.interface_in != 'any' else '' %}
+{% set trns_addr = "dnat to " + rule.translation_address %}
+
+{% elif chain == "POSTROUTING" %}
+{% set interface = " oifname \"" + rule.interface_out + "\"" if rule.interface_out is defined and rule.interface_out != 'any' else '' %}
+{% if rule.translation_address == 'masquerade' %}
+{% set trns_addr = rule.translation_address %}
+{% if rule.translation_port %}
+{% set trns_addr = trns_addr + " to " %}
+{% endif %}
+{% else %}
+{% set trns_addr = "snat to " + rule.translation_address %}
+{% endif %}
+{% endif %}
+{% set trns_port = ":" + rule.translation_port if rule.translation_port %}
+
+{% if rule.protocol == "tcp_udp" %}
+{% set protocol = "tcp" %}
+{% set comment = comment + " tcp_udp" %}
+{% else %}
+{% set protocol = rule.protocol %}
+{% endif %}
+
+{% if rule.log %}
+{% set base_log = "[NAT-DST-" + rule.number %}
+{% if rule.exclude %}
+{% set log = base_log + "-EXCL]" %}
+{% elif rule.translation_address == 'masquerade' %}
+{% set log = base_log + "-MASQ]" %}
+{% else %}
+{% set log = base_log + "]" %}
+{% endif %}
+{% endif %}
+
+{% if rule.exclude %}
+{# rule has been marked as "exclude" thus we simply return here #}
+{% set trns_addr = "return" %}
+{% set trns_port = "" %}
+{% endif %}
+
+{% set output = "add rule ip nat " + chain + interface %}
+
+{% if protocol != "all" %}
+{% set output = output + " ip protocol " + protocol %}
+{% endif %}
+
+{% if src_addr %}
+{% set output = output + " " + src_addr %}
+{% endif %}
+{% if src_port %}
+{% set output = output + " " + protocol + " " + src_port %}
+{% endif %}
+
+{% if dst_addr %}
+{% set output = output + " " + dst_addr %}
+{% endif %}
+{% if dst_port %}
+{% set output = output + " " + protocol + " " + dst_port %}
+{% endif %}
+
+{# Count packets #}
+{% set output = output + " counter" %}
+
+{# Special handling of log option, we must repeat the entire rule before the #}
+{# NAT translation options are added, this is essential #}
+{% if log %}
+{% set log_output = output + " log prefix \"" + log + "\" comment \"" + comment + "\"" %}
+{% endif %}
+
+{% if trns_addr %}
+{% set output = output + " " + trns_addr %}
+{% endif %}
+
+{% if trns_port %}
+{# Do not add a whitespace here, translation port must be directly added after IP address #}
+{# e.g. 192.0.2.10:3389 #}
+{% set output = output + trns_port %}
+{% endif %}
+
+{% if comment %}
+{% set output = output + " comment \"" + comment + "\"" %}
+{% endif %}
+
+{{ log_output if log_output }}
+{{ output }}
+
+{# Special handling if protocol is tcp_udp, we must repeat the entire rule with udp as protocol #}
+{% if rule.protocol == "tcp_udp" %}
+{# Beware of trailing whitespace, without it the comment tcp_udp will be changed to udp_udp #}
+{{ log_output | replace("tcp ", "udp ") if log_output }}
+{{ output | replace("tcp ", "udp ") }}
+{% endif %}
+{% endmacro %}
+
+#
+# Destination NAT rules build up here
+#
+{% for rule in destination if not rule.disabled -%}
+{{ nat_rule(rule, 'PREROUTING') }}
+{% endfor %}
+
+#
+# Source NAT rules build up here
+#
+{% for rule in source if not rule.disabled -%}
+{{ nat_rule(rule, 'POSTROUTING') }}
+{% endfor %}
diff --git a/data/templates/frr/bfd.frr.tmpl b/data/templates/frr/bfd.frr.tmpl
new file mode 100644
index 000000000..7df4bfd01
--- /dev/null
+++ b/data/templates/frr/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/frr/bgp.frr.tmpl b/data/templates/frr/bgp.frr.tmpl
new file mode 100644
index 000000000..cdf4cb4fe
--- /dev/null
+++ b/data/templates/frr/bgp.frr.tmpl
@@ -0,0 +1 @@
+!
diff --git a/data/templates/frr/igmp.frr.tmpl b/data/templates/frr/igmp.frr.tmpl
new file mode 100644
index 000000000..de4696c1f
--- /dev/null
+++ b/data/templates/frr/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/frr/ldpd.frr.tmpl b/data/templates/frr/ldpd.frr.tmpl
new file mode 100644
index 000000000..dbaa917e8
--- /dev/null
+++ b/data/templates/frr/ldpd.frr.tmpl
@@ -0,0 +1,70 @@
+!
+{% 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 -%}
+{% if old_ldp.hello_holdtime -%}
+no discovery hello holdtime {{ old_ldp.hello_holdtime }}
+{% endif -%}
+{% if ldp.hello_holdtime -%}
+discovery hello holdtime {{ ldp.hello_holdtime }}
+{% endif -%}
+{% if old_ldp.hello_interval -%}
+no discovery hello interval {{ old_ldp.hello_interval }}
+{% endif -%}
+{% if ldp.hello_interval -%}
+discovery hello interval {{ ldp.hello_interval }}
+{% 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/frr/pimd.frr.tmpl b/data/templates/frr/pimd.frr.tmpl
new file mode 100644
index 000000000..1d1532c60
--- /dev/null
+++ b/data/templates/frr/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/frr/rip.frr.tmpl b/data/templates/frr/rip.frr.tmpl
new file mode 100644
index 000000000..60bc686bd
--- /dev/null
+++ b/data/templates/frr/rip.frr.tmpl
@@ -0,0 +1,143 @@
+!
+{% if rip_conf -%}
+router rip
+{% if old_default_distance -%}
+no distance {{old_default_distance}}
+{% endif -%}
+{% if default_distance -%}
+distance {{default_distance}}
+{% endif -%}
+{% if old_default_originate -%}
+no default-information originate
+{% endif -%}
+{% if default_originate -%}
+default-information originate
+{% endif -%}
+{% if old_rip.default_metric -%}
+no default-metric {{old_rip.default_metric}}
+{% endif -%}
+{% if rip.default_metric -%}
+default-metric {{rip.default_metric}}
+{% endif -%}
+{% for protocol in old_rip.redist -%}
+{% if old_rip.redist[protocol]['metric'] and old_rip.redist[protocol]['route_map'] -%}
+no redistribute {{protocol}} metric {{rip.redist[protocol]['metric']}} route-map {{rip.redist[protocol]['route_map']}}
+{% elif old_rip.redist[protocol]['metric'] -%}
+no redistribute {{protocol}} metric {{old_rip.redist[protocol]['metric']}}
+{% elif old_rip.redist[protocol]['route_map'] -%}
+no redistribute {{protocol}} route-map {{old_rip.redist[protocol]['route_map']}}
+{% else -%}
+no redistribute {{protocol}}
+{% endif -%}
+{% endfor -%}
+{% for protocol in rip.redist -%}
+{% if rip.redist[protocol]['metric'] and rip.redist[protocol]['route_map'] -%}
+redistribute {{protocol}} metric {{rip.redist[protocol]['metric']}} route-map {{rip.redist[protocol]['route_map']}}
+{% elif rip.redist[protocol]['metric'] -%}
+redistribute {{protocol}} metric {{rip.redist[protocol]['metric']}}
+{% elif rip.redist[protocol]['route_map'] -%}
+redistribute {{protocol}} route-map {{rip.redist[protocol]['route_map']}}
+{% else -%}
+redistribute {{protocol}}
+{% endif -%}
+{% endfor -%}
+{% for iface in old_rip.distribute -%}
+{% if old_rip.distribute[iface].iface_access_list_in -%}
+no distribute-list {{old_rip.distribute[iface].iface_access_list_in}} in {{iface}}
+{% endif -%}
+{% if old_rip.distribute[iface].iface_access_list_out -%}
+no distribute-list {{old_rip.distribute[iface].iface_access_list_out}} out {{iface}}
+{% endif -%}
+{% if old_rip.distribute[iface].iface_prefix_list_in -%}
+no distribute-list prefix {{old_rip.distribute[iface].iface_prefix_list_in}} in {{iface}}
+{% endif -%}
+{% if old_rip.distribute[iface].iface_prefix_list_out -%}
+no distribute-list prefix {{old_rip.distribute[iface].iface_prefix_list_out}} out {{iface}}
+{% endif -%}
+{% endfor -%}
+{% for iface in rip.distribute -%}
+{% if rip.distribute[iface].iface_access_list_in -%}
+distribute-list {{rip.distribute[iface].iface_access_list_in}} in {{iface}}
+{% endif -%}
+{% if rip.distribute[iface].iface_access_list_out -%}
+distribute-list {{rip.distribute[iface].iface_access_list_out}} out {{iface}}
+{% endif -%}
+{% if rip.distribute[iface].iface_prefix_list_in -%}
+distribute-list prefix {{rip.distribute[iface].iface_prefix_list_in}} in {{iface}}
+{% endif -%}
+{% if rip.distribute[iface].iface_prefix_list_out -%}
+distribute-list prefix {{rip.distribute[iface].iface_prefix_list_out}} out {{iface}}
+{% endif -%}
+{% endfor -%}
+{% if old_rip.dist_acl_in -%}
+no distribute-list {{old_rip.dist_acl_in}} in
+{% endif -%}
+{% if rip.dist_acl_in -%}
+distribute-list {{rip.dist_acl_in}} in
+{% endif -%}
+{% if old_rip.dist_acl_out -%}
+no distribute-list {{old_rip.dist_acl_out}} out
+{% endif -%}
+{% if rip.dist_acl_out -%}
+distribute-list {{rip.dist_acl_out}} out
+{% endif -%}
+{% if old_rip.dist_prfx_in -%}
+no distribute-list prefix {{old_rip.dist_prfx_in}} in
+{% endif -%}
+{% if rip.dist_prfx_in -%}
+distribute-list prefix {{rip.dist_prfx_in}} in
+{% endif -%}
+{% if old_rip.dist_prfx_out -%}
+no distribute-list prefix {{old_rip.dist_prfx_out}} out
+{% endif -%}
+{% if rip.dist_prfx_out -%}
+distribute-list prefix {{rip.dist_prfx_out}} out
+{% endif -%}
+{% for network in old_rip.networks -%}
+no network {{network}}
+{% endfor -%}
+{% for network in rip.networks -%}
+network {{network}}
+{% endfor -%}
+{% for iface in old_rip.ifaces -%}
+no network {{iface}}
+{% endfor -%}
+{% for iface in rip.ifaces -%}
+network {{iface}}
+{% endfor -%}
+{% for neighbor in old_rip.neighbors -%}
+no neighbor {{neighbor}}
+{% endfor -%}
+{% for neighbor in rip.neighbors -%}
+neighbor {{neighbor}}
+{% endfor -%}
+{% for net in rip.net_distance -%}
+{% if rip.net_distance[net].access_list and rip.net_distance[net].distance -%}
+distance {{rip.net_distance[net].distance}} {{net}} {{rip.net_distance[net].access_list}}
+{% else -%}
+distance {{rip.net_distance[net].distance}} {{net}}
+{% endif -%}
+{% endfor -%}
+{% for passive_iface in old_rip.passive_iface -%}
+no passive-interface {{passive_iface}}
+{% endfor -%}
+{% for passive_iface in rip.passive_iface -%}
+passive-interface {{passive_iface}}
+{% endfor -%}
+{% for route in old_rip.route -%}
+no route {{route}}
+{% endfor -%}
+{% for route in rip.route -%}
+route {{route}}
+{% endfor -%}
+{% if old_rip.timer_update or old_rip.timer_timeout or old_rip.timer_garbage -%}
+no timers basic
+{% endif -%}
+{% if rip.timer_update or rip.timer_timeout or rip.timer_garbage -%}
+timers basic {{rip.timer_update}} {{rip.timer_timeout}} {{rip.timer_garbage}}
+{% endif -%}
+!
+{% else -%}
+no router rip
+!
+{% endif -%}
diff --git a/data/templates/frr/static_mcast.frr.tmpl b/data/templates/frr/static_mcast.frr.tmpl
new file mode 100644
index 000000000..86d619ab0
--- /dev/null
+++ b/data/templates/frr/static_mcast.frr.tmpl
@@ -0,0 +1,20 @@
+!
+{% for route_gr in old_mroute -%}
+{% for nh in old_mroute[route_gr] -%}
+{% if old_mroute[route_gr][nh] -%}
+no ip mroute {{ route_gr }} {{ nh }} {{ old_mroute[route_gr][nh] }}
+{% else -%}
+no ip mroute {{ route_gr }} {{ nh }}
+{% endif -%}
+{% endfor -%}
+{% endfor -%}
+{% for route_gr in mroute -%}
+{% for nh in mroute[route_gr] -%}
+{% if mroute[route_gr][nh] -%}
+ip mroute {{ route_gr }} {{ nh }} {{ mroute[route_gr][nh] }}
+{% else -%}
+ip mroute {{ route_gr }} {{ nh }}
+{% endif -%}
+{% endfor -%}
+{% endfor -%}
+!
diff --git a/data/templates/getty/serial-getty.service.tmpl b/data/templates/getty/serial-getty.service.tmpl
new file mode 100644
index 000000000..0183eae7d
--- /dev/null
+++ b/data/templates/getty/serial-getty.service.tmpl
@@ -0,0 +1,37 @@
+[Unit]
+Description=Serial Getty on %I
+Documentation=man:agetty(8) man:systemd-getty-generator(8)
+Documentation=http://0pointer.de/blog/projects/serial-console.html
+BindsTo=dev-%i.device
+After=dev-%i.device systemd-user-sessions.service plymouth-quit-wait.service getty-pre.target
+After=vyos-router.service
+
+# If additional gettys are spawned during boot then we should make
+# sure that this is synchronized before getty.target, even though
+# getty.target didn't actually pull it in.
+Before=getty.target
+IgnoreOnIsolate=yes
+
+# IgnoreOnIsolate causes issues with sulogin, if someone isolates
+# rescue.target or starts rescue.service from multi-user.target or
+# graphical.target.
+Conflicts=rescue.service
+Before=rescue.service
+
+[Service]
+# The '-o' option value tells agetty to replace 'login' arguments with an
+# option to preserve environment (-p), followed by '--' for safety, and then
+# the entered username.
+ExecStart=-/sbin/agetty -o '-p -- \\u' --keep-baud {{ speed }} %I $TERM
+Type=idle
+Restart=always
+UtmpIdentifier=%I
+TTYPath=/dev/%I
+TTYReset=yes
+TTYVHangup=yes
+KillMode=process
+IgnoreSIGPIPE=no
+SendSIGHUP=yes
+
+[Install]
+WantedBy=getty.target
diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl
new file mode 100644
index 000000000..a20be45ae
--- /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 {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/fullchain.pem;
+ ssl_certificate_key {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/privkey.pem;
+ include {{ server.certbot_dir }}/options-ssl-nginx.conf;
+ ssl_dhparam {{ server.certbot_dir }}/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_set %}
+ 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/ids/fastnetmon.tmpl b/data/templates/ids/fastnetmon.tmpl
new file mode 100644
index 000000000..71a1b2bd7
--- /dev/null
+++ b/data/templates/ids/fastnetmon.tmpl
@@ -0,0 +1,60 @@
+# enable this option if you want to send logs to local syslog facility
+logging:local_syslog_logging = on
+
+# list of all your networks in CIDR format
+networks_list_path = /etc/networks_list
+
+# list networks in CIDR format which will be not monitored for attacks
+white_list_path = /etc/networks_whitelist
+
+# Enable/Disable any actions in case of attack
+enable_ban = on
+
+## How many packets will be collected from attack traffic
+ban_details_records_count = 500
+
+## How long (in seconds) we should keep an IP in blocked state
+## If you set 0 here it completely disables unban capability
+ban_time = 1900
+
+# Check if the attack is still active, before triggering an unban callback with this option
+# If the attack is still active, check each run of the unban watchdog
+unban_only_if_attack_finished = on
+
+# enable per subnet speed meters
+# For each subnet, list track speed in bps and pps for both directions
+enable_subnet_counters = off
+
+{% if "mirror" in mode %}
+mirror_afpacket = on
+{% endif -%}
+
+{% if "in" in direction %}
+process_incoming_traffic = on
+{% endif -%}
+{% if "out" in direction %}
+process_outgoing_traffic = on
+{% endif -%}
+{% for th in threshold %}
+{% if th == "fps" %}
+ban_for_flows = on
+threshold_flows = {{ threshold[th] }}
+{% endif -%}
+{% if th == "mbps" %}
+ban_for_bandwidth = on
+threshold_mbps = {{ threshold[th] }}
+{% endif -%}
+{% if th == "pps" %}
+ban_for_pps = on
+threshold_pps = {{ threshold[th] }}
+{% endif -%}
+{% endfor -%}
+
+{% if listen_interface %}
+{% set value = listen_interface if listen_interface is string else listen_interface | join(',') %}
+interfaces = {{ value }}
+{% endif -%}
+
+{% if alert_script %}
+notify_script_path = {{ alert_script }}
+{% endif -%}
diff --git a/data/templates/ids/fastnetmon_networks_list.tmpl b/data/templates/ids/fastnetmon_networks_list.tmpl
new file mode 100644
index 000000000..d58990053
--- /dev/null
+++ b/data/templates/ids/fastnetmon_networks_list.tmpl
@@ -0,0 +1,7 @@
+{% if network is string %}
+{{ network }}
+{% else %}
+{% for net in network %}
+{{ net }}
+{% endfor %}
+{% endif %}
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/ipsec/charon.tmpl b/data/templates/ipsec/charon.tmpl
new file mode 100644
index 000000000..4d710921e
--- /dev/null
+++ b/data/templates/ipsec/charon.tmpl
@@ -0,0 +1,342 @@
+# Options for the charon IKE daemon.
+charon {
+
+ # Accept unencrypted ID and HASH payloads in IKEv1 Main Mode.
+ # accept_unencrypted_mainmode_messages = no
+
+ # Maximum number of half-open IKE_SAs for a single peer IP.
+ # block_threshold = 5
+
+ # Whether Certicate Revocation Lists (CRLs) fetched via HTTP or LDAP should
+ # be saved under a unique file name derived from the public key of the
+ # Certification Authority (CA) to /etc/ipsec.d/crls (stroke) or
+ # /etc/swanctl/x509crl (vici), respectively.
+ # cache_crls = no
+
+ # Whether relations in validated certificate chains should be cached in
+ # memory.
+ # cert_cache = yes
+
+ # Send Cisco Unity vendor ID payload (IKEv1 only).
+ # cisco_unity = no
+
+ # Close the IKE_SA if setup of the CHILD_SA along with IKE_AUTH failed.
+ # close_ike_on_child_failure = no
+
+ # Number of half-open IKE_SAs that activate the cookie mechanism.
+ # cookie_threshold = 10
+
+ # Delete CHILD_SAs right after they got successfully rekeyed (IKEv1 only).
+ # delete_rekeyed = no
+
+ # Use ANSI X9.42 DH exponent size or optimum size matched to cryptographic
+ # strength.
+ # dh_exponent_ansi_x9_42 = yes
+
+ # Use RTLD_NOW with dlopen when loading plugins and IMV/IMCs to reveal
+ # missing symbols immediately.
+ # dlopen_use_rtld_now = no
+
+ # DNS server assigned to peer via configuration payload (CP).
+ # dns1 =
+
+ # DNS server assigned to peer via configuration payload (CP).
+ # dns2 =
+
+ # Enable Denial of Service protection using cookies and aggressiveness
+ # checks.
+ # dos_protection = yes
+
+ # Compliance with the errata for RFC 4753.
+ # ecp_x_coordinate_only = yes
+
+ # Free objects during authentication (might conflict with plugins).
+ # flush_auth_cfg = no
+
+ # Whether to follow IKEv2 redirects (RFC 5685).
+ # follow_redirects = yes
+
+ # Maximum size (complete IP datagram size in bytes) of a sent IKE fragment
+ # when using proprietary IKEv1 or standardized IKEv2 fragmentation, defaults
+ # to 1280 (use 0 for address family specific default values, which uses a
+ # lower value for IPv4). If specified this limit is used for both IPv4 and
+ # IPv6.
+ # fragment_size = 1280
+
+ # Name of the group the daemon changes to after startup.
+ # group =
+
+ # Timeout in seconds for connecting IKE_SAs (also see IKE_SA_INIT DROPPING).
+ # half_open_timeout = 30
+
+ # Enable hash and URL support.
+ # hash_and_url = no
+
+ # Allow IKEv1 Aggressive Mode with pre-shared keys as responder.
+ # i_dont_care_about_security_and_use_aggressive_mode_psk = no
+
+ # Whether to ignore the traffic selectors from the kernel's acquire events
+ # for IKEv2 connections (they are not used for IKEv1).
+ # ignore_acquire_ts = no
+
+ # A space-separated list of routing tables to be excluded from route
+ # lookups.
+ # ignore_routing_tables =
+
+ # Maximum number of IKE_SAs that can be established at the same time before
+ # new connection attempts are blocked.
+ # ikesa_limit = 0
+
+ # Number of exclusively locked segments in the hash table.
+ # ikesa_table_segments = 1
+
+ # Size of the IKE_SA hash table.
+ # ikesa_table_size = 1
+
+ # Whether to close IKE_SA if the only CHILD_SA closed due to inactivity.
+ # inactivity_close_ike = no
+
+ # Limit new connections based on the current number of half open IKE_SAs,
+ # see IKE_SA_INIT DROPPING in strongswan.conf(5).
+ # init_limit_half_open = 0
+
+ # Limit new connections based on the number of queued jobs.
+ # init_limit_job_load = 0
+
+ # Causes charon daemon to ignore IKE initiation requests.
+ # initiator_only = no
+
+ # Install routes into a separate routing table for established IPsec
+ # tunnels.
+ install_routes = {{ install_routes }}
+
+ # Install virtual IP addresses.
+ # install_virtual_ip = yes
+
+ # The name of the interface on which virtual IP addresses should be
+ # installed.
+ # install_virtual_ip_on =
+
+ # Check daemon, libstrongswan and plugin integrity at startup.
+ # integrity_test = no
+
+ # A comma-separated list of network interfaces that should be ignored, if
+ # interfaces_use is specified this option has no effect.
+ # interfaces_ignore =
+
+ # A comma-separated list of network interfaces that should be used by
+ # charon. All other interfaces are ignored.
+ # interfaces_use =
+
+ # NAT keep alive interval.
+ # keep_alive = 20s
+
+ # Plugins to load in the IKE daemon charon.
+ # load =
+
+ # Determine plugins to load via each plugin's load option.
+ # load_modular = no
+
+ # Initiate IKEv2 reauthentication with a make-before-break scheme.
+ # make_before_break = no
+
+ # Maximum number of IKEv1 phase 2 exchanges per IKE_SA to keep state about
+ # and track concurrently.
+ # max_ikev1_exchanges = 3
+
+ # Maximum packet size accepted by charon.
+ # max_packet = 10000
+
+ # Enable multiple authentication exchanges (RFC 4739).
+ # multiple_authentication = yes
+
+ # WINS servers assigned to peer via configuration payload (CP).
+ # nbns1 =
+
+ # WINS servers assigned to peer via configuration payload (CP).
+ # nbns2 =
+
+ # UDP port used locally. If set to 0 a random port will be allocated.
+ # port = 500
+
+ # UDP port used locally in case of NAT-T. If set to 0 a random port will be
+ # allocated. Has to be different from charon.port, otherwise a random port
+ # will be allocated.
+ # port_nat_t = 4500
+
+ # Prefer locally configured proposals for IKE/IPsec over supplied ones as
+ # responder (disabling this can avoid keying retries due to
+ # INVALID_KE_PAYLOAD notifies).
+ # prefer_configured_proposals = yes
+
+ # By default public IPv6 addresses are preferred over temporary ones (RFC
+ # 4941), to make connections more stable. Enable this option to reverse
+ # this.
+ # prefer_temporary_addrs = no
+
+ # Process RTM_NEWROUTE and RTM_DELROUTE events.
+ # process_route = yes
+
+ # Delay in ms for receiving packets, to simulate larger RTT.
+ # receive_delay = 0
+
+ # Delay request messages.
+ # receive_delay_request = yes
+
+ # Delay response messages.
+ # receive_delay_response = yes
+
+ # Specific IKEv2 message type to delay, 0 for any.
+ # receive_delay_type = 0
+
+ # Size of the AH/ESP replay window, in packets.
+ # replay_window = 32
+
+ # Base to use for calculating exponential back off, see IKEv2 RETRANSMISSION
+ # in strongswan.conf(5).
+ # retransmit_base = 1.8
+
+ # Timeout in seconds before sending first retransmit.
+ # retransmit_timeout = 4.0
+
+ # Number of times to retransmit a packet before giving up.
+ # retransmit_tries = 5
+
+ # Interval in seconds to use when retrying to initiate an IKE_SA (e.g. if
+ # DNS resolution failed), 0 to disable retries.
+ # retry_initiate_interval = 0
+
+ # Initiate CHILD_SA within existing IKE_SAs (always enabled for IKEv1).
+ # reuse_ikesa = yes
+
+ # Numerical routing table to install routes to.
+ # routing_table =
+
+ # Priority of the routing table.
+ # routing_table_prio =
+
+ # Delay in ms for sending packets, to simulate larger RTT.
+ # send_delay = 0
+
+ # Delay request messages.
+ # send_delay_request = yes
+
+ # Delay response messages.
+ # send_delay_response = yes
+
+ # Specific IKEv2 message type to delay, 0 for any.
+ # send_delay_type = 0
+
+ # Send strongSwan vendor ID payload
+ # send_vendor_id = no
+
+ # Whether to enable Signature Authentication as per RFC 7427.
+ # signature_authentication = yes
+
+ # Whether to enable constraints against IKEv2 signature schemes.
+ # signature_authentication_constraints = yes
+
+ # Number of worker threads in charon.
+ # threads = 16
+
+ # Name of the user the daemon changes to after startup.
+ # user =
+
+ crypto_test {
+
+ # Benchmark crypto algorithms and order them by efficiency.
+ # bench = no
+
+ # Buffer size used for crypto benchmark.
+ # bench_size = 1024
+
+ # Number of iterations to test each algorithm.
+ # bench_time = 50
+
+ # Test crypto algorithms during registration (requires test vectors
+ # provided by the test-vectors plugin).
+ # on_add = no
+
+ # Test crypto algorithms on each crypto primitive instantiation.
+ # on_create = no
+
+ # Strictly require at least one test vector to enable an algorithm.
+ # required = no
+
+ # Whether to test RNG with TRUE quality; requires a lot of entropy.
+ # rng_true = no
+
+ }
+
+ host_resolver {
+
+ # Maximum number of concurrent resolver threads (they are terminated if
+ # unused).
+ # max_threads = 3
+
+ # Minimum number of resolver threads to keep around.
+ # min_threads = 0
+
+ }
+
+ leak_detective {
+
+ # Includes source file names and line numbers in leak detective output.
+ # detailed = yes
+
+ # Threshold in bytes for leaks to be reported (0 to report all).
+ # usage_threshold = 10240
+
+ # Threshold in number of allocations for leaks to be reported (0 to
+ # report all).
+ # usage_threshold_count = 0
+
+ }
+
+ processor {
+
+ # Section to configure the number of reserved threads per priority class
+ # see JOB PRIORITY MANAGEMENT in strongswan.conf(5).
+ priority_threads {
+
+ }
+
+ }
+
+ # Section containing a list of scripts (name = path) that are executed when
+ # the daemon is started.
+ start-scripts {
+
+ }
+
+ # Section containing a list of scripts (name = path) that are executed when
+ # the daemon is terminated.
+ stop-scripts {
+
+ }
+
+ tls {
+
+ # List of TLS encryption ciphers.
+ # cipher =
+
+ # List of TLS key exchange methods.
+ # key_exchange =
+
+ # List of TLS MAC algorithms.
+ # mac =
+
+ # List of TLS cipher suites.
+ # suites =
+
+ }
+
+ x509 {
+
+ # Discard certificates with unsupported or unknown critical extensions.
+ # enforce_critical = yes
+
+ }
+
+}
+
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/lcd/LCDd.conf.tmpl b/data/templates/lcd/LCDd.conf.tmpl
new file mode 100644
index 000000000..6cf6a440f
--- /dev/null
+++ b/data/templates/lcd/LCDd.conf.tmpl
@@ -0,0 +1,132 @@
+### Autogenerted by system-display.py ##
+
+# LCDd.conf -- configuration file for the LCDproc server daemon LCDd
+#
+# This file contains the configuration for the LCDd server.
+#
+# The format is ini-file-like. It is divided into sections that start at
+# markers that look like [section]. Comments are all line-based comments,
+# and are lines that start with '#' or ';'.
+#
+# The server has a 'central' section named [server]. For the menu there is
+# a section called [menu]. Further each driver has a section which
+# defines how the driver acts.
+#
+# The drivers are activated by specifying them in a driver= line in the
+# server section, like:
+#
+# Driver=curses
+#
+# This tells LCDd to use the curses driver.
+# The first driver that is loaded and is capable of output defines the
+# size of the display. The default driver to use is curses.
+# If the driver is specified using the -d <driver> command line option,
+# the Driver= options in the config file are ignored.
+#
+# The drivers read their own options from the respective sections.
+
+## Server section with all kinds of settings for the LCDd server ##
+[server]
+
+# Where can we find the driver modules ?
+# NOTE: Always place a slash as last character !
+DriverPath=/usr/lib/x86_64-linux-gnu/lcdproc/
+
+# Tells the server to load the given drivers. Multiple lines can be given.
+# The name of the driver is case sensitive and determines the section
+# where to look for further configuration options of the specific driver
+# as well as the name of the dynamic driver module to load at runtime.
+# The latter one can be changed by giving a File= directive in the
+# driver specific section.
+#
+# The following drivers are supported:
+# bayrad, CFontz, CFontzPacket, curses, CwLnx, ea65, EyeboxOne, futaba,
+# g15, glcd, glcdlib, glk, hd44780, icp_a106, imon, imonlcd,, IOWarrior,
+# irman, joy, lb216, lcdm001, lcterm, linux_input, lirc, lis, MD8800,
+# mdm166a, ms6931, mtc_s16209x, MtxOrb, mx5000, NoritakeVFD,
+# Olimex_MOD_LCD1x9, picolcd, pyramid, rawserial, sdeclcd, sed1330,
+# sed1520, serialPOS, serialVFD, shuttleVFD, sli, stv5730, svga, t6963,
+# text, tyan, ula200, vlsys_m428, xosd, yard2LCD
+
+{% if model is defined %}
+{% if model.startswith('cfa-') %}
+Driver=CFontzPacket
+{% elif model == 'sdec' %}
+Driver=sdeclcd
+{% endif %}
+{% endif %}
+
+# Tells the driver to bind to the given interface. [default: 127.0.0.1]
+Bind=127.0.0.1
+
+# Listen on this specified port. [default: 13666]
+Port=13666
+
+# Sets the reporting level; defaults to warnings and errors only.
+# [default: 2; legal: 0-5]
+ReportLevel=3
+
+# Should we report to syslog instead of stderr? [default: no; legal: yes, no]
+ReportToSyslog=yes
+
+# User to run as. LCDd will drop its root privileges and run as this user
+# instead. [default: nobody]
+User=nobody
+
+# The server will stay in the foreground if set to yes.
+# [default: no, legal: yes, no]
+Foreground=yes
+
+# Hello message: each entry represents a display line; default: builtin
+Hello="Starting VyOS..."
+
+# GoodBye message: each entry represents a display line; default: builtin
+GoodBye="VyOS shutdown..."
+
+# Sets the interval in microseconds for updating the display.
+# [default: 125000 meaning 8Hz]
+FrameInterval=500000 # 2 updates per second
+
+# Sets the default time in seconds to displays a screen. [default: 4]
+WaitTime=1
+
+# If set to no, LCDd will start with screen rotation disabled. This has the
+# same effect as if the ToggleRotateKey had been pressed. Rotation will start
+# if the ToggleRotateKey is pressed. Note that this setting does not turn off
+# priority sorting of screens. [default: on; legal: on, off]
+AutoRotate=on
+
+# If yes, the the serverscreen will be rotated as a usual info screen. If no,
+# it will be a background screen, only visible when no other screens are
+# active. The special value 'blank' is similar to no, but only a blank screen
+# is displayed. [default: on; legal: on, off, blank]
+ServerScreen=blank
+
+# Set master backlight setting. If set to 'open' a client may control the
+# backlight for its own screens (only). [default: open; legal: off, open, on]
+Backlight=on
+
+# Set master heartbeat setting. If set to 'open' a client may control the
+# heartbeat for its own screens (only). [default: open; legal: off, open, on]
+Heartbeat=off
+
+# set title scrolling speed [default: 10; legal: 0-10]
+TitleSpeed=10
+
+{% if model is defined and model is not none %}
+{% if model.startswith('cfa-') %}
+## CrystalFontz packet driver (for CFA533, CFA631, CFA633 & CFA635) ##
+[CFontzPacket]
+Model={{ model.split('-')[1] }}
+Device={{ device }}
+Contrast=350
+Brightness=500
+OffBrightness=50
+Reboot=yes
+USB=yes
+{% elif model == 'sdec' %}
+## SDEC driver for Lanner, Watchguard, Sophos sppliances ##
+[sdeclcd]
+# No options
+{% endif %}
+{% endif %}
diff --git a/data/templates/lcd/lcdproc.conf.tmpl b/data/templates/lcd/lcdproc.conf.tmpl
new file mode 100644
index 000000000..c79f3cd0d
--- /dev/null
+++ b/data/templates/lcd/lcdproc.conf.tmpl
@@ -0,0 +1,60 @@
+### autogenerated by system-lcd.py ###
+
+# LCDproc client configuration file
+
+[lcdproc]
+Server=127.0.0.1
+Port=13666
+
+# set reporting level
+ReportLevel=3
+
+# report to to syslog ?
+ReportToSyslog=true
+
+Foreground=yes
+
+[CPU]
+Active=true
+OnTime=1
+OffTime=2
+ShowInvisible=false
+
+[SMP-CPU]
+Active=false
+
+[Memory]
+Active=false
+
+[Load]
+Active=false
+
+[Uptime]
+Active=true
+
+[ProcSize]
+Active=false
+
+[Disk]
+Active=false
+
+[About]
+Active=false
+
+[TimeDate]
+Active=true
+TimeFormat="%H:%M:%S"
+
+[OldTime]
+Active=false
+
+[BigClock]
+Active=false
+
+[MiniClock]
+Active=false
+
+# Display the title bar in two-line mode. Note that with four lines or more
+# the title is always shown. [default: true; legal: true, false]
+ShowTitle=false
+
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/macsec/wpa_supplicant.conf.tmpl b/data/templates/macsec/wpa_supplicant.conf.tmpl
new file mode 100644
index 000000000..1731bf160
--- /dev/null
+++ b/data/templates/macsec/wpa_supplicant.conf.tmpl
@@ -0,0 +1,89 @@
+# autogenerated by interfaces-macsec.py
+
+# see full documentation:
+# https://w1.fi/cgit/hostap/plain/wpa_supplicant/wpa_supplicant.conf
+
+# For UNIX domain sockets (default on Linux and BSD): This is a directory that
+# will be created for UNIX domain sockets for listening to requests from
+# external programs (CLI/GUI, etc.) for status information and configuration.
+# The socket file will be named based on the interface name, so multiple
+# wpa_supplicant processes can be run at the same time if more than one
+# interface is used.
+# /var/run/wpa_supplicant is the recommended directory for sockets and by
+# default, wpa_cli will use it when trying to connect with wpa_supplicant.
+ctrl_interface=/run/wpa_supplicant
+
+# Note: When using MACsec, eapol_version shall be set to 3, which is
+# defined in IEEE Std 802.1X-2010.
+eapol_version=3
+
+# No need to scan for access points in MACsec mode
+ap_scan=0
+
+# EAP fast re-authentication
+fast_reauth=1
+
+network={
+ key_mgmt=NONE
+
+ # Note: When using wired authentication (including MACsec drivers),
+ # eapol_flags must be set to 0 for the authentication to be completed
+ # successfully.
+ eapol_flags=0
+
+ # macsec_policy: IEEE 802.1X/MACsec options
+ # This determines how sessions are secured with MACsec (only for MACsec
+ # drivers).
+ # 0: MACsec not in use (default)
+ # 1: MACsec enabled - Should secure, accept key server's advice to
+ # determine whether to use a secure session or not.
+ macsec_policy=1
+
+ # macsec_integ_only: IEEE 802.1X/MACsec transmit mode
+ # This setting applies only when MACsec is in use, i.e.,
+ # - macsec_policy is enabled
+ # - the key server has decided to enable MACsec
+ # 0: Encrypt traffic (default)
+ # 1: Integrity only
+ macsec_integ_only={{ '0' if security is defined and security.encrypt is defined else '1' }}
+
+{% if security is defined %}
+{% if security.encrypt is defined %}
+ # mka_cak, mka_ckn, and mka_priority: IEEE 802.1X/MACsec pre-shared key mode
+ # This allows to configure MACsec with a pre-shared key using a (CAK,CKN) pair.
+ # In this mode, instances of wpa_supplicant can act as MACsec peers. The peer
+ # with lower priority will become the key server and start distributing SAKs.
+ # mka_cak (CAK = Secure Connectivity Association Key) takes a 16-byte (128-bit)
+ # hex-string (32 hex-digits) or a 32-byte (256-bit) hex-string (64 hex-digits)
+ # mka_ckn (CKN = CAK Name) takes a 1..32-bytes (8..256 bit) hex-string
+ # (2..64 hex-digits)
+ mka_cak={{ security.mka.cak }}
+ mka_ckn={{ security.mka.ckn }}
+
+ # mka_priority (Priority of MKA Actor) is in 0..255 range with 255 being
+ # default priority
+ mka_priority={{ security.mka.priority }}
+{% endif %}
+
+{% if security.replay_window is defined %}
+ # macsec_replay_protect: IEEE 802.1X/MACsec replay protection
+ # This setting applies only when MACsec is in use, i.e.,
+ # - macsec_policy is enabled
+ # - the key server has decided to enable MACsec
+ # 0: Replay protection disabled (default)
+ # 1: Replay protection enabled
+ macsec_replay_protect=1
+
+ # macsec_replay_window: IEEE 802.1X/MACsec replay protection window
+ # This determines a window in which replay is tolerated, to allow receipt
+ # of frames that have been misordered by the network.
+ # This setting applies only when MACsec replay protection active, i.e.,
+ # - macsec_replay_protect is enabled
+ # - the key server has decided to enable MACsec
+ # 0: No replay window, strict check (default)
+ # 1..2^32-1: number of packets that could be misordered
+ macsec_replay_window={{ security.replay_window }}
+{% endif %}
+{% endif %}
+}
+
diff --git a/data/templates/mdns-repeater/mdns-repeater.tmpl b/data/templates/mdns-repeater/mdns-repeater.tmpl
new file mode 100644
index 000000000..80f4ab047
--- /dev/null
+++ b/data/templates/mdns-repeater/mdns-repeater.tmpl
@@ -0,0 +1,2 @@
+### Autogenerated by mdns_repeater.py ###
+DAEMON_ARGS="{{ interface | join(' ') }}"
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..6ef0c0f2c
--- /dev/null
+++ b/data/templates/ntp/ntp.conf.tmpl
@@ -0,0 +1,47 @@
+### 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 server %}
+{% for srv in server %}
+{% set options = '' %}
+{% set options = options + 'noselect ' if server[srv].noselect is defined else '' %}
+{% set options = options + 'preempt ' if server[srv].preempt is defined else '' %}
+{% set options = options + 'prefer ' if server[srv].prefer is defined else '' %}
+server {{ srv | replace('_', '-') }} iburst {{ options }}
+{% endfor %}
+{% endif %}
+
+{% if allow_clients is defined and allow_clients.address is defined %}
+# Allowed clients configuration
+{% if allow_clients.address is string %}
+restrict {{ allow_clients.address|address_from_cidr }} mask {{ allow_clients.address|netmask_from_cidr }} nomodify notrap nopeer
+{% else %}
+{% for address in allow_clients.address %}
+restrict {{ address|address_from_cidr }} mask {{ address|netmask_from_cidr }} nomodify notrap nopeer
+{% endfor %}
+{% endif %}
+{% endif %}
+
+{% if listen_address %}
+# NTP should listen on configured addresses only
+interface ignore wildcard
+{% if listen_address is string %}
+interface listen {{ listen_address }}
+{% else %}
+{% for address in listen_address %}
+interface listen {{ address }}
+{% endfor %}
+{% endif %}
+{% endif %}
diff --git a/data/templates/ntp/override.conf.tmpl b/data/templates/ntp/override.conf.tmpl
new file mode 100644
index 000000000..466638e5a
--- /dev/null
+++ b/data/templates/ntp/override.conf.tmpl
@@ -0,0 +1,11 @@
+{% set vrf_command = '/sbin/ip vrf exec ' + vrf + ' ' if vrf is defined else '' %}
+[Unit]
+StartLimitIntervalSec=0
+After=vyos-router.service
+
+[Service]
+ExecStart=
+ExecStart={{vrf_command}}/usr/lib/ntp/ntp-systemd-wrapper
+Restart=on-failure
+RestartSec=10
+
diff --git a/data/templates/ocserv/ocserv_config.tmpl b/data/templates/ocserv/ocserv_config.tmpl
new file mode 100644
index 000000000..6aaeff693
--- /dev/null
+++ b/data/templates/ocserv/ocserv_config.tmpl
@@ -0,0 +1,82 @@
+### generated by vpn_anyconnect.py ###
+
+tcp-port = {{ listen_ports.tcp }}
+udp-port = {{ listen_ports.udp }}
+
+run-as-user = nobody
+run-as-group = daemon
+
+{% if "radius" in authentication.mode %}
+auth = "radius [config=/run/ocserv/radiusclient.conf]"
+{% else %}
+auth = "plain[/run/ocserv/ocpasswd]"
+{% endif %}
+
+{% if ssl.cert_file %}
+server-cert = {{ ssl.cert_file }}
+{% endif %}
+
+{% if ssl.key_file %}
+server-key = {{ ssl.key_file }}
+{% endif %}
+
+{% if ssl.ca_cert_file %}
+ca-cert = {{ ssl.ca_cert_file }}
+{% endif %}
+
+socket-file = /run/ocserv/ocserv.socket
+occtl-socket-file = /run/ocserv/occtl.socket
+use-occtl = true
+isolate-workers = true
+keepalive = 300
+dpd = 60
+mobile-dpd = 300
+switch-to-tcp-timeout = 30
+tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA:-VERS-SSL3.0:-ARCFOUR-128"
+auth-timeout = 240
+idle-timeout = 1200
+mobile-idle-timeout = 1800
+min-reauth-time = 3
+cookie-timeout = 300
+rekey-method = ssl
+try-mtu-discovery = true
+cisco-client-compat = true
+dtls-legacy = true
+
+
+# The name to use for the tun device
+device = sslvpn
+
+# An alternative way of specifying the network:
+{% if network_settings %}
+# DNS settings
+{% if network_settings.name_server is string %}
+dns = {{ network_settings.name_server }}
+{% else %}
+{% for dns in network_settings.name_server %}
+dns = {{ dns }}
+{% endfor %}
+{% endif %}
+# IPv4 network pool
+{% if network_settings.client_ip_settings %}
+{% if network_settings.client_ip_settings.subnet %}
+ipv4-network = {{ network_settings.client_ip_settings.subnet }}
+{% endif %}
+{% endif %}
+# IPv6 network pool
+{% if network_settings.client_ipv6_pool %}
+{% if network_settings.client_ipv6_pool.prefix %}
+ipv6-network = {{ network_settings.client_ipv6_pool.prefix }}
+ipv6-subnet-prefix = {{ network_settings.client_ipv6_pool.mask }}
+{% endif %}
+{% endif %}
+{% endif %}
+
+{% if network_settings.push_route is string %}
+route = {{ network_settings.push_route }}
+{% else %}
+{% for route in network_settings.push_route %}
+route = {{ route }}
+{% endfor %}
+{% endif %}
+
diff --git a/data/templates/ocserv/ocserv_passwd.tmpl b/data/templates/ocserv/ocserv_passwd.tmpl
new file mode 100644
index 000000000..ffadb4860
--- /dev/null
+++ b/data/templates/ocserv/ocserv_passwd.tmpl
@@ -0,0 +1,6 @@
+#<username>:<group>:<hash>
+{% for user in username if username is defined %}
+{% if not "disable" in username[user] %}
+{{ user }}:*:{{ username[user].hash }}
+{% endif %}
+{% endfor %} \ No newline at end of file
diff --git a/data/templates/ocserv/radius_conf.tmpl b/data/templates/ocserv/radius_conf.tmpl
new file mode 100644
index 000000000..2d19306a0
--- /dev/null
+++ b/data/templates/ocserv/radius_conf.tmpl
@@ -0,0 +1,22 @@
+### generated by cpn_anyconnect.py ###
+nas-identifier VyOS
+{% for srv in server %}
+{% if not "disable" in server[srv] %}
+{% if "port" in server[srv] %}
+authserver {{ srv }}:{{server[srv]["port"]}}
+{% else %}
+authserver {{ srv }}
+{% endif %}
+{% endif %}
+{% endfor %}
+radius_timeout {{ timeout }}
+{% if source_address %}
+bindaddr {{ source_address }}
+{% else %}
+bindaddr *
+{% endif %}
+servers /run/ocserv/radius_servers
+dictionary /etc/radcli/dictionary
+default_realm
+radius_retries 3
+# \ No newline at end of file
diff --git a/data/templates/ocserv/radius_servers.tmpl b/data/templates/ocserv/radius_servers.tmpl
new file mode 100644
index 000000000..ba21fa074
--- /dev/null
+++ b/data/templates/ocserv/radius_servers.tmpl
@@ -0,0 +1,7 @@
+### generated by cpn_anyconnect.py ###
+# server key
+{% for srv in server %}
+{% if not "disable" in server[srv] %}
+{{ srv }} {{ server[srv].key }}
+{% endif %}
+{% endfor %}
diff --git a/data/templates/openvpn/client.conf.tmpl b/data/templates/openvpn/client.conf.tmpl
new file mode 100644
index 000000000..508d8da94
--- /dev/null
+++ b/data/templates/openvpn/client.conf.tmpl
@@ -0,0 +1,35 @@
+### Autogenerated by interfaces-openvpn.py ###
+
+{% if ip -%}
+ifconfig-push {{ ip[0] }} {{ remote_netmask }}
+{% endif -%}
+
+{% for route in push_route -%}
+push "route {{ route }}"
+{% endfor -%}
+
+{% for net in subnet -%}
+iroute {{ net }}
+{% endfor -%}
+
+{# ipv6_remote is only set when IPv6 server is enabled #}
+{% if ipv6_remote -%}
+# IPv6
+
+{%- if ipv6_ip %}
+ifconfig-ipv6-push {{ ipv6_ip[0] }} {{ ipv6_remote }}
+{%- endif %}
+
+{%- for route6 in ipv6_push_route %}
+push "route-ipv6 {{ route6 }}"
+{%- endfor %}
+
+{%- for net6 in ipv6_subnet %}
+iroute {{ net6 }}
+{%- endfor %}
+
+{% endif -%}
+
+{% 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..401f8e04b
--- /dev/null
+++ b/data/templates/openvpn/server.conf.tmpl
@@ -0,0 +1,262 @@
+### Autogenerated by interfaces-openvpn.py ###
+#
+# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
+# for individual keyword definition
+
+{% if description -%}
+# {{ description }}
+
+{% endif -%}
+
+verb 3
+
+user {{ uid }}
+group {{ gid }}
+
+dev-type {{ type }}
+dev {{ intf }}
+persist-key
+iproute /usr/libexec/vyos/system/unpriv-ip
+
+proto {{ protocol_real }}
+
+{%- 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 server_topology == 'point-to-point' %}p2p{% else %}{{ server_topology }}{% endif %}
+{%- endif %}
+
+{%- if is_bridge_member %}
+mode server
+tls-server
+{%- else %}
+server {{ server_subnet[0] }} nopool
+{%- endif %}
+
+{%- if server_pool %}
+ifconfig-pool {{ server_pool_start }} {{ server_pool_stop }}{% if server_pool_netmask %} {{ server_pool_netmask }}{% endif %}
+{%- endif %}
+
+{%- if server_max_conn %}
+max-clients {{ server_max_conn }}
+{%- endif %}
+
+{%- if client %}
+client-config-dir /run/openvpn/ccd/{{ intf }}
+{%- endif %}
+
+{%- if server_reject_unconfigured %}
+ccd-exclusive
+{%- endif %}
+
+keepalive {{ ping_interval }} {{ ping_restart }}
+management /run/openvpn/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 -%}
+
+{%- if server_ipv6_local %}
+# IPv6
+push "tun-ipv6"
+ifconfig-ipv6 {{ server_ipv6_local }}/{{ server_ipv6_prefixlen }} {{ server_ipv6_remote }}
+
+{%- if server_ipv6_pool %}
+ifconfig-ipv6-pool {{ server_ipv6_pool_base }}/{{ server_ipv6_pool_prefixlen }}
+{%- endif %}
+
+{%- for route6 in server_ipv6_push_route %}
+push "route-ipv6 {{ route6 }}"
+{%- endfor %}
+
+{%- for ns6 in server_ipv6_dns_nameserver %}
+push "dhcp-option DNS6 {{ ns6 }}"
+{%- endfor %}
+
+{%- endif %}
+
+{% else -%}
+#
+# OpenVPN site-2-site mode
+#
+ping {{ ping_interval }}
+ping-restart {{ ping_restart }}
+
+{% if local_address_subnet -%}
+ifconfig {{ local_address[0] }} {{ local_address_subnet }}
+{%- elif remote_address -%}
+ifconfig {{ local_address[0] }} {{ remote_address[0] }}
+{%- endif %}
+
+{% if ipv6_local_address -%}
+ifconfig-ipv6 {{ ipv6_local_address[0] }} {{ ipv6_remote_address[0] }}
+{%- 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 {{ auth_user_pass_file }}
+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/pppoe/ip-down.script.tmpl b/data/templates/pppoe/ip-down.script.tmpl
new file mode 100644
index 000000000..c2d0cd09a
--- /dev/null
+++ b/data/templates/pppoe/ip-down.script.tmpl
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+# As PPPoE is an "on demand" interface we need to re-configure it when it
+# becomes up
+if [ "$6" != "{{ ifname }}" ]; then
+ exit
+fi
+
+# add some info to syslog
+DIALER_PID=$(cat /var/run/{{ ifname }}.pid)
+logger -t pppd[$DIALER_PID] "executing $0"
+
+{% if connect_on_demand is not defined %}
+# 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/{{ ifname }}/upper_* ]; then
+ # Determine upper (VRF) interface
+ VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/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 {{ ifname }} ${VRF_NAME}"
+{% if ipv6_enable %}
+vtysh -c "conf t" ${VRF_NAME} -c "no ipv6 route ::/0 {{ ifname }} ${VRF_NAME}"
+{% endif %}
+{% endif %}
+
+{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %}
+# Stop wide dhcpv6 client
+systemctl stop dhcp6c@{{ ifname }}.service
+{% 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..cf85ed067
--- /dev/null
+++ b/data/templates/pppoe/ip-pre-up.script.tmpl
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# As PPPoE is an "on demand" interface we need to re-configure it when it
+# becomes up
+if [ "$6" != "{{ ifname }}" ]; then
+ exit
+fi
+
+# add some info to syslog
+DIALER_PID=$(cat /var/run/{{ ifname }}.pid)
+logger -t pppd[$DIALER_PID] "executing $0"
+
+echo "{{ description }}" > /sys/class/net/{{ ifname }}/ifalias
+
+{% if vrf -%}
+logger -t pppd[$DIALER_PID] "configuring dialer interface $6 for VRF {{ vrf }}"
+ip link set dev {{ ifname }} 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..568e21c4e
--- /dev/null
+++ b/data/templates/pppoe/ip-up.script.tmpl
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+# As PPPoE is an "on demand" interface we need to re-configure it when it
+# becomes up
+if [ "$6" != "{{ ifname }}" ]; then
+ exit
+fi
+
+{% if connect_on_demand is not defined %}
+# add some info to syslog
+DIALER_PID=$(cat /var/run/{{ ifname }}.pid)
+logger -t pppd[$DIALER_PID] "executing $0"
+
+{% if default_route != 'none' -%}
+# See https://phabricator.vyos.net/T2248 & T2220. Determine if we are enslaved
+# to a VRF, this is needed to properly insert the default route.
+
+SED_OPT="^ip route"
+VRF_NAME=""
+if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then
+ # Determine upper (VRF) interface
+ VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/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 default_route == 'auto' -%}
+# 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 default_route == 'force' -%}
+# 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 {{ ifname }} ${VRF_NAME}"
+logger -t pppd[$DIALER_PID] "added default route via {{ ifname }} ${VRF_NAME}"
+{% endif %}
+{% 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..d0a62478c
--- /dev/null
+++ b/data/templates/pppoe/ipv6-up.script.tmpl
@@ -0,0 +1,83 @@
+#!/bin/sh
+
+# As PPPoE is an "on demand" interface we need to re-configure it when it
+# becomes up
+
+if [ "$6" != "{{ ifname }}" ]; then
+ exit
+fi
+
+{% if ipv6 is defined and ipv6.address is defined and ipv6.address.autoconf is defined -%}
+# add some info to syslog
+DIALER_PID=$(cat /var/run/{{ ifname }}.pid)
+logger -t pppd[$DIALER_PID] "executing $0"
+logger -t pppd[$DIALER_PID] "configuring interface {{ ifname }} 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/{{ ifname }}/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/{{ ifname }}/accept_ra
+
+# Autoconfigure addresses using Prefix Information in Router Advertisements.
+echo 1 > /proc/sys/net/ipv6/conf/{{ ifname }}/autoconf
+{% endif %}
+
+{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %}
+# Start wide dhcpv6 client
+systemctl start dhcp6c@{{ ifname }}.service
+{% endif %}
+
+{% if default_route != 'none' -%}
+# See https://phabricator.vyos.net/T2248 & T2220. Determine if we are enslaved
+# to a VRF, this is needed to properly insert the default route.
+
+SED_OPT="^ipv6 route"
+VRF_NAME=""
+if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then
+ # Determine upper (VRF) interface
+ VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/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 default_route == 'auto' -%}
+# 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 | wc -l)
+if [ "$routes" -ne 0 ]; then
+ exit 1
+fi
+
+{% elif default_route == 'force' -%}
+# Retrieve current static default routes and remove it from the routing table
+vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep ::/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 "ipv6 route ::/0 {{ ifname }} ${VRF_NAME}"
+logger -t pppd[$DIALER_PID] "added default route via {{ ifname }} ${VRF_NAME}"
+{% endif %}
+
diff --git a/data/templates/pppoe/peer.tmpl b/data/templates/pppoe/peer.tmpl
new file mode 100644
index 000000000..e909843a5
--- /dev/null
+++ b/data/templates/pppoe/peer.tmpl
@@ -0,0 +1,76 @@
+### 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 {{ ifname }}
+ipparam {{ ifname }}
+debug
+mtu {{ mtu }}
+mru {{ mtu }}
+
+{% if authentication is defined %}
+{{ "user " + authentication.user if authentication.user is defined }}
+{{ "password " + authentication.password if authentication.password is defined }}
+{% endif %}
+
+{{ "usepeerdns" if no_peer_dns is not defined }}
+
+{% if ipv6 is defined and ipv6.enable is defined -%}
++ipv6
+ipv6cp-use-ipaddr
+{% endif %}
+
+{% if service_name is defined -%}
+rp_pppoe_service "{{ service_name }}"
+{% endif %}
+
+{% if connect_on_demand is defined %}
+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/router-advert/radvd.conf.tmpl b/data/templates/router-advert/radvd.conf.tmpl
new file mode 100644
index 000000000..cebfc54b5
--- /dev/null
+++ b/data/templates/router-advert/radvd.conf.tmpl
@@ -0,0 +1,47 @@
+### Autogenerated by service_router-advert.py ###
+
+{% if interface is defined and interface is not none %}
+{% for iface in interface %}
+interface {{ iface }} {
+ IgnoreIfMissing on;
+{% if interface[iface].default_preference is defined and interface[iface].default_preference is not none %}
+ AdvDefaultPreference {{ interface[iface].default_preference }};
+{% endif %}
+{% if interface[iface].managed_flag is defined and interface[iface].managed_flag is not none %}
+ AdvManagedFlag {{ 'on' if interface[iface].managed_flag is defined else 'off' }};
+{% endif %}
+{% if interface[iface].interval.max is defined and interface[iface].interval.max is not none %}
+ MaxRtrAdvInterval {{ interface[iface].interval.max }};
+{% endif %}
+{% if interface[iface].interval.min is defined and interface[iface].interval.min is not none %}
+ MinRtrAdvInterval {{ interface[iface].interval.min }};
+{% endif %}
+{% if interface[iface].reachable_time is defined and interface[iface].reachable_time is not none %}
+ AdvReachableTime {{ interface[iface].reachable_time }};
+{% endif %}
+ AdvIntervalOpt {{ 'off' if interface[iface].no_send_advert is defined else 'on' }};
+ AdvSendAdvert {{ 'off' if interface[iface].no_send_advert is defined else 'on' }};
+{% if interface[iface].default_lifetime is defined %}
+ AdvDefaultLifetime {{ interface[iface].default_lifetime }};
+{% endif %}
+{% if interface[iface].link_mtu is defined %}
+ AdvLinkMTU {{ interface[iface].link_mtu }};
+{% endif %}
+ AdvOtherConfigFlag {{ 'on' if interface[iface].other_config_flag is defined else 'off' }};
+ AdvRetransTimer {{ interface[iface].retrans_timer }};
+ AdvCurHopLimit {{ interface[iface].hop_limit }};
+{% for prefix in interface[iface].prefix %}
+ prefix {{ prefix }} {
+ AdvAutonomous {{ 'off' if interface[iface].prefix[prefix].no_autonomous_flag is defined else 'on' }};
+ AdvValidLifetime {{ interface[iface].prefix[prefix].valid_lifetime }};
+ AdvOnLink {{ 'off' if interface[iface].prefix[prefix].no_on_link_flag is defined else 'on' }};
+ AdvPreferredLifetime {{ interface[iface].prefix[prefix].preferred_lifetime }};
+ };
+{% endfor %}
+{% if interface[iface].name_server is defined %}
+ RDNSS {{ interface[iface].name_server | join(" ") }} {
+ };
+{% endif %}
+};
+{% endfor -%}
+{% endif %}
diff --git a/data/templates/rsyslog/rsyslog.conf b/data/templates/rsyslog/rsyslog.conf
new file mode 100644
index 000000000..ab60fc0f0
--- /dev/null
+++ b/data/templates/rsyslog/rsyslog.conf
@@ -0,0 +1,59 @@
+# /etc/rsyslog.conf Configuration file for rsyslog.
+#
+
+#################
+#### MODULES ####
+#################
+
+$ModLoad imuxsock # provides support for local system logging
+$ModLoad imklog # provides kernel logging support (previously done by rklogd)
+#$ModLoad immark # provides --MARK-- message capability
+
+$OmitLocalLogging off
+$SystemLogSocketName /run/systemd/journal/syslog
+
+$KLogPath /proc/kmsg
+
+# provides UDP syslog reception
+#$ModLoad imudp
+#$UDPServerRun 514
+
+# provides TCP syslog reception
+#$ModLoad imtcp
+#$InputTCPServerRun 514
+
+###########################
+#### GLOBAL DIRECTIVES ####
+###########################
+
+#
+# Use traditional timestamp format.
+# To enable high precision timestamps, comment out the following line.
+#
+$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
+
+# Filter duplicated messages
+$RepeatedMsgReduction on
+
+#
+# Set the default permissions for all log files.
+#
+$FileOwner root
+$FileGroup adm
+$FileCreateMode 0640
+$DirCreateMode 0755
+$Umask 0022
+
+
+#
+# Include all config files in /etc/rsyslog.d/
+#
+$IncludeConfig /etc/rsyslog.d/*.conf
+
+###############
+#### RULES ####
+###############
+# Emergencies are sent to everybody logged in.
+
+*.emerg :omusrmsg:*
+
diff --git a/data/templates/salt-minion/minion.tmpl b/data/templates/salt-minion/minion.tmpl
new file mode 100644
index 000000000..9369573a4
--- /dev/null
+++ b/data/templates/salt-minion/minion.tmpl
@@ -0,0 +1,59 @@
+### 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 }}
+
+##### 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: file:///dev/log
+
+# 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: {{ 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..6e4c6f063
--- /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..278506350
--- /dev/null
+++ b/data/templates/snmp/etc.snmpd.conf.tmpl
@@ -0,0 +1,115 @@
+### 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 trap in trap_targets %}
+trap2sink {{ trap.target }}{{ ":" + trap.port if trap.port is defined }} {{ trap.community }}
+{% endfor %}
+{% endif %}
+
+{% if v3_enabled %}
+#
+# SNMPv3 stuff goes here
+#
+# views
+{% for view in v3_views %}
+{% for oid in view.oids %}
+view {{ view.name }} included .{{ oid.oid }}
+{% endfor %}
+{% endfor %}
+
+# access
+# context sec.model sec.level match read write notif
+{% for group in v3_groups %}
+access {{ group.name }} "" usm {{ group.seclevel }} exact {{ group.view }} {% if group.mode == 'ro' %}none{% else %}{{ group.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/override.conf.tmpl b/data/templates/snmp/override.conf.tmpl
new file mode 100644
index 000000000..e6302a9e1
--- /dev/null
+++ b/data/templates/snmp/override.conf.tmpl
@@ -0,0 +1,13 @@
+{% set vrf_command = '/sbin/ip vrf exec ' + vrf + ' ' if vrf is defined else '' %}
+[Unit]
+StartLimitIntervalSec=0
+After=vyos-router.service
+
+[Service]
+Environment=
+Environment="MIBSDIR=/usr/share/snmp/mibs:/usr/share/snmp/mibs/iana:/usr/share/snmp/mibs/ietf:/usr/share/mibs/site:/usr/share/snmp/mibs:/usr/share/mibs/iana:/usr/share/mibs/ietf:/usr/share/mibs/netsnmp"
+ExecStart=
+ExecStart={{vrf_command}}/usr/sbin/snmpd -LS0-5d -Lf /dev/null -u Debian-snmp -g Debian-snmp -I -ipCidrRouteTable,inetCidrRouteTable -f -p /run/snmpd.pid
+Restart=on-failure
+RestartSec=10
+
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..6cbc687ef
--- /dev/null
+++ b/data/templates/snmp/var.snmpd.conf.tmpl
@@ -0,0 +1,14 @@
+### Autogenerated by snmp.py ###
+# user
+{%- for u in v3_users %}
+{%- if u.authOID == 'none' %}
+createUser {{ u.name }}
+{%- else %}
+usmUser 1 3 0x{{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} 0x{{ u.authMasterKey }} {{ u.privOID }} 0x{{ u.privMasterKey }} 0x
+{%- endif %}
+{%- endfor %}
+
+createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES
+{%- if v3_engineid %}
+oldEngineID 0x{{ v3_engineid }}
+{%- endif %}
diff --git a/data/templates/ssh/override.conf.tmpl b/data/templates/ssh/override.conf.tmpl
new file mode 100644
index 000000000..843aa927b
--- /dev/null
+++ b/data/templates/ssh/override.conf.tmpl
@@ -0,0 +1,11 @@
+{% set vrf_command = '/sbin/ip vrf exec ' + vrf + ' ' if vrf is defined else '' %}
+[Unit]
+StartLimitIntervalSec=0
+After=vyos-router.service
+ConditionPathExists={{config_file}}
+
+[Service]
+ExecStart=
+ExecStart={{vrf_command}}/usr/sbin/sshd -f {{config_file}} -D $SSHD_OPTS
+RestartSec=10
+
diff --git a/data/templates/ssh/sshd_config.tmpl b/data/templates/ssh/sshd_config.tmpl
new file mode 100644
index 000000000..4fde24255
--- /dev/null
+++ b/data/templates/ssh/sshd_config.tmpl
@@ -0,0 +1,114 @@
+### Autogenerated by ssh.py ###
+
+# https://linux.die.net/man/5/sshd_config
+
+#
+# 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
+PermitRootLogin no
+
+#
+# User configurable section
+#
+
+# Look up remote host name and check that the resolved host name for the remote IP
+# address maps back to the very same IP address.
+UseDNS {{ "no" if disable_host_validation is defined else "yes" }}
+
+# Specifies the port number that sshd(8) listens on
+{% if port is string %}
+Port {{ port }}
+{% else %}
+{% for value in port %}
+Port {{ value }}
+{% endfor %}
+{% endif %}
+
+# Gives the verbosity level that is used when logging messages from sshd
+LogLevel {{ loglevel | upper }}
+
+# Specifies whether password authentication is allowed
+PasswordAuthentication {{ "no" if disable_password_authentication is defined else "yes" }}
+
+{% if listen_address %}
+# Specifies the local addresses sshd should listen on
+{% if listen_address is string %}
+ListenAddress {{ listen_address }}
+{% else %}
+{% for address in listen_address %}
+ListenAddress {{ address }}
+{% endfor %}
+{% endif %}
+{% endif %}
+
+{% if ciphers %}
+# Specifies the ciphers allowed for protocol version 2
+{% set value = ciphers if ciphers is string else ciphers | join(',') %}
+Ciphers {{ value }}
+{% endif %}
+
+{% if mac %}
+# Specifies the available MAC (message authentication code) algorithms
+{% set value = mac if mac is string else mac | join(',') %}
+MACs {{ value }}
+{% endif %}
+
+{% if key_exchange %}
+# Specifies the available Key Exchange algorithms
+{% set value = key_exchange if key_exchange is string else key_exchange | join(',') %}
+KexAlgorithms {{ value }}
+{% endif %}
+
+{% if access_control is defined %}
+{% if access_control.allow is defined %}
+{% if access_control.allow.user is defined %}
+# If specified, login is allowed only for user names that match
+{% set value = access_control.allow.user if access_control.allow.user is string else access_control.allow.user | join(' ') %}
+AllowUsers {{ value }}
+{% endif %}
+{% if access_control.allow.group is defined %}
+# If specified, login is allowed only for users whose primary group or supplementary group list matches
+{% set value = access_control.allow.group if access_control.allow.group is string else access_control.allow.group | join(' ') %}
+AllowGroups {{ value }}
+{% endif %}
+{% endif %}
+{% if access_control.deny is defined %}
+{% if access_control.deny.user is defined %}
+# Login is disallowed for user names that match
+{% set value = access_control.deny.user if access_control.deny.user is string else access_control.deny.user | join(' ') %}
+DenyUsers {{ value }}
+{% endif %}
+{% if access_control.deny.group is defined %}
+# Login is disallowed for users whose primary group or supplementary group list matches
+{% set value = access_control.deny.group if access_control.deny.group is string else access_control.deny.group | join(' ') %}
+DenyGroups {{ value }}
+{% endif %}
+{% endif %}
+{% endif %}
+
+{% if client_keepalive_interval %}
+# Sets a timeout interval in seconds after which if no data has been received from the client,
+# sshd(8) will send a message through the encrypted channel to request a response from the client
+ClientAliveInterval {{ client_keepalive_interval }}
+{% endif %}
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..ec2d6df95
--- /dev/null
+++ b/data/templates/system-login/pam_radius_auth.conf.tmpl
@@ -0,0 +1,16 @@
+# Automatically generated by system-login.py
+# RADIUS configuration file
+{% if radius_server %}
+# server[:port] shared_secret timeout source_ip
+{% for s in radius_server|sort(attribute='priority') if not s.disabled %}
+{% set addr_port = s.address + ":" + s.port %}
+{{ "%-22s" | format(addr_port) }} {{ "%-25s" | format(s.key) }} {{ "%-10s" | format(s.timeout) }} {{ radius_source_address if radius_source_address }}
+{% endfor %}
+
+priv-lvl 15
+mapped_priv_user radius_priv_user
+
+{% if radius_vrf %}
+vrf-name {{ radius_vrf }}
+{% endif %}
+{% endif %}
diff --git a/data/templates/system/curlrc.tmpl b/data/templates/system/curlrc.tmpl
new file mode 100644
index 000000000..3e5ce801c
--- /dev/null
+++ b/data/templates/system/curlrc.tmpl
@@ -0,0 +1,8 @@
+{% if http_client is defined %}
+{% if http_client.source_interface is defined %}
+--interface "{{ http_client.source_interface }}"
+{% endif %}
+{% if http_client.source_address is defined %}
+--interface "{{ http_client.source_address }}"
+{% endif %}
+{% endif %}
diff --git a/data/templates/system/ssh_config.tmpl b/data/templates/system/ssh_config.tmpl
new file mode 100644
index 000000000..509bd5479
--- /dev/null
+++ b/data/templates/system/ssh_config.tmpl
@@ -0,0 +1,3 @@
+{% if ssh_client is defined and ssh_client.source_address is defined and ssh_client.source_address is not none %}
+BindAddress {{ ssh_client.source_address }}
+{% 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..c9dbea72d
--- /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/vyos-hostsd/hosts.tmpl b/data/templates/vyos-hostsd/hosts.tmpl
new file mode 100644
index 000000000..566f9a5dd
--- /dev/null
+++ b/data/templates/vyos-hostsd/hosts.tmpl
@@ -0,0 +1,26 @@
+### Autogenerated by VyOS ###
+### Do not edit, your changes will get overwritten ###
+
+# Local host
+127.0.0.1 localhost
+127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }} {{ host_name }}{% endif %}
+
+# The following lines are desirable for IPv6 capable hosts
+::1 localhost ip6-localhost ip6-loopback
+fe00::0 ip6-localnet
+ff00::0 ip6-mcastprefix
+ff02::1 ip6-allnodes
+ff02::2 ip6-allrouters
+
+{% if hosts -%}
+# From 'system static-host-mapping' and DHCP server
+{%- for tag, taghosts in hosts.items() %}
+# {{ tag }}
+{%- for host, hostprops in taghosts.items() %}
+{%- if hostprops['address'] %}
+{{ hostprops['address'] }} {{ host }}{% for a in hostprops['aliases'] %} {{ a }}{% endfor %}
+{%- endif %}
+{%- endfor %}
+{%- endfor %}
+{%- endif %}
+
diff --git a/data/templates/vyos-hostsd/resolv.conf.tmpl b/data/templates/vyos-hostsd/resolv.conf.tmpl
new file mode 100644
index 000000000..b920b2e5f
--- /dev/null
+++ b/data/templates/vyos-hostsd/resolv.conf.tmpl
@@ -0,0 +1,26 @@
+### Autogenerated by VyOS ###
+### Do not edit, your changes will get overwritten ###
+
+{#- the code below ensures the order of nameservers is determined first by #}
+{# the order of tags, then by the order of nameservers within that tag #}
+
+{%- for tag in name_server_tags_system %}
+{%- if tag in name_servers %}
+# {{ tag }}
+{%- for ns in name_servers[tag] %}
+nameserver {{ ns }}
+{%- endfor %}
+{%- endif %}
+{%- endfor %}
+
+{%- if domain_name %}
+domain {{ domain_name }}
+{%- endif %}
+
+{% for tag in name_server_tags_system %}
+{%- if tag in search_domains %}
+# {{ tag }}
+search {{ search_domains[tag]|join(' ') }}
+{%- endif %}
+{%- endfor %}
+
diff --git a/data/templates/wifi/cfg80211.conf.tmpl b/data/templates/wifi/cfg80211.conf.tmpl
new file mode 100644
index 000000000..91df57aab
--- /dev/null
+++ b/data/templates/wifi/cfg80211.conf.tmpl
@@ -0,0 +1 @@
+{{ 'options cfg80211 ieee80211_regdom=' + regdom if regdom is defined }}
diff --git a/data/templates/wifi/crda.tmpl b/data/templates/wifi/crda.tmpl
new file mode 100644
index 000000000..6cd125e37
--- /dev/null
+++ b/data/templates/wifi/crda.tmpl
@@ -0,0 +1 @@
+{{ 'REGDOMAIN=' + regdom if regdom is defined }}
diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl
new file mode 100644
index 000000000..765668c57
--- /dev/null
+++ b/data/templates/wifi/hostapd.conf.tmpl
@@ -0,0 +1,687 @@
+### 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={{ ifname }}
+
+# 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
+{% 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 %}
+
+{% if capabilities is defined and capabilities.ht is defined %}
+# 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)
+{% set output = '' %}
+{% set output = output + '[40-INTOLERANT]' if capabilities.ht.fourtymhz_incapable is defined else '' %}
+{% set output = output + '[DELAYED-BA]' if capabilities.ht.delayed_block_ack is defined else '' %}
+{% set output = output + '[DSSS_CCK-40]' if capabilities.ht.dsss_cck_40 is defined else '' %}
+{% set output = output + '[GF]' if capabilities.ht.greenfield is defined else '' %}
+{% set output = output + '[LDPC]' if capabilities.ht.ldpc is defined else '' %}
+{% set output = output + '[LSIG-TXOP-PROT]' if capabilities.ht.lsig_protection is defined else '' %}
+{% set output = output + '[TX-STBC]' if capabilities.ht.stbc.tx is defined else '' %}
+{% set output = output + '[RX-STBC-' + capabilities.ht.stbc.rx | upper + ']' if capabilities.ht.stbc.tx is defined else '' %}
+{% set output = output + '[MAX-AMSDU-' + capabilities.ht.max_amsdu + ']' if capabilities.ht.max_amsdu is defined else '' %}
+{% set output = output + '[SMPS-' + capabilities.ht.smps | upper + ']' if capabilities.ht.smps is defined else '' %}
+
+{% if capabilities.ht.channel_set_width is defined %}
+{% for csw in capabilities.ht.channel_set_width %}
+{% set output = output + '[' + csw | upper + ']' %}
+{% endfor %}
+{% endif %}
+
+{% if capabilities.ht.short_gi is defined %}
+{% for short_gi in capabilities.ht.short_gi %}
+{% set output = output + '[SHORT-GI-' + short_gi | upper + ']' %}
+{% endfor %}
+{% endif %}
+
+ht_capab={{ output }}
+
+{% if capabilities.ht.auto_powersave is defined %}
+# 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 %}
+
+{% endif %}
+
+# Required for full HT and VHT functionality
+wme_enabled=1
+
+
+{% if capabilities is defined and capabilities.require_ht is defined %}
+# Require stations to support HT PHY (reject association if they do not)
+require_ht=1
+{% endif %}
+
+{% if capabilities is defined and capabilities.vht is defined %}
+# 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
+
+{% if capabilities.vht.center_channel_freq.freq_1 is defined %}
+# 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={{ capabilities.vht.center_channel_freq.freq_1 }}
+{% endif %}
+
+{% if capabilities.vht.center_channel_freq.freq_2 is defined %}
+# 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={{ capabilities.vht.center_channel_freq.freq_2 }}
+{% endif %}
+
+{% if capabilities.vht.channel_set_width is defined %}
+vht_oper_chwidth={{ capabilities.vht.channel_set_width }}
+{% endif %}
+
+{% set output = '' %}
+{% set output = output + '[TX-STBC-2BY1]' if capabilities.vht.stbc.tx is defined else '' %}
+{% set output = output + '[RXLDPC]' if capabilities.vht.ldpc is defined else '' %}
+{% set output = output + '[VHT-TXOP-PS]' if capabilities.vht.tx_powersave is defined else '' %}
+{% set output = output + '[HTC-VHT]' if capabilities.vht.vht_cf is defined else '' %}
+{% set output = output + '[RX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %}
+{% set output = output + '[TX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %}
+
+{% set output = output + '[RX-STBC-' + capabilities.vht.stbc.rx + ']' if capabilities.vht.stbc.rx is defined else '' %}
+{% set output = output + '[MAX-MPDU-' + capabilities.vht.max_mpdu + ']' if capabilities.vht.max_mpdu is defined else '' %}
+{% set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %}
+{% set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %}
+
+{% set output = output + '[VHT160]' if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '2' else '' %}
+{% set output = output + '[VHT160-80PLUS80]' if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '3' else '' %}
+{% set output = output + '[VHT-LINK-ADAPT2]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'unsolicited' else '' %}
+{% set output = output + '[VHT-LINK-ADAPT3]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'both' else '' %}
+
+{% if capabilities.vht.short_gi is defined %}
+{% for short_gi in capabilities.vht.short_gi %}
+{% set output = output + '[SHORT-GI-' + short_gi | upper + ']' %}
+{% endfor %}
+{% endif %}
+
+{% if capabilities.vht.beamform %}
+{% for beamform in capabilities.vht.beamform %}
+{% set output = output + '[SU-BEAMFORMER]' if beamform == 'single-user-beamformer' else '' %}
+{% set output = output + '[SU-BEAMFORMEE]' if beamform == 'single-user-beamformee' else '' %}
+{% set output = output + '[MU-BEAMFORMER]' if beamform == 'multi-user-beamformer' else '' %}
+{% set output = output + '[MU-BEAMFORMEE]' if beamform == 'multi-user-beamformee' else '' %}
+{% endfor %}
+{% endif %}
+
+{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 %}
+{% if capabilities.vht.beamform %}
+{% if beamform == 'single-user-beamformer' %}
+{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 6 %}
+{% set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count|int -1 + ']' %}
+{% set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count|int -1 + ']' %}
+{% endif %}
+{% endif %}
+{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 5 %}
+{% set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count + ']' %}
+{% set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count+ ']' %}
+{% endif %}
+{% endif %}
+{% endif %}
+
+vht_capab={{ output }}
+{% 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 capabilities is defined and capabilities.require_vht is defined %}
+ieee80211n=0
+# Require stations to support VHT PHY (reject association if they do not)
+require_vht=1
+{% else %}
+{% if 'n' in mode or 'ac' in mode %}
+ieee80211n=1
+{% else %}
+ieee80211n=0
+{% endif %}
+{% endif %}
+
+{% if disable_broadcast_ssid is defined %}
+# 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 is defined %}
+# 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 is defined %}
+# 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 is defined %}
+# 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 is defined %}
+# 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 security is defined and security.wep is defined %}
+# 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 security.wep.key is defined %}
+{% for key in sec_wep_key %}
+wep_key{{ loop.index -1 }}={{ security.wep.key }}
+{% endfor %}
+{% endif %}
+
+
+{% elif security is defined and security.wpa is defined %}
+##### 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 security.wpa.mode is defined %}
+{% if security.wpa.mode == 'both' %}
+wpa=3
+{% elif security.wpa.mode == 'wpa2' %}
+wpa=2
+{% elif security.wpa.mode == 'wpa' %}
+wpa=1
+{% endif %}
+{% endif %}
+
+{% if security.wpa.cipher is defined %}
+# 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 security.wpa.mode is defined and security.wpa.mode == 'wpa2' %}
+# Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value)
+{% if security.wpa.cipher is string %}
+rsn_pairwise={{ security.wpa.cipher }}
+{% else %}
+rsn_pairwise={{ security.wpa.cipher | join(" ") }}
+{% endif %}
+{% else %}
+# Pairwise cipher for WPA (v1) (default: TKIP)
+{% if security.wpa.cipher is string %}
+wpa_pairwise={{ security.wpa.cipher }}
+{% else %}
+wpa_pairwise={{ security.wpa.cipher | join(" ") }}
+{% endif %}
+{% endif %}
+{% endif %}
+
+{% if security.wpa.group_cipher is defined %}
+# Optional override for automatic group cipher selection
+# This can be used to select a specific group cipher regardless of which
+# pairwise ciphers were enabled for WPA and RSN. It should be noted that
+# overriding the group cipher with an unexpected value can result in
+# interoperability issues and in general, this parameter is mainly used for
+# testing purposes.
+{% if security.wpa.group_cipher is string %}
+group_cipher={{ security.wpa.group_cipher }}
+{% else %}
+group_cipher={{ security.wpa.group_cipher | join(" ") }}
+{% endif %}
+{% endif %}
+
+{% if security.wpa.passphrase is defined %}
+# 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={{ security.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 security.wpa.radius is defined %}
+##### 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 security.wpa.radius.server is defined %}
+# 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.
+# The own IP address of the access point (used as NAS-IP-Address)
+{% if security.wpa.radius.source_address is defined %}
+radius_client_addr={{ security.wpa.radius.source_address }}
+own_ip_addr={{ security.wpa.radius.source_address }}
+{% else %}
+own_ip_addr=127.0.0.1
+{% endif %}
+
+{% for radius in security.wpa.radius.server 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 %}
+{% endfor %}
+{% else %}
+# Open system
+auth_algs=1
+{% endif %}
+{% endif %}
+{% 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..9ddad35fd
--- /dev/null
+++ b/data/templates/wifi/wpa_supplicant.conf.tmpl
@@ -0,0 +1,9 @@
+# WPA supplicant config
+network={
+ ssid="{{ ssid }}"
+{% if security is defined and security.wpa is defined and security.wpa.passphrase is defined %}
+ psk="{{ security.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..9dc15ea99
--- /dev/null
+++ b/data/templates/wwan/ip-down.script.tmpl
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+# Script parameters will be like:
+# wlm0 /dev/serial/by-bus/usb0b1.3p1.3 115200 10.100.118.91 10.64.64.64 wlm0
+
+# Only applicable for Wireless Modems (WWAN)
+if [ -z $(echo $2 | egrep "(ttyS[0-9]+|usb[0-9]+b.*)$") ]; then
+ exit 0
+fi
+
+# Determine if we are running inside a VRF or not, required for proper routing table
+# NOTE: the down script can not be properly templated as we need the VRF name,
+# which is not present on deletion, thus we read it from the operating system.
+if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then
+ # Determine upper (VRF) interface
+ VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/upper_*))
+ # Remove upper_ prefix from result string
+ VRF_NAME=${VRF#"upper_"}
+ # Remove default route from VRF routing table
+ vtysh -c "conf t" -c "vrf ${VRF_NAME}" -c "no ip route 0.0.0.0/0 {{ ifname }}"
+else
+ # Remove default route from GRT (global routing table)
+ vtysh -c "conf t" -c "no ip route 0.0.0.0/0 {{ ifname }}"
+fi
+
+DIALER_PID=$(cat /var/run/{{ ifname }}.pid)
+logger -t pppd[$DIALER_PID] "removed default route via {{ ifname }} metric {{ backup.distance }}"
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..efc065bad
--- /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" != "{{ ifname }}" ]; then
+ exit
+fi
+
+# add some info to syslog
+DIALER_PID=$(cat /var/run/{{ ifname }}.pid)
+logger -t pppd[$DIALER_PID] "executing $0"
+
+echo "{{ description }}" > /sys/class/net/{{ ifname }}/ifalias
+
+{% if vrf -%}
+logger -t pppd[$DIALER_PID] "configuring interface {{ ifname }} for VRF {{ vrf }}"
+ip link set dev {{ ifname }} 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..2603a0286
--- /dev/null
+++ b/data/templates/wwan/ip-up.script.tmpl
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+# Script parameters will be like:
+# wlm0 /dev/serial/by-bus/usb0b1.3p1.3 115200 10.100.118.91 10.64.64.64 wlm0
+
+# Only applicable for Wireless Modems (WWAN)
+if [ -z $(echo $2 | egrep "(ttyS[0-9]+|usb[0-9]+b.*)$") ]; then
+ exit 0
+fi
+
+# Determine if we are running inside a VRF or not, required for proper routing table
+if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then
+ # Determine upper (VRF) interface
+ VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/upper_*))
+ # Remove upper_ prefix from result string
+ VRF_NAME=${VRF#"upper_"}
+ # Remove default route from VRF routing table
+ vtysh -c "conf t" -c "vrf ${VRF_NAME}" -c "ip route 0.0.0.0/0 {{ ifname }} {{ backup.distance }}"
+else
+ # Remove default route from GRT (global routing table)
+ vtysh -c "conf t" -c "ip route 0.0.0.0/0 {{ ifname }} {{ backup.distance }}"
+fi
+
+DIALER_PID=$(cat /var/run/{{ ifname }}.pid)
+logger -t pppd[$DIALER_PID] "added default route via {{ ifname }} metric {{ backup.distance }} ${VRF_NAME}"
diff --git a/data/templates/wwan/peer.tmpl b/data/templates/wwan/peer.tmpl
new file mode 100644
index 000000000..aa759f741
--- /dev/null
+++ b/data/templates/wwan/peer.tmpl
@@ -0,0 +1,27 @@
+### Autogenerated by interfaces-wirelessmodem.py ###
+
+{{ "# description: " + description if description is defined }}
+ifname {{ ifname }}
+ipparam {{ ifname }}
+linkname {{ ifname }}
+{{ "usepeerdns" if no_peer_dns is defined }}
+# physical device
+{{ device }}
+lcp-echo-failure 0
+115200
+debug
+debug
+mtu {{ mtu }}
+mru {{ mtu }}
+nodefaultroute
+ipcp-max-failure 4
+ipcp-accept-local
+ipcp-accept-remote
+noauth
+crtscts
+lock
+persist
+{{ "demand" if ondemand is defined }}
+
+connect '/usr/sbin/chat -v -t6 -f /etc/ppp/peers/chat.{{ ifname }}'
+
diff --git a/debian/changelog b/debian/changelog
index 2b24a8f9e..fba9d77d0 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,5 +1,10 @@
-vyos-smoketest (1.0.0) unstable; urgency=medium
+vyos-1x (1.0.0) unstable; urgency=medium
- * Initial release
+ * Dummy changelog entry for vyos-1x repository
+ This is a internal VyOS package and the VyOS package process does not use
+ the debian package changelog for its changes, please refer to the
+ GitHub commitlog and the vyos release-notes for more details.
+ The correct verion number of this package is auto-generated by GIT
+ on build-time
- -- Daniil Baturin <daniil@baturin.org> Tue, 02 Jul 2019 22:29:53 +0200
+ -- Runar Borge <runar@borge.nu> Sat, 9 May 2020 22:00:00 +0100
diff --git a/debian/control b/debian/control
index ada46a4c1..65808de86 100644
--- a/debian/control
+++ b/debian/control
@@ -1,22 +1,129 @@
-Source: vyos-smoketest
+Source: vyos-1x
Section: contrib/net
Priority: extra
Maintainer: VyOS Package Maintainers <maintainers@vyos.net>
-Build-Depends: debhelper (>= 9),
- quilt,
+Build-Depends:
+ debhelper (>= 9),
+ fakeroot,
+ libvyosconfig0 (>= 0.0.7),
python3,
- python3-setuptools,
- quilt,
+ python3-coverage,
python3-lxml,
python3-nose,
- python3-coverage
+ python3-setuptools,
+ python3-xmltodict,
+ quilt,
+ whois
Standards-Version: 3.9.6
-Package: vyos-smoketest
+Package: vyos-1x
Architecture: all
-Depends: python3,
+Depends:
+ accel-ppp,
+ beep,
+ bmon,
+ bsdmainutils,
+ conntrack,
+ conserver-client,
+ conserver-server,
+ crda,
+ cron,
+ dbus,
+ dropbear,
+ easy-rsa,
+ etherwake,
+ fastnetmon,
+ file,
+ frr,
+ frr-pythontools,
+ hostapd (>= 0.6.8),
+ hvinfo,
+ igmpproxy,
+ ipaddrcheck,
+ iperf,
+ iperf3,
+ iputils-arping,
+ isc-dhcp-client,
+ isc-dhcp-relay,
+ isc-dhcp-server,
+ iw,
+ keepalived (>=2.0.5),
+ lcdproc,
+ libatomic1,
+ libndp-tools,
+ libpam-radius-auth (>= 1.5.0),
+ libvyosconfig0,
+ lldpd,
+ lm-sensors,
+ lsscsi,
+ mdns-repeater,
+ mtr-tiny,
+ nftables (>= 0.9.3),
+ nginx-light,
+ ntp,
+ ntpdate,
+ ocserv,
+ openssh-server,
+ openvpn,
+ openvpn-auth-ldap,
+ openvpn-auth-radius,
+ pciutils,
+ pdns-recursor,
+ pmacct (>= 1.6.0),
+ pppoe,
+ procps,
+ python3,
+ python3-certbot-nginx,
${python3:Depends},
- ${misc:Depends},
- vyos-1x
+ python3-flask,
+ python3-hurry.filesize,
+ python3-isc-dhcp-leases,
+ python3-jinja2,
+ python3-jmespath,
+ python3-netaddr,
+ python3-netifaces,
+ python3-psutil,
+ python3-pystache,
+ python3-pyudev,
+ python3-six,
+ python3-tabulate,
+ python3-vici (>= 5.7.2),
+ python3-voluptuous,
+ python3-waitress,
+ python3-xmltodict,
+ python3-zmq,
+ radvd,
+ salt-minion,
+ snmp,
+ snmpd,
+ ssl-cert,
+ systemd,
+ tcpdump,
+ tcptraceroute,
+ telnet,
+ tftpd-hpa,
+ traceroute,
+ udp-broadcast-relay,
+ usb-modeswitch,
+ usbutils,
+ vyos-utils,
+ wide-dhcpv6-client,
+ wireguard,
+ wireless-regdb,
+ wpasupplicant (>= 0.6.7)
+Description: VyOS configuration scripts and data
+ VyOS configuration scripts, interface definitions, and everything
+
+Package: vyos-1x-vmware
+Architecture: amd64 i386
+Depends:
+ vyos-1x,
+ open-vm-tools
+Description: VyOS configuration scripts and data for VMware
+ Adds configuration files required for VyOS running on VMware hosts.
+
+Package: vyos-1x-smoketest
+Architecture: all
+Depends:
+ vyos-1x
Description: VyOS build sanity checking toolkit
- VyOS build sanity checking toolkit
diff --git a/debian/copyright b/debian/copyright
index a3260a7a1..20704c47c 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,7 +1,7 @@
This package was debianized by Daniil Baturin <daniil@baturin.org> on
-Tue, 02 Jul 2019 22:24:20 +0200
+Thu, 17 Aug 2017 20:17:04 -0400
-It's original content from the GIT repository <http://github.com/vyos/vyos-smoketest>
+It's original content from the GIT repository <http://github.com/vyos/vyos-1x>
Upstream Author:
@@ -9,13 +9,13 @@ Upstream Author:
Copyright:
- Copyright (C) 2019 VyOS maintainers and contributors
+ Copyright (C) 2017 VyOS maintainers and contributors
All Rights Reserved.
License:
This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU Lesser General Public License as published by
+it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
@@ -26,10 +26,10 @@ General Public License for more details.
A copy of the GNU General Public License is available as
`/usr/share/common-licenses/GPL' in the Debian GNU/Linux distribution
-or on the World Wide Web at `http://www.gnu.org/copyleft/lgpl.html'.
+or on the World Wide Web at `http://www.gnu.org/copyleft/gpl.html'.
You can also obtain it by writing to the Free Software Foundation,
Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
MA 02110-1301, USA.
-The Debian packaging is (C) 2019, Daniil Baturin <daniil@baturin.org> and
+The Debian packaging is (C) 2017, Daniil Baturin <daniil@baturin.org> and
is licensed under the GPL, see above.
diff --git a/debian/lintian-overrides b/debian/lintian-overrides
new file mode 100644
index 000000000..6c5d67159
--- /dev/null
+++ b/debian/lintian-overrides
@@ -0,0 +1,6 @@
+# It's FSH compliant!
+vyos-1x: file-in-unusual-dir usr/libexec/*
+vyos-1x: non-standard-dir-in-usr usr/libexec/
+
+# Nothing we can do about that right now
+vyos-1x: dir-or-file-in-opt
diff --git a/debian/rules b/debian/rules
index 526fb8537..58bafd333 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,25 +1,101 @@
#!/usr/bin/make -f
-DIR := debian/vyos-smoketest
-VYOS_BIN_DIR := usr/bin/
+DIR := debian/tmp
+VYOS_SBIN_DIR := usr/sbin
+VYOS_BIN_DIR := usr/bin
VYOS_LIBEXEC_DIR := usr/libexec/vyos
-VYOS_DATA_DIR := /usr/share/vyos
-VYOS_CFG_TMPL_DIR := /opt/vyatta/share/vyatta-cfg/templates
-VYOS_OP_TMPL_DIR := /opt/vyatta/share/vyatta-op/templates
+VYOS_DATA_DIR := usr/share/vyos
+VYOS_CFG_TMPL_DIR := opt/vyatta/share/vyatta-cfg/templates
+VYOS_OP_TMPL_DIR := opt/vyatta/share/vyatta-op/templates
-MIGRATION_SCRIPTS_DIR := /opt/vyatta/etc/config-migrate/migrate/
+MIGRATION_SCRIPTS_DIR := opt/vyatta/etc/config-migrate/migrate
+SYSTEM_SCRIPTS_DIR := usr/libexec/vyos/system
+SERVICES_DIR := usr/libexec/vyos/services
%:
dh $@ --with python3, --with quilt
+override_dh_gencontrol:
+ dh_gencontrol -- -v$(shell (git describe --tags --long --match 'vyos/*' --dirty 2>/dev/null || echo 0.0-no.git.tag) | sed -E 's%vyos/%%' | sed -E 's%-dirty%+dirty%')
+
override_dh_auto_build:
make all
override_dh_auto_install:
+ dh_auto_install
+
+ # convert the XML to dictionaries
+ env PYTHONPATH=python python3 python/vyos/xml/generate.py
+
+ cd python; python3 setup.py install --install-layout=deb --root ../$(DIR); cd ..
+
+ # Install scripts
+ mkdir -p $(DIR)/$(VYOS_SBIN_DIR)
+ mkdir -p $(DIR)/$(VYOS_BIN_DIR)
+ cp -r src/utils/* $(DIR)/$(VYOS_BIN_DIR)
+
+ # Install conf mode scripts
+ mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/conf_mode
+ cp -r src/conf_mode/* $(DIR)/$(VYOS_LIBEXEC_DIR)/conf_mode
+
+ # Install op mode scripts
+ mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/op_mode
+ cp -r src/op_mode/* $(DIR)/$(VYOS_LIBEXEC_DIR)/op_mode
+
+ # Install validators
+ mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/validators
+ cp -r src/validators/* $(DIR)/$(VYOS_LIBEXEC_DIR)/validators
+
+ # Install completion helpers
+ mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/completion
+ cp -r src/completion/* $(DIR)/$(VYOS_LIBEXEC_DIR)/completion
+
+ # Install helper scripts
+ cp -r src/helpers/* $(DIR)/$(VYOS_LIBEXEC_DIR)/
+
+ # Install migration scripts
+ mkdir -p $(DIR)/$(MIGRATION_SCRIPTS_DIR)
+ cp -r src/migration-scripts/* $(DIR)/$(MIGRATION_SCRIPTS_DIR)
+
+ # Install system scripts
+ mkdir -p $(DIR)/$(SYSTEM_SCRIPTS_DIR)
+ cp -r src/system/* $(DIR)/$(SYSTEM_SCRIPTS_DIR)
+
+ # Install system services
+ mkdir -p $(DIR)/$(SERVICES_DIR)
+ cp -r src/services/* $(DIR)/$(SERVICES_DIR)
+
+ # Install configuration command definitions
+ mkdir -p $(DIR)/$(VYOS_CFG_TMPL_DIR)
+ cp -r templates-cfg/* $(DIR)/$(VYOS_CFG_TMPL_DIR)
+
+ # Install operational command definitions
+ mkdir -p $(DIR)/$(VYOS_OP_TMPL_DIR)
+ cp -r templates-op/* $(DIR)/$(VYOS_OP_TMPL_DIR)
+
+ # Install data files
+ mkdir -p $(DIR)/$(VYOS_DATA_DIR)
+ cp -r data/* $(DIR)/$(VYOS_DATA_DIR)
+
+ # Install etc configuration files
+ mkdir -p $(DIR)/etc
+ cp -r src/etc/* $(DIR)/etc
+
+ # Install PAM configuration snippets
+ mkdir -p $(DIR)/usr/share/pam-configs
+ cp -r src/pam-configs/* $(DIR)/usr/share/pam-configs
+
+ # Install systemd service units
+ mkdir -p $(DIR)/lib/systemd/system
+ cp -r src/systemd/* $(DIR)/lib/systemd/system
+
+ # Make directory for generated configuration file
+ mkdir -p $(DIR)/etc/vyos
+
# Install smoke test scripts
mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/smoke/
- cp -r scripts/* $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/smoke
+ cp -r smoketest/scripts/* $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/smoke
# Install system programs
mkdir -p $(DIR)/$(VYOS_BIN_DIR)
- cp -r bin/* $(DIR)/$(VYOS_BIN_DIR)
+ cp -r smoketest/bin/* $(DIR)/$(VYOS_BIN_DIR)
diff --git a/debian/vyos-1x-smoketest.install b/debian/vyos-1x-smoketest.install
new file mode 100644
index 000000000..fdf949557
--- /dev/null
+++ b/debian/vyos-1x-smoketest.install
@@ -0,0 +1,2 @@
+usr/bin/vyos-smoketest
+usr/libexec/vyos/tests/smoke
diff --git a/debian/vyos-1x-vmware.install b/debian/vyos-1x-vmware.install
new file mode 100644
index 000000000..715015621
--- /dev/null
+++ b/debian/vyos-1x-vmware.install
@@ -0,0 +1 @@
+etc/vmware-tools
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
new file mode 100644
index 000000000..6cd678442
--- /dev/null
+++ b/debian/vyos-1x.install
@@ -0,0 +1,23 @@
+etc/dhcp
+etc/ppp
+etc/rsyslog.d
+etc/systemd
+etc/sysctl.d
+etc/udev
+etc/vyos
+lib/
+opt/
+usr/bin/initial-setup
+usr/bin/vyos-config-file-query
+usr/bin/vyos-config-to-commands
+usr/bin/vyos-config-to-json
+usr/bin/vyos-hostsd-client
+usr/lib
+usr/libexec/vyos/completion
+usr/libexec/vyos/conf_mode
+usr/libexec/vyos/op_mode
+usr/libexec/vyos/services
+usr/libexec/vyos/system
+usr/libexec/vyos/validators
+usr/libexec/vyos/*.py
+usr/share
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
new file mode 100644
index 000000000..dc129cb54
--- /dev/null
+++ b/debian/vyos-1x.postinst
@@ -0,0 +1,32 @@
+#!/bin/sh -e
+if ! deb-systemd-helper --quiet was-enabled salt-minion.service; then
+ # Enables the unit on first installation, creates new
+ # symlinks on upgrades if the unit file has changed.
+ deb-systemd-helper disable salt-minion.service >/dev/null || true
+fi
+
+if [ -x "/etc/init.d/salt-minion" ]; then
+ update-rc.d -f salt-minion remove >/dev/null
+fi
+
+# Add minion user for salt-minion
+if ! grep -q '^minion' /etc/passwd; then
+ adduser --quiet --firstuid 100 --system --disabled-login --ingroup vyattacfg --gecos "salt minion user" --shell /bin/vbash minion
+ adduser --quiet minion frrvty
+ adduser --quiet minion sudo
+ adduser --quiet minion adm
+ adduser --quiet minion dip
+ adduser --quiet minion disk
+ adduser --quiet minion users
+fi
+
+# add hostsd group for vyos-hostsd
+if ! grep -q '^hostsd' /etc/group; then
+ addgroup --quiet --system hostsd
+fi
+
+# add dhcpd user for dhcp-server
+if ! grep -q '^dhcpd' /etc/passwd; then
+ adduser --quiet --system --disabled-login --no-create-home --home /run/dhcp-server dhcpd
+ adduser --quiet dhcpd hostsd
+fi
diff --git a/interface-definitions/arp.xml.in b/interface-definitions/arp.xml.in
new file mode 100644
index 000000000..b72f025a8
--- /dev/null
+++ b/interface-definitions/arp.xml.in
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="protocols">
+ <children>
+ <node name="static">
+ <children>
+ <tagNode name="arp" owner="${vyos_conf_scripts_dir}/arp.py">
+ <properties>
+ <help>Static ARP translation</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 destination address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="hwaddr">
+ <properties>
+ <help>mac address to translate to</help>
+ <valueHelp>
+ <format>h:h:h:h:h:h</format>
+ <description>Hardware (MAC) address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/bcast-relay.xml.in b/interface-definitions/bcast-relay.xml.in
new file mode 100644
index 000000000..96ce16639
--- /dev/null
+++ b/interface-definitions/bcast-relay.xml.in
@@ -0,0 +1,80 @@
+<?xml version="1.0"?>
+<!-- UDP broadcast relay configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="broadcast-relay" owner="${vyos_conf_scripts_dir}/bcast_relay.py">
+ <properties>
+ <help>UDP broadcast relay service</help>
+ <priority>990</priority>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Globally disable broadcast relay service</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <tagNode name="id">
+ <properties>
+ <help>Unique ID for each UDP port to forward</help>
+ <valueHelp>
+ <format>1-99</format>
+ <description>Numerical ID #</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-99"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable broadcast relay service instance</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="address">
+ <properties>
+ <help>Set source IP of forwarded packets, otherwise original senders address is used</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Optional source address for forwarded packets</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Description</help>
+ </properties>
+ </leafNode>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface to repeat UDP broadcasts to [REQUIRED]</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Destination or source port to listen and retransmit on [REQUIRED]</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>UDP port to listen on</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/cron.xml.in b/interface-definitions/cron.xml.in
new file mode 100644
index 000000000..2d4921bf0
--- /dev/null
+++ b/interface-definitions/cron.xml.in
@@ -0,0 +1,75 @@
+<?xml version="1.0"?>
+
+<!-- Cron configuration -->
+
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="task-scheduler">
+ <properties>
+ <help>Task scheduler settings</help>
+ </properties>
+ <children>
+ <tagNode name="task" owner="${vyos_conf_scripts_dir}/task_scheduler.py">
+ <properties>
+ <help>Scheduled task</help>
+ <valueHelp>
+ <format>&lt;string&gt;</format>
+ <description>Task name</description>
+ </valueHelp>
+ <priority>999</priority>
+ </properties>
+ <children>
+ <leafNode name="crontab-spec">
+ <properties>
+ <help>UNIX crontab time specification string</help>
+ </properties>
+ </leafNode>
+ <leafNode name="interval">
+ <properties>
+ <help>Execution interval</help>
+ <valueHelp>
+ <format>&lt;minutes&gt;</format>
+ <description>Execution interval in minutes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;minutes&gt;m</format>
+ <description>Execution interval in minutes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;hours&gt;h</format>
+ <description>Execution interval in hours</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;days&gt;d</format>
+ <description>Execution interval in days</description>
+ </valueHelp>
+ <constraint>
+ <regex>[1-9]([0-9]*)([mhd]{0,1})</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="executable">
+ <properties>
+ <help>Executable path and arguments</help>
+ </properties>
+ <children>
+ <leafNode name="path">
+ <properties>
+ <help>Path to executable</help>
+ </properties>
+ </leafNode>
+ <leafNode name="arguments">
+ <properties>
+ <help>Arguments passed to the executable</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/dhcp-relay.xml.in b/interface-definitions/dhcp-relay.xml.in
new file mode 100644
index 000000000..b83402aa1
--- /dev/null
+++ b/interface-definitions/dhcp-relay.xml.in
@@ -0,0 +1,98 @@
+<?xml version="1.0"?>
+<!-- DHCP relay configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="dhcp-relay" owner="${vyos_conf_scripts_dir}/dhcp_relay.py">
+ <properties>
+ <help>Host Configuration Protocol (DHCP) relay agent</help>
+ <priority>910</priority>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>DHCP relay interface [REQUIRED]</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py -b</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="relay-options">
+ <properties>
+ <help>Relay options</help>
+ </properties>
+ <children>
+ <leafNode name="hop-count">
+ <properties>
+ <help>Policy to discard packets that have reached specified hop-count</help>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Hop count (default: 10)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ <constraintErrorMessage>hop-count must be a value between 1 and 255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="max-size">
+ <properties>
+ <help>Maximum packet size to send to a DHCPv4/BOOTP server</help>
+ <valueHelp>
+ <format>64-1400</format>
+ <description>Maximum packet size (default: 576)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 64-1400"/>
+ </constraint>
+ <constraintErrorMessage>max-size must be a value between 64 and 1400</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="relay-agents-packets">
+ <properties>
+ <help>Policy to handle incoming DHCPv4 packets which already contain relay agent options (default: forward)</help>
+ <completionHelp>
+ <list>append replace forward discard</list>
+ </completionHelp>
+ <valueHelp>
+ <format>append</format>
+ <description>append own relay options to packet</description>
+ </valueHelp>
+ <valueHelp>
+ <format>replace</format>
+ <description>replace existing agent option field</description>
+ </valueHelp>
+ <valueHelp>
+ <format>forward</format>
+ <description>forward packet unchanged</description>
+ </valueHelp>
+ <valueHelp>
+ <format>discard</format>
+ <description>discard packet (default action if giaddr not set in packet)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(append|replace|forward|discard)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="server">
+ <properties>
+ <help>DHCP server address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>DHCP server IPv4 address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in
new file mode 100644
index 000000000..e8bdff3df
--- /dev/null
+++ b/interface-definitions/dhcp-server.xml.in
@@ -0,0 +1,467 @@
+<?xml version="1.0"?>
+<!-- DHCP server configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="dhcp-server" owner="${vyos_conf_scripts_dir}/dhcp_server.py">
+ <properties>
+ <help>Dynamic Host Configuration Protocol (DHCP) for DHCP server</help>
+ <priority>911</priority>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable DHCP server</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="dynamic-dns-update">
+ <properties>
+ <help>DHCP server to dynamically update the Domain Name System (DNS)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="global-parameters">
+ <properties>
+ <help>Additional global parameters for DHCP server. You must
+ use the syntax of dhcpd.conf in this text-field. Using this
+ without proper knowledge may result in a crashed DHCP server.
+ Check system log to look for errors.</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="hostfile-update">
+ <properties>
+ <help>Enable DHCP server updating /etc/hosts (per client lease)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="host-decl-name">
+ <properties>
+ <help>Instruct server to use host declaration name for forward DNS name</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <tagNode name="shared-network-name">
+ <properties>
+ <help>DHCP shared network name [REQUIRED]</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9.]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="authoritative">
+ <properties>
+ <help>Option to make DHCP server authoritative for this physical network</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Shared-network-name description</help>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable DHCP configuration for shared-network</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="shared-network-parameters">
+ <properties>
+ <help>Additional shared-network parameters for DHCP server.
+ You must use the syntax of dhcpd.conf in this text-field.
+ Using this without proper knowledge may result in a crashed
+ DHCP server. Check system log to look for errors.</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <tagNode name="subnet">
+ <properties>
+ <help>DHCP subnet for shared network</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="bootfile-name">
+ <properties>
+ <help>Bootstrap file name</help>
+ </properties>
+ </leafNode>
+ <leafNode name="bootfile-server">
+ <properties>
+ <help>Server (IP address or domain name) from which the initial
+ boot file is to be loaded</help>
+ </properties>
+ </leafNode>
+ <leafNode name="client-prefix-length">
+ <properties>
+ <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help>
+ <valueHelp>
+ <format>0-32</format>
+ <description>DHCP client prefix length must be 0 to 32</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-32"/>
+ </constraint>
+ <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="default-router">
+ <properties>
+ <help>IP address of default router</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Default router IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="dns-server">
+ <properties>
+ <help>DNS server IPv4 address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>DNS server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="domain-name">
+ <properties>
+ <help>Client domain name</help>
+ </properties>
+ </leafNode>
+ <leafNode name="domain-search">
+ <properties>
+ <help>Client domain search</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="exclude">
+ <properties>
+ <help>IP address to exclude from DHCP lease range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to exclude from lease range</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="failover">
+ <properties>
+ <help>DHCP failover parameters</help>
+ </properties>
+ <children>
+ <leafNode name="local-address">
+ <properties>
+ <help>IP address for failover peer to connect [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to exclude from lease range</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="name">
+ <properties>
+ <help>DHCP failover peer name [REQUIRED]</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9.]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid failover peer name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="peer-address">
+ <properties>
+ <help>IP address of failover peer [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address of failover peer</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="status">
+ <properties>
+ <help>DHCP failover peer status (primary|secondary) [REQUIRED]</help>
+ <completionHelp>
+ <list>primary secondary</list>
+ </completionHelp>
+ <constraint>
+ <regex>(primary|secondary)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid DHCP failover peer status</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="ip-forwarding">
+ <properties>
+ <help>Enable IP forwarding on client</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="lease">
+ <properties>
+ <help>Lease timeout in seconds (default: 86400)</help>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>DHCP lease time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ <constraintErrorMessage>DHCP lease time must be between 0 and 4294967295 (49 days)</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="ntp-server">
+ <properties>
+ <help>IP address of NTP server</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>NTP server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="pop-server">
+ <properties>
+ <help>IP address of POP3 server</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>POP3 server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="server-identifier">
+ <properties>
+ <help>Address for DHCP server identifier</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>DHCP server identifier IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="smtp-server">
+ <properties>
+ <help>IP address of SMTP server</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>SMTP server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <tagNode name="range">
+ <properties>
+ <help>DHCP lease range</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9.]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid DHCP lease range name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="start">
+ <properties>
+ <help>First IP address for DHCP lease range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 start address of pool</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="stop">
+ <properties>
+ <help>Last IP address for DHCP lease range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 end address of pool</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="static-mapping">
+ <properties>
+ <help>Name of static mapping</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9.]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid static mapping name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable static mapping</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ip-address">
+ <properties>
+ <help>Fixed IP address of static mapping</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address used in static mapping</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mac-address">
+ <properties>
+ <help>MAC address of static mapping [REQUIRED]</help>
+ <valueHelp>
+ <format>h:h:h:h:h:h</format>
+ <description>MAC address used in static mapping [REQUIRED]</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="static-mapping-parameters">
+ <properties>
+ <help>Additional static-mapping parameters for DHCP server.
+ Will be placed inside the "host" block of the mapping.
+ You must use the syntax of dhcpd.conf in this text-field.
+ Using this without proper knowledge may result in a crashed
+ DHCP server. Check system log to look for errors.</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="static-route">
+ <properties>
+ <help>Classless static route</help>
+ </properties>
+ <children>
+ <leafNode name="destination-subnet">
+ <properties>
+ <help>Destination subnet [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="router">
+ <properties>
+ <help>IP address of router to be used to reach the destination subnet [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address of router</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="subnet-parameters">
+ <properties>
+ <help>Additional subnet parameters for DHCP server. You must
+ use the syntax of dhcpd.conf in this text-field. Using this
+ without proper knowledge may result in a crashed DHCP server.
+ Check system log to look for errors.</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="tftp-server-name">
+ <properties>
+ <help>TFTP server name</help>
+ </properties>
+ </leafNode>
+ <leafNode name="time-offset">
+ <properties>
+ <help>Client subnet offset in seconds from Coordinated Universal Time (UTC)</help>
+ <valueHelp>
+ <format>[-]N</format>
+ <description>Time offset (number, may be negative)</description>
+ </valueHelp>
+ <constraint>
+ <regex>-?[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid time offset value</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="time-server">
+ <properties>
+ <help>IP address of time server</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Time server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="wins-server">
+ <properties>
+ <help>IP address for Windows Internet Name Service (WINS) server</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>WINS server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="wpad-url">
+ <properties>
+ <help>Web Proxy Autodiscovery (WPAD) URL</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/dhcpv6-relay.xml.in b/interface-definitions/dhcpv6-relay.xml.in
new file mode 100644
index 000000000..0beb09d05
--- /dev/null
+++ b/interface-definitions/dhcpv6-relay.xml.in
@@ -0,0 +1,80 @@
+<?xml version="1.0"?>
+<!-- DHCPv6 relay configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="dhcpv6-relay" owner="${vyos_conf_scripts_dir}/dhcpv6_relay.py">
+ <properties>
+ <help>DHCPv6 Relay Agent parameters</help>
+ <priority>900</priority>
+ </properties>
+ <children>
+ <tagNode name="listen-interface">
+ <properties>
+ <help>Interface for DHCPv6 Relay Agent to listen for requests</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IPv6 address on listen-interface listen for requests on</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address on listen interface</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="max-hop-count">
+ <properties>
+ <help>Maximum hop count for which requests will be processed</help>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Hop count (default: 10)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ <constraintErrorMessage>max-hop-count must be a value between 1 and 255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <tagNode name="upstream-interface">
+ <properties>
+ <help>Interface for DHCPv6 Relay Agent forward requests out</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IPv6 address to forward requests to</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address of the DHCP server</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="use-interface-id-option">
+ <properties>
+ <help>Option to set DHCPv6 interface-ID option</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in
new file mode 100644
index 000000000..4073b46b2
--- /dev/null
+++ b/interface-definitions/dhcpv6-server.xml.in
@@ -0,0 +1,344 @@
+<?xml version="1.0"?>
+<!-- DHCPv6 server configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="dhcpv6-server" owner="${vyos_conf_scripts_dir}/dhcpv6_server.py">
+ <properties>
+ <help>DHCP for IPv6 (DHCPv6) server</help>
+ <priority>900</priority>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable DHCPv6 server</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="preference">
+ <properties>
+ <help>Preference of this DHCPv6 server compared with others</help>
+ <valueHelp>
+ <format>0-255</format>
+ <description>DHCPv6 server preference (0-255)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ <constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <tagNode name="shared-network-name">
+ <properties>
+ <help>DHCPv6 shared network name [REQUIRED]</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9.]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid DHCPv6 shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable DHCPv6 configuration for shared-network</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <tagNode name="subnet">
+ <properties>
+ <help>IPv6 DHCP subnet for this shared network [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <node name="address-range">
+ <properties>
+ <help>Parameters setting ranges for assigning IPv6 addresses</help>
+ </properties>
+ <children>
+ <tagNode name="prefix">
+ <properties>
+ <help>IPv6 prefix defining range of addresses to assign</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="temporary">
+ <properties>
+ <help>Address range will be used for temporary addresses</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="start">
+ <properties>
+ <help>First in range of consecutive IPv6 addresses to assign</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="stop">
+ <properties>
+ <help>Last in range of consecutive IPv6 addresses</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <leafNode name="domain-search">
+ <properties>
+ <help>Domain name for client to search</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9.]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid domain name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="lease-time">
+ <properties>
+ <help>Parameters relating to the lease time</help>
+ </properties>
+ <children>
+ <leafNode name="default">
+ <properties>
+ <help>Default time (in seconds) that will be assigned to a lease</help>
+ <valueHelp>
+ <format>1-4294967295</format>
+ <description>DHCPv6 valid lifetime</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="maximum">
+ <properties>
+ <help>Maximum time (in seconds) that will be assigned to a lease</help>
+ <valueHelp>
+ <format>1-4294967295</format>
+ <description>Maximum lease time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="minimum">
+ <properties>
+ <help>Minimum time (in seconds) that will be assigned to a lease</help>
+ <valueHelp>
+ <format>1-4294967295</format>
+ <description>Minimum lease time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="name-server">
+ <properties>
+ <help>IPv6 address of a 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="nis-domain">
+ <properties>
+ <help>NIS domain name for client to use</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9.]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid NIS domain name</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="nis-server">
+ <properties>
+ <help>IPv6 address of a NIS Server</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address of NIS server</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="nisplus-domain">
+ <properties>
+ <help>NIS+ domain name for client to use</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9.]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid NIS+ domain name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="nisplus-server">
+ <properties>
+ <help>IPv6 address of a NIS+ Server</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address of NIS+ server</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="prefix-delegation">
+ <properties>
+ <help>Parameters relating to IPv6 prefix delegation</help>
+ </properties>
+ <children>
+ <tagNode name="start">
+ <properties>
+ <help>First in range of IPv6 addresses to be used in prefix delegation</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address used in prefix delegation</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="prefix-length">
+ <properties>
+ <help>Length in bits of prefixes to be delegated</help>
+ <valueHelp>
+ <format>0-255</format>
+ <description>DHCPv6 server preference (0-255)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ <constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="stop">
+ <properties>
+ <help>Last in range of IPv6 addresses to be used in prefix delegation</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address used in prefix delegation</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <leafNode name="sip-server">
+ <properties>
+ <help>IPv6 address of SIP server</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address of SIP server</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hostname</format>
+ <description>FQDN of SIP server</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ <validator name="fqdn"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="sntp-server">
+ <properties>
+ <help>IPv6 address of an SNTP server for client to use</help>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <tagNode name="static-mapping">
+ <properties>
+ <help>Name of static mapping</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9.]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid static mapping name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable static mapping</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="identifier">
+ <properties>
+ <help>Client identifier (DUID) for this static mapping</help>
+ <valueHelp>
+ <format>h[[:h]...]</format>
+ <description>DUID: colon-separated hex list (as used by isc-dhcp option dhcpv6.client-id)</description>
+ </valueHelp>
+ <constraint>
+ <regex>([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2})</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid DUID, must be in the format h[[:h]...]</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="ipv6-address">
+ <properties>
+ <help>Client IPv6 address for this static mapping</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address for this static mapping</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in
new file mode 100644
index 000000000..3b5843b53
--- /dev/null
+++ b/interface-definitions/dns-domain-name.xml.in
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<!-- host-name configuration -->
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <leafNode name="name-server" owner="${vyos_conf_scripts_dir}/host_name.py">
+ <properties>
+ <help>Domain Name Servers (DNS) used by the system (resolv.conf)</help>
+ <priority>400</priority>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Domain Name Server (DNS) address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="name-servers-dhcp" owner="${vyos_conf_scripts_dir}/host_name.py">
+ <properties>
+ <help>Interfaces whose DHCP client nameservers will be used by the system (resolv.conf)</help>
+ <priority>400</priority>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="host-name" owner="${vyos_conf_scripts_dir}/host_name.py">
+ <properties>
+ <help>System host name (default: vyos)</help>
+ <constraint>
+ <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="domain-name" owner="${vyos_conf_scripts_dir}/host_name.py">
+ <properties>
+ <help>System domain name</help>
+ <constraint>
+ <regex>[A-Za-z0-9][-.A-Za-z0-9]*</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="domain-search" owner="${vyos_conf_scripts_dir}/host_name.py">
+ <properties>
+ <help>Domain Name Server (DNS) domain completion order</help>
+ <priority>400</priority>
+ </properties>
+ <children>
+ <leafNode name="domain">
+ <properties>
+ <help>DNS domain completion order</help>
+ <constraint>
+ <regex>[-a-zA-Z0-9.]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid domain name</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="static-host-mapping" owner="${vyos_conf_scripts_dir}/host_name.py">
+ <properties>
+ <help>Map host names to addresses</help>
+ <priority>400</priority>
+ </properties>
+ <children>
+ <tagNode name="host-name">
+ <properties>
+ <help>Host name for static address mapping</help>
+ <constraint>
+ <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$</regex>
+ </constraint>
+ <constraintErrorMessage>invalid hostname</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="alias">
+ <properties>
+ <help>Alias for this address</help>
+ <constraint>
+ <regex>.{1,63}$</regex>
+ </constraint>
+ <constraintErrorMessage>invalid alias hostname, needs to be between 1 and 63 charactes</constraintErrorMessage>
+ <multi />
+ </properties>
+ </leafNode>
+ <leafNode name="inet">
+ <properties>
+ <help>IP Address [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in
new file mode 100644
index 000000000..143c04ef6
--- /dev/null
+++ b/interface-definitions/dns-dynamic.xml.in
@@ -0,0 +1,242 @@
+<?xml version="1.0"?>
+<!-- Dynamic DNS configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="dns">
+ <properties>
+ <help>Domain Name System related services</help>
+ </properties>
+ <children>
+ <node name="dynamic" owner="${vyos_conf_scripts_dir}/dynamic_dns.py">
+ <properties>
+ <help>Dynamic DNS</help>
+ <priority>919</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>
+ <tagNode name="rfc2136">
+ <properties>
+ <help>RFC2136 Update name</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>File containing the secret key shared with remote DNS server [REQUIRED]</help>
+ <valueHelp>
+ <format>file</format>
+ <description>File in /config/auth directory</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="record">
+ <properties>
+ <help>Record to be updated [REQUIRED]</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="server">
+ <properties>
+ <help>Server to be updated [REQUIRED]</help>
+ </properties>
+ </leafNode>
+ <leafNode name="ttl">
+ <properties>
+ <help>Time To Live (default: 600)</help>
+ <valueHelp>
+ <format>1-86400</format>
+ <description>DNS forwarding cache size</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-86400"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="zone">
+ <properties>
+ <help>Zone to be updated [REQUIRED]</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="service">
+ <properties>
+ <help>Service being used for Dynamic DNS [REQUIRED]</help>
+ <completionHelp>
+ <list>&lt;custom&gt; afraid changeip cloudflare dnspark dslreports dyndns easydns namecheap noip sitelutions zoneedit</list>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;custom&gt;</format>
+ <description>Service with a custom name</description>
+ </valueHelp>
+ <valueHelp>
+ <format>afraid</format>
+ <description>afraid.org Services</description>
+ </valueHelp>
+ <valueHelp>
+ <format>changeip</format>
+ <description>changeip.com Services</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cloudflare</format>
+ <description>cloudflare.com Services</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dnspark</format>
+ <description>dnspark.com Services</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dslreports</format>
+ <description>dslreports.com Services</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dyndns</format>
+ <description>dyndns.com Services</description>
+ </valueHelp>
+ <valueHelp>
+ <format>easydns</format>
+ <description>easydns.com Services</description>
+ </valueHelp>
+ <valueHelp>
+ <format>namecheap</format>
+ <description>namecheap.com Services</description>
+ </valueHelp>
+ <valueHelp>
+ <format>noip</format>
+ <description>noip.com Services</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sitelutions</format>
+ <description>sitelutions.com Services</description>
+ </valueHelp>
+ <valueHelp>
+ <format>zoneedit</format>
+ <description>zoneedit.com Services</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(custom|afraid|changeip|cloudflare|dnspark|dslreports|dyndns|easydns|namecheap|noip|sitelutions|zoneedit|\w+)$</regex>
+ </constraint>
+ <constraintErrorMessage>You can use only predefined list of services or word characters (_, a-z, A-Z, 0-9) as service name</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="host-name">
+ <properties>
+ <help>Hostname registered with DDNS service [REQUIRED]</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="login">
+ <properties>
+ <help>Login for DDNS service [REQUIRED]</help>
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>Password for DDNS service [REQUIRED]</help>
+ </properties>
+ </leafNode>
+ <leafNode name="protocol">
+ <properties>
+ <help>ddclient protocol used for DDNS service [REQUIRED FOR CUSTOM]</help>
+ <completionHelp>
+ <list>changeip cloudflare dnspark dslreports1 dyndns2 easydns namecheap noip sitelutions zoneedit1</list>
+ </completionHelp>
+ <valueHelp>
+ <format>changeip</format>
+ <description>changeip protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cloudflare</format>
+ <description>cloudflare protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dnspark</format>
+ <description>dnspark protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dslreports1</format>
+ <description>dslreports1 protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dyndns2</format>
+ <description>dyndns2 protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>easydns</format>
+ <description>easydns protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>namecheap</format>
+ <description>namecheap protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>noip</format>
+ <description>noip protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sitelutions</format>
+ <description>sitelutions protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>zoneedit1</format>
+ <description>zoneedit1 protocol</description>
+ </valueHelp>
+ <constraint>
+ <regex>(changeip|cloudflare|dnspark|dslreports1|dyndns2|easydns|namecheap|noip|sitelutions|zoneedit1)</regex>
+ </constraint>
+ <constraintErrorMessage>Please choose from the list of allowed protocols</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="server">
+ <properties>
+ <help>Server to send DDNS update to [REQUIRED FOR CUSTOM]</help>
+ <valueHelp>
+ <format>IPv4</format>
+ <description>IP address of DDNS server</description>
+ </valueHelp>
+ <valueHelp>
+ <format>FQDN</format>
+ <description>Hostname of DDNS server</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="zone">
+ <properties>
+ <help>DNS zone to update (only available with CloudFlare)</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="use-web">
+ <properties>
+ <help>Web check used for obtaining the external IP address</help>
+ </properties>
+ <children>
+ <leafNode name="skip">
+ <properties>
+ <help>Skip everything before this on the given URL</help>
+ </properties>
+ </leafNode>
+ <leafNode name="url">
+ <properties>
+ <help>URL to obtain the current external IP address</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in
new file mode 100644
index 000000000..aaf8bb27d
--- /dev/null
+++ b/interface-definitions/dns-forwarding.xml.in
@@ -0,0 +1,189 @@
+<?xml version="1.0"?>
+<!-- DNS forwarder configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="dns">
+ <properties>
+ <help>Domain Name System related services</help>
+ </properties>
+ <children>
+ <node name="forwarding" owner="${vyos_conf_scripts_dir}/dns_forwarding.py">
+ <properties>
+ <help>DNS forwarding</help>
+ <priority>918</priority>
+ </properties>
+ <children>
+ <leafNode name="cache-size">
+ <properties>
+ <help>DNS forwarding cache size</help>
+ <valueHelp>
+ <format>0-10000</format>
+ <description>DNS forwarding cache size</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-10000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="dhcp">
+ <properties>
+ <help>Interfaces whose DHCP client nameservers to forward requests to</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="dnssec">
+ <properties>
+ <help>DNSSEC mode</help>
+ <completionHelp>
+ <list>off process-no-validate process log-fail validate</list>
+ </completionHelp>
+ <valueHelp>
+ <format>off</format>
+ <description>No DNSSEC processing whatsoever!</description>
+ </valueHelp>
+ <valueHelp>
+ <format>process-no-validate</format>
+ <description>Respond with DNSSEC records to clients that ask for it. No validation done at all!</description>
+ </valueHelp>
+ <valueHelp>
+ <format>process</format>
+ <description>Respond with DNSSEC records to clients that ask for it. Validation for clients that request it.</description>
+ </valueHelp>
+ <valueHelp>
+ <format>log-fail</format>
+ <description>Similar behaviour to process, but validate RRSIGs on responses and log bogus responses.</description>
+ </valueHelp>
+ <valueHelp>
+ <format>validate</format>
+ <description>Full blown DNSSEC validation. Send SERVFAIL to clients on bogus responses.</description>
+ </valueHelp>
+ <constraint>
+ <regex>(off|process-no-validate|process|log-fail|validate)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="domain">
+ <properties>
+ <help>Domain to forward to a custom DNS server</help>
+ </properties>
+ <children>
+ <leafNode name="server">
+ <properties>
+ <help>Domain Name Server (DNS) to forward queries to</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Domain Name Server (DNS) IPv6 address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="addnta">
+ <properties>
+ <help>Add NTA (negative trust anchor) for this domain (must be set if the domain does not support DNSSEC)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="recursion-desired">
+ <properties>
+ <help>Set the "recursion desired" bit in requests to the upstream nameserver</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="ignore-hosts-file">
+ <properties>
+ <help>Do not use local /etc/hosts file in name resolution</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="allow-from">
+ <properties>
+ <help>Networks allowed to query this server</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IP address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="listen-address">
+ <properties>
+ <help>Addresses to listen for DNS queries [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Domain Name Server (DNS) IPv6 address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="negative-ttl">
+ <properties>
+ <help>Maximum amount of time negative entries are cached</help>
+ <valueHelp>
+ <format>0-7200</format>
+ <description>Seconds to cache NXDOMAIN entries</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-7200"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="name-server">
+ <properties>
+ <help>Domain Name Servers (DNS) addresses [OPTIONAL]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Domain Name Server (DNS) IPv6 address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="system">
+ <properties>
+ <help>Use system name servers</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/firewall-options.xml.in b/interface-definitions/firewall-options.xml.in
new file mode 100644
index 000000000..defd44f06
--- /dev/null
+++ b/interface-definitions/firewall-options.xml.in
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="firewall">
+ <children>
+ <node name="options">
+ <properties>
+ <help>Firewall options/Packet manipulation</help>
+ <priority>990</priority>
+ </properties>
+ <children>
+ <tagNode name="interface" owner="${vyos_conf_scripts_dir}/firewall_options.py">
+ <properties>
+ <help>Interface clamping options</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable this rule</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="adjust-mss">
+ <properties>
+ <help>Adjust MSS for IPv4 transit packets</help>
+ <valueHelp>
+ <format>500-1460</format>
+ <description>TCP Maximum segment size in bytes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 500-1460"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="adjust-mss6">
+ <properties>
+ <help>Adjust MSS for IPv6 transit packets</help>
+ <valueHelp>
+ <format>1280-1492</format>
+ <description>TCP Maximum segment size in bytes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1280-1492"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/flow-accounting-conf.xml.in b/interface-definitions/flow-accounting-conf.xml.in
new file mode 100644
index 000000000..239269235
--- /dev/null
+++ b/interface-definitions/flow-accounting-conf.xml.in
@@ -0,0 +1,431 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- flow-accounting configuration -->
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="flow-accounting" owner="${vyos_conf_scripts_dir}/flow_accounting_conf.py">
+ <properties>
+ <help>Flow accounting settings</help>
+ <priority>990</priority>
+ </properties>
+ <children>
+ <leafNode name="buffer-size">
+ <properties>
+ <help>Buffer size</help>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>Buffer size in MiB</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-imt">
+ <properties>
+ <help>Disable in memory table plugin</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ <leafNode name="syslog-facility">
+ <properties>
+ <help>Syslog facility for flow-accounting</help>
+ <completionHelp>
+ <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list>
+ </completionHelp>
+ <constraint>
+ <regex>auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all</regex>
+ </constraint>
+ <valueHelp>
+ <format>auth</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>authpriv</format>
+ <description>Non-system authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cron</format>
+ <description>Cron daemon</description>
+ </valueHelp>
+ <valueHelp>
+ <format>daemon</format>
+ <description>System daemons</description>
+ </valueHelp>
+ <valueHelp>
+ <format>kern</format>
+ <description>Kernel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>lpr</format>
+ <description>Line printer spooler</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mail</format>
+ <description>Mail subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mark</format>
+ <description>Timestamp</description>
+ </valueHelp>
+ <valueHelp>
+ <format>news</format>
+ <description>USENET subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>protocols</format>
+ <description>Routing protocols (local7)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>security</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>syslog</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>user</format>
+ <description>Application processes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>uucp</format>
+ <description>UUCP subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local0</format>
+ <description>Local facility 0</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local1</format>
+ <description>Local facility 1</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local2</format>
+ <description>Local facility 2</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local3</format>
+ <description>Local facility 3</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local4</format>
+ <description>Local facility 4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local5</format>
+ <description>Local facility 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local6</format>
+ <description>Local facility 6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local7</format>
+ <description>Local facility 7</description>
+ </valueHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface for flow-accounting [REQUIRED]</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="netflow">
+ <properties>
+ <help>NetFlow settings</help>
+ </properties>
+ <children>
+ <leafNode name="engine-id">
+ <properties>
+ <help>NetFlow engine-id</help>
+ <valueHelp>
+ <format>0-255 or 0-255:0-255</format>
+ <description>NetFlow engine-id for v5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>NetFlow engine-id for v9 / IPFIX</description>
+ </valueHelp>
+ <constraint>
+ <regex>(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$|^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="max-flows">
+ <properties>
+ <help>NetFlow maximum flows</help>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>NetFlow maximum flows</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="sampling-rate">
+ <properties>
+ <help>NetFlow sampling-rate</help>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>Sampling rate (1 in N packets)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="source-ip">
+ <properties>
+ <help>IPv4 or IPv6 source address of NetFlow packets</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 source address of NetFlow packets</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 source address of NetFlow packets</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="version">
+ <properties>
+ <help>NetFlow version to export</help>
+ <completionHelp>
+ <list>5 9 10</list>
+ </completionHelp>
+ <valueHelp>
+ <format>5</format>
+ <description>NetFlow version 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>9</format>
+ <description>NetFlow version 9 (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>10</format>
+ <description>Internet Protocol Flow Information Export (IPFIX)</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <tagNode name="server">
+ <properties>
+ <help>Server to export NetFlow [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 server to export NetFlow</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 server to export NetFlow</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="port">
+ <properties>
+ <help>NetFlow port number</help>
+ <valueHelp>
+ <format>1025-65535</format>
+ <description>NetFlow port number (default 2055)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1025-65535" />
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="timeout">
+ <properties>
+ <help>NetFlow timeout values</help>
+ </properties>
+ <children>
+ <leafNode name="expiry-interval">
+ <properties>
+ <help>Expiry scan interval</help>
+ <valueHelp>
+ <format>0-2147483647</format>
+ <description>Expiry scan interval (default 60)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-2147483647" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="flow-generic">
+ <properties>
+ <help>Generic flow timeout value</help>
+ <valueHelp>
+ <format>0-2147483647</format>
+ <description>Generic flow timeout in seconds (default 3600)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-2147483647" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="icmp">
+ <properties>
+ <help>ICMP timeout value</help>
+ <valueHelp>
+ <format>0-2147483647</format>
+ <description>ICMP timeout in seconds (default 300)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-2147483647" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="max-active-life">
+ <properties>
+ <help>Max active timeout value</help>
+ <valueHelp>
+ <format>0-2147483647</format>
+ <description>Max active timeout in seconds (default 604800)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-2147483647" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="tcp-fin">
+ <properties>
+ <help>TCP finish timeout value</help>
+ <valueHelp>
+ <format>0-2147483647</format>
+ <description>TCP FIN timeout in seconds (default 300)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-2147483647" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="tcp-generic">
+ <properties>
+ <help>TCP generic timeout value</help>
+ <valueHelp>
+ <format>0-2147483647</format>
+ <description>TCP generic timeout in seconds (default 3600)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-2147483647" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="tcp-rst">
+ <properties>
+ <help>TCP reset timeout value</help>
+ <valueHelp>
+ <format>0-2147483647</format>
+ <description>TCP RST timeout in seconds (default 120)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-2147483647" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="udp">
+ <properties>
+ <help>UDP timeout value</help>
+ <valueHelp>
+ <format>0-2147483647</format>
+ <description>UDP timeout in seconds (default 300)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-2147483647" />
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="sflow">
+ <properties>
+ <help>sFlow settings</help>
+ </properties>
+ <children>
+ <leafNode name="agent-address">
+ <properties>
+ <help>sFlow agent IPv4 address</help>
+ <valueHelp>
+ <format>auto</format>
+ <description>auto select sFlow agent-address (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>sFlow IPv4 agent address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <regex>auto$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="sampling-rate">
+ <properties>
+ <help>sFlow sampling-rate</help>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>Sampling rate (1 in N packets)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="server">
+ <properties>
+ <help>Server to export sFlow [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 server to export sFlow</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 server to export sFlow</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="port">
+ <properties>
+ <help>sFlow port number</help>
+ <valueHelp>
+ <format>1025-65535</format>
+ <description>sFlow port number (default 6343)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1025-65535" />
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in
new file mode 100644
index 000000000..9bb96f1f0
--- /dev/null
+++ b/interface-definitions/https.xml.in
@@ -0,0 +1,174 @@
+<?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">
+ <properties>
+ <help>HTTPS configuration</help>
+ <priority>1001</priority>
+ </properties>
+ <children>
+ <tagNode name="virtual-host">
+ <properties>
+ <help>Identifier for virtual host</help>
+ <constraint>
+ <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>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="server-name">
+ <properties>
+ <help>Server names: exact, wildcard, or regex</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="api" owner="${vyos_conf_scripts_dir}/http-api.py">
+ <properties>
+ <help>VyOS HTTP API configuration</help>
+ <priority>1002</priority>
+ </properties>
+ <children>
+ <leafNode name="port">
+ <properties>
+ <help>Port for HTTP API service</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="keys">
+ <properties>
+ <help>HTTP API keys</help>
+ </properties>
+ <children>
+ <tagNode name="id">
+ <properties>
+ <help>HTTP API id</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>HTTP API plaintext key</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <leafNode name="strict">
+ <properties>
+ <help>Enforce strict path checking</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="debug">
+ <properties>
+ <help>Debug</help>
+ <valueless/>
+ <hidden/>
+ </properties>
+ </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>
+ </properties>
+ <children>
+ <node name="system-generated-certificate" owner="${vyos_conf_scripts_dir}/vyos_cert.py">
+ <properties>
+ <help>Use an automatically generated self-signed certificate</help>
+ </properties>
+ <children>
+ <leafNode name="lifetime">
+ <properties>
+ <help>Lifetime in days; default is 365</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Number of days</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="certbot" owner="${vyos_conf_scripts_dir}/le_cert.py">
+ <properties>
+ <help>Request or apply a letsencrypt certificate for domain-name</help>
+ </properties>
+ <children>
+ <leafNode name="domain-name">
+ <properties>
+ <help>Domain name(s) for which to obtain certificate</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="email">
+ <properties>
+ <help>Email address to associate with certificate</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/igmp-proxy.xml.in b/interface-definitions/igmp-proxy.xml.in
new file mode 100644
index 000000000..74fec6b48
--- /dev/null
+++ b/interface-definitions/igmp-proxy.xml.in
@@ -0,0 +1,100 @@
+<?xml version="1.0"?>
+<!-- IGMP Proxy configuration -->
+<interfaceDefinition>
+ <node name="protocols">
+ <children>
+ <node name="igmp-proxy" owner="${vyos_conf_scripts_dir}/igmp_proxy.py">
+ <properties>
+ <help>Internet Group Management Protocol (IGMP) proxy parameters</help>
+ <priority>740</priority>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable IGMP proxy</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-quickleave">
+ <properties>
+ <help>Option to disable "quickleave"</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Interface for IGMP proxy [REQUIRED]</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="alt-subnet">
+ <properties>
+ <help>Unicast source networks allowed for multicast traffic to be proxyed</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="role">
+ <properties>
+ <help>Role of this IGMP interface</help>
+ <completionHelp>
+ <list>upstream downstream disabled</list>
+ </completionHelp>
+ <valueHelp>
+ <format>upstream</format>
+ <description>Upstream interface (only 1 allowed)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>downstream</format>
+ <description>Downstream interface(s) (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disabled</format>
+ <description>Disabled interface</description>
+ </valueHelp>
+ <constraint>
+ <regex>(upstream|downstream|disabled)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="threshold">
+ <properties>
+ <help>TTL threshold</help>
+ <valueHelp>
+ <format>1-255</format>
+ <description>TTL threshold for the interfaces (default: 1)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ <constraintErrorMessage>threshold must be between 1 and 255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="whitelist">
+ <properties>
+ <help>Group to whitelist</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/include/accel-auth-mode.xml.i b/interface-definitions/include/accel-auth-mode.xml.i
new file mode 100644
index 000000000..e719112db
--- /dev/null
+++ b/interface-definitions/include/accel-auth-mode.xml.i
@@ -0,0 +1,19 @@
+<leafNode name="mode">
+ <properties>
+ <help>Authentication mode used by this server</help>
+ <valueHelp>
+ <format>local</format>
+ <description>Use local username/password configuration</description>
+ </valueHelp>
+ <valueHelp>
+ <format>radius</format>
+ <description>Use RADIUS server for user autentication</description>
+ </valueHelp>
+ <constraint>
+ <regex>(local|radius)</regex>
+ </constraint>
+ <completionHelp>
+ <list>local radius</list>
+ </completionHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/accel-client-ipv6-pool.xml.in b/interface-definitions/include/accel-client-ipv6-pool.xml.in
new file mode 100644
index 000000000..455ada6ef
--- /dev/null
+++ b/interface-definitions/include/accel-client-ipv6-pool.xml.in
@@ -0,0 +1,59 @@
+<node name="client-ipv6-pool">
+ <properties>
+ <help>Pool of client IPv6 addresses</help>
+ </properties>
+ <children>
+ <tagNode name="prefix">
+ <properties>
+ <help>Pool of addresses used to assign to clients</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="mask">
+ <properties>
+ <help>Prefix length used for individual client</help>
+ <valueHelp>
+ <format>&lt;48-128&gt;</format>
+ <description>Client prefix length (default: 64)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 48-128"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="delegate">
+ <properties>
+ <help>Subnet used to delegate prefix through DHCPv6-PD (RFC3633)</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="delegation-prefix">
+ <properties>
+ <help>Prefix length delegated to client</help>
+ <valueHelp>
+ <format>&lt;32-64&gt;</format>
+ <description>Delegated prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 32-64"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/accel-name-server.xml.in b/interface-definitions/include/accel-name-server.xml.in
new file mode 100644
index 000000000..82ed6771d
--- /dev/null
+++ b/interface-definitions/include/accel-name-server.xml.in
@@ -0,0 +1,18 @@
+<leafNode name="name-server">
+ <properties>
+ <help>Domain Name Server (DNS) propagated to client</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Domain Name Server (DNS) IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/accel-radius-additions.xml.in b/interface-definitions/include/accel-radius-additions.xml.in
new file mode 100644
index 000000000..e37b68514
--- /dev/null
+++ b/interface-definitions/include/accel-radius-additions.xml.in
@@ -0,0 +1,125 @@
+<node name="radius">
+ <children>
+ <tagNode name="server">
+ <children>
+ <leafNode name="acct-port">
+ <properties>
+ <help>Accounting port</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port (default: 1813)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <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 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 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>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>NAS-Identifier attribute sent to RADIUS</help>
+ </properties>
+ </leafNode>
+ <leafNode name="nas-ip-address">
+ <properties>
+ <help>NAS-IP-Address attribute sent to RADIUS</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>NAS-IP-Address attribute</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <node name="dynamic-author">
+ <properties>
+ <help>Dynamic Authorization Extension/Change of Authorization server</help>
+ </properties>
+ <children>
+ <leafNode name="server">
+ <properties>
+ <help>IP address for Dynamic Authorization Extension server (DM/CoA)</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address for aynamic authorization server</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Port for Dynamic Authorization Extension server (DM/CoA)</help>
+ <valueHelp>
+ <format>number</format>
+ <description>TCP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="key">
+ <properties>
+ <help>Shared secret for Dynamic Authorization Extension server</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+</node>
diff --git a/interface-definitions/include/accel-wins-server.xml.i b/interface-definitions/include/accel-wins-server.xml.i
new file mode 100644
index 000000000..461a65ddf
--- /dev/null
+++ b/interface-definitions/include/accel-wins-server.xml.i
@@ -0,0 +1,13 @@
+<leafNode name="wins-server">
+ <properties>
+ <help>Windows Internet Name Service (WINS) servers propagated to client</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/address-ipv4-ipv6-dhcp.xml.i b/interface-definitions/include/address-ipv4-ipv6-dhcp.xml.i
new file mode 100644
index 000000000..cca824d89
--- /dev/null
+++ b/interface-definitions/include/address-ipv4-ipv6-dhcp.xml.i
@@ -0,0 +1,29 @@
+<leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <completionHelp>
+ <list>dhcp dhcpv6</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dhcp</format>
+ <description>Dynamic Host Configuration Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dhcpv6</format>
+ <description>Dynamic Host Configuration Protocol for IPv6</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-host"/>
+ <regex>(dhcp|dhcpv6)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/address-ipv4-ipv6.xml.i b/interface-definitions/include/address-ipv4-ipv6.xml.i
new file mode 100644
index 000000000..a891085bd
--- /dev/null
+++ b/interface-definitions/include/address-ipv4-ipv6.xml.i
@@ -0,0 +1,17 @@
+<leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-host"/>
+ </constraint>
+ <multi/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/bgp-afi-aggregate-address.xml.i b/interface-definitions/include/bgp-afi-aggregate-address.xml.i
new file mode 100644
index 000000000..050ee0074
--- /dev/null
+++ b/interface-definitions/include/bgp-afi-aggregate-address.xml.i
@@ -0,0 +1,12 @@
+<leafNode name="as-set">
+ <properties>
+ <help>Generate AS-set path information for this aggregate address</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="summary-only">
+ <properties>
+ <help>Announce the aggregate summary network only</help>
+ <valueless/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/bgp-afi-redistribute-metric-route-map.xml.i b/interface-definitions/include/bgp-afi-redistribute-metric-route-map.xml.i
new file mode 100644
index 000000000..9b3f7a008
--- /dev/null
+++ b/interface-definitions/include/bgp-afi-redistribute-metric-route-map.xml.i
@@ -0,0 +1,17 @@
+<leafNode name="metric">
+ <properties>
+ <help>Metric for redistributed routes</help>
+ <valueHelp>
+ <format>&lt;1-4294967295&gt;</format>
+ <description>Metric for redistributed routes</description>
+ </valueHelp>
+ </properties>
+</leafNode>
+<leafNode name="route-map">
+ <properties>
+ <help>Route map to filter redistributed routes</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/bgp-neighbor-afi-ipv4-unicast.xml.i b/interface-definitions/include/bgp-neighbor-afi-ipv4-unicast.xml.i
new file mode 100644
index 000000000..74afb8851
--- /dev/null
+++ b/interface-definitions/include/bgp-neighbor-afi-ipv4-unicast.xml.i
@@ -0,0 +1,285 @@
+<node name="ipv4-unicast">
+ <properties>
+ <help>IPv4 BGP neighbor parameters</help>
+ </properties>
+ <children>
+ <node name="allowas-in">
+ <properties>
+ <help>Accept a IPv4-route that contains the local-AS in the as-path</help>
+ </properties>
+ <children>
+ <leafNode name="number">
+ <properties>
+ <help>Number of occurrences of AS number</help>
+ <valueHelp>
+ <format>&lt;1-10&gt;</format>
+ <description>Number of times AS is allowed in path</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-10"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="as-override">
+ <properties>
+ <help>AS for routes sent to this neighbor to be the local AS</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="attribute-unchanged">
+ <properties>
+ <help>BGP attributes are sent unchanged (IPv4)</help>
+ </properties>
+ <children>
+ <leafNode name="as-path">
+ <properties>
+ <help>Send AS path unchanged (IPv4)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="med">
+ <properties>
+ <help>Send multi-exit discriminator unchanged (IPv4)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="next-hop">
+ <properties>
+ <help>Send nexthop unchanged (IPv4)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="capability">
+ <properties>
+ <help>Advertise capabilities to this neighbor (IPv4)</help>
+ </properties>
+ <children>
+ <node name="orf">
+ <properties>
+ <help>Advertise ORF capability to this neighbor</help>
+ </properties>
+ <children>
+ <node name="prefix-list">
+ <properties>
+ <help>Advertise prefix-list ORF capability to this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="receive">
+ <properties>
+ <help>Capability to receive the ORF</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="send">
+ <properties>
+ <help>Capability to send the ORF</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="default-originate">
+ <properties>
+ <help>Send default IPv4-route to this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="route-map">
+ <properties>
+ <help>IPv4-Route-map to specify criteria of the default</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="distribute-list">
+ <properties>
+ <help>Access-list to filter IPv4-route updates to/from this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>Access-list to filter outgoing IPv4-route updates to this neighbor</help>
+ <completionHelp>
+ <path>policy access-list</path>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Access-list to filter outgoing IPv4-route updates to this neighbor</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>Access-list to filter incoming IPv4-route updates from this neighbor</help>
+ <completionHelp>
+ <path>policy access-list</path>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Access-list to filter incoming IPv4-route updates from this neighbor</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="filter-list">
+ <properties>
+ <help>As-path-list to filter IPv4-route updates to/from this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>As-path-list to filter outgoing IPv4-route updates to this neighbor</help>
+ <completionHelp>
+ <path>policy as-path-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>As-path-list to filter incoming IPv4-route updates from this neighbor</help>
+ <completionHelp>
+ <path>policy as-path-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="maximum-prefix">
+ <properties>
+ <help>Maximum number of IPv4-prefixes to accept from this neighbor</help>
+ <valueHelp>
+ <format>&lt;1-4294967295&gt;</format>
+ <description>Prefix limit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="nexthop-self">
+ <properties>
+ <help>Nexthop for IPv4-routes sent to this neighbor to be the local router</help>
+ </properties>
+ <children>
+ <leafNode name="force">
+ <properties>
+ <help>Set the next hop to self for reflected routes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="prefix-list">
+ <properties>
+ <help>IPv4-Prefix-list to filter route updates to/from this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>IPv4-Prefix-list to filter outgoing route updates to this neighbor</help>
+ <completionHelp>
+ <path>policy prefix-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>IPv4-Prefix-list to filter incoming route updates from this neighbor</help>
+ <completionHelp>
+ <path>policy prefix-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="remove-private-as">
+ <properties>
+ <help>Remove private AS numbers from AS path in outbound IPv4-route updates</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="route-map">
+ <properties>
+ <help>Route-map to filter IPv4-route updates to/from this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>IPv4-Route-map to filter outgoing route updates to this neighbor</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>IPv4-Route-map to filter incoming route updates from this neighbor</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="route-reflector-client">
+ <properties>
+ <help>Neighbor as a IPv4-route reflector client</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="route-server-client">
+ <properties>
+ <help>Neighbor is IPv4-route server client</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="soft-reconfiguration">
+ <properties>
+ <help>Soft reconfiguration for neighbor (IPv4)</help>
+ </properties>
+ <children>
+ <leafNode name="inbound">
+ <properties>
+ <help>Inbound soft reconfiguration for this neighbor [REQUIRED]</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="unsuppress-map">
+ <properties>
+ <help>Route-map to selectively unsuppress suppressed IPv4-routes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="weight">
+ <properties>
+ <help>Default weight for routes from this neighbor</help>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Weight for routes from this neighbor</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/bgp-neighbor-afi-ipv6-unicast.xml.i b/interface-definitions/include/bgp-neighbor-afi-ipv6-unicast.xml.i
new file mode 100644
index 000000000..e95cb6dd8
--- /dev/null
+++ b/interface-definitions/include/bgp-neighbor-afi-ipv6-unicast.xml.i
@@ -0,0 +1,322 @@
+<node name="ipv6-unicast">
+ <properties>
+ <help>IPv6 BGP neighbor parameters</help>
+ </properties>
+ <children>
+ <node name="allowas-in">
+ <properties>
+ <help>Accept a IPv6-route that contains the local-AS in the as-path</help>
+ </properties>
+ <children>
+ <leafNode name="number">
+ <properties>
+ <help>Number of occurrences of AS number</help>
+ <valueHelp>
+ <format>&lt;1-10&gt;</format>
+ <description>Number of times AS is allowed in path</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-10"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="as-override">
+ <properties>
+ <help>AS for routes sent to this neighbor to be the local AS</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="attribute-unchanged">
+ <properties>
+ <help>BGP attributes are sent unchanged</help>
+ </properties>
+ <children>
+ <leafNode name="as-path">
+ <properties>
+ <help>Send AS path unchanged</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="med">
+ <properties>
+ <help>Send multi-exit discriminator unchanged</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="next-hop">
+ <properties>
+ <help>Send nexthop unchanged</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="capability">
+ <properties>
+ <help>Advertise capabilities to this neighbor (IPv6)</help>
+ </properties>
+ <children>
+ <node name="orf">
+ <properties>
+ <help>Advertise ORF capability to this neighbor</help>
+ </properties>
+ <children>
+ <node name="prefix-list">
+ <properties>
+ <help>Advertise prefix-list ORF capability to this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="receive">
+ <properties>
+ <help>Capability to receive the ORF</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="send">
+ <properties>
+ <help>Capability to send the ORF</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="default-originate">
+ <properties>
+ <help>Send default IPv6-route to this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="route-map">
+ <properties>
+ <help>Route-map to specify criteria of the default</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="disable-send-community">
+ <properties>
+ <help>Disable sending community attributes to this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="extended">
+ <properties>
+ <help>Disable sending extended community attributes to this neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="standard">
+ <properties>
+ <help>Disable sending standard community attributes to this neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="distribute-list">
+ <properties>
+ <help>Access-list to filter route updates to/from this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>Access-list to filter outgoing route updates to this neighbor</help>
+ <completionHelp>
+ <path>policy access-list6</path>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Access-list to filter outgoing route updates to this neighbor</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>Access-list to filter incoming route updates from this neighbor</help>
+ <completionHelp>
+ <path>policy access-list6</path>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Access-list to filter incoming route updates from this neighbor</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="filter-list">
+ <properties>
+ <help>As-path-list to filter route updates to/from this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>As-path-list to filter outgoing route updates to this neighbor</help>
+ <completionHelp>
+ <path>policy as-path-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>As-path-list to filter incoming route updates from this neighbor</help>
+ <completionHelp>
+ <path>policy as-path-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="maximum-prefix">
+ <properties>
+ <help>Maximum number of prefixes to accept from this neighbor</help>
+ <valueHelp>
+ <format>&lt;1-4294967295&gt;</format>
+ <description>Prefix limit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="nexthop-local">
+ <properties>
+ <help>Nexthop attributes</help>
+ </properties>
+ <children>
+ <leafNode name="unchanged">
+ <properties>
+ <help>Leave link-local nexthop unchanged for this peer</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="nexthop-self">
+ <properties>
+ <help>Nexthop for IPv6-routes sent to this neighbor to be the local router</help>
+ </properties>
+ <children>
+ <leafNode name="force">
+ <properties>
+ <help>Set the next hop to self for reflected routes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="peer-group">
+ <properties>
+ <help>IPv6 peer group for this peer</help>
+ </properties>
+ </leafNode>
+ <node name="prefix-list">
+ <properties>
+ <help>Prefix-list to filter route updates to/from this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>Prefix-list to filter outgoing route updates to this neighbor</help>
+ <completionHelp>
+ <path>policy prefix-list6</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>Prefix-list to filter incoming route updates from this neighbor</help>
+ <completionHelp>
+ <path>policy prefix-list6</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="remove-private-as">
+ <properties>
+ <help>Remove private AS numbers from AS path in outbound route updates</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="route-map">
+ <properties>
+ <help>Route-map to filter route updates to/from this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>Route-map to filter outgoing route updates to this neighbor</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>Route-map to filter incoming route updates from this neighbor</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="route-reflector-client">
+ <properties>
+ <help>Neighbor as a IPv6-route reflector client</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="route-server-client">
+ <properties>
+ <help>Neighbor is IPv6-route server client</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="soft-reconfiguration">
+ <properties>
+ <help>Soft reconfiguration for neighbor (IPv6)</help>
+ </properties>
+ <children>
+ <leafNode name="inbound">
+ <properties>
+ <help>Inbound soft reconfiguration for this neighbor [REQUIRED]</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="unsuppress-map">
+ <properties>
+ <help>Route-map to selectively unsuppress suppressed IPv6-routes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="weight">
+ <properties>
+ <help>Default weight for routes from this neighbor</help>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Weight for routes from this neighbor</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/bgp-peer-group-afi-ipv4-unicast.xml.i b/interface-definitions/include/bgp-peer-group-afi-ipv4-unicast.xml.i
new file mode 100644
index 000000000..df051ace5
--- /dev/null
+++ b/interface-definitions/include/bgp-peer-group-afi-ipv4-unicast.xml.i
@@ -0,0 +1,301 @@
+<node name="ipv4-unicast">
+ <properties>
+ <help>IPv4 BGP peer group parameters</help>
+ </properties>
+ <children>
+ <node name="allowas-in">
+ <properties>
+ <help>Accept a route that contains the local-AS in the as-path</help>
+ </properties>
+ <children>
+ <leafNode name="number">
+ <properties>
+ <help>Number of occurrences of AS number</help>
+ <valueHelp>
+ <format>&lt;1-10&gt;</format>
+ <description>Number of times AS is allowed in path</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-10"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="attribute-unchanged">
+ <properties>
+ <help>BGP attributes are sent unchanged</help>
+ </properties>
+ <children>
+ <leafNode name="as-path">
+ <properties>
+ <help>Send AS path unchanged</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="med">
+ <properties>
+ <help>Send multi-exit discriminator unchanged</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="next-hop">
+ <properties>
+ <help>Send nexthop unchanged</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="capability">
+ <properties>
+ <help>Advertise capabilities to this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="dynamic">
+ <properties>
+ <help>Advertise dynamic capability to this peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="orf">
+ <properties>
+ <help>Advertise ORF capability to this peer-group</help>
+ </properties>
+ <children>
+ <node name="prefix-list">
+ <properties>
+ <help>Advertise prefix-list ORF capability to this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="receive">
+ <properties>
+ <help>Capability to receive the ORF</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="send">
+ <properties>
+ <help>Capability to send the ORF</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="default-originate">
+ <properties>
+ <help>Send default route to this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="route-map">
+ <properties>
+ <help>Route-map to specify criteria of the default</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="disable-send-community">
+ <properties>
+ <help>Disable sending community attributes to this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="extended">
+ <properties>
+ <help>Disable sending extended community attributes to this peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="standard">
+ <properties>
+ <help>Disable sending standard community attributes to this peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="distribute-list">
+ <properties>
+ <help>Access-list to filter route updates to/from this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>Access-list to filter outgoing route updates to this peer-group</help>
+ <completionHelp>
+ <path>policy access-list</path>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Access-list to filter outgoing route updates to this peer-group</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>Access-list to filter incoming route updates from this peer-group</help>
+ <completionHelp>
+ <path>policy access-list</path>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Access-list to filter incoming route updates from this peer-group</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="filter-list">
+ <properties>
+ <help>As-path-list to filter route updates to/from this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>As-path-list to filter outgoing route updates to this peer-group</help>
+ <completionHelp>
+ <path>policy as-path-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>As-path-list to filter incoming route updates from this peer-group</help>
+ <completionHelp>
+ <path>policy as-path-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="maximum-prefix">
+ <properties>
+ <help>Maximum number of prefixes to accept from this peer-group</help>
+ <valueHelp>
+ <format>&lt;1-4294967295&gt;</format>
+ <description>Prefix limit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="nexthop-self">
+ <properties>
+ <help>Nexthop for routes sent to this peer-group to be the local router</help>
+ </properties>
+ <children>
+ <leafNode name="force">
+ <properties>
+ <help>Set the next hop to self for reflected routes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="prefix-list">
+ <properties>
+ <help>Prefix-list to filter route updates to/from this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>Prefix-list to filter outgoing route updates to this peer-group</help>
+ <completionHelp>
+ <path>policy prefix-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>Prefix-list to filter incoming route updates from this peer-group</help>
+ <completionHelp>
+ <path>policy prefix-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="remove-private-as">
+ <properties>
+ <help>Remove private AS numbers from AS path in outbound route updates</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="route-map">
+ <properties>
+ <help>Route-map to filter route updates to/from this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>Route-map to filter outgoing route updates to this peer-group</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>Route-map to filter incoming route updates from this peer-group</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="route-reflector-client">
+ <properties>
+ <help>Peer-group as a route reflector client</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="route-server-client">
+ <properties>
+ <help>Peer-group as route server client</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="soft-reconfiguration">
+ <properties>
+ <help>Soft reconfiguration for peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="inbound">
+ <properties>
+ <help>Inbound soft reconfiguration for this peer-group [REQUIRED]</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="unsuppress-map">
+ <properties>
+ <help>Route-map to selectively unsuppress suppressed routes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="weight">
+ <properties>
+ <help>Default weight for routes from this peer-group</help>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Weight for routes from this peer-group</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/bgp-peer-group-afi-ipv6-unicast.xml.i b/interface-definitions/include/bgp-peer-group-afi-ipv6-unicast.xml.i
new file mode 100644
index 000000000..a381e02f0
--- /dev/null
+++ b/interface-definitions/include/bgp-peer-group-afi-ipv6-unicast.xml.i
@@ -0,0 +1,317 @@
+<node name="ipv6-unicast">
+ <properties>
+ <help>IPv6 BGP neighbor parameters</help>
+ </properties>
+ <children>
+ <node name="allowas-in">
+ <properties>
+ <help>Accept a IPv6-route that contains the local-AS in the as-path</help>
+ </properties>
+ <children>
+ <leafNode name="number">
+ <properties>
+ <help>Number of occurrences of AS number</help>
+ <valueHelp>
+ <format>&lt;1-10&gt;</format>
+ <description>Number of times AS is allowed in path</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-10"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="attribute-unchanged">
+ <properties>
+ <help>BGP attributes are sent unchanged</help>
+ </properties>
+ <children>
+ <leafNode name="as-path">
+ <properties>
+ <help>Send AS path unchanged</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="med">
+ <properties>
+ <help>Send multi-exit discriminator unchanged</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="next-hop">
+ <properties>
+ <help>Send nexthop unchanged</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="capability">
+ <properties>
+ <help>Advertise capabilities to this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="dynamic">
+ <properties>
+ <help>Advertise dynamic capability to this peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="orf">
+ <properties>
+ <help>Advertise ORF capability to this peer-group</help>
+ </properties>
+ <children>
+ <node name="prefix-list">
+ <properties>
+ <help>Advertise prefix-list ORF capability to this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="receive">
+ <properties>
+ <help>Capability to receive the ORF</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="send">
+ <properties>
+ <help>Capability to send the ORF</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="default-originate">
+ <properties>
+ <help>Send default route to this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="route-map">
+ <properties>
+ <help>Route-map to specify criteria of the default</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="disable-send-community">
+ <properties>
+ <help>Disable sending community attributes to this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="extended">
+ <properties>
+ <help>Disable sending extended community attributes to this peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="standard">
+ <properties>
+ <help>Disable sending standard community attributes to this peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="distribute-list">
+ <properties>
+ <help>Access-list to filter route updates to/from this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>Access-list to filter outgoing route updates to this peer-group</help>
+ <completionHelp>
+ <path>policy access-list6</path>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Access-list to filter outgoing route updates to this peer-group</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>Access-list to filter incoming route updates from this peer-group</help>
+ <completionHelp>
+ <path>policy access-list6</path>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Access-list to filter incoming route updates from this peer-group</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="filter-list">
+ <properties>
+ <help>As-path-list to filter route updates to/from this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>As-path-list to filter outgoing route updates to this peer-group</help>
+ <completionHelp>
+ <path>policy as-path-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>As-path-list to filter incoming route updates from this peer-group</help>
+ <completionHelp>
+ <path>policy as-path-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="maximum-prefix">
+ <properties>
+ <help>Maximum number of prefixes to accept from this peer-group</help>
+ <valueHelp>
+ <format>&lt;1-4294967295&gt;</format>
+ <description>Prefix limit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="nexthop-local">
+ <properties>
+ <help>Nexthop attributes</help>
+ </properties>
+ <children>
+ <leafNode name="unchanged">
+ <properties>
+ <help>Leave link-local nexthop unchanged for this peer</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="nexthop-self">
+ <properties>
+ <help>Nexthop for routes sent to this peer-group to be the local router</help>
+ </properties>
+ <children>
+ <leafNode name="force">
+ <properties>
+ <help>Set the next hop to self for reflected routes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="prefix-list">
+ <properties>
+ <help>Prefix-list to filter route updates to/from this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>Prefix-list to filter outgoing route updates to this peer-group</help>
+ <completionHelp>
+ <path>policy prefix-list6</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>Prefix-list to filter incoming route updates from this peer-group</help>
+ <completionHelp>
+ <path>policy prefix-list6</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="remove-private-as">
+ <properties>
+ <help>Remove private AS numbers from AS path in outbound route updates</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="route-map">
+ <properties>
+ <help>Route-map to filter route updates to/from this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="export">
+ <properties>
+ <help>Route-map to filter outgoing route updates to this peer-group</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import">
+ <properties>
+ <help>Route-map to filter incoming route updates from this peer-group</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="route-reflector-client">
+ <properties>
+ <help>Peer-group as a route reflector client</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="route-server-client">
+ <properties>
+ <help>Peer-group as route server client</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="soft-reconfiguration">
+ <properties>
+ <help>Soft reconfiguration for peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="inbound">
+ <properties>
+ <help>Inbound soft reconfiguration for this peer-group [REQUIRED]</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="unsuppress-map">
+ <properties>
+ <help>Route-map to selectively unsuppress suppressed routes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="weight">
+ <properties>
+ <help>Default weight for routes from this peer-group</help>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Weight for routes from this peer-group</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/dhcp-options.xml.i b/interface-definitions/include/dhcp-options.xml.i
new file mode 100644
index 000000000..9989291fc
--- /dev/null
+++ b/interface-definitions/include/dhcp-options.xml.i
@@ -0,0 +1,22 @@
+<node name="dhcp-options">
+ <properties>
+ <help>DHCP client settings/options</help>
+ </properties>
+ <children>
+ <leafNode name="client-id">
+ <properties>
+ <help>DHCP client identifier</help>
+ </properties>
+ </leafNode>
+ <leafNode name="host-name">
+ <properties>
+ <help>DHCP client host name (overrides system host name)</help>
+ </properties>
+ </leafNode>
+ <leafNode name="vendor-class-id">
+ <properties>
+ <help>DHCP client vendor type</help>
+ </properties>
+ </leafNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/dhcpv6-options.xml.i b/interface-definitions/include/dhcpv6-options.xml.i
new file mode 100644
index 000000000..b0a806806
--- /dev/null
+++ b/interface-definitions/include/dhcpv6-options.xml.i
@@ -0,0 +1,86 @@
+<node name="dhcpv6-options">
+ <properties>
+ <help>DHCPv6 client settings/options</help>
+ </properties>
+ <children>
+ <leafNode name="parameters-only">
+ <properties>
+ <help>Acquire only config parameters, no address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <tagNode name="pd">
+ <properties>
+ <help>DHCPv6 prefix delegation interface statement</help>
+ <valueHelp>
+ <format>instance number</format>
+ <description>Prefix delegation instance (>= 0)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--non-negative"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="length">
+ <properties>
+ <help>Request IPv6 prefix length from peer</help>
+ <valueHelp>
+ <format>32-64</format>
+ <description>Length of delegated prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 32-64"/>
+ </constraint>
+ </properties>
+ <defaultValue>64</defaultValue>
+ </leafNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Delegate IPv6 prefix from provider to this interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>Local interface address assigned to interface</help>
+ <valueHelp>
+ <format>&gt;0</format>
+ <description>Used to form IPv6 interface address (default: EUI-64)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--non-negative"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="sla-id">
+ <properties>
+ <help>Interface site-Level aggregator (SLA)</help>
+ <valueHelp>
+ <format>0-128</format>
+ <description>Decimal integer which fits in the length of SLA IDs</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-128"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <leafNode name="rapid-commit">
+ <properties>
+ <help>Wait for immediate reply instead of advertisements</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="temporary">
+ <properties>
+ <help>IPv6 temporary address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/interface-arp-cache-timeout.xml.i b/interface-definitions/include/interface-arp-cache-timeout.xml.i
new file mode 100644
index 000000000..e65321158
--- /dev/null
+++ b/interface-definitions/include/interface-arp-cache-timeout.xml.i
@@ -0,0 +1,14 @@
+<leafNode name="arp-cache-timeout">
+ <properties>
+ <help>ARP cache entry timeout in seconds</help>
+ <valueHelp>
+ <format>1-86400</format>
+ <description>ARP cache entry timout in seconds (default 30)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-86400"/>
+ </constraint>
+ <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage>
+ </properties>
+ <defaultValue>30</defaultValue>
+</leafNode>
diff --git a/interface-definitions/include/interface-description.xml.i b/interface-definitions/include/interface-description.xml.i
new file mode 100644
index 000000000..961533e26
--- /dev/null
+++ b/interface-definitions/include/interface-description.xml.i
@@ -0,0 +1,9 @@
+<leafNode name="description">
+ <properties>
+ <help>Interface specific description</help>
+ <constraint>
+ <regex>.{1,256}$</regex>
+ </constraint>
+ <constraintErrorMessage>Description too long (limit 256 characters)</constraintErrorMessage>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-disable-arp-filter.xml.i b/interface-definitions/include/interface-disable-arp-filter.xml.i
new file mode 100644
index 000000000..ec3f51b2d
--- /dev/null
+++ b/interface-definitions/include/interface-disable-arp-filter.xml.i
@@ -0,0 +1,6 @@
+<leafNode name="disable-arp-filter">
+ <properties>
+ <help>Disable ARP filter on this interface</help>
+ <valueless/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-disable-link-detect.xml.i b/interface-definitions/include/interface-disable-link-detect.xml.i
new file mode 100644
index 000000000..619cd03b0
--- /dev/null
+++ b/interface-definitions/include/interface-disable-link-detect.xml.i
@@ -0,0 +1,6 @@
+<leafNode name="disable-link-detect">
+ <properties>
+ <help>Ignore link state changes</help>
+ <valueless/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-disable.xml.i b/interface-definitions/include/interface-disable.xml.i
new file mode 100644
index 000000000..7bd3df5da
--- /dev/null
+++ b/interface-definitions/include/interface-disable.xml.i
@@ -0,0 +1,6 @@
+<leafNode name="disable">
+ <properties>
+ <help>Administratively disable interface</help>
+ <valueless/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-enable-arp-accept.xml.i b/interface-definitions/include/interface-enable-arp-accept.xml.i
new file mode 100644
index 000000000..69f26b322
--- /dev/null
+++ b/interface-definitions/include/interface-enable-arp-accept.xml.i
@@ -0,0 +1,6 @@
+<leafNode name="enable-arp-accept">
+ <properties>
+ <help>Enable ARP accept on this interface</help>
+ <valueless/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-enable-arp-announce.xml.i b/interface-definitions/include/interface-enable-arp-announce.xml.i
new file mode 100644
index 000000000..8d51874c1
--- /dev/null
+++ b/interface-definitions/include/interface-enable-arp-announce.xml.i
@@ -0,0 +1,6 @@
+<leafNode name="enable-arp-announce">
+ <properties>
+ <help>Enable ARP announce on this interface</help>
+ <valueless/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-enable-arp-ignore.xml.i b/interface-definitions/include/interface-enable-arp-ignore.xml.i
new file mode 100644
index 000000000..9adc0f17e
--- /dev/null
+++ b/interface-definitions/include/interface-enable-arp-ignore.xml.i
@@ -0,0 +1,6 @@
+<leafNode name="enable-arp-ignore">
+ <properties>
+ <help>Enable ARP ignore on this interface</help>
+ <valueless/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-enable-proxy-arp.xml.i b/interface-definitions/include/interface-enable-proxy-arp.xml.i
new file mode 100644
index 000000000..14ab08875
--- /dev/null
+++ b/interface-definitions/include/interface-enable-proxy-arp.xml.i
@@ -0,0 +1,6 @@
+<leafNode name="enable-proxy-arp">
+ <properties>
+ <help>Enable proxy-arp on this interface</help>
+ <valueless/>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-hw-id.xml.i b/interface-definitions/include/interface-hw-id.xml.i
new file mode 100644
index 000000000..318ddd1c4
--- /dev/null
+++ b/interface-definitions/include/interface-hw-id.xml.i
@@ -0,0 +1,12 @@
+<leafNode name="hw-id">
+ <properties>
+ <help>Associate Ethernet Interface with given Media Access Control (MAC) address</help>
+ <valueHelp>
+ <format>h:h:h:h:h:h</format>
+ <description>Hardware Media Access Control (MAC) address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-ipv4.xml.i b/interface-definitions/include/interface-ipv4.xml.i
new file mode 100644
index 000000000..15932a9d3
--- /dev/null
+++ b/interface-definitions/include/interface-ipv4.xml.i
@@ -0,0 +1,11 @@
+<node name="ip">
+ <properties>
+ <help>IPv4 routing parameters</help>
+ </properties>
+ <children>
+ #include <include/interface-disable-arp-filter.xml.i>
+ #include <include/interface-enable-arp-accept.xml.i>
+ #include <include/interface-enable-arp-announce.xml.i>
+ #include <include/interface-enable-arp-ignore.xml.i>
+ </children>
+</node>
diff --git a/interface-definitions/include/interface-ipv6.xml.i b/interface-definitions/include/interface-ipv6.xml.i
new file mode 100644
index 000000000..23362f75a
--- /dev/null
+++ b/interface-definitions/include/interface-ipv6.xml.i
@@ -0,0 +1,10 @@
+<node name="ipv6">
+ <properties>
+ <help>IPv6 routing parameters</help>
+ </properties>
+ <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>
diff --git a/interface-definitions/include/interface-mac.xml.i b/interface-definitions/include/interface-mac.xml.i
new file mode 100644
index 000000000..7b2456236
--- /dev/null
+++ b/interface-definitions/include/interface-mac.xml.i
@@ -0,0 +1,12 @@
+<leafNode name="mac">
+ <properties>
+ <help>Media Access Control (MAC) address</help>
+ <valueHelp>
+ <format>h:h:h:h:h:h</format>
+ <description>Hardware (MAC) address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/interface-mtu-1200-9000.xml.i b/interface-definitions/include/interface-mtu-1200-9000.xml.i
new file mode 100644
index 000000000..de48db65e
--- /dev/null
+++ b/interface-definitions/include/interface-mtu-1200-9000.xml.i
@@ -0,0 +1,14 @@
+<leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>1200-9000</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1200-9000"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 1200 and 9000</constraintErrorMessage>
+ </properties>
+ <defaultValue>1500</defaultValue>
+</leafNode>
diff --git a/interface-definitions/include/interface-mtu-1450-9000.xml.i b/interface-definitions/include/interface-mtu-1450-9000.xml.i
new file mode 100644
index 000000000..d15987394
--- /dev/null
+++ b/interface-definitions/include/interface-mtu-1450-9000.xml.i
@@ -0,0 +1,14 @@
+<leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>1450-9000</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1450-9000"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 1450 and 9000</constraintErrorMessage>
+ </properties>
+ <defaultValue>1500</defaultValue>
+</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..e60867e35
--- /dev/null
+++ b/interface-definitions/include/interface-mtu-64-8024.xml.i
@@ -0,0 +1,14 @@
+<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>
+ <defaultValue>1500</defaultValue>
+</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..d47efd2c9
--- /dev/null
+++ b/interface-definitions/include/interface-mtu-68-1500.xml.i
@@ -0,0 +1,14 @@
+<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>
+ <defaultValue>1500</defaultValue>
+</leafNode>
diff --git a/interface-definitions/include/interface-mtu-68-9000.xml.i b/interface-definitions/include/interface-mtu-68-9000.xml.i
new file mode 100644
index 000000000..8fae2043c
--- /dev/null
+++ b/interface-definitions/include/interface-mtu-68-9000.xml.i
@@ -0,0 +1,14 @@
+<leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>68-9000</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 68-9000"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 68 and 9000</constraintErrorMessage>
+ </properties>
+ <defaultValue>1500</defaultValue>
+</leafNode>
diff --git a/interface-definitions/include/interface-proxy-arp-pvlan.xml.i b/interface-definitions/include/interface-proxy-arp-pvlan.xml.i
new file mode 100644
index 000000000..7e72b3800
--- /dev/null
+++ b/interface-definitions/include/interface-proxy-arp-pvlan.xml.i
@@ -0,0 +1,6 @@
+<leafNode name="proxy-arp-pvlan">
+ <properties>
+ <help>Enable private VLAN proxy ARP on this interface</help>
+ <valueless/>
+ </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..34f54e4c1
--- /dev/null
+++ b/interface-definitions/include/ipv6-address.xml.i
@@ -0,0 +1,29 @@
+<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>Prefix for IPv6 address with MAC-based EUI-64</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 network and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="no-default-link-local">
+ <properties>
+ <help>Remove the default link-local address from the interface</help>
+ <valueless/>
+ </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/isis-redistribute-ipv4.xml.i b/interface-definitions/include/isis-redistribute-ipv4.xml.i
new file mode 100644
index 000000000..f90900da1
--- /dev/null
+++ b/interface-definitions/include/isis-redistribute-ipv4.xml.i
@@ -0,0 +1,82 @@
+<node name="level-1">
+ <properties>
+ <help>Redistribute into level-1</help>
+ </properties>
+ <children>
+ <leafNode name="metric">
+ <properties>
+ <help>Metric for redistributed routes</help>
+ <valueHelp>
+ <format>&lt;0-16777215&gt;</format>
+ <description>ISIS default metric</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-16777215"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="route-map">
+ <properties>
+ <help>Route map reference</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="metric">
+ <properties>
+ <help>Metric for redistributed routes</help>
+ <valueHelp>
+ <format>&lt;0-16777215&gt;</format>
+ <description>ISIS default metric</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-16777215"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+</node>
+<node name="level-2">
+ <properties>
+ <help>Redistribute into level-2</help>
+ </properties>
+ <children>
+ <leafNode name="metric">
+ <properties>
+ <help>Metric for redistributed routes</help>
+ <valueHelp>
+ <format>&lt;0-16777215&gt;</format>
+ <description>ISIS default metric</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-16777215"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="route-map">
+ <properties>
+ <help>Route map reference</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="metric">
+ <properties>
+ <help>Metric for redistributed routes</help>
+ <valueHelp>
+ <format>&lt;0-16777215&gt;</format>
+ <description>ISIS default metric</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-16777215"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/nat-address.xml.i b/interface-definitions/include/nat-address.xml.i
new file mode 100644
index 000000000..933dae07b
--- /dev/null
+++ b/interface-definitions/include/nat-address.xml.i
@@ -0,0 +1,37 @@
+<leafNode name="address">
+ <properties>
+ <help>IP address, subnet, or range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4range</format>
+ <description>IPv4 address range to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv4</format>
+ <description>Match everything except the specified address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv4net</format>
+ <description>Match everything except the specified prefix</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv4range</format>
+ <description>Match everything except the specified range</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv4-prefix"/>
+ <validator name="ipv4-range"/>
+ <validator name="ipv4-address-exclude"/>
+ <validator name="ipv4-prefix-exclude"/>
+ <validator name="ipv4-range-exclude"/>
+ </constraint>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/nat-interface.xml.i b/interface-definitions/include/nat-interface.xml.i
new file mode 100644
index 000000000..c49483297
--- /dev/null
+++ b/interface-definitions/include/nat-interface.xml.i
@@ -0,0 +1,9 @@
+<leafNode name="outbound-interface">
+ <properties>
+ <help>Outbound interface of NAT traffic</help>
+ <completionHelp>
+ <list>any</list>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/nat-port.xml.i b/interface-definitions/include/nat-port.xml.i
new file mode 100644
index 000000000..24803ae05
--- /dev/null
+++ b/interface-definitions/include/nat-port.xml.i
@@ -0,0 +1,17 @@
+<leafNode name="port">
+ <properties>
+ <help>Port number</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <valueHelp>
+ <format>start-end</format>
+ <description>Numbered port range (e.g., 1001-1005)</description>
+ </valueHelp>
+ <valueHelp>
+ <format> </format>
+ <description>\n\nMultiple destination ports can be specified as a comma-separated list.\nThe whole list can also be negated using '!'.\nFor example: '!22,telnet,http,123,1001-1005'</description>
+ </valueHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i
new file mode 100644
index 000000000..a2d058479
--- /dev/null
+++ b/interface-definitions/include/nat-rule.xml.i
@@ -0,0 +1,303 @@
+<tagNode name="rule">
+ <properties>
+ <help>Rule number for NAT</help>
+ <valueHelp>
+ <format>1-999999</format>
+ <description>Number for this NAT rule</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="description">
+ <properties>
+ <help>Rule description</help>
+ </properties>
+ </leafNode>
+ <node name="destination">
+ <properties>
+ <help>NAT destination parameters</help>
+ </properties>
+ <children>
+ #include <include/nat-address.xml.i>
+ #include <include/nat-port.xml.i>
+ </children>
+ </node>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable NAT rule</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="exclude">
+ <properties>
+ <help>Exclude packets matching this rule from NAT</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="log">
+ <properties>
+ <help>NAT rule logging</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="protocol">
+ <properties>
+ <help>Protocol to NAT</help>
+ <completionHelp>
+ <list>all ip hopopt icmp igmp ggp ipencap st tcp egp igp pup udp tcp_udp hmp xns-idp rdp iso-tp4 dccp xtp ddp idpr-cmtp ipv6 ipv6-route ipv6-frag idrp rsvp gre esp ah skip ipv6-icmp ipv6-nonxt ipv6-opts rspf vmtp eigrp ospf ax.25 ipip etherip encap 99 pim ipcomp vrrp l2tp isis sctp fc mobility-header udplite mpls-in-ip manet hip shim6 wesp rohc</list>
+ </completionHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>All IP protocols</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ip</format>
+ <description>Internet Protocol, pseudo protocol number</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hopopt</format>
+ <description>IPv6 Hop-by-Hop Option [RFC1883]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>icmp</format>
+ <description>internet control message protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>igmp</format>
+ <description>Internet Group Management</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ggp</format>
+ <description>gateway-gateway protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipencap</format>
+ <description>IP encapsulated in IP (officially IP)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>st</format>
+ <description>ST datagram mode</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp</format>
+ <description>transmission control protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>egp</format>
+ <description>exterior gateway protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>igp</format>
+ <description>any private interior gateway (Cisco)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>pup</format>
+ <description>PARC universal packet protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>udp</format>
+ <description>user datagram protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp_udp</format>
+ <description>Both TCP and UDP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hmp</format>
+ <description>host monitoring protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>xns-idp</format>
+ <description>Xerox NS IDP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rdp</format>
+ <description>"reliable datagram" protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>iso-tp4</format>
+ <description>ISO Transport Protocol class 4 [RFC905]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dccp</format>
+ <description>Datagram Congestion Control Prot. [RFC4340]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>xtp</format>
+ <description>Xpress Transfer Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ddp</format>
+ <description>Datagram Delivery Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>idpr-cmtp</format>
+ <description>IDPR Control Message Transport</description>
+ </valueHelp>
+ <valueHelp>
+ <format>Ipv6</format>
+ <description>Internet Protocol, version 6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6-route</format>
+ <description>Routing Header for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6-frag</format>
+ <description>Fragment Header for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>idrp</format>
+ <description>Inter-Domain Routing Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rsvp</format>
+ <description>Reservation Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>gre</format>
+ <description>General Routing Encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>esp</format>
+ <description>Encap Security Payload [RFC2406]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ah</format>
+ <description>Authentication Header [RFC2402]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>skip</format>
+ <description>SKIP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6-icmp</format>
+ <description>ICMP for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6-nonxt</format>
+ <description>No Next Header for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6-opts</format>
+ <description>Destination Options for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rspf</format>
+ <description>Radio Shortest Path First (officially CPHB)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>vmtp</format>
+ <description>Versatile Message Transport</description>
+ </valueHelp>
+ <valueHelp>
+ <format>eigrp</format>
+ <description>Enhanced Interior Routing Protocol (Cisco)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ospf</format>
+ <description>Open Shortest Path First IGP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ax.25</format>
+ <description>AX.25 frames</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipip</format>
+ <description>IP-within-IP Encapsulation Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>etherip</format>
+ <description>Ethernet-within-IP Encapsulation [RFC3378]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>encap</format>
+ <description>Yet Another IP encapsulation [RFC1241]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>99</format>
+ <description>Any private encryption scheme</description>
+ </valueHelp>
+ <valueHelp>
+ <format>pim</format>
+ <description>Protocol Independent Multicast</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipcomp</format>
+ <description>IP Payload Compression Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>vrrp</format>
+ <description>Virtual Router Redundancy Protocol [RFC5798]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>l2tp</format>
+ <description>Layer Two Tunneling Protocol [RFC2661]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>isis</format>
+ <description>IS-IS over IPv4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sctp</format>
+ <description>Stream Control Transmission Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>fc</format>
+ <description>Fibre Channel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mobility-header</format>
+ <description>Mobility Support for IPv6 [RFC3775]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>udplite</format>
+ <description>UDP-Lite [RFC3828]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mpls-in-ip</format>
+ <description>MPLS-in-IP [RFC4023]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>manet</format>
+ <description>MANET Protocols [RFC5498]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hip</format>
+ <description>Host Identity Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>shim6</format>
+ <description>Shim6 Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>wesp</format>
+ <description>Wrapped Encapsulating Security Payload</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rohc</format>
+ <description>Robust Header Compression</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0-255</format>
+ <description>IP protocol number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-protocol"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="source">
+ <properties>
+ <help>NAT source parameters</help>
+ </properties>
+ <children>
+ #include <include/nat-address.xml.i>
+ #include <include/nat-port.xml.i>
+ </children>
+ </node>
+ </children>
+</tagNode>
diff --git a/interface-definitions/include/nat-translation-port.xml.i b/interface-definitions/include/nat-translation-port.xml.i
new file mode 100644
index 000000000..93de471e3
--- /dev/null
+++ b/interface-definitions/include/nat-translation-port.xml.i
@@ -0,0 +1,13 @@
+<leafNode name="port">
+ <properties>
+ <help>Port number</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;start&gt;-&lt;end&gt;</format>
+ <description>Numbered port range (e.g., 1001-1005)</description>
+ </valueHelp>
+ </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..29d2f55fd
--- /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/rip-redistribute.xml.i b/interface-definitions/include/rip-redistribute.xml.i
new file mode 100644
index 000000000..d94dfa5a8
--- /dev/null
+++ b/interface-definitions/include/rip-redistribute.xml.i
@@ -0,0 +1,24 @@
+<leafNode name="metric">
+ <properties>
+ <help>Metric for redistributed routes</help>
+ <valueHelp>
+ <format>&lt;1-16&gt;</format>
+ <description>Redistribute route metric</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-16"/>
+ </constraint>
+ </properties>
+</leafNode>
+<leafNode name="route-map">
+ <properties>
+ <help>Route map reference</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Route map reference</description>
+ </valueHelp>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/source-address-ipv4-ipv6.xml.i b/interface-definitions/include/source-address-ipv4-ipv6.xml.i
new file mode 100644
index 000000000..6d2d77c95
--- /dev/null
+++ b/interface-definitions/include/source-address-ipv4-ipv6.xml.i
@@ -0,0 +1,17 @@
+<leafNode name="source-address">
+ <properties>
+ <help>IPv4/IPv6 source address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 source-address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 source-address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/source-interface-ethernet.xml.i b/interface-definitions/include/source-interface-ethernet.xml.i
new file mode 100644
index 000000000..ad90bc4ac
--- /dev/null
+++ b/interface-definitions/include/source-interface-ethernet.xml.i
@@ -0,0 +1,12 @@
+<leafNode name="source-interface">
+ <properties>
+ <help>Physical interface the traffic will go through</help>
+ <valueHelp>
+ <format>interface</format>
+ <description>Physical interface used for traffic forwarding</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py -t ethernet</script>
+ </completionHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/source-interface.xml.i b/interface-definitions/include/source-interface.xml.i
new file mode 100644
index 000000000..ae579c2a6
--- /dev/null
+++ b/interface-definitions/include/source-interface.xml.i
@@ -0,0 +1,12 @@
+<leafNode name="source-interface">
+ <properties>
+ <help>Physical interface used for connection</help>
+ <valueHelp>
+ <format>interface</format>
+ <description>Physical interface used for connection</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/vif-s.xml.i b/interface-definitions/include/vif-s.xml.i
new file mode 100644
index 000000000..a6d7c81ce
--- /dev/null
+++ b/interface-definitions/include/vif-s.xml.i
@@ -0,0 +1,67 @@
+<tagNode name="vif-s">
+ <properties>
+ <help>QinQ TAG-S Virtual Local Area Network (VLAN) ID</help>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4094"/>
+ </constraint>
+ <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6-dhcp.xml.i>
+ #include <include/interface-description.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
+ #include <include/interface-disable-link-detect.xml.i>
+ #include <include/interface-disable.xml.i>
+ <leafNode name="ethertype">
+ <properties>
+ <help>Set Ethertype</help>
+ <completionHelp>
+ <list>0x88A8 0x8100</list>
+ </completionHelp>
+ <valueHelp>
+ <format>0x88A8</format>
+ <description>802.1ad</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0x8100</format>
+ <description>802.1q</description>
+ </valueHelp>
+ <constraint>
+ <regex>(0x88A8|0x8100)</regex>
+ </constraint>
+ <constraintErrorMessage>Ethertype must be 0x88A8 or 0x8100</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="ip">
+ <children>
+ #include <include/interface-disable-arp-filter.xml.i>
+ #include <include/interface-enable-arp-accept.xml.i>
+ #include <include/interface-enable-arp-announce.xml.i>
+ #include <include/interface-enable-arp-ignore.xml.i>
+ </children>
+ </node>
+ #include <include/interface-mac.xml.i>
+ #include <include/interface-mtu-68-9000.xml.i>
+ <tagNode name="vif-c">
+ <properties>
+ <help>QinQ TAG-C Virtual Local Area Network (VLAN) ID</help>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4094"/>
+ </constraint>
+ <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6-dhcp.xml.i>
+ #include <include/interface-description.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
+ #include <include/interface-disable-link-detect.xml.i>
+ #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>
+</tagNode>
diff --git a/interface-definitions/include/vif.xml.i b/interface-definitions/include/vif.xml.i
new file mode 100644
index 000000000..5a4e52122
--- /dev/null
+++ b/interface-definitions/include/vif.xml.i
@@ -0,0 +1,65 @@
+<tagNode name="vif">
+ <properties>
+ <help>Virtual Local Area Network (VLAN) ID</help>
+ <valueHelp>
+ <format>0-4094</format>
+ <description>Virtual Local Area Network (VLAN) ID</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4094"/>
+ </constraint>
+ <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6-dhcp.xml.i>
+ #include <include/interface-description.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/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>
+ <completionHelp>
+ <script>echo Format for qos mapping, e.g.: '0:1 1:6 7:6'</script>
+ </completionHelp>
+ <constraint>
+ <regex>[:0-7 ]+$</regex>
+ </constraint>
+ <constraintErrorMessage>QoS mapping should be in the format of '0:7 2:3' with numbers 0-9</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="ingress-qos">
+ <properties>
+ <help>VLAN ingress QoS</help>
+ <completionHelp>
+ <script>echo Format for qos mapping '0:1 1:6 7:6'</script>
+ </completionHelp>
+ <constraint>
+ <regex>[:0-7 ]+$</regex>
+ </constraint>
+ <constraintErrorMessage>QoS mapping should be in the format of '0:7 2:3' with numbers 0-9</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="ip">
+ <children>
+ #include <include/interface-arp-cache-timeout.xml.i>
+ #include <include/interface-disable-arp-filter.xml.i>
+ #include <include/interface-enable-arp-accept.xml.i>
+ #include <include/interface-enable-arp-announce.xml.i>
+ #include <include/interface-enable-arp-ignore.xml.i>
+ #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>
+</tagNode>
diff --git a/interface-definitions/intel_qat.xml.in b/interface-definitions/intel_qat.xml.in
new file mode 100644
index 000000000..812484184
--- /dev/null
+++ b/interface-definitions/intel_qat.xml.in
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="acceleration" owner="${vyos_conf_scripts_dir}/intel_qat.py">
+ <properties>
+ <help>Acceleration components</help>
+ <priority>50</priority>
+ </properties>
+ <children>
+ <leafNode name="qat">
+ <properties>
+ <help>Enable Intel QAT (Quick Assist Technology) for cryptographic acceleration</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in
new file mode 100644
index 000000000..7d658f6a0
--- /dev/null
+++ b/interface-definitions/interfaces-bonding.xml.in
@@ -0,0 +1,174 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="bonding" owner="${vyos_conf_scripts_dir}/interfaces-bonding.py">
+ <properties>
+ <help>Bonding Interface/Link Aggregation</help>
+ <priority>320</priority>
+ <constraint>
+ <regex>^bond[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Bonding interface must be named bondN</constraintErrorMessage>
+ <valueHelp>
+ <format>bondN</format>
+ <description>Bonding interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6-dhcp.xml.i>
+ <node name="arp-monitor">
+ <properties>
+ <help>ARP link monitoring parameters</help>
+ </properties>
+ <children>
+ <leafNode name="interval">
+ <properties>
+ <help>ARP link monitoring interval</help>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>Specifies the ARP link monitoring frequency in milliseconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="target">
+ <properties>
+ <help>IP address used for ARP monitoring</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Specify IPv4 address of ARP requests when interval is enabled</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/interface-description.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/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>
+ <completionHelp>
+ <list>layer2 layer2+3 layer3+4</list>
+ </completionHelp>
+ <valueHelp>
+ <format>layer2</format>
+ <description>use MAC addresses to generate the hash (802.3ad, default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>layer2+3</format>
+ <description>combine MAC address and IP address to make hash</description>
+ </valueHelp>
+ <valueHelp>
+ <format>layer3+4</format>
+ <description>combine IP address and port to make hash</description>
+ </valueHelp>
+ <constraint>
+ <regex>(layer2\+3|layer3\+4|layer2)</regex>
+ </constraint>
+ <constraintErrorMessage>hash-policy must be layer2 layer2+3 or layer3+4</constraintErrorMessage>
+ </properties>
+ <defaultValue>layer2</defaultValue>
+ </leafNode>
+ <node name="ip">
+ <children>
+ #include <include/interface-arp-cache-timeout.xml.i>
+ #include <include/interface-disable-arp-filter.xml.i>
+ #include <include/interface-enable-arp-accept.xml.i>
+ #include <include/interface-enable-arp-announce.xml.i>
+ #include <include/interface-enable-arp-ignore.xml.i>
+ #include <include/interface-enable-proxy-arp.xml.i>
+ #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>
+ <help>Bonding mode</help>
+ <completionHelp>
+ <list>802.3ad active-backup broadcast round-robin transmit-load-balance adaptive-load-balance xor-hash</list>
+ </completionHelp>
+ <valueHelp>
+ <format>802.3ad</format>
+ <description>IEEE 802.3ad Dynamic link aggregation (Default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>active-backup</format>
+ <description>Fault tolerant: only one slave in the bond is active</description>
+ </valueHelp>
+ <valueHelp>
+ <format>broadcast</format>
+ <description>Fault tolerant: transmits everything on all slave interfaces</description>
+ </valueHelp>
+ <valueHelp>
+ <format>round-robin</format>
+ <description>Load balance: transmit packets in sequential order</description>
+ </valueHelp>
+ <valueHelp>
+ <format>transmit-load-balance</format>
+ <description>Load balance: adapts based on transmit load and speed</description>
+ </valueHelp>
+ <valueHelp>
+ <format>adaptive-load-balance</format>
+ <description>Load balance: adapts based on transmit and receive plus ARP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>xor-hash</format>
+ <description>Distribute based on MAC address</description>
+ </valueHelp>
+ <constraint>
+ <regex>(802.3ad|active-backup|broadcast|round-robin|transmit-load-balance|adaptive-load-balance|xor-hash)</regex>
+ </constraint>
+ <constraintErrorMessage>mode must be 802.3ad, active-backup, broadcast, round-robin, transmit-load-balance, adaptive-load-balance, or xor</constraintErrorMessage>
+ </properties>
+ <defaultValue>802.3ad</defaultValue>
+ </leafNode>
+ <node name="member">
+ <properties>
+ <help>Bridge member interfaces</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Member interface name</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --bondable</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/interface-mtu-68-9000.xml.i>
+ <leafNode name="primary">
+ <properties>
+ <help>Primary device interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --bondable</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ #include <include/vif-s.xml.i>
+ #include <include/vif.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in
new file mode 100644
index 000000000..92356d696
--- /dev/null
+++ b/interface-definitions/interfaces-bridge.xml.in
@@ -0,0 +1,184 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="bridge" owner="${vyos_conf_scripts_dir}/interfaces-bridge.py">
+ <properties>
+ <help>Bridge Interface</help>
+ <priority>489</priority>
+ <constraint>
+ <regex>^br[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Bridge interface must be named brN</constraintErrorMessage>
+ <valueHelp>
+ <format>brN</format>
+ <description>Bridge interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6-dhcp.xml.i>
+ <leafNode name="aging">
+ <properties>
+ <help>MAC address aging interval</help>
+ <valueHelp>
+ <format>0</format>
+ <description>Disable MAC address learning (always flood)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>10-1000000</format>
+ <description>MAC address aging time in seconds (default: 300)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-0 --range 10-1000000"/>
+ </constraint>
+ </properties>
+ <defaultValue>300</defaultValue>
+ </leafNode>
+ #include <include/interface-description.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/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>
+ <valueHelp>
+ <format>0-200</format>
+ <description>Spanning Tree Protocol forwarding delay in seconds (default 15)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-200"/>
+ </constraint>
+ <constraintErrorMessage>Forwarding delay must be between 0 and 200 seconds</constraintErrorMessage>
+ </properties>
+ <defaultValue>14</defaultValue>
+ </leafNode>
+ <leafNode name="hello-time">
+ <properties>
+ <help>Hello packet advertisment interval</help>
+ <valueHelp>
+ <format>1-10</format>
+ <description>Spanning Tree Protocol hello advertisement interval in seconds (default 2)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-10"/>
+ </constraint>
+ <constraintErrorMessage>Bridge Hello interval must be between 1 and 10 seconds</constraintErrorMessage>
+ </properties>
+ <defaultValue>2</defaultValue>
+ </leafNode>
+ <node name="igmp">
+ <properties>
+ <help>Internet Group Management Protocol (IGMP) settings</help>
+ </properties>
+ <children>
+ <leafNode name="querier">
+ <properties>
+ <help>Enable IGMP querier</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="ip">
+ <children>
+ #include <include/interface-arp-cache-timeout.xml.i>
+ #include <include/interface-enable-arp-accept.xml.i>
+ #include <include/interface-enable-arp-announce.xml.i>
+ #include <include/interface-enable-arp-ignore.xml.i>
+ #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>
+ <help>Interval at which neighbor bridges are removed</help>
+ <valueHelp>
+ <format>1-40</format>
+ <description>Bridge maximum aging time in seconds (default 20)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-40"/>
+ </constraint>
+ <constraintErrorMessage>Bridge max aging value must be between 1 and 40 seconds</constraintErrorMessage>
+ </properties>
+ <defaultValue>20</defaultValue>
+ </leafNode>
+ <node name="member">
+ <properties>
+ <help>Bridge member interfaces</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Member interface name</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --bridgeable</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="cost">
+ <properties>
+ <help>Bridge port cost</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Path cost value for Spanning Tree Protocol</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Path cost value must be between 1 and 65535</constraintErrorMessage>
+ </properties>
+ <defaultValue>100</defaultValue>
+ </leafNode>
+ <leafNode name="priority">
+ <properties>
+ <help>Bridge port priority</help>
+ <valueHelp>
+ <format>0-63</format>
+ <description>Bridge port priority</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-63"/>
+ </constraint>
+ <constraintErrorMessage>Port priority value must be between 0 and 63</constraintErrorMessage>
+ </properties>
+ <defaultValue>32</defaultValue>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <leafNode name="priority">
+ <properties>
+ <help>Priority for this bridge</help>
+ <valueHelp>
+ <format>0-65535</format>
+ <description>Bridge priority (default 32768)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-65535"/>
+ </constraint>
+ <constraintErrorMessage>Bridge priority must be between 0 and 65535 (multiples of 4096)</constraintErrorMessage>
+ </properties>
+ <defaultValue>32768</defaultValue>
+ </leafNode>
+ <leafNode name="stp">
+ <properties>
+ <help>Enable spanning tree protocol</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in
new file mode 100644
index 000000000..135adfc10
--- /dev/null
+++ b/interface-definitions/interfaces-dummy.xml.in
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="dummy" owner="${vyos_conf_scripts_dir}/interfaces-dummy.py">
+ <properties>
+ <help>Dummy Interface</help>
+ <priority>300</priority>
+ <constraint>
+ <regex>^dum[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Dummy interface must be named dumN</constraintErrorMessage>
+ <valueHelp>
+ <format>dumN</format>
+ <description>Dummy interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #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>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in
new file mode 100644
index 000000000..e8f3f09f1
--- /dev/null
+++ b/interface-definitions/interfaces-ethernet.xml.in
@@ -0,0 +1,277 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="ethernet" owner="${vyos_conf_scripts_dir}/interfaces-ethernet.py">
+ <properties>
+ <help>Ethernet Interface</help>
+ <priority>318</priority>
+ <constraint>
+ <regex>^((eth|lan)[0-9]+|(eno|ens|enp|enx).+)$</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid Ethernet interface name</constraintErrorMessage>
+ <valueHelp>
+ <format>ethN</format>
+ <description>Ethernet interface name</description>
+ </valueHelp>
+ <valueHelp>
+ <format>en[ospx]N</format>
+ <description>Ethernet interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6-dhcp.xml.i>
+ #include <include/interface-description.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
+ <leafNode name="disable-flow-control">
+ <properties>
+ <help>Disable Ethernet flow control (pause frames)</help>
+ <valueless/>
+ </properties>
+ </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>
+ <completionHelp>
+ <list>auto half full</list>
+ </completionHelp>
+ <valueHelp>
+ <format>auto</format>
+ <description>Auto negotiation (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>half</format>
+ <description>Half duplex</description>
+ </valueHelp>
+ <valueHelp>
+ <format>full</format>
+ <description>Full duplex</description>
+ </valueHelp>
+ <constraint>
+ <regex>(auto|half|full)</regex>
+ </constraint>
+ <constraintErrorMessage>duplex must be auto, half or full</constraintErrorMessage>
+ </properties>
+ <defaultValue>auto</defaultValue>
+ </leafNode>
+ #include <include/interface-hw-id.xml.i>
+ <node name="ip">
+ <children>
+ #include <include/interface-arp-cache-timeout.xml.i>
+ #include <include/interface-disable-arp-filter.xml.i>
+ #include <include/interface-enable-arp-accept.xml.i>
+ #include <include/interface-enable-arp-announce.xml.i>
+ #include <include/interface-enable-arp-ignore.xml.i>
+ #include <include/interface-enable-proxy-arp.xml.i>
+ #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">
+ <properties>
+ <help>Configurable offload options</help>
+ </properties>
+ <children>
+ <leafNode name="generic-receive">
+ <properties>
+ <help>Configure GRO (generic receive offload)</help>
+ <completionHelp>
+ <list>on off</list>
+ </completionHelp>
+ <valueHelp>
+ <format>on</format>
+ <description>Enable GRO (generic receive offload)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>off</format>
+ <description>Disable GRO (generic receive offload)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(on|off)</regex>
+ </constraint>
+ <constraintErrorMessage>Must be either 'on' or 'off'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="generic-segmentation">
+ <properties>
+ <help>Configure GSO (generic segmentation offload)</help>
+ <completionHelp>
+ <list>on off</list>
+ </completionHelp>
+ <valueHelp>
+ <format>on</format>
+ <description>Enable GSO (generic segmentation offload)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>off</format>
+ <description>Disable GSO (generic segmentation offload)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(on|off)</regex>
+ </constraint>
+ <constraintErrorMessage>Must be either 'on' or 'off'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="scatter-gather">
+ <properties>
+ <help>Configure scatter-gather option</help>
+ <completionHelp>
+ <list>on off</list>
+ </completionHelp>
+ <valueHelp>
+ <format>on</format>
+ <description>Enable scatter-gather</description>
+ </valueHelp>
+ <valueHelp>
+ <format>off</format>
+ <description>Disable scatter-gather</description>
+ </valueHelp>
+ <constraint>
+ <regex>(on|off)</regex>
+ </constraint>
+ <constraintErrorMessage>Must be either 'on' or 'off'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="tcp-segmentation">
+ <properties>
+ <help>Configure TSO (TCP segmentation offloading)</help>
+ <completionHelp>
+ <list>on off</list>
+ </completionHelp>
+ <valueHelp>
+ <format>on</format>
+ <description>Enable TSO (TCP segmentation offloading)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>off</format>
+ <description>Disable TSO (TCP segmentation offloading)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(on|off)</regex>
+ </constraint>
+ <constraintErrorMessage>Must be either 'on' or 'off'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="udp-fragmentation">
+ <properties>
+ <help>Configure UDP fragmentation offloading</help>
+ <completionHelp>
+ <list>on off</list>
+ </completionHelp>
+ <valueHelp>
+ <format>on</format>
+ <description>Enable UDP fragmentation offloading</description>
+ </valueHelp>
+ <valueHelp>
+ <format>off</format>
+ <description>Disable UDP fragmentation offloading</description>
+ </valueHelp>
+ <constraint>
+ <regex>(on|off)</regex>
+ </constraint>
+ <constraintErrorMessage>Must be either 'on' or 'off'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="smp-affinity">
+ <properties>
+ <help>CPU interrupt affinity mask</help>
+ <completionHelp>
+ <list>auto 10 100 1000 2500 5000 10000</list>
+ </completionHelp>
+ <valueHelp>
+ <format>auto</format>
+ <description>Auto negotiation (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hex</format>
+ <description>Bitmask representing CPUs that this NIC will interrupt</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hex,hex</format>
+ <description>Bitmasks representing CPUs for interrupt and receive processing</description>
+ </valueHelp>
+ <constraint>
+ <regex>(auto)</regex>
+ <regex>[0-9a-f]+(|,[0-9a-f]+)$</regex>
+ </constraint>
+ <constraintErrorMessage>IRQ affinity mask must be hex value or auto</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="speed">
+ <properties>
+ <help>Link speed</help>
+ <completionHelp>
+ <list>auto 10 100 1000 2500 5000 10000 25000 40000 50000 100000</list>
+ </completionHelp>
+ <valueHelp>
+ <format>auto</format>
+ <description>Auto negotiation (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>10</format>
+ <description>10 Mbit/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>100</format>
+ <description>100 Mbit/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>1000</format>
+ <description>1 Gbit/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>2500</format>
+ <description>2.5 Gbit/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>5000</format>
+ <description>5 Gbit/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>10000</format>
+ <description>10 Gbit/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>25000</format>
+ <description>25 Gbit/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>40000</format>
+ <description>40 Gbit/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>50000</format>
+ <description>50 Gbit/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>100000</format>
+ <description>100 Gbit/sec</description>
+ </valueHelp>
+ <constraint>
+ <regex>(auto|10|100|1000|2500|5000|10000|25000|40000|50000|100000)</regex>
+ </constraint>
+ <constraintErrorMessage>Speed must be auto, 10, 100, 1000, 2500, 5000, 10000, 25000, 40000, 50000 or 100000</constraintErrorMessage>
+ </properties>
+ <defaultValue>auto</defaultValue>
+ </leafNode>
+ #include <include/vif-s.xml.i>
+ #include <include/vif.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in
new file mode 100644
index 000000000..31a3ebb7a
--- /dev/null
+++ b/interface-definitions/interfaces-geneve.xml.in
@@ -0,0 +1,60 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="geneve" owner="${vyos_conf_scripts_dir}/interfaces-geneve.py">
+ <properties>
+ <help>Generic Network Virtualization Encapsulation (GENEVE) Interface</help>
+ <priority>460</priority>
+ <constraint>
+ <regex>^gnv[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>GENEVE interface must be named gnvN</constraintErrorMessage>
+ <valueHelp>
+ <format>gnvN</format>
+ <description>GENEVE interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6.xml.i>
+ #include <include/interface-description.xml.i>
+ #include <include/interface-disable.xml.i>
+ <node name="ip">
+ <properties>
+ <help>IPv4 routing parameters</help>
+ </properties>
+ <children>
+ #include <include/interface-arp-cache-timeout.xml.i>
+ #include <include/interface-enable-proxy-arp.xml.i>
+ </children>
+ </node>
+ #include <include/interface-mtu-1450-9000.xml.i>
+ <leafNode name="remote">
+ <properties>
+ <help>Remote address of GENEVE tunnel</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Remote address of GENEVE tunnel</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="vni">
+ <properties>
+ <help>Virtual Network Identifier</help>
+ <valueHelp>
+ <format>0-16777214</format>
+ <description>GENEVE virtual network identifier</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-16777214"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in
new file mode 100644
index 000000000..3a878ad76
--- /dev/null
+++ b/interface-definitions/interfaces-l2tpv3.xml.in
@@ -0,0 +1,161 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="l2tpv3" owner="${vyos_conf_scripts_dir}/interfaces-l2tpv3.py">
+ <properties>
+ <help>Layer 2 Tunnel Protocol Version 3 (L2TPv3) Interface</help>
+ <priority>485</priority>
+ <constraint>
+ <regex>^l2tpeth[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>L2TPv3 interface must be named l2tpethN</constraintErrorMessage>
+ <valueHelp>
+ <format>l2tpethN</format>
+ <description>L2TPv3 interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6.xml.i>
+ #include <include/interface-description.xml.i>
+ <leafNode name="destination-port">
+ <properties>
+ <help>UDP destination port for L2TPv3 tunnel (default: 5000)</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>5000</defaultValue>
+ </leafNode>
+ #include <include/interface-disable.xml.i>
+ <leafNode name="encapsulation">
+ <properties>
+ <help>Encapsulation type (default: UDP)</help>
+ <completionHelp>
+ <list>udp ip</list>
+ </completionHelp>
+ <valueHelp>
+ <format>udp</format>
+ <description>UDP encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ip</format>
+ <description>IP encapsulation</description>
+ </valueHelp>
+ <constraint>
+ <regex>(udp|ip)</regex>
+ </constraint>
+ <constraintErrorMessage>Encapsulation must be UDP or IP</constraintErrorMessage>
+ </properties>
+ <defaultValue>udp</defaultValue>
+ </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>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Local IPv4 address of tunnel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Local IPv6 address of tunnel</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/interface-mtu-68-9000.xml.i>
+ <leafNode name="peer-session-id">
+ <properties>
+ <help>Peer session identifier</help>
+ <valueHelp>
+ <format>1-429496729</format>
+ <description>L2TPv3 peer session identifier</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-429496729"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="peer-tunnel-id">
+ <properties>
+ <help>Peer tunnel identifier</help>
+ <valueHelp>
+ <format>1-429496729</format>
+ <description>L2TPv3 peer tunnel identifier</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-429496729"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-ip">
+ <properties>
+ <help>Remote IP address for L2TPv3 tunnel</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Remote IPv4 address of tunnel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Remote IPv6 address of tunnel</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="session-id">
+ <properties>
+ <help>Session identifier</help>
+ <valueHelp>
+ <format>1-429496729</format>
+ <description>L2TPv3 session identifier</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-429496729"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="source-port">
+ <properties>
+ <help>UDP source port for L2TPv3 tunnel (default: 5000)</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>5000</defaultValue>
+ </leafNode>
+ <leafNode name="tunnel-id">
+ <properties>
+ <help>Local tunnel identifier</help>
+ <valueHelp>
+ <format>1-429496729</format>
+ <description>L2TPv3 local tunnel identifier</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-429496729"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-loopback.xml.in b/interface-definitions/interfaces-loopback.xml.in
new file mode 100644
index 000000000..97d5bab90
--- /dev/null
+++ b/interface-definitions/interfaces-loopback.xml.in
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="loopback" owner="${vyos_conf_scripts_dir}/interfaces-loopback.py">
+ <properties>
+ <help>Loopback Interface</help>
+ <priority>300</priority>
+ <constraint>
+ <regex>^lo$</regex>
+ </constraint>
+ <constraintErrorMessage>Loopback interface must be named lo</constraintErrorMessage>
+ <valueHelp>
+ <format>lo</format>
+ <description>Loopback interface</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6.xml.i>
+ #include <include/interface-description.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in
new file mode 100644
index 000000000..dfef387d2
--- /dev/null
+++ b/interface-definitions/interfaces-macsec.xml.in
@@ -0,0 +1,116 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="macsec" owner="${vyos_conf_scripts_dir}/interfaces-macsec.py">
+ <properties>
+ <help>MACsec Interface (802.1ae)</help>
+ <priority>319</priority>
+ <constraint>
+ <regex>^macsec[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>MACsec interface must be named macsecN</constraintErrorMessage>
+ <valueHelp>
+ <format>macsecN</format>
+ <description>MACsec interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6.xml.i>
+ <node name="security">
+ <properties>
+ <help>Security/Encryption Settings</help>
+ </properties>
+ <children>
+ <leafNode name="cipher">
+ <properties>
+ <help>Cipher suite used</help>
+ <completionHelp>
+ <list>gcm-aes-128</list>
+ </completionHelp>
+ <valueHelp>
+ <format>gcm-aes-128</format>
+ <description>Galois/Counter Mode of AES cipher with 128-bit key (default)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(gcm-aes-128)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="encrypt">
+ <properties>
+ <help>Enable optional MACsec encryption</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="mka">
+ <properties>
+ <help>MACsec Key Agreement protocol (MKA)</help>
+ </properties>
+ <children>
+ <leafNode name="cak">
+ <properties>
+ <help>Secure Connectivity Association Key</help>
+ <valueHelp>
+ <format>key</format>
+ <description>16-byte (128-bit) hex-string (32 hex-digits)</description>
+ </valueHelp>
+ <constraint>
+ <regex>^[A-Fa-f0-9]{32}$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="ckn">
+ <properties>
+ <help>Secure Connectivity Association Key Name</help>
+ <valueHelp>
+ <format>key</format>
+ <description>32-byte (256-bit) hex-string (64 hex-digits)</description>
+ </valueHelp>
+ <constraint>
+ <regex>^[A-Fa-f0-9]{64}$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="priority">
+ <properties>
+ <help>Priority of MACsec Key Agreement protocol (MKA) actor (default: 255)</help>
+ <valueHelp>
+ <format>0-255</format>
+ <description>MACsec Key Agreement protocol (MKA) priority</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255" />
+ </constraint>
+ </properties>
+ <defaultValue>255</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="replay-window">
+ <properties>
+ <help>IEEE 802.1X/MACsec replay protection window</help>
+ <valueHelp>
+ <format>0</format>
+ <description>No replay window, strict check</description>
+ </valueHelp>
+ <valueHelp>
+ <format>1-4294967295</format>
+ <description>Number of packets that could be misordered</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295" />
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/interface-description.xml.i>
+ #include <include/interface-disable.xml.i>
+ #include <include/interface-vrf.xml.i>
+ #include <include/source-interface-ethernet.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in
new file mode 100644
index 000000000..905c76507
--- /dev/null
+++ b/interface-definitions/interfaces-openvpn.xml.in
@@ -0,0 +1,808 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="openvpn" owner="${vyos_conf_scripts_dir}/interfaces-openvpn.py">
+ <properties>
+ <help>OpenVPN Tunnel Interface</help>
+ <priority>460</priority>
+ <constraint>
+ <regex>^vtun[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>OpenVPN tunnel interface must be named vtunN</constraintErrorMessage>
+ <valueHelp>
+ <format>vtunN</format>
+ <description>OpenVPN interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <node name="authentication">
+ <properties>
+ <help>Authentication options</help>
+ </properties>
+ <children>
+ <leafNode name="password">
+ <properties>
+ <help>OpenVPN password used for authentication</help>
+ </properties>
+ </leafNode>
+ <leafNode name="username">
+ <properties>
+ <help>OpenVPN username used for authentication</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/interface-description.xml.i>
+ <leafNode name="device-type">
+ <properties>
+ <help>OpenVPN interface device-type</help>
+ <completionHelp>
+ <list>tun tap</list>
+ </completionHelp>
+ <valueHelp>
+ <format>tun</format>
+ <description>TUN device, required for OSI layer 3</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tap</format>
+ <description>TAP device, required for OSI layer 2</description>
+ </valueHelp>
+ <constraint>
+ <regex>(tun|tap)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/interface-disable.xml.i>
+ <node name="encryption">
+ <properties>
+ <help>Data Encryption settings</help>
+ </properties>
+ <children>
+ <leafNode name="cipher">
+ <properties>
+ <help>Standard Data Encryption Algorithm</help>
+ <completionHelp>
+ <list>des 3des bf128 bf256 aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list>
+ </completionHelp>
+ <valueHelp>
+ <format>des</format>
+ <description>DES algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>3des</format>
+ <description>DES algorithm with triple encryption</description>
+ </valueHelp>
+ <valueHelp>
+ <format>bf128</format>
+ <description>Blowfish algorithm with 128-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>bf256</format>
+ <description>Blowfish algorithm with 256-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes128</format>
+ <description>AES algorithm with 128-bit key CBC</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes128gcm</format>
+ <description>AES algorithm with 128-bit key GCM</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes192</format>
+ <description>AES algorithm with 192-bit key CBC</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes192gcm</format>
+ <description>AES algorithm with 192-bit key GCM</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes256</format>
+ <description>AES algorithm with 256-bit key CBC</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes256gcm</format>
+ <description>AES algorithm with 256-bit key GCM</description>
+ </valueHelp>
+ <constraint>
+ <regex>(des|3des|bf128|bf256|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="ncp-ciphers">
+ <properties>
+ <help>Cipher negotiation list for use in server or client mode</help>
+ <completionHelp>
+ <list>des 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list>
+ </completionHelp>
+ <valueHelp>
+ <format>des</format>
+ <description>DES algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>3des</format>
+ <description>DES algorithm with triple encryption</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes128</format>
+ <description>AES algorithm with 128-bit key CBC</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes128gcm</format>
+ <description>AES algorithm with 128-bit key GCM</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes192</format>
+ <description>AES algorithm with 192-bit key CBC</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes192gcm</format>
+ <description>AES algorithm with 192-bit key GCM</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes256</format>
+ <description>AES algorithm with 256-bit key CBC</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes256gcm</format>
+ <description>AES algorithm with 256-bit key GCM</description>
+ </valueHelp>
+ <constraint>
+ <regex>(des|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-ncp">
+ <properties>
+ <help>Disable support for ncp-ciphers</help>
+ <valueless/>
+ </properties>
+ </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>
+ <completionHelp>
+ <list>md5 sha1 sha256 sha384 sha512</list>
+ </completionHelp>
+ <valueHelp>
+ <format>md5</format>
+ <description>MD5 algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha1</format>
+ <description>SHA-1 algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha256</format>
+ <description>SHA-256 algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha384</format>
+ <description>SHA-384 algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha512</format>
+ <description>SHA-512 algorithm</description>
+ </valueHelp>
+ <constraint>
+ <regex>(md5|sha1|sha256|sha384|sha512)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="keep-alive">
+ <properties>
+ <help>Keepalive helper options</help>
+ </properties>
+ <children>
+ <leafNode name="failure-count">
+ <properties>
+ <help>Maximum number of keepalive packet failures [default 6]</help>
+ <valueHelp>
+ <format>0-1000</format>
+ <description>Maximum number of keepalive packet failures</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-1000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="interval">
+ <properties>
+ <help>Keepalive packet interval (seconds) [default 10]</help>
+ <valueHelp>
+ <format>0-600</format>
+ <description>Keepalive packet interval (seconds)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-600"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="local-address">
+ <properties>
+ <help>Local IP address of tunnel (IPv4 or IPv6)</help>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="subnet-mask">
+ <properties>
+ <help>Subnet-mask for local IP address of tunnel (IPv4 only)</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="local-host">
+ <properties>
+ <help>Local IP address to accept connections (all if not set)</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Local IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Local IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="local-port">
+ <properties>
+ <help>Local port number to accept connections</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mode">
+ <properties>
+ <help>OpenVPN mode of operation</help>
+ <completionHelp>
+ <list>site-to-site client server</list>
+ </completionHelp>
+ <valueHelp>
+ <format>site-to-site</format>
+ <description>Site-to-site mode</description>
+ </valueHelp>
+ <valueHelp>
+ <format>client</format>
+ <description>Client in client-server mode</description>
+ </valueHelp>
+ <valueHelp>
+ <format>server</format>
+ <description>Server in client-server mode</description>
+ </valueHelp>
+ <constraint>
+ <regex>(site-to-site|client|server)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="openvpn-option">
+ <properties>
+ <help>Additional OpenVPN options. You must
+ use the syntax of openvpn.conf in this text-field. Using this
+ without proper knowledge may result in a crashed OpenVPN server.
+ Check system log to look for errors.</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="persistent-tunnel">
+ <properties>
+ <help>Do not close and reopen interface (TUN/TAP device) on client restarts</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="protocol">
+ <properties>
+ <help>OpenVPN communication protocol</help>
+ <completionHelp>
+ <list>udp tcp-passive tcp-active</list>
+ </completionHelp>
+ <valueHelp>
+ <format>udp</format>
+ <description>UDP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp-passive</format>
+ <description>TCP and accepts connections passively</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp-active</format>
+ <description>TCP and initiates connections actively</description>
+ </valueHelp>
+ <constraint>
+ <regex>(udp|tcp-passive|tcp-active)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-address">
+ <properties>
+ <help>IP address of remote end of tunnel</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Remote end IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Remote end IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-host">
+ <properties>
+ <help>Remote host to connect to (dynamic if not set)</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address of remote host</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address of remote host</description>
+ </valueHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Hostname of remote host</description>
+ </valueHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-port">
+ <properties>
+ <help>Remote port number to connect to</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="replace-default-route">
+ <properties>
+ <help>OpenVPN tunnel to be used as the default route</help>
+ </properties>
+ <children>
+ <leafNode name="local">
+ <properties>
+ <help>Tunnel endpoints are on the same subnet</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="server">
+ <properties>
+ <help>Server-mode options</help>
+ </properties>
+ <children>
+ <tagNode name="client">
+ <properties>
+ <help>Client-specific settings</help>
+ <valueHelp>
+ <format>name</format>
+ <description>Client common-name in the certificate</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable client connection</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ip">
+ <properties>
+ <help>IP address of the client</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Client IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Client IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="push-route">
+ <properties>
+ <help>Route to be pushed to the client</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 network and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 network and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="subnet">
+ <properties>
+ <help>Subnet belonging to the client (iroute)</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 network and prefix length belonging to the client</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 network and prefix length belonging to the client</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="client-ip-pool">
+ <properties>
+ <help>Pool of client IPv4 addresses</help>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable client IP pool</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="start">
+ <properties>
+ <help>First IP address in the pool</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="stop">
+ <properties>
+ <help>Last IP address in the pool</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="subnet-mask">
+ <properties>
+ <help>Subnet mask pushed to dynamic clients.
+ If not set the server subnet mask will be used.
+ Only used with topology subnet or device type tap.
+ Not used with bridged interfaces.</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 subnet mask</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="client-ipv6-pool">
+ <properties>
+ <help>Pool of client IPv6 addresses</help>
+ </properties>
+ <children>
+ <leafNode name="base">
+ <properties>
+ <help>Client IPv6 pool base address with optional prefix length</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>Client IPv6 pool base address with optional prefix length (defaults: base = server subnet + 0x1000, prefix length = server prefix length)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable client IPv6 pool</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="domain-name">
+ <properties>
+ <help>DNS suffix to be pushed to all clients</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Domain Name Server suffix</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="max-connections">
+ <properties>
+ <help>Number of maximum client connections</help>
+ <valueHelp>
+ <format>1-4096</format>
+ <description>Number of concurrent clients</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4096"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="name-server">
+ <properties>
+ <help>Domain Name Server (DNS)</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>DNS server IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>DNS server IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="push-route">
+ <properties>
+ <help>Route to be pushed to all clients</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 network and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 network and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="reject-unconfigured-clients">
+ <properties>
+ <help>Reject connections from clients that are not explicitly configured</help>
+ </properties>
+ </leafNode>
+ <leafNode name="subnet">
+ <properties>
+ <help>Server-mode subnet (from which client IPs are allocated)</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 network and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 network and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="topology">
+ <properties>
+ <help>Topology for clients</help>
+ <completionHelp>
+ <list>net30 point-to-point subnet</list>
+ </completionHelp>
+ <valueHelp>
+ <format>net30</format>
+ <description>net30 topology (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>point-to-point</format>
+ <description>Point-to-point topology</description>
+ </valueHelp>
+ <valueHelp>
+ <format>subnet</format>
+ <description>Subnet topology</description>
+ </valueHelp>
+ <constraint>
+ <regex>(subnet|point-to-point|net30)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="shared-secret-key-file">
+ <properties>
+ <help>File containing the secret key shared with remote end of tunnel</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>
+ <node name="tls">
+ <properties>
+ <help>Transport Layer Security (TLS) options</help>
+ </properties>
+ <children>
+ <leafNode name="auth-file">
+ <properties>
+ <help>File containing tls static key for tls-auth</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="ca-cert-file">
+ <properties>
+ <help>File containing certificate for Certificate Authority (CA)</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="cert-file">
+ <properties>
+ <help>File containing certificate for this host</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="crl-file">
+ <properties>
+ <help>File containing certificate revocation list (CRL) for this host</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="dh-file">
+ <properties>
+ <help>File containing Diffie Hellman parameters (server only)</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="key-file">
+ <properties>
+ <help>Private key for this host</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="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>
+ <completionHelp>
+ <list>1.0 1.1 1.2</list>
+ </completionHelp>
+ <valueHelp>
+ <format>1.0</format>
+ <description>TLS v1.0</description>
+ </valueHelp>
+ <valueHelp>
+ <format>1.1</format>
+ <description>TLS v1.1</description>
+ </valueHelp>
+ <valueHelp>
+ <format>1.2</format>
+ <description>TLS v1.2</description>
+ </valueHelp>
+ <constraint>
+ <regex>(1.0|1.1|1.2)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="role">
+ <properties>
+ <help>Private key for this host</help>
+ <completionHelp>
+ <list>active passive</list>
+ </completionHelp>
+ <valueHelp>
+ <format>active</format>
+ <description>Initiate TLS negotiation actively</description>
+ </valueHelp>
+ <valueHelp>
+ <format>passive</format>
+ <description>Waiting for TLS connections passively</description>
+ </valueHelp>
+ <constraint>
+ <regex>(active|passive)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="use-lzo-compression">
+ <properties>
+ <help>Use fast LZO compression on this TUN/TAP interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ #include <include/interface-vrf.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in
new file mode 100644
index 000000000..8a6c61312
--- /dev/null
+++ b/interface-definitions/interfaces-pppoe.xml.in
@@ -0,0 +1,164 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="pppoe" owner="${vyos_conf_scripts_dir}/interfaces-pppoe.py">
+ <properties>
+ <help>Point-to-Point Protocol over Ethernet (PPPoE)</help>
+ <priority>321</priority>
+ <constraint>
+ <regex>^pppoe[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>PPPoE interface must be named pppoeN</constraintErrorMessage>
+ <valueHelp>
+ <format>pppoeN</format>
+ <description>PPPoE dialer interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="access-concentrator">
+ <properties>
+ <help>Access concentrator name (only connect to this concentrator)</help>
+ <constraint>
+ <regex>[a-zA-Z0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Access concentrator name must be composed of uppper and lower case letters or numbers only</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="authentication">
+ <properties>
+ <help>Authentication settings</help>
+ </properties>
+ <children>
+ <leafNode name="user">
+ <properties>
+ <help>User name</help>
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>Password</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="connect-on-demand">
+ <properties>
+ <help>Automatic establishment of PPPOE connection when traffic is sent</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="default-route">
+ <properties>
+ <help>Default route insertion behaviour (default: auto)</help>
+ <completionHelp>
+ <list>auto none force</list>
+ </completionHelp>
+ <constraint>
+ <regex>(auto|none|force)</regex>
+ </constraint>
+ <constraintErrorMessage>PPPoE default-route option must be 'auto', 'none', or 'force'</constraintErrorMessage>
+ <valueHelp>
+ <format>auto</format>
+ <description>Automatically install a default route</description>
+ </valueHelp>
+ <valueHelp>
+ <format>none</format>
+ <description>Do not install a default route</description>
+ </valueHelp>
+ <valueHelp>
+ <format>force</format>
+ <description>Replace existing default route</description>
+ </valueHelp>
+ </properties>
+ <defaultValue>auto</defaultValue>
+ </leafNode>
+ #include <include/dhcpv6-options.xml.i>
+ #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>
+ <valueHelp>
+ <format>n</format>
+ <description>Idle timeout in seconds</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <node name="ipv6">
+ <children>
+ <node name="address">
+ <properties>
+ <help>IPv6 address configuration modes</help>
+ </properties>
+ <children>
+ <leafNode name="autoconf">
+ <properties>
+ <help>Enable Stateless Address Autoconfiguration (SLAAC)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="enable">
+ <properties>
+ <help>Activate IPv6 support on this connection</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="source-interface">
+ <properties>
+ <help>Physical Interface used for this PPPoE session</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="local-address">
+ <properties>
+ <help>IPv4 address of local end of the PPPoE link</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Address of local end of the PPPoE link</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/interface-mtu-68-1500.xml.i>
+ <leafNode name="no-peer-dns">
+ <properties>
+ <help>Do not use DNS servers provided by the peer</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-address">
+ <properties>
+ <help>IPv4 address of remote end of the PPPoE link</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Address of remote end of the PPPoE link</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="service-name">
+ <properties>
+ <help>Service name, only connect to access concentrators advertising this</help>
+ <constraint>
+ <regex>[a-zA-Z0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Service name must be composed of uppper and lower case letters or numbers only</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in
new file mode 100644
index 000000000..4382db598
--- /dev/null
+++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="pseudo-ethernet" owner="${vyos_conf_scripts_dir}/interfaces-pseudo-ethernet.py">
+ <properties>
+ <help>Pseudo Ethernet</help>
+ <priority>321</priority>
+ <constraint>
+ <regex>^peth[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Pseudo Ethernet interface must be named pethN</constraintErrorMessage>
+ <valueHelp>
+ <format>pethN</format>
+ <description>Pseudo Ethernet interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6-dhcp.xml.i>
+ #include <include/interface-description.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/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>
+ #include <include/interface-disable-arp-filter.xml.i>
+ #include <include/interface-enable-arp-accept.xml.i>
+ #include <include/interface-enable-arp-announce.xml.i>
+ #include <include/interface-enable-arp-ignore.xml.i>
+ #include <include/interface-enable-proxy-arp.xml.i>
+ #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/source-interface-ethernet.xml.i>
+ #include <include/interface-mac.xml.i>
+ <leafNode name="mode">
+ <properties>
+ <help>Receive mode (default: private)</help>
+ <completionHelp>
+ <list>private vepa bridge passthru</list>
+ </completionHelp>
+ <valueHelp>
+ <format>private</format>
+ <description>No communication with other pseudo-devices</description>
+ </valueHelp>
+ <valueHelp>
+ <format>vepa</format>
+ <description>Virtual Ethernet Port Aggregator reflective relay</description>
+ </valueHelp>
+ <valueHelp>
+ <format>bridge</format>
+ <description>Simple bridge between pseudo-devices</description>
+ </valueHelp>
+ <valueHelp>
+ <format>passthru</format>
+ <description>Promicious mode passthrough of underlying device</description>
+ </valueHelp>
+ <constraint>
+ <regex>(private|vepa|bridge|passthru)</regex>
+ </constraint>
+ <constraintErrorMessage>mode must be private, vepa, bridge or passthru</constraintErrorMessage>
+ </properties>
+ <defaultValue>private</defaultValue>
+ </leafNode>
+ #include <include/interface-mtu-68-9000.xml.i>
+ #include <include/vif-s.xml.i>
+ #include <include/vif.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in
new file mode 100644
index 000000000..64520ce99
--- /dev/null
+++ b/interface-definitions/interfaces-tunnel.xml.in
@@ -0,0 +1,283 @@
+<?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>
+ #include <include/interface-ipv4.xml.i>
+ #include <include/interface-ipv6.xml.i>
+ <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="source-interface">
+ <properties>
+ <help>Physical Interface used for underlaying traffic</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </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
new file mode 100644
index 000000000..8529f6885
--- /dev/null
+++ b/interface-definitions/interfaces-vxlan.xml.in
@@ -0,0 +1,114 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="vxlan" owner="${vyos_conf_scripts_dir}/interfaces-vxlan.py">
+ <properties>
+ <help>Virtual Extensible LAN (VXLAN) Interface</help>
+ <priority>460</priority>
+ <constraint>
+ <regex>^vxlan[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>VXLAN interface must be named vxlanN</constraintErrorMessage>
+ <valueHelp>
+ <format>vxlanN</format>
+ <description>VXLAN interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6.xml.i>
+ #include <include/interface-description.xml.i>
+ #include <include/interface-disable.xml.i>
+ <leafNode name="group">
+ <properties>
+ <help>Multicast group address for VXLAN interface</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Multicast IPv4 group address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Multicast IPv6 group address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="ip">
+ <children>
+ #include <include/interface-arp-cache-timeout.xml.i>
+ #include <include/interface-disable-arp-filter.xml.i>
+ #include <include/interface-enable-arp-accept.xml.i>
+ #include <include/interface-enable-arp-announce.xml.i>
+ #include <include/interface-enable-arp-ignore.xml.i>
+ #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>
+ <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>
+ #include <include/source-interface.xml.i>
+ #include <include/interface-mtu-1200-9000.xml.i>
+ <leafNode name="remote">
+ <properties>
+ <help>Remote address of VXLAN tunnel</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Remote IPv4 address of VXLAN tunnel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Remote IPv6 address of VXLAN tunnel</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Destination port of VXLAN tunnel (default: 8472)</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>8472</defaultValue>
+ </leafNode>
+ <leafNode name="vni">
+ <properties>
+ <help>Virtual Network Identifier</help>
+ <valueHelp>
+ <format>0-16777214</format>
+ <description>VXLAN virtual network identifier</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-16777214"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in
new file mode 100644
index 000000000..981bce826
--- /dev/null
+++ b/interface-definitions/interfaces-wireguard.xml.in
@@ -0,0 +1,124 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="wireguard" owner="${vyos_conf_scripts_dir}/interfaces-wireguard.py">
+ <properties>
+ <help>WireGuard Interface</help>
+ <priority>459</priority>
+ <constraint>
+ <regex>^wg[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>WireGuard interface must be named wgN</constraintErrorMessage>
+ <valueHelp>
+ <format>wgN</format>
+ <description>WireGuard interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #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>
+ #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>
+ <valueHelp>
+ <format>number</format>
+ <description>value which marks the packet for QoS/shaper</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ <defaultValue>0</defaultValue>
+ </leafNode>
+ <leafNode name="private-key">
+ <properties>
+ <help>Private key to use on that interface</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
+ </completionHelp>
+ </properties>
+ <defaultValue>default</defaultValue>
+ </leafNode>
+ <tagNode name="peer">
+ <properties>
+ <help>peer alias</help>
+ <constraint>
+ <regex>[^ ]{1,100}$</regex>
+ </constraint>
+ <constraintErrorMessage>peer alias too long (limit 100 characters)</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>disables peer</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="pubkey">
+ <properties>
+ <help>base64 encoded public key</help>
+ <constraint>
+ <regex>[0-9a-zA-Z\+/]{43}=$</regex>
+ </constraint>
+ <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="preshared-key">
+ <properties>
+ <help>base64 encoded preshared key</help>
+ <constraint>
+ <regex>[0-9a-zA-Z\+/]{43}=$</regex>
+ </constraint>
+ <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="allowed-ips">
+ <properties>
+ <help>IP addresses allowed to traverse the peer</help>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="address">
+ <properties>
+ <help>IP address of tunnel remote end</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to listen for incoming connections</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address to listen for incoming connections</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/port-number.xml.i>
+ <leafNode name="persistent-keepalive">
+ <properties>
+ <help>Interval to send keepalive messages</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Interval in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in
new file mode 100644
index 000000000..6f0ec9e71
--- /dev/null
+++ b/interface-definitions/interfaces-wireless.xml.in
@@ -0,0 +1,800 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="wireless" owner="${vyos_conf_scripts_dir}/interfaces-wireless.py">
+ <properties>
+ <help>Wireless (WiFi/WLAN) Network Interface</help>
+ <priority>400</priority>
+ <constraint>
+ <regex>^wlan[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Wireless interface must be named wlanN</constraintErrorMessage>
+ <valueHelp>
+ <format>wlanN</format>
+ <description>Wireless (WiFi/WLAN) interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6-dhcp.xml.i>
+ <node name="capabilities">
+ <properties>
+ <help>HT and VHT capabilities for your card</help>
+ </properties>
+ <children>
+ <node name="ht">
+ <properties>
+ <help>HT (High Throughput) settings</help>
+ </properties>
+ <children>
+ <leafNode name="40mhz-incapable">
+ <properties>
+ <help>40MHz intolerance, use 20MHz only!</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="auto-powersave">
+ <properties>
+ <help>Enable WMM-PS unscheduled automatic power aave delivery [U-APSD]</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="channel-set-width">
+ <properties>
+ <help>Supported channel set width</help>
+ <completionHelp>
+ <list>ht20 ht40+ ht40-</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ht20</format>
+ <description>Supported channel set width both 20 MHz only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ht40+</format>
+ <description>Supported channel set width both 20 MHz and 40 MHz with secondary channel above primary channel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ht40-</format>
+ <description>Supported channel set width both 20 MHz and 40 MHz with secondary channel below primary channel</description>
+ </valueHelp>
+ <constraint>
+ <regex>(ht20|ht40\+|ht40-)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="delayed-block-ack">
+ <properties>
+ <help>Enable HT-delayed block ack</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="dsss-cck-40">
+ <properties>
+ <help>Enable DSSS_CCK-40</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="greenfield">
+ <properties>
+ <help>Enable HT-greenfield</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ldpc">
+ <properties>
+ <help>Enable LDPC coding capability</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="lsig-protection">
+ <properties>
+ <help>Enable L-SIG TXOP protection capability</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="max-amsdu">
+ <properties>
+ <help>Set maximum A-MSDU length</help>
+ <completionHelp>
+ <list>3839 7935</list>
+ </completionHelp>
+ <valueHelp>
+ <format>3839</format>
+ <description>Set maximum A-MSDU length to 3839 octets</description>
+ </valueHelp>
+ <valueHelp>
+ <format>7935</format>
+ <description>Set maximum A-MSDU length to 7935 octets</description>
+ </valueHelp>
+ <constraint>
+ <regex>(3839|7935)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="short-gi">
+ <properties>
+ <help>Short GI capabilities</help>
+ <completionHelp>
+ <list>20 40</list>
+ </completionHelp>
+ <valueHelp>
+ <format>20</format>
+ <description>Short GI for 20 MHz</description>
+ </valueHelp>
+ <valueHelp>
+ <format>40</format>
+ <description>Short GI for 40 MHz</description>
+ </valueHelp>
+ <constraint>
+ <regex>(20|40)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="smps">
+ <properties>
+ <help>Spatial Multiplexing Power Save (SMPS) settings</help>
+ <completionHelp>
+ <list>static dynamic</list>
+ </completionHelp>
+ <valueHelp>
+ <format>static</format>
+ <description>STATIC Spatial Multiplexing (SM) Power Save</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dynamic</format>
+ <description>DYNAMIC Spatial Multiplexing (SM) Power Save</description>
+ </valueHelp>
+ <constraint>
+ <regex>(static|dynamic)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="stbc">
+ <properties>
+ <help>Support for sending and receiving PPDU using STBC (Space Time Block Coding)</help>
+ </properties>
+ <children>
+ <leafNode name="rx">
+ <properties>
+ <help>Enable receiving PPDU using STBC (Space Time Block Coding)</help>
+ <valueHelp>
+ <format>[1-3]+</format>
+ <description>Number of spacial streams that can use RX STBC</description>
+ </valueHelp>
+ <constraint>
+ <regex>[1-3]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid capability item</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="tx">
+ <properties>
+ <help>Enable sending PPDU using STBC (Space Time Block Coding)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="require-ht">
+ <properties>
+ <help>Require stations to support HT PHY (reject association if they do not)</help>
+ <completionHelp>
+ <script>echo If you reject non-HT, you also disable 802.11g</script>
+ </completionHelp>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="require-vht">
+ <properties>
+ <help>Require stations to support VHT PHY (reject association if they do not)</help>
+ <completionHelp>
+ <script>echo If you reject non-VHT, you also disable 802.11n</script>
+ </completionHelp>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="vht">
+ <properties>
+ <help>VHT (Very High Throughput) settings</help>
+ </properties>
+ <children>
+ <leafNode name="antenna-count">
+ <properties>
+ <help>Number of antennas on this card</help>
+ <valueHelp>
+ <format>1-8</format>
+ <description>Number of antennas for this card</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-8"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="antenna-pattern-fixed">
+ <properties>
+ <help>Set if antenna pattern does not change during the lifetime of an association</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="beamform">
+ <properties>
+ <help>Beamforming capabilities</help>
+ <completionHelp>
+ <list>single-user-beamformer single-user-beamformee multi-user-beamformer multi-user-beamformee</list>
+ </completionHelp>
+ <valueHelp>
+ <format>single-user-beamformer</format>
+ <description>Support for operation as single user beamformer</description>
+ </valueHelp>
+ <valueHelp>
+ <format>single-user-beamformee</format>
+ <description>Support for operation as single user beamformee</description>
+ </valueHelp>
+ <valueHelp>
+ <format>multi-user-beamformer</format>
+ <description>Support for operation as multi user beamformer</description>
+ </valueHelp>
+ <valueHelp>
+ <format>multi-user-beamformee</format>
+ <description>Support for operation as multi user beamformee</description>
+ </valueHelp>
+ <constraint>
+ <regex>(single-user-beamformer|single-user-beamformee|multi-user-beamformer|multi-user-beamformee)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="center-channel-freq">
+ <properties>
+ <help>VHT operating channel center frequency</help>
+ </properties>
+ <children>
+ <leafNode name="freq-1">
+ <properties>
+ <help>VHT operating channel center frequency - center freq 1 (for use with 80, 80+80 and 160 modes)</help>
+ <valueHelp>
+ <format>&lt;34-173&gt;</format>
+ <description>5Ghz (802.11 a/h/j/n/ac) center channel index (use 42 for primary 80MHz channel 36)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 34-173"/>
+ </constraint>
+ <constraintErrorMessage>Channel center value must be between 34 and 173</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="freq-2">
+ <properties>
+ <help>VHT operating channel center frequency - center freq 2 (for use with the 80+80 mode)</help>
+ <valueHelp>
+ <format>34-173</format>
+ <description>5Ghz (802.11 a/h/j/n/ac) center channel index (use 58 for primary 80MHz channel 52)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 34-173"/>
+ </constraint>
+ <constraintErrorMessage>Channel center value must be between 34 and 173</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="channel-set-width">
+ <properties>
+ <help>VHT operating Channel width</help>
+ <completionHelp>
+ <list>0 1 2 3</list>
+ </completionHelp>
+ <valueHelp>
+ <format>0</format>
+ <description>20 or 40 MHz channel width (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>1</format>
+ <description>80 MHz channel width</description>
+ </valueHelp>
+ <valueHelp>
+ <format>2</format>
+ <description>160 MHz channel width</description>
+ </valueHelp>
+ <valueHelp>
+ <format>3</format>
+ <description>80+80 MHz channel width</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-3"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="ldpc">
+ <properties>
+ <help>Enable LDPC (Low Density Parity Check) coding capability</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="link-adaptation">
+ <properties>
+ <help>VHT link adaptation capabilities</help>
+ <completionHelp>
+ <list>unsolicited both</list>
+ </completionHelp>
+ <valueHelp>
+ <format>unsolicited</format>
+ <description>Station provides only unsolicited VHT MFB</description>
+ </valueHelp>
+ <valueHelp>
+ <format>both</format>
+ <description>Station can provide VHT MFB in response to VHT MRQ and unsolicited VHT MFB</description>
+ </valueHelp>
+ <constraint>
+ <regex>(unsolicited|both)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid capability item</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="max-mpdu-exp">
+ <properties>
+ <help>Set the maximum length of A-MPDU pre-EOF padding that the station can receive</help>
+ <valueHelp>
+ <format>&lt;0-7&gt;</format>
+ <description>Maximum length of A-MPDU pre-EOF padding = 2 pow(13 + x) -1 octets</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-7"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="max-mpdu">
+ <properties>
+ <help>Increase Maximum MPDU length to 7991 or 11454 octets (otherwise: 3895 octets)</help>
+ <completionHelp>
+ <list>7991 11454</list>
+ </completionHelp>
+ <valueHelp>
+ <format>7991</format>
+ <description>ncrease Maximum MPDU length to 7991 octets</description>
+ </valueHelp>
+ <valueHelp>
+ <format>11454</format>
+ <description>ncrease Maximum MPDU length to 11454 octets</description>
+ </valueHelp>
+ <constraint>
+ <regex>(7991|11454)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="short-gi">
+ <properties>
+ <help>Short GI capabilities</help>
+ <completionHelp>
+ <list>80 160</list>
+ </completionHelp>
+ <valueHelp>
+ <format>80</format>
+ <description>Short GI for 80 MHz</description>
+ </valueHelp>
+ <valueHelp>
+ <format>160</format>
+ <description>Short GI for 160 MHz</description>
+ </valueHelp>
+ <constraint>
+ <regex>(80|160)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="stbc">
+ <properties>
+ <help>Support for sending and receiving PPDU using STBC (Space Time Block Coding)</help>
+ </properties>
+ <children>
+ <leafNode name="rx">
+ <properties>
+ <help>Enable receiving PPDU using STBC (Space Time Block Coding)</help>
+ <valueHelp>
+ <format>[1-4]+</format>
+ <description>Number of spacial streams that can use RX STBC</description>
+ </valueHelp>
+ <constraint>
+ <regex>[1-4]+</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid capability item</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="tx">
+ <properties>
+ <help>Enable sending PPDU using STBC (Space Time Block Coding)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="tx-powersave">
+ <properties>
+ <help>Enable VHT TXOP Power Save Mode</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="vht-cf">
+ <properties>
+ <help>Station supports receiving VHT variant HT Control field</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="channel">
+ <properties>
+ <help>Wireless radio channel (use 0 for ACS auto channel selection)</help>
+ <valueHelp>
+ <format>&lt;1-14&gt;</format>
+ <description>2.4Ghz (802.11 b/g/n) Channel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;0,34-173&gt;</format>
+ <description>5Ghz (802.11 a/h/j/n/ac) Channel</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-0 --range 1-14 --range 34-173"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/interface-description.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
+ <leafNode name="disable-broadcast-ssid">
+ <properties>
+ <help>Disable broadcast of SSID from access-point</help>
+ <valueless/>
+ </properties>
+ </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>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="ip">
+ <children>
+ #include <include/interface-arp-cache-timeout.xml.i>
+ #include <include/interface-disable-arp-filter.xml.i>
+ #include <include/interface-enable-arp-accept.xml.i>
+ #include <include/interface-enable-arp-announce.xml.i>
+ #include <include/interface-enable-arp-ignore.xml.i>
+ #include <include/interface-enable-proxy-arp.xml.i>
+ #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-hw-id.xml.i>
+ <leafNode name="isolate-stations">
+ <properties>
+ <help>Isolate stations on the AP so they cannot see each other</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ #include <include/interface-mac.xml.i>
+ <leafNode name="max-stations">
+ <properties>
+ <help>Maximum number of wireless radio stations. Excess stations will be rejected upon authentication request.</help>
+ <valueHelp>
+ <format>&lt;1-2007&gt;</format>
+ <description>Number of allowed stations</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-2007"/>
+ </constraint>
+ <constraintErrorMessage>Number of stations must be between 1 and 2007</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="mgmt-frame-protection">
+ <properties>
+ <help>Management Frame Protection (MFP) according to IEEE 802.11w</help>
+ <completionHelp>
+ <list>disabled optional required</list>
+ </completionHelp>
+ <valueHelp>
+ <format>disabled</format>
+ <description>no MFP (hostapd default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>optional</format>
+ <description>MFP optional</description>
+ </valueHelp>
+ <valueHelp>
+ <format>required</format>
+ <description>MFP enforced</description>
+ </valueHelp>
+ <constraint>
+ <regex>(disabled|optional|required)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mode">
+ <properties>
+ <help>Wireless radio mode</help>
+ <completionHelp>
+ <list>a b g n ac</list>
+ </completionHelp>
+ <valueHelp>
+ <format>a</format>
+ <description>802.11a - 54 Mbits/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>b</format>
+ <description>802.11b - 11 Mbits/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>g</format>
+ <description>802.11g - 54 Mbits/sec (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>n</format>
+ <description>802.11n - 600 Mbits/sec</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ac</format>
+ <description>802.11ac - 1300 Mbits/sec</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(a|b|g|n|ac)$</regex>
+ </constraint>
+ </properties>
+ <defaultValue>g</defaultValue>
+ </leafNode>
+ <leafNode name="physical-device">
+ <properties>
+ <help>Wireless physical device</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_wireless_phys.sh</script>
+ </completionHelp>
+ <constraint>
+ <validator name="wireless-phy"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="reduce-transmit-power">
+ <properties>
+ <help>Transmission power reduction in dBm</help>
+ <valueHelp>
+ <format>&lt;0-255&gt;</format>
+ <description>TX power reduction in dBm</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ <constraintErrorMessage>dBm value must be between 0 and 255</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="security">
+ <properties>
+ <help>Wireless security settings</help>
+ </properties>
+ <children>
+ <node name="wep">
+ <properties>
+ <help>Wired Equivalent Privacy (WEP) parameters</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>WEP encryption key</help>
+ <valueHelp>
+ <format>&lt;hexdigits&gt;</format>
+ <description>Wired Equivalent Privacy key</description>
+ </valueHelp>
+ <constraint>
+ <regex>([a-fA-F0-9]{10}|[a-fA-F0-9]{26}|[a-fA-F0-9]{32})</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid WEP key</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="wpa">
+ <properties>
+ <help>Wifi Protected Access (WPA) parameters</help>
+ </properties>
+ <children>
+ <leafNode name="cipher">
+ <properties>
+ <help>Cipher suite for WPA unicast packets</help>
+ <completionHelp>
+ <list>GCMP-256 GCMP CCMP-256 CCMP TKIP</list>
+ </completionHelp>
+ <valueHelp>
+ <format>GCMP-256</format>
+ <description>AES in Galois/counter mode with 256-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>GCMP</format>
+ <description>AES in Galois/counter mode with 128-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>CCMP-256</format>
+ <description>AES in Counter mode with CBC-MAC with 256-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>CCMP</format>
+ <description>AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0] (supported on all WPA2 APs)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>TKIP</format>
+ <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)$</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid cipher selection</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="group-cipher">
+ <properties>
+ <help>Cipher suite for WPA multicast and broadcast packets</help>
+ <completionHelp>
+ <list>GCMP-256 GCMP CCMP-256 CCMP TKIP</list>
+ </completionHelp>
+ <valueHelp>
+ <format>GCMP-256</format>
+ <description>AES in Galois/counter mode with 256-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>GCMP</format>
+ <description>AES in Galois/counter mode with 128-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>CCMP-256</format>
+ <description>AES in Counter mode with CBC-MAC with 256-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>CCMP</format>
+ <description>AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0] (supported on all WPA2 APs)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>TKIP</format>
+ <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)$</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid group cipher selection</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="mode">
+ <properties>
+ <help>WPA mode</help>
+ <completionHelp>
+ <list>wpa wpa2 both</list>
+ </completionHelp>
+ <valueHelp>
+ <format>wpa</format>
+ <description>WPA (IEEE 802.11i/D3.0)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>wpa2</format>
+ <description>WPA2 (full IEEE 802.11i/RSN)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>both</format>
+ <description>Allow both WPA and WPA2</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(wpa|wpa2|both)$</regex>
+ </constraint>
+ <constraintErrorMessage>Unknown WPA mode</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="passphrase">
+ <properties>
+ <help>WPA personal shared pass phrase. If you are
+ using special characters in the WPA passphrase then single
+ quotes are required.</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Passphrase of at least 8 but not more than 63 printable characters</description>
+ </valueHelp>
+ <constraint>
+ <regex>.{8,63}$</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid WPA pass phrase, must be 8 to 63 printable characters!</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ #include <include/radius-server.xml.i>
+ <node name="radius">
+ <children>
+ <tagNode name="server">
+ <children>
+ <leafNode name="accounting">
+ <properties>
+ <help>Enable RADIUS server to receive accounting info</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="ssid">
+ <properties>
+ <help>Wireless access-point service set identifier (SSID)</help>
+ <constraint>
+ <regex>.{1,32}$</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid SSID</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Wireless device type for this interface</help>
+ <completionHelp>
+ <list>access-point station monitor</list>
+ </completionHelp>
+ <valueHelp>
+ <format>access-point</format>
+ <description>Access-point forwards packets between other nodes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>station</format>
+ <description>Connects to another access point</description>
+ </valueHelp>
+ <valueHelp>
+ <format>monitor</format>
+ <description>Passively monitor all packets on the frequency/channel</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(access-point|station|monitor)$</regex>
+ </constraint>
+ <constraintErrorMessage>Type must be access-point, station or monitor</constraintErrorMessage>
+ </properties>
+ <defaultValue>monitor</defaultValue>
+ </leafNode>
+ #include <include/vif.xml.i>
+ #include <include/vif-s.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="system">
+ <children>
+ <leafNode name="wifi-regulatory-domain" owner="${vyos_conf_scripts_dir}/system-wifi-regdom.py">
+ <properties>
+ <help>Wireless regulatory domain (mandatory)</help>
+ <priority>305</priority>
+ <completionHelp>
+ <list>US EU JP DE UK CN</list>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;code%gt;</format>
+ <description>Country code (ISO/IEC 3166-1)</description>
+ </valueHelp>
+ <constraint>
+ <regex>[A-Z][A-Z]$</regex>
+ </constraint>
+ <constraintErrorMessage>invalid country code</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-wirelessmodem.xml.in b/interface-definitions/interfaces-wirelessmodem.xml.in
new file mode 100644
index 000000000..d375b808d
--- /dev/null
+++ b/interface-definitions/interfaces-wirelessmodem.xml.in
@@ -0,0 +1,93 @@
+<?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>
+ <defaultValue>10</defaultValue>
+ </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>Serial device </help>
+ <completionHelp>
+ <script>ls -1 /dev | grep ttyS</script>
+ <script>if [ -d /dev/serial/by-bus ]; then ls -1 /dev/serial/by-bus; fi</script>
+ </completionHelp>
+ <valueHelp>
+ <format>ttySXX</format>
+ <description>TTY device name, regular serial port</description>
+ </valueHelp>
+ <valueHelp>
+ <format>usbNbXpY</format>
+ <description>TTY device name, USB based</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(ttyS[0-9]+|usb[0-9]+b.*)$</regex>
+ </constraint>
+ </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/ipsec-settings.xml.in b/interface-definitions/ipsec-settings.xml.in
new file mode 100644
index 000000000..bc54baa27
--- /dev/null
+++ b/interface-definitions/ipsec-settings.xml.in
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="vpn">
+ <children>
+ <node name="ipsec">
+ <children>
+ <node name="options" owner="${vyos_conf_scripts_dir}/ipsec-settings.py">
+ <properties>
+ <help>Global IPsec settings</help>
+ </properties>
+ <children>
+ <leafNode name="disable-route-autoinstall">
+ <properties>
+ <valueless/>
+ <help>Do not automatically install routes to remote networks</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/lldp.xml.in b/interface-definitions/lldp.xml.in
new file mode 100644
index 000000000..8f6629d81
--- /dev/null
+++ b/interface-definitions/lldp.xml.in
@@ -0,0 +1,191 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="lldp" owner="${vyos_conf_scripts_dir}/lldp.py">
+ <properties>
+ <help>LLDP settings</help>
+ <priority>985</priority>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Location data for interface</help>
+ <valueHelp>
+ <format>all</format>
+ <description>Location data all interfaces</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;intf&gt;</format>
+ <description>Location data for a specific interface</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyatta_sbindir}/vyatta-interfaces.pl --show all</script>
+ <list>all</list>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable lldp on this interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="location">
+ <properties>
+ <help>LLDP-MED location data [REQUIRED]</help>
+ </properties>
+ <children>
+ <node name="coordinate-based">
+ <properties>
+ <help>Coordinate based location</help>
+ </properties>
+ <children>
+ <leafNode name="altitude">
+ <properties>
+ <help>Altitude in meters</help>
+ <valueHelp>
+ <format>[+-]&lt;meters&gt;</format>
+ <description>Altitude in meters</description>
+ </valueHelp>
+ <constraintErrorMessage>Altitude should be a positive or negative number</constraintErrorMessage>
+ <constraint>
+ <validator name="numeric"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="datum">
+ <properties>
+ <help>Coordinate datum type</help>
+ <valueHelp>
+ <format>WGS84</format>
+ <description>WGS84 (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>NAD83</format>
+ <description>NAD83</description>
+ </valueHelp>
+ <valueHelp>
+ <format>MLLW</format>
+ <description>NAD83/MLLW</description>
+ </valueHelp>
+ <completionHelp>
+ <list>WGS84 NAD83 MLLW</list>
+ </completionHelp>
+ <constraintErrorMessage>Datum should be WGS84, NAD83, or MLLW</constraintErrorMessage>
+ <constraint>
+ <regex>^(WGS84|NAD83|MLLW)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="latitude">
+ <properties>
+ <help>Latitude [REQUIRED]</help>
+ <valueHelp>
+ <format>&lt;latitude&gt;</format>
+ <description>Latitude (example "37.524449N")</description>
+ </valueHelp>
+ <constraintErrorMessage>Latitude should be a number followed by S or N</constraintErrorMessage>
+ <constraint>
+ <regex>(\d+)(\.\d+)?[nNsS]$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="longitude">
+ <properties>
+ <help>Longitude [REQUIRED]</help>
+ <valueHelp>
+ <format>&lt;longitude&gt;</format>
+ <description>Longitude (example "122.267255W")</description>
+ </valueHelp>
+ <constraintErrorMessage>Longiture should be a number followed by E or W</constraintErrorMessage>
+ <constraint>
+ <regex>(\d+)(\.\d+)?[eEwW]$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="elin">
+ <properties>
+ <help>ECS ELIN (Emergency location identifier number)</help>
+ <valueHelp>
+ <format>0-9999999999</format>
+ <description>Emergency Call Service ELIN number (between 10-25 numbers)</description>
+ </valueHelp>
+ <constraint>
+ <regex>[0-9]{10,25}$</regex>
+ </constraint>
+ <constraintErrorMessage>ELIN number must be between 10-25 numbers</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <node name="legacy-protocols">
+ <properties>
+ <help>Legacy (vendor specific) protocols</help>
+ </properties>
+ <children>
+ <leafNode name="cdp">
+ <properties>
+ <help>Listen for CDP for Cisco routers/switches</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="edp">
+ <properties>
+ <help>Listen for EDP for Extreme routers/switches</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="fdp">
+ <properties>
+ <help>Listen for FDP for Foundry routers/switches</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="sonmp">
+ <properties>
+ <help>Listen for SONMP for Nortel routers/switches</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="management-address">
+ <properties>
+ <help>Management IP Address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 Management Address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 Management Address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="snmp">
+ <properties>
+ <help>SNMP parameters for LLDP</help>
+ </properties>
+ <children>
+ <leafNode name="enable">
+ <properties>
+ <help>Enable SNMP queries of the LLDP database</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in
new file mode 100644
index 000000000..8a14f4d25
--- /dev/null
+++ b/interface-definitions/nat.xml.in
@@ -0,0 +1,180 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="nat" owner="${vyos_conf_scripts_dir}/nat.py">
+ <properties>
+ <help>Network Address Translation (NAT) parameters</help>
+ <priority>220</priority>
+ </properties>
+ <children>
+ <node name="destination">
+ <properties>
+ <help>Destination NAT settings</help>
+ </properties>
+ <children>
+ #include <include/nat-rule.xml.i>
+ <tagNode name="rule">
+ <children>
+ <leafNode name="inbound-interface">
+ <properties>
+ <help>Inbound interface of NAT traffic</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <node name="translation">
+ <properties>
+ <help>Inside NAT IP (destination NAT only)</help>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address, subnet, or range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4range</format>
+ <description>IPv4 address range to match</description>
+ </valueHelp>
+ <!-- TODO: add general iptables constraint script -->
+ </properties>
+ </leafNode>
+ #include <include/nat-translation-port.xml.i>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="nptv6">
+ <properties>
+ <help>IPv6-to-IPv6 Network Prefix Translation Settings</help>
+ </properties>
+ <children>
+ <tagNode name="rule">
+ <properties>
+ <help>NPTv6 rule number</help>
+ <valueHelp>
+ <format>1-999999</format>
+ <description>Number for this rule</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="description">
+ <properties>
+ <help>Rule description</help>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable NAT rule</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ #include <include/nat-interface.xml.i>
+ <node name="source">
+ <properties>
+ <help>IPv6 source prefix options</help>
+ </properties>
+ <children>
+ <leafNode name="prefix">
+ <properties>
+ <help>IPv6 prefix to be translated</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="translation">
+ <properties>
+ <help>Translated IPv6 prefix options</help>
+ </properties>
+ <children>
+ <leafNode name="prefix">
+ <properties>
+ <help>IPv6 prefix to translate to</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="source">
+ <properties>
+ <help>Source NAT settings</help>
+ </properties>
+ <children>
+ #include <include/nat-rule.xml.i>
+ <tagNode name="rule">
+ <children>
+ #include <include/nat-interface.xml.i>
+ <node name="translation">
+ <properties>
+ <help>Outside NAT IP (source NAT only)</help>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address, subnet, or range</help>
+ <completionHelp>
+ <list>masquerade</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4range</format>
+ <description>IPv4 address range to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>masquerade</format>
+ <description>NAT to the primary address of outbound-interface</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ <validator name="ipv4-address"/>
+ <validator name="ipv4-range"/>
+ <regex>(masquerade)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/nat-translation-port.xml.i>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/ntp.xml.in b/interface-definitions/ntp.xml.in
new file mode 100644
index 000000000..485487a42
--- /dev/null
+++ b/interface-definitions/ntp.xml.in
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<!-- NTP configuration -->
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="ntp" owner="${vyos_conf_scripts_dir}/ntp.py">
+ <properties>
+ <help>Network Time Protocol (NTP) configuration</help>
+ <priority>400</priority>
+ </properties>
+ <children>
+ <tagNode name="server">
+ <properties>
+ <help>Network Time Protocol (NTP) server</help>
+ </properties>
+ <children>
+ <leafNode name="noselect">
+ <properties>
+ <help>Marks the server as unused</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="preempt">
+ <properties>
+ <help>Specifies the association as preemptable rather than the default persistent</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="prefer">
+ <properties>
+ <help>Marks the server as preferred</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="allow-clients">
+ <properties>
+ <help>Network Time Protocol (NTP) server options</help>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IP address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="listen-address">
+ <properties>
+ <help>Addresses to listen for NTP queries</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Network Time Protocol (NTP) IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Network Time Protocol (NTP) IPv6 address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/interface-vrf.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/protocols-bfd.xml.in b/interface-definitions/protocols-bfd.xml.in
new file mode 100644
index 000000000..8900e7955
--- /dev/null
+++ b/interface-definitions/protocols-bfd.xml.in
@@ -0,0 +1,140 @@
+<?xml version="1.0"?>
+<!-- Bidirectional Forwarding Detection (BFD) configuration -->
+<interfaceDefinition>
+ <node name="protocols">
+ <children>
+ <node name="bfd" owner="${vyos_conf_scripts_dir}/protocols_bfd.py">
+ <properties>
+ <help>Bidirectional Forwarding Detection (BFD)</help>
+ <priority>820</priority>
+ </properties>
+ <children>
+ <tagNode name="peer">
+ <properties>
+ <help>Configures a new BFD peer to listen and talk to</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>BFD peer IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>BFD peer IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <node name="source">
+ <properties>
+ <help>Bind listener to specified interface/address, mandatory for IPv6</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Local interface to bind our peer listener to</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="address">
+ <properties>
+ <help>Local address to bind our peer listener to</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Local IPv4 address used to connect to the peer</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Local IPv6 address used to connect to the peer</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="interval">
+ <properties>
+ <help>Configure timer intervals</help>
+ </properties>
+ <children>
+ <leafNode name="receive">
+ <properties>
+ <help>Minimum interval of receiving control packets</help>
+ <valueHelp>
+ <format>10-60000</format>
+ <description>Interval in milliseconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 10-60000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="transmit">
+ <properties>
+ <help>Minimum interval of transmitting control packets</help>
+ <valueHelp>
+ <format>10-60000</format>
+ <description>Interval in milliseconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 10-60000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="multiplier">
+ <properties>
+ <help>Multiplier to determine packet loss</help>
+ <valueHelp>
+ <format>2-255</format>
+ <description>Remote transmission interval will be multiplied by this value</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 2-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="echo-interval">
+ <properties>
+ <help>Echo receive transmission interval</help>
+ <valueHelp>
+ <format>10-60000</format>
+ <description>The minimal echo receive transmission interval that this system is capable of handling</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 10-60000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="shutdown">
+ <properties>
+ <help>Disable this peer</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="multihop">
+ <properties>
+ <help>Allow this BFD peer to not be directly connected</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="echo-mode">
+ <properties>
+ <help>Enables the echo transmission mode</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/protocols-bgp.xml.in b/interface-definitions/protocols-bgp.xml.in
new file mode 100644
index 000000000..3a4600753
--- /dev/null
+++ b/interface-definitions/protocols-bgp.xml.in
@@ -0,0 +1,1205 @@
+<?xml version="1.0"?>
+<!-- Border Gateway Protocol (BGP) configuration -->
+<interfaceDefinition>
+ <node name="protocols">
+ <children>
+ <tagNode name="nbgp" owner="${vyos_conf_scripts_dir}/protocols_bgp.py">
+ <properties>
+ <help>Border Gateway Protocol (BGP) parameters</help>
+ <valueHelp>
+ <format>&lt;1-4294967294&gt;</format>
+ <description>AS number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967294"/>
+ </constraint>
+ <priority>820</priority>
+ </properties>
+ <children>
+ <node name="address-family">
+ <properties>
+ <help>BGP address-family parameters</help>
+ </properties>
+ <children>
+ <node name="ipv4-unicast">
+ <properties>
+ <help>IPv4 BGP settings</help>
+ </properties>
+ <children>
+ <tagNode name="aggregate-address">
+ <properties>
+ <help>BGP aggregate network</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>BGP aggregate network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/bgp-afi-aggregate-address.xml.i>
+ </children>
+ </tagNode>
+ <tagNode name="network">
+ <properties>
+ <help>BGP network</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>BGP network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="backdoor">
+ <properties>
+ <help>Network as a backdoor route</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="route-map">
+ <properties>
+ <help>Route-map to modify route attributes</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="redistribute">
+ <properties>
+ <help>Redistribute routes from other protocols into BGP</help>
+ </properties>
+ <children>
+ <node name="connected">
+ <properties>
+ <help>Redistribute connected routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp-afi-redistribute-metric-route-map.xml.i>
+ </children>
+ </node>
+ <node name="kernel">
+ <properties>
+ <help>Redistribute kernel routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp-afi-redistribute-metric-route-map.xml.i>
+ </children>
+ </node>
+ <node name="ospf">
+ <properties>
+ <help>Redistribute OSPF routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp-afi-redistribute-metric-route-map.xml.i>
+ </children>
+ </node>
+ <node name="rip">
+ <properties>
+ <help>Redistribute RIP routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp-afi-redistribute-metric-route-map.xml.i>
+ </children>
+ </node>
+ <node name="static">
+ <properties>
+ <help>Redistribute static routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp-afi-redistribute-metric-route-map.xml.i>
+ </children>
+ </node>
+ <leafNode name="table">
+ <properties>
+ <help>Redistribute non-main Kernel Routing Table</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="ipv6-unicast">
+ <properties>
+ <help>IPv6 BGP settings</help>
+ </properties>
+ <children>
+ <tagNode name="aggregate-address">
+ <properties>
+ <help>BGP aggregate network</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>Aggregate network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/bgp-afi-aggregate-address.xml.i>
+ </children>
+ </tagNode>
+ <tagNode name="network">
+ <properties>
+ <help>BGP network</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>Aggregate network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="path-limit">
+ <properties>
+ <help>AS-path hopcount limit</help>
+ <valueHelp>
+ <format>&lt;0-255&gt;</format>
+ <description>AS path hop count limit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="route-map">
+ <properties>
+ <help>Route-map to modify route attributes</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="redistribute">
+ <properties>
+ <help>Redistribute routes from other protocols into BGP</help>
+ </properties>
+ <children>
+ <node name="connected">
+ <properties>
+ <help>Redistribute connected routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp-afi-redistribute-metric-route-map.xml.i>
+ </children>
+ </node>
+ <node name="kernel">
+ <properties>
+ <help>Redistribute kernel routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp-afi-redistribute-metric-route-map.xml.i>
+ </children>
+ </node>
+ <node name="ospf">
+ <properties>
+ <help>Redistribute OSPF routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp-afi-redistribute-metric-route-map.xml.i>
+ </children>
+ </node>
+ <node name="rip">
+ <properties>
+ <help>Redistribute RIP routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp-afi-redistribute-metric-route-map.xml.i>
+ </children>
+ </node>
+ <node name="static">
+ <properties>
+ <help>Redistribute static routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp-afi-redistribute-metric-route-map.xml.i>
+ </children>
+ </node>
+ <leafNode name="table">
+ <properties>
+ <help>Redistribute non-main Kernel Routing Table</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="maximum-paths">
+ <properties>
+ <help>BGP multipaths</help>
+ </properties>
+ <children>
+ <leafNode name="ebgp">
+ <properties>
+ <help>Maximum ebgp multipaths</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>EBGP multipaths</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="ibgp">
+ <properties>
+ <help>Maximum ibgp multipaths</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>EBGP multipaths</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="neighbor">
+ <properties>
+ <help>BGP neighbor</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>BGP neighbor IP address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>BGP neighbor IPv6 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;interface&gt;</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex>
+ </constraint>
+ </properties>
+ <children>
+ <node name="address-family">
+ <properties>
+ <help>Parameters relating to IPv4 or IPv6 routes</help>
+ </properties>
+ <children>
+ #include <include/bgp-neighbor-afi-ipv4-unicast.xml.i>
+ #include <include/bgp-neighbor-afi-ipv6-unicast.xml.i>
+ </children>
+ </node>
+ <leafNode name="advertisement-interval">
+ <properties>
+ <help>Minimum interval for sending routing updates</help>
+ <valueHelp>
+ <format>&lt;0-600&gt;</format>
+ <description>Advertisement interval in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-600"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="bfd">
+ <properties>
+ <help>Enable Bidirectional Forwarding Detection (BFD) support</help>
+ </properties>
+ <children>
+ <leafNode name="check-control-plane-failure">
+ <properties>
+ <help>Allow to write CBIT independence in BFD outgoing packets and read both C-BIT value of BFD and lookup BGP peer status</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="capability">
+ <properties>
+ <help>Advertise capabilities to this neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="dynamic">
+ <properties>
+ <help>Advertise dynamic capability to this neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="extended-nexthop">
+ <properties>
+ <help>Advertise extended-nexthop capability to this neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="description">
+ <properties>
+ <help>Description for this neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-capability-negotiation">
+ <properties>
+ <help>Disable capability negotiation with this neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-connected-check">
+ <properties>
+ <help>Disable check to see if eBGP peer address is a connected route</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="disable-send-community">
+ <properties>
+ <help>Disable sending community attributes to this neighbor (IPv4)</help>
+ </properties>
+ <children>
+ <leafNode name="extended">
+ <properties>
+ <help>Disable sending extended community attributes to this neighbor (IPv4)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="standard">
+ <properties>
+ <help>Disable sending standard community attributes to this neighbor (IPv4)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="ebgp-multihop">
+ <properties>
+ <help>Allow this EBGP neighbor to not be on a directly connected network</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>Number of hops</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="interface">
+ <properties>
+ <help>Interface parameters</help>
+ </properties>
+ <children>
+ <leafNode name="peer-group">
+ <properties>
+ <help>Peer group for this peer</help>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-as">
+ <properties>
+ <help>Neighbor BGP AS number [REQUIRED]</help>
+ <completionHelp>
+ <list>external internal</list>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-4294967294&gt;</format>
+ <description>Neighbor AS number</description>
+ </valueHelp>
+ <valueHelp>
+ <format>external</format>
+ <description>Any AS different from the local AS</description>
+ </valueHelp>
+ <valueHelp>
+ <format>internal</format>
+ <description>Neighbor AS number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967294"/>
+ <regex>(external|internal)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid ASN value</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="v6only">
+ <properties>
+ <help>Enable BGP with v6 link-local only</help>
+ </properties>
+ <children>
+ <leafNode name="peer-group">
+ <properties>
+ <help>Peer group for this peer</help>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-as">
+ <properties>
+ <help>Neighbor BGP AS number [REQUIRED]</help>
+ <completionHelp>
+ <list>external internal</list>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-4294967294&gt;</format>
+ <description>Neighbor AS number</description>
+ </valueHelp>
+ <valueHelp>
+ <format>external</format>
+ <description>Any AS different from the local AS</description>
+ </valueHelp>
+ <valueHelp>
+ <format>internal</format>
+ <description>Neighbor AS number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967294"/>
+ <regex>(external|internal)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid ASN value</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <tagNode name="local-as">
+ <properties>
+ <help>Local AS number</help>
+ <valueHelp>
+ <format>&lt;1-4294967294&gt;</format>
+ <description>Local AS number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967294"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="no-prepend">
+ <properties>
+ <help>Disable prepending local-as to updates from EBGP peers</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="override-capability">
+ <properties>
+ <help>Ignore capability negotiation with specified neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="passive">
+ <properties>
+ <help>Do not initiate a session with this neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>BGP MD5 password</help>
+ </properties>
+ </leafNode>
+ <leafNode name="peer-group">
+ <properties>
+ <help>IPv4 peer group for this peer</help>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Neighbor BGP port</help>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Neighbor BGP port number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-as">
+ <properties>
+ <help>Neighbor BGP AS number [REQUIRED]</help>
+ <completionHelp>
+ <list>external internal</list>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-4294967294&gt;</format>
+ <description>Neighbor AS number</description>
+ </valueHelp>
+ <valueHelp>
+ <format>external</format>
+ <description>Any AS different from the local AS</description>
+ </valueHelp>
+ <valueHelp>
+ <format>internal</format>
+ <description>Neighbor AS number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967294"/>
+ <regex>(external|internal)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid ASN value</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="shutdown">
+ <properties>
+ <help>Administratively shut down neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="strict-capability-match">
+ <properties>
+ <help>Enable strict capability negotiation</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="timers">
+ <properties>
+ <help>Neighbor timers</help>
+ </properties>
+ <children>
+ <leafNode name="connect">
+ <properties>
+ <help>BGP connect timer for this neighbor</help>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Connect timer in seconds</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0</format>
+ <description>Disable connect timer</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="holdtime">
+ <properties>
+ <help>BGP hold timer for this neighbor</help>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Hold timer in seconds</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0</format>
+ <description>Hold timer disabled</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="keepalive">
+ <properties>
+ <help>BGP keepalive interval for this neighbor</help>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Keepalive interval in seconds (default 60)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="ttl-security">
+ <properties>
+ <help>Ttl security mechanism for this BGP peer</help>
+ </properties>
+ <children>
+ <leafNode name="hops">
+ <properties>
+ <help>Number of the maximum number of hops to the BGP peer</help>
+ <valueHelp>
+ <format>&lt;1-254&gt;</format>
+ <description>Number of hops</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-254"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="update-source">
+ <!-- Need to check format interfaces -->
+ <properties>
+ <help>Source IP of routing updates</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IP address of route source</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;interface&gt;</format>
+ <description>Interface as route source</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="parameters">
+ <properties>
+ <help>BGP parameters</help>
+ </properties>
+ <children>
+ <leafNode name="always-compare-med">
+ <properties>
+ <help>Always compare MEDs from different neighbors</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="bestpath">
+ <properties>
+ <help>Default bestpath selection mechanism</help>
+ </properties>
+ <children>
+ <node name="as-path">
+ <properties>
+ <help>AS-path attribute comparison parameters</help>
+ </properties>
+ <children>
+ <leafNode name="confed">
+ <properties>
+ <help>Compare AS-path lengths including confederation sets and sequences</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ignore">
+ <properties>
+ <help>Ignore AS-path length in selecting a route</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="multipath-relax">
+ <properties>
+ <help>Allow load sharing across routes that have different AS paths (but same length)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="compare-routerid">
+ <properties>
+ <help>Compare the router-id for identical EBGP paths</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="med">
+ <properties>
+ <help>MED attribute comparison parameters</help>
+ </properties>
+ <children>
+ <leafNode name="confed">
+ <properties>
+ <help>Compare MEDs among confederation paths</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="missing-as-worst">
+ <properties>
+ <help>Treat missing route as a MED as the least preferred one</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="cluster-id">
+ <properties>
+ <help>Route-reflector cluster-id</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Route-reflector cluster-id</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="confederation">
+ <properties>
+ <help>AS confederation parameters</help>
+ </properties>
+ <children>
+ <leafNode name="identifier">
+ <properties>
+ <help>Confederation AS identifier [REQUIRED]</help>
+ <valueHelp>
+ <format>&lt;1-4294967294&gt;</format>
+ <description>Confederation AS id</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967294"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="peers">
+ <properties>
+ <help>Peer ASs in the BGP confederation</help>
+ <valueHelp>
+ <format>&lt;1-4294967294&gt;</format>
+ <description>Peer AS number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967294"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="dampening">
+ <properties>
+ <help>Enable route-flap dampening</help>
+ </properties>
+ <children>
+ <leafNode name="half-life">
+ <properties>
+ <help>Half-life time for dampening [REQUIRED]</help>
+ <valueHelp>
+ <format>&lt;1-45&gt;</format>
+ <description>Half-life penalty in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-45"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="max-suppress-time">
+ <properties>
+ <help>Maximum duration to suppress a stable route [REQUIRED]</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>Maximum suppress duration in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="re-use">
+ <properties>
+ <help>Time to start reusing a route [REQUIRED]</help>
+ <valueHelp>
+ <format>&lt;1-20000&gt;</format>
+ <description>Re-use time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-20000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="start-suppress-time">
+ <properties>
+ <help>When to start suppressing a route [REQUIRED]</help>
+ <valueHelp>
+ <format>&lt;1-20000&gt;</format>
+ <description>Start-suppress-time</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-20000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="default">
+ <properties>
+ <help>BGP defaults</help>
+ </properties>
+ <children>
+ <leafNode name="local-pref">
+ <properties>
+ <help>Default local preference</help>
+ <valueHelp>
+ <format>&lt;0-4294967295&gt;</format>
+ <description>Local preference</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="no-ipv4-unicast">
+ <properties>
+ <help>Deactivate IPv4 unicast for a peer by default</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="deterministic-med">
+ <properties>
+ <help>Compare MEDs between different peers in the same AS</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="distance">
+ <properties>
+ <help>Administratives distances for BGP routes</help>
+ </properties>
+ <children>
+ <node name="global">
+ <properties>
+ <help>Global administratives distances for BGP routes</help>
+ </properties>
+ <children>
+ <leafNode name="external">
+ <properties>
+ <help>Administrative distance for external BGP routes</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>Administrative distance for external BGP routes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="internal">
+ <properties>
+ <help>Administrative distance for internal BGP routes</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>Administrative distance for internal BGP routes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="local">
+ <properties>
+ <help>Administrative distance for local BGP routes</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>Administrative distance for internal BGP routes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="prefix">
+ <properties>
+ <help>Administrative distance for a specific BGP prefix</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>Administrative distance for a specific BGP prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="distance">
+ <properties>
+ <help>Administrative distance for prefix</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>Administrative distance for external BGP routes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <leafNode name="enforce-first-as">
+ <properties>
+ <help>Require first AS in the path to match peer AS number</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="graceful-restart">
+ <properties>
+ <help>Graceful restart capability parameters</help>
+ </properties>
+ <children>
+ <leafNode name="stalepath-time">
+ <properties>
+ <help>Maximum time to hold onto restarting neighbors stale paths</help>
+ <valueHelp>
+ <format>&lt;1-3600&gt;</format>
+ <description>Hold time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-3600"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="log-neighbor-changes">
+ <properties>
+ <help>Log neighbor up/down changes and reset reason</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="network-import-check">
+ <properties>
+ <help>Enable IGP route check for network statements</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="no-client-to-client-reflection">
+ <properties>
+ <help>Disable client to client route reflection</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="no-fast-external-failover">
+ <properties>
+ <help>Disable immediate session reset on peer link down event</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="router-id">
+ <properties>
+ <help>BGP router id</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>BGP router id</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="peer-group">
+ <properties>
+ <help>BGP peer-group</help>
+ </properties>
+ <children>
+ <node name="address-family">
+ <properties>
+ <help>BGP peer-group address-family parameters</help>
+ </properties>
+ <children>
+ #include <include/bgp-peer-group-afi-ipv4-unicast.xml.i>
+ #include <include/bgp-peer-group-afi-ipv6-unicast.xml.i>
+ </children>
+ </node>
+ <leafNode name="bfd">
+ <properties>
+ <help>Enable Bidirectional Forwarding Detection (BFD) support</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="capability">
+ <properties>
+ <help>Advertise capabilities to this peer-group</help>
+ </properties>
+ <children>
+ <leafNode name="dynamic">
+ <properties>
+ <help>Advertise dynamic capability to this peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="extended-nexthop">
+ <properties>
+ <help>Advertise extended-nexthop capability to this neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="description">
+ <properties>
+ <help>Description for this peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-capability-negotiation">
+ <properties>
+ <help>Disable capability negotiation with this peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-connected-check">
+ <properties>
+ <help>Disable check to see if eBGP peer address is a connected route</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ebgp-multihop">
+ <properties>
+ <help>Allow this EBGP peer-group to not be on a directly connected network</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>Number of hops</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="local-as">
+ <properties>
+ <help>Local AS number [REQUIRED]</help>
+ <valueHelp>
+ <format>&lt;1-4294967294&gt;</format>
+ <description>Local AS number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967294"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="no-prepend">
+ <properties>
+ <help>Disable prepending local-as to updates from EBGP peers</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="override-capability">
+ <properties>
+ <help>Ignore capability negotiation with specified peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="passive">
+ <properties>
+ <help>Do not intiate a session with this peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>BGP MD5 password</help>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-as">
+ <properties>
+ <help>Neighbor BGP AS number [REQUIRED]</help>
+ <completionHelp>
+ <list>external internal</list>
+ </completionHelp>
+ <valueHelp>
+ <format>&lt;1-4294967294&gt;</format>
+ <description>Neighbor AS number</description>
+ </valueHelp>
+ <valueHelp>
+ <format>external</format>
+ <description>Any AS different from the local AS</description>
+ </valueHelp>
+ <valueHelp>
+ <format>internal</format>
+ <description>Neighbor AS number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967294"/>
+ <regex>(external|internal)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid ASN value</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="shutdown">
+ <properties>
+ <help>Administratively shut down peer-group</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="ttl-security">
+ <properties>
+ <help>Ttl security mechanism</help>
+ </properties>
+ <children>
+ <leafNode name="hops">
+ <properties>
+ <help>Number of the maximum number of hops to the BGP peer</help>
+ <valueHelp>
+ <format>&lt;1-254&gt;</format>
+ <description>Number of hops</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-254"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="update-source">
+ <!-- Need to check format interfaces -->
+ <properties>
+ <help>Source IP of routing updates</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IP address of route source</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;interface&gt;</format>
+ <description>Interface as route source</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="route-map">
+ <properties>
+ <help>Filter routes installed in local route map</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <node name="timers">
+ <properties>
+ <help>BGP protocol timers</help>
+ </properties>
+ <children>
+ <leafNode name="holdtime">
+ <properties>
+ <help>BGP holdtime interval</help>
+ <valueHelp>
+ <format>&lt;4-65535&gt;</format>
+ <description>Hold-time in seconds (default 180)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0</format>
+ <description>Do not hold routes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="keepalive">
+ <properties>
+ <help>Keepalive interval</help>
+ <valueHelp>
+ <format>&lt;1-65535&gt;</format>
+ <description>Keep-alive time in seconds (default 60)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </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-isis.xml.in b/interface-definitions/protocols-isis.xml.in
new file mode 100644
index 000000000..988231108
--- /dev/null
+++ b/interface-definitions/protocols-isis.xml.in
@@ -0,0 +1,552 @@
+<?xml version="1.0"?>
+<!-- Protocol IS-IS configuration -->
+<interfaceDefinition>
+ <node name="protocols">
+ <children>
+ <tagNode name="isis" owner="${vyos_conf_scripts_dir}/protocols_isis.py">
+ <properties>
+ <help>Intermediate System to Intermediate System (ISIS)</help>
+ <valueHelp>
+ <format>text(TAG)</format>
+ <description>ISO Routing area tag</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <node name="area-password">
+ <properties>
+ <help>Configure the authentication password for an area</help>
+ </properties>
+ <children>
+ <leafNode name="plaintext-password">
+ <properties>
+ <help>Plain-text authentication type</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Level-wide password</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="md5">
+ <properties>
+ <help>MD5 authentication type</help>
+ <valueHelp>
+ <format>&lt;md5&gt;</format>
+ <description>Level-wide password</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="default-information">
+ <properties>
+ <help>Control distribution of default information</help>
+ </properties>
+ <children>
+ <node name="originate">
+ <properties>
+ <help>Distribute a default route</help>
+ </properties>
+ <children>
+ <node name="ipv4">
+ <properties>
+ <help>Distribute default route for IPv4</help>
+ </properties>
+ <children>
+ <leafNode name="level-1">
+ <properties>
+ <help>Distribute default route into level-1</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="level-2">
+ <properties>
+ <help>Distribute default route into level-2</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="ipv6">
+ <properties>
+ <help>Distribute default route for IPv6</help>
+ </properties>
+ <children>
+ <leafNode name="level-1">
+ <properties>
+ <help>Distribute default route into level-1</help>
+ <completionHelp>
+ <list>always</list>
+ </completionHelp>
+ <valueHelp>
+ <format>always</format>
+ <description>Always advertise default route</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="level-2">
+ <properties>
+ <help>Distribute default route into level-2</help>
+ <completionHelp>
+ <list>always</list>
+ </completionHelp>
+ <valueHelp>
+ <format>always</format>
+ <description>Always advertise default route</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="domain-password">
+ <properties>
+ <help>Set the authentication password for a routing domain</help>
+ </properties>
+ <children>
+ <leafNode name="plaintext-password">
+ <properties>
+ <help>Plain-text authentication type</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Level-wide password</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <!-- <leafNode name="md5">
+ <properties>
+ <help>MD5 authentication type</help>
+ <valueHelp>
+ <format>&lt;md5&gt;</format>
+ <description>Level-wide password</description>
+ </valueHelp>
+ </properties>
+ </leafNode> -->
+ </children>
+ </node>
+ <leafNode name="dynamic-hostname">
+ <properties>
+ <help>Dynamic hostname for IS-IS</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="level">
+ <properties>
+ <help>IS-IS level number</help>
+ <completionHelp>
+ <list>level-1 level-1-2 level-2</list>
+ </completionHelp>
+ <valueHelp>
+ <format>level-1</format>
+ <description>Act as a station router</description>
+ </valueHelp>
+ <valueHelp>
+ <format>level-1-2</format>
+ <description>Act as both a station and an area router</description>
+ </valueHelp>
+ <valueHelp>
+ <format>level-2</format>
+ <description>Act as an area router</description>
+ </valueHelp>
+ <constraint>
+ <regex>(level-1|level-1-2|level-2)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="lsp-gen-interval">
+ <properties>
+ <help>Minimum interval between regenerating same LSP</help>
+ <valueHelp>
+ <format>&lt;1-120&gt;</format>
+ <description>Minimum interval in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-120"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="lsp-mtu">
+ <properties>
+ <help>Configure the maximum size of generated LSPs</help>
+ <valueHelp>
+ <format>&lt;128-4352&gt;</format>
+ <description>Maximum size of generated LSPs</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 128-4352"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="lsp-refresh-interval">
+ <properties>
+ <help>LSP refresh interval</help>
+ <valueHelp>
+ <format>&lt;1-65235&gt;</format>
+ <description>LSP refresh interval in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65235"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="max-lsp-lifetime">
+ <properties>
+ <help>Maximum LSP lifetime</help>
+ <valueHelp>
+ <format>&lt;350-65535&gt;</format>
+ <description>LSP lifetime in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="metric-style">
+ <properties>
+ <help>Use old-style (ISO 10589) or new-style packet formats</help>
+ <completionHelp>
+ <list>narrow transition wide</list>
+ </completionHelp>
+ <valueHelp>
+ <format>narrow</format>
+ <description>Use old style of TLVs with narrow metric</description>
+ </valueHelp>
+ <valueHelp>
+ <format>transition</format>
+ <description>Send and accept both styles of TLVs during transition</description>
+ </valueHelp>
+ <valueHelp>
+ <format>wide</format>
+ <description>Use new style of TLVs to carry wider metric</description>
+ </valueHelp>
+ <constraint>
+ <regex>(narrow|transition|wide)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="net">
+ <properties>
+ <help>A Network Entity Title for this process (ISO only)</help>
+ <valueHelp>
+ <format>XX.XXXX. ... .XXX.XX</format>
+ <description>Network entity title (NET)</description>
+ </valueHelp>
+ <constraint>
+ <regex>[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2}</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="purge-originator">
+ <properties>
+ <help>Use the RFC 6232 purge-originator</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="redistribute">
+ <properties>
+ <help>Redistribute information from another routing protocol</help>
+ </properties>
+ <children>
+ <node name="ipv4">
+ <properties>
+ <help>Redistribute IPv4 routes</help>
+ </properties>
+ <children>
+ <node name="bgp">
+ <properties>
+ <help>Border Gateway Protocol (BGP)</help>
+ </properties>
+ <children>
+ #include <include/isis-redistribute-ipv4.xml.i>
+ </children>
+ </node>
+ <node name="connected">
+ <properties>
+ <help>Redistribute connected routes into ISIS</help>
+ </properties>
+ <children>
+ #include <include/isis-redistribute-ipv4.xml.i>
+ </children>
+ </node>
+ <node name="kernel">
+ <properties>
+ <help>Redistribute kernel routes into ISIS</help>
+ </properties>
+ <children>
+ #include <include/isis-redistribute-ipv4.xml.i>
+ </children>
+ </node>
+ <node name="ospf">
+ <properties>
+ <help>Redistribute OSPF routes into ISIS</help>
+ </properties>
+ <children>
+ #include <include/isis-redistribute-ipv4.xml.i>
+ </children>
+ </node>
+ <node name="rip">
+ <properties>
+ <help>Redistribute RIP routes into ISIS</help>
+ </properties>
+ <children>
+ #include <include/isis-redistribute-ipv4.xml.i>
+ </children>
+ </node>
+ <node name="static">
+ <properties>
+ <help>Redistribute static routes into ISIS</help>
+ </properties>
+ <children>
+ #include <include/isis-redistribute-ipv4.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="set-attached-bit">
+ <properties>
+ <help>Set attached bit to identify as L1/L2 router for inter-area traffic</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="set-overload-bit">
+ <properties>
+ <help>Set overload bit to avoid any transit traffic</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="spf-delay-ietf">
+ <properties>
+ <help>IETF SPF delay algorithm</help>
+ </properties>
+ <children>
+ <leafNode name="init-delay">
+ <properties>
+ <help>Delay used while in QUIET state</help>
+ <valueHelp>
+ <format>&lt;0-60000&gt;</format>
+ <description>Delay used while in QUIET state (in ms)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-60000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="short-delay">
+ <properties>
+ <help>Delay used while in SHORT_WAIT state</help>
+ <valueHelp>
+ <format>&lt;0-60000&gt;</format>
+ <description>Delay used while in SHORT_WAIT state (in ms)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-60000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="long-delay">
+ <properties>
+ <help>Delay used while in LONG_WAIT</help>
+ <valueHelp>
+ <format>&lt;0-60000&gt;</format>
+ <description>Delay used while in LONG_WAIT state (in ms)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-60000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="holddown">
+ <properties>
+ <help>Time with no received IGP events before considering IGP stable</help>
+ <valueHelp>
+ <format>&lt;0-60000&gt;</format>
+ <description>Time with no received IGP events before considering IGP stable (in ms)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-60000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="time-to-learn">
+ <properties>
+ <help>Maximum duration needed to learn all the events related to a single failure</help>
+ <valueHelp>
+ <format>&lt;0-60000&gt;</format>
+ <description>Maximum duration needed to learn all the events related to a single failure (in ms)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-60000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="spf-interval">
+ <properties>
+ <help>Minimum interval between SPF calculations</help>
+ <valueHelp>
+ <format>&lt;1-120&gt;</format>
+ <description>Minimum interval between consecutive SPFs in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-120"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="interface">
+ <!-- (config-if)# ip router isis WORD (same as name of IS-IS process)
+ if any section of "interface" pesent -->
+ <properties>
+ <help>Interface params</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="bfd">
+ <properties>
+ <help>Enable BFD support</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="circuit-type">
+ <properties>
+ <help>Configure circuit type for interface</help>
+ <completionHelp>
+ <list>level-1 level-1-2 level-2-only</list>
+ </completionHelp>
+ <valueHelp>
+ <format>level-1</format>
+ <description>Level-1 only adjacencies are formed</description>
+ </valueHelp>
+ <valueHelp>
+ <format>level-1-2</format>
+ <description>Level-1-2 adjacencies are formed</description>
+ </valueHelp>
+ <valueHelp>
+ <format>level-2-only</format>
+ <description>Level-2 only adjacencies are formed</description>
+ </valueHelp>
+ <constraint>
+ <regex>(level-1|level-1-2|level-2-only)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="hello-padding">
+ <properties>
+ <help>Add padding to IS-IS hello packets</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="hello-interval">
+ <properties>
+ <help>Set Hello interval</help>
+ <valueHelp>
+ <format>&lt;1-600&gt;</format>
+ <description>Set Hello interval</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-600"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="hello-multiplier">
+ <properties>
+ <help>Set Hello interval</help>
+ <valueHelp>
+ <format>&lt;2-100&gt;</format>
+ <description>Set multiplier for Hello holding time</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 2-100"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="metric">
+ <properties>
+ <help>Set default metric for circuit</help>
+ <valueHelp>
+ <format>&lt;0-16777215&gt;</format>
+ <description>Default metric value</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-16777215"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="network">
+ <properties>
+ <help>Set network type</help>
+ </properties>
+ <children>
+ <leafNode name="point-to-point">
+ <properties>
+ <help>point-to-point network type</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="passive">
+ <properties>
+ <help>Configure the passive mode for interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="password">
+ <properties>
+ <help>Configure the authentication password for a circuit</help>
+ </properties>
+ <children>
+ <leafNode name="plaintext-password">
+ <properties>
+ <help>Plain-text authentication type</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Circuit password</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="priority">
+ <properties>
+ <help>Set priority for Designated Router election</help>
+ <valueHelp>
+ <format>&lt;0-127&gt;</format>
+ <description>Priority value</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-127"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="psnp-interval">
+ <properties>
+ <help>Set PSNP interval in seconds</help>
+ <valueHelp>
+ <format>&lt;0-127&gt;</format>
+ <description>Priority value</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-127"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="three-way-handshake">
+ <properties>
+ <help>Enable/Disable three-way handshake</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </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..3e9edbf72
--- /dev/null
+++ b/interface-definitions/protocols-mpls.xml.in
@@ -0,0 +1,122 @@
+<?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="hello-holdtime">
+ <properties>
+ <help>Hello holdtime</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="hello-interval">
+ <properties>
+ <help>Hello interval</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <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>
diff --git a/interface-definitions/protocols-multicast.xml.in b/interface-definitions/protocols-multicast.xml.in
new file mode 100644
index 000000000..a06f2b287
--- /dev/null
+++ b/interface-definitions/protocols-multicast.xml.in
@@ -0,0 +1,95 @@
+<?xml version="1.0"?>
+<!-- Multicast static routing configuration -->
+<interfaceDefinition>
+ <node name="protocols">
+ <children>
+ <node name="static">
+ <children>
+ <node name="multicast" owner="${vyos_conf_scripts_dir}/protocols_static_multicast.py">
+ <properties>
+ <help>Multicast static route</help>
+ </properties>
+ <children>
+ <tagNode name="route">
+ <properties>
+ <help>Configure static unicast route into MRIB for multicast RPF lookup</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>Network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <tagNode name="next-hop">
+ <properties>
+ <help>Nexthop IPv4 address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Nexthop IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="distance">
+ <properties>
+ <help>Distance value for this route</help>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Distance for this route</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="interface-route">
+ <properties>
+ <help>Multicast interface based route</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>Network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <tagNode name="next-hop-interface">
+ <properties>
+ <help>Next-hop interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="distance">
+ <properties>
+ <help>Distance value for this route</help>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Distance for this route</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
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/protocols-rip.xml.in b/interface-definitions/protocols-rip.xml.in
new file mode 100644
index 000000000..107f0e0d5
--- /dev/null
+++ b/interface-definitions/protocols-rip.xml.in
@@ -0,0 +1,406 @@
+<!-- Routing Information Protocol (RIP) configuration -->
+<interfaceDefinition>
+ <node name="protocols">
+ <children>
+ <node name="rip" owner="${vyos_conf_scripts_dir}/protocols_rip.py">
+ <properties>
+ <help>Routing Information Protocol (RIP) parameters</help>
+ </properties>
+ <children>
+ <leafNode name="default-distance">
+ <properties>
+ <help>Administrative distance</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>Administrative distance</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="default-information">
+ <properties>
+ <help>Control distribution of default route</help>
+ </properties>
+ <children>
+ <leafNode name="originate">
+ <properties>
+ <help>Distribute a default route</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="default-metric">
+ <properties>
+ <help>Metric of redistributed routes</help>
+ <valueHelp>
+ <format>&lt;1-16&gt;</format>
+ <description>Redistributed routes metric</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-16"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="distribute-list">
+ <properties>
+ <help>Filter networks in routing updates</help>
+ </properties>
+ <children>
+ <node name="access-list">
+ <properties>
+ <help>Access-list</help>
+ </properties>
+ <children>
+ <leafNode name="in">
+ <properties>
+ <help>Access list to apply to input packets</help>
+ <valueHelp>
+ <format>&lt;0-4294967295&gt;</format>
+ <description>Access list to apply to input packets</description>
+ </valueHelp>
+ <completionHelp>
+ <path>policy access-list</path>
+ </completionHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="out">
+ <properties>
+ <help>Access list to apply to output packets</help>
+ <valueHelp>
+ <format>&lt;0-4294967295&gt;</format>
+ <description>Access list to apply to output packets</description>
+ </valueHelp>
+ <completionHelp>
+ <path>policy access-list</path>
+ </completionHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="interface">
+ <properties>
+ <help>Apply filtering to an interface</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Apply filtering to an interface</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <node name="access-list">
+ <properties>
+ <help>Access list</help>
+ </properties>
+ <children>
+ <leafNode name="in">
+ <properties>
+ <help>Access list to apply to input packets</help>
+ <valueHelp>
+ <format>&lt;0-4294967295&gt;</format>
+ <description>Access list to apply to input packets</description>
+ </valueHelp>
+ <completionHelp>
+ <path>policy access-list</path>
+ </completionHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="out">
+ <properties>
+ <help>Access list to apply to output packets</help>
+ <valueHelp>
+ <format>&lt;0-4294967295&gt;</format>
+ <description>Access list to apply to output packets</description>
+ </valueHelp>
+ <completionHelp>
+ <path>policy access-list</path>
+ </completionHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="prefix-list">
+ <properties>
+ <help>Prefix-list</help>
+ </properties>
+ <children>
+ <leafNode name="in">
+ <properties>
+ <help>Prefix-list to apply to input packets</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Prefix-list to apply to input packets</description>
+ </valueHelp>
+ <completionHelp>
+ <path>policy prefix-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="out">
+ <properties>
+ <help>Prefix-list to apply to output packets</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Prefix-list to apply to output packets</description>
+ </valueHelp>
+ <completionHelp>
+ <path>policy prefix-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <node name="prefix-list">
+ <properties>
+ <help>Prefix-list</help>
+ </properties>
+ <children>
+ <leafNode name="in">
+ <properties>
+ <help>Prefix-list to apply to input packets</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Prefix-list to apply to input packets</description>
+ </valueHelp>
+ <completionHelp>
+ <path>policy prefix-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="out">
+ <properties>
+ <help>Prefix-list to apply to output packets</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Prefix-list to apply to output packets</description>
+ </valueHelp>
+ <completionHelp>
+ <path>policy prefix-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface name</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Apply filtering to an interface</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="neighbor">
+ <properties>
+ <help>Neighbor router</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Neighbor router</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="network">
+ <properties>
+ <help>RIP network</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>RIP network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <tagNode name="network-distance">
+ <properties>
+ <help>Source network</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>Source network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="access-list">
+ <properties>
+ <help>Access list</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Access list</description>
+ </valueHelp>
+ <completionHelp>
+ <path>policy access-list</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="distance">
+ <properties>
+ <help>Administrative distance for network</help>
+ <valueHelp>
+ <format>&lt;1-255&gt;</format>
+ <description>Administrative distance</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="passive-interface">
+ <properties>
+ <help>Passive interface</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>Suppress routing updates on interface</description>
+ </valueHelp>
+ <valueHelp>
+ <format>default</format>
+ <description>Suppress routing updates on all interfaces by default</description>
+ </valueHelp>
+ <completionHelp>
+ <list>default</list>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="redistribute">
+ <properties>
+ <help>Redistribute information from another routing protocol</help>
+ </properties>
+ <children>
+ <node name="bgp">
+ <properties>
+ <help>Redistribute BGP routes</help>
+ </properties>
+ <children>
+ #include <include/rip-redistribute.xml.i>
+ </children>
+ </node>
+ <node name="connected">
+ <properties>
+ <help>Redistribute connected routes</help>
+ </properties>
+ <children>
+ #include <include/rip-redistribute.xml.i>
+ </children>
+ </node>
+ <node name="kernel">
+ <properties>
+ <help>Redistribute kernel routes</help>
+ </properties>
+ <children>
+ #include <include/rip-redistribute.xml.i>
+ </children>
+ </node>
+ <node name="ospf">
+ <properties>
+ <help>Redistribute OSPF routes</help>
+ </properties>
+ <children>
+ #include <include/rip-redistribute.xml.i>
+ </children>
+ </node>
+ <node name="static">
+ <properties>
+ <help>Redistribute static routes</help>
+ </properties>
+ <children>
+ #include <include/rip-redistribute.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="route">
+ <properties>
+ <help>RIP static route</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>RIP static route</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="timers">
+ <properties>
+ <help>RIP timer values</help>
+ </properties>
+ <children>
+ <leafNode name="garbage-collection">
+ <properties>
+ <help>Garbage collection timer</help>
+ <valueHelp>
+ <format>&lt;5-2147483647&gt;</format>
+ <description>Garbage colletion time (default 120)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 5-2147483647"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="timeout">
+ <properties>
+ <help>Routing information timeout timer</help>
+ <valueHelp>
+ <format>&lt;5-2147483647&gt;</format>
+ <description>Routing information timeout timer (default 180)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 5-2147483647"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="update">
+ <properties>
+ <help>Routing table update timer</help>
+ <valueHelp>
+ <format>&lt;5-2147483647&gt;</format>
+ <description>Routing table update timer in seconds (default 30)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 5-2147483647"/>
+ </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..97f882a6a
--- /dev/null
+++ b/interface-definitions/salt-minion.xml.in
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<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">
+ <properties>
+ <help>Hash used when discovering file on master server (default: sha256)</help>
+ <completionHelp>
+ <list>md5 sha1 sha224 sha256 sha384 sha512</list>
+ </completionHelp>
+ <constraint>
+ <regex>(md5|sha1|sha224|sha256|sha384|sha512)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="master">
+ <properties>
+ <help>The hostname or IP address of the master.</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Remote syslog server IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hostname</format>
+ <description>Remote syslog server FQDN</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ <validator name="fqdn"/>
+ </constraint>
+ <constraintErrorMessage>Invalid FQDN or IP address</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="id">
+ <properties>
+ <help>Explicitly declare ID for this minion to use (default: hostname)</help>
+ </properties>
+ </leafNode>
+ <leafNode name="interval">
+ <properties>
+ <help>Interval in minutes between updates (default: 60)</help>
+ <valueHelp>
+ <format>&lt;1-1440&gt;</format>
+ <description>Update interval in minutes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-1440"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="master-key">
+ <properties>
+ <help>URL with signature of master for auth reply verification</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/service-ids-ddos-protection.xml.in b/interface-definitions/service-ids-ddos-protection.xml.in
new file mode 100644
index 000000000..93d4cc682
--- /dev/null
+++ b/interface-definitions/service-ids-ddos-protection.xml.in
@@ -0,0 +1,118 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="ids">
+ <properties>
+ <help>Intrusion Detection System</help>
+ </properties>
+ <children>
+ <node name="ddos-protection" owner="${vyos_conf_scripts_dir}/service_ids_fastnetmon.py">
+ <properties>
+ <help>FastNetMon detection and protection parameters</help>
+ <priority>731</priority>
+ </properties>
+ <children>
+ <leafNode name="alert-script">
+ <properties>
+ <help>Path to fastnetmon alert script</help>
+ </properties>
+ </leafNode>
+ <leafNode name="direction">
+ <properties>
+ <help>Direction for processing traffic</help>
+ <completionHelp>
+ <list>in out</list>
+ </completionHelp>
+ <constraint>
+ <regex>(in|out)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="listen-interface">
+ <properties>
+ <help>Listen interface for mirroring traffic</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="mode">
+ <properties>
+ <help>Traffic capture modes</help>
+ </properties>
+ <children>
+ <!-- Future modes "mirror" "netflow" "combine (both)" -->
+ <leafNode name="mirror">
+ <properties>
+ <help>Listen mirrored traffic mode</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="network">
+ <properties>
+ <help>Define monitoring networks</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>Processed network</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="threshold">
+ <properties>
+ <help>Attack limits thresholds</help>
+ </properties>
+ <children>
+ <leafNode name="fps">
+ <properties>
+ <help>Flows per second</help>
+ <valueHelp>
+ <format>&lt;0-4294967294&gt;</format>
+ <description>Flows per second</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967294"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mbps">
+ <properties>
+ <help>Megabits per second</help>
+ <valueHelp>
+ <format>&lt;0-4294967294&gt;</format>
+ <description>Megabits per second</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967294"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="pps">
+ <properties>
+ <help>Packets per second</help>
+ <valueHelp>
+ <format>&lt;0-4294967294&gt;</format>
+ <description>Packets per second</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967294"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/service_console-server.xml.in b/interface-definitions/service_console-server.xml.in
new file mode 100644
index 000000000..59a9fe237
--- /dev/null
+++ b/interface-definitions/service_console-server.xml.in
@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="console-server" owner="${vyos_conf_scripts_dir}/service_console-server.py">
+ <properties>
+ <help>Serial Console Server</help>
+ <priority>990</priority>
+ </properties>
+ <children>
+ <tagNode name="device">
+ <properties>
+ <help>System serial interface name (ttyS or ttyUSB)</help>
+ <completionHelp>
+ <script>ls -1 /dev | grep ttyS</script>
+ <script>ls -1 /dev/serial/by-bus</script>
+ </completionHelp>
+ <valueHelp>
+ <format>ttySxxx</format>
+ <description>Regular serial interface</description>
+ </valueHelp>
+ <valueHelp>
+ <format>usbxbxpx</format>
+ <description>USB based serial interface</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(ttyS\d+|usb\d+b.*p.*)$</regex>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/interface-description.xml.i>
+ <leafNode name="speed">
+ <properties>
+ <help>Serial port baud rate</help>
+ <completionHelp>
+ <list>300 1200 2400 4800 9600 19200 38400 57600 115200</list>
+ </completionHelp>
+ <constraint>
+ <regex>(300|1200|2400|4800|9600|19200|38400|57600|115200)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="data-bits">
+ <properties>
+ <help>Serial port data bits (default: 8)</help>
+ <completionHelp>
+ <list>7 8</list>
+ </completionHelp>
+ <constraint>
+ <regex>(7|8)</regex>
+ </constraint>
+ </properties>
+ <defaultValue>8</defaultValue>
+ </leafNode>
+ <leafNode name="stop-bits">
+ <properties>
+ <help>Serial port stop bits (default: 1)</help>
+ <completionHelp>
+ <list>1 2</list>
+ </completionHelp>
+ <constraint>
+ <regex>(1|2)</regex>
+ </constraint>
+ </properties>
+ <defaultValue>1</defaultValue>
+ </leafNode>
+ <leafNode name="parity">
+ <properties>
+ <help>Parity setting (default: none)</help>
+ <completionHelp>
+ <list>even odd none</list>
+ </completionHelp>
+ <constraint>
+ <regex>(even|odd|none)</regex>
+ </constraint>
+ </properties>
+ <defaultValue>none</defaultValue>
+ </leafNode>
+ <node name="ssh">
+ <properties>
+ <help>SSH remote access to this console</help>
+ </properties>
+ <children>
+ #include <include/port-number.xml.i>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in
new file mode 100644
index 000000000..9ee5d5156
--- /dev/null
+++ b/interface-definitions/service_ipoe-server.xml.in
@@ -0,0 +1,208 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="ipoe-server" owner="${vyos_conf_scripts_dir}/service_ipoe-server.py">
+ <properties>
+ <help>Internet Protocol over Ethernet (IPoE) Server</help>
+ <priority>900</priority>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Network interface to server IPoE</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="network-mode">
+ <properties>
+ <help>Network Layer IPoE serves on</help>
+ <completionHelp>
+ <list>L2 L3</list>
+ </completionHelp>
+ <constraint>
+ <regex>(L2|L3)</regex>
+ </constraint>
+ <valueHelp>
+ <format>L2</format>
+ <description>client share the same subnet</description>
+ </valueHelp>
+ <valueHelp>
+ <format>L3</format>
+ <description>clients are behind this router</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="network">
+ <properties>
+ <help>Enables clients to share the same network or each client has its own vlan</help>
+ <completionHelp>
+ <list>shared vlan</list>
+ </completionHelp>
+ <constraint>
+ <regex>(shared|vlan)</regex>
+ </constraint>
+ <valueHelp>
+ <format>shared</format>
+ <description>Multiple clients share the same network</description>
+ </valueHelp>
+ <valueHelp>
+ <format>vlan</format>
+ <description>One VLAN per client</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="client-subnet">
+ <properties>
+ <help>Client address pool</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="external-dhcp">
+ <properties>
+ <help>DHCP requests will be forwarded</help>
+ </properties>
+ <children>
+ <leafNode name="dhcp-relay">
+ <properties>
+ <help>DHCP Server the request will be redirected to.</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address of the DHCP Server</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="giaddr">
+ <properties>
+ <help>address of the relay agent (Relay Agent IP Address)</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="vlan-id">
+ <properties>
+ <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4096"/>
+ </constraint>
+ <constraintErrorMessage>VLAN ID needs to be between 1 and 4096</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="vlan-range">
+ <properties>
+ <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help>
+ <constraint>
+ <regex>(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})-(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ #include <include/accel-name-server.xml.in>
+ #include <include/accel-client-ipv6-pool.xml.in>
+ <node name="authentication">
+ <properties>
+ <help>Client authentication methods</help>
+ </properties>
+ <children>
+ <leafNode name="mode">
+ <properties>
+ <help>Authetication mode</help>
+ <completionHelp>
+ <list>local radius noauth</list>
+ </completionHelp>
+ <constraint>
+ <regex>(local|radius|noauth)</regex>
+ </constraint>
+ <valueHelp>
+ <format>local</format>
+ <description>Authentication based on local definition</description>
+ </valueHelp>
+ <valueHelp>
+ <format>radius</format>
+ <description>Authentication based on a RADIUS server</description>
+ </valueHelp>
+ <valueHelp>
+ <format>noauth</format>
+ <description>Authentication disabled</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Network interface the client mac will appear on</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="mac-address">
+ <properties>
+ <help>Client mac address allowed to receive an IP address</help>
+ <valueHelp>
+ <format>h:h:h:h:h:h</format>
+ <description>Hardware (MAC) address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <node name="rate-limit">
+ <properties>
+ <help>Upload/Download speed limits</help>
+ </properties>
+ <children>
+ <leafNode name="upload">
+ <properties>
+ <help>Upload bandwidth limit in kbits/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="download">
+ <properties>
+ <help>Download bandwidth limit in kbits/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="vlan-id">
+ <properties>
+ <help>VLAN-ID of the client network</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4096"/>
+ </constraint>
+ <constraintErrorMessage>VLAN ID needs to be between 1 and 4096</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ #include <include/radius-server.xml.i>
+ #include <include/accel-radius-additions.xml.in>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/service_mdns-repeater.xml.in b/interface-definitions/service_mdns-repeater.xml.in
new file mode 100644
index 000000000..e21b1b27c
--- /dev/null
+++ b/interface-definitions/service_mdns-repeater.xml.in
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="mdns">
+ <properties>
+ <help>Multicast DNS (mDNS) parameters</help>
+ </properties>
+ <children>
+ <node name="repeater" owner="${vyos_conf_scripts_dir}/service_mdns-repeater.py">
+ <properties>
+ <help>mDNS repeater configuration</help>
+ <priority>990</priority>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable mDNS repeater service</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface to repeat mDNS advertisements [REQUIRED]</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in
new file mode 100644
index 000000000..605f47b37
--- /dev/null
+++ b/interface-definitions/service_pppoe-server.xml.in
@@ -0,0 +1,491 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="pppoe-server" owner="${vyos_conf_scripts_dir}/service_pppoe-server.py">
+ <properties>
+ <help>Point to Point over Ethernet (PPPoE) Server</help>
+ <priority>900</priority>
+ </properties>
+ <children>
+ <node name="snmp">
+ <properties>
+ <help>Enable SNMP</help>
+ </properties>
+ <children>
+ <leafNode name="master-agent">
+ <properties>
+ <help>enable SNMP master agent mode</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="access-concentrator">
+ <properties>
+ <help>Access concentrator name</help>
+ <constraint>
+ <regex>[a-zA-Z0-9]{1,100}</regex>
+ </constraint>
+ <constraintErrorMessage>access-concentrator name limited to alphanumerical characters only (max. 100)</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="session-control">
+ <properties>
+ <help>control sessions count</help>
+ <constraint>
+ <regex>(deny|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid value</constraintErrorMessage>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disables session control</description>
+ </valueHelp>
+ <valueHelp>
+ <format>deny</format>
+ <description>Deny second session authorization</description>
+ </valueHelp>
+ <completionHelp>
+ <list>deny disable</list>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <node name="authentication">
+ <properties>
+ <help>Authentication for remote access PPPoE Server</help>
+ </properties>
+ <children>
+ <node name="local-users">
+ <properties>
+ <help>Local user authentication for PPPoE server</help>
+ </properties>
+ <children>
+ <tagNode name="username">
+ <properties>
+ <help>User name for authentication</help>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable a PPPoE Server user</help>
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>Password for authentication</help>
+ </properties>
+ </leafNode>
+ <leafNode name="static-ip">
+ <properties>
+ <help>Static client IP address</help>
+ </properties>
+ </leafNode>
+ <node name="rate-limit">
+ <properties>
+ <help>Upload/Download speed limits</help>
+ </properties>
+ <children>
+ <leafNode name="upload">
+ <properties>
+ <help>Upload bandwidth limit in kbits/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="download">
+ <properties>
+ <help>Download bandwidth limit in kbits/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ #include <include/accel-auth-mode.xml.i>
+ #include <include/radius-server.xml.i>
+ #include <include/accel-radius-additions.xml.in>
+ <node name="radius">
+ <children>
+ <node name="rate-limit">
+ <properties>
+ <help>Upload/Download speed limits</help>
+ </properties>
+ <children>
+ <leafNode name="attribute">
+ <properties>
+ <help>Specifies which radius attribute contains rate information. (default is 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>
+ </properties>
+ </leafNode>
+ <leafNode name="enable">
+ <properties>
+ <help>Enables Bandwidth shaping via RADIUS</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="protocols">
+ <properties>
+ <help>Authentication protocol</help>
+ <valueHelp>
+ <format>pap</format>
+ <description>Allow PAP authentication [Password Authentication Protocol]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>chap</format>
+ <description>Allow CHAP authentication [Challenge Handshake Authentication Protocol]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mschap</format>
+ <description>Allow MS-CHAP authentication [Microsoft Challenge Handshake Authentication Protocol, Version 1]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mschap-v2</format>
+ <description>Allow MS-CHAPv2 authentication [Microsoft Challenge Handshake Authentication Protocol, Version 2]</description>
+ </valueHelp>
+ <constraint>
+ <regex>(pap|chap|mschap|mschap-v2)</regex>
+ </constraint>
+ <completionHelp>
+ <list>pap chap mschap mschap-v2</list>
+ </completionHelp>
+ <multi />
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="client-ip-pool">
+ <properties>
+ <help>Pool of client IP addresses (must be within a /24)</help>
+ </properties>
+ <children>
+ <leafNode name="start">
+ <properties>
+ <help>First IP address in the pool</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="stop">
+ <properties>
+ <help>Last IP address in the pool</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="subnet">
+ <properties>
+ <help>Client IP subnet (CIDR notation)</help>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage>
+ <multi />
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/accel-client-ipv6-pool.xml.in>
+ #include <include/accel-name-server.xml.in>
+ <tagNode name="interface">
+ <properties>
+ <help>interface(s) to listen on</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="vlan-id">
+ <properties>
+ <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4096"/>
+ </constraint>
+ <constraintErrorMessage>VLAN ID needs to be between 1 and 4096</constraintErrorMessage>
+ <multi />
+ </properties>
+ </leafNode>
+ <leafNode name="vlan-range">
+ <properties>
+ <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help>
+ <constraint>
+ <regex>(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})-(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})</regex>
+ </constraint>
+ <multi />
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="local-ip">
+ <properties>
+ <help>local gateway address</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU) - default 1492</help>
+ <constraint>
+ <validator name="numeric" argument="--range 128-16384"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="limits">
+ <properties>
+ <help>Limits the connection rate from a single source</help>
+ </properties>
+ <children>
+ <leafNode name="connection-limit">
+ <properties>
+ <help>Acceptable rate of connections (e.g. 1/min, 60/sec)</help>
+ <constraint>
+ <regex>[0-9]+\/(min|sec)$</regex>
+ </constraint>
+ <constraintErrorMessage>illegal value</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="burst">
+ <properties>
+ <help>Burst count</help>
+ </properties>
+ </leafNode>
+ <leafNode name="timeout">
+ <properties>
+ <help>Timeout in seconds</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="service-name">
+ <properties>
+ <help>Service name</help>
+ <constraint>
+ <regex>[a-zA-Z0-9\-]{1,100}</regex>
+ </constraint>
+ <constraintErrorMessage>servicename can contain aplhanumerical characters and dashes only (max. 100)</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ #include <include/accel-wins-server.xml.i>
+ <node name="ppp-options">
+ <properties>
+ <help>Advanced protocol options</help>
+ </properties>
+ <children>
+ <leafNode name="min-mtu">
+ <properties>
+ <help>Minimum acceptable MTU (68-65535)</help>
+ <constraint>
+ <validator name="numeric" argument="--range 68-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mru">
+ <properties>
+ <help>Preferred MRU (68-65535)</help>
+ <constraint>
+ <validator name="numeric" argument="--range 68-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="ccp">
+ <properties>
+ <help>CCP negotiation (default disabled)</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ <leafNode name="mppe">
+ <properties>
+ <help>Specifies MPPE negotiation preference. (default prefer mppe)</help>
+ <completionHelp>
+ <list>deny prefer require</list>
+ </completionHelp>
+ <valueHelp>
+ <format>deny</format>
+ <description>Deny MPPE</description>
+ </valueHelp>
+ <valueHelp>
+ <format>prefer</format>
+ <description>Ask client for MPPE - do not fail on reject</description>
+ </valueHelp>
+ <valueHelp>
+ <format>require</format>
+ <description>Ask client for MPPE - drop connection on reject</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(deny|prefer|require)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="lcp-echo-interval">
+ <properties>
+ <help>LCP echo-requests/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="lcp-echo-failure">
+ <properties>
+ <help>Maximum number of Echo-Requests may be sent without valid reply</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="lcp-echo-timeout">
+ <properties>
+ <help>Timeout in seconds to wait for any peer activity. If this option specified it turns on adaptive lcp echo functionality and "lcp-echo-failure" is not used.</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="ipv4">
+ <properties>
+ <help>IPv4 (IPCP) negotiation algorithm</help>
+ <constraint>
+ <regex>(deny|allow|prefer|require)</regex>
+ </constraint>
+ <constraintErrorMessage>invalid value</constraintErrorMessage>
+ <valueHelp>
+ <format>deny</format>
+ <description>Do not negotiate IPv4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>allow</format>
+ <description>Negotiate IPv4 only if client requests</description>
+ </valueHelp>
+ <valueHelp>
+ <format>prefer</format>
+ <description>Ask client for IPv4 negotiation, do not fail if it rejects</description>
+ </valueHelp>
+ <valueHelp>
+ <format>require</format>
+ <description>Require IPv4 negotiation</description>
+ </valueHelp>
+ <completionHelp>
+ <list>deny allow prefer require</list>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="ipv6">
+ <properties>
+ <help>IPv6 (IPCP6) negotiation algorithm</help>
+ <constraint>
+ <regex>(deny|allow|prefer|require)</regex>
+ </constraint>
+ <constraintErrorMessage>invalid value</constraintErrorMessage>
+ <valueHelp>
+ <format>deny</format>
+ <description>Do not negotiate IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>allow</format>
+ <description>Negotiate IPv6 only if client requests</description>
+ </valueHelp>
+ <valueHelp>
+ <format>prefer</format>
+ <description>Ask client for IPv6 negotiation, do not fail if it rejects</description>
+ </valueHelp>
+ <valueHelp>
+ <format>require</format>
+ <description>Require IPv6 negotiation</description>
+ </valueHelp>
+ <completionHelp>
+ <list>deny allow prefer require</list>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="ipv6-intf-id">
+ <properties>
+ <help>Fixed or random interface identifier for IPv6</help>
+ <valueHelp>
+ <format>random</format>
+ <description>Random interface identifier for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>x:x:x:x</format>
+ <description>specify interface identifier for IPv6</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="ipv6-peer-intf-id">
+ <properties>
+ <help>Peer interface identifier for IPv6</help>
+ <valueHelp>
+ <format>x:x:x:x</format>
+ <description>Interface identifier for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>random</format>
+ <description>Use a random interface identifier for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Calculate interface identifier from IPv4 address, for example 192:168:0:1</description>
+ </valueHelp>
+ <valueHelp>
+ <format>calling-sid</format>
+ <description>Calculate interface identifier from calling-station-id</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="ipv6-accept-peer-intf-id">
+ <properties>
+ <help>Accept peer interface identifier</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="pado-delay">
+ <properties>
+ <help>PADO delays</help>
+ <valueHelp>
+ <format>1-999999</format>
+ <description>Number in ms</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>Invalid PADO delay</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="sessions">
+ <properties>
+ <help>Number of sessions</help>
+ <valueHelp>
+ <format>1-999999</format>
+ <description>Number of sessions</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>Invalid number of delayed sessions</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </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..5a472fc9a
--- /dev/null
+++ b/interface-definitions/service_router-advert.xml.in
@@ -0,0 +1,273 @@
+<?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>
+ <defaultValue>64</defaultValue>
+ </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>
+ <defaultValue>medium</defaultValue>
+ </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>
+ <defaultValue>600</defaultValue>
+ </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>
+ <defaultValue>14400</defaultValue>
+ </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>
+ <defaultValue>2592000</defaultValue>
+ </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>
+ <defaultValue>0</defaultValue>
+ </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>
+ <defaultValue>0</defaultValue>
+ </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/snmp.xml.in b/interface-definitions/snmp.xml.in
new file mode 100644
index 000000000..2fe8ce583
--- /dev/null
+++ b/interface-definitions/snmp.xml.in
@@ -0,0 +1,631 @@
+<?xml version="1.0"?>
+<!-- SNMP forwarder configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="snmp" owner="${vyos_conf_scripts_dir}/snmp.py">
+ <properties>
+ <help>Simple Network Management Protocol (SNMP)</help>
+ <priority>980</priority>
+ </properties>
+ <children>
+ <tagNode name="community">
+ <properties>
+ <help>Community name</help>
+ <constraint>
+ <regex>^[a-zA-Z0-9\-_]{1,100}$</regex>
+ </constraint>
+ <constraintErrorMessage>Community string is limited to alphanumerical characters only with a total lenght of 100</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="authorization">
+ <properties>
+ <help>Authorization type (default: 'ro')</help>
+ <completionHelp>
+ <list>ro rw</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ro</format>
+ <description>read only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rw</format>
+ <description>read write</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(ro|rw)$</regex>
+ </constraint>
+ <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="client">
+ <properties>
+ <help>IP address of SNMP client allowed to contact system</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="network">
+ <properties>
+ <help>Subnet of SNMP client(s) allowed to contact system</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IP address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="contact">
+ <properties>
+ <help>Contact information</help>
+ <constraint>
+ <regex>^.{1,255}$</regex>
+ </constraint>
+ <constraintErrorMessage>Contact information is limited to 255 characters or less</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Description information</help>
+ <constraint>
+ <regex>^.{1,255}$</regex>
+ </constraint>
+ <constraintErrorMessage>Description is limited to 255 characters or less</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <tagNode name="listen-address">
+ <properties>
+ <help>IP address to listen for incoming SNMP requests</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to listen for incoming SNMP requests</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address to listen for incoming SNMP requests</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="port">
+ <properties>
+ <help>Port for SNMP service (default: '161')</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="location">
+ <properties>
+ <help>Location information</help>
+ <constraint>
+ <regex>^.{1,255}$</regex>
+ </constraint>
+ <constraintErrorMessage>Location is limited to 255 characters or less</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="smux-peer">
+ <properties>
+ <help>Register a subtree for SMUX-based processing</help>
+ <valueHelp>
+ <format>oid</format>
+ <description>Object Identifier</description>
+ </valueHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="trap-source">
+ <properties>
+ <help>SNMP trap source address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="trap-target">
+ <properties>
+ <help>Address of trap target</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="community">
+ <properties>
+ <help>Community used when sending trap information</help>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Destination port used for trap notification</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="v3">
+ <properties>
+ <help>Simple Network Management Protocol (SNMP) v3</help>
+ </properties>
+ <children>
+ <leafNode name="engineid">
+ <properties>
+ <help>Specifies the EngineID that uniquely identify an agent (e.g. 000000000000000000000002)</help>
+ <constraint>
+ <regex>^([0-9a-f][0-9a-f]){1,18}$</regex>
+ </constraint>
+ <constraintErrorMessage>ID must contain an even number (from 2 to 36) of hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <tagNode name="group">
+ <properties>
+ <help>Specifies the group with name groupname</help>
+ </properties>
+ <children>
+ <leafNode name="mode">
+ <properties>
+ <help>Define group access permission (default: 'ro')</help>
+ <completionHelp>
+ <list>ro rw</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ro</format>
+ <description>read only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rw</format>
+ <description>read write</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(ro|rw)$</regex>
+ </constraint>
+ <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="seclevel">
+ <properties>
+ <help>Security levels</help>
+ <completionHelp>
+ <list>noauth auth priv</list>
+ </completionHelp>
+ <valueHelp>
+ <format>noauth</format>
+ <description>Messages not authenticated and not encrypted (noAuthNoPriv)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>auth</format>
+ <description>Messages are authenticated but not encrypted (authNoPriv)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>priv</format>
+ <description>Messages are authenticated and encrypted (authPriv)</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(noauth|auth|priv)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="view">
+ <properties>
+ <help>Defines the name of view</help>
+ <completionHelp>
+ <path>service snmp v3 view</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="trap-target">
+ <properties>
+ <help>Defines SNMP target for inform or traps for IP</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IP address of trap target</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address of trap target</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <node name="auth">
+ <properties>
+ <help>Defines the privacy</help>
+ </properties>
+ <children>
+ <leafNode name="encrypted-password">
+ <properties>
+ <help>Defines the encrypted key for authentication</help>
+ <constraint>
+ <regex>^[0-9a-f]*$</regex>
+ </constraint>
+ <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="plaintext-password">
+ <properties>
+ <help>Defines the clear text key for authentication</help>
+ <constraint>
+ <regex>^.{8,}$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Defines the protocol used for authentication (default: 'md5')</help>
+ <completionHelp>
+ <list>md5 sha</list>
+ </completionHelp>
+ <valueHelp>
+ <format>md5</format>
+ <description>Message Digest 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha</format>
+ <description>Secure Hash Algorithm</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(md5|sha)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="port">
+ <properties>
+ <help>Specifies TCP/UDP port of destination SNMP traps/informs (default: '162')</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="privacy">
+ <properties>
+ <help>Defines the privacy</help>
+ </properties>
+ <children>
+ <leafNode name="encrypted-password">
+ <properties>
+ <help>Defines the encrypted key for privacy protocol</help>
+ <constraint>
+ <regex>^[0-9a-f]*$</regex>
+ </constraint>
+ <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="plaintext-password">
+ <properties>
+ <help>Defines the clear text key for privacy protocol</help>
+ <constraint>
+ <regex>^.{8,}$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Defines the protocol for privacy (default: 'des')</help>
+ <completionHelp>
+ <list>des aes</list>
+ </completionHelp>
+ <valueHelp>
+ <format>des</format>
+ <description>Data Encryption Standard</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes</format>
+ <description>Advanced Encryption Standard</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(des|aes)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="protocol">
+ <properties>
+ <help>Defines protocol for notification between TCP and UDP</help>
+ <completionHelp>
+ <list>tcp udp</list>
+ </completionHelp>
+ <valueHelp>
+ <format>tcp</format>
+ <description>Use Transmission Control Protocol for notifications</description>
+ </valueHelp>
+ <valueHelp>
+ <format>udp</format>
+ <description>Use User Datagram Protocol for notifications</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(tcp|udp)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Specifies the type of notification between inform and trap (default: 'inform')</help>
+ <completionHelp>
+ <list>inform trap</list>
+ </completionHelp>
+ <valueHelp>
+ <format>inform</format>
+ <description>Use INFORM</description>
+ </valueHelp>
+ <valueHelp>
+ <format>trap</format>
+ <description>Use TRAP</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(inform|trap)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Defines username for authentication</help>
+ <completionHelp>
+ <path>service snmp v3 user</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="user">
+ <properties>
+ <help>Specifies the user with name username</help>
+ <constraint>
+ <regex>[^\(\)\|\-]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Illegal characters in name</constraintErrorMessage>
+ </properties>
+ <children>
+ <node name="auth">
+ <properties>
+ <help>Specifies the auth</help>
+ </properties>
+ <children>
+ <leafNode name="encrypted-password">
+ <properties>
+ <help>Defines the encrypted key for authentication</help>
+ <constraint>
+ <regex>^[0-9a-f]*$</regex>
+ </constraint>
+ <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="plaintext-password">
+ <properties>
+ <help>Defines the clear text key for authentication</help>
+ <constraint>
+ <regex>^.{8,}$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Defines the protocol used for authentication (default: 'md5')</help>
+ <completionHelp>
+ <list>md5 sha</list>
+ </completionHelp>
+ <valueHelp>
+ <format>md5</format>
+ <description>Message Digest 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha</format>
+ <description>Secure Hash Algorithm</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(md5|sha)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="group">
+ <properties>
+ <help>Specifies group for user name</help>
+ <completionHelp>
+ <path>service snmp v3 group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="mode">
+ <properties>
+ <help>Define users access permission (default: 'ro')</help>
+ <completionHelp>
+ <list>ro rw</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ro</format>
+ <description>read only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rw</format>
+ <description>read write</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(ro|rw)$</regex>
+ </constraint>
+ <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="privacy">
+ <properties>
+ <help>Defines the privacy</help>
+ </properties>
+ <children>
+ <leafNode name="encrypted-password">
+ <properties>
+ <help>Defines the encrypted key for privacy protocol</help>
+ <constraint>
+ <regex>^[0-9a-f]*$</regex>
+ </constraint>
+ <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="plaintext-password">
+ <properties>
+ <help>Defines the clear text key for privacy protocol</help>
+ <constraint>
+ <regex>^.{8,}$</regex>
+ </constraint>
+ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Defines the protocol for privacy (default: 'des')</help>
+ <completionHelp>
+ <list>des aes</list>
+ </completionHelp>
+ <valueHelp>
+ <format>des</format>
+ <description>Data Encryption Standard</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes</format>
+ <description>Advanced Encryption Standard</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(des|aes)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <tagNode name="view">
+ <properties>
+ <help>Specifies the view with name viewname</help>
+ <constraint>
+ <regex>[^\(\)\|\-]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Illegal characters in name</constraintErrorMessage>
+ </properties>
+ <children>
+ <tagNode name="oid">
+ <properties>
+ <help>Specifies the oid</help>
+ <constraint>
+ <regex>^[0-9]+(\.[0-9]+)*$</regex>
+ </constraint>
+ <constraintErrorMessage>OID must start from a number</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="exclude">
+ <properties>
+ <help>Exclude is an optional argument</help>
+ </properties>
+ </leafNode>
+ <leafNode name="mask">
+ <properties>
+ <help>Defines a bit-mask that is indicating which subidentifiers of the associated subtree OID should be regarded as significant</help>
+ <constraint>
+ <regex>^[0-9a-f]{2}([\.:][0-9a-f]{2})*$</regex>
+ </constraint>
+ <constraintErrorMessage>MASK is a list of hex octets, separated by '.' or ':'</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="script-extensions">
+ <properties>
+ <help>SNMP script extensions</help>
+ </properties>
+ <children>
+ <tagNode name="extension-name">
+ <properties>
+ <help>Extension name</help>
+ <constraint>
+ <regex>^[a-z0-9\.\-\_]+</regex>
+ </constraint>
+ <constraintErrorMessage>Script extension contains invalid characters</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="script">
+ <properties>
+ <help>Script location and name</help>
+ <completionHelp>
+ <script>ls /config/user-data</script>
+ </completionHelp>
+ <constraint>
+ <regex>^[a-z0-9\.\-\_\/]+</regex>
+ </constraint>
+ <constraintErrorMessage>Script extension contains invalid characters</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ #include <include/interface-vrf.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in
new file mode 100644
index 000000000..d253c2f34
--- /dev/null
+++ b/interface-definitions/ssh.xml.in
@@ -0,0 +1,207 @@
+<?xml version="1.0"?>
+<!--SSH configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="ssh" owner="${vyos_conf_scripts_dir}/ssh.py">
+ <properties>
+ <help>Secure Shell (SSH)</help>
+ <priority>500</priority>
+ </properties>
+ <children>
+ <node name="access-control">
+ <properties>
+ <help>SSH user/group access controls. Directives are processed
+ in the following order: deny-users, allow-users, deny-groups and
+ allow-groups.</help>
+ </properties>
+ <children>
+ <node name="allow">
+ <properties>
+ <help>Allow user/group SSH access</help>
+ </properties>
+ <children>
+ <leafNode name="group">
+ <properties>
+ <help>Allow members of a group to login</help>
+ <constraint>
+ <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex>
+ </constraint>
+ <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Allow specific users to login</help>
+ <constraint>
+ <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex>
+ </constraint>
+ <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="deny">
+ <properties>
+ <help>Deny user/group SSH access</help>
+ </properties>
+ <children>
+ <leafNode name="group">
+ <properties>
+ <help>Disallow members of a group to login</help>
+ <constraint>
+ <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex>
+ </constraint>
+ <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Disallow specific users to login</help>
+ <constraint>
+ <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex>
+ </constraint>
+ <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="ciphers">
+ <properties>
+ <help>Allowed ciphers</help>
+ <completionHelp>
+ <!-- generated by ssh -Q cipher | tr '\n' ' ' as this will not change dynamically -->
+ <list>3des-cbc aes128-cbc aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com</list>
+ </completionHelp>
+ <constraint>
+ <regex>^(3des-cbc|aes128-cbc|aes192-cbc|aes256-cbc|rijndael-cbc@lysator.liu.se|aes128-ctr|aes192-ctr|aes256-ctr|aes128-gcm@openssh.com|aes256-gcm@openssh.com|chacha20-poly1305@openssh.com)$</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-host-validation">
+ <properties>
+ <help>Disable IP Address to Hostname lookup</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-password-authentication">
+ <properties>
+ <help>Disable password-based authentication</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="key-exchange">
+ <properties>
+ <help>Allowed key exchange (KEX) algorithms</help>
+ <completionHelp>
+ <!-- generated by ssh -Q kex | tr '\n' ' ' as this will not change dynamically -->
+ <list>diffie-hellman-group1-sha1 diffie-hellman-group14-sha1 diffie-hellman-group14-sha256 diffie-hellman-group16-sha512 diffie-hellman-group18-sha512 diffie-hellman-group-exchange-sha1 diffie-hellman-group-exchange-sha256 ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 curve25519-sha256 curve25519-sha256@libssh.org</list>
+ </completionHelp>
+ <multi/>
+ <constraint>
+ <regex>^(diffie-hellman-group1-sha1|diffie-hellman-group14-sha1|diffie-hellman-group14-sha256|diffie-hellman-group16-sha512|diffie-hellman-group18-sha512|diffie-hellman-group-exchange-sha1|diffie-hellman-group-exchange-sha256|ecdh-sha2-nistp256|ecdh-sha2-nistp384|ecdh-sha2-nistp521|curve25519-sha256|curve25519-sha256@libssh.org)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="listen-address">
+ <properties>
+ <help>Local addresses SSH service should listen on</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IP address to listen for incoming connections</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address to listen for incoming connections</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="loglevel">
+ <properties>
+ <help>Log level</help>
+ <completionHelp>
+ <list>quiet fatal error info verbose</list>
+ </completionHelp>
+ <valueHelp>
+ <format>quiet</format>
+ <description>stay silent</description>
+ </valueHelp>
+ <valueHelp>
+ <format>fatal</format>
+ <description>log fatals only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>error</format>
+ <description>log errors and fatals only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>default log level</description>
+ </valueHelp>
+ <valueHelp>
+ <format>verbose</format>
+ <description>enable logging of failed login attempts</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(quiet|fatal|error|info|verbose)$</regex>
+ </constraint>
+ </properties>
+ <defaultValue>INFO</defaultValue>
+ </leafNode>
+ <leafNode name="mac">
+ <properties>
+ <help>Allowed message authentication code (MAC) algorithms</help>
+ <completionHelp>
+ <!-- generated by ssh -Q mac | tr '\n' ' ' as this will not change dynamically -->
+ <list>hmac-sha1 hmac-sha1-96 hmac-sha2-256 hmac-sha2-512 hmac-md5 hmac-md5-96 umac-64@openssh.com umac-128@openssh.com hmac-sha1-etm@openssh.com hmac-sha1-96-etm@openssh.com hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-md5-etm@openssh.com hmac-md5-96-etm@openssh.com umac-64-etm@openssh.com umac-128-etm@openssh.com</list>
+ </completionHelp>
+ <constraint>
+ <regex>^(hmac-sha1|hmac-sha1-96|hmac-sha2-256|hmac-sha2-512|hmac-md5|hmac-md5-96|umac-64@openssh.com|umac-128@openssh.com|hmac-sha1-etm@openssh.com|hmac-sha1-96-etm@openssh.com|hmac-sha2-256-etm@openssh.com|hmac-sha2-512-etm@openssh.com|hmac-md5-etm@openssh.com|hmac-md5-96-etm@openssh.com|umac-64-etm@openssh.com|umac-128-etm@openssh.com)$</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Port for SSH service</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>22</defaultValue>
+ </leafNode>
+ <leafNode name="client-keepalive-interval">
+ <properties>
+ <help>Enable transmission of keepalives from server to client</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Time interval in seconds for keepalive message</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/interface-vrf.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-console.xml.in b/interface-definitions/system-console.xml.in
new file mode 100644
index 000000000..71e63d0cb
--- /dev/null
+++ b/interface-definitions/system-console.xml.in
@@ -0,0 +1,90 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="console" owner="${vyos_conf_scripts_dir}/system_console.py">
+ <properties>
+ <help>Serial console configuration</help>
+ <priority>100</priority>
+ </properties>
+ <children>
+ <tagNode name="device">
+ <properties>
+ <help>Serial console device name</help>
+ <completionHelp>
+ <script>ls -1 /dev | grep -e ttyS -e hvc</script>
+ <script>if [ -d /dev/serial/by-bus ]; then ls -1 /dev/serial/by-bus; fi</script>
+ </completionHelp>
+ <valueHelp>
+ <format>ttySN</format>
+ <description>TTY device name, regular serial port</description>
+ </valueHelp>
+ <valueHelp>
+ <format>usbNbXpY</format>
+ <description>TTY device name, USB based</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hvcN</format>
+ <description>Xen console</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(ttyS[0-9]+|hvc[0-9]+|usb[0-9]+b.*)$</regex>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="speed">
+ <properties>
+ <help>Console baud rate</help>
+ <completionHelp>
+ <list>1200 2400 4800 9600 19200 38400 57600 115200</list>
+ </completionHelp>
+ <valueHelp>
+ <format>1200</format>
+ <description>1200 bps</description>
+ </valueHelp>
+ <valueHelp>
+ <format>2400</format>
+ <description>2400 bps</description>
+ </valueHelp>
+ <valueHelp>
+ <format>4800</format>
+ <description>4800 bps</description>
+ </valueHelp>
+ <valueHelp>
+ <format>9600</format>
+ <description>9600 bps</description>
+ </valueHelp>
+ <valueHelp>
+ <format>19200</format>
+ <description>19200 bps</description>
+ </valueHelp>
+ <valueHelp>
+ <format>38400</format>
+ <description>38400 bps</description>
+ </valueHelp>
+ <valueHelp>
+ <format>57600</format>
+ <description>57600 bps</description>
+ </valueHelp>
+ <valueHelp>
+ <format>115200</format>
+ <description>115200 bps</description>
+ </valueHelp>
+ <constraint>
+ <regex>(1200|2400|4800|9600|19200|38400|57600|115200)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="powersave">
+ <properties>
+ <help>Enable screen blank powersaving on VGA console</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-ip.xml.in b/interface-definitions/system-ip.xml.in
new file mode 100644
index 000000000..14b3b8a07
--- /dev/null
+++ b/interface-definitions/system-ip.xml.in
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="ip" owner="${vyos_conf_scripts_dir}/system-ip.py">
+ <properties>
+ <help>IPv4 Settings</help>
+ <priority>400</priority>
+ </properties>
+ <children>
+ <node name="arp">
+ <properties>
+ <help>Parameters for ARP cache</help>
+ </properties>
+ <children>
+ <leafNode name="table-size">
+ <properties>
+ <help>Maximum number of entries to keep in the ARP cache</help>
+ <completionHelp>
+ <list>1024 2048 4096 8192 16384 32768</list>
+ </completionHelp>
+ <constraint>
+ <regex>(1024|2048|4096|8192|16384|32768)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="disable-forwarding">
+ <properties>
+ <help>Disable IPv4 forwarding on all interfaces</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="multipath">
+ <properties>
+ <help>IPv4 multipath settings</help>
+ </properties>
+ <children>
+ <leafNode name="ignore-unreachable-nexthops">
+ <properties>
+ <help>Ignore next hops that are not in the ARP table</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="layer4-hashing">
+ <properties>
+ <help>Use layer 4 information for ECMP hashing</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-ipv6.xml.in b/interface-definitions/system-ipv6.xml.in
new file mode 100644
index 000000000..47fbeb4e1
--- /dev/null
+++ b/interface-definitions/system-ipv6.xml.in
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="ipv6" owner="${vyos_conf_scripts_dir}/system-ipv6.py">
+ <properties>
+ <help>IPv6 Settings</help>
+ <priority>290</priority>
+ </properties>
+ <children>
+ <leafNode name="disable-forwarding">
+ <properties>
+ <help>Disable IPv6 forwarding on all interfaces</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable assignment of IPv6 addresses on all interfaces</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="multipath">
+ <properties>
+ <help>IPv4 multipath settings</help>
+ </properties>
+ <children>
+ <leafNode name="layer4-hashing">
+ <properties>
+ <help>Use layer 4 information for ECMP hashing</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="neighbor">
+ <properties>
+ <help>Parameters for Neighbor cache</help>
+ </properties>
+ <children>
+ <leafNode name="table-size">
+ <properties>
+ <help>Maximum number of entries to keep in the Neighbor cache</help>
+ <completionHelp>
+ <list>1024 2048 4096 8192 16384 32768</list>
+ </completionHelp>
+ <constraint>
+ <regex>(1024|2048|4096|8192|16384|32768)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="strict-dad">
+ <properties>
+ <help>Disable IPv6 operation on interface when DAD fails on LL addr</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-lcd.xml.in b/interface-definitions/system-lcd.xml.in
new file mode 100644
index 000000000..36116ae1b
--- /dev/null
+++ b/interface-definitions/system-lcd.xml.in
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="lcd" owner="${vyos_conf_scripts_dir}/system_lcd.py">
+ <properties>
+ <help>System LCD display</help>
+ <priority>100</priority>
+ </properties>
+ <children>
+ <leafNode name="model">
+ <properties>
+ <help>Model of the display attached to this system [REQUIRED]</help>
+ <completionHelp>
+ <list>cfa-533 cfa-631 cfa-633 cfa-635 sdec</list>
+ </completionHelp>
+ <valueHelp>
+ <format>cfa-533</format>
+ <description>Crystalfontz CFA-533</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cfa-631</format>
+ <description>Crystalfontz CFA-631</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cfa-633</format>
+ <description>Crystalfontz CFA-633</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cfa-635</format>
+ <description>Crystalfontz CFA-635</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sdec</format>
+ <description>Lanner, Watchguard, Nexcom NSA, Sophos UTM appliances</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(cfa-533|cfa-631|cfa-633|cfa-635|sdec)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="device">
+ <properties>
+ <help>Physical device used by LCD display</help>
+ <completionHelp>
+ <script>ls -1 /dev | grep ttyS</script>
+ <script>if [ -d /dev/serial/by-bus ]; then ls -1 /dev/serial/by-bus; fi</script>
+ </completionHelp>
+ <valueHelp>
+ <format>ttySXX</format>
+ <description>TTY device name, regular serial port</description>
+ </valueHelp>
+ <valueHelp>
+ <format>usbNbXpY</format>
+ <description>TTY device name, USB based</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(ttyS[0-9]+|usb[0-9]+b.*)$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-login-banner.xml.in b/interface-definitions/system-login-banner.xml.in
new file mode 100644
index 000000000..c4bb14bd6
--- /dev/null
+++ b/interface-definitions/system-login-banner.xml.in
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="login" owner="${vyos_conf_scripts_dir}/system-login.py">
+ <properties>
+ <help>System User Login Configuration</help>
+ <priority>400</priority>
+ </properties>
+ <children>
+ <node name="banner" owner="${vyos_conf_scripts_dir}/system-login-banner.py">
+ <properties>
+ <help>System login banners</help>
+ </properties>
+ <children>
+ <leafNode name="post-login">
+ <properties>
+ <help>System loging banner post-login</help>
+ </properties>
+ </leafNode>
+ <leafNode name="pre-login">
+ <properties>
+ <help>System loging banner pre-login</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in
new file mode 100644
index 000000000..812a50c8a
--- /dev/null
+++ b/interface-definitions/system-login.xml.in
@@ -0,0 +1,152 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="login" owner="${vyos_conf_scripts_dir}/system-login.py">
+ <properties>
+ <help>System User Login Configuration</help>
+ <priority>400</priority>
+ </properties>
+ <children>
+ <tagNode name="user">
+ <properties>
+ <help>Local user account information</help>
+ <constraint>
+ <regex>[a-zA-Z0-9\-_\.]{1,100}</regex>
+ </constraint>
+ <constraintErrorMessage>Username contains illegal characters or\nexceeds 100 character limitation.</constraintErrorMessage>
+ </properties>
+ <children>
+ <node name="authentication">
+ <properties>
+ <help>Password authentication</help>
+ </properties>
+ <children>
+ <leafNode name="encrypted-password">
+ <properties>
+ <help>Encrypted password</help>
+ <constraint>
+ <regex>(\*|\!)</regex>
+ <regex>[a-zA-Z0-9\.\/]{13}$</regex>
+ <regex>\$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22}</regex>
+ <regex>\$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43}</regex>
+ <regex>\$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86}</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid encrypted password for $VAR(../../@).</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="plaintext-password">
+ <properties>
+ <help>Plaintext password used for encryption</help>
+ </properties>
+ </leafNode>
+ <tagNode name="public-keys">
+ <properties>
+ <help>Remote access public keys</help>
+ <valueHelp>
+ <format>&gt;identifier&lt;</format>
+ <description>Key identifier used by ssh-keygen (usually of form user@host)</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>Public key value (base64-encoded)</help>
+ </properties>
+ </leafNode>
+ <leafNode name="options">
+ <properties>
+ <help>Optional public key options</help>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help></help>
+ <completionHelp>
+ <list>ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ssh-dss</format>
+ <description/>
+ </valueHelp>
+ <valueHelp>
+ <format>ssh-rsa</format>
+ <description/>
+ </valueHelp>
+ <valueHelp>
+ <format>ecdsa-sha2-nistp256</format>
+ <description/>
+ </valueHelp>
+ <valueHelp>
+ <format>ecdsa-sha2-nistp384</format>
+ <description/>
+ </valueHelp>
+ <valueHelp>
+ <format>ssh-ed25519</format>
+ <description/>
+ </valueHelp>
+ <constraint>
+ <regex>(ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <leafNode name="full-name">
+ <properties>
+ <help>Full name of the user (use quotes for names with spaces)</help>
+ <constraint>
+ <regex>[^:]*$</regex>
+ </constraint>
+ <constraintErrorMessage>Cannot use ':' in full name</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="home-directory">
+ <properties>
+ <help>Home directory</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ #include <include/radius-server.xml.i>
+ <node name="radius">
+ <children>
+ <tagNode name="server">
+ <children>
+ <leafNode name="timeout">
+ <properties>
+ <help>Session timeout</help>
+ <valueHelp>
+ <format>1-30</format>
+ <description>Session timeout in seconds (default: 2)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-30"/>
+ </constraint>
+ <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="priority">
+ <properties>
+ <help>Server priority</help>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Server priority (default: 255)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ #include <include/interface-vrf.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-options.xml.in b/interface-definitions/system-options.xml.in
new file mode 100644
index 000000000..a5fec10db
--- /dev/null
+++ b/interface-definitions/system-options.xml.in
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="options" owner="${vyos_conf_scripts_dir}/system-options.py">
+ <properties>
+ <help>System Options</help>
+ <priority>9999</priority>
+ </properties>
+ <children>
+ <leafNode name="beep-if-fully-booted">
+ <properties>
+ <help>plays sound via system speaker when you can login</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ctrl-alt-del-action">
+ <properties>
+ <help>Ctrl-Alt-Delete action</help>
+ <completionHelp>
+ <list>ignore reboot poweroff</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ignore</format>
+ <description>Ignore Ctrl-Alt-Delete</description>
+ </valueHelp>
+ <valueHelp>
+ <format>reboot</format>
+ <description>Reboot VyOS</description>
+ </valueHelp>
+ <valueHelp>
+ <format>poweroff</format>
+ <description>Poweroff VyOS</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(ignore|reboot|poweroff)$</regex>
+ </constraint>
+ <constraintErrorMessage>Must be ignore, reboot, or poweroff</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="reboot-on-panic">
+ <properties>
+ <help>Reboot system on kernel panic</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="http-client">
+ <properties>
+ <help>Global options used for HTTP client</help>
+ </properties>
+ <children>
+ #include <include/source-interface.xml.i>
+ #include <include/source-address-ipv4-ipv6.xml.i>
+ </children>
+ </node>
+ <node name="ssh-client">
+ <properties>
+ <help>Global options used for SSH client</help>
+ </properties>
+ <children>
+ #include <include/source-address-ipv4-ipv6.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-proxy.xml.in b/interface-definitions/system-proxy.xml.in
new file mode 100644
index 000000000..540fa97e3
--- /dev/null
+++ b/interface-definitions/system-proxy.xml.in
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="proxy" owner="${vyos_conf_scripts_dir}/system-proxy.py">
+ <properties>
+ <help>Sets a proxy for system wide use</help>
+ </properties>
+ <children>
+ <leafNode name="url">
+ <properties>
+ <help>Proxy URL</help>
+ <constraint>
+ <regex>http:\/\/[a-z0-9\.]+$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Proxy port</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="username">
+ <properties>
+ <help>Proxy username</help>
+ <constraint>
+ <regex>[a-z0-9-_\.]{1,100}$</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>Proxy password</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-syslog.xml.in b/interface-definitions/system-syslog.xml.in
new file mode 100644
index 000000000..194cdb851
--- /dev/null
+++ b/interface-definitions/system-syslog.xml.in
@@ -0,0 +1,949 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="syslog" owner="${vyos_conf_scripts_dir}/system-syslog.py">
+ <properties>
+ <help>System logging</help>
+ <priority>400</priority>
+ </properties>
+ <children>
+ <tagNode name="user">
+ <properties>
+ <help>Logging to specific terminal of given user</help>
+ <constraint>
+ <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex>
+ </constraint>
+ <constraintErrorMessage>illegal characters in user</constraintErrorMessage>
+ <valueHelp>
+ <format>username</format>
+ <description>user login name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <tagNode name="facility">
+ <properties>
+ <help>Facility for logging</help>
+ <completionHelp>
+ <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list>
+ </completionHelp>
+ <constraint>
+ <regex>(auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid facility type</constraintErrorMessage>
+ <valueHelp>
+ <format>all</format>
+ <description>All facilities excluding "mark"</description>
+ </valueHelp>
+ <valueHelp>
+ <format>auth</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>authpriv</format>
+ <description>Non-system authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cron</format>
+ <description>Cron daemon</description>
+ </valueHelp>
+ <valueHelp>
+ <format>daemon</format>
+ <description>System daemons</description>
+ </valueHelp>
+ <valueHelp>
+ <format>kern</format>
+ <description>Kernel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>lpr</format>
+ <description>Line printer spooler</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mail</format>
+ <description>Mail subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mark</format>
+ <description>Timestamp</description>
+ </valueHelp>
+ <valueHelp>
+ <format>news</format>
+ <description>USENET subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>protocols</format>
+ <description>depricated will be set to local7</description>
+ </valueHelp>
+ <valueHelp>
+ <format>security</format>
+ <description>depricated will be set to auth</description>
+ </valueHelp>
+ <valueHelp>
+ <format>syslog</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>user</format>
+ <description>Application processes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>uucp</format>
+ <description>UUCP subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local0</format>
+ <description>Local facility 0</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local1</format>
+ <description>Local facility 1</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local2</format>
+ <description>Local facility 2</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local3</format>
+ <description>Local facility 3</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local4</format>
+ <description>Local facility 4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local5</format>
+ <description>Local facility 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local6</format>
+ <description>Local facility 6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local7</format>
+ <description>Local facility 7</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="level">
+ <properties>
+ <help>Logging level</help>
+ <completionHelp>
+ <list>emerg alert crit err warning notice info debug all</list>
+ </completionHelp>
+ <constraint>
+ <regex>(emerg|alert|crit|err|warning|notice|info|debug|all)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid loglevel</constraintErrorMessage>
+ <valueHelp>
+ <format>emerg</format>
+ <description>Emergency messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>alert</format>
+ <description>Urgent messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>crit</format>
+ <description>Critical messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>err</format>
+ <description>Error messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>warning</format>
+ <description>Warning messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>notice</format>
+ <description>Messages for further investigation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>Informational messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>Debug messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>Log everything</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="host">
+ <properties>
+ <help>Logging to a remote host</help>
+ <constraint>
+ <validator name="ip-address"/>
+ <validator name="fqdn"/>
+ </constraint>
+ <constraintErrorMessage>Invalid host (FQDN or IP address)</constraintErrorMessage>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Remote syslog server IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hostname</format>
+ <description>Remote syslog server FQDN</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="port">
+ <properties>
+ <help>Destination port</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Destination port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Invalid destination port value</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <tagNode name="facility">
+ <properties>
+ <help>Facility for logging</help>
+ <completionHelp>
+ <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list>
+ </completionHelp>
+ <constraint>
+ <regex>(auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid facility type</constraintErrorMessage>
+ <valueHelp>
+ <format>all</format>
+ <description>All facilities excluding "mark"</description>
+ </valueHelp>
+ <valueHelp>
+ <format>auth</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>authpriv</format>
+ <description>Non-system authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cron</format>
+ <description>Cron daemon</description>
+ </valueHelp>
+ <valueHelp>
+ <format>daemon</format>
+ <description>System daemons</description>
+ </valueHelp>
+ <valueHelp>
+ <format>kern</format>
+ <description>Kernel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>lpr</format>
+ <description>Line printer spooler</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mail</format>
+ <description>Mail subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mark</format>
+ <description>Timestamp</description>
+ </valueHelp>
+ <valueHelp>
+ <format>news</format>
+ <description>USENET subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>protocols</format>
+ <description>depricated will be set to local7</description>
+ </valueHelp>
+ <valueHelp>
+ <format>security</format>
+ <description>depricated will be set to auth</description>
+ </valueHelp>
+ <valueHelp>
+ <format>syslog</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>user</format>
+ <description>Application processes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>uucp</format>
+ <description>UUCP subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local0</format>
+ <description>Local facility 0</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local1</format>
+ <description>Local facility 1</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local2</format>
+ <description>Local facility 2</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local3</format>
+ <description>Local facility 3</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local4</format>
+ <description>Local facility 4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local5</format>
+ <description>Local facility 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local6</format>
+ <description>Local facility 6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local7</format>
+ <description>Local facility 7</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="protocol">
+ <properties>
+ <help>syslog communication protocol</help>
+ <valueHelp>
+ <format>udp</format>
+ <description>send log messages to remote syslog server over udp</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp</format>
+ <description>send log messages to remote syslog server over tcp</description>
+ </valueHelp>
+ <completionHelp>
+ <list>udp tcp</list>
+ </completionHelp>
+ <constraint>
+ <regex>(udp|tcp)</regex>
+ </constraint>
+ <constraintErrorMessage>invalid protocol name</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="level">
+ <properties>
+ <help>Logging level</help>
+ <completionHelp>
+ <list>emerg alert crit err warning notice info debug all</list>
+ </completionHelp>
+ <constraint>
+ <regex>(emerg|alert|crit|err|warning|notice|info|debug|all)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid loglevel</constraintErrorMessage>
+ <valueHelp>
+ <format>emerg</format>
+ <description>Emergency messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>alert</format>
+ <description>Urgent messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>crit</format>
+ <description>Critical messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>err</format>
+ <description>Error messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>warning</format>
+ <description>Warning messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>notice</format>
+ <description>Messages for further investigation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>Informational messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>Debug messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>Log everything</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <node name="global">
+ <properties>
+ <help>Logging to system standard location</help>
+ </properties>
+ <children>
+ <node name="archive">
+ <properties>
+ <help>Log file size and rotation characteristics</help>
+ </properties>
+ <children>
+ <leafNode name="file">
+ <properties>
+ <help>Number of saved files (default is 5)</help>
+ <constraint>
+ <regex>[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="size">
+ <properties>
+ <help>Size of log files (in kbytes, default is 256)</help>
+ <constraint>
+ <regex>[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>illegal characters in size</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="facility">
+ <properties>
+ <help>Facility for logging</help>
+ <completionHelp>
+ <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list>
+ </completionHelp>
+ <constraint>
+ <regex>(auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid facility type</constraintErrorMessage>
+ <valueHelp>
+ <format>all</format>
+ <description>All facilities excluding "mark"</description>
+ </valueHelp>
+ <valueHelp>
+ <format>auth</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>authpriv</format>
+ <description>Non-system authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cron</format>
+ <description>Cron daemon</description>
+ </valueHelp>
+ <valueHelp>
+ <format>daemon</format>
+ <description>System daemons</description>
+ </valueHelp>
+ <valueHelp>
+ <format>kern</format>
+ <description>Kernel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>lpr</format>
+ <description>Line printer spooler</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mail</format>
+ <description>Mail subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mark</format>
+ <description>Timestamp</description>
+ </valueHelp>
+ <valueHelp>
+ <format>news</format>
+ <description>USENET subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>protocols</format>
+ <description>depricated will be set to local7</description>
+ </valueHelp>
+ <valueHelp>
+ <format>security</format>
+ <description>depricated will be set to auth</description>
+ </valueHelp>
+ <valueHelp>
+ <format>syslog</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>user</format>
+ <description>Application processes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>uucp</format>
+ <description>UUCP subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local0</format>
+ <description>Local facility 0</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local1</format>
+ <description>Local facility 1</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local2</format>
+ <description>Local facility 2</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local3</format>
+ <description>Local facility 3</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local4</format>
+ <description>Local facility 4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local5</format>
+ <description>Local facility 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local6</format>
+ <description>Local facility 6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local7</format>
+ <description>Local facility 7</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="level">
+ <properties>
+ <help>Logging level</help>
+ <completionHelp>
+ <list>emerg alert crit err warning notice info debug all</list>
+ </completionHelp>
+ <constraint>
+ <regex>(emerg|alert|crit|err|warning|notice|info|debug|all)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid loglevel</constraintErrorMessage>
+ <valueHelp>
+ <format>emerg</format>
+ <description>Emergency messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>alert</format>
+ <description>Urgent messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>crit</format>
+ <description>Critical messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>err</format>
+ <description>Error messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>warning</format>
+ <description>Warning messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>notice</format>
+ <description>Messages for further investigation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>Informational messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>Debug messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>Log everything</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="marker">
+ <properties>
+ <help>mark messages sent to syslog</help>
+ </properties>
+ <children>
+ <leafNode name="interval">
+ <properties>
+ <help>time interval how often a mark message is being sent in seconds (default: 1200)</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name ="preserve-fqdn">
+ <properties>
+ <help>uses FQDN for logging</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="file">
+ <properties>
+ <help>Logging to a file</help>
+ <constraint>
+ <regex>[a-zA-Z0-9\-_.]{1,255}</regex>
+ </constraint>
+ <constraintErrorMessage>illegal characters in filename or filename longer than 255 characters</constraintErrorMessage>
+ </properties>
+ <children>
+ <node name="archive">
+ <properties>
+ <help>Log file size and rotation characteristics</help>
+ </properties>
+ <children>
+ <leafNode name="file">
+ <properties>
+ <help>Number of saved files (default is 5)</help>
+ <constraint>
+ <regex>[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="size">
+ <properties>
+ <help>Size of log files (in kbytes, default is 256)</help>
+ <constraint>
+ <regex>[0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>illegal characters in size</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="facility">
+ <properties>
+ <help>Facility for logging</help>
+ <completionHelp>
+ <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list>
+ </completionHelp>
+ <constraint>
+ <regex>(auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid facility type</constraintErrorMessage>
+ <valueHelp>
+ <format>all</format>
+ <description>All facilities excluding "mark"</description>
+ </valueHelp>
+ <valueHelp>
+ <format>auth</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>authpriv</format>
+ <description>Non-system authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cron</format>
+ <description>Cron daemon</description>
+ </valueHelp>
+ <valueHelp>
+ <format>daemon</format>
+ <description>System daemons</description>
+ </valueHelp>
+ <valueHelp>
+ <format>kern</format>
+ <description>Kernel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>lpr</format>
+ <description>Line printer spooler</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mail</format>
+ <description>Mail subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mark</format>
+ <description>Timestamp</description>
+ </valueHelp>
+ <valueHelp>
+ <format>news</format>
+ <description>USENET subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>protocols</format>
+ <description>depricated will be set to local7</description>
+ </valueHelp>
+ <valueHelp>
+ <format>security</format>
+ <description>depricated will be set to auth</description>
+ </valueHelp>
+ <valueHelp>
+ <format>syslog</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>user</format>
+ <description>Application processes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>uucp</format>
+ <description>UUCP subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local0</format>
+ <description>Local facility 0</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local1</format>
+ <description>Local facility 1</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local2</format>
+ <description>Local facility 2</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local3</format>
+ <description>Local facility 3</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local4</format>
+ <description>Local facility 4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local5</format>
+ <description>Local facility 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local6</format>
+ <description>Local facility 6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local7</format>
+ <description>Local facility 7</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="level">
+ <properties>
+ <help>Logging level</help>
+ <completionHelp>
+ <list>emerg alert crit err warning notice info debug all</list>
+ </completionHelp>
+ <constraint>
+ <regex>(emerg|alert|crit|err|warning|notice|info|debug|all)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid loglevel</constraintErrorMessage>
+ <valueHelp>
+ <format>emerg</format>
+ <description>Emergency messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>alert</format>
+ <description>Urgent messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>crit</format>
+ <description>Critical messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>err</format>
+ <description>Error messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>warning</format>
+ <description>Warning messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>notice</format>
+ <description>Messages for further investigation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>Informational messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>Debug messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>Log everything</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <node name="console">
+ <properties>
+ <help>logging to serial console</help>
+ </properties>
+ <children>
+ <tagNode name="facility">
+ <properties>
+ <help>Facility for logging</help>
+ <completionHelp>
+ <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list>
+ </completionHelp>
+ <constraint>
+ <regex>(auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid facility type</constraintErrorMessage>
+ <valueHelp>
+ <format>all</format>
+ <description>All facilities excluding "mark"</description>
+ </valueHelp>
+ <valueHelp>
+ <format>auth</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>authpriv</format>
+ <description>Non-system authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cron</format>
+ <description>Cron daemon</description>
+ </valueHelp>
+ <valueHelp>
+ <format>daemon</format>
+ <description>System daemons</description>
+ </valueHelp>
+ <valueHelp>
+ <format>kern</format>
+ <description>Kernel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>lpr</format>
+ <description>Line printer spooler</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mail</format>
+ <description>Mail subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mark</format>
+ <description>Timestamp</description>
+ </valueHelp>
+ <valueHelp>
+ <format>news</format>
+ <description>USENET subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>protocols</format>
+ <description>depricated will be set to local7</description>
+ </valueHelp>
+ <valueHelp>
+ <format>security</format>
+ <description>depricated will be set to auth</description>
+ </valueHelp>
+ <valueHelp>
+ <format>syslog</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>user</format>
+ <description>Application processes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>uucp</format>
+ <description>UUCP subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local0</format>
+ <description>Local facility 0</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local1</format>
+ <description>Local facility 1</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local2</format>
+ <description>Local facility 2</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local3</format>
+ <description>Local facility 3</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local4</format>
+ <description>Local facility 4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local5</format>
+ <description>Local facility 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local6</format>
+ <description>Local facility 6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local7</format>
+ <description>Local facility 7</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="level">
+ <properties>
+ <help>Logging level</help>
+ <completionHelp>
+ <list>emerg alert crit err warning notice info debug all</list>
+ </completionHelp>
+ <constraint>
+ <regex>(emerg|alert|crit|err|warning|notice|info|debug|all)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid loglevel</constraintErrorMessage>
+ <valueHelp>
+ <format>emerg</format>
+ <description>Emergency messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>alert</format>
+ <description>Urgent messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>crit</format>
+ <description>Critical messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>err</format>
+ <description>Error messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>warning</format>
+ <description>Warning messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>notice</format>
+ <description>Messages for further investigation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>Informational messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>Debug messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>Log everything</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-time-zone.xml.in b/interface-definitions/system-time-zone.xml.in
new file mode 100644
index 000000000..ff815c9d3
--- /dev/null
+++ b/interface-definitions/system-time-zone.xml.in
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <leafNode name="time-zone" owner="${vyos_conf_scripts_dir}/system-timezone.py">
+ <properties>
+ <help>Local time zone (default UTC)</help>
+ <priority>100</priority>
+ <completionHelp>
+ <script>find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/:: | sort</script>
+ </completionHelp>
+ <constraint>
+ <validator name="timezone" argument="--validate"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/tftp-server.xml.in b/interface-definitions/tftp-server.xml.in
new file mode 100644
index 000000000..2874b034c
--- /dev/null
+++ b/interface-definitions/tftp-server.xml.in
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+<!-- TFTP configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="tftp-server" owner="${vyos_conf_scripts_dir}/tftp_server.py">
+ <properties>
+ <help>Trivial File Transfer Protocol (TFTP) server</help>
+ <priority>990</priority>
+ </properties>
+ <children>
+ <leafNode name="directory">
+ <properties>
+ <help>Folder containing files served by TFTP [REQUIRED]</help>
+ </properties>
+ </leafNode>
+ <leafNode name="allow-upload">
+ <properties>
+ <help>Allow TFTP file uploads</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Port for TFTP service</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port (default: 69)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="listen-address">
+ <properties>
+ <help>Addresses for TFTP server to listen [REQUIRED]</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>TFTP IPv4 listen address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>TFTP IPv6 listen address</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/vpn_anyconnect.xml.in b/interface-definitions/vpn_anyconnect.xml.in
new file mode 100644
index 000000000..e74326986
--- /dev/null
+++ b/interface-definitions/vpn_anyconnect.xml.in
@@ -0,0 +1,258 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="vpn">
+ <children>
+ <node name="anyconnect" owner="${vyos_conf_scripts_dir}/vpn_anyconnect.py">
+ <properties>
+ <help>SSL VPN AnyConnect</help>
+ <priority>901</priority>
+ </properties>
+ <children>
+ <node name="authentication">
+ <properties>
+ <help>Authentication for remote access SSL VPN Server</help>
+ </properties>
+ <children>
+ <leafNode name="mode">
+ <properties>
+ <help>Authentication mode used by this server</help>
+ <valueHelp>
+ <format>local</format>
+ <description>Use local username/password configuration</description>
+ </valueHelp>
+ <valueHelp>
+ <format>radius</format>
+ <description>Use RADIUS server for user autentication</description>
+ </valueHelp>
+ <constraint>
+ <regex>(local|radius)</regex>
+ </constraint>
+ <completionHelp>
+ <list>local radius</list>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <node name="local-users">
+ <properties>
+ <help>Local user authentication for SSL VPN server</help>
+ </properties>
+ <children>
+ <tagNode name="username">
+ <properties>
+ <help>User name for authentication</help>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable a SSL VPN Server user</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>Password for authentication</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ #include <include/radius-server.xml.i>
+ <node name="radius">
+ <children>
+ <leafNode name="timeout">
+ <properties>
+ <help>Session timeout</help>
+ <valueHelp>
+ <format>1-30</format>
+ <description>Session timeout in seconds (default: 2)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-30"/>
+ </constraint>
+ <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage>
+ </properties>
+ <defaultValue>2</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="listen-ports">
+ <properties>
+ <help>SSL Certificate, SSL Key and CA (/config/auth)</help>
+ </properties>
+ <children>
+ <leafNode name="tcp">
+ <properties>
+ <help>tcp port number to accept connections (default: 443)</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port (default: 443)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>443</defaultValue>
+ </leafNode>
+ <leafNode name="udp">
+ <properties>
+ <help>udp port number to accept connections (default: 443)</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port (default: 443)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>443</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ <node name="ssl">
+ <properties>
+ <help>SSL Certificate, SSL Key and CA (/config/auth)</help>
+ </properties>
+ <children>
+ <leafNode name="ca-cert-file">
+ <properties>
+ <help>Certificate Authority certificate</help>
+ <completionHelp>
+ <script>ls /config/auth</script>
+ </completionHelp>
+ <valueHelp>
+ <format>file</format>
+ <description>File in /config/auth directory</description>
+ </valueHelp>
+ <constraint>
+ <validator name="file-exists" argument="--directory /config"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="cert-file">
+ <properties>
+ <help>Server Certificate</help>
+ <valueHelp>
+ <format>file</format>
+ <description>File in /config/auth directory</description>
+ </valueHelp>
+ <constraint>
+ <validator name="file-exists" argument="--directory /config"/>
+ </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"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="network-settings">
+ <properties>
+ <help>Network settings</help>
+ </properties>
+ <children>
+ <leafNode name="push-route">
+ <properties>
+ <help>Route to be pushed to the client</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 network and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 network and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="client-ip-settings">
+ <properties>
+ <help>Client IP pools settings</help>
+ </properties>
+ <children>
+ <leafNode name="subnet">
+ <properties>
+ <help>Client IP subnet (CIDR notation)</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="client-ipv6-pool">
+ <properties>
+ <help>Pool of client IPv6 addresses</help>
+ </properties>
+ <children>
+ <leafNode name="prefix">
+ <properties>
+ <help>Pool of addresses used to assign to clients</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mask">
+ <properties>
+ <help>Prefix length used for individual client</help>
+ <valueHelp>
+ <format>&lt;48-128&gt;</format>
+ <description>Client prefix length (default: 64)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 48-128"/>
+ </constraint>
+ </properties>
+ <defaultValue>64</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="name-server">
+ <properties>
+ <help>Domain Name Server (DNS) propagated to client</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Domain Name Server (DNS) IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+</node>
+</interfaceDefinition>
diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in
new file mode 100644
index 000000000..702ef8b5a
--- /dev/null
+++ b/interface-definitions/vpn_l2tp.xml.in
@@ -0,0 +1,457 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="vpn">
+ <children>
+ <node name="l2tp" owner="${vyos_conf_scripts_dir}/vpn_l2tp.py">
+ <properties>
+ <help>L2TP Virtual Private Network (VPN)</help>
+ </properties>
+ <children>
+ <node name="remote-access">
+ <properties>
+ <help>Remote access L2TP VPN</help>
+ </properties>
+ <children>
+ <leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <constraint>
+ <validator name="numeric" argument="--range 128-16384"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="outside-address">
+ <properties>
+ <help>External IP address to which VPN clients will connect</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="gateway-address">
+ <properties>
+ <help>Gatway address uses as client tunnel termination point</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/accel-name-server.xml.in>
+ <node name="lns">
+ <properties>
+ <help>L2TP Network Server (LNS)</help>
+ </properties>
+ <children>
+ <leafNode name="shared-secret">
+ <properties>
+ <help>Tunnel password used to authenticate the client (LAC)</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="ccp-disable">
+ <properties>
+ <help>Disable Compression Control Protocol (CCP)</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ <node name="ipsec-settings">
+ <properties>
+ <help>Internet Protocol Security (IPsec) for remote access L2TP VPN</help>
+ </properties>
+ <children>
+ <node name="authentication">
+ <properties>
+ <help>IPsec authentication settings</help>
+ </properties>
+ <children>
+ <leafNode name="mode">
+ <properties>
+ <help>Authentication mode for IPsec</help>
+ <valueHelp>
+ <format>pre-shared-secret</format>
+ <description>Use pre-shared secret for IPsec authentication</description>
+ </valueHelp>
+ <valueHelp>
+ <format>x509</format>
+ <description>Use X.509 certificate for IPsec authentication</description>
+ </valueHelp>
+ <constraint>
+ <regex>(pre-shared-secret|x509)</regex>
+ </constraint>
+ <completionHelp>
+ <list>pre-shared-secret x509</list>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="pre-shared-secret">
+ <properties>
+ <help>Pre-shared secret for IPsec</help>
+ </properties>
+ </leafNode>
+ <node name="x509">
+ <properties>
+ <help>X.509 certificate</help>
+ </properties>
+ <children>
+ <leafNode name="ca-cert-file">
+ <properties>
+ <help>File containing the X.509 certificate for the Certificate Authority (CA)</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>File in /config/auth</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="crl-file">
+ <properties>
+ <help>File containing the X.509 Certificate Revocation List (CRL)</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>File in /config/auth</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="server-cert-file">
+ <properties>
+ <help>File containing the X.509 certificate for the remote access VPN server (this host)</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>File in /config/auth</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="server-key-file">
+ <properties>
+ <help>File containing the private key for the X.509 certificate for the remote access VPN server (this host)</help>
+ <valueHelp>
+ <format>&lt;text&gt;</format>
+ <description>File in /config/auth</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="server-key-password">
+ <properties>
+ <help>Password that protects the private key</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="ike-lifetime">
+ <properties>
+ <help>IKE lifetime</help>
+ <valueHelp>
+ <format>&lt;30-86400&gt;</format>
+ <description>IKE lifetime in seconds (default 3600)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 30-86400"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="lifetime">
+ <properties>
+ <help>ESP lifetime</help>
+ <valueHelp>
+ <format>&lt;30-86400&gt;</format>
+ <description>IKE lifetime in seconds (default 3600)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 30-86400"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/accel-wins-server.xml.i>
+ <node name="client-ip-pool">
+ <properties>
+ <help>Pool of client IP addresses (must be within a /24)</help>
+ </properties>
+ <children>
+ <leafNode name="start">
+ <properties>
+ <help>First IP address in the pool (will be used as gateway address)</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="stop">
+ <properties>
+ <help>Last IP address in the pool</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="subnet">
+ <properties>
+ <help>Client IP subnet (CIDR notation)</help>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 subnet address</description>
+ </valueHelp>
+ <multi />
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/accel-client-ipv6-pool.xml.in>
+ <leafNode name="description">
+ <properties>
+ <help>Description for L2TP remote-access settings</help>
+ </properties>
+ </leafNode>
+ <leafNode name="dhcp-interface">
+ <properties>
+ <help>DHCP interface to listen on</help>
+ </properties>
+ </leafNode>
+ <leafNode name="idle">
+ <properties>
+ <help>PPP idle timeout</help>
+ <valueHelp>
+ <format>&lt;30-86400&gt;</format>
+ <description>PPP idle timeout in seconds (default 1800)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 30-86400"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="authentication">
+ <properties>
+ <help>Authentication for remote access L2TP VPN</help>
+ </properties>
+ <children>
+ <leafNode name="require">
+ <properties>
+ <help>Authentication protocol for remote access peer L2TP VPN</help>
+ <valueHelp>
+ <format>pap</format>
+ <description>Require the peer to authenticate itself using PAP [Password Authentication Protocol].</description>
+ </valueHelp>
+ <valueHelp>
+ <format>chap</format>
+ <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mschap</format>
+ <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mschap-v2</format>
+ <description>Require the peer to authenticate itself using MS-CHAPv2 [Microsoft Challenge Handshake Authentication Protocol, Version 2].</description>
+ </valueHelp>
+ <constraint>
+ <regex>(pap|chap|mschap|mschap-v2)</regex>
+ </constraint>
+ <completionHelp>
+ <list>pap chap mschap mschap-v2</list>
+ </completionHelp>
+ <multi />
+ </properties>
+ </leafNode>
+ <leafNode name="mppe">
+ <properties>
+ <help>Specifies mppe negotioation preference. (default require mppe 128-bit stateless</help>
+ <valueHelp>
+ <format>deny</format>
+ <description>deny mppe</description>
+ </valueHelp>
+ <valueHelp>
+ <format>prefer</format>
+ <description>Ask client for mppe, if it rejects do not fail</description>
+ </valueHelp>
+ <valueHelp>
+ <format>require</format>
+ <description>ask client for mppe, if it rejects drop connection</description>
+ </valueHelp>
+ <constraint>
+ <regex>(deny|prefer|require)</regex>
+ </constraint>
+ <completionHelp>
+ <list>deny prefer require</list>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ #include <include/accel-auth-mode.xml.i>
+ <node name="local-users">
+ <properties>
+ <help>Local user authentication for remote access L2TP VPN</help>
+ </properties>
+ <children>
+ <tagNode name="username">
+ <properties>
+ <help>User name for authentication</help>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable a L2TP Server user</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>Password for authentication</help>
+ </properties>
+ </leafNode>
+ <leafNode name="static-ip">
+ <properties>
+ <help>Static client IP address</help>
+ </properties>
+ </leafNode>
+ <node name="rate-limit">
+ <properties>
+ <help>Upload/Download speed limits</help>
+ </properties>
+ <children>
+ <leafNode name="upload">
+ <properties>
+ <help>Upload bandwidth limit in kbits/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="download">
+ <properties>
+ <help>Download bandwidth limit in kbits/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ #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>
+ </properties>
+ </leafNode>
+ <leafNode name="acct-timeout">
+ <properties>
+ <help>Timeout to wait reply for Interim-Update packets. (default 3 seconds)</help>
+ </properties>
+ </leafNode>
+ <leafNode name="max-try">
+ <properties>
+ <help>Maximum number of tries to send Access-Request/Accounting-Request queries</help>
+ </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>
+ </properties>
+ </leafNode>
+ <node name="dae-server">
+ <properties>
+ <help>IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA)</help>
+ </properties>
+ <children>
+ <leafNode name="ip-address">
+ <properties>
+ <help>IP address for Dynamic Authorization Extension server (DM/CoA)</help>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Port for Dynamic Authorization Extension server (DM/CoA)</help>
+ </properties>
+ </leafNode>
+ <leafNode name="secret">
+ <properties>
+ <help>Secret for Dynamic Authorization Extension server (DM/CoA)</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="rate-limit">
+ <properties>
+ <help>Upload/Download speed limits</help>
+ </properties>
+ <children>
+ <leafNode name="attribute">
+ <properties>
+ <help>Specifies which radius attribute contains rate information. (default is 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>
+ </properties>
+ </leafNode>
+ <leafNode name="enable">
+ <properties>
+ <help>Enables Bandwidth shaping via RADIUS</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="ppp-options">
+ <properties>
+ <help>Advanced protocol options</help>
+ </properties>
+ <children>
+ <leafNode name="lcp-echo-interval">
+ <properties>
+ <help>LCP echo-requests/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="lcp-echo-failure">
+ <properties>
+ <help>Maximum number of Echo-Requests may be sent without valid reply</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/vpn_pptp.xml.in b/interface-definitions/vpn_pptp.xml.in
new file mode 100644
index 000000000..032455b4d
--- /dev/null
+++ b/interface-definitions/vpn_pptp.xml.in
@@ -0,0 +1,165 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="vpn">
+ <children>
+ <node name="pptp" owner="${vyos_conf_scripts_dir}/vpn_pptp.py">
+ <properties>
+ <help>Point to Point Tunneling Protocol (PPTP) Virtual Private Network (VPN)</help>
+ </properties>
+ <children>
+ <node name="remote-access">
+ <properties>
+ <help>Remote access PPTP VPN</help>
+ </properties>
+ <children>
+ <leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <constraint>
+ <validator name="numeric" argument="--range 128-16384"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="outside-address">
+ <properties>
+ <help>External IP address to which VPN clients will connect</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="name-server">
+ <properties>
+ <help>Domain Name Server (DNS) propagated to client</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Domain Name Server (DNS) IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ #include <include/accel-wins-server.xml.i>
+ <node name="client-ip-pool">
+ <properties>
+ <help>Pool of client IP addresses (must be within a /24)</help>
+ </properties>
+ <children>
+ <leafNode name="start">
+ <properties>
+ <help>First IP address in the pool (will be used as gateway address)</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="stop">
+ <properties>
+ <help>Last IP address in the pool</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="gateway-address">
+ <properties>
+ <help>Gatway address uses as client tunnel termination point</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="authentication">
+ <properties>
+ <help>Authentication for remote access PPTP VPN</help>
+ </properties>
+ <children>
+ <leafNode name="require">
+ <properties>
+ <help>Authentication protocol for remote access peer PPTP VPN</help>
+ <valueHelp>
+ <format>pap</format>
+ <description>Require the peer to authenticate itself using PAP [Password Authentication Protocol].</description>
+ </valueHelp>
+ <valueHelp>
+ <format>chap</format>
+ <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mschap</format>
+ <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mschap-v2</format>
+ <description>Require the peer to authenticate itself using MS-CHAPv2 [Microsoft Challenge Handshake Authentication Protocol, Version 2].</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="mppe">
+ <properties>
+ <help>Specifies mppe negotioation preference. (default require mppe 128-bit stateless</help>
+ <valueHelp>
+ <format>deny</format>
+ <description>deny mppe</description>
+ </valueHelp>
+ <valueHelp>
+ <format>prefer</format>
+ <description>ask client for mppe, if it rejects do not fail</description>
+ </valueHelp>
+ <valueHelp>
+ <format>require</format>
+ <description>ask client for mppe, if it rejects drop connection</description>
+ </valueHelp>
+ <constraint>
+ <regex>(deny|prefer|require)</regex>
+ </constraint>
+ <completionHelp>
+ <list>deny prefer require</list>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ #include <include/accel-auth-mode.xml.i>
+ <node name="local-users">
+ <properties>
+ <help>Local user authentication for remote access PPTP VPN</help>
+ </properties>
+ <children>
+ <tagNode name="username">
+ <properties>
+ <help>User name for authentication</help>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable a PPTP Server user</help>
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>Password for authentication</help>
+ </properties>
+ </leafNode>
+ <leafNode name="static-ip">
+ <properties>
+ <help>Static client IP address</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ #include <include/radius-server.xml.i>
+ #include <include/accel-radius-additions.xml.in>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in
new file mode 100644
index 000000000..f0c93b882
--- /dev/null
+++ b/interface-definitions/vpn_sstp.xml.in
@@ -0,0 +1,273 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="vpn">
+ <children>
+ <node name="sstp" owner="${vyos_conf_scripts_dir}/vpn_sstp.py">
+ <properties>
+ <help>Secure Socket Tunneling Protocol (SSTP) server</help>
+ <priority>901</priority>
+ </properties>
+ <children>
+ <node name="authentication">
+ <properties>
+ <help>Authentication for remote access SSTP Server</help>
+ </properties>
+ <children>
+ <node name="local-users">
+ <properties>
+ <help>Local user authentication for SSTP server</help>
+ </properties>
+ <children>
+ <tagNode name="username">
+ <properties>
+ <help>User name for authentication</help>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable a SSTP Server user</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>Password for authentication</help>
+ </properties>
+ </leafNode>
+ <leafNode name="static-ip">
+ <properties>
+ <help>Static client IP address</help>
+ </properties>
+ </leafNode>
+ <node name="rate-limit">
+ <properties>
+ <help>Upload/Download speed limits</help>
+ </properties>
+ <children>
+ <leafNode name="upload">
+ <properties>
+ <help>Upload bandwidth limit in kbits/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="download">
+ <properties>
+ <help>Download bandwidth limit in kbits/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ #include <include/accel-auth-mode.xml.i>
+ <leafNode name="protocols">
+ <properties>
+ <help>Authentication protocol for remote access peer SSTP VPN</help>
+ <completionHelp>
+ <list>pap chap mschap mschap-v2</list>
+ </completionHelp>
+ <valueHelp>
+ <format>pap</format>
+ <description>Authentication via PAP (Password Authentication Protocol)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>chap</format>
+ <description>Authentication via CHAP (Challenge Handshake Authentication Protocol)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mschap</format>
+ <description>Authentication via MS-CHAP (Microsoft Challenge Handshake Authentication Protocol)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mschap-v2</format>
+ <description>Authentication via MS-CHAPv2 (Microsoft Challenge Handshake Authentication Protocol, version 2)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(pap|chap|mschap|mschap-v2)</regex>
+ </constraint>
+ <multi />
+ </properties>
+ </leafNode>
+ #include <include/radius-server.xml.i>
+ #include <include/accel-radius-additions.xml.in>
+ <node name="radius">
+ <children>
+ <node name="rate-limit">
+ <properties>
+ <help>Upload/Download speed limits</help>
+ </properties>
+ <children>
+ <leafNode name="attribute">
+ <properties>
+ <help>Specifies RADIUS attribute containing rate information (default 'Filter-Id')</help>
+ </properties>
+ </leafNode>
+ <leafNode name="vendor">
+ <properties>
+ <help>Specifies vendor dictionary (needs to be in /usr/share/accel-ppp/radius)</help>
+ </properties>
+ </leafNode>
+ <leafNode name="enable">
+ <properties>
+ <help>Enable RADIUS bandwidth shaping</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="ssl">
+ <properties>
+ <help>SSL Certificate, SSL Key and CA (/config/user-data/sstp)</help>
+ </properties>
+ <children>
+ <leafNode name="ca-cert-file">
+ <properties>
+ <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>
+ </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">
+ <properties>
+ <help>Network settings</help>
+ </properties>
+ <children>
+ <node name="client-ip-settings">
+ <properties>
+ <help>Client IP pools and gateway setting</help>
+ </properties>
+ <children>
+ <leafNode name="subnet">
+ <properties>
+ <help>Client IP subnet (CIDR notation)</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage>
+ <multi />
+ </properties>
+ </leafNode>
+ <leafNode name="gateway-address">
+ <properties>
+ <help>Gateway IP address</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <constraintErrorMessage>invalid IPv4 address</constraintErrorMessage>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Default Gateway send to the client</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/accel-client-ipv6-pool.xml.in>
+ #include <include/accel-name-server.xml.in>
+ #include <include/interface-mtu-68-1500.xml.i>
+ </children>
+ </node>
+ <node name="ppp-settings">
+ <properties>
+ <help>PPP (Point-to-Point Protocol) settings</help>
+ </properties>
+ <children>
+ <leafNode name="mppe">
+ <properties>
+ <help>Specifies mppe negotiation preferences</help>
+ <completionHelp>
+ <list>require prefer deny</list>
+ </completionHelp>
+ <constraint>
+ <regex>(^require|prefer|deny)</regex>
+ </constraint>
+ <valueHelp>
+ <format>require</format>
+ <description>send mppe request, if client rejects, drop the connection</description>
+ </valueHelp>
+ <valueHelp>
+ <format>prefer</format>
+ <description>send mppe request, if client rejects continue</description>
+ </valueHelp>
+ <valueHelp>
+ <format>deny</format>
+ <description>drop all mppe</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="lcp-echo-interval">
+ <properties>
+ <help>LCP echo-requests/sec</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="lcp-echo-failure">
+ <properties>
+ <help>Maximum number of Echo-Requests may be sent without valid reply</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="lcp-echo-timeout">
+ <properties>
+ <help>Timeout in seconds to wait for any peer activity. If this option specified it turns on adaptive lcp echo functionality and "lcp-echo-failure" is not used.</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+</node>
+</interfaceDefinition>
diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in
new file mode 100644
index 000000000..159f4ea3e
--- /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>60</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 15 characters or less and can not\nbe named as regular network interfaces.\n</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
new file mode 100644
index 000000000..120c7d218
--- /dev/null
+++ b/interface-definitions/vrrp.xml.in
@@ -0,0 +1,302 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="high-availability">
+ <properties>
+ <help>High availability settings</help>
+ </properties>
+ <children>
+ <node name="vrrp" owner="${vyos_conf_scripts_dir}/vrrp.py">
+ <properties>
+ <priority>800</priority> <!-- after all interfaces and conntrack-sync -->
+ <help>Virtual Router Redundancy Protocol settings</help>
+ </properties>
+ <children>
+ <tagNode name="group">
+ <properties>
+ <help>VRRP group</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Network interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="advertise-interval">
+ <properties>
+ <help>Advertise interval</help>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Advertise interval in seconds (default: 1)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="authentication">
+ <properties>
+ <help>VRRP authentication</help>
+ </properties>
+ <children>
+ <leafNode name="password">
+ <properties>
+ <help>VRRP password</help>
+ <valueHelp>
+ <format>text</format>
+ <description>Password string (up to 8 characters)</description>
+ </valueHelp>
+ <constraint>
+ <regex>.{1,8}</regex>
+ </constraint>
+ <constraintErrorMessage>Password must not be longer than 8 characters</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help>Authentication type</help>
+ <completionHelp>
+ <list>plaintext-password ah</list>
+ </completionHelp>
+ <constraint>
+ <regex>(plaintext-password|ah)</regex>
+ </constraint>
+ <constraintErrorMessage>Authentication type must be plaintext-password or ah</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="description">
+ <properties>
+ <help>Group description</help>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <valueless/>
+ <help>Disable VRRP group</help>
+ </properties>
+ </leafNode>
+ <node name="health-check">
+ <properties>
+ <help>Health check script</help>
+ </properties>
+ <children>
+ <leafNode name="failure-count">
+ <properties>
+ <help>Health check failure count required for transition to fault (default: 3)</help>
+ <constraint>
+ <validator name="numeric" argument="--positive" />
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="interval">
+ <properties>
+ <help>Health check execution interval in seconds (default: 60)</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="script">
+ <properties>
+ <help>Health check script file</help>
+ <constraint>
+ <validator name="script"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="hello-source-address">
+ <properties>
+ <help>VRRP hello source address (IPv4 or IPv6)</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <valueHelp>
+ <format>&lt;IPv4|IPv6&gt;</format>
+ <description>IPv4 or IPv6 hello source address</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="peer-address">
+ <properties>
+ <help>Unicast VRRP peer address (IPv4 or IPv6)</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ <valueHelp>
+ <format>&lt;IPv4|IPv6&gt;</format>
+ <description>IPv4 or IPv6 unicast peer address</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="no-preempt">
+ <properties>
+ <valueless/>
+ <help>Disable master preemption</help>
+ </properties>
+ </leafNode>
+ <leafNode name="preempt-delay">
+ <properties>
+ <help>Preempt delay (in seconds)</help>
+ <constraint>
+ <validator name="numeric" argument="--range 0-1000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="priority">
+ <properties>
+ <help>Router priority</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Router priority (default: 100)</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="rfc3768-compatibility">
+ <properties>
+ <valueless/>
+ <help>Use VRRP virtual MAC address as per RFC3768</help>
+ </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>
+ <leafNode name="virtual-address">
+ <properties>
+ <multi/>
+ <help>Virtual address (IPv4 or IPv6, but they must not be mixed in one group)</help>
+ <constraint>
+ <validator name="ipv4-host"/>
+ <validator name="ipv6-host"/>
+ </constraint>
+ <constraintErrorMessage>Virtual address must be a valid IPv4 or IPv6 address with prefix length (e.g. 192.0.2.3/24 or 2001:db8:ff::10/64)</constraintErrorMessage>
+ <valueHelp>
+ <format>&lt;IPv4|IPv6&gt;</format>
+ <description>IPv4 or IPv6 virtual address</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="vrid">
+ <properties>
+ <help>Virtual router identifier</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Virtual router identifier</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="sync-group">
+ <properties>
+ <help>VRRP sync group</help>
+ </properties>
+ <children>
+ <leafNode name="member">
+ <properties>
+ <multi/>
+ <help>Sync group member</help>
+ <valueHelp>
+ <format>text</format>
+ <description>VRRP group name</description>
+ </valueHelp>
+ <completionHelp>
+ <path>high-availability vrrp group</path>
+ </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>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/add-system-image.xml b/op-mode-definitions/add-system-image.xml
new file mode 100644
index 000000000..3dc1c67ab
--- /dev/null
+++ b/op-mode-definitions/add-system-image.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="add">
+ <children>
+ <node name="system">
+ <properties>
+ <help>Add item to a system facility</help>
+ </properties>
+ <children>
+ <tagNode name="image">
+ <properties>
+ <help>Add a new image to the system</help>
+ <completionHelp>
+ <list>/path/to/vyos-image.iso http://example.com/vyos-image.iso</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyatta_sbindir}/install-image --url "${4}"</command>
+ <children>
+ <tagNode name="vrf">
+ <properties>
+ <help>Download image via specified VRF</help>
+ <completionHelp>
+ <path>vrf name</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --vrf "${6}"</command>
+ <children>
+ <tagNode name="username">
+ <properties>
+ <help>Username for authentication</help>
+ </properties>
+ <children>
+ <tagNode name="password">
+ <properties>
+ <help>Password to use with authentication</help>
+ </properties>
+ <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --vrf "${6}" --username "${8}" --password "${10}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="username">
+ <properties>
+ <help>Username for authentication</help>
+ </properties>
+ <children>
+ <tagNode name="password">
+ <properties>
+ <help>Password to use with authentication</help>
+ </properties>
+ <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --username "${6}" --password "${8}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/anyconnect.xml b/op-mode-definitions/anyconnect.xml
new file mode 100644
index 000000000..7e8cdd35b
--- /dev/null
+++ b/op-mode-definitions/anyconnect.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="anyconnect-server">
+ <properties>
+ <help>show anyconnect-server information</help>
+ </properties>
+ <children>
+ <leafNode name="sessions">
+ <properties>
+ <help>Show active anyconnect server sessions</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/anyconnect-control.py --action="show_sessions"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/configure.xml b/op-mode-definitions/configure.xml
new file mode 100644
index 000000000..3dd5a0f45
--- /dev/null
+++ b/op-mode-definitions/configure.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="configure">
+ <properties>
+ <help>Enter configuration mode</help>
+ </properties>
+ <command>if [ `id -u` == 0 ]; then
+ echo "You are attempting to enter configuration mode as root."
+ echo "It may have unintended consequences and render your system"
+ echo "unusable until restart."
+ echo "Please do it as an administrator level VyOS user instead."
+ else
+ if grep -q -e '^overlay.*/filesystem.squashfs' /proc/mounts; then
+ echo "WARNING: You are currently configuring a live-ISO environment, changes will not persist until installed"
+ fi
+ history -w
+ export _OFR_CONFIGURE=ok
+ newgrp vyattacfg
+ unset _OFR_CONFIGURE
+ _vyatta_op_do_key_bindings
+ history -r
+ fi</command>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/connect.xml b/op-mode-definitions/connect.xml
new file mode 100644
index 000000000..1ec62949a
--- /dev/null
+++ b/op-mode-definitions/connect.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="connect">
+ <properties>
+ <help>Establish connection</help>
+ </properties>
+ <children>
+ <tagNode name="console">
+ <properties>
+ <help>Connect to device attached to serial console server</help>
+ <completionHelp>
+ <path>service console-server device</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/console "$3"</command>
+ </tagNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Bring up a connection-oriented network interface</help>
+ <completionHelp>
+ <path>interfaces pppoe</path>
+ <path>interfaces wirelessmodem</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --connect "$3"</command>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/date.xml b/op-mode-definitions/date.xml
new file mode 100644
index 000000000..15a69dbd9
--- /dev/null
+++ b/op-mode-definitions/date.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="date">
+ <properties>
+ <help>Show system time and date</help>
+ </properties>
+ <command>/bin/date</command>
+ <children>
+ <node name="utc">
+ <properties>
+ <help>Show system date and time as Coordinated Universal Time</help>
+ </properties>
+ <command>/bin/date -u</command>
+ <children>
+ <leafNode name="maya">
+ <properties>
+ <help>Show UTC date in Maya calendar format</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/maya_date.py $(date +%s)</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="set">
+ <children>
+ <tagNode name="date">
+ <properties>
+ <help>Set system date and time</help>
+ <completionHelp>
+ <list>&lt;MMDDhhmm&gt; &lt;MMDDhhmmYY&gt; &lt;MMDDhhmmCCYY&gt; &lt;MMDDhhmmCCYY.ss&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/bin/date "$3"</command>
+ </tagNode>
+ <node name="date">
+ <properties>
+ <help>Set system date and time</help>
+ </properties>
+ <children>
+ <node name="ntp">
+ <properties>
+ <help>Set system date and time from NTP server (default: 0.pool.ntp.org)</help>
+ </properties>
+ <command>/usr/sbin/ntpdate -u 0.pool.ntp.org</command>
+ </node>
+ <tagNode name="ntp">
+ <properties>
+ <help>Set system date and time from NTP server</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_ntp_servers.sh</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/sbin/ntpdate -u "$4"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/dhcp.xml b/op-mode-definitions/dhcp.xml
new file mode 100644
index 000000000..48752cfd5
--- /dev/null
+++ b/op-mode-definitions/dhcp.xml
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="dhcp">
+ <properties>
+ <help>Show DHCP (Dynamic Host Configuration Protocol) information</help>
+ </properties>
+ <children>
+ <node name="server">
+ <properties>
+ <help>Show DHCP server information</help>
+ </properties>
+ <children>
+ <node name="leases">
+ <properties>
+ <help>Show DHCP server leases</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases</command>
+ <children>
+ <tagNode name="pool">
+ <properties>
+ <help>Show DHCP server leases for a specific pool</help>
+ <completionHelp>
+ <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed pool</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $6</command>
+ </tagNode>
+ <tagNode name="sort">
+ <properties>
+ <help>Show DHCP server leases sorted by the specified key</help>
+ <completionHelp>
+ <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed sort</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --sort $6</command>
+ </tagNode>
+ <tagNode name="state">
+ <properties>
+ <help>Show DHCP server leases with a specific state (can be multiple, comma-separated)</help>
+ <completionHelp>
+ <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed state</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --state $(echo $6 | tr , " ")</command>
+ </tagNode>
+ </children>
+ </node>
+ <node name="statistics">
+ <properties>
+ <help>Show DHCP server statistics</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics</command>
+ <children>
+ <tagNode name="pool">
+ <properties>
+ <help>Show DHCP server statistics for a specific pool</help>
+ <completionHelp>
+ <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed pool</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $6</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="dhcpv6">
+ <properties>
+ <help>Show DHCPv6 (IPv6 Dynamic Host Configuration Protocol) information</help>
+ </properties>
+ <children>
+ <node name="server">
+ <properties>
+ <help>Show DHCPv6 server information</help>
+ </properties>
+ <children>
+ <node name="leases">
+ <properties>
+ <help>Show DHCPv6 server leases</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases</command>
+ <children>
+ <tagNode name="pool">
+ <properties>
+ <help>Show DHCPv6 server leases for a specific pool</help>
+ <completionHelp>
+ <script>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --allowed pool</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --pool $6</command>
+ </tagNode>
+ <tagNode name="sort">
+ <properties>
+ <help>Show DHCPv6 server leases sorted by the specified key</help>
+ <completionHelp>
+ <script>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --allowed sort</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --sort $6</command>
+ </tagNode>
+ <tagNode name="state">
+ <properties>
+ <help>Show DHCPv6 server leases with a specific state (can be multiple, comma-separated)</help>
+ <completionHelp>
+ <script>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --allowed state</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --state $(echo $6 | tr , " ")</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="restart">
+ <children>
+ <node name="dhcp">
+ <properties>
+ <help>Restart DHCP processes</help>
+ </properties>
+ <children>
+ <node name="server">
+ <properties>
+ <help>Restart the DHCP server process</help>
+ </properties>
+ <command>sudo systemctl restart isc-dhcp-server.service</command>
+ </node>
+ <node name="relay-agent">
+ <properties>
+ <help>Restart the DHCP server process</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_dhcp_relay.py --ipv4</command>
+ </node>
+ </children>
+ </node>
+ <node name="dhcpv6">
+ <properties>
+ <help>Restart DHCPv6 processes</help>
+ </properties>
+ <children>
+ <node name="server">
+ <properties>
+ <help>Restart the DHCPv6 server process</help>
+ </properties>
+ <command>sudo systemctl restart isc-dhcp-server6.service</command>
+ </node>
+ <node name="relay-agent">
+ <properties>
+ <help>Restart the DHCP server process</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_dhcp_relay.py --ipv6</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="renew">
+ <properties>
+ <help>Renew specified variable</help>
+ </properties>
+ <children>
+ <node name="dhcp">
+ <properties>
+ <help>Renew DHCP client lease</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Renew DHCP client lease for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>sudo systemctl restart "dhclient@$4.service"</command>
+ </tagNode>
+ </children>
+ </node>
+ <node name="dhcpv6">
+ <properties>
+ <help>Renew DHCPv6 client lease</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Renew DHCPv6 client lease for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>sudo systemctl restart "dhcp6c@$4.service"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/disconnect.xml b/op-mode-definitions/disconnect.xml
new file mode 100644
index 000000000..bf2c37b89
--- /dev/null
+++ b/op-mode-definitions/disconnect.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="disconnect">
+ <properties>
+ <help>Take down a connection</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Take down a connection-oriented network interface</help>
+ <completionHelp>
+ <path>interfaces pppoe</path>
+ <path>interfaces wirelessmodem</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --disconnect "$3"</command>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/disks.xml b/op-mode-definitions/disks.xml
new file mode 100644
index 000000000..fb39c4f3c
--- /dev/null
+++ b/op-mode-definitions/disks.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="format">
+ <properties>
+ <help>Format a device</help>
+ </properties>
+ <children>
+ <tagNode name="disk">
+ <properties>
+ <help>Format a disk drive</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_disks.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="like">
+ <properties>
+ <help>Format this disk the same as another disk</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_disks.py --exclude ${COMP_WORDS[2]}</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/format_disk.py --target $3 --proto $5</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+
+ <node name="show">
+ <children>
+ <tagNode name="disk">
+ <properties>
+ <help>Show status of disk device</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_disks.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="format">
+ <properties>
+ <help>Show disk drive formatting</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_disk_format.sh $3</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/dns-dynamic.xml b/op-mode-definitions/dns-dynamic.xml
new file mode 100644
index 000000000..9c37874fb
--- /dev/null
+++ b/op-mode-definitions/dns-dynamic.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="log">
+ <children>
+ <node name="dns">
+ <children>
+ <node name="dynamic">
+ <properties>
+ <help>Show log for dynamic DNS</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "ddclient"</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="dns">
+ <properties>
+ <help>Show DNS information</help>
+ </properties>
+ <children>
+ <node name="dynamic">
+ <properties>
+ <help>Show Dynamic DNS information</help>
+ </properties>
+ <children>
+ <leafNode name="status">
+ <properties>
+ <help>Show Dynamic DNS status</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --status</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="restart">
+ <children>
+ <node name="dns">
+ <children>
+ <node name="dynamic">
+ <properties>
+ <help>Restart Dynamic DNS service</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="update">
+ <properties>
+ <help>Update data for a service</help>
+ </properties>
+ <children>
+ <node name="dns">
+ <properties>
+ <help>Update DNS information</help>
+ </properties>
+ <children>
+ <node name="dynamic">
+ <properties>
+ <help>Update Dynamic DNS information</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/dns-forwarding.xml b/op-mode-definitions/dns-forwarding.xml
new file mode 100644
index 000000000..23de97704
--- /dev/null
+++ b/op-mode-definitions/dns-forwarding.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="log">
+ <children>
+ <node name="dns">
+ <properties>
+ <help>Show log for Domain Name Service (DNS)</help>
+ </properties>
+ <children>
+ <node name="forwarding">
+ <properties>
+ <help>Show log for DNS Forwarding</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "pdns_recursor"</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="dns">
+ <properties>
+ <help>Show DNS information</help>
+ </properties>
+ <children>
+ <node name="forwarding">
+ <properties>
+ <help>Show DNS forwarding information</help>
+ </properties>
+ <children>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show DNS forwarding statistics</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_statistics.py</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="restart">
+ <children>
+ <node name="dns">
+ <properties>
+ <help>Restart a DNS service</help>
+ </properties>
+ <children>
+ <leafNode name="forwarding">
+ <properties>
+ <help>Restart DNS forwarding service</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_restart.sh</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="dns">
+ <properties>
+ <help>Reset a DNS service state</help>
+ </properties>
+ <children>
+ <node name="forwarding">
+ <properties>
+ <help>Reset DNS forwarding cache</help>
+ </properties>
+ <children>
+ <tagNode name="domain">
+ <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_reset.py $5</command>
+ <properties>
+ <help>Reset DNS forwarding cache for a domain</help>
+ </properties>
+ </tagNode>
+ <leafNode name="all">
+ <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_reset.py --all</command>
+ <properties>
+ <help>Reset DNS forwarding cache</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/flow-accounting-op.xml b/op-mode-definitions/flow-accounting-op.xml
new file mode 100644
index 000000000..912805d59
--- /dev/null
+++ b/op-mode-definitions/flow-accounting-op.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- flow-accounting op mode commands -->
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="flow-accounting">
+ <properties>
+ <help>Show flow accounting statistics</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action show</command>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Show flow accounting statistics for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action show --interface $4</command>
+ <children>
+ <tagNode name="host">
+ <properties>
+ <help>Show flow accounting statistics for specified interface/host</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action show --interface $4 --host $6</command>
+ </tagNode>
+ <tagNode name="port">
+ <properties>
+ <help>Show flow accounting statistics for specified interface/port</help>
+ <completionHelp>
+ <list>1-65535</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action show --interface $4 --ports $6</command>
+ </tagNode>
+ <tagNode name="top">
+ <properties>
+ <help>Show top N flows for specified interface</help>
+ <completionHelp>
+ <list>1-100</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action show --interface $4 --top $6</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="restart">
+ <children>
+ <leafNode name="flow-accounting">
+ <properties>
+ <help>Restart flow-accounting service</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action restart</command>
+ </leafNode>
+ </children>
+ </node>
+ <node name="clear">
+ <children>
+ <node name="flow-accounting">
+ <properties>
+ <help>Clear flow accounting</help>
+ </properties>
+ <children>
+ <leafNode name="counters">
+ <properties>
+ <help>Clear flow accounting statistics</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action clear</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/force-arp.xml b/op-mode-definitions/force-arp.xml
new file mode 100644
index 000000000..c7bcad413
--- /dev/null
+++ b/op-mode-definitions/force-arp.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="force">
+ <properties>
+ <help>Force an operation</help>
+ </properties>
+ <children>
+ <node name="arp">
+ <properties>
+ <help>Send gratuitous ARP request or reply</help>
+ </properties>
+ <children>
+ <node name="reply">
+ <properties>
+ <help>Send gratuitous ARP reply</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Send gratuitous ARP reply on specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Send gratuitous ARP reply for specified address</help>
+ </properties>
+ <command>sudo /usr/bin/arping -I $5 -c 1 -A $7</command>
+ <children>
+ <tagNode name="count">
+ <properties>
+ <help>Send specified number of ARP replies</help>
+ </properties>
+ <command>sudo /usr/bin/arping -I $5 -c $9 -A $7</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="request">
+ <properties>
+ <help>Send gratuitous ARP request</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Send gratuitous ARP request on specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Send gratuitous ARP request for specified address</help>
+ </properties>
+ <command>sudo /usr/bin/arping -I $5 -c 1 -U $7</command>
+ <children>
+ <tagNode name="count">
+ <properties>
+ <help>Send specified number of ARP requests</help>
+ </properties>
+ <command>sudo /usr/bin/arping -I $5 -c $9 -U $7</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/force-ipv6-nd.xml b/op-mode-definitions/force-ipv6-nd.xml
new file mode 100644
index 000000000..49de097f6
--- /dev/null
+++ b/op-mode-definitions/force-ipv6-nd.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="force">
+ <children>
+ <node name="ipv6-nd">
+ <properties>
+ <help>IPv6 Neighbor Discovery</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>IPv6 Neighbor Discovery on specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>IPv6 address of node to lookup</help>
+ <completionHelp>
+ <list>&lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/ndisc6 -m "$6" "$4"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/force-ipv6-rd.xml b/op-mode-definitions/force-ipv6-rd.xml
new file mode 100644
index 000000000..8c901af25
--- /dev/null
+++ b/op-mode-definitions/force-ipv6-rd.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="force">
+ <children>
+ <node name="ipv6-rd">
+ <properties>
+ <help>IPv6 Router Discovery</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>IPv6 Router Discovery on specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/rdisc6 "$4"</command>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>IPv6 address of target</help>
+ <completionHelp>
+ <list>&lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/rdisc6 -m "$6" "$4"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/generate-macsec-key.xml b/op-mode-definitions/generate-macsec-key.xml
new file mode 100644
index 000000000..40d2b9061
--- /dev/null
+++ b/op-mode-definitions/generate-macsec-key.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="macsec">
+ <properties>
+ <help>Generate MACsec Key</help>
+ </properties>
+ <children>
+ <node name="mka-cak">
+ <properties>
+ <help>Generate MACsec connectivity association key (CAK)</help>
+ </properties>
+ <command>/usr/bin/hexdump -n 16 -e '4/4 "%08x" 1 "\n"' /dev/random</command>
+ </node>
+ <node name="mka-ckn">
+ <properties>
+ <help>Generate MACsec connectivity association name (CKN)</help>
+ </properties>
+ <command>/usr/bin/hexdump -n 32 -e '8/4 "%08x" 1 "\n"' /dev/random</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/generate-ssh-server-key.xml b/op-mode-definitions/generate-ssh-server-key.xml
new file mode 100644
index 000000000..a6ebf1b78
--- /dev/null
+++ b/op-mode-definitions/generate-ssh-server-key.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <properties>
+ <help>Generate an object</help>
+ </properties>
+ <children>
+ <node name="ssh-server-key">
+ <properties>
+ <help>Regenerate the host SSH keys and restart the SSH server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/generate_ssh_server_key.py</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/igmp-proxy.xml b/op-mode-definitions/igmp-proxy.xml
new file mode 100644
index 000000000..8533138d7
--- /dev/null
+++ b/op-mode-definitions/igmp-proxy.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interfaceDefinition>
+ <node name="restart">
+ <children>
+ <node name="igmp-proxy">
+ <properties>
+ <help>Restart the IGMP proxy process</help>
+ </properties>
+ <command>sudo systemctl restart igmpproxy.service</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/ipoe-server.xml b/op-mode-definitions/ipoe-server.xml
new file mode 100644
index 000000000..c20d3aa2a
--- /dev/null
+++ b/op-mode-definitions/ipoe-server.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="reset">
+ <children>
+ <node name="ipoe-server">
+ <properties>
+ <help>Clear ipoe-server sessions or process</help>
+ </properties>
+ <children>
+ <node name="session">
+ <properties>
+ <help>Clear ipoe-server session</help>
+ </properties>
+ <children>
+ <tagNode name="username">
+ <properties>
+ <help>Clear ipoe-server session by username</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_ipoe.py --selector="username"</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ipoe-control.py --action="terminate" --selector="username" --target="$5"</command>
+ </tagNode>
+ <tagNode name="sid">
+ <properties>
+ <help>Clear ipoe-server session by Session ID</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_ipoe.py --selector="sid"</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ipoe-control.py --action="terminate" --selector="sid" --target="$5"</command>
+ </tagNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Clear ipoe-server session by interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_ipoe.py --selector="ifname"</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ipoe-control.py --action="terminate" --selector="if" --target="$5"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="show">
+ <children>
+ <node name="ipoe-server">
+ <properties>
+ <help>show ipoe-server status</help>
+ </properties>
+ <children>
+ <leafNode name="sessions">
+ <properties>
+ <help>Show active IPoE server sessions</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ipoe-control.py --action="show_sessions"</command>
+ </leafNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show IPoE server statistics</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ipoe-control.py --action="show_stat"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="restart">
+ <children>
+ <leafNode name="ipoe-server">
+ <properties>
+ <help>show ipoe-server status</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ipoe-control.py --action="restart"</command>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/ipv4-route.xml b/op-mode-definitions/ipv4-route.xml
new file mode 100644
index 000000000..1bda3ac11
--- /dev/null
+++ b/op-mode-definitions/ipv4-route.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <properties>
+ <help>Show system information</help>
+ </properties>
+ <children>
+ <node name="ip">
+ <properties>
+ <help>Show IPv4 information</help>
+ </properties>
+ <children>
+ <leafNode name="groups">
+ <properties>
+ <help>Show IP multicast group membership</help>
+ </properties>
+ <command>netstat -gn4</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="ip">
+ <properties>
+ <help>Reset Internet Protocol (IP) parameters</help>
+ </properties>
+ <children>
+ <node name="arp">
+ <properties>
+ <help>Reset Address Resolution Protocol (ARP) cache</help>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Reset ARP cache for an IPv4 address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /sbin/ip neigh flush to "$5"</command>
+ </tagNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Reset ARP cache for interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>sudo /sbin/ip neigh flush dev "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+
+ <node name="route">
+ <properties>
+ <help>Reset IP route</help>
+ </properties>
+ <children>
+ <leafNode name= "cache">
+ <properties>
+ <help>Flush the kernel route cache</help>
+ </properties>
+ <command>sudo /sbin/ip route flush cache</command>
+ </leafNode>
+
+ <tagNode name="cache">
+ <properties>
+ <help>Flush the kernel route cache for a given route</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;x.x.x.x/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /sbin/ip route flush cache "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/ipv6-route.xml b/op-mode-definitions/ipv6-route.xml
new file mode 100644
index 000000000..fbf6489ba
--- /dev/null
+++ b/op-mode-definitions/ipv6-route.xml
@@ -0,0 +1,133 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <properties>
+ <help>Show system information</help>
+ </properties>
+ <children>
+ <node name="ipv6">
+ <properties>
+ <help>Show IPv6 routing information</help>
+ </properties>
+ <children>
+ <leafNode name="groups">
+ <properties>
+ <help>Show IPv6 multicast group membership</help>
+ </properties>
+ <command>netstat -gn6</command>
+ </leafNode>
+
+ <leafNode name="neighbors">
+ <properties>
+ <help>Show IPv6 Neighbor Discovery (ND) information</help>
+ </properties>
+ <command>ip -f inet6 neigh list</command>
+ </leafNode>
+
+ <node name="route">
+ <properties>
+ <help>Show IPv6 routes</help>
+ </properties>
+ <children>
+ <node name="cache">
+ <properties>
+ <help>Show kernel IPv6 route cache</help>
+ </properties>
+ <command>ip -s -f inet6 route list cache</command>
+ </node>
+ <tagNode name="cache">
+ <properties>
+ <help>Show kernel IPv6 route cache for a given route</help>
+ <completionHelp>
+ <list>&lt;h:h:h:h:h:h:h:h&gt; &lt;h:h:h:h:h:h:h:h/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>ip -s -f inet6 route list cache $5</command>
+ </tagNode>
+ <node name="forward">
+ <properties>
+ <help>Show kernel IPv6 route table</help>
+ </properties>
+ <command>ip -f inet6 route list</command>
+ </node>
+ <tagNode name="forward">
+ <properties>
+ <help>Show kernel IPv6 route table for a given route</help>
+ <completionHelp>
+ <list>&lt;h:h:h:h:h:h:h:h&gt; &lt;h:h:h:h:h:h:h:h/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>ip -s -f inet6 route list $5</command>
+ </tagNode>
+ </children>
+ </node>
+
+ </children>
+ </node>
+ </children>
+ </node>
+
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="ipv6">
+ <properties>
+ <help>Reset Internet Protocol version 6 (IPv6) parameters</help>
+ </properties>
+ <children>
+ <node name="neighbors">
+ <properties>
+ <help>Reset IPv6 Neighbor Discovery (ND) cache</help>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Reset ND cache for an IPv6 address</help>
+ <completionHelp>
+ <list>&lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ip -f inet6 neigh flush to "$5"</command>
+ </tagNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Reset IPv6 ND cache for interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ip -f inet6 neigh flush dev "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+
+ <node name="route">
+ <properties>
+ <help>Reset IPv6 route</help>
+ </properties>
+ <children>
+ <leafNode name= "cache">
+ <properties>
+ <help>Flush the kernel IPv6 route cache</help>
+ </properties>
+ <command>sudo ip -f inet6 route flush cache</command>
+ </leafNode>
+
+ <tagNode name="cache">
+ <properties>
+ <help>Flush the kernel IPv6 route cache for a given route</help>
+ <completionHelp>
+ <list>&lt;h:h:h:h:h:h:h:h&gt; &lt;h:h:h:h:h:h:h:h/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ip -f inet6 route flush cache "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/l2tp-server.xml b/op-mode-definitions/l2tp-server.xml
new file mode 100644
index 000000000..3e96b9365
--- /dev/null
+++ b/op-mode-definitions/l2tp-server.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="l2tp-server">
+ <properties>
+ <help>Show L2TP server information</help>
+ </properties>
+ <children>
+ <leafNode name="sessions">
+ <properties>
+ <help>Show active L2TP server sessions</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="l2tp" --action="show sessions"</command>
+ </leafNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show L2TP server statistics</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="l2tp" --action="show stat"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/lldp.xml b/op-mode-definitions/lldp.xml
new file mode 100644
index 000000000..297ccf1f4
--- /dev/null
+++ b/op-mode-definitions/lldp.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="lldp">
+ <properties>
+ <help>Show LLDP (Link Layer Discovery Protocol)</help>
+ </properties>
+ <children>
+ <node name="neighbors">
+ <properties>
+ <help>Show LLDP neighbors</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/lldp_op.py --all</command>
+ <children>
+ <node name="detail">
+ <properties>
+ <help>Show LLDP neighbor details</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/lldp_op.py --detail</command>
+ </node>
+ <tagNode name="interface">
+ <properties>
+ <help>Show LLDP for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/lldp_op.py --interface $5</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/monitor-bandwidth-test.xml b/op-mode-definitions/monitor-bandwidth-test.xml
new file mode 100644
index 000000000..d1e459b17
--- /dev/null
+++ b/op-mode-definitions/monitor-bandwidth-test.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="monitor">
+ <children>
+ <node name="bandwidth-test">
+ <properties>
+ <help>Initiate or wait for bandwidth test</help>
+ </properties>
+ <children>
+ <leafNode name="accept">
+ <properties>
+ <help>Wait for bandwidth test connections (port TCP/5001)</help>
+ </properties>
+ <command>iperf -s</command>
+ </leafNode>
+ <tagNode name="initiate">
+ <properties>
+ <help>Initiate a bandwidth test to specified host (port TCP/5001)</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>iperf -c $4</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/monitor-bandwidth.xml b/op-mode-definitions/monitor-bandwidth.xml
new file mode 100644
index 000000000..9af0a9e70
--- /dev/null
+++ b/op-mode-definitions/monitor-bandwidth.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="monitor">
+ <children>
+ <node name="bandwidth">
+ <properties>
+ <help>Monitor interface bandwidth in real time</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <command>bmon -b -p $4</command>
+ <properties>
+ <help>Monitor bandwidth usage on specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/monitor-log.xml b/op-mode-definitions/monitor-log.xml
new file mode 100644
index 000000000..99efe5306
--- /dev/null
+++ b/op-mode-definitions/monitor-log.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="monitor">
+ <children>
+ <node name="log">
+ <properties>
+ <help>Monitor last lines of messages file</help>
+ </properties>
+ <command>tail --follow=name /var/log/messages</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/monitor-ndp.xml b/op-mode-definitions/monitor-ndp.xml
new file mode 100644
index 000000000..1ac6ce39b
--- /dev/null
+++ b/op-mode-definitions/monitor-ndp.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="monitor">
+ <children>
+ <node name="ndp">
+ <properties>
+ <help>Monitor the NDP information received by the router through the device</help>
+ </properties>
+ <command>sudo ndptool monitor</command>
+ <children>
+ <tagNode name="interface">
+ <command>sudo ndptool monitor --ifname=$4</command>
+ <properties>
+ <help>Monitor ndp protocol on specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="type">
+ <command>sudo ndptool monitor --ifname=$4 --msg-type=$6</command>
+ <properties>
+ <help>Monitor specific types of NDP protocols</help>
+ <completionHelp>
+ <list>rs ra ns na</list>
+ </completionHelp>
+ </properties>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="type">
+ <command>sudo ndptool monitor --msg-type=$4</command>
+ <properties>
+ <help>Monitor specific types of NDP protocols</help>
+ <completionHelp>
+ <list>rs ra ns na</list>
+ </completionHelp>
+ </properties>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/nat.xml b/op-mode-definitions/nat.xml
new file mode 100644
index 000000000..f6c0fa748
--- /dev/null
+++ b/op-mode-definitions/nat.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="nat">
+ <properties>
+ <help>Show Network Address Translation (NAT) information</help>
+ </properties>
+ <children>
+ <node name="source">
+ <properties>
+ <help>Show source Network Address Translation (NAT) information</help>
+ </properties>
+ <children>
+ <node name="rules">
+ <properties>
+ <help>Show configured source NAT rules</help>
+ </properties>
+ <command>echo To be migrated to Python - https://phabricator.vyos.net/T2459</command>
+ </node>
+ <node name="statistics">
+ <properties>
+ <help>Show statistics for configured source NAT rules</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_nat_statistics.py --source</command>
+ </node>
+ <node name="translations">
+ <properties>
+ <help>Show active source NAT translations</help>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Show active source NAT translations for an IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source --verbose --ipaddr="$6"</command>
+ </tagNode>
+ <node name="detail">
+ <properties>
+ <help>Show active source NAT translations detail</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source --verbose</command>
+ </node>
+ </children>
+ <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source</command>
+ </node>
+ </children>
+ </node>
+ <node name="destination">
+ <properties>
+ <help>Show destination Network Address Translation (NAT) information</help>
+ </properties>
+ <children>
+ <node name="rules">
+ <properties>
+ <help>Show configured destination NAT rules</help>
+ </properties>
+ <command>echo To be migrated to Python - https://phabricator.vyos.net/T2459</command>
+ </node>
+ <node name="statistics">
+ <properties>
+ <help>Show statistics for configured destination NAT rules</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_nat_statistics.py --destination</command>
+ </node>
+ <node name="translations">
+ <properties>
+ <help>Show active destination NAT translations</help>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Show active NAT destination translations for an IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination --verbose --ipaddr="$6"</command>
+ </tagNode>
+ <node name="detail">
+ <properties>
+ <help>Show active destination NAT translations detail</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination --verbose</command>
+ </node>
+ </children>
+ <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml
new file mode 100644
index 000000000..b9cb06dca
--- /dev/null
+++ b/op-mode-definitions/openvpn.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="openvpn">
+ <properties>
+ <help>OpenVPN key generation tool</help>
+ </properties>
+ <children>
+ <tagNode name="key">
+ <properties>
+ <help>Generate shared-secret key with specified file name</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>
+ result=1;
+ key_path=$4
+ full_path=
+
+ # Prepend /config/auth if the path is not absolute
+ if echo $key_path | egrep -ve '^/.*' &gt; /dev/null; then
+ full_path=/config/auth/$key_path
+ else
+ full_path=$key_path
+ fi
+
+ key_dir=`dirname $full_path`
+ if [ ! -d $key_dir ]; then
+ echo "Directory $key_dir does not exist!"
+ exit 1
+ fi
+
+ echo "Generating OpenVPN key to $full_path"
+ sudo /usr/sbin/openvpn --genkey --secret "$full_path"
+ result=$?
+ if [ $result = 0 ]; then
+ echo "Your new local OpenVPN key has been generated"
+ fi
+ /usr/libexec/vyos/validators/file-exists --directory /config/auth "$full_path"
+ </command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="openvpn">
+ <children>
+ <tagNode name="client">
+ <properties>
+ <help>Reset specified OpenVPN client</help>
+ <completionHelp>
+ <script>sudo ${vyos_completion_dir}/list_openvpn_clients.py --all</script>
+ </completionHelp>
+ </properties>
+ <command>echo kill $4 | socat - UNIX-CONNECT:/run/openvpn/openvpn-mgmt-intf &gt; /dev/null</command>
+ </tagNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Reset OpenVPN process on interface</help>
+ <completionHelp>
+ <script>sudo ${vyos_completion_dir}/list_interfaces.py --type openvpn</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/reset_openvpn.py $4</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <node name="openvpn">
+ <properties>
+ <help>Show OpenVPN interface information</help>
+ </properties>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed OpenVPN interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=openvpn --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="openvpn">
+ <properties>
+ <help>Show OpenVPN interface information</help>
+ <completionHelp>
+ <script>sudo ${vyos_completion_dir}/list_interfaces.py --type openvpn</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf=$4</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of specified OpenVPN interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="openvpn">
+ <properties>
+ <help>Show OpenVPN information</help>
+ </properties>
+ <children>
+ <leafNode name="client">
+ <properties>
+ <help>Show tunnel status for OpenVPN client interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=client</command>
+ </leafNode>
+ <leafNode name="server">
+ <properties>
+ <help>Show tunnel status for OpenVPN server interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=server</command>
+ </leafNode>
+ <leafNode name="site-to-site">
+ <properties>
+ <help>Show tunnel status for OpenVPN site-to-site interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=site-to-site</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/ping.xml b/op-mode-definitions/ping.xml
new file mode 100644
index 000000000..4c25a59ab
--- /dev/null
+++ b/op-mode-definitions/ping.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <tagNode name="ping">
+ <properties>
+ <help>Send Internet Control Message Protocol (ICMP) echo request</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>${vyos_op_scripts_dir}/ping.py ${@:2}</command>
+ <children>
+ <leafNode name="node.tag">
+ <properties>
+ <help>Ping options</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/ping.py --get-options "${COMP_WORDS[@]}"</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ping.py ${@:2}</command>
+ </leafNode>
+ </children>
+ </tagNode>
+</interfaceDefinition>
diff --git a/op-mode-definitions/poweroff.xml b/op-mode-definitions/poweroff.xml
new file mode 100644
index 000000000..b4163bcb9
--- /dev/null
+++ b/op-mode-definitions/poweroff.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="poweroff">
+ <properties>
+ <help>Poweroff the system</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --poweroff</command>
+ <children>
+ <leafNode name="now">
+ <properties>
+ <help>Poweroff the system without confirmation</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff</command>
+ </leafNode>
+ <leafNode name="cancel">
+ <properties>
+ <help>Cancel a pending poweroff</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --cancel</command>
+ </leafNode>
+ <tagNode name="in">
+ <properties>
+ <help>Poweroff in X minutes</help>
+ <completionHelp>
+ <list>&lt;Minutes&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3 $4</command>
+ </tagNode>
+ <tagNode name="at">
+ <properties>
+ <help>Poweroff at a specific time</help>
+ <completionHelp>
+ <list>&lt;HH:MM&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3</command>
+ <children>
+ <tagNode name="date">
+ <properties>
+ <help>Poweroff at a specific date</help>
+ <completionHelp>
+ <list>&lt;DDMMYYYY&gt; &lt;DD/MM/YYYY&gt; &lt;DD.MM.YYYY&gt; &lt;DD:MM:YYYY&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3 $5</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/pppoe-server.xml b/op-mode-definitions/pppoe-server.xml
new file mode 100644
index 000000000..5ac9d9497
--- /dev/null
+++ b/op-mode-definitions/pppoe-server.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="pppoe-server">
+ <properties>
+ <help>Show pppoe-server status</help>
+ </properties>
+ <children>
+ <leafNode name="sessions">
+ <properties>
+ <help>Show active PPPoE server sessions</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="show sessions"</command>
+ </leafNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show PPPoE server statistics</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="show stat"</command>
+ </leafNode>
+ <leafNode name="interfaces">
+ <properties>
+ <help>Show interfaces where pppoe-server listens on</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="pppoe interface show"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="restart">
+ <children>
+ <leafNode name="pppoe-server">
+ <properties>
+ <help>Restarts pppoe-server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="restart"</command>
+ </leafNode>
+ </children>
+ </node>
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="pppoe-server">
+ <properties>
+ <help>Reset PPPoE server sessions</help>
+ </properties>
+ <children>
+ <leafNode name="all">
+ <properties>
+ <help>Terminate all pppoe-server users</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="terminate all"</command>
+ </leafNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Terminate a ppp interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="terminate if $4"</command>
+ </tagNode>
+ <tagNode name="username">
+ <properties>
+ <help>Terminate specified users</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="terminate username $4"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="set">
+ <children>
+ <node name="pppoe-server">
+ <properties>
+ <help>Set PPPoE server maintenance mode</help>
+ </properties>
+ <children>
+ <node name="maintenance-mode">
+ <properties>
+ <help>Set PPPoE server maintenance mode</help>
+ </properties>
+ <children>
+ <leafNode name="enable">
+ <properties>
+ <help>Deny new connections and stop to serve pppoe after disconnect last session</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="shutdown soft"</command>
+ </leafNode>
+ <leafNode name="cancel">
+ <properties>
+ <help>Cancel maintenance mode</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="shutdown cancel"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/pptp-server.xml b/op-mode-definitions/pptp-server.xml
new file mode 100644
index 000000000..59be68611
--- /dev/null
+++ b/op-mode-definitions/pptp-server.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="pptp-server">
+ <properties>
+ <help>Show PPTP server information</help>
+ </properties>
+ <children>
+ <leafNode name="sessions">
+ <properties>
+ <help>Show active PPTP server sessions</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pptp" --action="show sessions"</command>
+ </leafNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show PPTP server statistics</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pptp" --action="show stat"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/reboot.xml b/op-mode-definitions/reboot.xml
new file mode 100644
index 000000000..2c8daec5d
--- /dev/null
+++ b/op-mode-definitions/reboot.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="reboot">
+ <properties>
+ <help>Reboot the system</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --reboot</command>
+ <children>
+ <leafNode name="now">
+ <properties>
+ <help>Reboot the system without confirmation</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot</command>
+ </leafNode>
+ <leafNode name="cancel">
+ <properties>
+ <help>Cancel a pending reboot</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --cancel</command>
+ </leafNode>
+ <tagNode name="in">
+ <properties>
+ <help>Reboot in X minutes</help>
+ <completionHelp>
+ <list>&lt;Minutes&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $4</command>
+ </tagNode>
+ <tagNode name="at">
+ <properties>
+ <help>Reboot at a specific time</help>
+ <completionHelp>
+ <list>&lt;HH:MM&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3</command>
+ <children>
+ <tagNode name="date">
+ <properties>
+ <help>Reboot at a specific date</help>
+ <completionHelp>
+ <list>&lt;DDMMYYYY&gt; &lt;DD/MM/YYYY&gt; &lt;DD.MM.YYYY&gt; &lt;DD:MM:YYYY&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $5</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/reset-conntrack.xml b/op-mode-definitions/reset-conntrack.xml
new file mode 100644
index 000000000..827ba4af4
--- /dev/null
+++ b/op-mode-definitions/reset-conntrack.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="conntrack">
+ <properties>
+ <help>Reset all currently tracked connections</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/clear_conntrack.py</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
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/reset-vpn.xml b/op-mode-definitions/reset-vpn.xml
new file mode 100644
index 000000000..ae553c272
--- /dev/null
+++ b/op-mode-definitions/reset-vpn.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="vpn">
+ <properties>
+ <help>Reset Virtual Private Network (VPN) information</help>
+ </properties>
+ <children>
+ <node name="remote-access">
+ <properties>
+ <help>Reset remote access VPN connections</help>
+ </properties>
+ <children>
+ <node name="all">
+ <properties>
+ <help>Terminate all user's current remote access VPN session(s)</help>
+ </properties>
+ <children>
+ <node name="protocol">
+ <properties>
+ <help>Terminate specified user's current remote access VPN session(s) with specified protocol</help>
+ </properties>
+ <children>
+ <leafNode name="l2tp">
+ <properties>
+ <help>Terminate all user's current remote access VPN session(s) with L2TP protocol</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users" --protocol="l2tp"</command>
+ </leafNode>
+ <leafNode name="pptp">
+ <properties>
+ <help>Terminate all user's current remote access VPN session(s) with PPTP protocol</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users" --protocol="pptp"</command>
+ </leafNode>
+ <leafNode name="sstp">
+ <properties>
+ <help>Terminate all user's current remote access VPN session(s) with SSTP protocol</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users" --protocol="sstp"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users"</command>
+ </node>
+ <tagNode name="interface">
+ <properties>
+ <help>Terminate a remote access VPN interface</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --interface="$5"</command>
+ </tagNode>
+ <tagNode name="user">
+ <properties>
+ <help>Terminate specified user's current remote access VPN session(s)</help>
+ </properties>
+ <children>
+ <node name="protocol">
+ <properties>
+ <help>Terminate specified user's current remote access VPN session(s) with specified protocol</help>
+ </properties>
+ <children>
+ <leafNode name="l2tp">
+ <properties>
+ <help>Terminate all user's current remote access VPN session(s) with L2TP protocol</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5" --protocol="l2tp"</command>
+ </leafNode>
+ <leafNode name="pptp">
+ <properties>
+ <help>Terminate all user's current remote access VPN session(s) with PPTP protocol</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5" --protocol="pptp"</command>
+ </leafNode>
+ <leafNode name="sstp">
+ <properties>
+ <help>Terminate all user's current remote access VPN session(s) with SSTP protocol</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5" --protocol="sstp"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/restart-frr.xml b/op-mode-definitions/restart-frr.xml
new file mode 100644
index 000000000..96ad1a650
--- /dev/null
+++ b/op-mode-definitions/restart-frr.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="restart">
+ <children>
+ <node name="frr">
+ <properties>
+ <help>Restart FRRouting daemons</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart</command>
+ <children>
+ <leafNode name="bfdd">
+ <properties>
+ <help>Restart Bidirectional Forwarding Detection daemon</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bfdd</command>
+ </leafNode>
+ <leafNode name="bgpd">
+ <properties>
+ <help>Restart Border Gateway Protocol daemon</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bgpd</command>
+ </leafNode>
+ <leafNode name="ospfd">
+ <properties>
+ <help>Restart OSPFv2 daemon</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ospfd</command>
+ </leafNode>
+ <leafNode name="ospf6d">
+ <properties>
+ <help>Restart OSPFv3 daemon</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ospf6d</command>
+ </leafNode>
+ <leafNode name="ripd">
+ <properties>
+ <help>Restart Routing Information Protocol daemon</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ripd</command>
+ </leafNode>
+ <leafNode name="ripngd">
+ <properties>
+ <help>Restart RIPng daemon</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ripngd</command>
+ </leafNode>
+ <leafNode name="staticd">
+ <properties>
+ <help>Restart Static Route daemon</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon staticd</command>
+ </leafNode>
+ <leafNode name="zebra">
+ <properties>
+ <help>Restart IP routing manager daemon</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon zebra</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-acceleration.xml b/op-mode-definitions/show-acceleration.xml
new file mode 100644
index 000000000..d0dcea2d6
--- /dev/null
+++ b/op-mode-definitions/show-acceleration.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="system">
+ <properties>
+ <help>Show system information</help>
+ </properties>
+ <children>
+ <node name="acceleration">
+ <properties>
+ <help>Acceleration components</help>
+ </properties>
+ <children>
+ <node name="qat">
+ <properties>
+ <help>Intel QAT (Quick Assist Technology) Devices</help>
+ </properties>
+ <children>
+ <tagNode name="device">
+ <properties>
+ <help>Show QAT information for a given acceleration device</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/show_acceleration.py --dev_list</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <node name="flows">
+ <properties>
+ <help>Intel QAT flows</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_acceleration.py --flow --dev $6</command>
+ </node>
+ <node name="config">
+ <properties>
+ <help>Intel QAT configuration</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_acceleration.py --conf --dev $6</command>
+ </node>
+ </children>
+ </tagNode>
+ <node name="status">
+ <properties>
+ <help>Intel QAT status</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_acceleration.py --status</command>
+ </node>
+ <node name="interrupts">
+ <properties>
+ <help>Intel QAT interrupts</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_acceleration.py --interrupts</command>
+ </node>
+ </children>
+ <command>${vyos_op_scripts_dir}/show_acceleration.py --hw</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-bridge.xml b/op-mode-definitions/show-bridge.xml
new file mode 100644
index 000000000..8c1f7c398
--- /dev/null
+++ b/op-mode-definitions/show-bridge.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <leafNode name="bridge">
+ <properties>
+ <help>Show bridging information</help>
+ </properties>
+ <command>/sbin/brctl show</command>
+ </leafNode>
+ <tagNode name="bridge">
+ <properties>
+ <help>Show bridge information for a given bridge interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --type bridge</script>
+ </completionHelp>
+ </properties>
+ <command>/sbin/brctl show $3</command>
+ <children>
+ <leafNode name="macs">
+ <properties>
+ <help>Show bridge Media Access Control (MAC) address table</help>
+ </properties>
+ <command>/sbin/brctl showmacs $3</command>
+ </leafNode>
+ <leafNode name="spanning-tree">
+ <properties>
+ <help>Show bridge spanning tree information</help>
+ </properties>
+ <command>/sbin/brctl showstp $3</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-configuration.xml b/op-mode-definitions/show-configuration.xml
new file mode 100644
index 000000000..318942ab0
--- /dev/null
+++ b/op-mode-definitions/show-configuration.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="configuration">
+ <properties>
+ <help>Show available saved configurations</help>
+ </properties>
+ <!-- no admin check -->
+ <command>cli-shell-api showCfg --show-active-only --show-hide-secrets</command>
+ <children>
+ <node name="all">
+ <properties>
+ <help>Show running configuration (including default values)</help>
+ </properties>
+ <!-- no admin check -->
+ <command>cli-shell-api showCfg --show-show-defaults --show-active-only --show-hide-secrets</command>
+ </node>
+ <node name="commands">
+ <properties>
+ <help> Show running configuration as set commands </help>
+ </properties>
+ <!-- no admin check -->
+ <command>cli-shell-api showCfg --show-active-only | vyos-config-to-commands</command>
+ </node>
+ <node name="files">
+ <properties>
+ <help> Show available saved configurations </help>
+ </properties>
+ <!-- no admin check -->
+ <command>${vyos_op_scripts_dir}/show_configuration_files.sh</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-console-server.xml b/op-mode-definitions/show-console-server.xml
new file mode 100644
index 000000000..77a7f3376
--- /dev/null
+++ b/op-mode-definitions/show-console-server.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="log">
+ <children>
+ <leafNode name="console-server">
+ <properties>
+ <help>Show log for serial console server</help>
+ </properties>
+ <command>/usr/bin/journalctl -u conserver-server.service</command>
+ </leafNode>
+ </children>
+ </node>
+ <node name="console-server">
+ <properties>
+ <help>Show Console-Server information</help>
+ </properties>
+ <children>
+ <leafNode name="ports">
+ <properties>
+ <help>Examine console ports and configured baud rates</help>
+ </properties>
+ <command>/usr/bin/console -x</command>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Show users on various consoles</help>
+ </properties>
+ <command>/usr/bin/console -u</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-environment.xml b/op-mode-definitions/show-environment.xml
new file mode 100644
index 000000000..95b658785
--- /dev/null
+++ b/op-mode-definitions/show-environment.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="environment">
+ <properties>
+ <help>Show current system environmental conditions</help>
+ </properties>
+ <children>
+ <leafNode name="sensors">
+ <properties>
+ <help>Show hardware monitoring results</help>
+ </properties>
+ <!-- Linux always adds "hypervisor" to CPU flags -->
+ <command>if ! grep -q hypervisor /proc/cpuinfo; then ${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_sensors.py; else echo "VyOS running under hypervisor, no sensors available"; fi</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-hardware.xml b/op-mode-definitions/show-hardware.xml
new file mode 100644
index 000000000..c3ff3a60f
--- /dev/null
+++ b/op-mode-definitions/show-hardware.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="hardware">
+ <properties>
+ <help>Show system hardware details</help>
+ </properties>
+ <children>
+ <node name="cpu">
+ <properties>
+ <help>Show CPU info</help>
+ </properties>
+ <command>lscpu</command>
+ <children>
+ <node name="detail">
+ <properties>
+ <help> Show system CPU details</help>
+ </properties>
+ <command>cat /proc/cpuinfo</command>
+ </node>
+ <node name="summary">
+ <properties>
+ <help>Show CPU's on system</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/cpu_summary.py</command>
+ </node>
+ </children>
+ </node>
+ <node name="dmi">
+ <properties>
+ <help>Show system DMI details</help>
+ </properties>
+ <command>${vyatta_bindir}/vyatta-show-dmi</command>
+ </node>
+ <node name="mem">
+ <properties>
+ <help>Show system RAM details</help>
+ </properties>
+ <command>cat /proc/meminfo</command>
+ </node>
+ <node name="pci">
+ <properties>
+ <help>Show system PCI bus details</help>
+ </properties>
+ <command>lspci</command>
+ <children>
+ <node name="detail">
+ <properties>
+ <help>Show verbose system PCI bus details</help>
+ </properties>
+ <command>lspci -vvv</command>
+ </node>
+ </children>
+ </node>
+ <node name="scsi">
+ <properties>
+ <help>Show SCSI device information</help>
+ </properties>
+ <command>lsscsi</command>
+ <children>
+ <node name="detail">
+ <properties>
+ <help>Show detailed SCSI device information</help>
+ </properties>
+ <command>lsscsi -vvv</command>
+ </node>
+ </children>
+ </node>
+ <node name="usb">
+ <properties>
+ <help>Show peripherals connected to the USB bus</help>
+ </properties>
+ <command>/usr/bin/lsusb -t</command>
+ <children>
+ <node name="detail">
+ <properties>
+ <help>Show detailed USB bus information</help>
+ </properties>
+ <command>/usr/bin/lsusb -v</command>
+ </node>
+ <leafNode name="serial">
+ <properties>
+ <help>Show information about connected USB serial ports</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_usb_serial.py</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-history.xml b/op-mode-definitions/show-history.xml
new file mode 100644
index 000000000..7fb286264
--- /dev/null
+++ b/op-mode-definitions/show-history.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="history">
+ <properties>
+ <help>Show command history</help>
+ </properties>
+ <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show recent command history</help>
+ </properties>
+ <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history 20</command>
+ </leafNode>
+ </children>
+ </node>
+
+ <tagNode name="history">
+ <properties>
+ <help>Show last N commands in history</help>
+ <completionHelp>
+ <list>&lt;NUMBER&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history $3</command>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-host.xml b/op-mode-definitions/show-host.xml
new file mode 100644
index 000000000..eee1288a1
--- /dev/null
+++ b/op-mode-definitions/show-host.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="host">
+ <properties>
+ <help>Show host information</help>
+ </properties>
+ <children>
+ <leafNode name="date">
+ <properties>
+ <help>Show host current date</help>
+ </properties>
+ <command>/bin/date</command>
+ </leafNode>
+ <leafNode name="domain">
+ <properties>
+ <help>Show domain name</help>
+ </properties>
+ <command>/bin/domainname -d</command>
+ </leafNode>
+ <leafNode name="name">
+ <properties>
+ <help>Show host name</help>
+ </properties>
+ <command>/bin/hostname</command>
+ </leafNode>
+ <tagNode name="lookup">
+ <properties>
+ <help>Lookup host information for hostname|IPv4 address</help>
+ </properties>
+ <command>/usr/bin/host $4</command>
+ </tagNode>
+ <leafNode name="os">
+ <properties>
+ <help>Show host operating system details</help>
+ </properties>
+ <command>/bin/uname -a</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-bonding.xml b/op-mode-definitions/show-interfaces-bonding.xml
new file mode 100644
index 000000000..568b215af
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-bonding.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="bonding">
+ <properties>
+ <help>Show bonding interface information</help>
+ <completionHelp>
+ <path>interfaces bonding</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified bonding interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ <tagNode name="vif">
+ <properties>
+ <help>Show specified virtual network interface (vif) information</help>
+ <completionHelp>
+ <path>interfaces bonding ${COMP_WORDS[3]} vif</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of specified virtual network interface (vif) information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <node name="bonding">
+ <properties>
+ <help>Show bonding interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bonding --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed bonding interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bonding --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-bridge.xml b/op-mode-definitions/show-interfaces-bridge.xml
new file mode 100644
index 000000000..85fde95b5
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-bridge.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="bridge">
+ <properties>
+ <help>Show bridge interface information</help>
+ <completionHelp>
+ <path>interfaces bridge</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified bridge interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="bridge">
+ <properties>
+ <help>Show bridge interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bridge --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed bridge interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bridge --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-dummy.xml b/op-mode-definitions/show-interfaces-dummy.xml
new file mode 100644
index 000000000..7c24c6921
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-dummy.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="dummy">
+ <properties>
+ <help>Show dummy interface information</help>
+ <completionHelp>
+ <path>interfaces dummy</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified dummy interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="dummy">
+ <properties>
+ <help>Show dummy interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=dummy --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed dummy interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=dummy --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-ethernet.xml b/op-mode-definitions/show-interfaces-ethernet.xml
new file mode 100644
index 000000000..bdcfa55f1
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-ethernet.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="ethernet">
+ <properties>
+ <help>Show ethernet interface information</help>
+ <completionHelp>
+ <path>interfaces ethernet</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified ethernet interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ <leafNode name="identify">
+ <properties>
+ <help>Visually identify specified ethernet interface</help>
+ </properties>
+ <command>echo "Blinking interface $4 for 30 seconds."; /sbin/ethtool --identify "$4" 30</command>
+ </leafNode>
+ <node name="physical">
+ <properties>
+ <help>Show physical device information for specified ethernet interface</help>
+ </properties>
+ <command>/sbin/ethtool "$4"; /sbin/ethtool -i "$4"</command>
+ <children>
+ <leafNode name="offload">
+ <properties>
+ <help>Show physical device offloading capabilities</help>
+ </properties>
+ <command>/sbin/ethtool -k "$4" | sed -e 1d -e '/fixed/d' -e 's/^\t*//g' -e 's/://' | column -t -s' '</command>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show physical device statistics for specified ethernet interface</help>
+ </properties>
+ <command>/sbin/ethtool -S "$4"</command>
+ </leafNode>
+ <leafNode name="transceiver">
+ <properties>
+ <help>Show transceiver information from modules (e.g SFP+, QSFP)</help>
+ </properties>
+ <command>/sbin/ethtool -m "$4"</command>
+ </leafNode>
+ <tagNode name="vif">
+ <properties>
+ <help>Show specified virtual network interface (vif) information</help>
+ <completionHelp>
+ <path>interfaces ethernet ${COMP_WORDS[3]} vif</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of specified virtual network interface (vif) information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <node name="ethernet">
+ <properties>
+ <help>Show ethernet interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=ethernet --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed ethernet interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=ethernet --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-input.xml b/op-mode-definitions/show-interfaces-input.xml
new file mode 100644
index 000000000..15e8203e5
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-input.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="input">
+ <properties>
+ <help>Show input interface information</help>
+ <completionHelp>
+ <path>interfaces input</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified input interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="input">
+ <properties>
+ <help>Show input interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=input --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed input interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=input --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-l2tpv3.xml b/op-mode-definitions/show-interfaces-l2tpv3.xml
new file mode 100644
index 000000000..60fee34a1
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-l2tpv3.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="l2tpv3">
+ <properties>
+ <help>Show L2TPv3 interface information</help>
+ <completionHelp>
+ <path>interfaces l2tpv3</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified L2TPv3 interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="l2tpv3">
+ <properties>
+ <help>Show L2TPv3 interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=l2tpv3 --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed L2TPv3 interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=l2tpv3 --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-loopback.xml b/op-mode-definitions/show-interfaces-loopback.xml
new file mode 100644
index 000000000..b30b57909
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-loopback.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="loopback">
+ <properties>
+ <help>Show loopback interface information</help>
+ <completionHelp>
+ <path>interfaces loopback</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified dummy interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="loopback">
+ <properties>
+ <help>Show loopback interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=loopback --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed dummy interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=dummy --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-macsec.xml b/op-mode-definitions/show-interfaces-macsec.xml
new file mode 100644
index 000000000..6aeab66af
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-macsec.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <node name="macsec">
+ <properties>
+ <help>Show MACsec interface information</help>
+ <completionHelp>
+ <path>interfaces macsec</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/sbin/ip macsec show</command>
+ </node>
+ <tagNode name="macsec">
+ <properties>
+ <help>Show specified MACsec interface information</help>
+ <completionHelp>
+ <path>interfaces macsec</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/sbin/ip macsec show $4</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-pppoe.xml b/op-mode-definitions/show-interfaces-pppoe.xml
new file mode 100644
index 000000000..393ca912f
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-pppoe.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="pppoe">
+ <properties>
+ <help>Show PPPoE interface information</help>
+ <completionHelp>
+ <path>interfaces pppoe</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="log">
+ <properties>
+ <help>Show specified PPPoE interface log</help>
+ </properties>
+ <command>/usr/bin/journalctl -u "ppp@$4".service</command>
+ </leafNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show specified PPPoE interface statistics</help>
+ <completionHelp>
+ <path>interfaces pppoe</path>
+ </completionHelp>
+ </properties>
+ <command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="pppoe">
+ <properties>
+ <help>Show PPPoE interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pppoe --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed PPPoE interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pppoe --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-pseudo-ethernet.xml b/op-mode-definitions/show-interfaces-pseudo-ethernet.xml
new file mode 100644
index 000000000..195944745
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-pseudo-ethernet.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="pseudo-ethernet">
+ <properties>
+ <help>Show pseudo-ethernet/MACvlan interface information</help>
+ <completionHelp>
+ <path>interfaces pseudo-ethernet</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified pseudo-ethernet/MACvlan interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="pseudo-ethernet">
+ <properties>
+ <help>Show pseudo-ethernet/MACvlan interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pseudo-ethernet --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed pseudo-ethernet/MACvlan interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pseudo-ethernet --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-tunnel.xml b/op-mode-definitions/show-interfaces-tunnel.xml
new file mode 100644
index 000000000..416de0299
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-tunnel.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="tunnel">
+ <properties>
+ <help>Show tunnel interface information</help>
+ <completionHelp>
+ <path>interfaces tunnel</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified tunnel interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="tunnel">
+ <properties>
+ <help>Show tunnel interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=tunnel --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed tunnel interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=tunnel --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-vti.xml b/op-mode-definitions/show-interfaces-vti.xml
new file mode 100644
index 000000000..f51be2d19
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-vti.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="vti">
+ <properties>
+ <help>Show vti interface information</help>
+ <completionHelp>
+ <path>interfaces vti</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified vti interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="vti">
+ <properties>
+ <help>Show vti interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vti --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed vti interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vti --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-vxlan.xml b/op-mode-definitions/show-interfaces-vxlan.xml
new file mode 100644
index 000000000..4e3cb93cd
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-vxlan.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="vxlan">
+ <properties>
+ <help>Show VXLAN interface information</help>
+ <completionHelp>
+ <path>interfaces vxlan</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified VXLAN interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="vxlan">
+ <properties>
+ <help>Show VXLAN interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vxlan --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed VXLAN interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vxlan --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-wirelessmodem.xml b/op-mode-definitions/show-interfaces-wirelessmodem.xml
new file mode 100644
index 000000000..c0ab9c66f
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-wirelessmodem.xml
@@ -0,0 +1,51 @@
+<?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>
+ <path>interfaces wirelessmodem</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="log">
+ <properties>
+ <help>Show specified WWAN interface log</help>
+ </properties>
+ <command>/usr/bin/journalctl -u "ppp@$4".service</command>
+ </leafNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show specified WWAN interface statistics</help>
+ <completionHelp>
+ <path>interfaces wirelessmodem</path>
+ </completionHelp>
+ </properties>
+ <command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="wirelessmodem">
+ <properties>
+ <help>Show Wireless Modem (WWAN) interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wirelessmodem --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed Wireless Modem (WWAN( interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wirelessmodem --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces.xml b/op-mode-definitions/show-interfaces.xml
new file mode 100644
index 000000000..39b0f0a2c
--- /dev/null
+++ b/op-mode-definitions/show-interfaces.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <properties>
+ <help>Show network interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --action=show-brief</command>
+ <children>
+ <leafNode name="counters">
+ <properties>
+ <help>Show network interface counters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --action=show-count</command>
+ </leafNode>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed information of all interfaces</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ip-access-paths-prefix-community-lists.xml b/op-mode-definitions/show-ip-access-paths-prefix-community-lists.xml
new file mode 100644
index 000000000..a5ec65c94
--- /dev/null
+++ b/op-mode-definitions/show-ip-access-paths-prefix-community-lists.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ip">
+ <properties>
+ <help>Show IPv4 routing information</help>
+ </properties>
+ <children>
+ <leafNode name="access-list">
+ <properties>
+ <help>Show all IP access-lists</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip access-list"</command>
+ </leafNode>
+ <tagNode name="access-list">
+ <properties>
+ <help>Show all IP access-lists</help>
+ <completionHelp>
+ <path>policy access-list</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip access-list $4"</command>
+ </tagNode>
+ <leafNode name="as-path-access-list">
+ <properties>
+ <help>Show all as-path-access-lists</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip as-path-access-list"</command>
+ </leafNode>
+ <tagNode name="as-path-access-list">
+ <properties>
+ <help>Show all as-path-access-lists</help>
+ <completionHelp>
+ <path>policy as-path-list</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip as-path-access-list $4"</command>
+ </tagNode>
+ <leafNode name="community-list">
+ <properties>
+ <help>Show IP community-lists</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bgp community-list"</command>
+ </leafNode>
+ <tagNode name="community-list">
+ <properties>
+ <help>Show IP community-lists</help>
+ <completionHelp>
+ <path>policy community-list</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bgp community-list $4 detail"</command>
+ </tagNode>
+ <leafNode name="extcommunity-list">
+ <properties>
+ <help>Show extended IP community-lists</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bgp extcommunity-list"</command>
+ </leafNode>
+ <tagNode name="extcommunity-list">
+ <properties>
+ <help>Show extended IP community-lists</help>
+ <completionHelp>
+ <path>policy extcommunity-list</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bgp extcommunity-list $4 detail"</command>
+ </tagNode>
+ <leafNode name="forwarding">
+ <properties>
+ <help>Show IP forwarding status</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip forwarding"</command>
+ </leafNode>
+ <leafNode name="large-community-list">
+ <properties>
+ <help>Show IP large-community-lists</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bgp large-community-list"</command>
+ </leafNode>
+ <tagNode name="large-community-list">
+ <properties>
+ <help>Show IP large-community-lists</help>
+ <completionHelp>
+ <path>policy large-community-list</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bgp large-community-list $4 detail"</command>
+ </tagNode>
+ <leafNode name="prefix-list">
+ <properties>
+ <help>Show all IP prefix-lists</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip prefix-list"</command>
+ </leafNode>
+ <tagNode name="prefix-list">
+ <properties>
+ <help>Show all IP prefix-lists</help>
+ <completionHelp>
+ <path>policy prefix-list</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip prefix-list $4"</command>
+ </tagNode>
+ <leafNode name="protocol">
+ <properties>
+ <help>Show IP route-maps per protocol</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip protocol"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ip-bgp.xml b/op-mode-definitions/show-ip-bgp.xml
new file mode 100644
index 000000000..5eb2ae56e
--- /dev/null
+++ b/op-mode-definitions/show-ip-bgp.xml
@@ -0,0 +1,342 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ip">
+ <children>
+ <node name="bgp">
+ <properties>
+ <help>Show Border Gateway Protocol (BGP) information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp"</command>
+ <children>
+ <leafNode name="attribute-info">
+ <properties>
+ <help>Show BGP attribute information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp attribute-info"</command>
+ </leafNode>
+ <leafNode name="cidr-only">
+ <properties>
+ <help>Display only routes with non-natural netmasks</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp cidr-only"</command>
+ </leafNode>
+ <node name="community">
+ <properties>
+ <help>Show BGP routes matching the communities</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp community"</command>
+ </node>
+ <tagNode name="community">
+ <properties>
+ <help>Display routes matching the specified communities</help>
+ <completionHelp>
+ <list>&lt;AA:NN&gt; local-AS no-advertise no-export</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp community $5"</command>
+ </tagNode>
+ <leafNode name="community-info">
+ <properties>
+ <help>List all bgp community information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp community-info"</command>
+ </leafNode>
+ <tagNode name="community-list">
+ <properties>
+ <help>Show BGP routes matching specified community list</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp community-list $5"</command>
+ <children>
+ <leafNode name="exact-match">
+ <properties>
+ <help>Show BGP routes exactly matching specified community list</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp community-list $5 exact-match"</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="dampened-paths">
+ <properties>
+ <help>Show dampened BGP paths</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp dampening dampened-paths"</command>
+ </leafNode>
+ <tagNode name="filter-list">
+ <properties>
+ <help>Show BGP information for specified word</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp filter-list $5"</command>
+ </tagNode>
+ <leafNode name="flap-statistics">
+ <properties>
+ <help>Show flap statistics of routes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp dampening flap-statistics"</command>
+ </leafNode>
+ <node name="ipv4">
+ <properties>
+ <help>Show BGP IPv4 information</help>
+ </properties>
+ <children>
+ <node name="unicast">
+ <properties>
+ <help>Show BGP IPv4 unicast information</help>
+ </properties>
+ <children>
+ <leafNode name="cidr-only">
+ <properties>
+ <help>Display only routes with non-natural netmasks</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast cidr-only"</command>
+ </leafNode>
+ <node name="community"> <!-- START new code -->
+ <properties>
+ <help>Show BGP routes matching the communities</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast community"</command>
+ </node>
+ <tagNode name="community">
+ <properties>
+ <help>Display routes matching the specified communities</help>
+ <completionHelp>
+ <list>&lt;AA:NN&gt; local-AS no-advertise no-export</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast community $7"</command>
+ </tagNode>
+ <tagNode name="community-list">
+ <properties>
+ <help>Show BGP routes matching specified community list</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast community-list $7"</command>
+ <children>
+ <leafNode name="exact-match">
+ <properties>
+ <help>Show BGP routes exactly matching specified community list</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast community-list $7 exact-match"</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="filter-list">
+ <properties>
+ <help>Show BGP information for specified word</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp filter-list $5"</command>
+ </tagNode>
+ <tagNode name="neighbors">
+ <properties>
+ <help>Show detailed BGP IPv4 unicast neighbor information</help>
+ <completionHelp>
+ <script>vtysh -c "show ip bgp ipv4 unicast summary" | awk '{print $1}' | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast neighbors $7"</command>
+ <children>
+ <leafNode name="advertised-routes">
+ <properties>
+ <help>Show routes advertised to a BGP neighbor</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast neighbor $7 advertised-routes"</command>
+ </leafNode>
+ <leafNode name="prefix-counts">
+ <properties>
+ <help>Show detailed prefix count information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast neighbor $7 prefix-counts"</command>
+ </leafNode>
+ <leafNode name="received-routes">
+ <properties>
+ <help>Show the received routes from neighbor</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast neighbor $7 received-routes"</command>
+ </leafNode>
+ <leafNode name="routes">
+ <properties>
+ <help>Show routes learned from neighbor</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast neighbor $7 routes"</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="paths">
+ <properties>
+ <help>Show BGP path information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast paths"</command>
+ </leafNode>
+ <tagNode name="prefix-list">
+ <properties>
+ <help>Show BGP routes matching the specified prefix list</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast prefix-list $7"</command>
+ </tagNode>
+ <tagNode name="regexp">
+ <properties>
+ <help>Show BGP routes matching the specified AS path regular expression</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast regexp $5"</command>
+ </tagNode>
+ <tagNode name="route-map">
+ <properties>
+ <help>Show BGP routes matching the specified route map</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp route-map $5"</command>
+ </tagNode>
+ <leafNode name="summary">
+ <properties>
+ <help>Show summary of BGP information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp summary"</command>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="unicast">
+ <properties>
+ <help>Show BGP information for specified IP address or prefix</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;x.x.x.x/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp $6"</command>
+ </tagNode>
+ </children>
+ </node>
+ <node name="large-community">
+ <properties>
+ <help>Show BGP routes matching the specified large-communities</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp large-community"</command>
+ </node>
+ <leafNode name="large-community-info">
+ <properties>
+ <help>Show BGP large-community information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp large-community-info"</command>
+ </leafNode>
+ <tagNode name="large-community-list">
+ <properties>
+ <help>Show BGP routes matching the specified large-community list</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp large-community-list $5"</command>
+ </tagNode>
+ <leafNode name="memory">
+ <properties>
+ <help>Show BGP memory usage</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp memory"</command>
+ </leafNode>
+ <tagNode name="neighbors">
+ <properties>
+ <help>Show detailed BGP IPv4 unicast neighbor information</help>
+ <completionHelp>
+ <script>vtysh -c "show ip bgp summary" | awk '{print $1}' | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp neighbors $5"</command>
+ <children>
+ <leafNode name="advertised-routes">
+ <properties>
+ <help>Show routes advertised to a BGP neighbor</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 advertised-routes"</command>
+ </leafNode>
+ <leafNode name="dampened-routes">
+ <properties>
+ <help>Show dampened routes received from BGP neighbor</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 dampened-routes"</command>
+ </leafNode>
+ <leafNode name="flap-statistics">
+ <properties>
+ <help>Show flap statistics of the routes learned from BGP neighbor</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 flap-statistics"</command>
+ </leafNode>
+ <leafNode name="prefix-counts">
+ <properties>
+ <help>Show detailed prefix count information for BGP neighbor</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 prefix-counts"</command>
+ </leafNode>
+ <node name="received">
+ <properties>
+ <help>Show information received from BGP neighbor</help>
+ </properties>
+ <children>
+ <leafNode name="prefix-filter">
+ <properties>
+ <help>Show prefixlist filter</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 received prefix-filter"</command>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="received-routes">
+ <properties>
+ <help>Show received routes from BGP neighbor</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 received-routes"</command>
+ </leafNode>
+ <leafNode name="routes">
+ <properties>
+ <help>Show routes learned from BGP neighbor</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 routes"</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="paths">
+ <properties>
+ <help>Show BGP path information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp paths"</command>
+ </leafNode>
+ <tagNode name="prefix-list">
+ <properties>
+ <help>Show BGP routes matching the specified prefix list</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp prefix-list $5"</command>
+ </tagNode>
+ <tagNode name="regexp">
+ <properties>
+ <help>Show BGP routes matching the specified AS path regular expression</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp regexp $5"</command>
+ </tagNode>
+ <tagNode name="route-map">
+ <properties>
+ <help>Show BGP routes matching the specified route map</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp route-map $5"</command>
+ </tagNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show summary of BGP information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp statistics"</command>
+ </leafNode>
+ <leafNode name="summary">
+ <properties>
+ <help>Show summary of BGP information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp summary"</command>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="bgp">
+ <properties>
+ <help>Show BGP information for specified IP address or prefix</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;x.x.x.x/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip bgp $4"</command>
+ </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
new file mode 100644
index 000000000..5331d2e35
--- /dev/null
+++ b/op-mode-definitions/show-ip-multicast.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ip">
+ <children>
+ <node name="multicast">
+ <properties>
+ <help>Show IP multicast</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Show multicast interfaces</help>
+ </properties>
+ <command>if ps -C igmpproxy &amp;&gt;/dev/null; then ${vyos_op_scripts_dir}/show_igmpproxy.py --interface; else echo IGMP proxy not configured; fi</command>
+ </leafNode>
+ <leafNode name="mfc">
+ <properties>
+ <help>Show multicast fowarding cache</help>
+ </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>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ip-ospf.xml b/op-mode-definitions/show-ip-ospf.xml
new file mode 100644
index 000000000..99441d185
--- /dev/null
+++ b/op-mode-definitions/show-ip-ospf.xml
@@ -0,0 +1,579 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ip">
+ <properties>
+ <help>Show IPv4 routing information</help>
+ </properties>
+ <children>
+ <node name="ospf">
+ <properties>
+ <help>Show IPv4 Open Shortest Path First (OSPF) routing information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf"</command>
+ <children>
+ <leafNode name="border-routers">
+ <properties>
+ <help>Show IPv4 OSPF border-routers information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf border-routers"</command>
+ </leafNode>
+ <node name="database">
+ <properties>
+ <help>Show IPv4 OSPF database information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database"</command>
+ <children>
+ <node name="asbr-summary">
+ <properties>
+ <help>Show IPv4 OSPF ASBR summary database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database asbr-summary"</command>
+ <children>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF ASBR summary database for given address of advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database asbr-summary adv-router $7"</command>
+ </tagNode>
+ <node name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF ASBR summary database for given address of advertised router</help>
+ </properties>
+ </node>
+ </children>
+ </node>
+ <tagNode name="asbr-summary">
+ <properties>
+ <help>Show IPv4 OSPF ASBR summary database information of given address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database asbr-summary"</command>
+ <children>
+ <node name="adv-router">
+ <properties>
+ <help>Show advertising router link states</help>
+ </properties>
+ </node>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF ASBR summary database of given address for given advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database asbr-summary $6 adv-router $8"</command>
+ </tagNode>
+ <leafNode name="self-originate">
+ <properties>
+ <help>Show summary of self-originate IPv4 OSPF ASBR database</help>
+ </properties>
+ <command>show ip ospf database asbr-summary $6 self-originate</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="external">
+ <properties>
+ <help>Show IPv4 OSPF external database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database external"</command>
+ <children>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF external database for specified IP address of advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database external adv-router $7"</command>
+ </tagNode>
+ <node name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF external database for specified IP address of advertised router</help>
+ </properties>
+ </node>
+ </children>
+ </node>
+ <tagNode name="external">
+ <properties>
+ <help>Show IPv4 OSPF external database information of specified IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database external"</command>
+ <children>
+ <node name="adv-router">
+ <properties>
+ <help>Show advertising router link states</help>
+ </properties>
+ </node>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF external database of specified IP address for specified advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database external $6 adv-router $8"</command>
+ </tagNode>
+ <leafNode name="self-originate">
+ <properties>
+ <help>Show self-originate IPv4 OSPF external database</help>
+ </properties>
+ <command>show ip ospf database external $6 self-originate</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="max-age">
+ <properties>
+ <help>Show IPv4 OSPF max-age database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database max-age"</command>
+ </leafNode>
+ <node name="network">
+ <properties>
+ <help>Show IPv4 OSPF network database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database network"</command>
+ <children>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF network database for specified IP address of advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database network adv-router $7"</command>
+ </tagNode>
+ <node name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF network database for given address of advertised router</help>
+ </properties>
+ </node>
+ </children>
+ </node>
+ <tagNode name="network">
+ <properties>
+ <help>Show IPv4 OSPF network database information of specified IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database network"</command>
+ <children>
+ <node name="adv-router">
+ <properties>
+ <help>Show advertising router link states</help>
+ </properties>
+ </node>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF network database of specified IP address for specified advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database network $6 adv-router $8"</command>
+ </tagNode>
+ <leafNode name="self-originate">
+ <properties>
+ <help>Show self-originate IPv4 OSPF network database</help>
+ </properties>
+ <command>show ip ospf database network $6 self-originate</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="nssa-external">
+ <properties>
+ <help>Show IPv4 OSPF NSSA external database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database nssa-external"</command>
+ <children>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF NSSA external database for specified IP address of advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database nssa-external adv-router $7"</command>
+ </tagNode>
+ <node name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF NSSA external database for specified IP address of advertised router</help>
+ </properties>
+ </node>
+ </children>
+ </node>
+ <tagNode name="nssa-external">
+ <properties>
+ <help>Show IPv4 OSPF NSSA external database information of specified IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database nssa-external"</command>
+ <children>
+ <node name="adv-router">
+ <properties>
+ <help>Show advertising router link states</help>
+ </properties>
+ </node>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF NSSA external database of specified IP address for specified advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database nssa-external $6 adv-router $8"</command>
+ </tagNode>
+ <leafNode name="self-originate">
+ <properties>
+ <help>Show self-originate IPv4 OSPF NSSA external database</help>
+ </properties>
+ <command>show ip ospf database nssa-external $6 self-originate</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="opaque-area">
+ <properties>
+ <help>Show IPv4 OSPF opaque-area database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-area"</command>
+ <children>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF opaque-area database for specified IP address of advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-area adv-router $7"</command>
+ </tagNode>
+ <node name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF opaque-area database for specified IP address of advertised router</help>
+ </properties>
+ </node>
+ </children>
+ </node>
+ <tagNode name="opaque-area">
+ <properties>
+ <help>Show IPv4 OSPF opaque-area database information of specified IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-area"</command>
+ <children>
+ <node name="adv-router">
+ <properties>
+ <help>Show advertising router link states</help>
+ </properties>
+ </node>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF opaque-area database of specified IP address for specified advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-area $6 adv-router $8"</command>
+ </tagNode>
+ <leafNode name="self-originate">
+ <properties>
+ <help>Show self-originate IPv4 OSPF opaque-area database</help>
+ </properties>
+ <command>show ip ospf database opaque-area $6 self-originate</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="opaque-as">
+ <properties>
+ <help>Show IPv4 OSPF opaque-as database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-as"</command>
+ <children>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF opaque-as database for specified IP address of advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-as adv-router $7"</command>
+ </tagNode>
+ <node name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF opaque-as database for specified IP address of advertised router</help>
+ </properties>
+ </node>
+ </children>
+ </node>
+ <tagNode name="opaque-as">
+ <properties>
+ <help>Show IPv4 OSPF opaque-as database information of specified IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-as"</command>
+ <children>
+ <node name="adv-router">
+ <properties>
+ <help>Show advertising router link states</help>
+ </properties>
+ </node>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF opaque-as database of specified IP address for specified advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-as $6 adv-router $8"</command>
+ </tagNode>
+ <leafNode name="self-originate">
+ <properties>
+ <help>Show self-originate IPv4 OSPF opaque-as database</help>
+ </properties>
+ <command>show ip ospf database opaque-as $6 self-originate</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="opaque-link">
+ <properties>
+ <help>Show IPv4 OSPF opaque-link database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-link"</command>
+ <children>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF opaque-link database for specified IP address of advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-link adv-router $7"</command>
+ </tagNode>
+ <node name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF opaque-link database for specified IP address of advertised router</help>
+ </properties>
+ </node>
+ </children>
+ </node>
+ <tagNode name="opaque-link">
+ <properties>
+ <help>Show IPv4 OSPF opaque-link database information of specified IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-link"</command>
+ <children>
+ <node name="adv-router">
+ <properties>
+ <help>Show advertising router link states</help>
+ </properties>
+ </node>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF opaque-link database of specified IP address for specified advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database opaque-link $6 adv-router $8"</command>
+ </tagNode>
+ <leafNode name="self-originate">
+ <properties>
+ <help>Show self-originate IPv4 OSPF opaque-link database</help>
+ </properties>
+ <command>show ip ospf database opaque-link $6 self-originate</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="router">
+ <properties>
+ <help>Show IPv4 OSPF router database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database router"</command>
+ <children>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF router database for specified IP address of advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database router adv-router $7"</command>
+ </tagNode>
+ <node name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF router database for specified IP address of advertised router</help>
+ </properties>
+ </node>
+ </children>
+ </node>
+ <tagNode name="router">
+ <properties>
+ <help>Show IPv4 OSPF router database information of specified IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database router"</command>
+ <children>
+ <node name="adv-router">
+ <properties>
+ <help>Show advertising router link states</help>
+ </properties>
+ </node>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF router database of specified IP address for specified advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database router $6 adv-router $8"</command>
+ </tagNode>
+ <leafNode name="self-originate">
+ <properties>
+ <help>Show self-originate IPv4 OSPF router database</help>
+ </properties>
+ <command>show ip ospf database router $6 self-originate</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="self-originate">
+ <properties>
+ <help>Show IPv4 OSPF self-originate database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database self-originate"</command>
+ </leafNode>
+ <node name="summary">
+ <properties>
+ <help>Show summary of IPv4 OSPF database</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database summary"</command>
+ <children>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF summary database for specified IP address of advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database summary adv-router $7"</command>
+ </tagNode>
+ <node name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF summary database for specified IP address of advertised router</help>
+ </properties>
+ </node>
+ </children>
+ </node>
+ <tagNode name="summary">
+ <properties>
+ <help>Show IPv4 OSPF summary database information of specified IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database summary"</command>
+ <children>
+ <node name="adv-router">
+ <properties>
+ <help>Show advertising router link states</help>
+ </properties>
+ </node>
+ <tagNode name="adv-router">
+ <properties>
+ <help>Show IPv4 OSPF summary database of specified IP address for specified advertised router</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf database summary $6 adv-router $8"</command>
+ </tagNode>
+ <leafNode name="self-originate">
+ <properties>
+ <help>Show self-originate IPv4 OSPF summary database</help>
+ </properties>
+ <command>show ip ospf database summary $6 self-originate</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="interface">
+ <properties>
+ <help>Show IPv4 OSPF interface information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf interface"</command>
+ </node>
+ <tagNode name="interface">
+ <properties>
+ <help>Show IPv4 OSPF information for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf interface $5"</command>
+ </tagNode>
+ <node name="neighbor">
+ <properties>
+ <help>Show IPv4 OSPF neighbor information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf neighbor"</command>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Show IPv4 OSPF neighbor information for specified IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf neighbor $6"</command>
+ </tagNode>
+ <node name="detail">
+ <properties>
+ <help>Show detailed IPv4 OSPF neighbor information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf neighbor detail"</command>
+ </node>
+ </children>
+ </node>
+ <tagNode name="neighbor">
+ <properties>
+ <help>Show IPv4 OSPF neighbor information for specified IP address or interface</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf neighbor $5"</command>
+ </tagNode>
+ <leafNode name="route">
+ <properties>
+ <help>Show IPv4 OSPF route information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip ospf route"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
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-ip-ports.xml b/op-mode-definitions/show-ip-ports.xml
new file mode 100644
index 000000000..a74b68ffc
--- /dev/null
+++ b/op-mode-definitions/show-ip-ports.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ip">
+ <children>
+ <leafNode name="ports">
+ <properties>
+ <help>Show IP ports in use by various system services</help>
+ </properties>
+ <command>sudo /usr/bin/netstat -tulnp</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ip-rip.xml b/op-mode-definitions/show-ip-rip.xml
new file mode 100644
index 000000000..b61ab10a7
--- /dev/null
+++ b/op-mode-definitions/show-ip-rip.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ip">
+ <properties>
+ <help>Show IPv4 routing information</help>
+ </properties>
+ <children>
+ <node name="rip">
+ <properties>
+ <help>Show Routing Information Protocol (RIP) information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip rip"</command>
+ <children>
+ <leafNode name="status">
+ <properties>
+ <help>Show RIP protocol status</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip rip status"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ip-route.xml b/op-mode-definitions/show-ip-route.xml
new file mode 100644
index 000000000..d12d132c0
--- /dev/null
+++ b/op-mode-definitions/show-ip-route.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ip">
+ <properties>
+ <help>Show IPv4 routing information</help>
+ </properties>
+ <children>
+ <node name="route">
+ <properties>
+ <help>Show IP routes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route"</command>
+ <children>
+ <leafNode name="bgp">
+ <properties>
+ <help>Show IP BGP routes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route bgp"</command>
+ </leafNode>
+ <node name="cache">
+ <properties>
+ <help>Show kernel route cache</help>
+ </properties>
+ <command>ip -s route list cache</command>
+ </node>
+ <tagNode name="cache">
+ <properties>
+ <help>Show kernel route cache for a given route</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;x.x.x.x/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>ip -s route list cache $5</command>
+ </tagNode>
+ <leafNode name="connected">
+ <properties>
+ <help>Show IP connected routes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route connected"</command>
+ </leafNode>
+ <node name="forward">
+ <properties>
+ <help>Show kernel route table</help>
+ </properties>
+ <command>ip route list</command>
+ </node>
+ <tagNode name="forward">
+ <properties>
+ <help>Show kernel route table for a given route</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;x.x.x.x/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>ip -s route list $5</command>
+ </tagNode>
+ <leafNode name="kernel">
+ <properties>
+ <help>Show IP kernel routes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route kernel"</command>
+ </leafNode>
+ <leafNode name="ospf">
+ <properties>
+ <help>Show IP OSPF routes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route ospf"</command>
+ </leafNode>
+ <leafNode name="rip">
+ <properties>
+ <help>Show IP RIP routes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route rip"</command>
+ </leafNode>
+ <leafNode name="static">
+ <properties>
+ <help>Show IP static routes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route static"</command>
+ </leafNode>
+ <leafNode name="summary">
+ <properties>
+ <help>Show IP routes summary</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route summary"</command>
+ </leafNode>
+ <leafNode name="supernets-only">
+ <properties>
+ <help>Show IP supernet routes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route supernets-only"</command>
+ </leafNode>
+ <node name="table">
+ <properties>
+ <help>Show IP routes in policy table</help>
+ </properties>
+ </node>
+ <tagNode name="table">
+ <properties>
+ <help>Show IP routes in policy table</help>
+ <completionHelp>
+ <list>&lt;1-200&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route table $5"</command>
+ </tagNode>
+ <node name="tag">
+ <properties>
+ <help>Show only routes with tag</help>
+ </properties>
+ </node>
+ <tagNode name="tag">
+ <properties>
+ <help>Tag value</help>
+ <completionHelp>
+ <list>&lt;1-4294967295&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route tag $5"</command>
+ </tagNode>
+ <node name="vrf">
+ <properties>
+ <help>Show IP routes in VRF</help>
+ </properties>
+ </node>
+ <tagNode name="vrf">
+ <properties>
+ <help>Show IP routes in VRF</help>
+ <completionHelp>
+ <list>&lt;vrf&gt;</list>
+ <path>vrf name</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route vrf $5"</command>
+ </tagNode>
+ </children>
+ </node>
+ <tagNode name="route">
+ <properties>
+ <help>Show IP routes of specified IP address or prefix</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;x.x.x.x/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route $4"</command>
+ <children>
+ <leafNode name="longer-prefixes">
+ <properties>
+ <help>Show longer prefixes of routes for specified IP address or prefix</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show ip route $4"</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ipv6-bgp.xml b/op-mode-definitions/show-ipv6-bgp.xml
new file mode 100644
index 000000000..67a8c8658
--- /dev/null
+++ b/op-mode-definitions/show-ipv6-bgp.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ipv6">
+ <children>
+ <node name="bgp">
+ <children>
+ <tagNode name="route-map">
+ <properties>
+ <help>Show BGP routes matching the specified route map</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bgp ipv6 route-map $5"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-license.xml b/op-mode-definitions/show-license.xml
new file mode 100644
index 000000000..2ce11567d
--- /dev/null
+++ b/op-mode-definitions/show-license.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <leafNode name="license">
+ <properties>
+ <help>Show VyOS license information</help>
+ </properties>
+ <command>less $_vyatta_less_options --prompt=".license, page %dt of %D" -- ${vyatta_sysconfdir}/LICENSE</command>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-log.xml b/op-mode-definitions/show-log.xml
new file mode 100644
index 000000000..0c4da647b
--- /dev/null
+++ b/op-mode-definitions/show-log.xml
@@ -0,0 +1,218 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="log">
+ <properties>
+ <help>Show contents of current master log file</help>
+ </properties>
+ <command>/bin/journalctl</command>
+ <children>
+ <leafNode name="all">
+ <properties>
+ <help>Show contents of all master log files</help>
+ </properties>
+ <command>eval $(lesspipe); less $_vyatta_less_options --prompt=".log?m, file %i of %m., page %dt of %D" -- `printf "%s\n" /var/log/messages* | sort -nr`</command>
+ </leafNode>
+ <leafNode name="authorization">
+ <properties>
+ <help>Show listing of authorization attempts</help>
+ </properties>
+ <command>/bin/journalctl -q SYSLOG_FACILITY=10 SYSLOG_FACILITY=4</command>
+ </leafNode>
+ <leafNode name="cluster">
+ <properties>
+ <help>Show log for Cluster</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e heartbeat -e cl_status -e mach_down -e ha_log</command>
+ </leafNode>
+ <leafNode name="conntrack-sync">
+ <properties>
+ <help>Show log for Conntrack-sync</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr ) | grep -e conntrackd</command>
+ </leafNode>
+ <leafNode name="dhcp">
+ <properties>
+ <help>Show log for Dynamic Host Control Protocol (DHCP)</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep dhcpd</command>
+ </leafNode>
+ <node name="firewall">
+ <properties>
+ <help>Show log for Firewall</help>
+ </properties>
+ <children>
+ <tagNode name="ipv6-name">
+ <properties>
+ <help>Show log for a specified firewall (IPv6)</help>
+ <completionHelp>
+ <path>firewall ipv6-name</path>
+ </completionHelp>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr ) | egrep "\[$5-([0-9]+|default)-[ADR]\]"</command>
+ <children>
+ <tagNode name="rule">
+ <properties>
+ <help>Show log for a rule in the specified firewall</help>
+ <completionHelp>
+ <path>firewall ipv6-name ${COMP_WORDS[4]} rule</path>
+ </completionHelp>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "\[$5-$7-[ADR]\]"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="name">
+ <properties>
+ <help>Show log for a specified firewall (IPv4)</help>
+ <completionHelp>
+ <path>firewall name</path>
+ </completionHelp>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr ) | egrep "\[$5-([0-9]+|default)-[ADR]\]"</command>
+ <children>
+ <tagNode name="rule">
+ <properties>
+ <help>Show log for a rule in the specified firewall</help>
+ <completionHelp>
+ <path>firewall name ${COMP_WORDS[4]} rule</path>
+ </completionHelp>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | egrep "\[$5-$7-[ADR]\]"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <leafNode name="https">
+ <properties>
+ <help>Show log for HTTPs</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e nginx</command>
+ </leafNode>
+ <tagNode name="image">
+ <properties>
+ <help>Show contents of master log file for image</help>
+ <completionHelp>
+ <script>compgen -f /lib/live/mount/persistence/boot/ | grep -v grub | sed -e s@/lib/live/mount/persistence/boot/@@</script>
+ </completionHelp>
+ </properties>
+ <command>less $_vyatta_less_options --prompt=".log, page %dt of %D" -- /lib/live/mount/persistence/boot/$4/rw/var/log/messages</command>
+ <children>
+ <leafNode name="all">
+ <properties>
+ <help>Show contents of all master log files for image</help>
+ </properties>
+ <command>eval $(lesspipe); less $_vyatta_less_options --prompt=".log?m, file %i of %m., page %dt of %D" -- `printf "%s\n" /lib/live/mount/persistence/boot/$4/rw/var/log/messages* | sort -nr`</command>
+ </leafNode>
+ <leafNode name="authorization">
+ <properties>
+ <help>Show listing of authorization attempts for image</help>
+ </properties>
+ <command>less $_vyatta_less_options --prompt=".log, page %dt of %D" -- /lib/live/mount/persistence/boot/$4/rw/var/log/auth.log</command>
+ </leafNode>
+ <tagNode name="tail">
+ <properties>
+ <help>Show last changes to messages</help>
+ <completionHelp>
+ <list>&lt;NUMBER&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>tail -n "$6" /lib/live/mount/persistence/boot/$4/rw/var/log/messages | ${VYATTA_PAGER:-cat}</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ <leafNode name="lldp">
+ <properties>
+ <help>Show log for LLDP</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e lldpd</command>
+ </leafNode>
+ <leafNode name="nat">
+ <properties>
+ <help>Show log for Network Address Translation (NAT)</help>
+ </properties>
+ <command>egrep -i "kernel:.*\[NAT-[A-Z]{3,}-[0-9]+(-MASQ)?\]" $(find /var/log -maxdepth 1 -type f -name messages\* | sort -t. -k2nr)</command>
+ </leafNode>
+ <leafNode name="nat">
+ <properties>
+ <help>Show log for OpenVPN</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e openvpn</command>
+ </leafNode>
+ <leafNode name="snmp">
+ <properties>
+ <help>Show log for Simple Network Monitoring Protocol (SNMP)</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e snmpd</command>
+ </leafNode>
+ <tagNode name="tail">
+ <properties>
+ <help>Show last n changes to messages</help>
+ <completionHelp>
+ <list>&lt;NUMBER&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>tail -n "$4" /var/log/messages | ${VYATTA_PAGER:-cat}</command>
+ </tagNode>
+ <node name="tail">
+ <properties>
+ <help>Show last 10 lines of /var/log/messages file</help>
+ </properties>
+ <command>tail -n 10 /var/log/messages</command>
+ </node>
+ <node name="vpn">
+ <properties>
+ <help>Show log for Virtual Private Network (VPN)</help>
+ </properties>
+ <children>
+ <leafNode name="all">
+ <properties>
+ <help>Show log for ALL</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e charon -e accel -e pptpd -e ppp</command>
+ </leafNode>
+ <leafNode name="ipsec">
+ <properties>
+ <help>Show log for IPSec</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e charon</command>
+ </leafNode>
+ <leafNode name="l2tp">
+ <properties>
+ <help>Show log for L2TP</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e remote-access-aaa-win -e remote-access-zzz-mac -e accel-l2tp -e ppp</command>
+ </leafNode>
+ <leafNode name="pptp">
+ <properties>
+ <help>Show log for PPTP</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e accel-pptp -e ppp</command>
+ </leafNode>
+ <leafNode name="sstp">
+ <properties>
+ <help>Show log for SSTP</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e accel-sstp -e ppp</command>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="vrrp">
+ <properties>
+ <help>Show log for Virtual Router Redundancy Protocol (VRRP)</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e Keepalived_vrrp</command>
+ </leafNode>
+ <leafNode name="webproxy">
+ <properties>
+ <help>Show log for Webproxy</help>
+ </properties>
+ <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "squid"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-login.xml b/op-mode-definitions/show-login.xml
new file mode 100644
index 000000000..6d8c782c4
--- /dev/null
+++ b/op-mode-definitions/show-login.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="login">
+ <properties>
+ <help>Show current login credentials</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_current_user.sh</command>
+ <children>
+ <leafNode name="groups">
+ <properties>
+ <help>Show current login group information</help>
+ </properties>
+ <command>/usr/bin/id -Gn</command>
+ </leafNode>
+ <leafNode name="level">
+ <properties>
+ <help>Show current login level</help>
+ </properties>
+ <command>if [ -n "$VYATTA_USER_LEVEL_DIR" ]; then basename $VYATTA_USER_LEVEL_DIR; fi</command>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Show current login user id</help>
+ </properties>
+ <command>/usr/bin/id -un</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-monitoring.xml b/op-mode-definitions/show-monitoring.xml
new file mode 100644
index 000000000..2651b3438
--- /dev/null
+++ b/op-mode-definitions/show-monitoring.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <leafNode name="monitoring">
+ <properties>
+ <help>Show currently monitored services</help>
+ </properties>
+ <command>vtysh -c "show debugging"</command>
+ </leafNode>
+ </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..3f160b6dc
--- /dev/null
+++ b/op-mode-definitions/show-mpls.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="mpls">
+ <properties>
+ <help>Show Multiprotocol Label Switching (MPLS)</help>
+ </properties>
+ <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
new file mode 100644
index 000000000..b7f0acdf8
--- /dev/null
+++ b/op-mode-definitions/show-ntp.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ntp">
+ <properties>
+ <help>Show peer status of NTP daemon</help>
+ </properties>
+ <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>
+ <help>Show date and time of specified NTP server</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_ntp_servers.sh</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/sbin/ntpdate -q "$4"</command>
+ </tagNode>
+ <node name="info">
+ <properties>
+ <help>Show NTP operational summary</help>
+ </properties>
+ <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>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-poweroff.xml b/op-mode-definitions/show-poweroff.xml
new file mode 100644
index 000000000..1fd2afcc3
--- /dev/null
+++ b/op-mode-definitions/show-poweroff.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <leafNode name="poweroff">
+ <properties>
+ <help>Show scheduled poweroff</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/powerctrl.py --check</command>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-protocols-bfd.xml b/op-mode-definitions/show-protocols-bfd.xml
new file mode 100644
index 000000000..4483b39bf
--- /dev/null
+++ b/op-mode-definitions/show-protocols-bfd.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="protocols">
+ <properties>
+ <help>Show protocol specific information</help>
+ </properties>
+ <children>
+ <node name="bfd">
+ <children>
+ <node name="peer">
+ <properties>
+ <help>Show all Bidirectional Forwarding Detection (BFD) peer status</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bfd peers"</command>
+ <children>
+ <leafNode name="counters">
+ <properties>
+ <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bfd peers counters"</command>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="peer">
+ <properties>
+ <help>Show Bidirectional Forwarding Detection (BFD) peer status</help>
+ <completionHelp>
+ <script>/usr/bin/vtysh -c "show bfd peers" | awk '/[:blank:]*peer/ { printf "%s\n", $2 }'</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 'BEGIN { regex = sprintf("(peer %s.*)vrf", BFD_PEER) } { if (match($0, regex, bfd_peer_value)) peer=bfd_peer_value[1] } END { if (peer) system("/usr/bin/vtysh -c \"show bfd " peer "\"") }'</command>
+ <children>
+ <leafNode name="counters">
+ <properties>
+ <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 'BEGIN { regex = sprintf("(peer %s.*)vrf", BFD_PEER) } { if (match($0, regex, bfd_peer_value)) peer=bfd_peer_value[1] } END { if (peer) system("/usr/bin/vtysh -c \"show bfd " peer " counters\"") }'</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-protocols-static.xml b/op-mode-definitions/show-protocols-static.xml
new file mode 100644
index 000000000..1211a7fe5
--- /dev/null
+++ b/op-mode-definitions/show-protocols-static.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="protocols">
+ <children>
+ <node name="static">
+ <children>
+ <node name="arp">
+ <properties>
+ <help>Show Address Resolution Protocol (ARP) information</help>
+ </properties>
+ <command>/usr/sbin/arp -e -n</command>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Show Address Resolution Protocol (ARP) cache for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py -b</script>
+ </completionHelp>
+ </properties>
+ <command>/usr/sbin/arp -e -n -i "$6"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-raid.xml b/op-mode-definitions/show-raid.xml
new file mode 100644
index 000000000..8bf394552
--- /dev/null
+++ b/op-mode-definitions/show-raid.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <tagNode name="raid">
+ <properties>
+ <help>Show status of RAID set</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_raidset.sh</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_raid.sh $3</command>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-reboot.xml b/op-mode-definitions/show-reboot.xml
new file mode 100644
index 000000000..c85966bcb
--- /dev/null
+++ b/op-mode-definitions/show-reboot.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <leafNode name="reboot">
+ <properties>
+ <help>Show scheduled reboot</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/powerctrl.py --check</command>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-route-map.xml b/op-mode-definitions/show-route-map.xml
new file mode 100644
index 000000000..0e376757b
--- /dev/null
+++ b/op-mode-definitions/show-route-map.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="route-map">
+ <properties>
+ <help>Show route-map information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show route-map"</command>
+ </node>
+ <tagNode name="route-map">
+ <properties>
+ <help>Show specified route-map information</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show route-map $3"</command>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-rpki.xml b/op-mode-definitions/show-rpki.xml
new file mode 100644
index 000000000..d68c3b862
--- /dev/null
+++ b/op-mode-definitions/show-rpki.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="rpki">
+ <properties>
+ <help>Show RPKI information</help>
+ </properties>
+ <children>
+ <leafNode name="cache-connection">
+ <properties>
+ <help>Show RPKI cache connections</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show rpki cache-connection"</command>
+ </leafNode>
+ <leafNode name="cache-server">
+ <properties>
+ <help>Show RPKI cache servers information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show rpki cache-server"</command>
+ </leafNode>
+ <leafNode name="prefix-table">
+ <properties>
+ <help>Show RPKI-validated prefixes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show rpki prefix-table"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-system.xml b/op-mode-definitions/show-system.xml
new file mode 100644
index 000000000..1b98b559b
--- /dev/null
+++ b/op-mode-definitions/show-system.xml
@@ -0,0 +1,183 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="system">
+ <properties>
+ <help>Show system information</help>
+ </properties>
+ <children>
+ <node name="connections">
+ <properties>
+ <help>Show active network connections on the system</help>
+ </properties>
+ <command>netstat -an</command>
+ <children>
+ <node name="tcp">
+ <properties>
+ <help>Show TCP connection information</help>
+ </properties>
+ <command>ss -t -r</command>
+ <children>
+ <leafNode name="all">
+ <properties>
+ <help>Show all TCP connections</help>
+ </properties>
+ <command>ss -t -a</command>
+ </leafNode>
+ <leafNode name="numeric">
+ <properties>
+ <help>Show TCP connection without resolving names</help>
+ </properties>
+ <command>ss -t -n</command>
+ </leafNode>
+ </children>
+ </node>
+ <node name="udp">
+ <properties>
+ <help>Show UDP socket information</help>
+ </properties>
+ <command>ss -u -a -r</command>
+ <children>
+ <leafNode name="numeric">
+ <properties>
+ <help>Show UDP socket information without resolving names</help>
+ </properties>
+ <command>ss -u -a -n</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="cpu">
+ <properties>
+ <help>Show CPU information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_cpu.py</command>
+ </leafNode>
+ <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>
+ </properties>
+ <command>sudo dmesg</command>
+ </leafNode>
+ <node name="login">
+ <properties>
+ <help>Show user accounts</help>
+ </properties>
+ <children>
+ <node name="users">
+ <properties>
+ <help>Show user account information</help>
+ </properties>
+ <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py</command>
+ <children>
+ <leafNode name="all">
+ <properties>
+ <help>Show information about all accounts</help>
+ </properties>
+ <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py all</command>
+ </leafNode>
+ <leafNode name="locked">
+ <properties>
+ <help>Show information about locked accounts</help>
+ </properties>
+ <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py locked</command>
+ </leafNode>
+ <leafNode name="other">
+ <properties>
+ <help>Show information about non VyOS user accounts</help>
+ </properties>
+ <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py other</command>
+ </leafNode>
+ <leafNode name="vyos">
+ <properties>
+ <help>Show information about VyOS user accounts</help>
+ </properties>
+ <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py vyos</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="memory">
+ <properties>
+ <help>Show system memory usage</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_ram.sh</command>
+ <children>
+ <leafNode name="cache">
+ <properties>
+ <help>Show kernel cache information</help>
+ </properties>
+ <command>sudo slabtop -o</command>
+ </leafNode>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed system memory usage</help>
+ </properties>
+ <command>cat /proc/meminfo</command>
+ </leafNode>
+ <leafNode name="routing-daemons">
+ <properties>
+ <help>Show memory usage of all routing protocols</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show memory"</command>
+ </leafNode>
+ </children>
+ </node>
+ <node name="processes">
+ <properties>
+ <help>Show system processes</help>
+ </properties>
+ <command>ps ax</command>
+ <children>
+ <leafNode name="extensive">
+ <properties>
+ <help>Show extensive process info</help>
+ </properties>
+ <command>top -b -n1</command>
+ </leafNode>
+ <leafNode name="summary">
+ <properties>
+ <help>Show summary of system processes</help>
+ </properties>
+ <command>uptime</command>
+ </leafNode>
+ <leafNode name="tree">
+ <properties>
+ <help>Show process tree</help>
+ </properties>
+ <command>ps -ejH</command>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="routing-daemons">
+ <properties>
+ <help>Show Quagga routing daemons</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show daemons"</command>
+ </leafNode>
+ <leafNode name="storage">
+ <properties>
+ <help>Show filesystem usage</help>
+ </properties>
+ <command>df -h -x squashfs</command>
+ </leafNode>
+ <leafNode name="uptime">
+ <properties>
+ <help>Show how long the system has been up</help>
+ </properties>
+ <command>uptime</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-table.xml b/op-mode-definitions/show-table.xml
new file mode 100644
index 000000000..b093a5de7
--- /dev/null
+++ b/op-mode-definitions/show-table.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <leafNode name="table">
+ <properties>
+ <help>Show routing tables</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show zebra router table summary"</command>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-users.xml b/op-mode-definitions/show-users.xml
new file mode 100644
index 000000000..a026e47e7
--- /dev/null
+++ b/op-mode-definitions/show-users.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="users">
+ <properties>
+ <help>Show user information</help>
+ </properties>
+ <command>who -H</command>
+ <children>
+ <node name="recent">
+ <properties>
+ <help>Show 10 recently logged in users</help>
+ </properties>
+ <command>last -aF -n 10 | sed -e 's/^wtmp begins/Displaying logins since/'</command>
+ </node>
+ <tagNode name="recent">
+ <properties>
+ <help>Show specified number of recently logged in users</help>
+ <completionHelp>
+ <list>NUMBER</list>
+ </completionHelp>
+ </properties>
+ <command>last -aF -n $4 | sed -e 's/^wtmp begins/Displaying logins since/'</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-version.xml b/op-mode-definitions/show-version.xml
new file mode 100644
index 000000000..aae5bb008
--- /dev/null
+++ b/op-mode-definitions/show-version.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="version">
+ <properties>
+ <help>Show system version information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_version.py</command>
+ <children>
+ <leafNode name="funny">
+ <properties>
+ <help>Show system version and some fun stuff</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_version.py --funny</command>
+ </leafNode>
+ <leafNode name="all">
+ <properties>
+ <help>Show system version and versions of all packages</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_version.py --all</command>
+ </leafNode>
+ <leafNode name="quagga">
+ <properties>
+ <help>Show Quagga version information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show version"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-vpn.xml b/op-mode-definitions/show-vpn.xml
new file mode 100644
index 000000000..0e7fc38e9
--- /dev/null
+++ b/op-mode-definitions/show-vpn.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="vpn">
+ <properties>
+ <help>Show active remote access Virtual Private Network (VPN) sessions</help>
+ </properties>
+ <children>
+ <leafNode name="remote-access">
+ <properties>
+ <help>Show active VPN server sessions</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_vpn_ra.py</command>
+ </leafNode>
+ </children>
+ </node>
+ </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..438e7c334
--- /dev/null
+++ b/op-mode-definitions/show-vrf.xml
@@ -0,0 +1,30 @@
+<?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>
+ <children>
+ <leafNode name="processes">
+ <properties>
+ <help>Shows all process ids associated with VRF</help>
+ </properties>
+ <command>/usr/sbin/ip vrf pids "$3"</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/snmp.xml b/op-mode-definitions/snmp.xml
new file mode 100644
index 000000000..a0a47da40
--- /dev/null
+++ b/op-mode-definitions/snmp.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="snmp">
+ <properties>
+ <help>Show status of SNMP on localhost</help>
+ </properties>
+ <children>
+ <tagNode name="community">
+ <properties>
+ <help>Show status of SNMP community</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/snmp.py --allowed</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp.py --community="$4"</command>
+ <children>
+ <tagNode name="host">
+ <properties>
+ <help>Show status of SNMP on remote host</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp.py --community="$4" --host "$6"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ <node name="mib">
+ <properties>
+ <help>Show SNMP MIB information</help>
+ </properties>
+ <children>
+ <node name="ifmib">
+ <properties>
+ <help>Show all SNMP interfaces MIB information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_ifmib.py</command>
+ <children>
+ <tagNode name="ifAlias">
+ <properties>
+ <help>Show SNMP ifAlias for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_ifmib.py --ifalias="$6"</command>
+ </tagNode>
+ <tagNode name="ifDescr">
+ <properties>
+ <help>Show SNMP ifDescr for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_ifmib.py --ifdescr="$6"</command>
+ </tagNode>
+ <tagNode name="ifIndex">
+ <properties>
+ <help>Show SNMP ifDescr for specified interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_ifmib.py --ifindex="$6"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="v3">
+ <properties>
+ <help>Show SNMP v3 status on localhost</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3.py --all</command>
+ <children>
+ <leafNode name="certificates">
+ <properties>
+ <help>Show TSM certificates</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3_showcerts.sh</command>
+ </leafNode>
+ <leafNode name="group">
+ <properties>
+ <help>Show the list of configured groups</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3.py --group</command>
+ </leafNode>
+ <leafNode name="trap-target">
+ <properties>
+ <help>Show the list of configured targets</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3.py --trap</command>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Show the list of configured users</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3.py --user</command>
+ </leafNode>
+ <leafNode name="view">
+ <properties>
+ <help>Show the list of configured views</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/snmp_v3.py --view</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/sstp-server.xml b/op-mode-definitions/sstp-server.xml
new file mode 100644
index 000000000..03dfc4262
--- /dev/null
+++ b/op-mode-definitions/sstp-server.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="sstp-server">
+ <properties>
+ <help>Show SSTP server information</help>
+ </properties>
+ <children>
+ <leafNode name="sessions">
+ <properties>
+ <help>Show active SSTP server sessions</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="sstp" --action="show sessions"</command>
+ </leafNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>Show SSTP server statistics</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="sstp" --action="show stat"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/telnet.xml b/op-mode-definitions/telnet.xml
new file mode 100644
index 000000000..c5bb6d283
--- /dev/null
+++ b/op-mode-definitions/telnet.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="telnet">
+ <properties>
+ <help>Telnet to a node</help>
+ </properties>
+ <children>
+ <tagNode name="to">
+ <properties>
+ <help>Telnet to a host</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>/usr/bin/telnet $3</command>
+ <children>
+ <tagNode name="port">
+ <properties>
+ <help>Telnet to a host:port</help>
+ <completionHelp>
+ <list>&lt;0-65535&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/telnet $3 $5</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/terminal.xml b/op-mode-definitions/terminal.xml
new file mode 100644
index 000000000..9c4e629cb
--- /dev/null
+++ b/op-mode-definitions/terminal.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="clear">
+ <properties>
+ <help>Clear system information</help>
+ </properties>
+ <children>
+ <node name="console">
+ <properties>
+ <help>Clear screen</help>
+ </properties>
+ <command>/usr/bin/clear</command>
+ </node>
+ </children>
+ </node>
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="terminal">
+ <properties>
+ <help>Reset terminal</help>
+ </properties>
+ <command>/usr/bin/reset</command>
+ </node>
+ </children>
+ </node>
+ <node name="set">
+ <properties>
+ <help>Set operational options</help>
+ </properties>
+ <children>
+ <tagNode name="builtin">
+ <properties>
+ <help>Bash builtin set command</help>
+ <completionHelp>
+ <list>&lt;OPTION&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>builtin $3</command>
+ </tagNode>
+
+ <node name="console">
+ <properties>
+ <help>Control console behaviors</help>
+ </properties>
+ <children>
+ <leafNode name="keymap">
+ <properties>
+ <help>Reconfigure console keyboard layout</help>
+ </properties>
+ <command>sudo dpkg-reconfigure -f dialog keyboard-configuration &amp;&amp; sudo systemctl restart keyboard-setup</command>
+ </leafNode>
+ </children>
+ </node>
+
+ <node name="terminal">
+ <properties>
+ <help>Control terminal behaviors</help>
+ </properties>
+ <children>
+
+ <node name="key">
+ <properties>
+ <help>Set key behaviors</help>
+ </properties>
+ <children>
+ <tagNode name="query-help">
+ <properties>
+ <help>Enable/disable getting help using question mark (default enabled)</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/toggle_help_binding.sh $5</command>
+ </tagNode>
+ </children>
+ </node>
+
+ <node name="pager">
+ <properties>
+ <help>Set terminal pager to default (less)</help>
+ </properties>
+ <command>VYATTA_PAGER=${_vyatta_default_pager}</command>
+ </node>
+ <tagNode name="pager">
+ <properties>
+ <help>Set terminal pager</help>
+ <completionHelp>
+ <list>&lt;PROGRAM&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>VYATTA_PAGER=$4</command>
+ </tagNode>
+
+ <tagNode name="length">
+ <properties>
+ <help>Set terminal to given number of rows (0 disables paging)</help>
+ <completionHelp>
+ <list>&lt;NUMBER&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>if [ "$4" -eq 0 ]; then VYATTA_PAGER=cat; else VYATTA_PAGER=${_vyatta_default_pager}; stty rows $4; fi</command>
+ </tagNode>
+
+ <tagNode name="width">
+ <properties>
+ <help>Set terminal to given number of columns</help>
+ <completionHelp>
+ <list>&lt;NUMBER&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>stty columns $4</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+
+
+</interfaceDefinition>
diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml
new file mode 100644
index 000000000..1b619ed43
--- /dev/null
+++ b/op-mode-definitions/traceroute.xml
@@ -0,0 +1,227 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <tagNode name="traceroute">
+ <properties>
+ <help>Track network path to 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>/usr/bin/traceroute "$2"</command>
+ </tagNode>
+ <node name="traceroute">
+ <properties>
+ <help>Track network path to 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>
+ <children>
+ <tagNode name="ipv4">
+ <properties>
+ <help>Explicitly use IPv4 when tracing the path</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/traceroute -4 "$3"</command>
+ <children>
+ <node name="tcp">
+ <properties>
+ <help>Route tracing and port detection using TCP</help>
+ </properties>
+ <command>sudo /usr/bin/tcptraceroute "$3" </command>
+ <children>
+ <tagNode name="port">
+ <properties>
+ <help>TCP port to connect to for path tracing</help>
+ <completionHelp>
+ <list>0-65535</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /usr/bin/tcptraceroute "$3" $6</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <tagNode name="ipv6">
+ <properties>
+ <help>Explicitly use IPv6 when tracing the path</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/traceroute -6 "$3"</command>
+ <children>
+ <node name="tcp">
+ <properties>
+ <help>Use TCP/IPv6 packets to perform a traceroute</help>
+ </properties>
+ <command>sudo /usr/bin/tcptraceroute6 "$3" </command>
+ <children>
+ <tagNode name="port">
+ <properties>
+ <help>TCP port to connect to for path tracing</help>
+ <completionHelp>
+ <list>0-65535</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /usr/bin/tcptraceroute6 "$3" $6</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <tagNode name="vrf">
+ <properties>
+ <help>Track network path to specified node via given VRF</help>
+ <completionHelp>
+ <path>vrf name</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <!-- we need an empty tagNode to pass in a plain fqdn/ip address and
+ let traceroute decide how to handle this parameter -->
+ <tagNode name="">
+ <properties>
+ <help>Track network path to specified node via given VRF</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 /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute "$4"</command>
+ </tagNode>
+ <tagNode name="ipv4">
+ <properties>
+ <help>Explicitly use IPv4 when tracing the path via given VRF</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -4 "$5"</command>
+ <children>
+ <node name="tcp">
+ <properties>
+ <help>Route tracing and port detection using TCP</help>
+ </properties>
+ <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute "$5" </command>
+ <children>
+ <tagNode name="port">
+ <properties>
+ <help>TCP port to connect to for path tracing</help>
+ <completionHelp>
+ <list>0-65535</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute "$5" $8</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <tagNode name="ipv6">
+ <properties>
+ <help>Explicitly use IPv6 when tracing the path via given VRF</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -6 "$5"</command>
+ <children>
+ <node name="tcp">
+ <properties>
+ <help>Use TCP/IPv6 packets to perform a traceroute</help>
+ </properties>
+ <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute6 "$5" </command>
+ <children>
+ <tagNode name="port">
+ <properties>
+ <help>TCP port to connect to for path tracing</help>
+ <completionHelp>
+ <list>0-65535</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute6 "$5" $8</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="monitor">
+ <children>
+ <tagNode name="traceroute">
+ <properties>
+ <help>Monitor path to destination in realtime</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>/usr/bin/mtr "$3"</command>
+ </tagNode>
+ <node name="traceroute">
+ <children>
+ <tagNode name="ipv4">
+ <properties>
+ <help>IPv4 fully qualified domain name (FQDN)</help>
+ <completionHelp>
+ <list>&lt;fqdn&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/mtr -4 "$4"</command>
+ </tagNode>
+ <tagNode name="ipv6">
+ <properties>
+ <help>IPv6 fully qualified domain name (FQDN)</help>
+ <completionHelp>
+ <list>&lt;fqdn&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/mtr -6 "$4"</command>
+ </tagNode>
+ <tagNode name="vrf">
+ <properties>
+ <help>Monitor path to destination in realtime via given VRF</help>
+ <completionHelp>
+ <path>vrf name</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="ipv4">
+ <properties>
+ <help>IPv4 fully qualified domain name (FQDN)</help>
+ <completionHelp>
+ <list>&lt;fqdn&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /usr/sbin/ip vrf exec "$4" /usr/bin/mtr -4 "$6"</command>
+ </tagNode>
+ <tagNode name="ipv6">
+ <properties>
+ <help>IPv6 fully qualified domain name (FQDN)</help>
+ <completionHelp>
+ <list>&lt;fqdn&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /usr/sbin/ip vrf exec "$4" /usr/bin/mtr -6 "$6"</command>
+ </tagNode>
+ <tagNode name="">
+ <properties>
+ <help>Track network path to specified node via given VRF</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 /usr/sbin/ip vrf exec "$4" /usr/bin/mtr "$5"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/traffic-dump.xml b/op-mode-definitions/traffic-dump.xml
new file mode 100644
index 000000000..6d86f7423
--- /dev/null
+++ b/op-mode-definitions/traffic-dump.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="monitor">
+ <children>
+ <node name="traffic">
+ <properties>
+ <help>Monitor traffic dumps</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <command>sudo tcpdump -i $4</command>
+ <properties>
+ <help>Monitor traffic dump from an interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_dumpable_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="filter">
+ <command>sudo tcpdump -n -i $4 "${@:6}"</command>
+ <properties>
+ <help>Monitor traffic matching filter conditions</help>
+ </properties>
+ </tagNode>
+ <tagNode name="save">
+ <command>sudo tcpdump -n -i $4 -w $6</command>
+ <properties>
+ <help>Save traffic dump from an interface to a file</help>
+ </properties>
+ <children>
+ <tagNode name="filter">
+ <command>sudo tcpdump -n -i $4 -w $6 "${@:8}"</command>
+ <properties>
+ <help>Save a dump of traffic matching filter conditions to a file</help>
+ </properties>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/vrrp.xml b/op-mode-definitions/vrrp.xml
new file mode 100644
index 000000000..856fb440d
--- /dev/null
+++ b/op-mode-definitions/vrrp.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="vrrp">
+ <properties>
+ <help>Show VRRP (Virtual Router Redundancy Protocol) information</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vrrp.py --summary</command>
+ <children>
+ <node name="statistics">
+ <properties>
+ <help>Show VRRP statistics</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vrrp.py --statistics</command>
+ </node>
+ <node name="detail">
+ <properties>
+ <help>Show detailed VRRP state information</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/vrrp.py --data</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="restart">
+ <children>
+ <node name="vrrp">
+ <properties>
+ <help>Restart the VRRP (Virtual Router Redundancy Protocol) process</help>
+ </properties>
+ <command>sudo systemctl restart keepalived.service</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/wake-on-lan.xml b/op-mode-definitions/wake-on-lan.xml
new file mode 100644
index 000000000..1a9b88596
--- /dev/null
+++ b/op-mode-definitions/wake-on-lan.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="wake-on-lan">
+ <properties>
+ <help>Send Wake-On-LAN (WOL) Magic Packet</help>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Interface where the station is connected</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="host">
+ <properties>
+ <help>Station (MAC) address to wake up</help>
+ </properties>
+ <command>sudo /usr/sbin/etherwake -i "$3" "$5"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml
new file mode 100644
index 000000000..a7bfa36a3
--- /dev/null
+++ b/op-mode-definitions/wireguard.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0"?>
+<!-- wireguard key management -->
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="wireguard">
+ <properties>
+ <help>wireguard key generation utility</help>
+ </properties>
+ <children>
+ <leafNode name="default-keypair">
+ <properties>
+ <help>generates the wireguard default-keypair</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey</command>
+ </leafNode>
+ <leafNode name="preshared-key">
+ <properties>
+ <help>generate a wireguard preshared key</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/wireguard.py --genpsk</command>
+ </leafNode>
+ <tagNode name="named-keypairs">
+ <properties>
+ <help>Generates named wireguard keypairs</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey --location "$4"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="show">
+ <children>
+ <node name="wireguard">
+ <properties>
+ <help>Show wireguard properties</help>
+ </properties>
+ <children>
+ <node name="keypairs">
+ <properties>
+ <help>Shows named wireguard keys</help>
+ </properties>
+ <children>
+ <tagNode name="pubkey">
+ <properties>
+ <help>Show wireguard private named key</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/wireguard.py --showpub --location "$5"</command>
+ </tagNode>
+ <tagNode name="privkey">
+ <properties>
+ <help>Show wireguard public named key</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/wireguard.py --showpriv --location "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="interfaces">
+ <children>
+ <tagNode name="wireguard">
+ <properties>
+ <help>show wireguard interface information</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard.py --showinterface "$4"</command>
+ <children>
+ <leafNode name="allowed-ips">
+ <properties>
+ <help>show all allowed-ips for the specified interface</help>
+ </properties>
+ <command>sudo wg show "$4" allowed-ips</command>
+ </leafNode>
+ <leafNode name="endpoints">
+ <properties>
+ <help>show all endpoints for the specified interface</help>
+ </properties>
+ <command>sudo wg show "$4" endpoints</command>
+ </leafNode>
+ <leafNode name="peers">
+ <properties>
+ <help>show all peer IDs for the specified interface</help>
+ </properties>
+ <command>sudo wg show "$4" peers</command>
+ </leafNode>
+ <!-- more commands upon request -->
+ </children>
+ </tagNode>
+ <node name="wireguard">
+ <properties>
+ <help>Show wireguard interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed wireguard interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="delete">
+ <children>
+ <node name="wireguard">
+ <properties>
+ <help>Delete wireguard properties</help>
+ </properties>
+ <children>
+ <tagNode name="keypair">
+ <properties>
+ <help>Delete a wireguard keypair</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard.py --delkdir --location "$4"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
+
diff --git a/op-mode-definitions/wireless.xml b/op-mode-definitions/wireless.xml
new file mode 100644
index 000000000..a3a9d1f55
--- /dev/null
+++ b/op-mode-definitions/wireless.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="clear">
+ <children>
+ <node name="interfaces">
+ <children>
+ <node name="wireless">
+ <properties>
+ <help>Clear wireless interface information</help>
+ </properties>
+ <children>
+ <leafNode name="counters">
+ <properties>
+ <help>Clear all wireless interface counters</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_interfaces.py --action=clear --intf-type="$3"</command>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="wireless">
+ <properties>
+ <help>Clear interface information for a given wireless interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --type wireless</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="counters">
+ <properties>
+ <help>Clear all wireless interface counters</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_interfaces.py --action=clear --intf="$4"</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <node name="wireless">
+ <properties>
+ <help>Show wireless interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireless --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed wireless interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireless --action=show</command>
+ </leafNode>
+ <leafNode name="info">
+ <properties>
+ <help>Show wireless interface configuration</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_wireless.py --brief</command>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="wireless">
+ <properties>
+ <help>Show specified wireless interface information</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --type wireless</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified wireless interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ <node name="scan">
+ <properties>
+ <help>Show summary of the specified wireless interface information</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_wireless.py --scan "$4"</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed scan results</help>
+ </properties>
+ <command>sudo /sbin/iw dev "$4" scan ap-force</command>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="stations">
+ <properties>
+ <help>Show specified wireless interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_wireless.py --stations "$4"</command>
+ </leafNode>
+ <tagNode name="vif">
+ <properties>
+ <help>Show specified virtual network interface (vif) information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of specified virtual network interface (vif) information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/python/setup.py b/python/setup.py
new file mode 100644
index 000000000..e2d28bd6b
--- /dev/null
+++ b/python/setup.py
@@ -0,0 +1,27 @@
+import os
+from setuptools import setup
+
+def packages(directory):
+ return [
+ _[0].replace('/','.')
+ for _ in os.walk(directory)
+ if os.path.isfile(os.path.join(_[0], '__init__.py'))
+ ]
+
+setup(
+ name = "vyos",
+ version = "1.3.0",
+ author = "VyOS maintainers and contributors",
+ author_email = "maintainers@vyos.net",
+ description = ("VyOS configuration libraries."),
+ license = "LGPLv2+",
+ keywords = "vyos",
+ url = "http://www.vyos.io",
+ packages = packages('vyos'),
+ long_description="VyOS configuration libraries",
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Topic :: Utilities",
+ "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)",
+ ],
+)
diff --git a/python/vyos/__init__.py b/python/vyos/__init__.py
new file mode 100644
index 000000000..e3e14fdd8
--- /dev/null
+++ b/python/vyos/__init__.py
@@ -0,0 +1 @@
+from .base import ConfigError
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
new file mode 100644
index 000000000..510ab7f46
--- /dev/null
+++ b/python/vyos/airbag.py
@@ -0,0 +1,181 @@
+# 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 sys
+from datetime import datetime
+
+from vyos import debug
+from vyos.logger import syslog
+from vyos.version import get_version
+from vyos.version import get_full_version_data
+
+
+def enable(log=True):
+ if log:
+ _intercepting_logger()
+ _intercepting_exceptions()
+
+
+_noteworthy = []
+
+
+def noteworthy(msg):
+ """
+ noteworthy can be use to take note things which we may not want to
+ report to the user may but be worth including in bug report
+ if something goes wrong later on
+ """
+ _noteworthy.append(msg)
+
+
+# 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)
+ 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 = get_full_version_data()
+ trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n')
+ note = ''
+ if _noteworthy:
+ note = 'noteworthy:\n'
+ note += '\n'.join(_noteworthy)
+
+ information.update({
+ 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'trace': trace,
+ 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED,
+ 'note': note,
+ })
+
+ 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)
+ if debug.enabled('developer'):
+ import pdb
+ pdb.pm()
+
+
+def _intercepting_logger(_singleton=[False]):
+ skip = _singleton.pop()
+ _singleton.append(True)
+ if skip:
+ return
+
+ # log to syslog any message sent to stderr
+ sys.stderr = _IO(sys.stderr, syslog.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 _intercepting_exceptions(_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 = _intercepter
+
+
+# Messages to print
+# if the key before the value has not time, syslog takes that as the source of the message
+
+FAULT = """\
+Report Time: {date}
+Image Version: VyOS {version}
+Release Train: {release_train}
+
+Built by: {built_by}
+Built on: {built_on}
+Build UUID: {build_uuid}
+Build Commit ID: {build_git}
+
+Architecture: {system_arch}
+Boot via: {boot_via}
+System type: {system_type}
+
+Hardware vendor: {hardware_vendor}
+Hardware model: {hardware_model}
+Hardware S/N: {hardware_serial}
+Hardware UUID: {hardware_uuid}
+
+{trace}
+{note}
+"""
+
+INTRO = """\
+VyOS had an issue completing a command.
+
+We are sorry that you encountered a problem while using VyOS.
+There are a few things you can do to help us (and yourself):
+{instructions}
+
+When reporting problems, please include as much information as possible:
+- do not obfuscate any data (feel free to contact us privately if your
+ business policy requires it)
+- 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 using the online help desk
+ https://support.vyos.io/
+- Join our community on slack where our users exchange help and advice
+ https://vyos.slack.com
+""".strip()
diff --git a/python/vyos/authutils.py b/python/vyos/authutils.py
new file mode 100644
index 000000000..66b5f4a74
--- /dev/null
+++ b/python/vyos/authutils.py
@@ -0,0 +1,41 @@
+# authutils -- miscelanneous functions for handling passwords and publis keys
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import re
+
+from vyos.util import cmd
+
+
+def make_password_hash(password):
+ """ Makes a password hash for /etc/shadow using mkpasswd """
+
+ mkpassword = 'mkpasswd --method=sha-512 --stdin'
+ return cmd(mkpassword, input=password, timeout=5)
+
+def split_ssh_public_key(key_string, defaultname=""):
+ """ Splits an SSH public key into its components """
+
+ key_string = key_string.strip()
+ parts = re.split(r'\s+', key_string)
+
+ if len(parts) == 3:
+ key_type, key_data, key_name = parts[0], parts[1], parts[2]
+ else:
+ key_type, key_data, key_name = parts[0], parts[1], defaultname
+
+ if key_type not in ['ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519']:
+ raise ValueError("Bad key type \'{0}\', must be one of must be one of ssh-rsa, ssh-dss, ecdsa-sha2-nistp<256|384|521> or ssh-ed25519".format(key_type))
+
+ return({"type": key_type, "data": key_data, "name": key_name})
diff --git a/python/vyos/base.py b/python/vyos/base.py
new file mode 100644
index 000000000..4e23714e5
--- /dev/null
+++ b/python/vyos/base.py
@@ -0,0 +1,18 @@
+# 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/>.
+
+
+class ConfigError(Exception):
+ pass
diff --git a/python/vyos/certbot_util.py b/python/vyos/certbot_util.py
new file mode 100644
index 000000000..df42d4780
--- /dev/null
+++ b/python/vyos/certbot_util.py
@@ -0,0 +1,58 @@
+# certbot_util -- adaptation of certbot_nginx name matching functions for VyOS
+# https://github.com/certbot/certbot/blob/master/LICENSE.txt
+
+from certbot_nginx import parser
+
+NAME_RANK = 0
+START_WILDCARD_RANK = 1
+END_WILDCARD_RANK = 2
+REGEX_RANK = 3
+
+def _rank_matches_by_name(server_block_list, target_name):
+ """Returns a ranked list of server_blocks that match target_name.
+ Adapted from the function of the same name in
+ certbot_nginx.NginxConfigurator
+ """
+ matches = []
+ for server_block in server_block_list:
+ name_type, name = parser.get_best_match(target_name,
+ server_block['name'])
+ if name_type == 'exact':
+ matches.append({'vhost': server_block,
+ 'name': name,
+ 'rank': NAME_RANK})
+ elif name_type == 'wildcard_start':
+ matches.append({'vhost': server_block,
+ 'name': name,
+ 'rank': START_WILDCARD_RANK})
+ elif name_type == 'wildcard_end':
+ matches.append({'vhost': server_block,
+ 'name': name,
+ 'rank': END_WILDCARD_RANK})
+ elif name_type == 'regex':
+ matches.append({'vhost': server_block,
+ 'name': name,
+ 'rank': REGEX_RANK})
+
+ return sorted(matches, key=lambda x: x['rank'])
+
+def _select_best_name_match(matches):
+ """Returns the best name match of a ranked list of server_blocks.
+ Adapted from the function of the same name in
+ certbot_nginx.NginxConfigurator
+ """
+ if not matches:
+ return None
+ elif matches[0]['rank'] in [START_WILDCARD_RANK, END_WILDCARD_RANK]:
+ rank = matches[0]['rank']
+ wildcards = [x for x in matches if x['rank'] == rank]
+ return max(wildcards, key=lambda x: len(x['name']))['vhost']
+ else:
+ return matches[0]['vhost']
+
+def choose_server_block(server_block_list, target_name):
+ matches = _rank_matches_by_name(server_block_list, target_name)
+ server_blocks = [x for x in [_select_best_name_match(matches)]
+ if x is not None]
+ return server_blocks
+
diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py
new file mode 100644
index 000000000..90b458aae
--- /dev/null
+++ b/python/vyos/component_versions.py
@@ -0,0 +1,57 @@
+# Copyright 2017 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/>.
+
+"""
+The version data looks like:
+
+/* Warning: Do not remove the following line. */
+/* === vyatta-config-version:
+"cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@1:dhcp-server@4:firewall@5:ipsec@4:nat@4:qos@1:quagga@2:system@8:vrrp@1:wanloadbalance@3:webgui@1:webproxy@1:zone-policy@1"
+=== */
+/* Release version: 1.2.0-rolling+201806131737 */
+"""
+
+import re
+
+def get_component_version(string_line):
+ """
+ Get component version dictionary from string
+ return empty dictionary if string contains no config information
+ or raise error if component version string malformed
+ """
+ return_value = {}
+ if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line):
+
+ if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line):
+ raise ValueError("malformed configuration string: " + str(string_line))
+
+ for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
+ if pair[0] in return_value.keys():
+ raise ValueError("duplicate unit name: \"" + str(pair[0]) + "\" in string: \"" + string_line + "\"")
+ return_value[pair[0]] = int(pair[1])
+
+ return return_value
+
+
+def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/config.boot'):
+ """
+ Get component version dictionary parsing config file line by line
+ """
+ f = open(config_file_name, 'r')
+ for line_in_config in f:
+ component_version = get_component_version(line_in_config)
+ if component_version:
+ return component_version
+ raise ValueError("no config string in file:", config_file_name)
diff --git a/python/vyos/config.py b/python/vyos/config.py
new file mode 100644
index 000000000..884d6d947
--- /dev/null
+++ b/python/vyos/config.py
@@ -0,0 +1,454 @@
+# Copyright 2017, 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/>.
+
+"""
+A library for reading VyOS running config data.
+
+This library is used internally by all config scripts of VyOS,
+but its API should be considered stable and safe to use
+in user scripts.
+
+Note that this module will not work outside VyOS.
+
+Node taxonomy
+#############
+
+There are multiple types of config tree nodes in VyOS, each requires
+its own set of operations.
+
+*Leaf nodes* (such as "address" in interfaces) can have values, but cannot
+have children.
+Leaf nodes can have one value, multiple values, or no values at all.
+
+For example, "system host-name" is a single-value leaf node,
+"system name-server" is a multi-value leaf node (commonly abbreviated "multi node"),
+and "system ip disable-forwarding" is a valueless leaf node.
+
+Non-leaf nodes cannot have values, but they can have child nodes. They are divided into
+two classes depending on whether the names of their children are fixed or not.
+For example, under "system", the names of all valid child nodes are predefined
+("login", "name-server" etc.).
+
+To the contrary, children of the "system task-scheduler task" node can have arbitrary names.
+Such nodes are called *tag nodes*. This terminology is confusing but we keep using it for lack
+of a better word. No one remembers if the "tag" in "task Foo" is "task" or "Foo",
+but the distinction is irrelevant in practice.
+
+Configuration modes
+###################
+
+VyOS has two distinct modes: operational mode and configuration mode. When a user logins,
+the CLI is in the operational mode. In this mode, only the running (effective) config is accessible for reading.
+
+When a user enters the "configure" command, a configuration session is setup. Every config session
+has its *proposed* (or *session*) config built on top of the current running config. When changes are commited, if commit succeeds,
+the proposed config is merged into the running config.
+
+In configuration mode, "base" functions like `exists`, `return_value` return values from the session config,
+while functions prefixed "effective" return values from the running config.
+
+In operational mode, all functions return values from the running config.
+
+"""
+
+import re
+import json
+from copy import deepcopy
+
+import vyos.util
+import vyos.configtree
+from vyos.configsource import ConfigSource, ConfigSourceSession
+
+class Config(object):
+ """
+ The class of config access objects.
+
+ Internally, in the current implementation, this object is *almost* stateless,
+ the only state it keeps is relative *config path* for convenient access to config
+ subtrees.
+ """
+ def __init__(self, session_env=None, config_source=None):
+ if config_source is None:
+ self._config_source = ConfigSourceSession(session_env)
+ else:
+ if not isinstance(config_source, ConfigSource):
+ raise TypeError("config_source not of type ConfigSource")
+ self._config_source = config_source
+
+ self._level = []
+ self._dict_cache = {}
+ (self._running_config,
+ self._session_config) = self._config_source.get_configtree_tuple()
+
+ def _make_path(self, path):
+ # Backwards-compatibility stuff: original implementation used string paths
+ # libvyosconfig paths are lists, but since node names cannot contain whitespace,
+ # splitting at whitespace is reasonably safe.
+ # It may cause problems with exists() when it's used for checking values,
+ # since values may contain whitespace.
+ if isinstance(path, str):
+ path = re.split(r'\s+', path)
+ elif isinstance(path, list):
+ pass
+ else:
+ raise TypeError("Path must be a whitespace-separated string or a list")
+ return (self._level + path)
+
+ def set_level(self, path):
+ """
+ Set the *edit level*, that is, a relative config tree path.
+ Once set, all operations will be relative to this path,
+ for example, after ``set_level("system")``, calling
+ ``exists("name-server")`` is equivalent to calling
+ ``exists("system name-server"`` without ``set_level``.
+
+ Args:
+ path (str|list): relative config path
+ """
+ # Make sure there's always a space between default path (level)
+ # and path supplied as method argument
+ # XXX: for small strings in-place concatenation is not a problem
+ if isinstance(path, str):
+ if path:
+ self._level = re.split(r'\s+', path)
+ else:
+ self._level = []
+ elif isinstance(path, list):
+ self._level = path.copy()
+ else:
+ raise TypeError("Level path must be either a whitespace-separated string or a list")
+
+ def get_level(self):
+ """
+ Gets the current edit level.
+
+ Returns:
+ str: current edit level
+ """
+ return(self._level.copy())
+
+ def exists(self, path):
+ """
+ Checks if a node with given path exists in the running or proposed config
+
+ Returns:
+ True if node exists, False otherwise
+
+ Note:
+ This function cannot be used outside a configuration sessions.
+ In operational mode scripts, use ``exists_effective``.
+ """
+ if not self._session_config:
+ return False
+ if self._session_config.exists(self._make_path(path)):
+ return True
+ else:
+ # libvyosconfig exists() works only for _nodes_, not _values_
+ # libvyattacfg one also worked for values, so we emulate that case here
+ if isinstance(path, str):
+ path = re.split(r'\s+', path)
+ path_without_value = path[:-1]
+ path_str = " ".join(path_without_value)
+ try:
+ value = self._session_config.return_value(self._make_path(path_str))
+ return (value == path[-1])
+ except vyos.configtree.ConfigTreeError:
+ # node doesn't exist at all
+ return False
+
+ def session_changed(self):
+ """
+ Returns:
+ True if the config session has uncommited changes, False otherwise.
+ """
+ return self._config_source.session_changed()
+
+ def in_session(self):
+ """
+ Returns:
+ True if called from a configuration session, False otherwise.
+ """
+ return self._config_source.in_session()
+
+ def show_config(self, path=[], default=None, effective=False):
+ """
+ Args:
+ path (str list): Configuration tree path, or empty
+ default (str): Default value to return
+
+ Returns:
+ str: working configuration
+ """
+ return self._config_source.show_config(path, default, effective)
+
+ def get_cached_dict(self, effective=False):
+ cached = self._dict_cache.get(effective, {})
+ if cached:
+ config_dict = cached
+ else:
+ config_dict = {}
+
+ if effective:
+ if self._running_config:
+ config_dict = json.loads((self._running_config).to_json())
+ else:
+ if self._session_config:
+ config_dict = json.loads((self._session_config).to_json())
+
+ self._dict_cache[effective] = config_dict
+
+ return config_dict
+
+ def get_config_dict(self, path=[], effective=False, key_mangling=None, get_first_key=False):
+ """
+ Args:
+ path (str list): Configuration tree path, can be empty
+ effective=False: effective or session config
+ key_mangling=None: mangle dict keys according to regex and replacement
+ get_first_key=False: if k = path[:-1], return sub-dict d[k] instead of {k: d[k]}
+
+ Returns: a dict representation of the config under path
+ """
+ config_dict = self.get_cached_dict(effective)
+
+ config_dict = vyos.util.get_sub_dict(config_dict, self._make_path(path), get_first_key)
+
+ if key_mangling:
+ if not (isinstance(key_mangling, tuple) and \
+ (len(key_mangling) == 2) and \
+ isinstance(key_mangling[0], str) and \
+ isinstance(key_mangling[1], str)):
+ raise ValueError("key_mangling must be a tuple of two strings")
+ else:
+ config_dict = vyos.util.mangle_dict_keys(config_dict, key_mangling[0], key_mangling[1])
+ else:
+ config_dict = deepcopy(config_dict)
+
+ return config_dict
+
+ def is_multi(self, path):
+ """
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if a node can have multiple values, False otherwise.
+
+ Note:
+ It also returns False if node doesn't exist.
+ """
+ self._config_source.set_level(self.get_level)
+ return self._config_source.is_multi(path)
+
+ def is_tag(self, path):
+ """
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if a node is a tag node, False otherwise.
+
+ Note:
+ It also returns False if node doesn't exist.
+ """
+ self._config_source.set_level(self.get_level)
+ return self._config_source.is_tag(path)
+
+ def is_leaf(self, path):
+ """
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if a node is a leaf node, False otherwise.
+
+ Note:
+ It also returns False if node doesn't exist.
+ """
+ self._config_source.set_level(self.get_level)
+ return self._config_source.is_leaf(path)
+
+ def return_value(self, path, default=None):
+ """
+ Retrieve a value of single-value leaf node in the running or proposed config
+
+ Args:
+ path (str): Configuration tree path
+ default (str): Default value to return if node does not exist
+
+ Returns:
+ str: Node value, if it has any
+ None: if node is valueless *or* if it doesn't exist
+
+ Note:
+ Due to the issue with treatment of valueless nodes by this function,
+ valueless nodes should be checked with ``exists`` instead.
+
+ This function cannot be used outside a configuration session.
+ In operational mode scripts, use ``return_effective_value``.
+ """
+ if self._session_config:
+ try:
+ value = self._session_config.return_value(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ value = None
+ else:
+ value = None
+
+ if not value:
+ return(default)
+ else:
+ return(value)
+
+ def return_values(self, path, default=[]):
+ """
+ Retrieve all values of a multi-value leaf node in the running or proposed config
+
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ str list: Node values, if it has any
+ []: if node does not exist
+
+ Note:
+ This function cannot be used outside a configuration session.
+ In operational mode scripts, use ``return_effective_values``.
+ """
+ if self._session_config:
+ try:
+ values = self._session_config.return_values(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ values = []
+ else:
+ values = []
+
+ if not values:
+ return(default.copy())
+ else:
+ return(values)
+
+ def list_nodes(self, path, default=[]):
+ """
+ Retrieve names of all children of a tag node in the running or proposed config
+
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ string list: child node names
+
+ """
+ if self._session_config:
+ try:
+ nodes = self._session_config.list_nodes(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ nodes = []
+ else:
+ nodes = []
+
+ if not nodes:
+ return(default.copy())
+ else:
+ return(nodes)
+
+ def exists_effective(self, path):
+ """
+ Check if a node exists in the running (effective) config
+
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if node exists in the running config, False otherwise
+
+ Note:
+ This function is safe to use in operational mode. In configuration mode,
+ it ignores uncommited changes.
+ """
+ if self._running_config:
+ return(self._running_config.exists(self._make_path(path)))
+
+ return False
+
+ def return_effective_value(self, path, default=None):
+ """
+ Retrieve a values of a single-value leaf node in a running (effective) config
+
+ Args:
+ path (str): Configuration tree path
+ default (str): Default value to return if node does not exist
+
+ Returns:
+ str: Node value
+ """
+ if self._running_config:
+ try:
+ value = self._running_config.return_value(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ value = None
+ else:
+ value = None
+
+ if not value:
+ return(default)
+ else:
+ return(value)
+
+ def return_effective_values(self, path, default=[]):
+ """
+ Retrieve all values of a multi-value node in a running (effective) config
+
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ str list: A list of values
+ """
+ if self._running_config:
+ try:
+ values = self._running_config.return_values(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ values = []
+ else:
+ values = []
+
+ if not values:
+ return(default.copy())
+ else:
+ return(values)
+
+ def list_effective_nodes(self, path, default=[]):
+ """
+ Retrieve names of all children of a tag node in the running config
+
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ str list: child node names
+ """
+ if self._running_config:
+ try:
+ nodes = self._running_config.list_nodes(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ nodes = []
+ else:
+ nodes = []
+
+ if not nodes:
+ return(default.copy())
+ else:
+ return(nodes)
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
new file mode 100644
index 000000000..bd8624ced
--- /dev/null
+++ b/python/vyos/configdict.py
@@ -0,0 +1,314 @@
+# 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/>.
+
+"""
+A library for retrieving value dicts from VyOS configs in a declarative fashion.
+"""
+import os
+
+from enum import Enum
+from copy import deepcopy
+
+from vyos import ConfigError
+
+def retrieve_config(path_hash, base_path, config):
+ """
+ Retrieves a VyOS config as a dict according to a declarative description
+
+ The description dict, passed in the first argument, must follow this format:
+ ``field_name : <path, type, [inner_options_dict]>``.
+
+ Supported types are: ``str`` (for normal nodes),
+ ``list`` (returns a list of strings, for multi nodes),
+ ``bool`` (returns True if valueless node exists),
+ ``dict`` (for tag nodes, returns a dict indexed by node names,
+ according to description in the third item of the tuple).
+
+ Args:
+ path_hash (dict): Declarative description of the config to retrieve
+ base_path (list): A base path to prepend to all option paths
+ config (vyos.config.Config): A VyOS config object
+
+ Returns:
+ dict: config dict
+ """
+ config_hash = {}
+
+ for k in path_hash:
+
+ if type(path_hash[k]) != tuple:
+ raise ValueError("In field {0}: expected a tuple, got a value {1}".format(k, str(path_hash[k])))
+ if len(path_hash[k]) < 2:
+ raise ValueError("In field {0}: field description must be a tuple of at least two items, path (list) and type".format(k))
+
+ path = path_hash[k][0]
+ if type(path) != list:
+ raise ValueError("In field {0}: path must be a list, not a {1}".format(k, type(path)))
+
+ typ = path_hash[k][1]
+ if type(typ) != type:
+ raise ValueError("In field {0}: type must be a type, not a {1}".format(k, type(typ)))
+
+ path = base_path + path
+
+ path_str = " ".join(path)
+
+ if typ == str:
+ config_hash[k] = config.return_value(path_str)
+ elif typ == list:
+ config_hash[k] = config.return_values(path_str)
+ elif typ == bool:
+ config_hash[k] = config.exists(path_str)
+ elif typ == dict:
+ try:
+ inner_hash = path_hash[k][2]
+ except IndexError:
+ raise ValueError("The type of the \'{0}\' field is dict, but inner options hash is missing from the tuple".format(k))
+ config_hash[k] = {}
+ nodes = config.list_nodes(path_str)
+ for node in nodes:
+ config_hash[k][node] = retrieve_config(inner_hash, path + [node], config)
+
+ return config_hash
+
+
+def dict_merge(source, destination):
+ """ Merge two dictionaries. Only keys which are not present in destination
+ will be copied from source, anything else will be kept untouched. Function
+ will return a new dict which has the merged key/value pairs. """
+ from copy import deepcopy
+ tmp = deepcopy(destination)
+
+ for key, value in source.items():
+ if key not in tmp:
+ tmp[key] = value
+ elif isinstance(source[key], dict):
+ tmp[key] = dict_merge(source[key], tmp[key])
+
+ return tmp
+
+def list_diff(first, second):
+ """ Diff two dictionaries and return only unique items """
+ second = set(second)
+ return [item for item in first if item not in second]
+
+def T2665_default_dict_cleanup(dict):
+ """ Cleanup default keys for tag nodes https://phabricator.vyos.net/T2665. """
+ # Cleanup
+ for vif in ['vif', 'vif_s']:
+ if vif in dict:
+ for key in ['ip', 'mtu', 'dhcpv6_options']:
+ if key in dict[vif]:
+ del dict[vif][key]
+
+ # cleanup VIF-S defaults
+ if 'vif_c' in dict[vif]:
+ for key in ['ip', 'mtu', 'dhcpv6_options']:
+ if key in dict[vif]['vif_c']:
+ del dict[vif]['vif_c'][key]
+ # If there is no vif-c defined and we just cleaned the default
+ # keys - we can clean the entire vif-c dict as it's useless
+ if not dict[vif]['vif_c']:
+ del dict[vif]['vif_c']
+
+ # If there is no real vif/vif-s defined and we just cleaned the default
+ # keys - we can clean the entire vif dict as it's useless
+ if not dict[vif]:
+ del dict[vif]
+
+ if 'dhcpv6_options' in dict and 'pd' in dict['dhcpv6_options']:
+ if 'length' in dict['dhcpv6_options']['pd']:
+ del dict['dhcpv6_options']['pd']['length']
+
+ # delete empty dicts
+ if 'dhcpv6_options' in dict:
+ if 'pd' in dict['dhcpv6_options']:
+ # test if 'pd' is an empty node so we can remove it
+ if not dict['dhcpv6_options']['pd']:
+ del dict['dhcpv6_options']['pd']
+
+ # test if 'dhcpv6_options' is an empty node so we can remove it
+ if not dict['dhcpv6_options']:
+ del dict['dhcpv6_options']
+
+ return dict
+
+def leaf_node_changed(conf, path):
+ """
+ Check if a leaf node was altered. If it has been altered - values has been
+ changed, or it was added/removed, we will return the old value. If nothing
+ has been changed, None is returned
+ """
+ from vyos.configdiff import get_config_diff
+ D = get_config_diff(conf, key_mangling=('-', '_'))
+ D.set_level(conf.get_level())
+ (new, old) = D.get_value_diff(path)
+ if new != old:
+ if isinstance(old, str):
+ return old
+ elif isinstance(old, list):
+ if isinstance(new, str):
+ new = [new]
+ elif isinstance(new, type(None)):
+ new = []
+ return list_diff(old, new)
+
+ return None
+
+def node_changed(conf, path):
+ """
+ Check if a leaf node was altered. If it has been altered - values has been
+ changed, or it was added/removed, we will return the old value. If nothing
+ has been changed, None is returned
+ """
+ from vyos.configdiff import get_config_diff, Diff
+ D = get_config_diff(conf, key_mangling=('-', '_'))
+ D.set_level(conf.get_level())
+ # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
+ keys = D.get_child_nodes_diff(path, expand_nodes=Diff.DELETE)['delete'].keys()
+ return list(keys)
+
+def get_removed_vlans(conf, dict):
+ """
+ Common function to parse a dictionary retrieved via get_config_dict() and
+ determine any added/removed VLAN interfaces - be it 802.1q or Q-in-Q.
+ """
+ from vyos.configdiff import get_config_diff, Diff
+
+ # Check vif, vif-s/vif-c VLAN interfaces for removal
+ D = get_config_diff(conf, key_mangling=('-', '_'))
+ D.set_level(conf.get_level())
+ # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
+ keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys()
+ if keys:
+ dict.update({'vif_remove': [*keys]})
+
+ # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
+ keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys()
+ if keys:
+ dict.update({'vif_s_remove': [*keys]})
+
+ for vif in dict.get('vif_s', {}).keys():
+ keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys()
+ if keys:
+ dict.update({'vif_s': { vif : {'vif_c_remove': [*keys]}}})
+
+ return dict
+
+
+def dict_add_dhcpv6pd_defaults(defaults, config_dict):
+ # Implant default dictionary for DHCPv6-PD instances
+ if 'dhcpv6_options' in config_dict and 'pd' in config_dict['dhcpv6_options']:
+ for pd, pd_config in config_dict['dhcpv6_options']['pd'].items():
+ config_dict['dhcpv6_options']['pd'][pd] = dict_merge(
+ defaults, pd_config)
+
+ return config_dict
+
+def get_interface_dict(config, base, ifname=''):
+ """
+ Common utility function to retrieve and mandgle the interfaces available
+ in CLI configuration. All interfaces have a common base ground where the
+ value retrival is identical - so it can and should be reused
+
+ Will return a dictionary with the necessary interface configuration
+ """
+ from vyos.util import vyos_dict_search
+ from vyos.validate import is_member
+ from vyos.xml import defaults
+
+ if not ifname:
+ # determine tagNode instance
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # retrieve interface default values
+ default_values = defaults(base)
+
+ # setup config level which is extracted in get_removed_vlans()
+ config.set_level(base + [ifname])
+ dict = config.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+
+ # Check if interface has been removed
+ if dict == {}:
+ dict.update({'deleted' : ''})
+
+ # Add interface instance name into dictionary
+ dict.update({'ifname': ifname})
+
+ # We have gathered the dict representation of the CLI, but there are
+ # default options which we need to update into the dictionary
+ # retrived.
+ dict = dict_merge(default_values, dict)
+
+ # Check if we are a member of a bridge device
+ bridge = is_member(config, ifname, 'bridge')
+ if bridge:
+ dict.update({'is_bridge_member' : bridge})
+
+ # Check if we are a member of a bond device
+ bond = is_member(config, ifname, 'bonding')
+ if bond:
+ dict.update({'is_bond_member' : bond})
+
+ mac = leaf_node_changed(config, ['mac'])
+ if mac:
+ dict.update({'mac_old' : mac})
+
+ eui64 = leaf_node_changed(config, ['ipv6', 'address', 'eui64'])
+ if eui64:
+ # XXX: T2636 workaround: convert string to a list with one element
+ if isinstance(eui64, str):
+ eui64 = [eui64]
+ tmp = vyos_dict_search('ipv6.address', dict)
+ if not tmp:
+ dict.update({'ipv6': {'address': {'eui64_old': eui64}}})
+ else:
+ dict['ipv6']['address'].update({'eui64_old': eui64})
+
+ # remove wrongly inserted values
+ dict = T2665_default_dict_cleanup(dict)
+
+ # Implant default dictionary for DHCPv6-PD instances
+ default_pd_values = defaults(base + ['dhcpv6-options', 'pd'])
+ dict = dict_add_dhcpv6pd_defaults(default_pd_values, dict)
+
+ # Implant default dictionary in vif/vif-s VLAN interfaces. Values are
+ # identical for all types of VLAN interfaces as they all include the same
+ # XML definitions which hold the defaults.
+ default_vif_values = defaults(base + ['vif'])
+ for vif, vif_config in dict.get('vif', {}).items():
+ dict['vif'][vif] = dict_add_dhcpv6pd_defaults(
+ default_pd_values, vif_config)
+ dict['vif'][vif] = T2665_default_dict_cleanup(
+ dict_merge(default_vif_values, vif_config))
+
+ for vif_s, vif_s_config in dict.get('vif_s', {}).items():
+ dict['vif_s'][vif_s] = dict_add_dhcpv6pd_defaults(
+ default_pd_values, vif_s_config)
+ dict['vif_s'][vif_s] = T2665_default_dict_cleanup(
+ dict_merge(default_vif_values, vif_s_config))
+ for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
+ dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_add_dhcpv6pd_defaults(
+ default_pd_values, vif_c_config)
+ dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_default_dict_cleanup(
+ dict_merge(default_vif_values, vif_c_config))
+
+ # Check vif, vif-s/vif-c VLAN interfaces for removal
+ dict = get_removed_vlans(config, dict)
+
+ return dict
+
diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py
new file mode 100644
index 000000000..b79893507
--- /dev/null
+++ b/python/vyos/configdiff.py
@@ -0,0 +1,249 @@
+# 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 enum import IntFlag, auto
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.util import get_sub_dict, mangle_dict_keys
+from vyos.xml import defaults
+
+class ConfigDiffError(Exception):
+ """
+ Raised on config dict access errors, for example, calling get_value on
+ a non-leaf node.
+ """
+ pass
+
+def enum_to_key(e):
+ return e.name.lower()
+
+class Diff(IntFlag):
+ MERGE = auto()
+ DELETE = auto()
+ ADD = auto()
+ STABLE = auto()
+
+requires_effective = [enum_to_key(Diff.DELETE)]
+target_defaults = [enum_to_key(Diff.MERGE)]
+
+def _key_sets_from_dicts(session_dict, effective_dict):
+ session_keys = list(session_dict)
+ effective_keys = list(effective_dict)
+
+ ret = {}
+ stable_keys = [k for k in session_keys if k in effective_keys]
+
+ ret[enum_to_key(Diff.MERGE)] = session_keys
+ ret[enum_to_key(Diff.DELETE)] = [k for k in effective_keys if k not in stable_keys]
+ ret[enum_to_key(Diff.ADD)] = [k for k in session_keys if k not in stable_keys]
+ ret[enum_to_key(Diff.STABLE)] = stable_keys
+
+ return ret
+
+def _dict_from_key_set(key_set, d):
+ # This will always be applied to a key_set obtained from a get_sub_dict,
+ # hence there is no possibility of KeyError, as get_sub_dict guarantees
+ # a return type of dict
+ ret = {k: d[k] for k in key_set}
+
+ return ret
+
+def get_config_diff(config, key_mangling=None):
+ """
+ Check type and return ConfigDiff instance.
+ """
+ if not config or not isinstance(config, Config):
+ raise TypeError("argument must me a Config instance")
+ if key_mangling and not (isinstance(key_mangling, tuple) and \
+ (len(key_mangling) == 2) and \
+ isinstance(key_mangling[0], str) and \
+ isinstance(key_mangling[1], str)):
+ raise ValueError("key_mangling must be a tuple of two strings")
+
+ return ConfigDiff(config, key_mangling)
+
+class ConfigDiff(object):
+ """
+ The class of config changes as represented by comparison between the
+ session config dict and the effective config dict.
+ """
+ def __init__(self, config, key_mangling=None):
+ self._level = config.get_level()
+ self._session_config_dict = config.get_cached_dict()
+ self._effective_config_dict = config.get_cached_dict(effective=True)
+ self._key_mangling = key_mangling
+
+ # mirrored from Config; allow path arguments relative to level
+ def _make_path(self, path):
+ if isinstance(path, str):
+ path = path.split()
+ elif isinstance(path, list):
+ pass
+ else:
+ raise TypeError("Path must be a whitespace-separated string or a list")
+
+ ret = self._level + path
+ return ret
+
+ def set_level(self, path):
+ """
+ Set the *edit level*, that is, a relative config dict path.
+ Once set, all operations will be relative to this path,
+ for example, after ``set_level("system")``, calling
+ ``get_value("name-server")`` is equivalent to calling
+ ``get_value("system name-server")`` without ``set_level``.
+
+ Args:
+ path (str|list): relative config path
+ """
+ if isinstance(path, str):
+ if path:
+ self._level = path.split()
+ else:
+ self._level = []
+ elif isinstance(path, list):
+ self._level = path.copy()
+ else:
+ raise TypeError("Level path must be either a whitespace-separated string or a list")
+
+ def get_level(self):
+ """
+ Gets the current edit level.
+
+ Returns:
+ str: current edit level
+ """
+ ret = self._level.copy()
+ return ret
+
+ def _mangle_dict_keys(self, config_dict):
+ config_dict = mangle_dict_keys(config_dict, self._key_mangling[0],
+ self._key_mangling[1])
+ return config_dict
+
+ def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False):
+ """
+ Args:
+ path (str|list): config path
+ expand_nodes=Diff(0): bit mask of enum indicating for which nodes
+ to provide full dict; for example, Diff.MERGE
+ will expand dict['merge'] into dict under
+ value
+ no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default
+ values to ret['merge']
+
+ Returns: dict of lists, representing differences between session
+ and effective config, under path
+ dict['merge'] = session config values
+ dict['delete'] = effective config values, not in session
+ dict['add'] = session config values, not in effective
+ dict['stable'] = config values in both session and effective
+ """
+ session_dict = get_sub_dict(self._session_config_dict,
+ self._make_path(path), get_first_key=True)
+ effective_dict = get_sub_dict(self._effective_config_dict,
+ self._make_path(path), get_first_key=True)
+
+ ret = _key_sets_from_dicts(session_dict, effective_dict)
+
+ if not expand_nodes:
+ return ret
+
+ for e in Diff:
+ if expand_nodes & e:
+ k = enum_to_key(e)
+ if k in requires_effective:
+ ret[k] = _dict_from_key_set(ret[k], effective_dict)
+ else:
+ ret[k] = _dict_from_key_set(ret[k], session_dict)
+
+ if self._key_mangling:
+ ret[k] = self._mangle_dict_keys(ret[k])
+
+ if k in target_defaults and not no_defaults:
+ default_values = defaults(self._make_path(path))
+ ret[k] = dict_merge(default_values, ret[k])
+
+ return ret
+
+ def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False):
+ """
+ Args:
+ path (str|list): config path
+ expand_nodes=Diff(0): bit mask of enum indicating for which nodes
+ to provide full dict; for example, Diff.MERGE
+ will expand dict['merge'] into dict under
+ value
+ no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default
+ values to ret['merge']
+
+ Returns: dict of lists, representing differences between session
+ and effective config, at path
+ dict['merge'] = session config values
+ dict['delete'] = effective config values, not in session
+ dict['add'] = session config values, not in effective
+ dict['stable'] = config values in both session and effective
+ """
+ session_dict = get_sub_dict(self._session_config_dict, self._make_path(path))
+ effective_dict = get_sub_dict(self._effective_config_dict, self._make_path(path))
+
+ ret = _key_sets_from_dicts(session_dict, effective_dict)
+
+ if not expand_nodes:
+ return ret
+
+ for e in Diff:
+ if expand_nodes & e:
+ k = enum_to_key(e)
+ if k in requires_effective:
+ ret[k] = _dict_from_key_set(ret[k], effective_dict)
+ else:
+ ret[k] = _dict_from_key_set(ret[k], session_dict)
+
+ if self._key_mangling:
+ ret[k] = self._mangle_dict_keys(ret[k])
+
+ if k in target_defaults and not no_defaults:
+ default_values = defaults(self._make_path(path))
+ ret[k] = dict_merge(default_values, ret[k])
+
+ return ret
+
+ def get_value_diff(self, path=[]):
+ """
+ Args:
+ path (str|list): config path
+
+ Returns: (new, old) tuple of values in session config/effective config
+ """
+ # one should properly use is_leaf as check; for the moment we will
+ # deduce from type, which will not catch call on non-leaf node if None
+ new_value_dict = get_sub_dict(self._session_config_dict, self._make_path(path))
+ old_value_dict = get_sub_dict(self._effective_config_dict, self._make_path(path))
+
+ new_value = None
+ old_value = None
+ if new_value_dict:
+ new_value = next(iter(new_value_dict.values()))
+ if old_value_dict:
+ old_value = next(iter(old_value_dict.values()))
+
+ if new_value and isinstance(new_value, dict):
+ raise ConfigDiffError("get_value_changed called on non-leaf node")
+ if old_value and isinstance(old_value, dict):
+ raise ConfigDiffError("get_value_changed called on non-leaf node")
+
+ return new_value, old_value
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
new file mode 100644
index 000000000..0994fd974
--- /dev/null
+++ b/python/vyos/configsession.py
@@ -0,0 +1,191 @@
+# configsession -- the write API for the VyOS running config
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import os
+import re
+import sys
+import subprocess
+
+CLI_SHELL_API = '/bin/cli-shell-api'
+SET = '/opt/vyatta/sbin/my_set'
+DELETE = '/opt/vyatta/sbin/my_delete'
+COMMENT = '/opt/vyatta/sbin/my_comment'
+COMMIT = '/opt/vyatta/sbin/my_commit'
+DISCARD = '/opt/vyatta/sbin/my_discard'
+SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig']
+LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile']
+SAVE_CONFIG = ['/opt/vyatta/sbin/vyatta-save-config.pl']
+INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url']
+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"
+
+# When started as a service rather than from a user shell,
+# the process lacks the VyOS-specific environment that comes
+# from bash configs, so we have to inject it
+# XXX: maybe it's better to do via a systemd environment file
+def inject_vyos_env(env):
+ env['VYATTA_CFG_GROUP_NAME'] = 'vyattacfg'
+ env['VYATTA_USER_LEVEL_DIR'] = '/opt/vyatta/etc/shell/level/admin'
+ env['VYATTA_PROCESS_CLIENT'] = 'gui2_rest'
+ env['VYOS_HEADLESS_CLIENT'] = 'vyos_http_api'
+ env['vyatta_bindir']= '/opt/vyatta/bin'
+ env['vyatta_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates'
+ env['vyatta_configdir'] = '/opt/vyatta/config'
+ env['vyatta_datadir'] = '/opt/vyatta/share'
+ env['vyatta_datarootdir'] = '/opt/vyatta/share'
+ env['vyatta_libdir'] = '/opt/vyatta/lib'
+ env['vyatta_libexecdir'] = '/opt/vyatta/libexec'
+ env['vyatta_op_templates'] = '/opt/vyatta/share/vyatta-op/templates'
+ env['vyatta_prefix'] = '/opt/vyatta'
+ env['vyatta_sbindir'] = '/opt/vyatta/sbin'
+ env['vyatta_sysconfdir'] = '/opt/vyatta/etc'
+ env['vyos_bin_dir'] = '/usr/bin'
+ env['vyos_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates'
+ env['vyos_completion_dir'] = '/usr/libexec/vyos/completion'
+ env['vyos_configdir'] = '/opt/vyatta/config'
+ env['vyos_conf_scripts_dir'] = '/usr/libexec/vyos/conf_mode'
+ env['vyos_datadir'] = '/opt/vyatta/share'
+ env['vyos_datarootdir']= '/opt/vyatta/share'
+ env['vyos_libdir'] = '/opt/vyatta/lib'
+ env['vyos_libexec_dir'] = '/usr/libexec/vyos'
+ env['vyos_op_scripts_dir'] = '/usr/libexec/vyos/op_mode'
+ env['vyos_op_templates'] = '/opt/vyatta/share/vyatta-op/templates'
+ env['vyos_prefix'] = '/opt/vyatta'
+ env['vyos_sbin_dir'] = '/usr/sbin'
+ env['vyos_validators_dir'] = '/usr/libexec/vyos/validators'
+
+ return env
+
+
+class ConfigSessionError(Exception):
+ pass
+
+
+class ConfigSession(object):
+ """
+ The write API of VyOS.
+ """
+ def __init__(self, session_id, app=APP):
+ """
+ Creates a new config session.
+
+ Args:
+ session_id (str): Session identifier
+ app (str): Application name, purely informational
+
+ Note:
+ The session identifier MUST be globally unique within the system.
+ The best practice is to only have one ConfigSession object per process
+ and used the PID for the session identifier.
+ """
+
+ env_str = subprocess.check_output([CLI_SHELL_API, 'getSessionEnv', str(session_id)])
+ self.__session_id = session_id
+
+ # Extract actual variables from the chunk of shell it outputs
+ # XXX: it's better to extend cli-shell-api to provide easily readable output
+ env_list = re.findall(r'([A-Z_]+)=([^;\s]+)', env_str.decode())
+
+ session_env = os.environ
+ session_env = inject_vyos_env(session_env)
+ for k, v in env_list:
+ session_env[k] = v
+
+ self.__session_env = session_env
+ self.__session_env["COMMIT_VIA"] = app
+
+ self.__run_command([CLI_SHELL_API, 'setupSession'])
+
+ def __del__(self):
+ try:
+ output = subprocess.check_output([CLI_SHELL_API, 'teardownSession'], env=self.__session_env).decode().strip()
+ if output:
+ print("cli-shell-api teardownSession output for sesion {0}: {1}".format(self.__session_id, output), file=sys.stderr)
+ except Exception as e:
+ print("Could not tear down session {0}: {1}".format(self.__session_id, e), file=sys.stderr)
+
+ def __run_command(self, cmd_list):
+ p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.__session_env)
+ result = p.wait()
+ output = p.stdout.read().decode()
+ p.communicate()
+ if result != 0:
+ raise ConfigSessionError(output)
+ return output
+
+ def get_session_env(self):
+ return self.__session_env
+
+ def set(self, path, value=None):
+ if not value:
+ value = []
+ else:
+ value = [value]
+ self.__run_command([SET] + path + value)
+
+ def delete(self, path, value=None):
+ if not value:
+ value = []
+ else:
+ value = [value]
+ self.__run_command([DELETE] + path + value)
+
+ def comment(self, path, value=None):
+ if not value:
+ value = [""]
+ else:
+ value = [value]
+ self.__run_command([COMMENT] + path + value)
+
+ def commit(self):
+ out = self.__run_command([COMMIT])
+ return out
+
+ def discard(self):
+ self.__run_command([DISCARD])
+
+ def show_config(self, path, format='raw'):
+ config_data = self.__run_command(SHOW_CONFIG + path)
+
+ if format == 'raw':
+ return config_data
+
+ def load_config(self, file_path):
+ out = self.__run_command(LOAD_CONFIG + [file_path])
+ return out
+
+ def save_config(self, file_path):
+ out = self.__run_command(SAVE_CONFIG + [file_path])
+ return out
+
+ def install_image(self, url):
+ out = self.__run_command(INSTALL_IMAGE + [url])
+ return out
+
+ def remove_image(self, name):
+ out = self.__run_command(REMOVE_IMAGE + [name])
+ return out
+
+ def generate(self, path):
+ out = self.__run_command(GENERATE + path)
+ return out
+
+ def show(self, path):
+ out = self.__run_command(SHOW + path)
+ return out
+
diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py
new file mode 100644
index 000000000..50222e385
--- /dev/null
+++ b/python/vyos/configsource.py
@@ -0,0 +1,318 @@
+
+# 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 re
+import subprocess
+
+from vyos.configtree import ConfigTree
+
+class VyOSError(Exception):
+ """
+ Raised on config access errors.
+ """
+ pass
+
+class ConfigSourceError(Exception):
+ '''
+ Raised on error in ConfigSource subclass init.
+ '''
+ pass
+
+class ConfigSource:
+ def __init__(self):
+ self._running_config: ConfigTree = None
+ self._session_config: ConfigTree = None
+
+ def get_configtree_tuple(self):
+ return self._running_config, self._session_config
+
+ def session_changed(self):
+ """
+ Returns:
+ True if the config session has uncommited changes, False otherwise.
+ """
+ raise NotImplementedError(f"function not available for {type(self)}")
+
+ def in_session(self):
+ """
+ Returns:
+ True if called from a configuration session, False otherwise.
+ """
+ raise NotImplementedError(f"function not available for {type(self)}")
+
+ def show_config(self, path=[], default=None, effective=False):
+ """
+ Args:
+ path (str|list): Configuration tree path, or empty
+ default (str): Default value to return
+
+ Returns:
+ str: working configuration
+ """
+ raise NotImplementedError(f"function not available for {type(self)}")
+
+ def is_multi(self, path):
+ """
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if a node can have multiple values, False otherwise.
+
+ Note:
+ It also returns False if node doesn't exist.
+ """
+ raise NotImplementedError(f"function not available for {type(self)}")
+
+ def is_tag(self, path):
+ """
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if a node is a tag node, False otherwise.
+
+ Note:
+ It also returns False if node doesn't exist.
+ """
+ raise NotImplementedError(f"function not available for {type(self)}")
+
+ def is_leaf(self, path):
+ """
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if a node is a leaf node, False otherwise.
+
+ Note:
+ It also returns False if node doesn't exist.
+ """
+ raise NotImplementedError(f"function not available for {type(self)}")
+
+class ConfigSourceSession(ConfigSource):
+ def __init__(self, session_env=None):
+ super().__init__()
+ self._cli_shell_api = "/bin/cli-shell-api"
+ self._level = []
+ if session_env:
+ self.__session_env = session_env
+ else:
+ self.__session_env = None
+
+ # Running config can be obtained either from op or conf mode, it always succeeds
+ # once the config system is initialized during boot;
+ # before initialization, set to empty string
+ if os.path.isfile('/tmp/vyos-config-status'):
+ try:
+ running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig'])
+ except VyOSError:
+ running_config_text = ''
+ else:
+ running_config_text = ''
+
+ # Session config ("active") only exists in conf mode.
+ # In op mode, we'll just use the same running config for both active and session configs.
+ if self.in_session():
+ try:
+ session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig'])
+ except VyOSError:
+ session_config_text = ''
+ else:
+ session_config_text = running_config_text
+
+ if running_config_text:
+ self._running_config = ConfigTree(running_config_text)
+ else:
+ self._running_config = None
+
+ if session_config_text:
+ self._session_config = ConfigTree(session_config_text)
+ else:
+ self._session_config = None
+
+ def _make_command(self, op, path):
+ args = path.split()
+ cmd = [self._cli_shell_api, op] + args
+ return cmd
+
+ def _run(self, cmd):
+ if self.__session_env:
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env)
+ else:
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ out = p.stdout.read()
+ p.wait()
+ p.communicate()
+ if p.returncode != 0:
+ raise VyOSError()
+ else:
+ return out.decode('ascii')
+
+ def set_level(self, path):
+ """
+ Set the *edit level*, that is, a relative config tree path.
+ Once set, all operations will be relative to this path,
+ for example, after ``set_level("system")``, calling
+ ``exists("name-server")`` is equivalent to calling
+ ``exists("system name-server"`` without ``set_level``.
+
+ Args:
+ path (str|list): relative config path
+ """
+ # Make sure there's always a space between default path (level)
+ # and path supplied as method argument
+ # XXX: for small strings in-place concatenation is not a problem
+ if isinstance(path, str):
+ if path:
+ self._level = re.split(r'\s+', path)
+ else:
+ self._level = []
+ elif isinstance(path, list):
+ self._level = path.copy()
+ else:
+ raise TypeError("Level path must be either a whitespace-separated string or a list")
+
+ def session_changed(self):
+ """
+ Returns:
+ True if the config session has uncommited changes, False otherwise.
+ """
+ try:
+ self._run(self._make_command('sessionChanged', ''))
+ return True
+ except VyOSError:
+ return False
+
+ def in_session(self):
+ """
+ Returns:
+ True if called from a configuration session, False otherwise.
+ """
+ try:
+ self._run(self._make_command('inSession', ''))
+ return True
+ except VyOSError:
+ return False
+
+ def show_config(self, path=[], default=None, effective=False):
+ """
+ Args:
+ path (str|list): Configuration tree path, or empty
+ default (str): Default value to return
+
+ Returns:
+ 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,
+ # so we need to supress diff production using appropriate
+ # options for getting either running (active)
+ # or proposed (working) config.
+ if effective:
+ path = ['--show-active-only'] + path
+ else:
+ path = ['--show-working-only'] + path
+
+ if isinstance(path, list):
+ 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 is_multi(self, path):
+ """
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if a node can have multiple values, False otherwise.
+
+ Note:
+ It also returns False if node doesn't exist.
+ """
+ try:
+ path = " ".join(self._level) + " " + path
+ self._run(self._make_command('isMulti', path))
+ return True
+ except VyOSError:
+ return False
+
+ def is_tag(self, path):
+ """
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if a node is a tag node, False otherwise.
+
+ Note:
+ It also returns False if node doesn't exist.
+ """
+ try:
+ path = " ".join(self._level) + " " + path
+ self._run(self._make_command('isTag', path))
+ return True
+ except VyOSError:
+ return False
+
+ def is_leaf(self, path):
+ """
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if a node is a leaf node, False otherwise.
+
+ Note:
+ It also returns False if node doesn't exist.
+ """
+ try:
+ path = " ".join(self._level) + " " + path
+ self._run(self._make_command('isLeaf', path))
+ return True
+ except VyOSError:
+ return False
+
+class ConfigSourceString(ConfigSource):
+ def __init__(self, running_config_text=None, session_config_text=None):
+ super().__init__()
+
+ try:
+ self._running_config = ConfigTree(running_config_text) if running_config_text else None
+ self._session_config = ConfigTree(session_config_text) if session_config_text else None
+ except ValueError:
+ raise ConfigSourceError(f"Init error in {type(self)}")
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
new file mode 100644
index 000000000..d8ffaca99
--- /dev/null
+++ b/python/vyos/configtree.py
@@ -0,0 +1,283 @@
+# configtree -- a standalone VyOS config file manipulation library (Python bindings)
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import re
+import json
+
+from ctypes import cdll, c_char_p, c_void_p, c_int
+
+
+def escape_backslash(string: str) -> str:
+ """Escape single backslashes in string that are not in escape sequence"""
+ p = re.compile(r'(?<!\\)[\\](?!b|f|n|r|t|\\[^bfnrt])')
+ result = p.sub(r'\\\\', string)
+ return result
+
+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
+ if not isinstance(path, list):
+ raise TypeError("Expected a list, got a {}".format(type(path)))
+ else:
+ pass
+
+
+class ConfigTreeError(Exception):
+ pass
+
+
+class ConfigTree(object):
+ def __init__(self, config_string, libpath='/usr/lib/libvyosconfig.so.0'):
+ self.__config = None
+ self.__lib = cdll.LoadLibrary(libpath)
+
+ # Import functions
+ self.__from_string = self.__lib.from_string
+ self.__from_string.argtypes = [c_char_p]
+ self.__from_string.restype = c_void_p
+
+ self.__get_error = self.__lib.get_error
+ self.__get_error.argtypes = []
+ self.__get_error.restype = c_char_p
+
+ self.__to_string = self.__lib.to_string
+ self.__to_string.argtypes = [c_void_p]
+ self.__to_string.restype = c_char_p
+
+ self.__to_commands = self.__lib.to_commands
+ self.__to_commands.argtypes = [c_void_p]
+ self.__to_commands.restype = c_char_p
+
+ self.__to_json = self.__lib.to_json
+ self.__to_json.argtypes = [c_void_p]
+ self.__to_json.restype = c_char_p
+
+ self.__to_json_ast = self.__lib.to_json_ast
+ self.__to_json_ast.argtypes = [c_void_p]
+ self.__to_json_ast.restype = c_char_p
+
+ self.__set_add_value = self.__lib.set_add_value
+ self.__set_add_value.argtypes = [c_void_p, c_char_p, c_char_p]
+ self.__set_add_value.restype = c_int
+
+ self.__delete_value = self.__lib.delete_value
+ self.__delete_value.argtypes = [c_void_p, c_char_p, c_char_p]
+ self.__delete_value.restype = c_int
+
+ self.__delete = self.__lib.delete_node
+ self.__delete.argtypes = [c_void_p, c_char_p]
+ self.__delete.restype = c_int
+
+ self.__rename = self.__lib.rename_node
+ self.__rename.argtypes = [c_void_p, c_char_p, c_char_p]
+ self.__rename.restype = c_int
+
+ self.__copy = self.__lib.copy_node
+ self.__copy.argtypes = [c_void_p, c_char_p, c_char_p]
+ self.__copy.restype = c_int
+
+ self.__set_replace_value = self.__lib.set_replace_value
+ self.__set_replace_value.argtypes = [c_void_p, c_char_p, c_char_p]
+ self.__set_replace_value.restype = c_int
+
+ self.__set_valueless = self.__lib.set_valueless
+ self.__set_valueless.argtypes = [c_void_p, c_char_p]
+ self.__set_valueless.restype = c_int
+
+ self.__exists = self.__lib.exists
+ self.__exists.argtypes = [c_void_p, c_char_p]
+ self.__exists.restype = c_int
+
+ self.__list_nodes = self.__lib.list_nodes
+ self.__list_nodes.argtypes = [c_void_p, c_char_p]
+ self.__list_nodes.restype = c_char_p
+
+ self.__return_value = self.__lib.return_value
+ self.__return_value.argtypes = [c_void_p, c_char_p]
+ self.__return_value.restype = c_char_p
+
+ self.__return_values = self.__lib.return_values
+ self.__return_values.argtypes = [c_void_p, c_char_p]
+ self.__return_values.restype = c_char_p
+
+ self.__is_tag = self.__lib.is_tag
+ self.__is_tag.argtypes = [c_void_p, c_char_p]
+ self.__is_tag.restype = c_int
+
+ self.__set_tag = self.__lib.set_tag
+ self.__set_tag.argtypes = [c_void_p, c_char_p]
+ self.__set_tag.restype = c_int
+
+ self.__destroy = self.__lib.destroy
+ self.__destroy.argtypes = [c_void_p]
+
+ 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:
+ msg = self.__get_error().decode()
+ raise ValueError("Failed to parse config: {0}".format(msg))
+ else:
+ self.__config = config
+ self.__version = version_section
+
+ def __del__(self):
+ if self.__config is not None:
+ self.__destroy(self.__config)
+
+ def __str__(self):
+ return self.to_string()
+
+ def to_string(self):
+ config_string = self.__to_string(self.__config).decode()
+ config_string = "{0}\n{1}".format(config_string, self.__version)
+ return config_string
+
+ def to_commands(self):
+ return self.__to_commands(self.__config).decode()
+
+ def to_json(self):
+ return self.__to_json(self.__config).decode()
+
+ def to_json_ast(self):
+ return self.__to_json_ast(self.__config).decode()
+
+ def set(self, path, value=None, replace=True):
+ """Set new entry in VyOS configuration.
+ path: configuration path e.g. 'system dns forwarding listen-address'
+ value: value to be added to node, e.g. '172.18.254.201'
+ replace: True: current occurance will be replaced
+ False: new value will be appended to current occurances - use
+ this for adding values to a multi node
+ """
+
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ if value is None:
+ self.__set_valueless(self.__config, path_str)
+ else:
+ if replace:
+ self.__set_replace_value(self.__config, path_str, str(value).encode())
+ else:
+ self.__set_add_value(self.__config, path_str, str(value).encode())
+
+ def delete(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ self.__delete(self.__config, path_str)
+
+ def delete_value(self, path, value):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ self.__delete_value(self.__config, path_str, value.encode())
+
+ def rename(self, path, new_name):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+ newname_str = new_name.encode()
+
+ # Check if a node with intended new name already exists
+ new_path = path[:-1] + [new_name]
+ if self.exists(new_path):
+ raise ConfigTreeError()
+ res = self.__rename(self.__config, path_str, newname_str)
+ if (res != 0):
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path))
+
+ def copy(self, old_path, new_path):
+ check_path(old_path)
+ check_path(new_path)
+ oldpath_str = " ".join(map(str, old_path)).encode()
+ newpath_str = " ".join(map(str, new_path)).encode()
+
+ # Check if a node with intended new name already exists
+ if self.exists(new_path):
+ raise ConfigTreeError()
+ res = self.__copy(self.__config, oldpath_str, newpath_str)
+ if (res != 0):
+ raise ConfigTreeError("Path [{}] doesn't exist".format(old_path))
+
+ def exists(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res = self.__exists(self.__config, path_str)
+ if (res == 0):
+ return False
+ else:
+ return True
+
+ def list_nodes(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res_json = self.__list_nodes(self.__config, path_str).decode()
+ res = json.loads(res_json)
+
+ if res is None:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ else:
+ return res
+
+ def return_value(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res_json = self.__return_value(self.__config, path_str).decode()
+ res = json.loads(res_json)
+
+ if res is None:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ else:
+ return res
+
+ def return_values(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res_json = self.__return_values(self.__config, path_str).decode()
+ res = json.loads(res_json)
+
+ if res is None:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ else:
+ return res
+
+ def is_tag(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res = self.__is_tag(self.__config, path_str)
+ if (res >= 1):
+ return True
+ else:
+ return False
+
+ def set_tag(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res = self.__set_tag(self.__config, path_str)
+ if (res == 0):
+ return True
+ else:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
new file mode 100644
index 000000000..7e1930878
--- /dev/null
+++ b/python/vyos/configverify.py
@@ -0,0 +1,139 @@
+# 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/>.
+
+# The sole purpose of this module is to hold common functions used in
+# all kinds of implementations to verify the CLI configuration.
+# It is started by migrating the interfaces to the new get_config_dict()
+# approach which will lead to a lot of code that can be reused.
+
+# NOTE: imports should be as local as possible to the function which
+# makes use of it!
+
+from vyos import ConfigError
+
+def verify_vrf(config):
+ """
+ Common helper function used by interface implementations to perform
+ recurring validation of VRF configuration.
+ """
+ from netifaces import interfaces
+ if 'vrf' in config:
+ if config['vrf'] not in interfaces():
+ raise ConfigError('VRF "{vrf}" does not exist'.format(**config))
+
+ if 'is_bridge_member' in config:
+ raise ConfigError(
+ 'Interface "{ifname}" cannot be both a member of VRF "{vrf}" '
+ 'and bridge "{is_bridge_member}"!'.format(**config))
+
+
+def verify_address(config):
+ """
+ Common helper function used by interface implementations to perform
+ recurring validation of IP address assignment when interface is part
+ of a bridge or bond.
+ """
+ if {'is_bridge_member', 'address'} <= set(config):
+ raise ConfigError(
+ 'Cannot assign address to interface "{ifname}" as it is a '
+ 'member of bridge "{is_bridge_member}"!'.format(**config))
+
+
+def verify_bridge_delete(config):
+ """
+ Common helper function used by interface implementations to
+ perform recurring validation of IP address assignmenr
+ when interface also is part of a bridge.
+ """
+ if 'is_bridge_member' in config:
+ raise ConfigError(
+ 'Interface "{ifname}" cannot be deleted as it is a '
+ 'member of bridge "{is_bridge_member}"!'.format(**config))
+
+def verify_interface_exists(config):
+ """
+ Common helper function used by interface implementations to perform
+ recurring validation if an interface actually exists.
+ """
+ from netifaces import interfaces
+ if not config['ifname'] in interfaces():
+ raise ConfigError('Interface "{ifname}" does not exist!'
+ .format(**config))
+
+def verify_source_interface(config):
+ """
+ Common helper function used by interface implementations to
+ perform recurring validation of the existence of a source-interface
+ required by e.g. peth/MACvlan, MACsec ...
+ """
+ from netifaces import interfaces
+ if 'source_interface' not in config:
+ raise ConfigError('Physical source-interface required for '
+ 'interface "{ifname}"'.format(**config))
+ if config['source_interface'] not in interfaces():
+ raise ConfigError('Source interface {source_interface} does not '
+ 'exist'.format(**config))
+
+def verify_dhcpv6(config):
+ """
+ Common helper function used by interface implementations to perform
+ recurring validation of DHCPv6 options which are mutually exclusive.
+ """
+ if 'dhcpv6_options' in config:
+ from vyos.util import vyos_dict_search
+
+ if {'parameters_only', 'temporary'} <= set(config['dhcpv6_options']):
+ raise ConfigError('DHCPv6 temporary and parameters-only options '
+ 'are mutually exclusive!')
+
+ # It is not allowed to have duplicate SLA-IDs as those identify an
+ # assigned IPv6 subnet from a delegated prefix
+ for pd in vyos_dict_search('dhcpv6_options.pd', config):
+ sla_ids = []
+ for interface in vyos_dict_search(f'dhcpv6_options.pd.{pd}.interface', config):
+ sla_id = vyos_dict_search(
+ f'dhcpv6_options.pd.{pd}.interface.{interface}.sla_id', config)
+ sla_ids.append(sla_id)
+
+ # Check for duplicates
+ duplicates = [x for n, x in enumerate(sla_ids) if x in sla_ids[:n]]
+ if duplicates:
+ raise ConfigError('Site-Level Aggregation Identifier (SLA-ID) '
+ 'must be unique per prefix-delegation!')
+
+def verify_vlan_config(config):
+ """
+ Common helper function used by interface implementations to perform
+ recurring validation of interface VLANs
+ """
+ # 802.1q VLANs
+ for vlan in config.get('vif', {}):
+ vlan = config['vif'][vlan]
+ verify_dhcpv6(vlan)
+ verify_address(vlan)
+ verify_vrf(vlan)
+
+ # 802.1ad (Q-in-Q) VLANs
+ for vlan in config.get('vif_s', {}):
+ vlan = config['vif_s'][vlan]
+ verify_dhcpv6(vlan)
+ verify_address(vlan)
+ verify_vrf(vlan)
+
+ for vlan in config.get('vif_s', {}).get('vif_c', {}):
+ vlan = config['vif_c'][vlan]
+ verify_dhcpv6(vlan)
+ verify_address(vlan)
+ verify_vrf(vlan)
diff --git a/python/vyos/debug.py b/python/vyos/debug.py
new file mode 100644
index 000000000..6ce42b173
--- /dev/null
+++ b/python/vyos/debug.py
@@ -0,0 +1,205 @@
+# 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 sys
+from datetime import datetime
+
+def message(message, flag='', destination=sys.stdout):
+ """
+ print a debug message line on stdout if debugging is enabled for the flag
+ also log it to a file if the flag 'log' is enabled
+
+ message: the message to print
+ flag: which flag must be set for it to print
+ destination: which file like object to write to (default: sys.stdout)
+
+ returns if any message was logged or not
+ """
+ enable = enabled(flag)
+ if enable:
+ destination.write(_format(flag,message))
+
+ # the log flag is special as it logs all the commands
+ # executed to a log
+ logfile = _logfile('log', '/tmp/developer-log')
+ if not logfile:
+ return enable
+
+ try:
+ # at boot the file is created as root:vyattacfg
+ # at runtime the file is created as user:vyattacfg
+ # but the helper scripts are not run as this so it
+ # need the default permission to be 666 (an not 660)
+ mask = os.umask(0o111)
+
+ with open(logfile, 'a') as f:
+ f.write(_timed(_format('log', message)))
+ finally:
+ os.umask(mask)
+
+ return enable
+
+
+def enabled(flag):
+ """
+ a flag can be set by touching the file in /tmp or /config
+
+ The current flags are:
+ - developer: the code will drop into PBD on un-handled exception
+ - log: the code will log all command to a file
+ - ifconfig: when modifying an interface,
+ prints command with result and sysfs access on stdout for interface
+ - command: print command run with result
+
+ Having the flag setup on the filesystem is required to have
+ debuging at boot time, however, setting the flag via environment
+ does not require a seek to the filesystem and is more efficient
+ it can be done on the shell on via .bashrc for the user
+
+ The function returns an empty string if the flag was not set otherwise
+ the function returns either the file or environment name used to set it up
+ """
+
+ # this is to force all new flags to be registered here to be
+ # documented both here and a reminder to update readthedocs :-)
+ if flag not in ['developer', 'log', 'ifconfig', 'command']:
+ return ''
+
+ return _fromenv(flag) or _fromfile(flag)
+
+
+def _timed(message):
+ now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ return f'{now} {message}'
+
+
+def _remove_invisible(string):
+ for char in ('\0', '\a', '\b', '\f', '\v'):
+ string = string.replace(char, '')
+ return string
+
+
+def _format(flag, message):
+ """
+ format a log message
+ """
+ message = _remove_invisible(message)
+ return f'DEBUG/{flag.upper():<7} {message}\n'
+
+
+def _fromenv(flag):
+ """
+ check if debugging is set for this flag via environment
+
+ For a given debug flag named "test"
+ The presence of the environment VYOS_TEST_DEBUG (uppercase) enables it
+
+ return empty string if not
+ return content of env value it is
+ """
+
+ flagname = f'VYOS_{flag.upper()}_DEBUG'
+ flagenv = os.environ.get(flagname, None)
+
+ if flagenv is None:
+ return ''
+ return flagenv
+
+
+def _fromfile(flag):
+ """
+ Check if debug exist for a given debug flag name
+
+ Check is a debug flag was set by the user. the flag can be set either:
+ - in /tmp for a non-persistent presence between reboot
+ - in /config for always on (an existence at boot time)
+
+ For a given debug flag named "test"
+ The presence of the file vyos.test.debug (all lowercase) enables it
+
+ The function returns an empty string if the flag was not set otherwise
+ the function returns the full flagname
+ """
+
+ for folder in ('/tmp', '/config'):
+ flagfile = f'{folder}/vyos.{flag}.debug'
+ if os.path.isfile(flagfile):
+ return flagfile
+
+ return ''
+
+
+def _contentenv(flag):
+ return os.environ.get(f'VYOS_{flag.upper()}_DEBUG', '').strip()
+
+
+def _contentfile(flag, default=''):
+ """
+ Check if debug exist for a given debug flag name
+
+ Check is a debug flag was set by the user. the flag can be set either:
+ - in /tmp for a non-persistent presence between reboot
+ - in /config for always on (an existence at boot time)
+
+ For a given debug flag named "test"
+ The presence of the file vyos.test.debug (all lowercase) enables it
+
+ The function returns an empty string if the flag was not set otherwise
+ the function returns the full flagname
+ """
+
+ for folder in ('/tmp', '/config'):
+ flagfile = f'{folder}/vyos.{flag}.debug'
+ if not os.path.isfile(flagfile):
+ continue
+ with open(flagfile) as f:
+ content = f.readline().strip()
+ return content or default
+
+ return ''
+
+
+def _logfile(flag, default):
+ """
+ return the name of the file to use for logging when the flag 'log' is set
+ if it could not be established or the location is invalid it returns
+ an empty string
+ """
+
+ # For log we return the location of the log file
+ log_location = _contentenv(flag) or _contentfile(flag, default)
+
+ # it was not set
+ if not log_location:
+ return ''
+
+ # Make sure that the logs can only be in /tmp, /var/log, or /tmp
+ if not log_location.startswith('/tmp/') and \
+ not log_location.startswith('/config/') and \
+ not log_location.startswith('/var/log/'):
+ return default
+ # Do not allow to escape the folders
+ if '..' in log_location:
+ return default
+
+ if not os.path.exists(log_location):
+ return log_location
+
+ # this permission is unique the the config and var folder
+ stat = os.stat(log_location).st_mode
+ if stat != 0o100666:
+ return default
+ return log_location
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
new file mode 100644
index 000000000..9921e3b5f
--- /dev/null
+++ b/python/vyos/defaults.py
@@ -0,0 +1,53 @@
+# 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/>.
+
+
+directories = {
+ "data": "/usr/share/vyos/",
+ "conf_mode": "/usr/libexec/vyos/conf_mode",
+ "config": "/opt/vyatta/etc/config",
+ "current": "/opt/vyatta/etc/config-migrate/current",
+ "migrate": "/opt/vyatta/etc/config-migrate/migrate",
+ "log": "/var/log/vyatta",
+ "templates": "/usr/share/vyos/templates/",
+ "certbot": "/config/auth/letsencrypt"
+}
+
+cfg_group = 'vyattacfg'
+
+cfg_vintage = 'vyos'
+
+commit_lock = '/opt/vyatta/config/.lock'
+
+version_file = '/usr/share/vyos/component-versions.json'
+
+https_data = {
+ 'listen_addresses' : { '*': ['_'] }
+}
+
+api_data = {
+ 'listen_address' : '127.0.0.1',
+ 'port' : '8080',
+ 'strict' : 'false',
+ 'debug' : 'false',
+ 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ]
+}
+
+vyos_cert_data = {
+ "conf": "/etc/nginx/snippets/vyos-cert.conf",
+ "crt": "/etc/ssl/certs/vyos-selfsigned.crt",
+ "key": "/etc/ssl/private/vyos-selfsign",
+ "lifetime": "365",
+}
diff --git a/python/vyos/dicts.py b/python/vyos/dicts.py
new file mode 100644
index 000000000..b12cda40f
--- /dev/null
+++ b/python/vyos/dicts.py
@@ -0,0 +1,53 @@
+#!/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/>.
+
+
+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)
diff --git a/python/vyos/formatversions.py b/python/vyos/formatversions.py
new file mode 100644
index 000000000..29117a5d3
--- /dev/null
+++ b/python/vyos/formatversions.py
@@ -0,0 +1,109 @@
+# 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 sys
+import os
+import re
+import fileinput
+
+def read_vyatta_versions(config_file):
+ config_file_versions = {}
+
+ with open(config_file, 'r') as config_file_handle:
+ for config_line in config_file_handle:
+ if re.match(r'/\* === vyatta-config-version:.+=== \*/$', config_line):
+ if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', config_line):
+ raise ValueError("malformed configuration string: "
+ "{}".format(config_line))
+
+ for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
+ config_file_versions[pair[0]] = int(pair[1])
+
+
+ return config_file_versions
+
+def read_vyos_versions(config_file):
+ config_file_versions = {}
+
+ with open(config_file, 'r') as config_file_handle:
+ for config_line in config_file_handle:
+ if re.match(r'// vyos-config-version:.+', config_line):
+ if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', config_line):
+ raise ValueError("malformed configuration string: "
+ "{}".format(config_line))
+
+ for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
+ config_file_versions[pair[0]] = int(pair[1])
+
+ return config_file_versions
+
+def remove_versions(config_file):
+ """
+ Remove old version string.
+ """
+ for line in fileinput.input(config_file, inplace=True):
+ if re.match(r'/\* Warning:.+ \*/$', line):
+ continue
+ if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line):
+ continue
+ if re.match(r'/\* Release version:.+ \*/$', line):
+ continue
+ if re.match('// vyos-config-version:.+', line):
+ continue
+ if re.match('// Warning:.+', line):
+ continue
+ if re.match('// Release version:.+', line):
+ continue
+ sys.stdout.write(line)
+
+def format_versions_string(config_versions):
+ cfg_keys = list(config_versions.keys())
+ cfg_keys.sort()
+
+ component_version_strings = []
+
+ for key in cfg_keys:
+ cfg_vers = config_versions[key]
+ component_version_strings.append('{}@{}'.format(key, cfg_vers))
+
+ separator = ":"
+ component_version_string = separator.join(component_version_strings)
+
+ return component_version_string
+
+def write_vyatta_versions_foot(config_file, component_version_string,
+ os_version_string):
+ if config_file:
+ with open(config_file, 'a') as config_file_handle:
+ config_file_handle.write('/* Warning: Do not remove the following line. */\n')
+ config_file_handle.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
+ config_file_handle.write('/* Release version: {} */\n'.format(os_version_string))
+ else:
+ sys.stdout.write('/* Warning: Do not remove the following line. */\n')
+ sys.stdout.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
+ sys.stdout.write('/* Release version: {} */\n'.format(os_version_string))
+
+def write_vyos_versions_foot(config_file, component_version_string,
+ os_version_string):
+ if config_file:
+ with open(config_file, 'a') as config_file_handle:
+ config_file_handle.write('// Warning: Do not remove the following line.\n')
+ config_file_handle.write('// vyos-config-version: "{}"\n'.format(component_version_string))
+ config_file_handle.write('// Release version: {}\n'.format(os_version_string))
+ else:
+ sys.stdout.write('// Warning: Do not remove the following line.\n')
+ sys.stdout.write('// vyos-config-version: "{}"\n'.format(component_version_string))
+ sys.stdout.write('// Release version: {}\n'.format(os_version_string))
+
diff --git a/python/vyos/frr.py b/python/vyos/frr.py
new file mode 100644
index 000000000..3fc75bbdf
--- /dev/null
+++ b/python/vyos/frr.py
@@ -0,0 +1,288 @@
+# 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/>.
+
+r"""
+A Library for interracting with the FRR daemon suite.
+It supports simple configuration manipulation and loading using the official tools
+supplied with FRR (vtysh and frr-reload)
+
+All configuration management and manipulation is done using strings and regex.
+
+
+Example Usage
+#####
+
+# Reading configuration from frr:
+```
+>>> original_config = get_configuration()
+>>> repr(original_config)
+'!\nfrr version 7.3.1\nfrr defaults traditional\nhostname debian\n......
+```
+
+
+# Modify a configuration section:
+```
+>>> new_bgp_section = 'router bgp 65000\n neighbor 192.0.2.1 remote-as 65000\n'
+>>> modified_config = replace_section(original_config, new_bgp_section, replace_re=r'router bgp \d+')
+>>> repr(modified_config)
+'............router bgp 65000\n neighbor 192.0.2.1 remote-as 65000\n...........'
+```
+
+Remove a configuration section:
+```
+>>> modified_config = remove_section(original_config, r'router ospf')
+```
+
+Test the new configuration:
+```
+>>> try:
+>>> mark_configuration(modified configuration)
+>>> except ConfigurationNotValid as e:
+>>> print('resulting configuration is not valid')
+>>> sys.exit(1)
+```
+
+Apply the new configuration:
+```
+>>> try:
+>>> replace_configuration(modified_config)
+>>> except CommitError as e:
+>>> print('Exception while commiting the supplied configuration')
+>>> print(e)
+>>> exit(1)
+```
+"""
+
+import tempfile
+import re
+from vyos import util
+
+_frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd',
+ 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd']
+
+path_vtysh = '/usr/bin/vtysh'
+path_frr_reload = '/usr/lib/frr/frr-reload.py'
+
+
+class FrrError(Exception):
+ pass
+
+
+class ConfigurationNotValid(FrrError):
+ """
+ The configuratioin supplied to vtysh is not valid
+ """
+ pass
+
+
+class CommitError(FrrError):
+ """
+ Commiting the supplied configuration failed to commit by a unknown reason
+ see commit error and/or run mark_configuration on the specified configuration
+ to se error generated
+
+ used by: reload_configuration()
+ """
+ pass
+
+
+class ConfigSectionNotFound(FrrError):
+ """
+ Removal of configuration failed because it is not existing in the supplied configuration
+ """
+ pass
+
+
+def get_configuration(daemon=None, marked=False):
+ """ Get current running FRR configuration
+ daemon: Collect only configuration for the specified FRR daemon,
+ supplying daemon=None retrieves the complete configuration
+ marked: Mark the configuration with "end" tags
+
+ return: string containing the running configuration from frr
+
+ """
+ if daemon and daemon not in _frr_daemons:
+ raise ValueError(f'The specified daemon type is not supported {repr(daemon)}')
+
+ cmd = f"{path_vtysh} -c 'show run'"
+ if daemon:
+ cmd += f' -d {daemon}'
+
+ output, code = util.popen(cmd, stderr=util.STDOUT)
+ if code:
+ raise OSError(code, output)
+
+ config = output.replace('\r', '')
+ # Remove first header lines from FRR config
+ config = config.split("\n", 3)[-1]
+ # Mark the configuration with end tags
+ if marked:
+ config = mark_configuration(config)
+
+ return config
+
+
+def mark_configuration(config):
+ """ Add end marks and Test the configuration for syntax faults
+ If the configuration is valid a marked version of the configuration is returned,
+ or else it failes with a ConfigurationNotValid Exception
+
+ config: The configuration string to mark/test
+ return: The marked configuration from FRR
+ """
+ output, code = util.popen(f"{path_vtysh} -m -f -", stderr=util.STDOUT, input=config)
+
+ if code == 2:
+ raise ConfigurationNotValid(str(output))
+ elif code:
+ raise OSError(code, output)
+
+ config = output.replace('\r', '')
+ return config
+
+
+def reload_configuration(config, daemon=None):
+ """ Execute frr-reload with the new configuration
+ This will try to reapply the supplied configuration inside FRR.
+ The configuration needs to be a complete configuration from the integrated config or
+ from a daemon.
+
+ config: The configuration to apply
+ daemon: Apply the conigutaion to the specified FRR daemon,
+ supplying daemon=None applies to the integrated configuration
+ return: None
+ """
+ if daemon and daemon not in _frr_daemons:
+ raise ValueError(f'The specified daemon type is not supported {repr(daemon)}')
+
+ f = tempfile.NamedTemporaryFile('w')
+ f.write(config)
+ f.flush()
+
+ cmd = f'{path_frr_reload} --reload'
+ if daemon:
+ cmd += f' --daemon {daemon}'
+ cmd += f' {f.name}'
+
+ output, code = util.popen(cmd, stderr=util.STDOUT)
+ f.close()
+ if code == 1:
+ raise CommitError(f'Configuration FRR failed while commiting code: {repr(output)}')
+ elif code:
+ raise OSError(code, output)
+
+ return output
+
+
+def execute(command):
+ """ Run commands inside vtysh
+ command: str containing commands to execute inside a vtysh session
+ """
+ if not isinstance(command, str):
+ raise ValueError(f'command needs to be a string: {repr(command)}')
+
+ cmd = f"{path_vtysh} -c '{command}'"
+
+ output, code = util.popen(cmd, stderr=util.STDOUT)
+ if code:
+ raise OSError(code, output)
+
+ config = output.replace('\r', '')
+ return config
+
+
+def configure(lines, daemon=False):
+ """ run commands inside config mode vtysh
+ lines: list or str conaining commands to execute inside a configure session
+ only one command executed on each configure()
+ Executing commands inside a subcontext uses the list to describe the context
+ ex: ['router bgp 6500', 'neighbor 192.0.2.1 remote-as 65000']
+ return: None
+ """
+ if isinstance(lines, str):
+ lines = [lines]
+ elif not isinstance(lines, list):
+ raise ValueError('lines needs to be string or list of commands')
+
+ if daemon and daemon not in _frr_daemons:
+ raise ValueError(f'The specified daemon type is not supported {repr(daemon)}')
+
+ cmd = f'{path_vtysh}'
+ if daemon:
+ cmd += f' -d {daemon}'
+
+ cmd += " -c 'configure terminal'"
+ for x in lines:
+ cmd += f" -c '{x}'"
+
+ output, code = util.popen(cmd, stderr=util.STDOUT)
+ if code == 1:
+ raise ConfigurationNotValid(f'Configuration FRR failed: {repr(output)}')
+ elif code:
+ raise OSError(code, output)
+
+ config = output.replace('\r', '')
+ return config
+
+
+def _replace_section(config, replacement, replace_re, before_re):
+ r"""Replace a section of FRR config
+ config: full original configuration
+ replacement: replacement configuration section
+ replace_re: The regex to replace
+ example: ^router bgp \d+$.?*^!$
+ this will replace everything between ^router bgp X$ and ^!$
+ before_re: When replace_re is not existant, the config will be added before this tag
+ example: ^line vty$
+
+ return: modified configuration as a text file
+ """
+ # Check if block is configured, remove the existing instance else add a new one
+ if re.findall(replace_re, config, flags=re.MULTILINE | re.DOTALL):
+ # Section is in the configration, replace it
+ return re.sub(replace_re, replacement, config, count=1,
+ flags=re.MULTILINE | re.DOTALL)
+ if before_re:
+ if not re.findall(before_re, config, flags=re.MULTILINE | re.DOTALL):
+ raise ConfigSectionNotFound(f"Config section {before_re} not found in config")
+
+ # If no section is in the configuration, add it before the line vty line
+ return re.sub(before_re, rf'{replacement}\n\g<1>', config, count=1,
+ flags=re.MULTILINE | re.DOTALL)
+
+ raise ConfigSectionNotFound(f"Config section {replacement} not found in config")
+
+
+def replace_section(config, replacement, from_re, to_re=r'!', before_re=r'line vty'):
+ r"""Replace a section of FRR config
+ config: full original configuration
+ replacement: replacement configuration section
+ from_re: Regex for the start of section matching
+ example: 'router bgp \d+'
+ to_re: Regex for stop of section matching
+ default: '!'
+ example: '!' or 'end'
+ before_re: When from_re/to_re does not return a match, the config will
+ be added before this tag
+ default: ^line vty$
+
+ startline and endline tags will be automatically added to the resulting from_re/to_re and before_re regex'es
+ """
+ return _replace_section(config, replacement, replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=rf'^({before_re})$')
+
+
+def remove_section(config, from_re, to_re='!'):
+ return _replace_section(config, '', replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=None)
diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py
new file mode 100644
index 000000000..303b6ea47
--- /dev/null
+++ b/python/vyos/hostsd_client.py
@@ -0,0 +1,119 @@
+import json
+import zmq
+
+SOCKET_PATH = "ipc:///run/vyos-hostsd/vyos-hostsd.sock"
+
+class VyOSHostsdError(Exception):
+ pass
+
+class Client(object):
+ def __init__(self):
+ try:
+ context = zmq.Context()
+ self.__socket = context.socket(zmq.REQ)
+ self.__socket.RCVTIMEO = 10000 #ms
+ self.__socket.setsockopt(zmq.LINGER, 0)
+ self.__socket.connect(SOCKET_PATH)
+ except zmq.error.Again:
+ raise VyOSHostsdError("Could not connect to vyos-hostsd")
+
+ def _communicate(self, msg):
+ try:
+ request = json.dumps(msg).encode()
+ self.__socket.send(request)
+
+ reply_msg = self.__socket.recv().decode()
+ reply = json.loads(reply_msg)
+ if 'error' in reply:
+ raise VyOSHostsdError(reply['error'])
+ else:
+ return reply["data"]
+ except zmq.error.Again:
+ raise VyOSHostsdError("Could not connect to vyos-hostsd")
+
+ def add_name_servers(self, data):
+ msg = {'type': 'name_servers', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_name_servers(self, data):
+ msg = {'type': 'name_servers', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_name_servers(self, tag_regex):
+ msg = {'type': 'name_servers', 'op': 'get', 'tag_regex': tag_regex}
+ return self._communicate(msg)
+
+ def add_name_server_tags_recursor(self, data):
+ msg = {'type': 'name_server_tags_recursor', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_name_server_tags_recursor(self, data):
+ msg = {'type': 'name_server_tags_recursor', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_name_server_tags_recursor(self):
+ msg = {'type': 'name_server_tags_recursor', 'op': 'get'}
+ return self._communicate(msg)
+
+ def add_name_server_tags_system(self, data):
+ msg = {'type': 'name_server_tags_system', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_name_server_tags_system(self, data):
+ msg = {'type': 'name_server_tags_system', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_name_server_tags_system(self):
+ msg = {'type': 'name_server_tags_system', 'op': 'get'}
+ return self._communicate(msg)
+
+ def add_forward_zones(self, data):
+ msg = {'type': 'forward_zones', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_forward_zones(self, data):
+ msg = {'type': 'forward_zones', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_forward_zones(self):
+ msg = {'type': 'forward_zones', 'op': 'get'}
+ return self._communicate(msg)
+
+ def add_search_domains(self, data):
+ msg = {'type': 'search_domains', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_search_domains(self, data):
+ msg = {'type': 'search_domains', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_search_domains(self, tag_regex):
+ msg = {'type': 'search_domains', 'op': 'get', 'tag_regex': tag_regex}
+ return self._communicate(msg)
+
+ def add_hosts(self, data):
+ msg = {'type': 'hosts', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_hosts(self, data):
+ msg = {'type': 'hosts', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_hosts(self, tag_regex):
+ msg = {'type': 'hosts', 'op': 'get', 'tag_regex': tag_regex}
+ return self._communicate(msg)
+
+ def set_host_name(self, host_name, domain_name):
+ msg = {
+ 'type': 'host_name',
+ 'op': 'set',
+ 'data': {
+ 'host_name': host_name,
+ 'domain_name': domain_name,
+ }
+ }
+ self._communicate(msg)
+
+ def apply(self):
+ msg = {'op': 'apply'}
+ return self._communicate(msg)
diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py
new file mode 100644
index 000000000..9cd8d44c1
--- /dev/null
+++ b/python/vyos/ifconfig/__init__.py
@@ -0,0 +1,44 @@
+# 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.section import Section
+from vyos.ifconfig.control import Control
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.operational import Operational
+from vyos.ifconfig.vrrp import VRRP
+
+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.vti import VTIIf
+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
+from vyos.ifconfig.macsec import MACsecIf
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..64407401b
--- /dev/null
+++ b/python/vyos/ifconfig/bond.py
@@ -0,0 +1,383 @@
+# 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.util import cmd
+from vyos.util import vyos_dict_search
+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'
+ """
+ # As this function might also be called from update() of a VLAN interface
+ # we must check if the bond_arp_ip_target retrieval worked or not - as this
+ # can not be set for a bond vif interface
+ try:
+ return self.get_interface('bond_arp_ip_target')
+ except FileNotFoundError:
+ return ''
+
+ 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')
+ """
+
+ # From drivers/net/bonding/bond_main.c:
+ # ...
+ # bond_set_slave_link_state(new_slave,
+ # BOND_LINK_UP,
+ # BOND_SLAVE_NOTIFY_NOW);
+ # ...
+ #
+ # The kernel will ALWAYS place new bond members in "up" state regardless
+ # what the CLI will tell us!
+
+ # Physical interface must be in admin down state before they can be
+ # enslaved. If this is not the case an error will be shown:
+ # bond0: eth0 is up - this may be due to an out of date ifenslave
+ slave = Interface(interface)
+ slave_state = slave.get_admin_state()
+ if slave_state == 'up':
+ slave.set_admin_state('down')
+
+ ret = self.set_interface('bond_add_port', f'+{interface}')
+ # The kernel will ALWAYS place new bond members in "up" state regardless
+ # what the LI is configured for - thus we place the interface in its
+ # desired state
+ slave.set_admin_state(slave_state)
+ return ret
+
+ 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)
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # use ref-counting function to place an interface into admin down state.
+ # set_admin_state_up() must be called the same amount of times else the
+ # interface won't come up. This can/should be used to prevent link flapping
+ # when changing interface parameters require the interface to be down.
+ # We will disable it once before reconfiguration and enable it afterwards.
+ if 'shutdown_required' in config:
+ self.set_admin_state('down')
+
+ # call base class first
+ super().update(config)
+
+ # ARP monitor targets need to be synchronized between sysfs and CLI.
+ # Unfortunately an address can't be send twice to sysfs as this will
+ # result in the following exception: OSError: [Errno 22] Invalid argument.
+ #
+ # We remove ALL addresses prior to adding new ones, this will remove
+ # addresses manually added by the user too - but as we are limited to 16 adresses
+ # from the kernel side this looks valid to me. We won't run into an error
+ # when a user added manual adresses which would result in having more
+ # then 16 adresses in total.
+ arp_tgt_addr = list(map(str, self.get_arp_ip_target().split()))
+ for addr in arp_tgt_addr:
+ self.set_arp_ip_target('-' + addr)
+
+ # Add configured ARP target addresses
+ value = vyos_dict_search('arp_monitor.target', config)
+ if isinstance(value, str):
+ value = [value]
+ if value:
+ for addr in value:
+ self.set_arp_ip_target('+' + addr)
+
+ # Bonding transmit hash policy
+ value = config.get('hash_policy')
+ if value: self.set_hash_policy(value)
+
+ # Some interface options can only be changed if the interface is
+ # administratively down
+ if self.get_admin_state() == 'down':
+ # Delete bond member port(s)
+ for interface in self.get_slaves():
+ self.del_port(interface)
+
+ # Bonding policy/mode
+ value = config.get('mode')
+ if value: self.set_mode(value)
+
+ # Add (enslave) interfaces to bond
+ value = vyos_dict_search('member.interface', config)
+ if value:
+ for interface in value:
+ # if we've come here we already verified the interface
+ # does not have an addresses configured so just flush
+ # any remaining ones
+ Interface(interface).flush_addrs()
+ self.add_port(interface)
+
+ # Primary device interface - must be set after 'mode'
+ value = config.get('primary')
+ if value: self.set_primary(value)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
new file mode 100644
index 000000000..4c76fe996
--- /dev/null
+++ b/python/vyos/ifconfig/bridge.py
@@ -0,0 +1,263 @@
+# 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.stp import STP
+from vyos.validate import assert_boolean
+from vyos.validate import assert_positive
+from vyos.util import cmd
+from vyos.util import vyos_dict_search
+
+@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)
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # call base class first
+ super().update(config)
+
+ # Set ageing time
+ value = config.get('aging')
+ self.set_ageing_time(value)
+
+ # set bridge forward delay
+ value = config.get('forwarding_delay')
+ self.set_forward_delay(value)
+
+ # set hello time
+ value = config.get('hello_time')
+ self.set_hello_time(value)
+
+ # set max message age
+ value = config.get('max_age')
+ self.set_max_age(value)
+
+ # set bridge priority
+ value = config.get('priority')
+ self.set_priority(value)
+
+ # enable/disable spanning tree
+ value = '1' if 'stp' in config else '0'
+ self.set_stp(value)
+
+ # enable or disable IGMP querier
+ tmp = vyos_dict_search('igmp.querier', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_multicast_querier(value)
+
+ # remove interface from bridge
+ tmp = vyos_dict_search('member.interface_remove', config)
+ if tmp:
+ for member in tmp:
+ self.del_port(member)
+
+ STPBridgeIf = STP.enable(BridgeIf)
+ tmp = vyos_dict_search('member.interface', config)
+ if tmp:
+ for interface, interface_config in tmp.items():
+ # if we've come here we already verified the interface
+ # does not have an addresses configured so just flush
+ # any remaining ones
+ Interface(interface).flush_addrs()
+ # enslave interface port to bridge
+ self.add_port(interface)
+
+ tmp = STPBridgeIf(interface)
+ # set bridge port path cost
+ value = interface_config.get('cost')
+ tmp.set_path_cost(value)
+
+ # set bridge port path priority
+ value = interface_config.get('priority')
+ tmp.set_path_priority(value)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py
new file mode 100644
index 000000000..a6fc8ac6c
--- /dev/null
+++ b/python/vyos/ifconfig/control.py
@@ -0,0 +1,185 @@
+# 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 inspect import signature
+from inspect import _empty
+
+from vyos import debug
+from vyos.util import popen
+from vyos.util import cmd
+from vyos.ifconfig.section import Section
+
+
+class Control(Section):
+ _command_get = {}
+ _command_set = {}
+ _signature = {}
+
+ 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) and debug.enabled('ifconfig'):
+ self.debug = 'ifconfig'
+
+ def _debug_msg (self, message):
+ return debug.message(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 _values(self, name, validate, value):
+ """
+ looks at the validation function "validate"
+ for the interface sysfs or command and
+ returns a dict with the right options to call it
+ """
+ if name not in self._signature:
+ self._signature[name] = signature(validate)
+
+ values = {}
+
+ for k in self._signature[name].parameters:
+ default = self._signature[name].parameters[k].default
+ if default is not _empty:
+ continue
+ if k == 'self':
+ values[k] = self
+ elif k == 'ifname':
+ values[k] = self.ifname
+ else:
+ values[k] = value
+
+ return values
+
+ 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(**self._values(name, 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:
+ try:
+ validate(**self._values(name, validate, value))
+ except Exception as e:
+ raise e.__class__(f'Could not set {name}. {e}')
+
+ 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/dummy.py b/python/vyos/ifconfig/dummy.py
new file mode 100644
index 000000000..43614cd1c
--- /dev/null
+++ b/python/vyos/ifconfig/dummy.py
@@ -0,0 +1,56 @@
+# 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', ],
+ },
+ }
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
new file mode 100644
index 000000000..17c1bd64d
--- /dev/null
+++ b/python/vyos/ifconfig/ethernet.py
@@ -0,0 +1,309 @@
+# 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
+from vyos.util import vyos_dict_search
+
+@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,
+ 'eternal': '(lan|eth|eno|ens|enp|enx)[0-9]+$',
+ }
+ }
+
+ @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 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)
+
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # call base class first
+ super().update(config)
+
+ # disable ethernet flow control (pause frames)
+ value = 'off' if 'disable_flow_control' in config.keys() else 'on'
+ self.set_flow_control(value)
+
+ # GRO (generic receive offload)
+ tmp = vyos_dict_search('offload_options.generic_receive', config)
+ value = tmp if (tmp != None) else 'off'
+ self.set_gro(value)
+
+ # GSO (generic segmentation offload)
+ tmp = vyos_dict_search('offload_options.generic_segmentation', config)
+ value = tmp if (tmp != None) else 'off'
+ self.set_gso(value)
+
+ # scatter-gather option
+ tmp = vyos_dict_search('offload_options.scatter_gather', config)
+ value = tmp if (tmp != None) else 'off'
+ self.set_sg(value)
+
+ # TSO (TCP segmentation offloading)
+ tmp = vyos_dict_search('offload_options.udp_fragmentation', config)
+ value = tmp if (tmp != None) else 'off'
+ self.set_tso(value)
+
+ # UDP fragmentation offloading
+ tmp = vyos_dict_search('offload_options.udp_fragmentation', config)
+ value = tmp if (tmp != None) else 'off'
+ self.set_ufo(value)
+
+ # Set physical interface speed and duplex
+ if {'speed', 'duplex'} <= set(config):
+ speed = config.get('speed')
+ duplex = config.get('duplex')
+ self.set_speed_duplex(speed, duplex)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py
new file mode 100644
index 000000000..dd0658668
--- /dev/null
+++ b/python/vyos/ifconfig/geneve.py
@@ -0,0 +1,85 @@
+# 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': '',
+ }
+ options = Interface.options + \
+ ['vni', '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)
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py
new file mode 100644
index 000000000..bfab36335
--- /dev/null
+++ b/python/vyos/ifconfig/input.py
@@ -0,0 +1,31 @@
+# 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 InputIf(Interface):
+ default = {
+ 'type': '',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'input',
+ 'prefixes': ['ifb', ],
+ },
+ }
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
new file mode 100644
index 000000000..67ba973c4
--- /dev/null
+++ b/python/vyos/ifconfig/interface.py
@@ -0,0 +1,1067 @@
+# 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 re
+import json
+import jmespath
+
+from copy import deepcopy
+from glob import glob
+
+from ipaddress import IPv4Network
+from ipaddress import IPv6Address
+from ipaddress import IPv6Network
+from netifaces import ifaddresses
+# this is not the same as socket.AF_INET/INET6
+from netifaces import AF_INET
+from netifaces import AF_INET6
+
+from vyos import ConfigError
+from vyos.configdict import list_diff
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import mac2eui64
+from vyos.util import vyos_dict_search
+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
+
+from vyos.ifconfig.control import Control
+from vyos.ifconfig.vrrp import VRRP
+from vyos.ifconfig.operational import Operational
+from vyos.ifconfig import Section
+
+def get_ethertype(ethertype_val):
+ if ethertype_val == '0x88A8':
+ return '802.1ad'
+ elif ethertype_val == '0x8100':
+ return '802.1q'
+ else:
+ raise ConfigError('invalid ethertype "{}"'.format(ethertype_val))
+
+class Interface(Control):
+ # This is the class which will be used to create
+ # self.operational, it allows subclasses, such as
+ # WireGuard to modify their display behaviour
+ OperationalClass = Operational
+
+ options = ['debug', 'create']
+ required = []
+ default = {
+ 'type': '',
+ 'debug': True,
+ 'create': True,
+ }
+ definition = {
+ 'section': '',
+ 'prefixes': [],
+ 'vlan': False,
+ 'bondable': False,
+ 'broadcast': False,
+ 'bridgeable': False,
+ 'eternal': '',
+ }
+
+ _command_get = {
+ 'admin_state': {
+ 'shellcmd': 'ip -json link show dev {ifname}',
+ 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down',
+ },
+ 'vlan_protocol': {
+ 'shellcmd': 'ip -json -details link show dev {ifname}',
+ 'format': lambda j: jmespath.search('[*].linkinfo.info_data.protocol | [0]', json.loads(j)),
+ },
+ }
+
+ _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_accept_ra': {
+ 'validate': lambda ara: assert_range(ara,0,3),
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
+ },
+ 'ipv6_autoconf': {
+ 'validate': lambda aco: assert_range(aco,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',
+ },
+ }
+
+ @classmethod
+ def exists(cls, ifname):
+ return os.path.exists(f'/sys/class/net/{ifname}')
+
+ 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
+ self._admin_state_down_cnt = 0
+
+ # we must have updated config before initialising the Interface
+ super().__init__(**kargs)
+ self.ifname = ifname
+
+ if not self.exists(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(f'interface "{ifname}" not found')
+
+ # 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']))
+
+ # temporary list of assigned IP addresses
+ self._addr = []
+
+ self.operational = self.OperationalClass(ifname)
+ self.vrrp = VRRP(ifname)
+
+ 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()
+ """
+
+ # 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
+ self.flush_addrs()
+
+ # ---------------------------------------------------------------------
+ # Any class can define an eternal regex in its definition
+ # interface matching the regex will not be deleted
+
+ eternal = self.definition['eternal']
+ if not eternal:
+ self._delete()
+ elif not re.match(eternal, self.ifname):
+ 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 {ifname}'.format(**self.config)
+ 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)
+
+ # Turn an interface to the 'up' state if it was changed to 'down' by this fucntion
+ if prev_state == 'up':
+ self.set_admin_state('up')
+
+ 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_accept_ra(self, accept_ra):
+ """
+ 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.
+
+ 0 - Do not accept Router Advertisements.
+ 1 - (default) Accept Router Advertisements if forwarding is disabled.
+ 2 - Overrule forwarding behaviour. Accept Router Advertisements even if
+ forwarding is enabled.
+ """
+ return self.set_interface('ipv6_accept_ra', accept_ra)
+
+ def set_ipv6_autoconf(self, autoconf):
+ """
+ Autoconfigure addresses using Prefix Information in Router
+ Advertisements.
+ """
+ return self.set_interface('ipv6_autoconf', autoconf)
+
+ def add_ipv6_eui64_address(self, prefix):
+ """
+ Extended Unique Identifier (EUI), as per RFC2373, allows a host to
+ assign itself a unique IPv6 address based on a given IPv6 prefix.
+
+ Calculate the EUI64 from the interface's MAC, then assign it
+ with the given prefix to the interface.
+ """
+
+ eui64 = mac2eui64(self.get_mac(), prefix)
+ prefixlen = prefix.split('/')[1]
+ self.add_addr(f'{eui64}/{prefixlen}')
+
+ def del_ipv6_eui64_address(self, prefix):
+ """
+ Delete the address based on the interface's MAC-based EUI64
+ combined with the prefix address.
+ """
+ eui64 = mac2eui64(self.get_mac(), prefix)
+ prefixlen = prefix.split('/')[1]
+ self.del_addr(f'{eui64}/{prefixlen}')
+
+
+ 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_vlan_protocol(self):
+ """
+ Retrieve VLAN protocol in use, this can be 802.1Q, 802.1ad or None
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0.10').get_vlan_protocol()
+ '802.1Q'
+ """
+ return self.get_interface('vlan_protocol')
+
+ 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'
+ """
+ # A VLAN interface can only be placed in admin up state when
+ # the lower interface is up, too
+ if self.get_vlan_protocol():
+ lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0]
+ with open(lower_interface, 'r') as f:
+ flags = f.read()
+ # If parent is not up - bail out as we can not bring up the VLAN.
+ # Flags are defined in kernel source include/uapi/linux/if.h
+ if not int(flags, 16) & 1:
+ return None
+
+ if state == 'up':
+ self._admin_state_down_cnt -= 1
+ if self._admin_state_down_cnt < 1:
+ return self.set_interface('admin_state', state)
+ else:
+ self._admin_state_down_cnt += 1
+ return self.set_interface('admin_state', 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. Address format must be validated
+ and compressed/normalized before calling this function.
+
+ 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 WIDE DHCPv6 (IPv6) on interface
+
+ Returns False if address is already assigned and wasn't re-added.
+ 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']
+ """
+ # XXX: normalize/compress with ipaddress if calling functions don't?
+ # is subnet mask always passed, and in the same way?
+
+ # do not add same address twice
+ if addr in self._addr:
+ return False
+
+ addr_is_v4 = is_ipv4(addr)
+
+ # we can't have both DHCP and static IPv4 addresses assigned
+ for a in self._addr:
+ if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or
+ ( a == 'dhcp' and addr != 'dhcpv6' and addr_is_v4 ) ):
+ raise ConfigError((
+ "Can't configure both static IPv4 and DHCP address "
+ "on the same interface"))
+
+ # add to interface
+ if addr == 'dhcp':
+ self.set_dhcp(True)
+ elif addr == 'dhcpv6':
+ self.set_dhcpv6(True)
+ elif not is_intf_addr_assigned(self.ifname, addr):
+ self._cmd(f'ip addr add "{addr}" '
+ f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"')
+ else:
+ return False
+
+ # add to cache
+ self._addr.append(addr)
+
+ return True
+
+ def del_addr(self, addr):
+ """
+ Delete IP(v6) address from interface. Address is only deleted if it is
+ assigned to that interface. Address format must be exactly the same as
+ was used when adding the address.
+
+ 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
+
+ Returns False if address isn't already assigned and wasn't deleted.
+ 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']
+ """
+
+ # remove from interface
+ if addr == 'dhcp':
+ self.set_dhcp(False)
+ elif addr == 'dhcpv6':
+ self.set_dhcpv6(False)
+ elif is_intf_addr_assigned(self.ifname, addr):
+ self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"')
+ else:
+ return False
+
+ # remove from cache
+ if addr in self._addr:
+ self._addr.remove(addr)
+
+ return True
+
+ def flush_addrs(self):
+ """
+ Flush all addresses from an interface, including DHCP.
+
+ Will raise an exception on error.
+ """
+ # stop DHCP(v6) if running
+ self.set_dhcp(False)
+ self.set_dhcpv6(False)
+
+ # flush all addresses
+ self._cmd(f'ip addr flush dev "{self.ifname}"')
+
+ def add_to_bridge(self, br):
+ """
+ Adds the interface to the bridge with the passed port config.
+
+ Returns False if bridge doesn't exist.
+ """
+
+ # check if the bridge exists (on boot it doesn't)
+ if br not in Section.interfaces('bridge'):
+ return False
+
+ self.flush_addrs()
+ # add interface to bridge - use Section.klass to get BridgeIf class
+ Section.klass(br)(br, create=False).add_port(self.ifname)
+
+ # TODO: port config (STP)
+
+ return True
+
+ def set_dhcp(self, enable):
+ """
+ Enable/Disable DHCP client on a given interface.
+ """
+ if enable not in [True, False]:
+ raise ValueError()
+
+ ifname = self.ifname
+ config_base = r'/var/lib/dhcp/dhclient'
+ config_file = f'{config_base}_{ifname}.conf'
+ options_file = f'{config_base}_{ifname}.options'
+ pid_file = f'{config_base}_{ifname}.pid'
+ lease_file = f'{config_base}_{ifname}.leases'
+
+ if enable and 'disable' not in self._config:
+ if vyos_dict_search('dhcp_options.host_name', self._config) == None:
+ # read configured system hostname.
+ # maybe change to vyos hostd client ???
+ hostname = 'vyos'
+ with open('/etc/hostname', 'r') as f:
+ hostname = f.read().rstrip('\n')
+ tmp = {'dhcp_options' : { 'host_name' : hostname}}
+ self._config = dict_merge(tmp, self._config)
+
+ render(options_file, 'dhcp-client/daemon-options.tmpl',
+ self._config, trim_blocks=True)
+ render(config_file, 'dhcp-client/ipv4.tmpl',
+ self._config, trim_blocks=True)
+
+ # 'up' check is mandatory b/c even if the interface is A/D, as soon as
+ # the DHCP client is started the interface will be placed in u/u state.
+ # This is not what we intended to do when disabling an interface.
+ return self._cmd(f'systemctl restart dhclient@{ifname}.service')
+ else:
+ self._cmd(f'systemctl stop dhclient@{ifname}.service')
+
+ # cleanup old config files
+ for file in [config_file, options_file, pid_file, lease_file]:
+ if os.path.isfile(file):
+ os.remove(file)
+
+
+ def set_dhcpv6(self, enable):
+ """
+ Enable/Disable DHCPv6 client on a given interface.
+ """
+ if enable not in [True, False]:
+ raise ValueError()
+
+ ifname = self.ifname
+ config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf'
+
+ if enable and 'disable' not in self._config:
+ render(config_file, 'dhcp-client/ipv6.tmpl',
+ self._config, trim_blocks=True)
+
+ # We must ignore any return codes. This is required to enable DHCPv6-PD
+ # for interfaces which are yet not up and running.
+ return self._popen(f'systemctl restart dhcp6c@{ifname}.service')
+ else:
+ self._popen(f'systemctl stop dhcp6c@{ifname}.service')
+
+ if os.path.isfile(config_file):
+ os.remove(config_file)
+
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # Cache the configuration - it will be reused inside e.g. DHCP handler
+ # XXX: maybe pass the option via __init__ in the future and rename this
+ # method to apply()?
+ self._config = config
+
+ # Update interface description
+ self.set_alias(config.get('description', ''))
+
+ # Ignore link state changes
+ value = '2' if 'disable_link_detect' in config else '1'
+ self.set_link_detect(value)
+
+ # Configure assigned interface IP addresses. No longer
+ # configured addresses will be removed first
+ new_addr = config.get('address', [])
+
+ # XXX: T2636 workaround: convert string to a list with one element
+ if isinstance(new_addr, str):
+ new_addr = [new_addr]
+
+ # always ensure DHCP client is stopped (when not configured explicitly)
+ if 'dhcp' not in new_addr:
+ self.del_addr('dhcp')
+
+ # always ensure DHCPv6 client is stopped (when not configured as client
+ # for IPv6 address or prefix delegation
+ dhcpv6pd = vyos_dict_search('dhcpv6_options.pd', config)
+ if 'dhcpv6' not in new_addr or dhcpv6pd == None:
+ self.del_addr('dhcpv6')
+
+ # determine IP addresses which are assigned to the interface and build a
+ # list of addresses which are no longer in the dict so they can be removed
+ cur_addr = self.get_addr()
+ for addr in list_diff(cur_addr, new_addr):
+ self.del_addr(addr)
+
+ for addr in new_addr:
+ self.add_addr(addr)
+
+ # start DHCPv6 client when only PD was configured
+ if dhcpv6pd != None:
+ self.set_dhcpv6(True)
+
+ # There are some items in the configuration which can only be applied
+ # if this instance is not bound to a bridge. This should be checked
+ # by the caller but better save then sorry!
+ if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config):
+ # Bind interface to given VRF or unbind it if vrf node is not set.
+ # unbinding will call 'ip link set dev eth0 nomaster' which will
+ # also drop the interface out of a bridge or bond - thus this is
+ # checked before
+ self.set_vrf(config.get('vrf', ''))
+
+ # Configure ARP cache timeout in milliseconds - has default value
+ tmp = vyos_dict_search('ip.arp_cache_timeout', config)
+ value = tmp if (tmp != None) else '30'
+ self.set_arp_cache_tmo(value)
+
+ # Configure ARP filter configuration
+ tmp = vyos_dict_search('ip.disable_arp_filter', config)
+ value = '0' if (tmp != None) else '1'
+ self.set_arp_filter(value)
+
+ # Configure ARP accept
+ tmp = vyos_dict_search('ip.enable_arp_accept', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_arp_accept(value)
+
+ # Configure ARP announce
+ tmp = vyos_dict_search('ip.enable_arp_announce', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_arp_announce(value)
+
+ # Configure ARP ignore
+ tmp = vyos_dict_search('ip.enable_arp_ignore', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_arp_ignore(value)
+
+ # Enable proxy-arp on this interface
+ tmp = vyos_dict_search('ip.enable_proxy_arp', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_proxy_arp(value)
+
+ # Enable private VLAN proxy ARP on this interface
+ tmp = vyos_dict_search('ip.proxy_arp_pvlan', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_proxy_arp_pvlan(value)
+
+ # IPv6 forwarding
+ tmp = vyos_dict_search('ipv6.disable_forwarding', config)
+ value = '0' if (tmp != None) else '1'
+ self.set_ipv6_forwarding(value)
+
+ # IPv6 router advertisements
+ tmp = vyos_dict_search('ipv6.address.autoconf', config)
+ value = '2' if (tmp != None) else '1'
+ if 'dhcpv6' in new_addr:
+ value = '2'
+ self.set_ipv6_accept_ra(value)
+
+ # IPv6 address autoconfiguration
+ tmp = vyos_dict_search('ipv6.address.autoconf', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_ipv6_autoconf(value)
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ tmp = vyos_dict_search('ipv6.dup_addr_detect_transmits', config)
+ value = tmp if (tmp != None) else '1'
+ self.set_ipv6_dad_messages(value)
+
+ # MTU - Maximum Transfer Unit
+ if 'mtu' in config:
+ self.set_mtu(config.get('mtu'))
+
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ tmp = vyos_dict_search('ipv6.address.eui64_old', config)
+ if tmp:
+ for addr in tmp:
+ self.del_ipv6_eui64_address(addr)
+
+ # Change interface MAC address - re-set to real hardware address (hw-id)
+ # if custom mac is removed. Skip if bond member.
+ if 'is_bond_member' not in config:
+ mac = config.get('hw_id')
+ if 'mac' in config:
+ mac = config.get('mac')
+ if mac:
+ self.set_mac(mac)
+
+ # Manage IPv6 link-local addresses
+ tmp = vyos_dict_search('ipv6.address.no_default_link_local', config)
+ # we must check explicitly for None type as if the key is set we will
+ # get an empty dict (<class 'dict'>)
+ if tmp is not None:
+ self.del_ipv6_eui64_address('fe80::/64')
+ else:
+ self.add_ipv6_eui64_address('fe80::/64')
+
+ # Add IPv6 EUI-based addresses
+ tmp = vyos_dict_search('ipv6.address.eui64', config)
+ if tmp:
+ # XXX: T2636 workaround: convert string to a list with one element
+ if isinstance(tmp, str):
+ tmp = [tmp]
+ for addr in tmp:
+ self.add_ipv6_eui64_address(addr)
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if 'is_bridge_member' in config:
+ bridge = config.get('is_bridge_member')
+ self.add_to_bridge(bridge)
+
+ # remove no longer required 802.1ad (Q-in-Q VLANs)
+ for vif_s_id in config.get('vif_s_remove', {}):
+ self.del_vlan(vif_s_id)
+
+ # create/update 802.1ad (Q-in-Q VLANs)
+ ifname = config['ifname']
+ for vif_s_id, vif_s in config.get('vif_s', {}).items():
+ tmp=get_ethertype(vif_s.get('ethertype', '0x88A8'))
+ s_vlan = self.add_vlan(vif_s_id, ethertype=tmp)
+ vif_s['ifname'] = f'{ifname}.{vif_s_id}'
+ s_vlan.update(vif_s)
+
+ # remove no longer required client VLAN (vif-c)
+ for vif_c_id in vif_s.get('vif_c_remove', {}):
+ s_vlan.del_vlan(vif_c_id)
+
+ # create/update client VLAN (vif-c) interface
+ for vif_c_id, vif_c in vif_s.get('vif_c', {}).items():
+ c_vlan = s_vlan.add_vlan(vif_c_id)
+ vif_c['ifname'] = f'{ifname}.{vif_s_id}.{vif_c_id}'
+ c_vlan.update(vif_c)
+
+ # remove no longer required 802.1q VLAN interfaces
+ for vif_id in config.get('vif_remove', {}):
+ self.del_vlan(vif_id)
+
+ # create/update 802.1q VLAN interfaces
+ for vif_id, vif in config.get('vif', {}).items():
+ vlan = self.add_vlan(vif_id)
+ vif['ifname'] = f'{ifname}.{vif_id}'
+ vlan.update(vif)
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..2b4ebfdcc
--- /dev/null
+++ b/python/vyos/ifconfig/loopback.py
@@ -0,0 +1,89 @@
+# 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.
+ """
+ _persistent_addresses = ['127.0.0.1/8', '::1/128']
+ 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 self._persistent_addresses:
+ # 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)
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ addr = config.get('address', [])
+ # XXX workaround for T2636, convert IP address string to a list
+ # with one element
+ if isinstance(addr, str):
+ addr = [addr]
+
+ # We must ensure that the loopback addresses are never deleted from the system
+ addr += self._persistent_addresses
+
+ # Update IP address entry in our dictionary
+ config.update({'address' : addr})
+
+ # call base class
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py
new file mode 100644
index 000000000..6f570d162
--- /dev/null
+++ b/python/vyos/ifconfig/macsec.py
@@ -0,0 +1,92 @@
+# 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 MACsecIf(Interface):
+ """
+ MACsec is an IEEE standard (IEEE 802.1AE) for MAC security, introduced in
+ 2006. It defines a way to establish a protocol independent connection
+ between two hosts with data confidentiality, authenticity and/or integrity,
+ using GCM-AES-128. MACsec operates on the Ethernet layer and as such is a
+ layer 2 protocol, which means it's designed to secure traffic within a
+ layer 2 network, including DHCP or ARP requests. It does not compete with
+ other security solutions such as IPsec (layer 3) or TLS (layer 4), as all
+ those solutions are used for their own specific use cases.
+ """
+
+ default = {
+ 'type': 'macsec',
+ 'security_cipher': '',
+ 'source_interface': ''
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'macsec',
+ 'prefixes': ['macsec', ],
+ },
+ }
+ options = Interface.options + \
+ ['security_cipher', 'source_interface']
+
+ def _create(self):
+ """
+ Create MACsec interface in OS kernel. Interface is administrative
+ down by default.
+ """
+ # create tunnel interface
+ cmd = 'ip link add link {source_interface} {ifname} type {type}'
+ cmd += ' cipher {security_cipher}'
+ self._cmd(cmd.format(**self.config))
+
+ # interface is always A/D down. It needs to be enabled explicitly
+ self.set_admin_state('down')
+
+ @staticmethod
+ def get_config():
+ """
+ MACsec interfaces require a configuration when they are added using
+ iproute2. This static method will provide the configuration dictionary
+ used by this class.
+
+ Example:
+ >> dict = MACsecIf().get_config()
+ """
+ config = {
+ 'security_cipher': '',
+ 'source_interface': '',
+ }
+ return config
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py
new file mode 100644
index 000000000..b068ce873
--- /dev/null
+++ b/python/vyos/ifconfig/macvlan.py
@@ -0,0 +1,89 @@
+# 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
+from vyos.ifconfig.vlan import VLAN
+
+
+@Interface.register
+@VLAN.enable
+class MACVLANIf(Interface):
+ """
+ Abstraction of a Linux MACvlan interface
+ """
+
+ default = {
+ 'type': 'macvlan',
+ 'address': '',
+ 'source_interface': '',
+ 'mode': '',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'pseudo-ethernet',
+ 'prefixes': ['peth', ],
+ },
+ }
+ options = Interface.options + \
+ ['source_interface', 'mode']
+
+ def _create(self):
+ # please do not change the order when assembling the command
+ cmd = 'ip link add {ifname}'
+ if self.config['source_interface']:
+ cmd += ' link {source_interface}'
+ cmd += ' type macvlan'
+ if self.config['mode']:
+ cmd += ' mode {mode}'
+ self._cmd(cmd.format(**self.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)
+
+ @classmethod
+ def get_config(cls):
+ """
+ MACVLAN interfaces require a configuration when they are added using
+ iproute2. This method will provide the configuration dictionary used
+ by this class.
+
+ Example:
+ >> dict = MACVLANIf().get_config()
+ """
+ return deepcopy(cls.default)
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/operational.py b/python/vyos/ifconfig/operational.py
new file mode 100644
index 000000000..d585c1873
--- /dev/null
+++ b/python/vyos/ifconfig/operational.py
@@ -0,0 +1,179 @@
+# 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 time import time
+from datetime import datetime
+from functools import reduce
+
+from tabulate import tabulate
+
+from vyos.ifconfig import Control
+
+
+class Operational(Control):
+ """
+ A class able to load Interface statistics
+ """
+
+ cache_magic = 'XYZZYX'
+
+ _stat_names = {
+ 'rx': ['bytes', 'packets', 'errors', 'dropped', 'overrun', 'mcast'],
+ 'tx': ['bytes', 'packets', 'errors', 'dropped', 'carrier', 'collisions'],
+ }
+
+ _stats_dir = {
+ 'rx': ['rx_bytes', 'rx_packets', 'rx_errors', 'rx_dropped', 'rx_over_errors', 'multicast'],
+ 'tx': ['tx_bytes', 'tx_packets', 'tx_errors', 'tx_dropped', 'tx_carrier_errors', 'collisions'],
+ }
+
+ # a list made of the content of _stats_dir['rx'] + _stats_dir['tx']
+ _stats_all = reduce(lambda x, y: x+y, _stats_dir.values())
+
+ # this is not an interface but will be able to be controlled like one
+ _sysfs_get = {
+ 'oper_state':{
+ 'location': '/sys/class/net/{ifname}/operstate',
+ },
+ }
+
+
+ @classmethod
+ def cachefile (cls, ifname):
+ # the file where we are saving the counters
+ return f'/var/run/vyatta/{ifname}.stats'
+
+
+ def __init__(self, ifname):
+ """
+ Operational provide access to the counters of an interface
+ It behave like an interface when it comes to access sysfs
+
+ interface is an instance of the interface for which we want
+ to look at (a subclass of Interface, such as EthernetIf)
+ """
+
+ # add a self.config to minic Interface behaviour and make
+ # coding similar. Perhaps part of class Interface could be
+ # moved into a shared base class.
+ self.config = {
+ 'ifname': ifname,
+ 'create': False,
+ 'debug': False,
+ }
+ super().__init__(**self.config)
+ self.ifname = ifname
+
+ # adds all the counters of an interface
+ for stat in self._stats_all:
+ self._sysfs_get[stat] = {
+ 'location': '/sys/class/net/{ifname}/statistics/'+stat,
+ }
+
+ def get_state(self):
+ """
+ Get interface operational state
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').operational.get_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')
+
+ @classmethod
+ def strtime (cls, epoc):
+ """
+ represent an epoc/unix date in the format used by operation commands
+ """
+ return datetime.fromtimestamp(epoc).strftime("%a %b %d %R:%S %Z %Y")
+
+ def save_counters(self, stats):
+ """
+ record the provided stats to a file keeping vyatta compatibility
+ """
+
+ with open(self.cachefile(self.ifname), 'w') as f:
+ f.write(self.cache_magic)
+ f.write('\n')
+ f.write(str(int(time())))
+ f.write('\n')
+ for k,v in stats.items():
+ if v:
+ f.write(f'{k},{v}\n')
+
+ def load_counters(self):
+ """
+ load the stats from a file keeping vyatta compatibility
+ return a dict() with the value for each interface counter for the cache
+ """
+ ifname = self.config['ifname']
+
+ stats = {}
+ no_stats = {}
+ for name in self._stats_all:
+ stats[name] = 0
+ no_stats[name] = 0
+
+ try:
+ with open(self.cachefile(self.ifname),'r') as f:
+ magic = f.readline().strip()
+ if magic != self.cache_magic:
+ print(f'bad magic {ifname}')
+ return no_stats
+ stats['timestamp'] = f.readline().strip()
+ for line in f:
+ k, v = line.split(',')
+ stats[k] = int(v)
+ return stats
+ except IOError:
+ return no_stats
+
+ def clear_counters(self, counters=None):
+ clear = self._stats_all if counters is None else []
+ stats = self.load_counters()
+ for counter, value in stats.items():
+ stats[counter] = 0 if counter in clear else value
+ self.save_counters(stats)
+
+ def reset_counters(self):
+ os.remove(self.cachefile(self.ifname))
+
+ def get_stats(self):
+ """ return a dict() with the value for each interface counter """
+ stats = {}
+ for counter in self._stats_all:
+ stats[counter] = int(self.get_interface(counter))
+ return stats
+
+ def formated_stats(self, indent=4):
+ tabs = []
+ stats = self.get_stats()
+ for rtx in self._stats_dir:
+ tabs.append([f'{rtx.upper()}:', ] + [_ for _ in self._stat_names[rtx]])
+ tabs.append(['', ] + [stats[_] for _ in self._stats_dir[rtx]])
+
+ s = tabulate(
+ tabs,
+ stralign="right",
+ numalign="right",
+ tablefmt="plain"
+ )
+
+ p = ' '*indent
+ return f'{p}' + s.replace('\n', f'\n{p}')
diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py
new file mode 100644
index 000000000..787245696
--- /dev/null
+++ b/python/vyos/ifconfig/pppoe.py
@@ -0,0 +1,41 @@
+# 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', ],
+ },
+ }
+
+ # stub this interface is created in the configure script
+
+ def _create(self):
+ # we can not create this interface as it is managed outside
+ pass
+
+ def _delete(self):
+ # we can not create this interface as it is managed outside
+ pass
diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py
new file mode 100644
index 000000000..173a90bb4
--- /dev/null
+++ b/python/vyos/ifconfig/section.py
@@ -0,0 +1,189 @@
+# 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 re
+import netifaces
+
+
+class Section:
+ # the known interface prefixes
+ _prefixes = {}
+ _classes = []
+
+ # 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):
+ """
+ A function to use as decorator the interfaces classes
+ It register the prefix for the interface (eth, dum, vxlan, ...)
+ with the class which can handle it (EthernetIf, DummyIf,VXLANIf, ...)
+ """
+ if not klass.definition.get('prefixes',[]):
+ raise RuntimeError(f'valid interface prefixes not defined for {klass.__name__}')
+
+ cls._classes.append(klass)
+
+ 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 the number at the end of interface name
+ name: name of the interface
+ vlan: if vlan is True, do not stop at the vlan number
+ """
+ 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: name of the interface (eth0, dum1, ...)
+ vlan: should we try try to remove the VLAN from the number
+ """
+ name = cls._basename(name, vlan)
+
+ if name in cls._prefixes:
+ return cls._prefixes[name].definition['section']
+ return ''
+
+ @classmethod
+ def sections(cls):
+ """
+ return all the sections we found under 'set interfaces'
+ """
+ return list(set([cls._prefixes[_].definition['section'] for _ in cls._prefixes]))
+
+ @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 _intf_under_section (cls,section=''):
+ """
+ return a generator with the name of the configured interface
+ which are under a section
+ """
+ interfaces = netifaces.interfaces()
+
+ for ifname in interfaces:
+ ifsection = cls.section(ifname)
+ if not ifsection:
+ continue
+
+ if section and ifsection != section:
+ continue
+
+ yield ifname
+
+ @classmethod
+ def _sort_interfaces(cls, generator):
+ """
+ return a list of the sorted interface by number, vlan, qinq
+ """
+ def key(ifname):
+ value = 0
+ parts = re.split(r'([^0-9]+)([0-9]+)[.]?([0-9]+)?[.]?([0-9]+)?', ifname)
+ length = len(parts)
+ name = parts[1] if length >= 3 else parts[0]
+ # the +1 makes sure eth0.0.0 after eth0.0
+ number = int(parts[2]) + 1 if length >= 4 and parts[2] is not None else 0
+ vlan = int(parts[3]) + 1 if length >= 5 and parts[3] is not None else 0
+ qinq = int(parts[4]) + 1 if length >= 6 and parts[4] is not None else 0
+
+ # so that "lo" (or short names) are handled (as "loa")
+ for n in (name + 'aaa')[:3]:
+ value *= 100
+ value += (ord(n) - ord('a'))
+ value += number
+ # vlan are 16 bits, so this can not overflow
+ value = (value << 16) + vlan
+ value = (value << 16) + qinq
+ return value
+
+ l = list(generator)
+ l.sort(key=key)
+ return l
+
+ @classmethod
+ def interfaces(cls, section=''):
+ """
+ return a list of the name of the configured interface which are under a section
+ if no section is provided, then it returns all configured interfaces
+ """
+
+ return cls._sort_interfaces(cls._intf_under_section(section))
+
+ @classmethod
+ def _intf_with_feature(cls, feature=''):
+ """
+ return a generator with the name of the configured interface which have
+ a particular feature set in their definition such as:
+ bondable, broadcast, bridgeable, ...
+ """
+ for klass in cls._classes:
+ if klass.definition[feature]:
+ yield klass.definition['section']
+
+ @classmethod
+ def feature(cls, feature=''):
+ """
+ return list with the name of the configured interface which have
+ a particular feature set in their definition such as:
+ bondable, broadcast, bridgeable, ...
+ """
+ return list(cls._intf_with_feature(feature))
+
+ @classmethod
+ def reserved(cls):
+ """
+ return list with the interface name prefixes
+ eth, lo, vxlan, dum, ...
+ """
+ return list(cls._prefixes.keys())
+
+ @classmethod
+ def get_config_path(cls, name):
+ """
+ get config path to interface with .vif or .vif-s.vif-c
+ example: eth0.1.2 -> 'ethernet eth0 vif-s 1 vif-c 2'
+ Returns False if interface name is invalid (not found in sections)
+ """
+ sect = cls.section(name)
+ if sect:
+ splinterface = name.split('.')
+ intfpath = f'{sect} {splinterface[0]}'
+ if len(splinterface) == 2:
+ intfpath += f' vif {splinterface[1]}'
+ elif len(splinterface) == 3:
+ intfpath += f' vif-s {splinterface[1]} vif-c {splinterface[2]}'
+ return intfpath
+ else:
+ return False
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..85c22b5b4
--- /dev/null
+++ b/python/vyos/ifconfig/tunnel.py
@@ -0,0 +1,338 @@
+# 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': False,
+ },
+ }
+
+ # 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
+ """
+
+ definition = {
+ **_Tunnel.definition,
+ **{
+ 'bridgeable': True,
+ },
+ }
+
+ ip = [IP4, IP6]
+ tunnel = IP4
+
+ default = {'type': 'gre'}
+ required = ['local', ] # mGRE is a GRE without remote endpoint
+
+ options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key']
+ updates = ['local', 'remote', 'dev', 'ttl', 'tos',
+ 'mtu', '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
+
+ definition = {
+ **_Tunnel.definition,
+ **{
+ 'bridgeable': True,
+ },
+ }
+
+ ip = [IP4, ]
+ tunnel = IP4
+
+ default = {'type': 'gretap'}
+ required = ['local', ]
+
+ options = ['local', 'remote', ]
+ updates = ['mtu', ]
+
+ 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', 'dev', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel']
+ updates = ['local', 'remote', 'dev', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel',
+ 'mtu', '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', 'dev', 'ttl', 'tos', 'key']
+ updates = ['local', 'remote', 'dev', 'ttl', 'tos',
+ 'mtu', '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', 'dev', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel']
+ updates = ['local', 'remote', 'dev', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel',
+ 'mtu', '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', 'dev', 'ttl', 'tos', 'key']
+ updates = ['local', 'remote', 'dev', 'ttl', 'tos',
+ 'mtu', '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',
+ 'mtu', '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..d68e8f6cd
--- /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 os.path.exists(f'/sys/class/net/{vlan_ifname}'):
+ return self.__class__(vlan_ifname)
+
+ 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.ifname, vlan=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/vrrp.py b/python/vyos/ifconfig/vrrp.py
new file mode 100644
index 000000000..01a7cc7ab
--- /dev/null
+++ b/python/vyos/ifconfig/vrrp.py
@@ -0,0 +1,151 @@
+# 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 json
+import signal
+from time import time
+from time import sleep
+
+from tabulate import tabulate
+
+from vyos import airbag
+from vyos import util
+
+
+class VRRPError(Exception):
+ pass
+
+class VRRPNoData(VRRPError):
+ pass
+
+class VRRP(object):
+ _vrrp_prefix = '00:00:5E:00:01:'
+ location = {
+ 'pid': '/run/keepalived.pid',
+ 'fifo': '/run/keepalived_notify_fifo',
+ 'state': '/tmp/keepalived.data',
+ 'stats': '/tmp/keepalived.stats',
+ 'json': '/tmp/keepalived.json',
+ 'daemon': '/etc/default/keepalived',
+ 'config': '/etc/keepalived/keepalived.conf',
+ 'vyos': '/run/keepalived_config.dict',
+ }
+
+ _signal = {
+ 'state': signal.SIGUSR1,
+ 'stats': signal.SIGUSR2,
+ 'json': signal.SIGRTMIN + 2,
+ }
+
+ _name = {
+ 'state': 'information',
+ 'stats': 'statistics',
+ 'json': 'data',
+ }
+
+ state = {
+ 0: 'INIT',
+ 1: 'BACKUP',
+ 2: 'MASTER',
+ 3: 'FAULT',
+ # UNKNOWN
+ }
+
+ def __init__(self,ifname):
+ self.ifname = ifname
+
+ def enabled(self):
+ return self.ifname in self.active_interfaces()
+
+ @classmethod
+ def active_interfaces(cls):
+ if not os.path.exists(cls.location['pid']):
+ return []
+ data = cls.collect('json')
+ return [group['data']['ifp_ifname'] for group in json.loads(data)]
+
+ @classmethod
+ def decode_state(cls, code):
+ return cls.state.get(code,'UNKNOWN')
+
+ # used in conf mode
+ @classmethod
+ def is_running(cls):
+ if not os.path.exists(cls.location['pid']):
+ return False
+ return util.process_running(cls.location['pid'])
+
+ @classmethod
+ def collect(cls, what):
+ fname = cls.location[what]
+ try:
+ # send signal to generate the configuration file
+ pid = util.read_file(cls.location['pid'])
+ os.kill(int(pid), cls._signal[what])
+
+ # should look for file size change?
+ sleep(0.2)
+ return util.read_file(fname)
+ except FileNotFoundError:
+ raise VRRPNoData("VRRP data is not available (process not running or no active groups)")
+ except Exception:
+ name = cls._name[what]
+ raise VRRPError(f'VRRP {name} is not available')
+ finally:
+ if os.path.exists(fname):
+ os.remove(fname)
+
+ @classmethod
+ def disabled(cls):
+ if not os.path.exists(cls.location['vyos']):
+ return []
+
+ disabled = []
+ config = json.loads(util.read_file(cls.location['vyos']))
+
+ # add disabled groups to the list
+ for group in config['vrrp_groups']:
+ if group['disable']:
+ disabled.append(
+ [group['name'], group['interface'], group['vrid'], 'DISABLED', ''])
+
+ # return list with disabled instances
+ return disabled
+
+ @classmethod
+ def format(cls, data):
+ headers = ["Name", "Interface", "VRID", "State", "Priority", "Last Transition"]
+ groups = []
+
+ data = json.loads(data)
+ for group in data:
+ data = group['data']
+
+ name = data['iname']
+ intf = data['ifp_ifname']
+ vrid = data['vrid']
+ state = cls.decode_state(data["state"])
+ priority = data['effective_priority']
+
+ since = int(time() - float(data['last_transition']))
+ last = util.seconds_to_human(since)
+
+ groups.append([name, intf, vrid, state, priority, last])
+
+ # add to the active list disabled instances
+ groups.extend(cls.disabled())
+ return(tabulate(groups, headers))
+
diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py
new file mode 100644
index 000000000..56ebe01d1
--- /dev/null
+++ b/python/vyos/ifconfig/vti.py
@@ -0,0 +1,31 @@
+# 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 VTIIf(Interface):
+ default = {
+ 'type': 'vti',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'vti',
+ 'prefixes': ['vti', ],
+ },
+ }
diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py
new file mode 100644
index 000000000..60c178b9a
--- /dev/null
+++ b/python/vyos/ifconfig/vtun.py
@@ -0,0 +1,44 @@
+# 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,
+ },
+ }
+
+ # stub this interface is created in the configure script
+
+ def _create(self):
+ # we can not create this interface as it is managed outside
+ # it requires configuring OpenVPN
+ pass
+
+ def _delete(self):
+ # we can not create this interface as it is managed outside
+ # it requires configuring OpenVPN
+ pass
diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py
new file mode 100644
index 000000000..18a500336
--- /dev/null
+++ b/python/vyos/ifconfig/vxlan.py
@@ -0,0 +1,130 @@
+# 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': '',
+ 'source_address': '',
+ 'source_interface': '',
+ 'vni': 0
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'vxlan',
+ 'prefixes': ['vxlan', ],
+ 'bridgeable': True,
+ }
+ }
+ options = Interface.options + \
+ ['group', 'remote', 'source_interface', 'port', 'vni', 'source_address']
+
+ mapping = {
+ 'ifname': 'add',
+ 'vni': 'id',
+ 'port': 'dstport',
+ 'source_address': 'local',
+ 'source_interface': 'dev',
+ }
+
+ def _create(self):
+ cmdline = ['ifname', 'type', 'vni', 'port']
+
+ if self.config['source_address']:
+ cmdline.append('source_address')
+
+ if self.config['remote']:
+ cmdline.append('remote')
+
+ if self.config['group'] or self.config['source_interface']:
+ if self.config['group'] and self.config['source_interface']:
+ cmdline.append('group')
+ cmdline.append('source_interface')
+ else:
+ ifname = self.config['ifname']
+ raise ConfigError(
+ f'VXLAN "{ifname}" is missing mandatory underlay multicast'
+ 'group or source 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)
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
new file mode 100644
index 000000000..fad4ef282
--- /dev/null
+++ b/python/vyos/ifconfig/wireguard.py
@@ -0,0 +1,247 @@
+# 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 hurry.filesize import size
+from hurry.filesize import alternative
+
+from vyos.config import Config
+from vyos.ifconfig import Interface
+from vyos.ifconfig import Operational
+from vyos.validate import is_ipv6
+
+class WireGuardOperational(Operational):
+ 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
+
+ def 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"])
+
+ answer = "interface: {}\n".format(self.config['ifname'])
+ if (description):
+ answer += " description: {}\n".format(description)
+ if (ips):
+ answer += " address: {}\n".format(", ".join(ips))
+
+ answer += " public key: {}\n".format(wgdump['public_key'])
+ answer += " private key: (hidden)\n"
+ answer += " listening port: {}\n".format(wgdump['listen_port'])
+ answer += "\n"
+
+ 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]
+
+ answer += " peer: {}\n".format(peer)
+ answer += " public key: {}\n".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']))
+ answer += " latest handshake: {}\n".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"
+ answer += " status: {}\n".format(status)
+
+ if wgpeer['endpoint'] is not None:
+ answer += " endpoint: {}\n".format(wgpeer['endpoint'])
+
+ if wgpeer['allowed_ips'] is not None:
+ answer += " allowed ips: {}\n".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)
+ answer += " transfer: {} received, {} sent\n".format(
+ rx_size, tx_size)
+
+ if wgpeer['persistent_keepalive'] is not None:
+ answer += " persistent keepalive: every {} seconds\n".format(
+ wgpeer['persistent_keepalive'])
+ answer += '\n'
+ return answer + super().formated_stats()
+
+
+@Interface.register
+class WireGuardIf(Interface):
+ OperationalClass = WireGuardOperational
+
+ default = {
+ 'type': 'wireguard',
+ 'port': 0,
+ 'private_key': None,
+ 'pubkey': None,
+ 'psk': '',
+ 'allowed_ips': [],
+ 'fwmark': 0x00,
+ 'endpoint': None,
+ 'keepalive': 0
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'wireguard',
+ 'prefixes': ['wg', ],
+ 'bridgeable': True,
+ }
+ }
+ options = Interface.options + \
+ ['port', 'private_key', 'pubkey', 'psk',
+ 'allowed_ips', 'fwmark', 'endpoint', 'keepalive']
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # remove no longer associated peers first
+ if 'peer_remove' in config:
+ for tmp in config['peer_remove']:
+ peer = config['peer_remove'][tmp]
+ peer['ifname'] = config['ifname']
+
+ cmd = 'wg set {ifname} peer {pubkey} remove'
+ self._cmd(cmd.format(**peer))
+
+ # Wireguard base command is identical for every peer
+ base_cmd = 'wg set {ifname} private-key {private_key}'
+ if 'port' in config:
+ base_cmd += ' listen-port {port}'
+ if 'fwmark' in config:
+ base_cmd += ' fwmark {fwmark}'
+
+ base_cmd = base_cmd.format(**config)
+
+ for tmp in config['peer']:
+ peer = config['peer'][tmp]
+
+ # start of with a fresh 'wg' command
+ cmd = base_cmd + ' peer {pubkey}'
+
+ # If no PSK is given remove it by using /dev/null - passing keys via
+ # the shell (usually bash) is considered insecure, thus we use a file
+ no_psk_file = '/dev/null'
+ psk_file = no_psk_file
+ if 'preshared_key' in peer:
+ psk_file = '/tmp/tmp.wireguard.psk'
+ with open(psk_file, 'w') as f:
+ f.write(peer['preshared_key'])
+ cmd += f' preshared-key {psk_file}'
+
+ # Persistent keepalive is optional
+ if 'persistent_keepalive'in peer:
+ cmd += ' persistent-keepalive {persistent_keepalive}'
+
+ # Multiple allowed-ip ranges can be defined - ensure we are always
+ # dealing with a list
+ if isinstance(peer['allowed_ips'], str):
+ peer['allowed_ips'] = [peer['allowed_ips']]
+ cmd += ' allowed-ips ' + ','.join(peer['allowed_ips'])
+
+ # Endpoint configuration is optional
+ if {'address', 'port'} <= set(peer):
+ if is_ipv6(config['address']):
+ cmd += ' endpoint [{address}]:{port}'
+ else:
+ cmd += ' endpoint {address}:{port}'
+
+ self._cmd(cmd.format(**peer))
+
+ # PSK key file is not required to be stored persistently as its backed by CLI
+ if psk_file != no_psk_file and os.path.exists(psk_file):
+ os.remove(psk_file)
+
+ # call base class
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
+
diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py
new file mode 100644
index 000000000..a50346ffa
--- /dev/null
+++ b/python/vyos/ifconfig/wireless.py
@@ -0,0 +1,102 @@
+# 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 = Interface.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
+
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
+
+
+@Interface.register
+class WiFiModemIf(WiFiIf):
+ definition = {
+ **WiFiIf.definition,
+ **{
+ 'section': 'wirelessmodem',
+ 'prefixes': ['wlm', ],
+ }
+ }
diff --git a/python/vyos/iflag.py b/python/vyos/iflag.py
new file mode 100644
index 000000000..7ff8e5623
--- /dev/null
+++ b/python/vyos/iflag.py
@@ -0,0 +1,38 @@
+# 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 enum import Enum, unique, IntEnum
+
+
+class IFlag(IntEnum):
+ """ net/if.h interface flags """
+
+ IFF_UP = 0x1 #: Interface up/down status
+ IFF_BROADCAST = 0x2 #: Broadcast address valid
+ IFF_DEBUG = 0x4, #: Debugging
+ IFF_LOOPBACK = 0x8 #: Is loopback network
+ IFF_POINTOPOINT = 0x10 #: Is point-to-point link
+ IFF_NOTRAILERS = 0x20 #: Avoid use of trailers
+ IFF_RUNNING = 0x40 #: Resources allocated
+ IFF_NOARP = 0x80 #: No address resolution protocol
+ IFF_PROMISC = 0x100 #: Promiscuous mode
+ IFF_ALLMULTI = 0x200 #: Receive all multicast
+ IFF_MASTER = 0x400 #: Load balancer master
+ IFF_SLAVE = 0x800 #: Load balancer slave
+ IFF_MULTICAST = 0x1000 #: Supports multicast
+ IFF_PORTSEL = 0x2000 #: Media type adjustable
+ IFF_AUTOMEDIA = 0x4000 #: Automatic media type enabled
+ IFF_DYNAMIC = 0x8000 #: Is a dial-up device with dynamic address
+
diff --git a/python/vyos/initialsetup.py b/python/vyos/initialsetup.py
new file mode 100644
index 000000000..574e7892d
--- /dev/null
+++ b/python/vyos/initialsetup.py
@@ -0,0 +1,72 @@
+# initialsetup -- functions for setting common values in config file,
+# for use in installation and first boot scripts
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import vyos.configtree
+import vyos.authutils
+
+def set_interface_address(config, intf, addr, intf_type="ethernet"):
+ config.set(["interfaces", intf_type, intf, "address"], value=addr)
+ config.set_tag(["interfaces", intf_type])
+
+def set_host_name(config, hostname):
+ config.set(["system", "host-name"], value=hostname)
+
+def set_name_servers(config, servers):
+ for s in servers:
+ config.set(["system", "name-server"], replace=False, value=s)
+
+def set_default_gateway(config, gateway):
+ config.set(["protocols", "static", "route", "0.0.0.0/0", "next-hop", gateway])
+ config.set_tag(["protocols", "static", "route"])
+ config.set_tag(["protocols", "static", "route", "0.0.0.0/0", "next-hop"])
+
+def set_user_password(config, user, password):
+ # Make a password hash
+ hash = vyos.authutils.make_password_hash(password)
+
+ config.set(["system", "login", "user", user, "authentication", "encrypted-password"], value=hash)
+ config.set(["system", "login", "user", user, "authentication", "plaintext-password"], value="")
+
+def disable_user_password(config, user):
+ config.set(["system", "login", "user", user, "authentication", "encrypted-password"], value="!")
+ config.set(["system", "login", "user", user, "authentication", "plaintext-password"], value="")
+
+def set_user_level(config, user, level):
+ config.set(["system", "login", "user", user, "level"], value=level)
+
+def set_user_ssh_key(config, user, key_string):
+ key = vyos.authutils.split_ssh_public_key(key_string, defaultname=user)
+
+ config.set(["system", "login", "user", user, "authentication", "public-keys", key["name"], "key"], value=key["data"])
+ config.set(["system", "login", "user", user, "authentication", "public-keys", key["name"], "type"], value=key["type"])
+ config.set_tag(["system", "login", "user", user, "authentication", "public-keys"])
+
+def create_user(config, user, password=None, key=None, level="admin"):
+ config.set(["system", "login", "user", user])
+ config.set_tag(["system", "login", "user", user])
+
+ if not key and not password:
+ raise ValueError("Must set at least password or SSH public key")
+
+ if password:
+ set_user_password(config, user, password)
+ else:
+ disable_user_password(config, user)
+
+ if key:
+ set_user_ssh_key(config, user, key)
+
+ set_user_level(config, user, level)
diff --git a/python/vyos/ioctl.py b/python/vyos/ioctl.py
new file mode 100644
index 000000000..cfa75aac6
--- /dev/null
+++ b/python/vyos/ioctl.py
@@ -0,0 +1,36 @@
+# 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 sys
+import os
+import socket
+import fcntl
+import struct
+
+SIOCGIFFLAGS = 0x8913
+
+def get_terminal_size():
+ """ pull the terminal size """
+ """ rows,cols = vyos.ioctl.get_terminal_size() """
+ columns, rows = os.get_terminal_size(0)
+ return (rows,columns)
+
+def get_interface_flags(intf):
+ """ Pull the SIOCGIFFLAGS """
+ nullif = '\0'*256
+ 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/limericks.py b/python/vyos/limericks.py
new file mode 100644
index 000000000..e03ccd32b
--- /dev/null
+++ b/python/vyos/limericks.py
@@ -0,0 +1,72 @@
+# Copyright 2015, 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 random
+
+limericks = [
+
+"""
+A programmer who's name was Searle
+Once wrote a long program in Perl.
+Despite very few quirks
+No one got how it works,
+Not even the interpreter.
+""",
+
+"""
+There was a young lady of Maine
+Who set up IPsec VPN.
+Problems didn't arise
+'til other vendors' device
+had to add she to that VPN.
+""",
+
+"""
+One day a programmer from York
+started his own Vyatta fork.
+Though he was a huge geek,
+it still took him a week
+to get the damn build scripts to work.
+""",
+
+"""
+A network admin from Hong Kong
+knew MPPE cipher's not strong.
+But he was behind NAT,
+so he put up we that,
+sad network admin from Hong Kong.
+""",
+
+"""
+A network admin named Drake
+greeted friends with a three-way handshake
+and refused to proceed
+if they didn't complete it,
+that standards-compliant guy Drake.
+""",
+
+"""
+A network admin from Nantucket
+used hierarchy token buckets.
+Bandwidth limits he set
+slowed down his net,
+users drove him away from Nantucket.
+"""
+
+]
+
+
+def get_random():
+ return limericks[random.randint(0, len(limericks) - 1)]
diff --git a/python/vyos/logger.py b/python/vyos/logger.py
new file mode 100644
index 000000000..f7cc964d5
--- /dev/null
+++ b/python/vyos/logger.py
@@ -0,0 +1,143 @@
+# 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/>.
+
+# A wrapper class around logging to make it easier to use
+
+# for a syslog logger:
+# from vyos.logger import syslog
+# syslog.critical('message')
+
+# for a stderr logger:
+# from vyos.logger import stderr
+# stderr.critical('message')
+
+# for a custom logger (syslog and file):
+# from vyos.logger import getLogger
+# combined = getLogger(__name__, syslog=True, stream=sys.stdout, filename='/tmp/test')
+# combined.critical('message')
+
+import sys
+import logging
+import logging.handlers as handlers
+
+TIMED = '%(asctime)s: %(message)s'
+SHORT = '%(filename)s: %(message)s'
+CLEAR = '%(levelname) %(asctime)s %(filename)s: %(message)s'
+
+_levels = {
+ 'CRITICAL': logging.CRITICAL,
+ 'ERROR': logging.CRITICAL,
+ 'WARNING': logging.WARNING,
+ 'INFO': logging.INFO,
+ 'DEBUG': logging.DEBUG,
+ 'NOTSET': logging.NOTSET,
+}
+
+# prevent recreation of already created logger
+_created = {}
+
+def getLogger(name=None, **kwargs):
+ if name in _created:
+ if len(kwargs) == 0:
+ return _created[name]
+ raise ValueError('a logger with the name "{name} already exists')
+
+ logger = logging.getLogger(name)
+ logger.setLevel(_levels[kwargs.get('level', 'DEBUG')])
+
+ if 'address' in kwargs or kwargs.get('syslog', False):
+ logger.addHandler(_syslog(**kwargs))
+ if 'stream' in kwargs:
+ logger.addHandler(_stream(**kwargs))
+ if 'filename' in kwargs:
+ logger.addHandler(_file(**kwargs))
+
+ _created[name] = logger
+ return logger
+
+
+def _syslog(**kwargs):
+ formating = kwargs.get('format', SHORT)
+ handler = handlers.SysLogHandler(
+ address=kwargs.get('address', '/dev/log'),
+ facility=kwargs.get('facility', 'syslog'),
+ )
+ handler.setFormatter(logging.Formatter(formating))
+ return handler
+
+
+def _stream(**kwargs):
+ formating = kwargs.get('format', CLEAR)
+ handler = logging.StreamHandler(
+ stream=kwargs.get('stream', sys.stderr),
+ )
+ handler.setFormatter(logging.Formatter(formating))
+ return handler
+
+
+def _file(**kwargs):
+ formating = kwargs.get('format', CLEAR)
+ handler = handlers.RotatingFileHandler(
+ filename=kwargs.get('filename', 1048576),
+ maxBytes=kwargs.get('maxBytes', 1048576),
+ backupCount=kwargs.get('backupCount', 3),
+ )
+ handler.setFormatter(logging.Formatter(formating))
+ return handler
+
+
+# exported pre-built logger, please keep in mind that the names
+# must be unique otherwise the logger are shared
+
+# a logger for stderr
+stderr = getLogger(
+ 'VyOS Syslog',
+ format=SHORT,
+ stream=sys.stderr,
+ address='/dev/log'
+)
+
+# a logger to syslog
+syslog = getLogger(
+ 'VyOS StdErr',
+ format='%(message)s',
+ address='/dev/log'
+)
+
+
+# testing
+if __name__ == '__main__':
+ # from vyos.logger import getLogger
+ formating = '%(asctime)s (%(filename)s) %(levelname)s: %(message)s'
+
+ # syslog logger
+ # syslog=True if no 'address' field is provided
+ syslog = getLogger(__name__ + '.1', syslog=True, format=formating)
+ syslog.info('syslog test')
+
+ # steam logger
+ stream = getLogger(__name__ + '.2', stream=sys.stdout, level='ERROR')
+ stream.info('steam test')
+
+ # file logger
+ filelog = getLogger(__name__ + '.3', filename='/tmp/test')
+ filelog.info('file test')
+
+ # create a combined logger
+ getLogger('VyOS', syslog=True, stream=sys.stdout, filename='/tmp/test')
+
+ # recover the created logger from name
+ combined = getLogger('VyOS')
+ combined.info('combined test')
diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py
new file mode 100644
index 000000000..9a5fdef2f
--- /dev/null
+++ b/python/vyos/migrator.py
@@ -0,0 +1,220 @@
+# 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 sys
+import os
+import subprocess
+import vyos.version
+import vyos.defaults
+import vyos.systemversions as systemversions
+import vyos.formatversions as formatversions
+
+class MigratorError(Exception):
+ pass
+
+class Migrator(object):
+ def __init__(self, config_file, force=False, set_vintage='vyos'):
+ self._config_file = config_file
+ self._force = force
+ self._set_vintage = set_vintage
+ self._config_file_vintage = None
+ self._log_file = None
+ self._changed = False
+
+ def read_config_file_versions(self):
+ """
+ Get component versions from config file footer and set vintage;
+ return empty dictionary if config string is missing.
+ """
+ cfg_file = self._config_file
+ component_versions = {}
+
+ cfg_versions = formatversions.read_vyatta_versions(cfg_file)
+
+ if cfg_versions:
+ self._config_file_vintage = 'vyatta'
+ component_versions = cfg_versions
+
+ cfg_versions = formatversions.read_vyos_versions(cfg_file)
+
+ if cfg_versions:
+ self._config_file_vintage = 'vyos'
+ component_versions = cfg_versions
+
+ return component_versions
+
+ def update_vintage(self):
+ old_vintage = self._config_file_vintage
+
+ if self._set_vintage:
+ self._config_file_vintage = self._set_vintage
+
+ if self._config_file_vintage not in ['vyatta', 'vyos']:
+ raise MigratorError("Unknown vintage.")
+
+ if self._config_file_vintage == old_vintage:
+ return False
+ else:
+ return True
+
+ def open_log_file(self):
+ """
+ Open log file for migration, catching any error.
+ Note that, on boot, migration takes place before the canonical log
+ directory is created, hence write to the config file directory.
+ """
+ self._log_file = os.path.join(vyos.defaults.directories['config'],
+ 'vyos-migrate.log')
+ # on creation, allow write permission for cfg_group;
+ # restore original umask on exit
+ mask = os.umask(0o113)
+ try:
+ log = open('{0}'.format(self._log_file), 'w')
+ log.write("List of executed migration scripts:\n")
+ except Exception as e:
+ os.umask(mask)
+ print("Logging error: {0}".format(e))
+ return None
+
+ os.umask(mask)
+ return log
+
+ def run_migration_scripts(self, config_file_versions, system_versions):
+ """
+ Run migration scripts iteratively, until config file version equals
+ system component version.
+ """
+ log = self.open_log_file()
+
+ cfg_versions = config_file_versions
+ sys_versions = system_versions
+
+ sys_keys = list(sys_versions.keys())
+ sys_keys.sort()
+
+ rev_versions = {}
+
+ for key in sys_keys:
+ sys_ver = sys_versions[key]
+ if key in cfg_versions:
+ cfg_ver = cfg_versions[key]
+ else:
+ cfg_ver = 0
+
+ migrate_script_dir = os.path.join(
+ vyos.defaults.directories['migrate'], key)
+
+ while cfg_ver < sys_ver:
+ next_ver = cfg_ver + 1
+
+ migrate_script = os.path.join(migrate_script_dir,
+ '{}-to-{}'.format(cfg_ver, next_ver))
+
+ try:
+ subprocess.check_call([migrate_script,
+ self._config_file])
+ except FileNotFoundError:
+ pass
+ except Exception as err:
+ print("\nMigration script error: {0}: {1}."
+ "".format(migrate_script, err))
+ sys.exit(1)
+
+ if log:
+ try:
+ log.write('{0}\n'.format(migrate_script))
+ except Exception as e:
+ print("Error writing log: {0}".format(e))
+
+ cfg_ver = next_ver
+
+ rev_versions[key] = cfg_ver
+
+ if log:
+ log.close()
+
+ return rev_versions
+
+ def write_config_file_versions(self, cfg_versions):
+ """
+ Write new versions string.
+ """
+ versions_string = formatversions.format_versions_string(cfg_versions)
+
+ os_version_string = vyos.version.get_version()
+
+ if self._config_file_vintage == 'vyatta':
+ formatversions.write_vyatta_versions_foot(self._config_file,
+ versions_string,
+ os_version_string)
+
+ if self._config_file_vintage == 'vyos':
+ formatversions.write_vyos_versions_foot(self._config_file,
+ versions_string,
+ os_version_string)
+
+ def run(self):
+ """
+ Gather component versions from config file and system.
+ Run migration scripts.
+ Update vintage ('vyatta' or 'vyos'), if needed.
+ If changed, remove old versions string from config file, and
+ write new versions string.
+ """
+ cfg_file = self._config_file
+
+ cfg_versions = self.read_config_file_versions()
+ if self._force:
+ # This will force calling all migration scripts:
+ cfg_versions = {}
+
+ sys_versions = systemversions.get_system_versions()
+
+ rev_versions = self.run_migration_scripts(cfg_versions, sys_versions)
+
+ if rev_versions != cfg_versions:
+ self._changed = True
+
+ if self.update_vintage():
+ self._changed = True
+
+ if not self._changed:
+ return
+
+ formatversions.remove_versions(cfg_file)
+
+ self.write_config_file_versions(rev_versions)
+
+ def config_changed(self):
+ return self._changed
+
+class VirtualMigrator(Migrator):
+ def run(self):
+ cfg_file = self._config_file
+
+ cfg_versions = self.read_config_file_versions()
+ if not cfg_versions:
+ return
+
+ if self.update_vintage():
+ self._changed = True
+
+ if not self._changed:
+ return
+
+ formatversions.remove_versions(cfg_file)
+
+ self.write_config_file_versions(cfg_versions)
+
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
new file mode 100644
index 000000000..3f46d979b
--- /dev/null
+++ b/python/vyos/remote.py
@@ -0,0 +1,143 @@
+# 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 sys
+import os
+import re
+import fileinput
+
+from vyos.util import cmd
+from vyos.util import DEVNULL
+
+
+def check_and_add_host_key(host_name):
+ """
+ Filter host keys and prompt for adding key to known_hosts file, if
+ needed.
+ """
+ known_hosts = '{}/.ssh/known_hosts'.format(os.getenv('HOME'))
+ if not os.path.exists(known_hosts):
+ mode = 0o600
+ os.mknod(known_hosts, 0o600)
+
+ keyscan_cmd = 'ssh-keyscan -t rsa {}'.format(host_name)
+
+ try:
+ host_key = cmd(keyscan_cmd, stderr=DEVNULL)
+ except OSError:
+ sys.exit("Can not get RSA host key")
+
+ # libssh2 (jessie; stretch) does not recognize ec host keys, and curl
+ # will fail with error 51 if present in known_hosts file; limit to rsa.
+ usable_keys = False
+ offending_keys = []
+ for line in fileinput.input(known_hosts, inplace=True):
+ if host_name in line and 'ssh-rsa' in line:
+ if line.split()[-1] != host_key.split()[-1]:
+ offending_keys.append(line)
+ continue
+ else:
+ usable_keys = True
+ if host_name in line and not 'ssh-rsa' in line:
+ continue
+
+ sys.stdout.write(line)
+
+ if usable_keys:
+ return
+
+ if offending_keys:
+ print("Host key has changed!")
+ print("If you trust the host key fingerprint below, continue.")
+
+ fingerprint_cmd = 'ssh-keygen -lf /dev/stdin'
+ try:
+ fingerprint = cmd(fingerprint_cmd, stderr=DEVNULL, input=host_key)
+ except OSError:
+ sys.exit("Can not get RSA host key fingerprint.")
+
+ print("RSA host key fingerprint is {}".format(fingerprint.split()[1]))
+ response = input("Do you trust this host? [y]/n ")
+
+ if not response or response == 'y':
+ with open(known_hosts, 'a+') as f:
+ print("Adding {} to the list of known"
+ " hosts.".format(host_name))
+ f.write(host_key)
+ else:
+ sys.exit("Host not trusted")
+
+def get_remote_config(remote_file):
+ """ Invoke curl to download remote (config) file.
+
+ Args:
+ remote file URI:
+ scp://<user>[:<passwd>]@<host>/<file>
+ sftp://<user>[:<passwd>]@<host>/<file>
+ http://<host>/<file>
+ https://<host>/<file>
+ ftp://<user>[:<passwd>]@<host>/<file>
+ tftp://<host>/<file>
+ """
+ request = dict.fromkeys(['protocol', 'user', 'host', 'file'])
+ protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp']
+ or_protocols = '|'.join(protocols)
+
+ request_match = re.match(r'(' + or_protocols + r')://(.*?)(/.*)',
+ remote_file)
+ if request_match:
+ (request['protocol'], request['host'],
+ request['file']) = request_match.groups()
+ else:
+ print("Malformed URI")
+ sys.exit(1)
+
+ user_match = re.search(r'(.*)@(.*)', request['host'])
+ if user_match:
+ request['user'] = user_match.groups()[0]
+ request['host'] = user_match.groups()[1]
+
+ remote_file = '{0}://{1}{2}'.format(request['protocol'], request['host'], request['file'])
+
+ if request['protocol'] in ('scp', 'sftp'):
+ check_and_add_host_key(request['host'])
+
+ redirect_opt = ''
+
+ if request['protocol'] in ('http', 'https'):
+ redirect_opt = '-L'
+ # 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 = cmd(curl_cmd)
+ except OSError:
+ sys.exit(1)
+
+ return_vals = re.findall(r'^HTTP\/\d+\.?\d\s+(\d+)\s+(.*)$',
+ curl_output, re.MULTILINE)
+ for val in return_vals:
+ if int(val[0]) not in [200, 301, 302]:
+ print('HTTP error: {0} {1}'.format(*val))
+ sys.exit(1)
+
+ if request['user']:
+ curl_cmd = 'curl -# -u {0} {1}'.format(request['user'], remote_file)
+ else:
+ curl_cmd = 'curl {0} -# {1}'.format(redirect_opt, remote_file)
+
+ try:
+ return cmd(curl_cmd, stderr=None)
+ except OSError:
+ return None
diff --git a/python/vyos/snmpv3_hashgen.py b/python/vyos/snmpv3_hashgen.py
new file mode 100644
index 000000000..324c3274d
--- /dev/null
+++ b/python/vyos/snmpv3_hashgen.py
@@ -0,0 +1,50 @@
+# 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/>.
+
+# Documentation / Inspiration
+# - https://tools.ietf.org/html/rfc3414#appendix-A.3
+# - https://github.com/TheMysteriousX/SNMPv3-Hash-Generator
+
+key_length = 1048576
+
+def random(l):
+ # os.urandom(8) returns 8 bytes of random data
+ import os
+ from binascii import hexlify
+ return hexlify(os.urandom(l)).decode('utf-8')
+
+def expand(s, l):
+ """ repead input string (s) as long as we reach the desired length in bytes """
+ from itertools import repeat
+ reps = l // len(s) + 1 # approximation; worst case: overrun = l + len(s)
+ return ''.join(list(repeat(s, reps)))[:l].encode('utf-8')
+
+def plaintext_to_md5(passphrase, engine):
+ """ Convert input plaintext passphrase to MD5 hashed version usable by net-snmp """
+ from hashlib import md5
+ tmp = expand(passphrase, key_length)
+ hash = md5(tmp).digest()
+ engine = bytearray.fromhex(engine)
+ out = b''.join([hash, engine, hash])
+ return md5(out).digest().hex()
+
+def plaintext_to_sha1(passphrase, engine):
+ """ Convert input plaintext passphrase to SHA1hashed version usable by net-snmp """
+ from hashlib import sha1
+ tmp = expand(passphrase, key_length)
+ hash = sha1(tmp).digest()
+ engine = bytearray.fromhex(engine)
+ out = b''.join([hash, engine, hash])
+ return sha1(out).digest().hex()
diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py
new file mode 100644
index 000000000..5c4deca29
--- /dev/null
+++ b/python/vyos/systemversions.py
@@ -0,0 +1,63 @@
+# 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 sys
+import json
+
+import vyos.defaults
+
+def get_system_versions():
+ """
+ Get component versions from running system: read vyatta directory
+ structure for versions, then read vyos JSON file. It is a critical
+ error if either migration directory or JSON file is unreadable.
+ """
+ system_versions = {}
+
+ try:
+ version_info = os.listdir(vyos.defaults.directories['current'])
+ except OSError as err:
+ print("OS error: {}".format(err))
+ sys.exit(1)
+
+ for info in version_info:
+ if re.match(r'[\w,-]+@\d+', info):
+ pair = info.split('@')
+ system_versions[pair[0]] = int(pair[1])
+
+ version_dict = {}
+ path = vyos.defaults.version_file
+
+ if os.path.isfile(path):
+ with open(path, 'r') as f:
+ try:
+ version_dict = json.load(f)
+ except ValueError as err:
+ print(f"\nValue error in {path}: {err}")
+ sys.exit(1)
+
+ for k, v in version_dict.items():
+ if not isinstance(v, int):
+ print(f"\nType error in {path}; expecting Dict[str, int]")
+ sys.exit(1)
+ existing = system_versions.get(k)
+ if existing is None:
+ system_versions[k] = v
+ elif v > existing:
+ system_versions[k] = v
+
+ return system_versions
diff --git a/python/vyos/template.py b/python/vyos/template.py
new file mode 100644
index 000000000..c88ab04a0
--- /dev/null
+++ b/python/vyos/template.py
@@ -0,0 +1,143 @@
+# 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 functools
+import os
+
+from ipaddress import ip_network
+from jinja2 import Environment
+from jinja2 import FileSystemLoader
+from vyos.defaults import directories
+from vyos.util import chmod, chown, makedir
+
+
+# Holds template filters registered via register_filter()
+_FILTERS = {}
+
+
+# reuse Environments with identical trim_blocks setting to improve performance
+@functools.lru_cache(maxsize=2)
+def _get_environment(trim_blocks):
+ env = Environment(
+ # Don't check if template files were modified upon re-rendering
+ auto_reload=False,
+ # Cache up to this number of templates for quick re-rendering
+ cache_size=100,
+ loader=FileSystemLoader(directories["templates"]),
+ trim_blocks=trim_blocks,
+ )
+ env.filters.update(_FILTERS)
+ return env
+
+
+def register_filter(name, func=None):
+ """Register a function to be available as filter in templates under given name.
+
+ It can also be used as a decorator, see below in this module for examples.
+
+ :raise RuntimeError:
+ when trying to register a filter after a template has been rendered already
+ :raise ValueError: when trying to register a name which was taken already
+ """
+ if func is None:
+ return functools.partial(register_filter, name)
+ if _get_environment.cache_info().currsize:
+ raise RuntimeError(
+ "Filters can only be registered before rendering the first template"
+ )
+ if name in _FILTERS:
+ raise ValueError(f"A filter with name {name!r} was registered already")
+ _FILTERS[name] = func
+ return func
+
+
+def render_to_string(template, content, trim_blocks=False, formater=None):
+ """Render a template from the template directory, raise on any errors.
+
+ :param template: the path to the template relative to the template folder
+ :param content: the dictionary of variables to put into rendering context
+ :param trim_blocks: controls the trim_blocks jinja2 feature
+ :param formater:
+ if given, it has to be a callable the rendered string is passed through
+
+ The parsed template files are cached, so rendering the same file multiple times
+ does not cause as too much overhead.
+ If used everywhere, it could be changed to load the template from Python
+ environment variables from an importable Python module generated when the Debian
+ package is build (recovering the load time and overhead caused by having the
+ file out of the code).
+ """
+ template = _get_environment(bool(trim_blocks)).get_template(template)
+ rendered = template.render(content)
+ if formater is not None:
+ rendered = formater(rendered)
+ return rendered
+
+
+def render(
+ destination,
+ template,
+ content,
+ trim_blocks=False,
+ formater=None,
+ permission=None,
+ user=None,
+ group=None,
+):
+ """Render a template from the template directory to a file, raise on any errors.
+
+ :param destination: path to the file to save the rendered template in
+ :param permission: permission bitmask to set for the output file
+ :param user: user to own the output file
+ :param group: group to own the output file
+
+ All other parameters are as for :func:`render_to_string`.
+ """
+ # Create the directory if it does not exist
+ folder = os.path.dirname(destination)
+ makedir(folder, user, group)
+
+ # As we are opening the file with 'w', we are performing the rendering before
+ # calling open() to not accidentally erase the file if rendering fails
+ rendered = render_to_string(template, content, trim_blocks, formater)
+
+ # Write to file
+ with open(destination, "w") as file:
+ chmod(file.fileno(), permission)
+ chown(file.fileno(), user, group)
+ file.write(rendered)
+
+
+##################################
+# Custom template filters follow #
+##################################
+
+
+@register_filter("address_from_cidr")
+def vyos_address_from_cidr(text):
+ """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address".
+ Example:
+ 192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8::
+ """
+ return ip_network(text).network_address
+
+
+@register_filter("netmask_from_cidr")
+def vyos_netmask_from_cidr(text):
+ """ Take an IPv4/IPv6 CIDR prefix and convert the prefix length to a "subnet mask".
+ Example:
+ 192.0.2.0/24 -> 255.255.255.0, 2001:db8::/48 -> ffff:ffff:ffff::
+ """
+ return ip_network(text).netmask
diff --git a/python/vyos/util.py b/python/vyos/util.py
new file mode 100644
index 000000000..84aa16791
--- /dev/null
+++ b/python/vyos/util.py
@@ -0,0 +1,688 @@
+# 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 re
+import sys
+
+#
+# NOTE: Do not import full classes here, move your import to the function
+# where it is used so it is as local as possible to the execution
+#
+
+
+def _need_sudo(command):
+ return os.path.basename(command.split()[0]) in ('systemctl', )
+
+
+def _add_sudo(command):
+ if _need_sudo(command):
+ return 'sudo ' + command
+ return command
+
+
+from subprocess import Popen
+from subprocess import PIPE
+from subprocess import STDOUT
+from subprocess import DEVNULL
+
+
+def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True):
+ """
+ popen is a wrapper helper aound subprocess.Popen
+ with it default setting it will return a tuple (out, err)
+ out: the output of the program run
+ err: the error code returned by the program
+
+ it can be affected by the following flags:
+ shell: do not try to auto-detect if a shell is required
+ for example if a pipe (|) or redirection (>, >>) is used
+ input: data to sent to the child process via STDIN
+ the data should be bytes but string will be converted
+ timeout: time after which the command will be considered to have failed
+ env: mapping that defines the environment variables for the new process
+ stdout: define how the output of the program should be handled
+ - PIPE (default), sends stdout to the output
+ - DEVNULL, discard the output
+ stderr: define how the output of the program should be handled
+ - None (default), send/merge the data to/with stderr
+ - PIPE, popen will append it to output
+ - STDOUT, send the data to be merged with stdout
+ - DEVNULL, discard the output
+ decode: specify the expected text encoding (utf-8, ascii, ...)
+ the default is explicitely utf-8 which is python's own default
+
+ usage:
+ get both stdout and stderr: popen('command', stdout=PIPE, stderr=STDOUT)
+ discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE)
+ """
+
+ # airbag must be left as an import in the function as otherwise we have a
+ # a circual import dependency
+ from vyos import debug
+ from vyos import airbag
+
+ # log if the flag is set, otherwise log if command is set
+ if not debug.enabled(flag):
+ flag = 'command'
+
+ if autosudo:
+ command = _add_sudo(command)
+
+ cmd_msg = f"cmd '{command}'"
+ debug.message(cmd_msg, flag)
+
+ use_shell = shell
+ stdin = None
+ if shell is None:
+ use_shell = False
+ if ' ' in command:
+ use_shell = True
+ if env:
+ use_shell = True
+
+ if input:
+ stdin = PIPE
+ input = input.encode() if type(input) is str else input
+
+ p = Popen(
+ command,
+ stdin=stdin, stdout=stdout, stderr=stderr,
+ env=env, shell=use_shell,
+ )
+
+ pipe = p.communicate(input, timeout)
+
+ pipe_out = b''
+ if stdout == PIPE:
+ pipe_out = pipe[0]
+
+ pipe_err = b''
+ if stderr == PIPE:
+ pipe_err = pipe[1]
+
+ str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip()
+ str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip()
+
+ out_msg = f"returned (out):\n{str_out}"
+ if str_out:
+ debug.message(out_msg, flag)
+
+ if str_err:
+ err_msg = f"returned (err):\n{str_err}"
+ # this message will also be send to syslog via airbag
+ debug.message(err_msg, flag, destination=sys.stderr)
+
+ # should something go wrong, report this too via airbag
+ airbag.noteworthy(cmd_msg)
+ airbag.noteworthy(out_msg)
+ airbag.noteworthy(err_msg)
+
+ return str_out, p.returncode
+
+
+def run(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=DEVNULL, stderr=PIPE, decode='utf-8', autosudo=True):
+ """
+ A wrapper around popen, which discard the stdout and
+ will return the error code of a command
+ """
+ _, code = popen(
+ command, flag,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ decode=decode,
+ )
+ return code
+
+
+def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True,
+ raising=None, message='', expect=[0]):
+ """
+ A wrapper around popen, which returns the stdout and
+ will raise the error code of a command
+
+ raising: specify which call should be used when raising
+ the class should only require a string as parameter
+ (default is OSError) with the error code
+ expect: a list of error codes to consider as normal
+ """
+ decoded, code = popen(
+ command, flag,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ decode=decode,
+ )
+ if code not in expect:
+ 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
+
+
+def call(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True):
+ """
+ A wrapper around popen, which print the stdout and
+ will return the error code of a command
+ """
+ out, code = popen(
+ command, flag,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ decode=decode,
+ )
+ if out:
+ print(out)
+ return code
+
+
+def read_file(fname, defaultonfailure=None):
+ """
+ read the content of a file, stripping any end characters (space, newlines)
+ should defaultonfailure be not None, it is returned on failure to read
+ """
+ try:
+ """ Read a file to string """
+ with open(fname, 'r') as f:
+ data = f.read().strip()
+ return data
+ except Exception as e:
+ if defaultonfailure is not None:
+ return defaultonfailure
+ raise e
+
+
+def read_json(fname, defaultonfailure=None):
+ """
+ read and json decode the content of a file
+ should defaultonfailure be not None, it is returned on failure to read
+ """
+ import json
+ try:
+ with open(fname, 'r') as f:
+ data = json.load(f)
+ return data
+ except Exception as e:
+ if defaultonfailure is not None:
+ return defaultonfailure
+ raise e
+
+
+def chown(path, user, group):
+ """ change file/directory owner """
+ from pwd import getpwnam
+ from grp import getgrnam
+
+ if user is None or group is None:
+ return False
+
+ # path may also be an open file descriptor
+ if not isinstance(path, int) and not os.path.exists(path):
+ return False
+
+ uid = getpwnam(user).pw_uid
+ gid = getgrnam(group).gr_gid
+ os.chown(path, uid, gid)
+ return True
+
+
+def chmod(path, bitmask):
+ # path may also be an open file descriptor
+ if not isinstance(path, int) and not os.path.exists(path):
+ return
+ if bitmask is None:
+ return
+ os.chmod(path, bitmask)
+
+
+def chmod_600(path):
+ """ make file only read/writable by owner """
+ from stat import S_IRUSR, S_IWUSR
+
+ bitmask = S_IRUSR | S_IWUSR
+ chmod(path, bitmask)
+
+
+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
+
+ bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP
+ chmod(path, bitmask)
+
+
+def chmod_755(path):
+ """ make file executable by all """
+ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH
+
+ bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \
+ S_IROTH | S_IXOTH
+ chmod(path, bitmask)
+
+
+def makedir(path, user=None, group=None):
+ if os.path.exists(path):
+ return
+ os.mkdir(path)
+ chown(path, user, group)
+
+
+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.
+
+ Such files are common in Linux /proc filesystem
+
+ Args:
+ data_string (str): data string
+ uniquekeys (bool): whether to insist that keys are unique or not
+
+ Returns: dict
+
+ Raises:
+ ValueError: if uniquekeys=True and the data string has
+ duplicate keys.
+
+ Note:
+ If uniquekeys=True, then dict entries are always strings,
+ otherwise they are always lists of strings.
+ """
+ import re
+ key_value_re = re.compile('([^:]+)\s*\:\s*(.*)')
+
+ data_raw = re.split('\n', data_string)
+
+ data = {}
+
+ for l in data_raw:
+ l = l.strip()
+ if l:
+ match = re.match(key_value_re, l)
+ if match:
+ key = match.groups()[0].strip()
+ value = match.groups()[1].strip()
+ if key in data.keys():
+ if uniquekeys:
+ raise ValueError("Data string has duplicate keys: {0}".format(key))
+ else:
+ data[key].append(value)
+ else:
+ if uniquekeys:
+ data[key] = value
+ else:
+ data[key] = [value]
+ else:
+ pass
+
+ return data
+
+def mangle_dict_keys(data, regex, replacement):
+ """ Mangles dict keys according to a regex and replacement character.
+ Some libraries like Jinja2 do not like certain characters in dict keys.
+ This function can be used for replacing all offending characters
+ with something acceptable.
+
+ Args:
+ data (dict): Original dict to mangle
+
+ Returns: dict
+ """
+ new_dict = {}
+ for key in data.keys():
+ new_key = re.sub(regex, replacement, key)
+
+ value = data[key]
+ if isinstance(value, dict):
+ new_dict[new_key] = mangle_dict_keys(value, regex, replacement)
+ else:
+ new_dict[new_key] = value
+
+ return new_dict
+
+def _get_sub_dict(d, lpath):
+ k = lpath[0]
+ if k not in d.keys():
+ return {}
+ c = {k: d[k]}
+ lpath = lpath[1:]
+ if not lpath:
+ return c
+ elif not isinstance(c[k], dict):
+ return {}
+ return _get_sub_dict(c[k], lpath)
+
+def get_sub_dict(source, lpath, get_first_key=False):
+ """ Returns the sub-dict of a nested dict, defined by path of keys.
+
+ Args:
+ source (dict): Source dict to extract from
+ lpath (list[str]): sequence of keys
+
+ Returns: source, if lpath is empty, else
+ {key : source[..]..[key]} for key the last element of lpath, if exists
+ {} otherwise
+ """
+ if not isinstance(source, dict):
+ raise TypeError("source must be of type dict")
+ if not isinstance(lpath, list):
+ raise TypeError("path must be of type list")
+ if not lpath:
+ return source
+
+ ret = _get_sub_dict(source, lpath)
+
+ if get_first_key and lpath and ret:
+ tmp = next(iter(ret.values()))
+ if not isinstance(tmp, dict):
+ raise TypeError("Data under node is not of type dict")
+ ret = tmp
+
+ return ret
+
+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 pid_exists(int(pid))
+
+
+def process_named_running(name):
+ """ Checks if process with given name is running and returns its PID.
+ If Process is not running, return None
+ """
+ from psutil import process_iter
+ for p in process_iter():
+ if name in p.name():
+ return p.pid
+ return None
+
+
+def seconds_to_human(s, separator=""):
+ """ Converts number of seconds passed to a human-readable
+ interval such as 1w4d18h35m59s
+ """
+ s = int(s)
+
+ week = 60 * 60 * 24 * 7
+ day = 60 * 60 * 24
+ hour = 60 * 60
+
+ remainder = 0
+ result = ""
+
+ weeks = s // week
+ if weeks > 0:
+ result = "{0}w".format(weeks)
+ s = s % week
+
+ days = s // day
+ if days > 0:
+ result = "{0}{1}{2}d".format(result, separator, days)
+ s = s % day
+
+ hours = s // hour
+ if hours > 0:
+ result = "{0}{1}{2}h".format(result, separator, hours)
+ s = s % hour
+
+ minutes = s // 60
+ if minutes > 0:
+ result = "{0}{1}{2}m".format(result, separator, minutes)
+ s = s % 60
+
+ seconds = s
+ if seconds > 0:
+ result = "{0}{1}{2}s".format(result, separator, seconds)
+
+ return result
+
+
+def get_cfg_group_id():
+ 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):
+ import re
+ location = r'^(/config|/opt/vyatta/etc/config)'
+ absolute = os.path.abspath(os.path.dirname(path))
+ return re.match(location,absolute)
+
+
+def commit_in_progress():
+ """ Not to be used in normal op mode scripts! """
+
+ # The CStore backend locks the config by opening a file
+ # The file is not removed after commit, so just checking
+ # if it exists is insufficient, we need to know if it's open by anyone
+
+ # There are two ways to check if any other process keeps a file open.
+ # The first one is to try opening it and see if the OS objects.
+ # That's faster but prone to race conditions and can be intrusive.
+ # The other one is to actually check if any process keeps it open.
+ # It's non-intrusive but needs root permissions, else you can't check
+ # processes of other users.
+ #
+ # 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.
+ 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 process_iter():
+ try:
+ files = proc.open_files()
+ if files:
+ for f in files:
+ if f.path == commit_lock:
+ return True
+ 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():
+ sleep(1)
+
+
+def ask_yes_no(question, default=False) -> bool:
+ """Ask a yes/no question via input() and return their answer."""
+ from sys import stdout
+ default_msg = "[Y/n]" if default else "[y/N]"
+ while True:
+ stdout.write("%s %s " % (question, default_msg))
+ c = input().lower()
+ if c == '':
+ return default
+ elif c in ("y", "ye", "yes"):
+ return True
+ elif c in ("n", "no"):
+ return False
+ else:
+ stdout.write("Please respond with yes/y or no/n\n")
+
+
+def is_admin() -> bool:
+ """Look if current user is in sudo group"""
+ 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
+ """
+ import re
+ 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 get_half_cpus():
+ """ return 1/2 of the numbers of available CPUs """
+ cpu = os.cpu_count()
+ if cpu > 1:
+ cpu /= 2
+ return int(cpu)
+
+def ifname_from_config(conf):
+ """
+ Gets interface name with VLANs from current config level.
+ Level must be at the interface whose name we want.
+
+ Example:
+ >>> from vyos.util import ifname_from_config
+ >>> from vyos.config import Config
+ >>> conf = Config()
+ >>> conf.set_level('interfaces ethernet eth0 vif-s 1 vif-c 2')
+ >>> ifname_from_config(conf)
+ 'eth0.1.2'
+ """
+ level = conf.get_level()
+
+ # vlans
+ if level[-2] == 'vif' or level[-2] == 'vif-s':
+ return level[-3] + '.' + level[-1]
+ if level[-2] == 'vif-c':
+ return level[-5] + '.' + level[-3] + '.' + level[-1]
+
+ # no vlans
+ return level[-1]
+
+def get_bridge_member_config(conf, br, intf):
+ """
+ Gets bridge port (member) configuration
+
+ Arguments:
+ conf: Config
+ br: bridge name
+ intf: interface name
+
+ Returns:
+ dict with the configuration
+ False if bridge or bridge port doesn't exist
+ """
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ bridge = f'interfaces bridge {br}'
+ member = f'{bridge} member interface {intf}'
+ if not ( conf.exists(bridge) and conf.exists(member) ):
+ return False
+
+ # default bridge port configuration
+ # cost and priority initialized with linux defaults
+ # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority}
+ # after adding interface to bridge after reboot
+ memberconf = {
+ 'cost': 100,
+ 'priority': 32,
+ 'arp_cache_tmo': 30,
+ 'disable_link_detect': 1,
+ }
+
+ if conf.exists(f'{member} cost'):
+ memberconf['cost'] = int(conf.return_value(f'{member} cost'))
+
+ if conf.exists(f'{member} priority'):
+ memberconf['priority'] = int(conf.return_value(f'{member} priority'))
+
+ if conf.exists(f'{bridge} ip arp-cache-timeout'):
+ memberconf['arp_cache_tmo'] = int(conf.return_value(f'{bridge} ip arp-cache-timeout'))
+
+ if conf.exists(f'{bridge} disable-link-detect'):
+ memberconf['disable_link_detect'] = 2
+
+ conf.set_level(old_level)
+ return memberconf
+
+def check_kmod(k_mod):
+ """ Common utility function to load required kernel modules on demand """
+ if isinstance(k_mod, str):
+ k_mod = k_mod.split()
+ for module in k_mod:
+ if not os.path.exists(f'/sys/module/{module}'):
+ if call(f'modprobe {module}') != 0:
+ raise ConfigError(f'Loading Kernel module {module} failed')
+
+def find_device_file(device):
+ """ Recurively search /dev for the given device file and return its full path.
+ If no device file was found 'None' is returned """
+ from fnmatch import fnmatch
+
+ for root, dirs, files in os.walk('/dev'):
+ for basename in files:
+ if fnmatch(basename, device):
+ return os.path.join(root, basename)
+
+ return None
+
+def vyos_dict_search(path, dict):
+ """ Traverse Python dictionary (dict) delimited by dot (.).
+ Return value of key if found, None otherwise.
+
+ This is faster implementation then jmespath.search('foo.bar', dict)"""
+ parts = path.split('.')
+ inside = parts[:-1]
+ if not inside:
+ return dict[path]
+ c = dict
+ for p in parts[:-1]:
+ c = c.get(p, {})
+ return c.get(parts[-1], None)
diff --git a/python/vyos/validate.py b/python/vyos/validate.py
new file mode 100644
index 000000000..ceeb6888a
--- /dev/null
+++ b/python/vyos/validate.py
@@ -0,0 +1,331 @@
+# 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 json
+import socket
+import netifaces
+import ipaddress
+
+from vyos.util import cmd
+from vyos import xml
+
+# Important note when you are adding new validation functions:
+#
+# The Control class will analyse the signature of the function in this file
+# and will build the parameters to be passed to it.
+#
+# The parameter names "ifname" and "self" will get the Interface name and class
+# parameters with default will be left unset
+# all other paramters will receive the value to check
+
+
+def is_ip(addr):
+ """
+ Check addr if it is an IPv4 or IPv6 address
+ """
+ return is_ipv4(addr) or is_ipv6(addr)
+
+def is_ipv4(addr):
+ """
+ Check addr if it is an IPv4 address/network. Returns True/False
+ """
+
+ # With the below statement we can check for IPv4 networks and host
+ # addresses at the same time
+ try:
+ if ipaddress.ip_address(addr.split(r'/')[0]).version == 4:
+ return True
+ except:
+ pass
+
+ return False
+
+def is_ipv6(addr):
+ """
+ Check addr if it is an IPv6 address/network. Returns True/False
+ """
+
+ # With the below statement we can check for IPv4 networks and host
+ # addresses at the same time
+ try:
+ if ipaddress.ip_network(addr.split(r'/')[0]).version == 6:
+ return True
+ except:
+ pass
+
+ return False
+
+def is_ipv6_link_local(addr):
+ """
+ Check addr if it is an IPv6 link-local address/network. Returns True/False
+ """
+
+ addr = addr.split('%')[0]
+ if is_ipv6(addr):
+ if ipaddress.IPv6Address(addr).is_link_local:
+ return True
+
+ 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.
+ """
+
+ # 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:
+ ifaces = netifaces.ifaddresses(intf)
+ except ValueError as e:
+ print(e)
+ return False
+
+ # 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
+
+ # 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
+
+def is_addr_assigned(addr):
+ """
+ Verify if the given IPv4/IPv6 address is assigned to any interface
+ """
+
+ for intf in netifaces.interfaces():
+ tmp = is_intf_addr_assigned(intf, addr)
+ if tmp == True:
+ return True
+
+ return False
+
+def is_loopback_addr(addr):
+ """
+ Check if supplied IPv4/IPv6 address is a loopback address
+ """
+ return ipaddress.ip_address(addr).is_loopback
+
+def is_subnet_connected(subnet, primary=False):
+ """
+ Verify is the given IPv4/IPv6 subnet is connected to any interface on this
+ system.
+
+ primary check if the subnet is reachable via the primary IP address of this
+ interface, or in other words has a broadcast address configured. ISC DHCP
+ for instance will complain if it should listen on non broadcast interfaces.
+
+ Return True/False
+ """
+
+ # determine IP version (AF_INET or AF_INET6) depending on passed address
+ addr_type = netifaces.AF_INET
+ if is_ipv6(subnet):
+ addr_type = netifaces.AF_INET6
+
+ for interface in netifaces.interfaces():
+ # check if the requested address type is configured at all
+ if addr_type not in netifaces.ifaddresses(interface).keys():
+ continue
+
+ # An interface can have multiple addresses, but some software components
+ # only support the primary address :(
+ if primary:
+ ip = netifaces.ifaddresses(interface)[addr_type][0]['addr']
+ if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet):
+ return True
+ else:
+ # Check every assigned IP address if it is connected to the subnet
+ # in question
+ for ip in netifaces.ifaddresses(interface)[addr_type]:
+ # remove interface extension (e.g. %eth0) that gets thrown on the end of _some_ addrs
+ addr = ip['addr'].split('%')[0]
+ if ipaddress.ip_address(addr) in ipaddress.ip_network(subnet):
+ 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 {smaller}')
+
+
+def assert_mtu(mtu, ifname):
+ assert_number(mtu)
+
+ out = cmd(f'ip -j -d link show dev {ifname}')
+ # [{"ifindex":2,"ifname":"eth0","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","linkmode":"DEFAULT","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:d9:5b:04","broadcast":"ff:ff:ff:ff:ff:ff","promiscuity":0,"min_mtu":46,"max_mtu":16110,"inet6_addr_gen_mode":"none","num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535}]
+ parsed = json.loads(out)[0]
+ min_mtu = int(parsed.get('min_mtu', '0'))
+ # cur_mtu = parsed.get('mtu',0),
+ max_mtu = int(parsed.get('max_mtu', '0'))
+ cur_mtu = int(mtu)
+
+ if (min_mtu and cur_mtu < min_mtu) or cur_mtu < 68:
+ raise ValueError(f'MTU is too small for interface "{ifname}": {mtu} < {min_mtu}')
+ if (max_mtu and cur_mtu > max_mtu) or cur_mtu > 65536:
+ raise ValueError(f'MTU is too small for interface "{ifname}": {mtu} > {max_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')
+
+
+def is_member(conf, interface, intftype=None):
+ """
+ Checks if passed interface is member of other interface of specified type.
+ intftype is optional, if not passed it will search all known types
+ (currently bridge and bonding)
+
+ Returns:
+ None -> Interface is not a member
+ interface name -> Interface is a member of this interface
+ False -> interface type cannot have members
+ """
+ ret_val = None
+ if intftype not in ['bonding', 'bridge', None]:
+ raise ValueError((
+ f'unknown interface type "{intftype}" or it cannot '
+ f'have member interfaces'))
+
+ intftype = ['bonding', 'bridge'] if intftype == None else [intftype]
+
+ # set config level to root
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ for it in intftype:
+ base = ['interfaces', it]
+ for intf in conf.list_nodes(base):
+ memberintf = base + [intf, 'member', 'interface']
+ if xml.is_tag(memberintf):
+ if interface in conf.list_nodes(memberintf):
+ ret_val = intf
+ break
+ elif xml.is_leaf(memberintf):
+ if ( conf.exists(memberintf) and
+ interface in conf.return_values(memberintf) ):
+ ret_val = intf
+ break
+
+ old_level = conf.set_level(old_level)
+ return ret_val
+
+def has_address_configured(conf, intf):
+ """
+ Checks if interface has an address configured.
+ Checks the following config nodes:
+ 'address', 'ipv6 address eui64', 'ipv6 address autoconf'
+
+ Returns True if interface has address configured, False if it doesn't.
+ """
+ from vyos.ifconfig import Section
+ ret = False
+
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ intfpath = 'interfaces ' + Section.get_config_path(intf)
+ if ( conf.exists(f'{intfpath} address') or
+ conf.exists(f'{intfpath} ipv6 address autoconf') or
+ conf.exists(f'{intfpath} ipv6 address eui64') ):
+ ret = True
+
+ conf.set_level(old_level)
+ return ret
diff --git a/python/vyos/version.py b/python/vyos/version.py
new file mode 100644
index 000000000..871bb0f1b
--- /dev/null
+++ b/python/vyos/version.py
@@ -0,0 +1,107 @@
+# Copyright 2017-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/>.
+
+"""
+VyOS version data access library.
+
+VyOS stores its version data, which include the version number and some
+additional information in a JSON file. This module provides a convenient
+interface to reading it.
+
+Example of the version data dict::
+ {
+ 'built_by': 'autobuild@vyos.net',
+ 'build_id': '021ac2ee-cd07-448b-9991-9c68d878cddd',
+ 'version': '1.2.0-rolling+201806200337',
+ 'built_on': 'Wed 20 Jun 2018 03:37 UTC'
+ }
+"""
+
+import os
+import json
+import vyos.defaults
+
+from vyos.util import read_file
+from vyos.util import read_json
+from vyos.util import popen
+from vyos.util import run
+from vyos.util import DEVNULL
+
+
+version_file = os.path.join(vyos.defaults.directories['data'], 'version.json')
+
+
+def get_version_data(fname=version_file):
+ """
+ Get complete version data
+
+ Args:
+ file (str): path to the version file
+
+ Returns:
+ 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.
+ It should not be used on a running system since the location of that file
+ is an implementation detail and may change in the future, while the interface
+ of this module will stay the same.
+ """
+ return read_json(fname, {})
+
+
+def get_version(fname=version_file):
+ """
+ Get the version number, or an empty string if it could not be determined
+ """
+ return get_version_data(fname=fname).get('version', '')
+
+
+def get_full_version_data(fname=version_file):
+ version_data = get_version_data(fname)
+
+ # Get system architecture (well, kernel architecture rather)
+ version_data['system_arch'], _ = popen('uname -m', stderr=DEVNULL)
+
+ hypervisor,code = popen('hvinfo', stderr=DEVNULL)
+ if code == 1:
+ # hvinfo returns 1 if it cannot detect any hypervisor
+ version_data['system_type'] = 'bare metal'
+ else:
+ version_data['system_type'] = f"{hypervisor} guest"
+
+ # Get boot type, it can be livecd, installed image, or, possible, a system installed
+ # via legacy "install system" mechanism
+ # In installed images, the squashfs image file is named after its image version,
+ # 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 run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0:
+ boot_via = "livecd"
+ elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0:
+ boot_via = "legacy non-image installation"
+ version_data['boot_via'] = boot_via
+
+ # Get hardware details from DMI
+ dmi = '/sys/class/dmi/id'
+ version_data['hardware_vendor'] = read_file(dmi + '/sys_vendor', 'Unknown')
+ version_data['hardware_model'] = read_file(dmi +'/product_name','Unknown')
+
+ # These two assume script is run as root, normal users can't access those files
+ subsystem = '/sys/class/dmi/id/subsystem/id'
+ version_data['hardware_serial'] = read_file(subsystem + '/product_serial','Unknown')
+ version_data['hardware_uuid'] = read_file(subsystem + '/product_uuid', 'Unknown')
+
+ return version_data
diff --git a/python/vyos/xml/.gitignore b/python/vyos/xml/.gitignore
new file mode 100644
index 000000000..e934adfd1
--- /dev/null
+++ b/python/vyos/xml/.gitignore
@@ -0,0 +1 @@
+cache/
diff --git a/python/vyos/xml/__init__.py b/python/vyos/xml/__init__.py
new file mode 100644
index 000000000..0f914fed2
--- /dev/null
+++ b/python/vyos/xml/__init__.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+from vyos.xml import definition
+from vyos.xml import load
+from vyos.xml import kw
+
+
+def load_configuration(cache=[]):
+ if cache:
+ return cache[0]
+
+ xml = definition.XML()
+
+ try:
+ from vyos.xml.cache import configuration
+ xml.update(configuration.definition)
+ cache.append(xml)
+ except Exception:
+ xml = definition.XML()
+ print('no xml configuration cache')
+ xml.update(load.xml(load.configuration_definition))
+
+ return xml
+
+
+# def is_multi(lpath):
+# return load_configuration().is_multi(lpath)
+
+
+def is_tag(lpath):
+ return load_configuration().is_tag(lpath)
+
+
+def is_leaf(lpath, flat=True):
+ return load_configuration().is_leaf(lpath, flat)
+
+
+def defaults(lpath, flat=False):
+ return load_configuration().defaults(lpath, flat)
+
+
+if __name__ == '__main__':
+ print(defaults(['service'], flat=True))
+ print(defaults(['service'], flat=False))
+
+ print(is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"]))
+ print(is_tag(['protocols', 'static', 'multicast', 'route', '0.0.0.0/0', 'next-hop']))
diff --git a/python/vyos/xml/cache/__init__.py b/python/vyos/xml/cache/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/vyos/xml/cache/__init__.py
diff --git a/python/vyos/xml/definition.py b/python/vyos/xml/definition.py
new file mode 100644
index 000000000..098e64f7e
--- /dev/null
+++ b/python/vyos/xml/definition.py
@@ -0,0 +1,330 @@
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from vyos.xml import kw
+
+# As we index by key, the name is first and then the data:
+# {'dummy': {
+# '[node]': '[tagNode]',
+# 'address': { ... }
+# } }
+
+# so when we encounter a tagNode, we are really encountering
+# the tagNode data.
+
+
+class XML(dict):
+ def __init__(self):
+ self[kw.tree] = {}
+ self[kw.priorities] = {}
+ self[kw.owners] = {}
+ self[kw.default] = {}
+ self[kw.tags] = []
+
+ dict.__init__(self)
+
+ self.tree = self[kw.tree]
+ # the options which matched the last incomplete world we had
+ # or the last word in a list
+ self.options = []
+ # store all the part of the command we processed
+ self.inside = []
+ # should we check the data pass with the constraints
+ self.check = False
+ # are we still typing a word
+ self.filling = False
+ # do what have the tagNode value ?
+ self.filled = False
+ # last word seen
+ self.word = ''
+ # do we have all the data we want ?
+ self.final = False
+ # do we have too much data ?
+ self.extra = False
+ # what kind of node are we in plain vs data not
+ self.plain = True
+
+ def reset(self):
+ self.tree = self[kw.tree]
+ self.options = []
+ self.inside = []
+ self.check = False
+ self.filling = False
+ self.filled = False
+ self.word = ''
+ self.final = False
+ self.extra = False
+ self.plain = True
+
+ # from functools import lru_cache
+ # @lru_cache(maxsize=100)
+ # XXX: need to use cachetool instead - for later
+
+ def traverse(self, cmd):
+ self.reset()
+
+ # using split() intead of split(' ') eats the final ' '
+ words = cmd.split(' ')
+ passed = []
+ word = ''
+ data_node = False
+ space = False
+
+ while words:
+ word = words.pop(0)
+ space = word == ''
+ perfect = False
+ if word in self.tree:
+ passed = []
+ perfect = True
+ self.tree = self.tree[word]
+ data_node = self.tree[kw.node]
+ self.inside.append(word)
+ word = ''
+ continue
+ if word and data_node:
+ passed.append(word)
+
+ is_valueless = self.tree.get(kw.valueless, False)
+ is_leafNode = data_node == kw.leafNode
+ is_dataNode = data_node in (kw.leafNode, kw.tagNode)
+ named_options = [_ for _ in self.tree if not kw.found(_)]
+
+ if is_leafNode:
+ self.final = is_valueless or len(passed) > 0
+ self.extra = is_valueless and len(passed) > 0
+ self.check = len(passed) >= 1
+ else:
+ self.final = False
+ self.extra = False
+ self.check = len(passed) == 1 and not space
+
+ if self.final:
+ self.word = ' '.join(passed)
+ else:
+ self.word = word
+
+ if self.final:
+ self.filling = True
+ else:
+ self.filling = not perfect and bool(cmd and word != '')
+
+ self.filled = self.final or (is_dataNode and len(passed) > 0 and word == '')
+
+ if is_dataNode and len(passed) == 0:
+ self.options = []
+ elif word:
+ if data_node != kw.plainNode or len(passed) == 1:
+ self.options = [_ for _ in self.tree if _.startswith(word)]
+ self.options.sort()
+ else:
+ self.options = []
+ else:
+ self.options = named_options
+ self.options.sort()
+
+ self.plain = not is_dataNode
+
+ # self.debug()
+
+ return self.word
+
+ def speculate(self):
+ if len(self.options) == 1:
+ self.tree = self.tree[self.options[0]]
+ self.word = ''
+ if self.tree.get(kw.node,'') not in (kw.tagNode, kw.leafNode):
+ self.options = [_ for _ in self.tree if not kw.found(_)]
+ self.options.sort()
+
+ def checks(self, cmd):
+ # as we move thought the named node twice
+ # the first time we get the data with the node
+ # and the second with the pass parameters
+ xml = self[kw.tree]
+
+ words = cmd.split(' ')
+ send = True
+ last = []
+ while words:
+ word = words.pop(0)
+ if word in xml:
+ xml = xml[word]
+ send = True
+ last = []
+ continue
+ if xml[kw.node] in (kw.tagNode, kw.leafNode):
+ if kw.constraint in xml:
+ if send:
+ yield (word, xml[kw.constraint])
+ send = False
+ else:
+ last.append((word, None))
+ if len(last) >= 2:
+ yield last[0]
+
+ def summary(self):
+ yield ('enter', '[ summary ]', str(self.inside))
+
+ if kw.help not in self.tree:
+ yield ('skip', '[ summary ]', str(self.inside))
+ return
+
+ if self.filled:
+ return
+
+ yield('', '', '\nHelp:')
+
+ if kw.help in self.tree:
+ summary = self.tree[kw.help].get(kw.summary)
+ values = self.tree[kw.help].get(kw.valuehelp, [])
+ if summary:
+ yield(summary, '', '')
+ for value in values:
+ yield(value[kw.format], value[kw.description], '')
+
+ def constraint(self):
+ yield ('enter', '[ constraint ]', str(self.inside))
+
+ if kw.help in self.tree:
+ yield ('skip', '[ constraint ]', str(self.inside))
+ return
+ if kw.error not in self.tree:
+ yield ('skip', '[ constraint ]', str(self.inside))
+ return
+ if not self.word or self.filling:
+ yield ('skip', '[ constraint ]', str(self.inside))
+ return
+
+ yield('', '', '\nData Constraint:')
+
+ yield('', 'constraint', str(self.tree[kw.error]))
+
+ def listing(self):
+ yield ('enter', '[ listing ]', str(self.inside))
+
+ # only show the details when we passed the tagNode data
+ if not self.plain and not self.filled:
+ yield ('skip', '[ listing ]', str(self.inside))
+ return
+
+ yield('', '', '\nPossible completions:')
+
+ options = list(self.tree.keys())
+ options.sort()
+ for option in options:
+ if kw.found(option):
+ continue
+ if not option.startswith(self.word):
+ continue
+ inner = self.tree[option]
+ prefix = '+> ' if inner.get(kw.node, '') != kw.leafNode else ' '
+ if kw.help in inner:
+ yield (prefix + option, inner[kw.help].get(kw.summary), '')
+ else:
+ yield (prefix + option, '(no help available)', '')
+
+ def debug(self):
+ print('------')
+ print("word '%s'" % self.word)
+ print("filling " + str(self.filling))
+ print("filled " + str(self.filled))
+ print("final " + str(self.final))
+ print("extra " + str(self.extra))
+ print("plain " + str(self.plain))
+ print("options " + str(self.options))
+
+ # from functools import lru_cache
+ # @lru_cache(maxsize=100)
+ # XXX: need to use cachetool instead - for later
+
+ def defaults(self, lpath, flat):
+ d = self[kw.default]
+ for k in lpath:
+ d = d.get(k, {})
+
+ if not flat:
+ r = {}
+ for k in d:
+ under = k.replace('-','_')
+ if isinstance(d[k],dict):
+ r[under] = self.defaults(lpath + [k], flat)
+ continue
+ r[under] = d[k]
+ return r
+
+ def _flatten(inside, index, d):
+ r = {}
+ local = inside[index:]
+ prefix = '_'.join(_.replace('-','_') for _ in local) + '_' if local else ''
+ for k in d:
+ under = prefix + k.replace('-','_')
+ level = inside + [k]
+ if isinstance(d[k],dict):
+ r.update(_flatten(level, index, d[k]))
+ continue
+ if self.is_multi(level, with_tag=False):
+ r[under] = [_.strip() for _ in d[k].split(',')]
+ continue
+ r[under] = d[k]
+ return r
+
+ return _flatten(lpath, len(lpath), d)
+
+ # from functools import lru_cache
+ # @lru_cache(maxsize=100)
+ # XXX: need to use cachetool instead - for later
+
+ def _tree(self, lpath, with_tag=True):
+ """
+ returns the part of the tree searched or None if it does not exists
+ if with_tag is set, this is a configuration path (with tagNode names)
+ and tag name will be removed from the path when traversing the tree
+ """
+ tree = self[kw.tree]
+ spath = lpath.copy()
+ while spath:
+ p = spath.pop(0)
+ if p not in tree:
+ return None
+ tree = tree[p]
+ if with_tag and spath and tree[kw.node] == kw.tagNode:
+ spath.pop(0)
+ return tree
+
+ def _get(self, lpath, tag, with_tag=True):
+ tree = self._tree(lpath, with_tag)
+ if tree is None:
+ return None
+ return tree.get(tag, None)
+
+ def is_multi(self, lpath, with_tag=True):
+ tree = self._get(lpath, kw.multi, with_tag)
+ if tree is None:
+ return None
+ return tree is True
+
+ def is_tag(self, lpath, with_tag=True):
+ tree = self._get(lpath, kw.node, with_tag)
+ if tree is None:
+ return None
+ return tree == kw.tagNode
+
+ def is_leaf(self, lpath, with_tag=True):
+ tree = self._get(lpath, kw.node, with_tag)
+ if tree is None:
+ return None
+ return tree == kw.leafNode
+
+ def exists(self, lpath, with_tag=True):
+ return self._get(lpath, kw.node, with_tag) is not None
diff --git a/python/vyos/xml/generate.py b/python/vyos/xml/generate.py
new file mode 100755
index 000000000..dfbbadd74
--- /dev/null
+++ b/python/vyos/xml/generate.py
@@ -0,0 +1,70 @@
+
+#!/usr/bin/env python3
+
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import os
+import sys
+import pprint
+import argparse
+
+from vyos.xml import kw
+from vyos.xml import load
+
+
+# import json
+# def save_json(fname, loaded):
+# with open(fname, 'w') as w:
+# print(f'saving {fname}')
+# w.write(json.dumps(loaded))
+
+
+def save_dict(fname, loaded):
+ with open(fname, 'w') as w:
+ print(f'saving {fname}')
+ w.write(f'# generated by {__file__}\n\n')
+ w.write('definition = ')
+ w.write(str(loaded))
+
+
+def main():
+ parser = argparse.ArgumentParser(description='generate python file from xml defintions')
+ parser.add_argument('--conf-folder', type=str, default=load.configuration_definition, help='XML interface definition folder')
+ parser.add_argument('--conf-cache', type=str, default=load.configuration_cache, help='python file with the conf mode dict')
+
+ # parser.add_argument('--op-folder', type=str, default=load.operational_definition, help='XML interface definition folder')
+ # parser.add_argument('--op-cache', type=str, default=load.operational_cache, help='python file with the conf mode dict')
+
+ parser.add_argument('--dry', action='store_true', help='dry run, print to screen')
+
+ args = parser.parse_args()
+
+ if os.path.exists(load.configuration_cache):
+ os.remove(load.configuration_cache)
+ # if os.path.exists(load.operational_cache):
+ # os.remove(load.operational_cache)
+
+ conf = load.xml(args.conf_folder)
+ # op = load.xml(args.op_folder)
+
+ if args.dry:
+ pprint.pprint(conf)
+ return
+
+ save_dict(args.conf_cache, conf)
+ # save_dict(args.op_cache, op)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python/vyos/xml/kw.py b/python/vyos/xml/kw.py
new file mode 100644
index 000000000..64521c51a
--- /dev/null
+++ b/python/vyos/xml/kw.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+# all named used as key (keywords) in this module are defined here.
+# using variable name will allow the linter to warn on typos
+# it separates our dict syntax from the xmldict one, making it easy to change
+
+# we are redefining a python keyword "list" for ease
+
+
+def found(word):
+ """
+ is the word following the format for a keyword
+ """
+ return word and word[0] == '[' and word[-1] == ']'
+
+
+# root
+
+version = '[version]'
+tree = '[tree]'
+priorities = '[priorities]'
+owners = '[owners]'
+tags = '[tags]'
+default = '[default]'
+
+# nodes
+
+node = '[node]'
+
+plainNode = '[plainNode]'
+leafNode = '[leafNode]'
+tagNode = '[tagNode]'
+
+owner = '[owner]'
+
+valueless = '[valueless]'
+multi = '[multi]'
+hidden = '[hidden]'
+
+# properties
+
+priority = '[priority]'
+
+completion = '[completion]'
+list = '[list]'
+script = '[script]'
+path = '[path]'
+
+# help
+
+help = '[help]'
+
+summary = '[summary]'
+
+valuehelp = '[valuehelp]'
+format = 'format'
+description = 'description'
+
+# constraint
+
+constraint = '[constraint]'
+name = '[name]'
+
+regex = '[regex]'
+validator = '[validator]'
+argument = '[argument]'
+
+error = '[error]'
+
+# created
+
+node = '[node]'
diff --git a/python/vyos/xml/load.py b/python/vyos/xml/load.py
new file mode 100644
index 000000000..1f463a5b7
--- /dev/null
+++ b/python/vyos/xml/load.py
@@ -0,0 +1,290 @@
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import glob
+
+from os.path import join
+from os.path import abspath
+from os.path import dirname
+
+import xmltodict
+
+from vyos import debug
+from vyos.xml import kw
+from vyos.xml import definition
+
+
+# where the files are located
+
+_here = dirname(__file__)
+
+configuration_definition = abspath(join(_here, '..', '..' ,'..', 'interface-definitions'))
+configuration_cache = abspath(join(_here, 'cache', 'configuration.py'))
+
+operational_definition = abspath(join(_here, '..', '..' ,'..', 'op-mode-definitions'))
+operational_cache = abspath(join(_here, 'cache', 'operational.py'))
+
+
+# This code is only ran during the creation of the debian package
+# therefore we accept that failure can be fatal and not handled
+# gracefully.
+
+
+def _fatal(debug_info=''):
+ """
+ raise a RuntimeError or if in developer mode stop the code
+ """
+ if not debug.enabled('developer'):
+ raise RuntimeError(str(debug_info))
+
+ if debug_info:
+ print(debug_info)
+ breakpoint()
+
+
+def _safe_update(dict1, dict2):
+ """
+ return a dict made of two, raise if any root key would be overwritten
+ """
+ if set(dict1).intersection(dict2):
+ raise RuntimeError('overlapping configuration')
+ return {**dict1, **dict2}
+
+
+def _merge(dict1, dict2):
+ """
+ merge dict2 in to dict1 and return it
+ """
+ for k in list(dict2):
+ if k not in dict1:
+ dict1[k] = dict2[k]
+ continue
+ if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
+ dict1[k] = _merge(dict1[k], dict2[k])
+ elif isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
+ dict1[k].extend(dict2[k])
+ elif dict1[k] == dict2[k]:
+ # A definition shared between multiple files
+ if k in (kw.valueless, kw.multi, kw.hidden, kw.node, kw.summary, kw.owner, kw.priority):
+ continue
+ _fatal()
+ raise RuntimeError('parsing issue - undefined leaf?')
+ else:
+ raise RuntimeError('parsing issue - we messed up?')
+ return dict1
+
+
+def _include(fname, folder=''):
+ """
+ return the content of a file, including any file referenced with a #include
+ """
+ if not folder:
+ folder = dirname(fname)
+ content = ''
+ with open(fname, 'r') as r:
+ for line in r.readlines():
+ if '#include' in line:
+ content += _include(join(folder,line.strip()[10:-1]), folder)
+ continue
+ content += line
+ return content
+
+
+def _format_nodes(inside, conf, xml):
+ r = {}
+ while conf:
+ nodetype = ''
+ nodename = ''
+ if 'node' in conf.keys():
+ nodetype = 'node'
+ nodename = kw.plainNode
+ elif 'leafNode' in conf.keys():
+ nodetype = 'leafNode'
+ nodename = kw.leafNode
+ elif 'tagNode' in conf.keys():
+ nodetype = 'tagNode'
+ nodename = kw.tagNode
+ elif 'syntaxVersion' in conf.keys():
+ r[kw.version] = conf.pop('syntaxVersion')['@version']
+ continue
+ else:
+ _fatal(conf.keys())
+
+ nodes = conf.pop(nodetype)
+ if isinstance(nodes, list):
+ for node in nodes:
+ name = node.pop('@name')
+ into = inside + [name]
+ r[name] = _format_node(into, node, xml)
+ r[name][kw.node] = nodename
+ xml[kw.tags].append(' '.join(into))
+ else:
+ node = nodes
+ name = node.pop('@name')
+ into = inside + [name]
+ r[name] = _format_node(inside + [name], node, xml)
+ r[name][kw.node] = nodename
+ xml[kw.tags].append(' '.join(into))
+ return r
+
+
+def _set_validator(r, validator):
+ v = {}
+ while validator:
+ if '@name' in validator:
+ v[kw.name] = validator.pop('@name')
+ elif '@argument' in validator:
+ v[kw.argument] = validator.pop('@argument')
+ else:
+ _fatal(validator)
+ r[kw.constraint][kw.validator].append(v)
+
+
+def _format_node(inside, conf, xml):
+ r = {
+ kw.valueless: False,
+ kw.multi: False,
+ kw.hidden: False,
+ }
+
+ if '@owner' in conf:
+ owner = conf.pop('@owner', '')
+ r[kw.owner] = owner
+ xml[kw.owners][' '.join(inside)] = owner
+
+ while conf:
+ keys = conf.keys()
+ if 'children' in keys:
+ children = conf.pop('children')
+
+ if isinstance(conf, list):
+ for child in children:
+ r = _safe_update(r, _format_nodes(inside, child, xml))
+ else:
+ child = children
+ r = _safe_update(r, _format_nodes(inside, child, xml))
+
+ elif 'properties' in keys:
+ properties = conf.pop('properties')
+
+ while properties:
+ if 'help' in properties:
+ helpname = properties.pop('help')
+ r[kw.help] = {}
+ r[kw.help][kw.summary] = helpname
+
+ elif 'valueHelp' in properties:
+ valuehelps = properties.pop('valueHelp')
+ if kw.valuehelp in r[kw.help]:
+ _fatal(valuehelps)
+ r[kw.help][kw.valuehelp] = []
+ if isinstance(valuehelps, list):
+ for valuehelp in valuehelps:
+ r[kw.help][kw.valuehelp].append(dict(valuehelp))
+ else:
+ valuehelp = valuehelps
+ r[kw.help][kw.valuehelp].append(dict(valuehelp))
+
+ elif 'constraint' in properties:
+ constraint = properties.pop('constraint')
+ r[kw.constraint] = {}
+ while constraint:
+ if 'regex' in constraint:
+ regexes = constraint.pop('regex')
+ if kw.regex in kw.constraint:
+ _fatal(regexes)
+ r[kw.constraint][kw.regex] = []
+ if isinstance(regexes, list):
+ r[kw.constraint][kw.regex] = []
+ for regex in regexes:
+ r[kw.constraint][kw.regex].append(regex)
+ else:
+ regex = regexes
+ r[kw.constraint][kw.regex].append(regex)
+ elif 'validator' in constraint:
+ validators = constraint.pop('validator')
+ if kw.validator in r[kw.constraint]:
+ _fatal(validators)
+ r[kw.constraint][kw.validator] = []
+ if isinstance(validators, list):
+ for validator in validators:
+ _set_validator(r, validator)
+ else:
+ validator = validators
+ _set_validator(r, validator)
+ else:
+ _fatal(constraint)
+
+ elif 'constraintErrorMessage' in properties:
+ r[kw.error] = properties.pop('constraintErrorMessage')
+
+ elif 'valueless' in properties:
+ properties.pop('valueless')
+ r[kw.valueless] = True
+
+ elif 'multi' in properties:
+ properties.pop('multi')
+ r[kw.multi] = True
+
+ elif 'hidden' in properties:
+ properties.pop('hidden')
+ r[kw.hidden] = True
+
+ elif 'completionHelp' in properties:
+ completionHelp = properties.pop('completionHelp')
+ r[kw.completion] = {}
+ while completionHelp:
+ if 'list' in completionHelp:
+ r[kw.completion][kw.list] = completionHelp.pop('list')
+ elif 'script' in completionHelp:
+ r[kw.completion][kw.script] = completionHelp.pop('script')
+ elif 'path' in completionHelp:
+ r[kw.completion][kw.path] = completionHelp.pop('path')
+ else:
+ _fatal(completionHelp.keys())
+
+ elif 'priority' in properties:
+ priority = int(properties.pop('priority'))
+ r[kw.priority] = priority
+ xml[kw.priorities].setdefault(priority, []).append(' '.join(inside))
+
+ else:
+ _fatal(properties.keys())
+
+ elif 'defaultValue' in keys:
+ default = conf.pop('defaultValue')
+ x = xml[kw.default]
+ for k in inside[:-1]:
+ x = x.setdefault(k,{})
+ x[inside[-1]] = '' if default is None else default
+
+ else:
+ _fatal(conf)
+
+ return r
+
+
+def xml(folder):
+ """
+ read all the xml in the folder
+ """
+ xml = definition.XML()
+ for fname in glob.glob(f'{folder}/*.xml.in'):
+ parsed = xmltodict.parse(_include(fname))
+ formated = _format_nodes([], parsed['interfaceDefinition'], xml)
+ _merge(xml[kw.tree], formated)
+ # fix the configuration root node for completion
+ # as we moved all the name "up" the chain to use them as index.
+ xml[kw.tree][kw.node] = kw.plainNode
+ # XXX: do the others
+ return xml
diff --git a/python/vyos/xml/test_xml.py b/python/vyos/xml/test_xml.py
new file mode 100644
index 000000000..ff55151d2
--- /dev/null
+++ b/python/vyos/xml/test_xml.py
@@ -0,0 +1,279 @@
+#!/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 unittest
+from unittest import TestCase, mock
+
+from vyos.xml import load_configuration
+
+import sys
+
+
+class TestSearch(TestCase):
+ def setUp(self):
+ self.xml = load_configuration()
+
+ def test_(self):
+ last = self.xml.traverse("")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, [])
+ self.assertEqual(self.xml.options, ['firewall', 'high-availability', 'interfaces', 'nat', 'protocols', 'service', 'system', 'vpn', 'vrf'])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, True)
+
+ def test_i(self):
+ last = self.xml.traverse("i")
+ self.assertEqual(last, 'i')
+ self.assertEqual(self.xml.inside, [])
+ self.assertEqual(self.xml.options, ['interfaces'])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, True)
+
+ def test_interfaces(self):
+ last = self.xml.traverse("interfaces")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces'])
+ self.assertEqual(self.xml.options, ['bonding', 'bridge', 'dummy', 'ethernet', 'geneve', 'l2tpv3', 'loopback', 'macsec', 'openvpn', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan', 'wireguard', 'wireless', 'wirelessmodem'])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, '')
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, True)
+
+ def test_interfaces_space(self):
+ last = self.xml.traverse("interfaces ")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces'])
+ self.assertEqual(self.xml.options, ['bonding', 'bridge', 'dummy', 'ethernet', 'geneve', 'l2tpv3', 'loopback', 'macsec', 'openvpn', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan', 'wireguard', 'wireless', 'wirelessmodem'])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, True)
+
+ def test_interfaces_w(self):
+ last = self.xml.traverse("interfaces w")
+ self.assertEqual(last, 'w')
+ self.assertEqual(self.xml.inside, ['interfaces'])
+ self.assertEqual(self.xml.options, ['wireguard', 'wireless', 'wirelessmodem'])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, True)
+
+ def test_interfaces_ethernet(self):
+ last = self.xml.traverse("interfaces ethernet")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, '')
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_space(self):
+ last = self.xml.traverse("interfaces ethernet ")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, '')
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_e(self):
+ last = self.xml.traverse("interfaces ethernet e")
+ self.assertEqual(last, 'e')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_la(self):
+ last = self.xml.traverse("interfaces ethernet la")
+ self.assertEqual(last, 'la')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0(self):
+ last = self.xml.traverse("interfaces ethernet lan0")
+ self.assertEqual(last, 'lan0')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_space(self):
+ last = self.xml.traverse("interfaces ethernet lan0 ")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(len(self.xml.options), 19)
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_ad(self):
+ last = self.xml.traverse("interfaces ethernet lan0 ad")
+ self.assertEqual(last, 'ad')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, ['address'])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address ")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space_11(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address 1.1")
+ self.assertEqual(last, '1.1')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, True)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space_1111_32(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32")
+ self.assertEqual(last, '1.1.1.1/32')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, True)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space_1111_32_space(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 ")
+ self.assertEqual(last, '1.1.1.1/32')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, True)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space_1111_32_space_text(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 text")
+ self.assertEqual(last, '1.1.1.1/32 text')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, True)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space_1111_32_space_text_space(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 text ")
+ self.assertEqual(last, '1.1.1.1/32 text')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, True)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ # Need to add a check for a valuless leafNode \ No newline at end of file
diff --git a/schema/interface_definition.rnc b/schema/interface_definition.rnc
new file mode 100644
index 000000000..6647f5e11
--- /dev/null
+++ b/schema/interface_definition.rnc
@@ -0,0 +1,175 @@
+# interface_definition.rnc: VyConf reference tree XML grammar
+#
+# Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net>
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+# The language of this file is compact form RELAX-NG
+# http://relaxng.org/compact-tutorial-20030326.htm
+# (unless converted to XML, then just RELAX-NG :)
+
+# Interface definition starts with interfaceDefinition tag that may contain node tags
+start = element interfaceDefinition
+{
+ syntaxVersion*,
+ node*
+}
+
+# interfaceDefinition may contain syntax version attribute lists.
+syntaxVersion = element syntaxVersion
+{
+ (componentAttr & versionAttr)
+}
+
+# node tag may contain node, leafNode, or tagNode tags
+# Those are intermediate configuration nodes that may only contain
+# other nodes and must not have values
+node = element node
+{
+ (ownerAttr? & nodeNameAttr),
+ (properties? & children? )
+}
+
+# Tag nodes are containers for nodes without predefined names, like network interfaces
+# or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker")
+# Tag nodes may contain node and leafNode elements, and also nameConstraint tags
+# They must not contain other tag nodes
+tagNode = element tagNode
+{
+ (ownerAttr? & nodeNameAttr),
+ (properties? & children )
+}
+
+# Leaf nodes are terminal configuration nodes that can't have children,
+# but can have values.
+# Leaf node may contain one or more valueConstraint tags
+# If multiple valueConstraint tags are used, they work a logical OR
+# Leaf nodes can have "multi" attribute that indicated that it can have more than one value
+# It can also have a default value
+leafNode = element leafNode
+{
+ (ownerAttr? & nodeNameAttr),
+ (defaultValue? & properties)
+}
+
+# Default value for leaf node, if applicable
+# It is used to generate default node state representation
+defaultValue = element defaultValue { text }
+
+# Normal and tag nodes may have children
+children = element children
+{
+ (node | tagNode | leafNode)+
+}
+
+# Nodes may have properties
+# For simplicity, any property is allowed in any node,
+# but whether they are used or not is implementation-defined
+#
+# Leaf nodes may differ in number of values that can be
+# associated with them.
+# By default, a leaf node can have only one value.
+# "multi" tag means a node can have one or more values,
+# "valueless" means it can have no values at all.
+# "hidden" means node visibility can be toggled, eg 'dangerous' commands,
+# "secret" allows a node to hide its value from unprivileged users.
+#
+# "priority" is used to influence node processing order for nodes
+# with exact same dependencies and in compatibility modes.
+properties = element properties
+{
+ help? &
+ constraint? &
+ valueHelp* &
+ (element constraintErrorMessage { text })? &
+ completionHelp* &
+
+ # These are meaningful only for leaf nodes
+ (element valueless { empty })? &
+ (element multi { empty })? &
+ (element hidden { empty })? &
+ (element secret { empty })? &
+ (element priority { text })? &
+
+ # These are meaningful only for tag nodes
+ (element keepChildOrder { empty })?
+}
+
+componentAttr = attribute component
+{
+ text
+}
+
+versionAttr = attribute version
+{
+ text
+}
+
+# All nodes must have "name" attribute
+nodeNameAttr = attribute name
+{
+ text
+}
+
+# Ordinary nodes and tag nodes can have "owner" attribute.
+# Owner is the component that is notified when node changes.
+ownerAttr = attribute owner
+{
+ text
+}
+
+# Tag and leaf nodes may have constraints on their names and values
+# (respectively).
+# When multiple constraints are listed, they work as logical OR
+constraint = element constraint
+{
+ ( (element regex { text }) |
+ validator )+
+}
+
+# A constraint may also use an external validator rather than regex
+validator = element validator
+{
+ ( (attribute name { text }) &
+ (attribute argument { text })? ),
+ empty
+}
+
+# help tags contains brief description of the purpose of the node
+help = element help
+{
+ text
+}
+
+# valueHelp tags contain information about acceptable value format
+valueHelp = element valueHelp
+{
+ element format { text } &
+ element description { text }
+}
+
+# completionHelp tags contain information about allowed values of a node that is used for generating
+# tab completion in the CLI frontend and drop-down lists in GUI frontends
+# It is only meaninful for leaf nodes
+# Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>),
+# as a configuration path (e.g. <path>interfaces ethernet</path>),
+# or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>
+completionHelp = element completionHelp
+{
+ (element list { text })* &
+ (element path { text })* &
+ (element script { text })*
+}
diff --git a/schema/interface_definition.rng b/schema/interface_definition.rng
new file mode 100644
index 000000000..22e886006
--- /dev/null
+++ b/schema/interface_definition.rng
@@ -0,0 +1,307 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+ <!--
+ interface_definition.rnc: VyConf reference tree XML grammar
+
+ Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net>
+
+ 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, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ USA
+ -->
+ <!--
+ The language of this file is compact form RELAX-NG
+ http://relaxng.org/compact-tutorial-20030326.htm
+ (unless converted to XML, then just RELAX-NG :)
+ -->
+ <!-- Interface definition starts with interfaceDefinition tag that may contain node tags -->
+ <start>
+ <element name="interfaceDefinition">
+ <zeroOrMore>
+ <ref name="syntaxVersion"/>
+ </zeroOrMore>
+ <zeroOrMore>
+ <ref name="node"/>
+ </zeroOrMore>
+ </element>
+ </start>
+ <!-- interfaceDefinition may contain syntax version attribute lists. -->
+ <define name="syntaxVersion">
+ <element name="syntaxVersion">
+ <interleave>
+ <ref name="componentAttr"/>
+ <ref name="versionAttr"/>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ node tag may contain node, leafNode, or tagNode tags
+ Those are intermediate configuration nodes that may only contain
+ other nodes and must not have values
+ -->
+ <define name="node">
+ <element name="node">
+ <interleave>
+ <optional>
+ <ref name="ownerAttr"/>
+ </optional>
+ <ref name="nodeNameAttr"/>
+ </interleave>
+ <interleave>
+ <optional>
+ <ref name="properties"/>
+ </optional>
+ <optional>
+ <ref name="children"/>
+ </optional>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ Tag nodes are containers for nodes without predefined names, like network interfaces
+ or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker")
+ Tag nodes may contain node and leafNode elements, and also nameConstraint tags
+ They must not contain other tag nodes
+ -->
+ <define name="tagNode">
+ <element name="tagNode">
+ <interleave>
+ <optional>
+ <ref name="ownerAttr"/>
+ </optional>
+ <ref name="nodeNameAttr"/>
+ </interleave>
+ <interleave>
+ <optional>
+ <ref name="properties"/>
+ </optional>
+ <ref name="children"/>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ Leaf nodes are terminal configuration nodes that can't have children,
+ but can have values.
+ Leaf node may contain one or more valueConstraint tags
+ If multiple valueConstraint tags are used, they work a logical OR
+ Leaf nodes can have "multi" attribute that indicated that it can have more than one value
+ It can also have a default value
+ -->
+ <define name="leafNode">
+ <element name="leafNode">
+ <interleave>
+ <optional>
+ <ref name="ownerAttr"/>
+ </optional>
+ <ref name="nodeNameAttr"/>
+ </interleave>
+ <interleave>
+ <optional>
+ <ref name="defaultValue"/>
+ </optional>
+ <ref name="properties"/>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ Default value for leaf node, if applicable
+ It is used to generate default node state representation
+ -->
+ <define name="defaultValue">
+ <element name="defaultValue">
+ <text/>
+ </element>
+ </define>
+ <!-- Normal and tag nodes may have children -->
+ <define name="children">
+ <element name="children">
+ <oneOrMore>
+ <choice>
+ <ref name="node"/>
+ <ref name="tagNode"/>
+ <ref name="leafNode"/>
+ </choice>
+ </oneOrMore>
+ </element>
+ </define>
+ <!--
+ Nodes may have properties
+ For simplicity, any property is allowed in any node,
+ but whether they are used or not is implementation-defined
+
+ Leaf nodes may differ in number of values that can be
+ associated with them.
+ By default, a leaf node can have only one value.
+ "multi" tag means a node can have one or more values,
+ "valueless" means it can have no values at all.
+ "hidden" means node visibility can be toggled, eg 'dangerous' commands,
+ "secret" allows a node to hide its value from unprivileged users.
+
+ "priority" is used to influence node processing order for nodes
+ with exact same dependencies and in compatibility modes.
+ -->
+ <define name="properties">
+ <element name="properties">
+ <interleave>
+ <optional>
+ <ref name="help"/>
+ </optional>
+ <optional>
+ <ref name="constraint"/>
+ </optional>
+ <zeroOrMore>
+ <ref name="valueHelp"/>
+ </zeroOrMore>
+ <optional>
+ <element name="constraintErrorMessage">
+ <text/>
+ </element>
+ </optional>
+ <zeroOrMore>
+ <ref name="completionHelp"/>
+ </zeroOrMore>
+ <optional>
+ <!-- These are meaningful only for leaf nodes -->
+ <group>
+ <element name="valueless">
+ <empty/>
+ </element>
+ </group>
+ </optional>
+ <optional>
+ <element name="multi">
+ <empty/>
+ </element>
+ </optional>
+ <optional>
+ <element name="hidden">
+ <empty/>
+ </element>
+ </optional>
+ <optional>
+ <element name="secret">
+ <empty/>
+ </element>
+ </optional>
+ <optional>
+ <element name="priority">
+ <text/>
+ </element>
+ </optional>
+ <optional>
+ <!-- These are meaningful only for tag nodes -->
+ <group>
+ <element name="keepChildOrder">
+ <empty/>
+ </element>
+ </group>
+ </optional>
+ </interleave>
+ </element>
+ </define>
+ <define name="componentAttr">
+ <attribute name="component"/>
+ </define>
+ <define name="versionAttr">
+ <attribute name="version"/>
+ </define>
+ <!-- All nodes must have "name" attribute -->
+ <define name="nodeNameAttr">
+ <attribute name="name"/>
+ </define>
+ <!--
+ Ordinary nodes and tag nodes can have "owner" attribute.
+ Owner is the component that is notified when node changes.
+ -->
+ <define name="ownerAttr">
+ <attribute name="owner"/>
+ </define>
+ <!--
+ Tag and leaf nodes may have constraints on their names and values
+ (respectively).
+ When multiple constraints are listed, they work as logical OR
+ -->
+ <define name="constraint">
+ <element name="constraint">
+ <oneOrMore>
+ <choice>
+ <element name="regex">
+ <text/>
+ </element>
+ <ref name="validator"/>
+ </choice>
+ </oneOrMore>
+ </element>
+ </define>
+ <!-- A constraint may also use an external validator rather than regex -->
+ <define name="validator">
+ <element name="validator">
+ <interleave>
+ <attribute name="name"/>
+ <optional>
+ <attribute name="argument"/>
+ </optional>
+ </interleave>
+ <empty/>
+ </element>
+ </define>
+ <!-- help tags contains brief description of the purpose of the node -->
+ <define name="help">
+ <element name="help">
+ <text/>
+ </element>
+ </define>
+ <!-- valueHelp tags contain information about acceptable value format -->
+ <define name="valueHelp">
+ <element name="valueHelp">
+ <interleave>
+ <element name="format">
+ <text/>
+ </element>
+ <element name="description">
+ <text/>
+ </element>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ completionHelp tags contain information about allowed values of a node that is used for generating
+ tab completion in the CLI frontend and drop-down lists in GUI frontends
+ It is only meaninful for leaf nodes
+ Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>),
+ as a configuration path (e.g. <path>interfaces ethernet</path>),
+ or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>
+ -->
+ <define name="completionHelp">
+ <element name="completionHelp">
+ <interleave>
+ <zeroOrMore>
+ <element name="list">
+ <text/>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="path">
+ <text/>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="script">
+ <text/>
+ </element>
+ </zeroOrMore>
+ </interleave>
+ </element>
+ </define>
+</grammar>
diff --git a/schema/op-mode-definition.rnc b/schema/op-mode-definition.rnc
new file mode 100644
index 000000000..cbe51e6dc
--- /dev/null
+++ b/schema/op-mode-definition.rnc
@@ -0,0 +1,107 @@
+# interface_definition.rnc: VyConf reference tree XML grammar
+#
+# Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net>
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+# The language of this file is compact form RELAX-NG
+# http://relaxng.org/compact-tutorial-20030326.htm
+# (unless converted to XML, then just RELAX-NG :)
+
+# Interface definition starts with interfaceDefinition tag that may contain node tags
+start = element interfaceDefinition
+{
+ (node | tagNode)*
+}
+
+# node tag may contain node, leafNode, or tagNode tags
+# Those are intermediate configuration nodes that may only contain
+# other nodes and must not have values
+node = element node
+{
+ nodeNameAttr,
+ (properties? & children? & command?)
+}
+
+# Tag nodes are containers for nodes without predefined names, like network interfaces
+# or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker")
+# Tag nodes may contain node and leafNode elements, and also nameConstraint tags
+# They must not contain other tag nodes
+tagNode = element tagNode
+{
+ nodeNameAttr,
+ (properties? & children? & command?)
+}
+
+# Leaf nodes are terminal configuration nodes that can't have children,
+# but can have values.
+
+leafNode = element leafNode
+{
+ nodeNameAttr,
+ (command & properties)
+}
+
+# Normal and tag nodes may have children
+children = element children
+{
+ (node | tagNode | leafNode)+
+}
+
+# Nodes may have properties
+# For simplicity, any property is allowed in any node,
+# but whether they are used or not is implementation-defined
+
+
+properties = element properties
+{
+ help? &
+ completionHelp*
+}
+
+# All nodes must have "name" attribute
+nodeNameAttr = attribute name
+{
+ text
+}
+
+
+
+
+
+# help tags contains brief description of the purpose of the node
+help = element help
+{
+ text
+}
+
+command = element command
+{
+ text
+}
+
+# completionHelp tags contain information about allowed values of a node that is used for generating
+# tab completion in the CLI frontend and drop-down lists in GUI frontends
+# It is only meaninful for leaf nodes
+# Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>),
+# as a configuration path (e.g. <path>interfaces ethernet</path>),
+# or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>
+completionHelp = element completionHelp
+{
+ (element list { text })* &
+ (element path { text })* &
+ (element script { text })*
+}
diff --git a/schema/op-mode-definition.rng b/schema/op-mode-definition.rng
new file mode 100644
index 000000000..900f41e27
--- /dev/null
+++ b/schema/op-mode-definition.rng
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+ <!--
+ interface_definition.rnc: VyConf reference tree XML grammar
+
+ Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net>
+
+ 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, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ USA
+ -->
+ <!--
+ The language of this file is compact form RELAX-NG
+ http://relaxng.org/compact-tutorial-20030326.htm
+ (unless converted to XML, then just RELAX-NG :)
+ -->
+ <!-- Interface definition starts with interfaceDefinition tag that may contain node tags -->
+ <start>
+ <element name="interfaceDefinition">
+ <zeroOrMore>
+ <choice>
+ <ref name="node"/>
+ <ref name="tagNode"/>
+ </choice>
+ </zeroOrMore>
+ </element>
+ </start>
+ <!--
+ node tag may contain node, leafNode, or tagNode tags
+ Those are intermediate configuration nodes that may only contain
+ other nodes and must not have values
+ -->
+ <define name="node">
+ <element name="node">
+ <ref name="nodeNameAttr"/>
+ <interleave>
+ <optional>
+ <ref name="properties"/>
+ </optional>
+ <optional>
+ <ref name="children"/>
+ </optional>
+ <optional>
+ <ref name="command"/>
+ </optional>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ Tag nodes are containers for nodes without predefined names, like network interfaces
+ or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker")
+ Tag nodes may contain node and leafNode elements, and also nameConstraint tags
+ They must not contain other tag nodes
+ -->
+ <define name="tagNode">
+ <element name="tagNode">
+ <ref name="nodeNameAttr"/>
+ <interleave>
+ <optional>
+ <ref name="properties"/>
+ </optional>
+ <optional>
+ <ref name="children"/>
+ </optional>
+ <optional>
+ <ref name="command"/>
+ </optional>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ Leaf nodes are terminal configuration nodes that can't have children,
+ but can have values.
+ -->
+ <define name="leafNode">
+ <element name="leafNode">
+ <ref name="nodeNameAttr"/>
+ <interleave>
+ <ref name="command"/>
+ <ref name="properties"/>
+ </interleave>
+ </element>
+ </define>
+ <!-- Normal and tag nodes may have children -->
+ <define name="children">
+ <element name="children">
+ <oneOrMore>
+ <choice>
+ <ref name="node"/>
+ <ref name="tagNode"/>
+ <ref name="leafNode"/>
+ </choice>
+ </oneOrMore>
+ </element>
+ </define>
+ <!--
+ Nodes may have properties
+ For simplicity, any property is allowed in any node,
+ but whether they are used or not is implementation-defined
+ -->
+ <define name="properties">
+ <element name="properties">
+ <interleave>
+ <optional>
+ <ref name="help"/>
+ </optional>
+ <zeroOrMore>
+ <ref name="completionHelp"/>
+ </zeroOrMore>
+ </interleave>
+ </element>
+ </define>
+ <!-- All nodes must have "name" attribute -->
+ <define name="nodeNameAttr">
+ <attribute name="name"/>
+ </define>
+ <!-- help tags contains brief description of the purpose of the node -->
+ <define name="help">
+ <element name="help">
+ <text/>
+ </element>
+ </define>
+ <define name="command">
+ <element name="command">
+ <text/>
+ </element>
+ </define>
+ <!--
+ completionHelp tags contain information about allowed values of a node that is used for generating
+ tab completion in the CLI frontend and drop-down lists in GUI frontends
+ It is only meaninful for leaf nodes
+ Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>),
+ as a configuration path (e.g. <path>interfaces ethernet</path>),
+ or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>
+ -->
+ <define name="completionHelp">
+ <element name="completionHelp">
+ <interleave>
+ <zeroOrMore>
+ <element name="list">
+ <text/>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="path">
+ <text/>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="script">
+ <text/>
+ </element>
+ </zeroOrMore>
+ </interleave>
+ </element>
+ </define>
+</grammar>
diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates
new file mode 100755
index 000000000..c60b32a1e
--- /dev/null
+++ b/scripts/build-command-op-templates
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+#
+# build-command-template: converts new style command definitions in XML
+# to the old style (bunch of dirs and node.def's) command templates
+#
+# Copyright (C) 2017 VyOS maintainers <maintainers@vyos.net>
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+import sys
+import os
+import argparse
+import copy
+import functools
+
+from lxml import etree as ET
+
+# Defaults
+
+validator_dir = "/opt/vyatta/libexec/validators"
+default_constraint_err_msg = "Invalid value"
+
+
+## Get arguments
+
+parser = argparse.ArgumentParser(description='Converts new-style XML interface definitions to old-style command templates')
+parser.add_argument('--debug', help='Enable debug information output', action='store_true')
+parser.add_argument('INPUT_FILE', type=str, help="XML interface definition file")
+parser.add_argument('SCHEMA_FILE', type=str, help="RelaxNG schema file")
+parser.add_argument('OUTPUT_DIR', type=str, help="Output directory")
+
+args = parser.parse_args()
+
+input_file = args.INPUT_FILE
+schema_file = args.SCHEMA_FILE
+output_dir = args.OUTPUT_DIR
+debug = args.debug
+
+## Load and validate the inputs
+
+try:
+ xml = ET.parse(input_file)
+except Exception as e:
+ print("Failed to load interface definition file {0}".format(input_file))
+ print(e)
+ sys.exit(1)
+
+try:
+ relaxng_xml = ET.parse(schema_file)
+ validator = ET.RelaxNG(relaxng_xml)
+
+ if not validator.validate(xml):
+ print(validator.error_log)
+ print("Interface definition file {0} does not match the schema!".format(input_file))
+ sys.exit(1)
+except Exception as e:
+ print("Failed to load the XML schema {0}".format(schema_file))
+ print(e)
+ sys.exit(1)
+
+if not os.access(output_dir, os.W_OK):
+ print("The output directory {0} is not writeable".format(output_dir))
+ sys.exit(1)
+
+## If we got this far, everything must be ok and we can convert the file
+
+def make_path(l):
+ path = functools.reduce(os.path.join, l)
+ if debug:
+ print(path)
+ return path
+
+def get_properties(p):
+ props = {}
+
+ if p is None:
+ return props
+
+ # Get the help string
+ try:
+ props["help"] = p.find("help").text
+ except:
+ props["help"] = "No help available"
+
+
+ # Get the completion help strings
+ try:
+ che = p.findall("completionHelp")
+ ch = ""
+ for c in che:
+ scripts = c.findall("script")
+ paths = c.findall("path")
+ lists = c.findall("list")
+
+ # Current backend doesn't support multiple allowed: tags
+ # so we get to emulate it
+ comp_exprs = []
+ for i in lists:
+ comp_exprs.append("echo \"{0}\"".format(i.text))
+ for i in paths:
+ comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\" && echo".format(i.text))
+ for i in scripts:
+ comp_exprs.append("{0}".format(i.text))
+ comp_help = " && ".join(comp_exprs)
+ props["comp_help"] = comp_help
+ except:
+ props["comp_help"] = []
+
+ return props
+
+
+def make_node_def(props, command):
+ # XXX: replace with a template processor if it grows
+ # out of control
+
+ node_def = ""
+
+ if "help" in props:
+ node_def += "help: {0}\n".format(props["help"])
+
+
+ if "comp_help" in props:
+ node_def += "allowed: {0}\n".format(props["comp_help"])
+
+
+ if command is not None:
+ node_def += "run: {0}\n".format(command.text)
+
+
+ if debug:
+ print("The contents of the node.def file:\n", node_def)
+
+ return node_def
+
+def process_node(n, tmpl_dir):
+ # Avoid mangling the path from the outer call
+ my_tmpl_dir = copy.copy(tmpl_dir)
+
+ props_elem = n.find("properties")
+ children = n.find("children")
+ command = n.find("command")
+
+ name = n.get("name")
+
+ node_type = n.tag
+
+ my_tmpl_dir.append(name)
+
+ if debug:
+ print("Name of the node: {};\n Created directory: ".format(name), end="")
+ os.makedirs(make_path(my_tmpl_dir), exist_ok=True)
+
+ props = get_properties(props_elem)
+
+ if node_type == "node":
+ if debug:
+ print("Processing node {}".format(name))
+
+ nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def")
+ if not os.path.exists(nodedef_path):
+ with open(nodedef_path, "w") as f:
+ f.write(make_node_def(props, command))
+ else:
+ # Something has already generated this file
+ pass
+
+ if children is not None:
+ inner_nodes = children.iterfind("*")
+ for inner_n in inner_nodes:
+ process_node(inner_n, my_tmpl_dir)
+ if node_type == "tagNode":
+ if debug:
+ print("Processing tag node {}".format(name))
+
+ os.makedirs(make_path(my_tmpl_dir), exist_ok=True)
+
+ nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def")
+ if not os.path.exists(nodedef_path):
+ with open(nodedef_path, "w") as f:
+ f.write('help: {0}\n'.format(props['help']))
+ else:
+ # Something has already generated this file
+ pass
+
+ # Create the inner node.tag part
+ my_tmpl_dir.append("node.tag")
+ os.makedirs(make_path(my_tmpl_dir), exist_ok=True)
+ if debug:
+ print("Created path for the tagNode: {}".format(make_path(my_tmpl_dir)), end="")
+
+ # Not sure if we want partially defined tag nodes, write the file unconditionally
+ with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "w") as f:
+ f.write(make_node_def(props, command))
+
+ if children is not None:
+ inner_nodes = children.iterfind("*")
+ for inner_n in inner_nodes:
+ process_node(inner_n, my_tmpl_dir)
+ else:
+ # This is a leaf node
+ if debug:
+ print("Processing leaf node {}".format(name))
+
+ with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "w") as f:
+ f.write(make_node_def(props, command))
+
+
+root = xml.getroot()
+
+nodes = root.iterfind("*")
+for n in nodes:
+ process_node(n, [output_dir])
diff --git a/scripts/build-command-templates b/scripts/build-command-templates
new file mode 100755
index 000000000..457adbec2
--- /dev/null
+++ b/scripts/build-command-templates
@@ -0,0 +1,306 @@
+#!/usr/bin/env python3
+#
+# build-command-template: converts new style command definitions in XML
+# to the old style (bunch of dirs and node.def's) command templates
+#
+# Copyright (C) 2017 VyOS maintainers <maintainers@vyos.net>
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+import sys
+import os
+import argparse
+import copy
+import functools
+
+from lxml import etree as ET
+
+# Defaults
+
+#validator_dir = "/usr/libexec/vyos/validators"
+validator_dir = "${vyos_validators_dir}"
+default_constraint_err_msg = "Invalid value"
+
+
+## Get arguments
+
+parser = argparse.ArgumentParser(description='Converts new-style XML interface definitions to old-style command templates')
+parser.add_argument('--debug', help='Enable debug information output', action='store_true')
+parser.add_argument('INPUT_FILE', type=str, help="XML interface definition file")
+parser.add_argument('SCHEMA_FILE', type=str, help="RelaxNG schema file")
+parser.add_argument('OUTPUT_DIR', type=str, help="Output directory")
+
+args = parser.parse_args()
+
+input_file = args.INPUT_FILE
+schema_file = args.SCHEMA_FILE
+output_dir = args.OUTPUT_DIR
+debug = args.debug
+
+#debug = True
+
+## Load and validate the inputs
+
+try:
+ xml = ET.parse(input_file)
+except Exception as e:
+ print("Failed to load interface definition file {0}".format(input_file))
+ print(e)
+ sys.exit(1)
+
+try:
+ relaxng_xml = ET.parse(schema_file)
+ validator = ET.RelaxNG(relaxng_xml)
+
+ if not validator.validate(xml):
+ print(validator.error_log)
+ print("Interface definition file {0} does not match the schema!".format(input_file))
+ sys.exit(1)
+except Exception as e:
+ print("Failed to load the XML schema {0}".format(schema_file))
+ print(e)
+ sys.exit(1)
+
+if not os.access(output_dir, os.W_OK):
+ print("The output directory {0} is not writeable".format(output_dir))
+ sys.exit(1)
+
+## If we got this far, everything must be ok and we can convert the file
+
+def make_path(l):
+ path = functools.reduce(os.path.join, l)
+ if debug:
+ print(path)
+ return path
+
+def get_properties(p):
+ props = {}
+
+ if p is None:
+ return props
+
+ # Get the help string
+ try:
+ props["help"] = p.find("help").text
+ except:
+ pass
+
+ # Get value help strings
+ try:
+ vhe = p.findall("valueHelp")
+ vh = []
+ for v in vhe:
+ vh.append( (v.find("format").text, v.find("description").text) )
+ props["val_help"] = vh
+ except:
+ props["val_help"] = []
+
+ # Get the constraint statements
+ error_msg = default_constraint_err_msg
+ # Get the error message if it's there
+ try:
+ error_msg = p.find("constraintErrorMessage").text
+ except:
+ pass
+
+ vce = p.find("constraint")
+ vc = []
+ if vce is not None:
+ # The old backend doesn't support multiple validators in OR mode
+ # so we emulate it
+
+ regexes = []
+ regex_elements = vce.findall("regex")
+ if regex_elements is not None:
+ regexes = list(map(lambda e: e.text.strip().replace('\\','\\\\'), regex_elements))
+ if "" in regexes:
+ print("Warning: empty regex, node will be accepting any value")
+
+ validator_elements = vce.findall("validator")
+ validators = []
+ if validator_elements is not None:
+ for v in validator_elements:
+ v_name = os.path.join(validator_dir, v.get("name"))
+
+ # XXX: lxml returns None for empty arguments
+ v_argument = None
+ try:
+ v_argument = v.get("argument")
+ except:
+ pass
+ if v_argument is None:
+ v_argument = ""
+
+ validators.append("{0} {1}".format(v_name, v_argument))
+
+
+ regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes))
+ validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators))
+ validator_script = '${vyos_libexec_dir}/validate-value'
+ validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, regex_args, validator_args, error_msg)
+
+ props["constraint"] = validator_string
+
+ # Get the completion help strings
+ try:
+ che = p.findall("completionHelp")
+ ch = ""
+ for c in che:
+ scripts = c.findall("script")
+ paths = c.findall("path")
+ lists = c.findall("list")
+
+ # Current backend doesn't support multiple allowed: tags
+ # so we get to emulate it
+ comp_exprs = []
+ 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))
+ for i in scripts:
+ comp_exprs.append("sh -c \"{0}\"".format(i.text))
+ comp_help = " && ".join(comp_exprs)
+ props["comp_help"] = comp_help
+ except:
+ props["comp_help"] = []
+
+ # Get priority
+ try:
+ props["priority"] = p.find("priority").text
+ except:
+ pass
+
+ # Get "multi"
+ if p.find("multi") is not None:
+ props["multi"] = True
+
+ # Get "valueless"
+ if p.find("valueless") is not None:
+ props["valueless"] = True
+
+ return props
+
+def make_node_def(props):
+ # XXX: replace with a template processor if it grows
+ # out of control
+
+ node_def = ""
+
+ if "tag" in props:
+ node_def += "tag:\n"
+
+ if "multi" in props:
+ node_def += "multi:\n"
+
+ if "type" in props:
+ # Will always be txt in practice if it's set
+ node_def += "type: {0}\n".format(props["type"])
+
+ if "priority" in props:
+ node_def += "priority: {0}\n".format(props["priority"])
+
+ if "help" in props:
+ node_def += "help: {0}\n".format(props["help"])
+
+ if "val_help" in props:
+ for v in props["val_help"]:
+ node_def += "val_help: {0}; {1}\n".format(v[0], v[1])
+
+ if "comp_help" in props:
+ node_def += "allowed: {0}\n".format(props["comp_help"])
+
+ if "constraint" in props:
+ node_def += "syntax:expression: {0}\n".format(props["constraint"])
+
+ if "owner" in props:
+ if "tag" in props:
+ node_def += "end: sudo sh -c \"VYOS_TAGNODE_VALUE='$VAR(@)' {0}\"\n".format(props["owner"])
+ else:
+ node_def += "end: sudo sh -c \"{0}\"\n".format(props["owner"])
+
+ if debug:
+ print("The contents of the node.def file:\n", node_def)
+
+ return node_def
+
+def process_node(n, tmpl_dir):
+ # Avoid mangling the path from the outer call
+ my_tmpl_dir = copy.copy(tmpl_dir)
+
+ props_elem = n.find("properties")
+ children = n.find("children")
+
+ name = n.get("name")
+ owner = n.get("owner")
+ node_type = n.tag
+
+ my_tmpl_dir.append(name)
+
+ if debug:
+ print("Name of the node: {0}. Created directory: {1}\n".format(name, "/".join(my_tmpl_dir)), end="")
+ os.makedirs(make_path(my_tmpl_dir), exist_ok=True)
+
+ props = get_properties(props_elem)
+ if owner:
+ props["owner"] = owner
+ # Type should not be set for non-tag, non-leaf nodes
+ # For non-valueless leaf nodes, set the type to txt: to make them have some type,
+ # actual value validation is handled by constraints translated to syntax:expression:
+ if node_type != "node":
+ if "valueless" not in props.keys():
+ props["type"] = "txt"
+ 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):
+ with open(nodedef_path, "w") as f:
+ f.write(make_node_def(props))
+ else:
+ # Something has already generated that file
+ pass
+
+
+ if node_type == "node":
+ inner_nodes = children.iterfind("*")
+ for inner_n in inner_nodes:
+ process_node(inner_n, my_tmpl_dir)
+ if node_type == "tagNode":
+ my_tmpl_dir.append("node.tag")
+ if debug:
+ print("Created path for the tagNode:", end="")
+ os.makedirs(make_path(my_tmpl_dir), exist_ok=True)
+ inner_nodes = children.iterfind("*")
+ for inner_n in inner_nodes:
+ process_node(inner_n, my_tmpl_dir)
+ else:
+ # This is a leaf node
+ pass
+
+
+root = xml.getroot()
+
+nodes = root.iterfind("*")
+for n in nodes:
+ if n.tag == "syntaxVersion":
+ continue
+ process_node(n, [output_dir])
diff --git a/scripts/build-component-versions b/scripts/build-component-versions
new file mode 100755
index 000000000..5362dbdd4
--- /dev/null
+++ b/scripts/build-component-versions
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+
+import sys
+import os
+import argparse
+import json
+
+from lxml import etree as ET
+
+parser = argparse.ArgumentParser()
+parser.add_argument('INPUT_DIR', type=str,
+ help="Directory containing XML interface definition files")
+parser.add_argument('OUTPUT_DIR', type=str,
+ help="Output directory for JSON file")
+
+args = parser.parse_args()
+
+input_dir = args.INPUT_DIR
+output_dir = args.OUTPUT_DIR
+
+version_dict = {}
+
+for filename in os.listdir(input_dir):
+ filepath = os.path.join(input_dir, filename)
+ print(filepath)
+ try:
+ xml = ET.parse(filepath)
+ except Exception as e:
+ print("Failed to load interface definition file {0}".format(filename))
+ print(e)
+ sys.exit(1)
+
+ root = xml.getroot()
+ version_data = root.iterfind("syntaxVersion")
+ for ver in version_data:
+ component = ver.get("component")
+ version = int(ver.get("version"))
+
+ v = version_dict.get(component)
+ if v is None:
+ version_dict[component] = version
+ elif version > v:
+ version_dict[component] = version
+
+out_file = os.path.join(output_dir, 'component-versions.json')
+with open(out_file, 'w') as f:
+ json.dump(version_dict, f, indent=4, sort_keys=True)
diff --git a/scripts/import-conf-mode-commands b/scripts/import-conf-mode-commands
new file mode 100755
index 000000000..996b31c9c
--- /dev/null
+++ b/scripts/import-conf-mode-commands
@@ -0,0 +1,255 @@
+#!/usr/bin/env python3
+#
+# build-command-template: converts old style commands definitions to XML
+#
+# Copyright (C) 2019 VyOS maintainers <maintainers@vyos.net>
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+
+import os
+import re
+import sys
+
+from lxml import etree
+
+
+# Node types
+NODE = 0
+LEAF_NODE = 1
+TAG_NODE = 2
+
+def parse_command_data(t):
+ regs = {
+ 'help': r'\bhelp:(.*)(?:\n|$)',
+ 'priority': r'\bpriority:(.*)(?:\n|$)',
+ 'type': r'\btype:(.*)(?:\n|$)',
+ 'syntax_expression_var': r'\bsyntax:expression: \$VAR\(\@\) in (.*)'
+ }
+
+ data = {'multi': False, 'help': ""}
+
+ for r in regs:
+ try:
+ data[r] = re.search(regs[r], t).group(1).strip()
+ except:
+ data[r] = None
+
+ # val_help is special: there can be multiple instances
+ val_help_strings = re.findall(r'\bval_help:(.*)(?:\n|$)', t)
+ val_help = []
+ for v in val_help_strings:
+ try:
+ fmt, msg = re.match(r'\s*(.*)\s*;\s*(.*)\s*(?:\n|$)', v).groups()
+ except:
+ fmt = "<text>"
+ msg = v
+ val_help.append((fmt, msg))
+ data['val_help'] = val_help
+
+ # multi is on/off
+ if re.match(r'\bmulti:', t):
+ data['multi'] = True
+
+ return(data)
+
+def walk(tree, base_path, name):
+ path = os.path.join(base_path, name)
+
+ contents = os.listdir(path)
+
+ # Determine node type and create XML element for the node
+ # Tag node dirs will always have 'node.tag' subdir and 'node.def' file
+ # Leaf node dirs have nothing but a 'node.def' file
+ # Everything that doesn't match either of these patterns is a normal node
+ if 'node.tag' in contents:
+ print("Creating a tag node from {0}".format(path))
+ elem = etree.Element('tagNode')
+ node_type = TAG_NODE
+ elif contents == ['node.def']:
+ print("Creating a leaf node from {0}".format(path))
+ elem = etree.Element('leafNode')
+ node_type = LEAF_NODE
+ else:
+ print("Creating a node from {0}".format(path))
+ elem = etree.Element('node')
+ node_type = NODE
+
+ # Read and parse the command definition data (the 'node.def' file)
+ with open(os.path.join(path, 'node.def'), 'r') as f:
+ node_def = f.read()
+ data = parse_command_data(node_def)
+
+ # Import the data into the properties element
+ props_elem = etree.Element('properties')
+
+ if data['priority']:
+ # Priority values sometimes come with comments that explain the value choice
+ try:
+ prio, prio_comment = re.match(r'\s*(\d+)\s*#(.*)', data['priority']).groups()
+ except:
+ prio = data['priority'].strip()
+ prio_comment = None
+ prio_elem = etree.Element('priority')
+ prio_elem.text = prio
+ props_elem.append(prio_elem)
+ if prio_comment:
+ prio_comment_elem = etree.Comment(prio_comment)
+ props_elem.append(prio_comment_elem)
+
+ if data['multi']:
+ multi_elem = etree.Element('multi')
+ props_elem.append(multi_elem)
+
+ if data['help']:
+ help_elem = etree.Element('help')
+ help_elem.text = data['help']
+ props_elem.append(help_elem)
+
+ # For leaf nodes, absense of a type: tag means they take no values
+ # For any other nodes, it doesn't mean anything
+ if not data['type'] and (node_type == LEAF_NODE):
+ valueless = etree.Element('valueless')
+ props_elem.append(valueless)
+
+ # There can be only one constraint element in the definition
+ # Create it now, we'll modify it in the next two cases, then append
+ constraint_elem = etree.Element('constraint')
+ has_constraint = False
+
+ # Add regexp field for multiple options
+ if data['syntax_expression_var']:
+ regex = etree.Element('regex')
+ constraint_error=etree.Element('constraintErrorMessage')
+ values = re.search(r'(.+) ; (.+)', data['syntax_expression_var']).group(1)
+ message = re.search(r'(.+) ; (.+)', data['syntax_expression_var']).group(2)
+ values = re.findall(r'\"(.+?)\"', values)
+ regex.text = '|'.join(values)
+ constraint_error.text = re.sub('\".*?VAR.*?\"', '', message)
+ constraint_error.text = re.sub(r'[\"|\\]', '', message)
+ constraint_elem.append(regex)
+ props_elem.append(constraint_elem)
+ props_elem.append(constraint_error)
+
+ if data['val_help']:
+ for vh in data['val_help']:
+ vh_elem = etree.Element('valueHelp')
+
+ vh_fmt_elem = etree.Element('format')
+ # Many commands use special "u32:<start>-<end>" format for ranges
+ if re.match(r'u32:', vh[0]):
+ vh_fmt = re.match(r'u32:(.*)', vh[0]).group(1).strip()
+
+ # If valid range of values is specified in val_help, we can automatically
+ # create a constraint for it
+ # Extracting it from syntax:expression: would be much more complicated
+ vh_validator = etree.Element('validator')
+ vh_validator.set("name", "numeric")
+ vh_validator.set("argument", "--range {0}".format(vh_fmt))
+ constraint_elem.append(vh_validator)
+ has_constraint = True
+ else:
+ vh_fmt = vh[0]
+ vh_fmt_elem.text = vh_fmt
+
+ vh_help_elem = etree.Element('description')
+ vh_help_elem.text = vh[1]
+
+ vh_elem.append(vh_fmt_elem)
+ vh_elem.append(vh_help_elem)
+ props_elem.append(vh_elem)
+
+ # Translate the "type:" to the new validator system
+ if data['type']:
+ t = data['type']
+ if t == 'txt':
+ # Can't infer anything from the generic "txt" type
+ pass
+ else:
+ validator = etree.Element('validator')
+ if t == 'u32':
+ validator.set('name', 'numeric')
+ validator.set('argument', '--non-negative')
+ elif t == 'ipv4':
+ validator.set('name', 'ipv4-address')
+ elif t == 'ipv4net':
+ validator.set('name', 'ipv4-prefix')
+ elif t == 'ipv6':
+ validator.set('name', 'ipv6-address')
+ elif t == 'ipv6net':
+ validator.set('name', 'ipv6-prefix')
+ elif t == 'macaddr':
+ validator.set('name', 'mac-address')
+ else:
+ print("Warning: unsupported type \'{0}\'".format(t))
+ validator = None
+
+ if (validator is not None) and (not has_constraint):
+ # If has_constraint is true, it means a more specific validator
+ # was already extracted from another option
+ constraint_elem.append(validator)
+ has_constraint = True
+
+ if has_constraint:
+ props_elem.append(constraint_elem)
+
+ elem.append(props_elem)
+
+ elem.set("name", name)
+
+ if node_type != LEAF_NODE:
+ children = etree.Element('children')
+
+ # Create the next level dir path,
+ # accounting for the "virtual" node.tag subdir for tag nodes
+ next_level = path
+ if node_type == TAG_NODE:
+ next_level = os.path.join(path, 'node.tag')
+
+ # Walk the subdirs of the next level
+ for d in os.listdir(next_level):
+ dp = os.path.join(next_level, d)
+ if os.path.isdir(dp):
+ walk(children, next_level, d)
+
+ elem.append(children)
+
+ tree.append(elem)
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Usage: {0} <base path>".format(sys.argv[0]))
+ sys.exit(1)
+ else:
+ base_path = sys.argv[1]
+
+ root = etree.Element('interfaceDefinition')
+ contents = os.listdir(base_path)
+ elem = etree.Element('node')
+ elem.set('name', os.path.basename(base_path))
+ children = etree.Element('children')
+
+ for c in contents:
+ path = os.path.join(base_path, c)
+ if os.path.isdir(path):
+ walk(children, base_path, c)
+
+ elem.append(children)
+ root.append(elem)
+
+ xml_data = etree.tostring(root, pretty_print=True).decode()
+ with open('output.xml', 'w') as f:
+ f.write(xml_data)
diff --git a/bin/vyos-smoketest b/smoketest/bin/vyos-smoketest
index cb039db42..cb039db42 100755
--- a/bin/vyos-smoketest
+++ b/smoketest/bin/vyos-smoketest
diff --git a/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index 14ec7e137..14ec7e137 100644
--- a/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
diff --git a/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py
index e3d3b25ee..e3d3b25ee 100755
--- a/scripts/cli/test_interfaces_bonding.py
+++ b/smoketest/scripts/cli/test_interfaces_bonding.py
diff --git a/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py
index bc0bb69c6..bc0bb69c6 100755
--- a/scripts/cli/test_interfaces_bridge.py
+++ b/smoketest/scripts/cli/test_interfaces_bridge.py
diff --git a/scripts/cli/test_interfaces_dummy.py b/smoketest/scripts/cli/test_interfaces_dummy.py
index 01942fc89..01942fc89 100755
--- a/scripts/cli/test_interfaces_dummy.py
+++ b/smoketest/scripts/cli/test_interfaces_dummy.py
diff --git a/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py
index 761ec7506..761ec7506 100755
--- a/scripts/cli/test_interfaces_ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_ethernet.py
diff --git a/scripts/cli/test_interfaces_geneve.py b/smoketest/scripts/cli/test_interfaces_geneve.py
index f84a55f86..f84a55f86 100755
--- a/scripts/cli/test_interfaces_geneve.py
+++ b/smoketest/scripts/cli/test_interfaces_geneve.py
diff --git a/scripts/cli/test_interfaces_l2tpv3.py b/smoketest/scripts/cli/test_interfaces_l2tpv3.py
index d8655d157..d8655d157 100755
--- a/scripts/cli/test_interfaces_l2tpv3.py
+++ b/smoketest/scripts/cli/test_interfaces_l2tpv3.py
diff --git a/scripts/cli/test_interfaces_loopback.py b/smoketest/scripts/cli/test_interfaces_loopback.py
index ba428b5d3..ba428b5d3 100755
--- a/scripts/cli/test_interfaces_loopback.py
+++ b/smoketest/scripts/cli/test_interfaces_loopback.py
diff --git a/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py
index 0f1b6486d..0f1b6486d 100755
--- a/scripts/cli/test_interfaces_macsec.py
+++ b/smoketest/scripts/cli/test_interfaces_macsec.py
diff --git a/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py
index 822f05de6..822f05de6 100755
--- a/scripts/cli/test_interfaces_pppoe.py
+++ b/smoketest/scripts/cli/test_interfaces_pppoe.py
diff --git a/scripts/cli/test_interfaces_pseudo_ethernet.py b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py
index bc2e6e7eb..bc2e6e7eb 100755
--- a/scripts/cli/test_interfaces_pseudo_ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py
diff --git a/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py
index 7611ffe26..7611ffe26 100755
--- a/scripts/cli/test_interfaces_tunnel.py
+++ b/smoketest/scripts/cli/test_interfaces_tunnel.py
diff --git a/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py
index 2628e0285..2628e0285 100755
--- a/scripts/cli/test_interfaces_vxlan.py
+++ b/smoketest/scripts/cli/test_interfaces_vxlan.py
diff --git a/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py
index 0c32a4696..0c32a4696 100755
--- a/scripts/cli/test_interfaces_wireguard.py
+++ b/smoketest/scripts/cli/test_interfaces_wireguard.py
diff --git a/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py
index fae233244..fae233244 100755
--- a/scripts/cli/test_interfaces_wireless.py
+++ b/smoketest/scripts/cli/test_interfaces_wireless.py
diff --git a/scripts/cli/test_interfaces_wirelessmodem.py b/smoketest/scripts/cli/test_interfaces_wirelessmodem.py
index 40cd03b93..40cd03b93 100755
--- a/scripts/cli/test_interfaces_wirelessmodem.py
+++ b/smoketest/scripts/cli/test_interfaces_wirelessmodem.py
diff --git a/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py
index 416810e40..416810e40 100755
--- a/scripts/cli/test_nat.py
+++ b/smoketest/scripts/cli/test_nat.py
diff --git a/scripts/cli/test_service_bcast-relay.py b/smoketest/scripts/cli/test_service_bcast-relay.py
index fe4531c3b..fe4531c3b 100755
--- a/scripts/cli/test_service_bcast-relay.py
+++ b/smoketest/scripts/cli/test_service_bcast-relay.py
diff --git a/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py
index be52360ed..be52360ed 100755
--- a/scripts/cli/test_service_dns_dynamic.py
+++ b/smoketest/scripts/cli/test_service_dns_dynamic.py
diff --git a/scripts/cli/test_service_mdns-repeater.py b/smoketest/scripts/cli/test_service_mdns-repeater.py
index 18900b6d2..18900b6d2 100755
--- a/scripts/cli/test_service_mdns-repeater.py
+++ b/smoketest/scripts/cli/test_service_mdns-repeater.py
diff --git a/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py
index 901ca792d..901ca792d 100755
--- a/scripts/cli/test_service_pppoe-server.py
+++ b/smoketest/scripts/cli/test_service_pppoe-server.py
diff --git a/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py
index ec2110c8a..ec2110c8a 100755
--- a/scripts/cli/test_service_router-advert.py
+++ b/smoketest/scripts/cli/test_service_router-advert.py
diff --git a/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py
index fb5f5393f..fb5f5393f 100755
--- a/scripts/cli/test_service_snmp.py
+++ b/smoketest/scripts/cli/test_service_snmp.py
diff --git a/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py
index 3ee498f3d..3ee498f3d 100755
--- a/scripts/cli/test_service_ssh.py
+++ b/smoketest/scripts/cli/test_service_ssh.py
diff --git a/scripts/cli/test_system_lcd.py b/smoketest/scripts/cli/test_system_lcd.py
index 931a91c53..931a91c53 100755
--- a/scripts/cli/test_system_lcd.py
+++ b/smoketest/scripts/cli/test_system_lcd.py
diff --git a/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index 3c4b1fa28..3c4b1fa28 100755
--- a/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
diff --git a/scripts/cli/test_system_nameserver.py b/smoketest/scripts/cli/test_system_nameserver.py
index 9040be072..9040be072 100755
--- a/scripts/cli/test_system_nameserver.py
+++ b/smoketest/scripts/cli/test_system_nameserver.py
diff --git a/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py
index 856a28916..856a28916 100755
--- a/scripts/cli/test_system_ntp.py
+++ b/smoketest/scripts/cli/test_system_ntp.py
diff --git a/scripts/cli/test_vpn_anyconnect.py b/smoketest/scripts/cli/test_vpn_anyconnect.py
index dd8ab1609..dd8ab1609 100755
--- a/scripts/cli/test_vpn_anyconnect.py
+++ b/smoketest/scripts/cli/test_vpn_anyconnect.py
diff --git a/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py
index efa095b30..efa095b30 100755
--- a/scripts/cli/test_vrf.py
+++ b/smoketest/scripts/cli/test_vrf.py
diff --git a/scripts/system/test_module_load.py b/smoketest/scripts/system/test_module_load.py
index 59c3e0b6a..59c3e0b6a 100755
--- a/scripts/system/test_module_load.py
+++ b/smoketest/scripts/system/test_module_load.py
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 000000000..1258da817
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,21 @@
+sonar.projectKey=vyos:vyos-1x
+sonar.projectName=vyos-1x
+sonar.projectVersion=1.2.0
+sonar.organization=vyos
+
+sonar.sources=src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators
+sonar.language=py
+sonar.sourceEncoding=UTF-8
+
+sonar.links.homepage=https://github.com/vyos/vyos-1x
+sonar.links.ci=https://ci.vyos.net/job/vyos-1x/
+sonar.links.scm=https://github.com/vyos/vyos-1x
+sonar.links.issue=https://phabricator.vyos.net/
+
+sonar.host.url=https://sonarcloud.io
+
+sonar.python.pylint=/usr/local/bin/pylint
+sonar.python.pylint_config=.pylintrc
+sonar.python.pylint.reportPath=pylint-report.txt
+sonar.python.xunit.reportPath=nosetests.xml
+sonar.python.coverage.reportPath=coverage.xml
diff --git a/sphinx/Makefile b/sphinx/Makefile
new file mode 100644
index 000000000..1e344639e
--- /dev/null
+++ b/sphinx/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/VyOS.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/VyOS.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/VyOS"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/VyOS"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/sphinx/source/conf.py b/sphinx/source/conf.py
new file mode 100644
index 000000000..3447259b3
--- /dev/null
+++ b/sphinx/source/conf.py
@@ -0,0 +1,261 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# VyOS documentation build configuration file, created by
+# sphinx-quickstart on Wed Jun 20 01:14:27 2018.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'VyOS'
+copyright = '2018, VyOS maintainers and contributors'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '1.2.0'
+# The full version, including alpha/beta/rc tags.
+release = '1.2.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'VyOSdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'VyOS.tex', 'VyOS Documentation',
+ 'VyOS maintainers and contributors', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'vyos', 'VyOS Documentation',
+ ['VyOS maintainers and contributors'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'VyOS', 'VyOS Documentation',
+ 'VyOS maintainers and contributors', 'VyOS', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
diff --git a/sphinx/source/index.rst b/sphinx/source/index.rst
new file mode 100644
index 000000000..c31cac4e5
--- /dev/null
+++ b/sphinx/source/index.rst
@@ -0,0 +1,22 @@
+.. VyOS documentation master file, created by
+ sphinx-quickstart on Wed Jun 20 01:14:27 2018.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to VyOS's documentation!
+================================
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/src/completion/list_disks.py b/src/completion/list_disks.py
new file mode 100755
index 000000000..ff1135e23
--- /dev/null
+++ b/src/completion/list_disks.py
@@ -0,0 +1,36 @@
+#!/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/>.
+
+# Completion script used by show disks to collect physical disk
+
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-e", "--exclude", type=str, help="Exclude specified device from the result list")
+args = parser.parse_args()
+
+disks = set()
+with open('/proc/partitions') as partitions_file:
+ for line in partitions_file:
+ fields = line.strip().split()
+ if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name':
+ disks.add(fields[3])
+
+if args.exclude:
+ disks.remove(args.exclude)
+
+for disk in disks:
+ print(disk)
diff --git a/src/completion/list_dumpable_interfaces.py b/src/completion/list_dumpable_interfaces.py
new file mode 100755
index 000000000..101c92fbe
--- /dev/null
+++ b/src/completion/list_dumpable_interfaces.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python3
+
+# Extract the list of interfaces available for traffic dumps from tcpdump -D
+
+import re
+
+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
new file mode 100755
index 000000000..e27281433
--- /dev/null
+++ b/src/completion/list_interfaces.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+
+import sys
+import argparse
+from vyos.ifconfig import Section
+
+
+def matching(feature):
+ for section in Section.feature(feature):
+ for intf in Section.interfaces(section):
+ yield intf
+
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("-t", "--type", type=str, help="List interfaces of specific type")
+group.add_argument("-b", "--broadcast", action="store_true", help="List all broadcast interfaces")
+group.add_argument("-br", "--bridgeable", action="store_true", help="List all bridgeable interfaces")
+group.add_argument("-bo", "--bondable", action="store_true", help="List all bondable interfaces")
+
+args = parser.parse_args()
+
+if args.type:
+ try:
+ interfaces = Section.interfaces(args.type)
+ print(" ".join(interfaces))
+ except ValueError as e:
+ print(e, file=sys.stderr)
+ print("")
+
+elif args.broadcast:
+ print(" ".join(matching("broadcast")))
+
+elif args.bridgeable:
+ print(" ".join(matching("bridgeable")))
+
+elif args.bondable:
+ # we need to filter out VLAN interfaces identified by a dot (.) in their name
+ print(" ".join([intf for intf in matching("bondable") if '.' not in intf]))
+
+else:
+ print(" ".join(Section.interfaces()))
diff --git a/src/completion/list_ipoe.py b/src/completion/list_ipoe.py
new file mode 100755
index 000000000..c386b46a2
--- /dev/null
+++ b/src/completion/list_ipoe.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python3
+
+import argparse
+from vyos.util import popen
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--selector', help='Selector: username|ifname|sid', required=True)
+ args = parser.parse_args()
+
+ output, err = popen("accel-cmd -p 2002 show sessions {0}".format(args.selector))
+ if not err:
+ res = output.split("\r\n")
+ # Delete header from list
+ del res[:2]
+ print(' '.join(res))
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_ntp_servers.sh b/src/completion/list_ntp_servers.sh
new file mode 100755
index 000000000..d0977fbd6
--- /dev/null
+++ b/src/completion/list_ntp_servers.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+# Completion script used to select specific NTP server
+/bin/cli-shell-api -- listEffectiveNodes system ntp server | sed "s/'//g"
diff --git a/src/completion/list_openvpn_clients.py b/src/completion/list_openvpn_clients.py
new file mode 100755
index 000000000..177ac90c9
--- /dev/null
+++ b/src/completion/list_openvpn_clients.py
@@ -0,0 +1,57 @@
+#!/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 sys
+import argparse
+
+from vyos.ifconfig import Section
+
+def get_client_from_interface(interface):
+ clients = []
+ with open('/opt/vyatta/etc/openvpn/status/' + interface + '.status', 'r') as f:
+ dump = False
+ for line in f:
+ if line.startswith("Common Name,"):
+ dump = True
+ continue
+ if line.startswith("ROUTING TABLE"):
+ dump = False
+ continue
+ if dump:
+ # client entry in this file looks like
+ # client1,172.18.202.10:47495,2957,2851,Sat Aug 17 00:07:11 2019
+ # we are only interested in the client name 'client1'
+ clients.append(line.split(',')[0])
+
+ return clients
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-i", "--interface", type=str, help="List connected clients per interface")
+ parser.add_argument("-a", "--all", action='store_true', help="List all connected OpenVPN clients")
+ args = parser.parse_args()
+
+ clients = []
+
+ if args.interface:
+ clients = get_client_from_interface(args.interface)
+ elif args.all:
+ for interface in Section.interfaces("openvpn"):
+ clients += get_client_from_interface(interface)
+
+ print(" ".join(clients))
+
diff --git a/src/completion/list_raidset.sh b/src/completion/list_raidset.sh
new file mode 100755
index 000000000..9ff3523aa
--- /dev/null
+++ b/src/completion/list_raidset.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+echo -n `cat /proc/partitions | grep md | awk '{ print $4 }'`
diff --git a/src/completion/list_wireless_phys.sh b/src/completion/list_wireless_phys.sh
new file mode 100755
index 000000000..70b8d1ff9
--- /dev/null
+++ b/src/completion/list_wireless_phys.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+if [ -d /sys/class/ieee80211 ]; then
+ ls -x /sys/class/ieee80211
+fi
diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py
new file mode 100755
index 000000000..aac07bd80
--- /dev/null
+++ b/src/conf_mode/arp.py
@@ -0,0 +1,104 @@
+#!/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 syslog as sl
+
+from vyos.config import Config
+from vyos.util import call
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+arp_cmd = '/usr/sbin/arp'
+
+def get_config():
+ c = Config()
+ if not c.exists('protocols static arp'):
+ return None
+
+ c.set_level('protocols static')
+ config_data = {}
+
+ for ip_addr in c.list_nodes('arp'):
+ config_data.update(
+ {
+ ip_addr : c.return_value('arp ' + ip_addr + ' hwaddr')
+ }
+ )
+
+ return config_data
+
+def generate(c):
+ c_eff = Config()
+ c_eff.set_level('protocols static')
+ c_eff_cnf = {}
+ for ip_addr in c_eff.list_effective_nodes('arp'):
+ c_eff_cnf.update(
+ {
+ ip_addr : c_eff.return_effective_value('arp ' + ip_addr + ' hwaddr')
+ }
+ )
+
+ config_data = {
+ 'remove' : [],
+ 'update' : {}
+ }
+ ### removal
+ if c == None:
+ for ip_addr in c_eff_cnf:
+ config_data['remove'].append(ip_addr)
+ else:
+ for ip_addr in c_eff_cnf:
+ if not ip_addr in c or c[ip_addr] == None:
+ config_data['remove'].append(ip_addr)
+
+ ### add/update
+ if c != None:
+ for ip_addr in c:
+ if not ip_addr in c_eff_cnf:
+ config_data['update'][ip_addr] = c[ip_addr]
+ if ip_addr in c_eff_cnf:
+ if c[ip_addr] != c_eff_cnf[ip_addr] and c[ip_addr] != None:
+ config_data['update'][ip_addr] = c[ip_addr]
+
+ return config_data
+
+def apply(c):
+ for ip_addr in c['remove']:
+ sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr)
+ 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])
+ updated = c['update'][ip_addr]
+ call(f'{arp_cmd} -s {ip_addr} {updated}')
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ ## syntax verification is done via cli
+ config = generate(c)
+ apply(config)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
new file mode 100755
index 000000000..a3e141a00
--- /dev/null
+++ b/src/conf_mode/bcast_relay.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 glob import glob
+from netifaces import interfaces
+from sys import exit
+
+from vyos.config import Config
+from vyos.util import call
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file_base = r'/etc/default/udp-broadcast-relay'
+
+def get_config():
+ conf = Config()
+ base = ['service', 'broadcast-relay']
+
+ relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ return relay
+
+def verify(relay):
+ if not relay or 'disabled' in relay:
+ return None
+
+ for instance, config in relay.get('id', {}).items():
+ # we don't have to check this instance when it's disabled
+ if 'disabled' in config:
+ continue
+
+ # we certainly require a UDP port to listen to
+ if 'port' not in config:
+ raise ConfigError(f'Port number mandatory for udp broadcast relay "{instance}"')
+
+ # if only oone interface is given it's a string -> move to list
+ if isinstance(config.get('interface', []), str):
+ config['interface'] = [ config['interface'] ]
+ # Relaying data without two interface is kinda senseless ...
+ if len(config.get('interface', [])) < 2:
+ raise ConfigError('At least two interfaces are required for udp broadcast relay "{instance}"')
+
+ for interface in config.get('interface', []):
+ if interface not in interfaces():
+ raise ConfigError('Interface "{interface}" does not exist!')
+
+ return None
+
+def generate(relay):
+ if not relay or 'disabled' in relay:
+ return None
+
+ for config in glob(config_file_base + '*'):
+ os.remove(config)
+
+ for instance, config in relay.get('id').items():
+ # we don't have to check this instance when it's disabled
+ if 'disabled' in config:
+ continue
+
+ config['instance'] = instance
+ render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.tmpl', config)
+
+ return None
+
+def apply(relay):
+ # first stop all running services
+ call('systemctl stop udp-broadcast-relay@*.service')
+
+ if not relay or 'disable' in relay:
+ return None
+
+ # start only required service instances
+ for instance, config in relay.get('id').items():
+ # we don't have to check this instance when it's disabled
+ if 'disabled' in config:
+ continue
+
+ call(f'systemctl start udp-broadcast-relay@{instance}.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/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
new file mode 100755
index 000000000..f093a005e
--- /dev/null
+++ b/src/conf_mode/dhcp_relay.py
@@ -0,0 +1,126 @@
+#!/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 sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/dhcp-relay/dhcp.conf'
+
+default_config_data = {
+ 'interface': [],
+ 'server': [],
+ 'options': [],
+ 'hop_count': '10',
+ 'relay_agent_packets': 'forward'
+}
+
+def get_config():
+ relay = default_config_data
+ conf = Config()
+ if not conf.exists(['service', 'dhcp-relay']):
+ return None
+ else:
+ conf.set_level(['service', 'dhcp-relay'])
+
+ # Network interfaces to listen on
+ 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'])
+
+ conf.set_level(['service', 'dhcp-relay', 'relay-options'])
+
+ 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.
+ # This might be done to allow sufficient space for addition of relay agent
+ # 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'])
+ relay['options'].append(size)
+
+ # Control the handling of incoming DHCPv4 packets which already contain
+ # relay agent options. If such a packet does not have giaddr set in its
+ # header, the DHCP standard requires that the packet be discarded. However,
+ # if giaddr is set, the relay agent may handle the situation in four ways:
+ # It may append its own set of relay options to the packet, leaving the
+ # supplied option field intact; it may replace the existing agent option
+ # 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'])
+ relay['options'].append(pkt)
+
+ return relay
+
+def verify(relay):
+ # bail out early - looks like removal from running config
+ if relay is None:
+ return None
+
+ if 'lo' in relay['interface']:
+ raise ConfigError('DHCP relay does not support the loopback interface.')
+
+ if len(relay['server']) == 0:
+ raise ConfigError('No DHCP relay server(s) configured.\n' \
+ 'At least one DHCP relay server required.')
+
+ return None
+
+def generate(relay):
+ # bail out early - looks like removal from running config
+ if not relay:
+ return None
+
+ render(config_file, 'dhcp-relay/config.tmpl', relay)
+ return None
+
+def apply(relay):
+ if relay:
+ call('systemctl restart isc-dhcp-relay.service')
+ else:
+ # DHCP relay support is removed in the commit
+ call('systemctl stop isc-dhcp-relay.service')
+ if os.path.exists(config_file):
+ os.unlink(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/dhcp_server.py b/src/conf_mode/dhcp_server.py
new file mode 100755
index 000000000..0eaa14c5b
--- /dev/null
+++ b/src/conf_mode/dhcp_server.py
@@ -0,0 +1,625 @@
+#!/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 ipaddress import ip_address, ip_network
+from socket import inet_ntoa
+from struct import pack
+from sys import exit
+
+from vyos.config import Config
+from vyos.validate import is_subnet_connected
+from vyos import ConfigError
+from vyos.template import render
+from vyos.util import call, chown
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/dhcp-server/dhcpd.conf'
+
+default_config_data = {
+ 'disabled': False,
+ 'ddns_enable': False,
+ 'global_parameters': [],
+ 'hostfile_update': False,
+ 'host_decl_name': False,
+ 'static_route': False,
+ 'wpad': False,
+ 'shared_network': [],
+}
+
+def dhcp_slice_range(exclude_list, range_list):
+ """
+ This function is intended to slice a DHCP range. What does it mean?
+
+ Lets assume we have a DHCP range from '192.0.2.1' to '192.0.2.100'
+ but want to exclude address '192.0.2.74' and '192.0.2.75'. We will
+ pass an input 'range_list' in the format:
+ [{'start' : '192.0.2.1', 'stop' : '192.0.2.100' }]
+ and we will receive an output list of:
+ [{'start' : '192.0.2.1' , 'stop' : '192.0.2.73' },
+ {'start' : '192.0.2.76', 'stop' : '192.0.2.100' }]
+ The resulting list can then be used in turn to build the proper dhcpd
+ configuration file.
+ """
+ output = []
+ # exclude list must be sorted for this to work
+ exclude_list = sorted(exclude_list)
+ for ra in range_list:
+ range_start = ra['start']
+ range_stop = ra['stop']
+ range_last_exclude = ''
+
+ for e in exclude_list:
+ if (ip_address(e) >= ip_address(range_start)) and \
+ (ip_address(e) <= ip_address(range_stop)):
+ range_last_exclude = e
+
+ for e in exclude_list:
+ if (ip_address(e) >= ip_address(range_start)) and \
+ (ip_address(e) <= ip_address(range_stop)):
+
+ # Build new IP address range ending one IP address before exclude address
+ r = {
+ 'start' : range_start,
+ 'stop' : str(ip_address(e) -1)
+ }
+ # On the next run our IP address range will start one address after the exclude address
+ range_start = str(ip_address(e) + 1)
+
+ # on subsequent exclude addresses we can not
+ # append them to our output
+ if not (ip_address(r['start']) > ip_address(r['stop'])):
+ # Everything is fine, add range to result
+ output.append(r)
+
+ # Take care of last IP address range spanning from the last exclude
+ # address (+1) to the end of the initial configured range
+ if ip_address(e) == ip_address(range_last_exclude):
+ r = {
+ 'start': str(ip_address(e) + 1),
+ 'stop': str(range_stop)
+ }
+ if not (ip_address(r['start']) > ip_address(r['stop'])):
+ output.append(r)
+ else:
+ # if we have no exclude in the whole range - we just take the range
+ # as it is
+ if not range_last_exclude:
+ if ra not in output:
+ output.append(ra)
+
+ 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()
+ if not conf.exists('service dhcp-server'):
+ return None
+ else:
+ conf.set_level('service dhcp-server')
+
+ # check for global disable of DHCP service
+ if conf.exists('disable'):
+ dhcp['disabled'] = True
+
+ # check for global dynamic DNS upste
+ if conf.exists('dynamic-dns-update'):
+ dhcp['ddns_enable'] = True
+
+ # HACKS AND TRICKS
+ #
+ # check for global 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters from any user
+ if conf.exists('global-parameters'):
+ dhcp['global_parameters'] = conf.return_values('global-parameters')
+
+ # check for global DHCP server updating /etc/host per lease
+ if conf.exists('hostfile-update'):
+ dhcp['hostfile_update'] = True
+
+ # If enabled every host declaration within that scope, the name provided
+ # for the host declaration will be supplied to the client as its hostname.
+ if conf.exists('host-decl-name'):
+ dhcp['host_decl_name'] = True
+
+ # check for multiple, shared networks served with DHCP addresses
+ if conf.exists('shared-network-name'):
+ for network in conf.list_nodes('shared-network-name'):
+ conf.set_level('service dhcp-server shared-network-name {0}'.format(network))
+ config = {
+ 'name': network,
+ 'authoritative': False,
+ 'description': '',
+ 'disabled': False,
+ 'network_parameters': [],
+ 'subnet': []
+ }
+ # check if DHCP server should be authoritative on this network
+ if conf.exists('authoritative'):
+ config['authoritative'] = True
+
+ # A description for this given network
+ if conf.exists('description'):
+ config['description'] = conf.return_value('description')
+
+ # If disabled, the shared-network configuration becomes inactive in
+ # the running DHCP server instance
+ if conf.exists('disable'):
+ config['disabled'] = True
+
+ # HACKS AND TRICKS
+ #
+ # check for 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters
+ # from any user
+ #
+ # deprecate this and issue a warning like we do for DNS forwarding?
+ if conf.exists('shared-network-parameters'):
+ config['network_parameters'] = conf.return_values('shared-network-parameters')
+
+ # check for multiple subnet configurations in a shared network
+ # config segment
+ if conf.exists('subnet'):
+ for net in conf.list_nodes('subnet'):
+ conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net))
+ subnet = {
+ 'network': net,
+ 'address': str(ip_network(net).network_address),
+ 'netmask': str(ip_network(net).netmask),
+ 'bootfile_name': '',
+ 'bootfile_server': '',
+ 'client_prefix_length': '',
+ 'default_router': '',
+ 'rfc3442_default_router': '',
+ 'dns_server': [],
+ 'domain_name': '',
+ 'domain_search': [],
+ 'exclude': [],
+ 'failover_local_addr': '',
+ 'failover_name': '',
+ 'failover_peer_addr': '',
+ 'failover_status': '',
+ 'ip_forwarding': False,
+ 'lease': '86400',
+ 'ntp_server': [],
+ 'pop_server': [],
+ 'server_identifier': '',
+ 'smtp_server': [],
+ 'range': [],
+ 'static_mapping': [],
+ 'static_subnet': '',
+ 'static_router': '',
+ 'static_route': '',
+ 'subnet_parameters': [],
+ 'tftp_server': '',
+ 'time_offset': '',
+ 'time_server': [],
+ 'wins_server': [],
+ 'wpad_url': ''
+ }
+
+ # Used to identify a bootstrap file
+ if conf.exists('bootfile-name'):
+ subnet['bootfile_name'] = conf.return_value('bootfile-name')
+
+ # Specify host address of the server from which the initial boot file
+ # (specified above) is to be loaded. Should be a numeric IP address or
+ # domain name.
+ if conf.exists('bootfile-server'):
+ subnet['bootfile_server'] = conf.return_value('bootfile-server')
+
+ # The subnet mask option specifies the client's subnet mask as per RFC 950. If no subnet
+ # mask option is provided anywhere in scope, as a last resort dhcpd will use the subnet
+ # mask from the subnet declaration for the network on which an address is being assigned.
+ 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'] = 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.
+ if conf.exists('dns-server'):
+ subnet['dns_server'] = conf.return_values('dns-server')
+
+ # Option specifies the domain name that client should use when resolving hostnames
+ # via the Domain Name System.
+ if conf.exists('domain-name'):
+ subnet['domain_name'] = conf.return_value('domain-name')
+
+ # The domain-search option specifies a 'search list' of Domain Names to be used
+ # by the client to locate not-fully-qualified domain names.
+ if conf.exists('domain-search'):
+ for domain in conf.return_values('domain-search'):
+ subnet['domain_search'].append('"' + domain + '"')
+
+ # IP address (local) for failover peer to connect
+ if conf.exists('failover local-address'):
+ subnet['failover_local_addr'] = conf.return_value('failover local-address')
+
+ # DHCP failover peer name
+ if conf.exists('failover name'):
+ subnet['failover_name'] = conf.return_value('failover name')
+
+ # IP address (remote) of failover peer
+ if conf.exists('failover peer-address'):
+ subnet['failover_peer_addr'] = conf.return_value('failover peer-address')
+
+ # DHCP failover peer status (primary|secondary)
+ if conf.exists('failover status'):
+ subnet['failover_status'] = conf.return_value('failover status')
+
+ # Option specifies whether the client should configure its IP layer for packet
+ # forwarding
+ if conf.exists('ip-forwarding'):
+ subnet['ip_forwarding'] = True
+
+ # Time should be the length in seconds that will be assigned to a lease if the
+ # client requesting the lease does not ask for a specific expiration time
+ if conf.exists('lease'):
+ subnet['lease'] = conf.return_value('lease')
+
+ # Specifies a list of IP addresses indicating NTP (RFC 5905) servers available
+ # to the client.
+ if conf.exists('ntp-server'):
+ subnet['ntp_server'] = conf.return_values('ntp-server')
+
+ # POP3 server option specifies a list of POP3 servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('pop-server'):
+ subnet['pop_server'] = conf.return_values('pop-server')
+
+ # DHCP servers include this option in the DHCPOFFER in order to allow the client
+ # to distinguish between lease offers. DHCP clients use the contents of the
+ # 'server identifier' field as the destination address for any DHCP messages
+ # unicast to the DHCP server
+ if conf.exists('server-identifier'):
+ subnet['server_identifier'] = conf.return_value('server-identifier')
+
+ # SMTP server option specifies a list of SMTP servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('smtp-server'):
+ subnet['smtp_server'] = conf.return_values('smtp-server')
+
+ # For any subnet on which addresses will be assigned dynamically, there must be at
+ # least one range statement. The range statement gives the lowest and highest IP
+ # addresses in a range. All IP addresses in the range should be in the subnet in
+ # which the range statement is declared.
+ if conf.exists('range'):
+ for range in conf.list_nodes('range'):
+ range = {
+ 'start': conf.return_value('range {0} start'.format(range)),
+ 'stop': conf.return_value('range {0} stop'.format(range))
+ }
+ subnet['range'].append(range)
+
+ # IP address that needs to be excluded from DHCP lease range
+ if conf.exists('exclude'):
+ subnet['exclude'] = conf.return_values('exclude')
+ subnet['range'] = dhcp_slice_range(subnet['exclude'], subnet['range'])
+
+ # Static DHCP leases
+ if conf.exists('static-mapping'):
+ addresses_for_exclude = []
+ for mapping in conf.list_nodes('static-mapping'):
+ conf.set_level('service dhcp-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping))
+ mapping = {
+ 'name': mapping,
+ 'disabled': False,
+ 'ip_address': '',
+ 'mac_address': '',
+ 'static_parameters': []
+ }
+
+ # This static lease is disabled
+ if conf.exists('disable'):
+ mapping['disabled'] = True
+
+ # IP address used for this DHCP client
+ if conf.exists('ip-address'):
+ mapping['ip_address'] = conf.return_value('ip-address')
+ addresses_for_exclude.append(mapping['ip_address'])
+
+ # MAC address of requesting DHCP client
+ if conf.exists('mac-address'):
+ mapping['mac_address'] = conf.return_value('mac-address')
+
+ # HACKS AND TRICKS
+ #
+ # check for 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters
+ # from any user
+ #
+ # deprecate this and issue a warning like we do for DNS forwarding?
+ if conf.exists('static-mapping-parameters'):
+ mapping['static_parameters'] = conf.return_values('static-mapping-parameters')
+
+ # append static-mapping configuration to subnet list
+ subnet['static_mapping'].append(mapping)
+
+ # Now we have all static DHCP leases - we also need to slice them
+ # out of our DHCP ranges to avoid ISC DHCPd warnings as:
+ # dhcpd: Dynamic and static leases present for 192.0.2.51.
+ # dhcpd: Remove host declaration DMZ_PC1 or remove 192.0.2.51
+ # dhcpd: from the dynamic address pool for DMZ
+ subnet['range'] = dhcp_slice_range(addresses_for_exclude, subnet['range'])
+
+ # Reset config level to matching hirachy
+ conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net))
+
+ # This option specifies a list of static routes that the client should install in its routing
+ # cache. If multiple routes to the same destination are specified, they are listed in descending
+ # order of priority.
+ if conf.exists('static-route destination-subnet'):
+ subnet['static_subnet'] = conf.return_value('static-route destination-subnet')
+ # Required for global config section
+ dhcp['static_route'] = True
+
+ if conf.exists('static-route router'):
+ subnet['static_router'] = conf.return_value('static-route router')
+
+ if subnet['static_router'] and subnet['static_subnet']:
+ subnet['static_route'] = dhcp_static_route(subnet['static_subnet'], subnet['static_router'])
+
+ # HACKS AND TRICKS
+ #
+ # check for 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters
+ # from any user
+ #
+ # deprecate this and issue a warning like we do for DNS forwarding?
+ if conf.exists('subnet-parameters'):
+ subnet['subnet_parameters'] = conf.return_values('subnet-parameters')
+
+ # This option is used to identify a TFTP server and, if supported by the client, should have
+ # the same effect as the server-name declaration. BOOTP clients are unlikely to support this
+ # option. Some DHCP clients will support it, and others actually require it.
+ if conf.exists('tftp-server-name'):
+ subnet['tftp_server'] = conf.return_value('tftp-server-name')
+
+ # The time-offset option specifies the offset of the client’s subnet in seconds from
+ # Coordinated Universal Time (UTC).
+ if conf.exists('time-offset'):
+ subnet['time_offset'] = conf.return_value('time-offset')
+
+ # The time-server option specifies a list of RFC 868 time servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('time-server'):
+ subnet['time_server'] = conf.return_values('time-server')
+
+ # The NetBIOS name server (NBNS) option specifies a list of RFC 1001/1002 NBNS name servers
+ # listed in order of preference. NetBIOS Name Service is currently more commonly referred to
+ # as WINS. WINS servers can be specified using the netbios-name-servers option.
+ if conf.exists('wins-server'):
+ subnet['wins_server'] = conf.return_values('wins-server')
+
+ # URL for Web Proxy Autodiscovery Protocol
+ if conf.exists('wpad-url'):
+ subnet['wpad_url'] = conf.return_value('wpad-url')
+ # Required for global config section
+ dhcp['wpad'] = True
+
+ # append subnet configuration to shared network subnet list
+ config['subnet'].append(subnet)
+
+ # append shared network configuration to config dictionary
+ dhcp['shared_network'].append(config)
+
+ return dhcp
+
+def verify(dhcp):
+ if not dhcp or dhcp['disabled']:
+ return None
+
+ # If DHCP is enabled we need one share-network
+ if len(dhcp['shared_network']) == 0:
+ raise ConfigError('No DHCP shared networks configured.\n' \
+ 'At least one DHCP shared network must be configured.')
+
+ # Inspect shared-network/subnet
+ failover_names = []
+ listen_ok = False
+ subnets = []
+
+ # A shared-network requires a subnet definition
+ for network in dhcp['shared_network']:
+ if len(network['subnet']) == 0:
+ raise ConfigError('No DHCP lease subnets configured for {0}. At least one\n' \
+ 'lease subnet must be configured for each shared network.'.format(network['name']))
+
+ for subnet in network['subnet']:
+ # Subnet static route declaration requires destination and router
+ if subnet['static_subnet'] or subnet['static_router']:
+ if not (subnet['static_subnet'] and subnet['static_router']):
+ raise ConfigError('Please specify missing DHCP static-route parameter(s):\n' \
+ 'destination-subnet | router')
+
+ # Failover requires all 4 parameters set
+ if subnet['failover_local_addr'] or subnet['failover_peer_addr'] or subnet['failover_name'] or subnet['failover_status']:
+ if not (subnet['failover_local_addr'] and subnet['failover_peer_addr'] and subnet['failover_name'] and subnet['failover_status']):
+ raise ConfigError('Please specify missing DHCP failover parameter(s):\n' \
+ 'local-address | peer-address | name | status')
+
+ # Failover names must be uniquie
+ if subnet['failover_name'] in failover_names:
+ raise ConfigError('Failover names must be unique:\n' \
+ '{0} has already been configured!'.format(subnet['failover_name']))
+ else:
+ failover_names.append(subnet['failover_name'])
+
+ # Failover requires start/stop ranges for pool
+ if (len(subnet['range']) == 0):
+ raise ConfigError('At least one start-stop range must be configured for {0}\n' \
+ 'to set up DHCP failover!'.format(subnet['network']))
+
+ # Check if DHCP address range is inside configured subnet declaration
+ range_start = []
+ range_stop = []
+ for range in subnet['range']:
+ start = range['start']
+ stop = range['stop']
+ # DHCP stop IP required after start IP
+ if start and not stop:
+ raise ConfigError('DHCP range stop address for start {0} is not defined!'.format(start))
+
+ # Start address must be inside network
+ if not ip_address(start) in ip_network(subnet['network']):
+ raise ConfigError('DHCP range start address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(start, subnet['network'], network['name']))
+
+ # Stop address must be inside network
+ if not ip_address(stop) in ip_network(subnet['network']):
+ raise ConfigError('DHCP range stop address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(stop, subnet['network'], network['name']))
+
+ # Stop address must be greater or equal to start address
+ if not ip_address(stop) >= ip_address(start):
+ raise ConfigError('DHCP range stop address {0} must be greater or equal\n' \
+ 'to the range start address {1}!'.format(stop, start))
+
+ # Range start address must be unique
+ if start in range_start:
+ raise ConfigError('Conflicting DHCP lease range:\n' \
+ 'Pool start address {0} defined multipe times!'.format(start))
+ else:
+ range_start.append(start)
+
+ # Range stop address must be unique
+ if stop in range_stop:
+ raise ConfigError('Conflicting DHCP lease range:\n' \
+ 'Pool stop address {0} defined multipe times!'.format(stop))
+ else:
+ range_stop.append(stop)
+
+ # Exclude addresses must be in bound
+ for exclude in subnet['exclude']:
+ if not ip_address(exclude) in ip_network(subnet['network']):
+ raise ConfigError('Exclude IP address {0} is outside of the DHCP lease network {1}\n' \
+ 'under shared network {2}!'.format(exclude, subnet['network'], network['name']))
+
+ # At least one DHCP address range or static-mapping required
+ active_mapping = False
+ if (len(subnet['range']) == 0):
+ for mapping in subnet['static_mapping']:
+ # we need at least one active mapping
+ if (not active_mapping) and (not mapping['disabled']):
+ active_mapping = True
+ else:
+ active_mapping = True
+
+ if not active_mapping:
+ raise ConfigError('No DHCP address range or active static-mapping set\n' \
+ 'for subnet {0}!'.format(subnet['network']))
+
+ # Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
+ for mapping in subnet['static_mapping']:
+
+ if mapping['ip_address']:
+ # Static IP address must be in bound
+ if not ip_address(mapping['ip_address']) in ip_network(subnet['network']):
+ raise ConfigError('DHCP static lease IP address {0} for static mapping {1}\n' \
+ 'in shared network {2} is outside DHCP lease subnet {3}!' \
+ .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network']))
+
+ # Static mapping requires MAC address
+ if not mapping['mac_address']:
+ raise ConfigError('DHCP static lease MAC address not specified for static mapping\n' \
+ '{0} under shared network name {1}!'.format(mapping['name'], network['name']))
+
+ # 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 is_subnet_connected(subnet['network'], primary=True):
+ listen_ok = True
+
+ # Subnets must be non overlapping
+ if subnet['network'] in subnets:
+ raise ConfigError('DHCP subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network']))
+ else:
+ subnets.append(subnet['network'])
+
+ # Check for overlapping subnets
+ net = ip_network(subnet['network'])
+ for n in subnets:
+ net2 = ip_network(n)
+ if (net != net2):
+ if net.overlaps(net2):
+ raise ConfigError('DHCP conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
+
+ if not listen_ok:
+ raise ConfigError('DHCP server configuration error!\n' \
+ 'None of configured DHCP subnets does not have appropriate\n' \
+ 'primary IP address on any broadcast interface.')
+
+ return None
+
+def generate(dhcp):
+ if not dhcp or dhcp['disabled']:
+ return None
+
+ # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters
+ # we can pass to ISC DHCPd
+ render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp,
+ formater=lambda _: _.replace("&quot;", '"'))
+ return None
+
+def apply(dhcp):
+ if not dhcp or dhcp['disabled']:
+ # DHCP server is removed in the commit
+ call('systemctl stop isc-dhcp-server.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ return None
+
+ call('systemctl restart isc-dhcp-server.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/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
new file mode 100755
index 000000000..6ef290bf0
--- /dev/null
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -0,0 +1,112 @@
+#!/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 sys import exit
+from copy import deepcopy
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/dhcp-relay/dhcpv6.conf'
+
+default_config_data = {
+ 'listen_addr': [],
+ 'upstream_addr': [],
+ 'options': [],
+}
+
+def get_config():
+ relay = deepcopy(default_config_data)
+ conf = Config()
+ if not conf.exists('service dhcpv6-relay'):
+ return None
+ else:
+ conf.set_level('service dhcpv6-relay')
+
+ # Network interfaces/address to listen on for DHCPv6 query(s)
+ if conf.exists('listen-interface'):
+ interfaces = conf.list_nodes('listen-interface')
+ for intf in interfaces:
+ if conf.exists('listen-interface {0} address'.format(intf)):
+ addr = conf.return_value('listen-interface {0} address'.format(intf))
+ listen = addr + '%' + intf
+ relay['listen_addr'].append(listen)
+
+ # Upstream interface/address for remote DHCPv6 server
+ if conf.exists('upstream-interface'):
+ interfaces = conf.list_nodes('upstream-interface')
+ for intf in interfaces:
+ addresses = conf.return_values('upstream-interface {0} address'.format(intf))
+ for addr in addresses:
+ server = addr + '%' + intf
+ relay['upstream_addr'].append(server)
+
+ # Maximum hop count. When forwarding packets, dhcrelay discards packets
+ # which have reached a hop count of COUNT. Default is 10. Maximum is 255.
+ if conf.exists('max-hop-count'):
+ count = '-c ' + conf.return_value('max-hop-count')
+ relay['options'].append(count)
+
+ if conf.exists('use-interface-id-option'):
+ relay['options'].append('-I')
+
+ return relay
+
+def verify(relay):
+ # bail out early - looks like removal from running config
+ if relay is None:
+ return None
+
+ if len(relay['listen_addr']) == 0 or len(relay['upstream_addr']) == 0:
+ raise ConfigError('Must set at least one listen and upstream interface addresses.')
+
+ return None
+
+def generate(relay):
+ # bail out early - looks like removal from running config
+ if relay is None:
+ return None
+
+ render(config_file, 'dhcpv6-relay/config.tmpl', relay)
+ return None
+
+def apply(relay):
+ if relay is not None:
+ call('systemctl restart isc-dhcp-relay6.service')
+ else:
+ # DHCPv6 relay support is removed in the commit
+ call('systemctl stop isc-dhcp-relay6.service')
+ if os.path.exists(config_file):
+ os.unlink(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/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
new file mode 100755
index 000000000..53c8358a5
--- /dev/null
+++ b/src/conf_mode/dhcpv6_server.py
@@ -0,0 +1,386 @@
+#!/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
+import ipaddress
+
+from sys import exit
+from copy import deepcopy
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call
+from vyos.validate import is_subnet_connected, is_ipv6
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/dhcp-server/dhcpdv6.conf'
+
+default_config_data = {
+ 'preference': '',
+ 'disabled': False,
+ 'shared_network': []
+}
+
+def get_config():
+ dhcpv6 = deepcopy(default_config_data)
+ conf = Config()
+ base = ['service', 'dhcpv6-server']
+ if not conf.exists(base):
+ return None
+ else:
+ conf.set_level(base)
+
+ # Check for global disable of DHCPv6 service
+ if conf.exists(['disable']):
+ dhcpv6['disabled'] = True
+ return dhcpv6
+
+ # Preference of this DHCPv6 server compared with others
+ if conf.exists(['preference']):
+ dhcpv6['preference'] = conf.return_value(['preference'])
+
+ # check for multiple, shared networks served with DHCPv6 addresses
+ if conf.exists(['shared-network-name']):
+ for network in conf.list_nodes(['shared-network-name']):
+ conf.set_level(base + ['shared-network-name', network])
+ config = {
+ 'name': network,
+ 'disabled': False,
+ 'subnet': []
+ }
+
+ # If disabled, the shared-network configuration becomes inactive
+ if conf.exists(['disable']):
+ config['disabled'] = True
+
+ # check for multiple subnet configurations in a shared network
+ if conf.exists(['subnet']):
+ for net in conf.list_nodes(['subnet']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net])
+ subnet = {
+ 'network': net,
+ 'range6_prefix': [],
+ 'range6': [],
+ 'default_router': '',
+ 'dns_server': [],
+ 'domain_name': '',
+ 'domain_search': [],
+ 'lease_def': '',
+ 'lease_min': '',
+ 'lease_max': '',
+ 'nis_domain': '',
+ 'nis_server': [],
+ 'nisp_domain': '',
+ 'nisp_server': [],
+ 'prefix_delegation': [],
+ 'sip_address': [],
+ 'sip_hostname': [],
+ 'sntp_server': [],
+ 'static_mapping': []
+ }
+
+ # For any subnet on which addresses will be assigned dynamically, there must be at
+ # least one address range statement. The range statement gives the lowest and highest
+ # IP addresses in a range. All IP addresses in the range should be in the subnet in
+ # which the range statement is declared.
+ if conf.exists(['address-range', 'prefix']):
+ for prefix in conf.list_nodes(['address-range', 'prefix']):
+ range = {
+ 'prefix': prefix,
+ 'temporary': False
+ }
+
+ # Address range will be used for temporary addresses
+ if conf.exists(['address-range' 'prefix', prefix, 'temporary']):
+ range['temporary'] = True
+
+ # Append to subnet temporary range6 list
+ subnet['range6_prefix'].append(range)
+
+ if conf.exists(['address-range', 'start']):
+ for range in conf.list_nodes(['address-range', 'start']):
+ range = {
+ 'start': range,
+ 'stop': conf.return_value(['address-range', 'start', range, 'stop'])
+ }
+
+ # Append to subnet range6 list
+ subnet['range6'].append(range)
+
+ # The domain-search option specifies a 'search list' of Domain Names to be used
+ # by the client to locate not-fully-qualified domain names.
+ if conf.exists(['domain-search']):
+ subnet['domain_search'] = conf.return_values(['domain-search'])
+
+ # IPv6 address valid lifetime
+ # (at the end the address is no longer usable by the client)
+ # (set to 30 days, the usual IPv6 default)
+ if conf.exists(['lease-time', 'default']):
+ subnet['lease_def'] = conf.return_value(['lease-time', 'default'])
+
+ # Time should be the maximum length in seconds that will be assigned to a lease.
+ # The only exception to this is that Dynamic BOOTP lease lengths, which are not
+ # specified by the client, are not limited by this maximum.
+ if conf.exists(['lease-time', 'maximum']):
+ subnet['lease_max'] = conf.return_value(['lease-time', 'maximum'])
+
+ # Time should be the minimum length in seconds that will be assigned to a lease
+ if conf.exists(['lease-time', 'minimum']):
+ subnet['lease_min'] = conf.return_value(['lease-time', 'minimum'])
+
+ # Specifies a list of Domain Name System name servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists(['name-server']):
+ subnet['dns_server'] = conf.return_values(['name-server'])
+
+ # Ancient NIS (Network Information Service) domain name
+ if conf.exists(['nis-domain']):
+ subnet['nis_domain'] = conf.return_value(['nis-domain'])
+
+ # Ancient NIS (Network Information Service) servers
+ if conf.exists(['nis-server']):
+ subnet['nis_server'] = conf.return_values(['nis-server'])
+
+ # Ancient NIS+ (Network Information Service) domain name
+ if conf.exists(['nisplus-domain']):
+ subnet['nisp_domain'] = conf.return_value(['nisplus-domain'])
+
+ # Ancient NIS+ (Network Information Service) servers
+ if conf.exists(['nisplus-server']):
+ subnet['nisp_server'] = conf.return_values(['nisplus-server'])
+
+ # Local SIP server that is to be used for all outbound SIP requests - IPv6 address
+ if conf.exists(['sip-server']):
+ for value in conf.return_values(['sip-server']):
+ if is_ipv6(value):
+ subnet['sip_address'].append(value)
+ else:
+ subnet['sip_hostname'].append(value)
+
+ # List of local SNTP servers available for the client to synchronize their clocks
+ if conf.exists(['sntp-server']):
+ subnet['sntp_server'] = conf.return_values(['sntp-server'])
+
+ # Prefix Delegation (RFC 3633)
+ if conf.exists(['prefix-delegation', 'start']):
+ for address in conf.list_nodes(['prefix-delegation', 'start']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'prefix-delegation', 'start', address])
+ prefix = {
+ 'start' : address,
+ 'stop' : '',
+ 'length' : ''
+ }
+
+ if conf.exists(['prefix-length']):
+ prefix['length'] = conf.return_value(['prefix-length'])
+
+ if conf.exists(['stop']):
+ prefix['stop'] = conf.return_value(['stop'])
+
+ subnet['prefix_delegation'].append(prefix)
+
+ #
+ # Static DHCP v6 leases
+ #
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net])
+ if conf.exists(['static-mapping']):
+ for mapping in conf.list_nodes(['static-mapping']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'static-mapping', mapping])
+ mapping = {
+ 'name': mapping,
+ 'disabled': False,
+ 'ipv6_address': '',
+ 'client_identifier': '',
+ }
+
+ # This static lease is disabled
+ if conf.exists(['disable']):
+ mapping['disabled'] = True
+
+ # IPv6 address used for this DHCP client
+ if conf.exists(['ipv6-address']):
+ mapping['ipv6_address'] = conf.return_value(['ipv6-address'])
+
+ # This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers
+ if conf.exists(['identifier']):
+ mapping['client_identifier'] = conf.return_value(['identifier'])
+
+ # append static mapping configuration tu subnet list
+ subnet['static_mapping'].append(mapping)
+
+ # append subnet configuration to shared network subnet list
+ config['subnet'].append(subnet)
+
+ # append shared network configuration to config dictionary
+ dhcpv6['shared_network'].append(config)
+
+ # If all shared-networks are disabled, there's nothing to do.
+ if all(net['disabled'] for net in dhcpv6['shared_network']):
+ return None
+
+ return dhcpv6
+
+def verify(dhcpv6):
+ if not dhcpv6 or dhcpv6['disabled']:
+ return None
+
+ # If DHCP is enabled we need one share-network
+ if len(dhcpv6['shared_network']) == 0:
+ raise ConfigError('No DHCPv6 shared networks configured.\n' \
+ 'At least one DHCPv6 shared network must be configured.')
+
+ # Inspect shared-network/subnet
+ subnets = []
+ listen_ok = False
+
+ for network in dhcpv6['shared_network']:
+ # A shared-network requires a subnet definition
+ if len(network['subnet']) == 0:
+ raise ConfigError('No DHCPv6 lease subnets configured for {0}. At least one\n' \
+ 'lease subnet must be configured for each shared network.'.format(network['name']))
+
+ range6_start = []
+ range6_stop = []
+ for subnet in network['subnet']:
+ # Ususal range declaration with a start and stop address
+ for range6 in subnet['range6']:
+ # shorten names
+ start = range6['start']
+ stop = range6['stop']
+
+ # DHCPv6 stop address is required
+ if start and not stop:
+ raise ConfigError('DHCPv6 range stop address for start {0} is not defined!'.format(start))
+
+ # Start address must be inside network
+ if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 range start address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(start, subnet['network'], network['name']))
+
+ # Stop address must be inside network
+ if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 range stop address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(stop, subnet['network'], network['name']))
+
+ # Stop address must be greater or equal to start address
+ if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start):
+ raise ConfigError('DHCPv6 range stop address {0} must be greater or equal\n' \
+ 'to the range start address {1}!'.format(stop, start))
+
+ # DHCPv6 range start address must be unique - two ranges can't
+ # start with the same address - makes no sense
+ if start in range6_start:
+ raise ConfigError('Conflicting DHCPv6 lease range:\n' \
+ 'Pool start address {0} defined multipe times!'.format(start))
+ else:
+ range6_start.append(start)
+
+ # DHCPv6 range stop address must be unique - two ranges can't
+ # end with the same address - makes no sense
+ if stop in range6_stop:
+ raise ConfigError('Conflicting DHCPv6 lease range:\n' \
+ 'Pool stop address {0} defined multipe times!'.format(stop))
+ else:
+ range6_stop.append(stop)
+
+ # Prefix delegation sanity checks
+ for prefix in subnet['prefix_delegation']:
+ if not prefix['stop']:
+ raise ConfigError('Stop address of delegated IPv6 prefix range must be configured')
+
+ if not prefix['length']:
+ raise ConfigError('Length of delegated IPv6 prefix must be configured')
+
+ # We also have prefixes that require checking
+ for prefix in subnet['range6_prefix']:
+ # If configured prefix does not match our subnet, we have to check that it's inside
+ if ipaddress.ip_network(prefix['prefix']) != ipaddress.ip_network(subnet['network']):
+ # Configured prefixes must be inside our network
+ if not ipaddress.ip_network(prefix['prefix']) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 prefix {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(prefix['prefix'], subnet['network'], network['name']))
+
+ # Static mappings don't require anything (but check if IP is in subnet if it's set)
+ for mapping in subnet['static_mapping']:
+ if mapping['ipv6_address']:
+ # Static address must be in subnet
+ if not ipaddress.ip_address(mapping['ipv6_address']) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 static mapping IPv6 address {0} for static mapping {1}\n' \
+ 'in shared network {2} is outside subnet {3}!' \
+ .format(mapping['ipv6_address'], mapping['name'], network['name'], subnet['network']))
+
+ # Subnets must be unique
+ if subnet['network'] in subnets:
+ raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network']))
+ else:
+ subnets.append(subnet['network'])
+
+ # DHCPv6 requires at least one configured address range or one static mapping
+ # (FIXME: is not actually checked right now?)
+
+ # There must be one subnet connected to a listen interface if network is not disabled.
+ if not network['disabled']:
+ if is_subnet_connected(subnet['network']):
+ listen_ok = True
+
+ # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping
+ # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32"
+ net = ipaddress.ip_network(subnet['network'])
+ for n in subnets:
+ net2 = ipaddress.ip_network(n)
+ if (net != net2):
+ if net.overlaps(net2):
+ raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
+
+ if not listen_ok:
+ raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \
+ 'this machine. At least one subnet6 must be connected such that\n' \
+ 'DHCPv6 listens on an interface!')
+
+
+ return None
+
+def generate(dhcpv6):
+ if not dhcpv6 or dhcpv6['disabled']:
+ return None
+
+ render(config_file, 'dhcpv6-server/dhcpdv6.conf.tmpl', dhcpv6)
+ return None
+
+def apply(dhcpv6):
+ if not dhcpv6 or dhcpv6['disabled']:
+ # DHCP server is removed in the commit
+ call('systemctl stop isc-dhcp-server6.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+
+ else:
+ call('systemctl restart isc-dhcp-server6.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/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
new file mode 100755
index 000000000..51631dc16
--- /dev/null
+++ b/src/conf_mode/dns_forwarding.py
@@ -0,0 +1,216 @@
+#!/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 sys import exit
+from copy import deepcopy
+
+from vyos.config import Config
+from vyos.hostsd_client import Client as hostsd_client
+from vyos import ConfigError
+from vyos.util import call, chown
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+pdns_rec_user = pdns_rec_group = 'pdns'
+pdns_rec_run_dir = '/run/powerdns'
+pdns_rec_lua_conf_file = f'{pdns_rec_run_dir}/recursor.conf.lua'
+pdns_rec_hostsd_lua_conf_file = f'{pdns_rec_run_dir}/recursor.vyos-hostsd.conf.lua'
+pdns_rec_hostsd_zones_file = f'{pdns_rec_run_dir}/recursor.forward-zones.conf'
+pdns_rec_config_file = f'{pdns_rec_run_dir}/recursor.conf'
+
+default_config_data = {
+ 'allow_from': [],
+ 'cache_size': 10000,
+ 'export_hosts_file': 'yes',
+ 'listen_address': [],
+ 'name_servers': [],
+ 'negative_ttl': 3600,
+ 'system': False,
+ 'domains': {},
+ 'dnssec': 'process-no-validate',
+ 'dhcp_interfaces': []
+}
+
+hostsd_tag = 'static'
+
+def get_config(conf):
+ dns = deepcopy(default_config_data)
+ base = ['service', 'dns', 'forwarding']
+
+ if not conf.exists(base):
+ return None
+
+ conf.set_level(base)
+
+ 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'])
+ dns['cache_size'] = cache_size
+
+ if conf.exists('negative-ttl'):
+ negative_ttl = conf.return_value(['negative-ttl'])
+ dns['negative_ttl'] = negative_ttl
+
+ if conf.exists(['domain']):
+ for domain in conf.list_nodes(['domain']):
+ conf.set_level(base + ['domain', domain])
+ entry = {
+ 'nslist': bracketize_ipv6_addrs(conf.return_values(['server'])),
+ 'addNTA': conf.exists(['addnta']),
+ 'recursion-desired': conf.exists(['recursion-desired'])
+ }
+ dns['domains'][domain] = entry
+
+ conf.set_level(base)
+
+ if conf.exists(['ignore-hosts-file']):
+ dns['export_hosts_file'] = "no"
+
+ if conf.exists(['name-server']):
+ dns['name_servers'] = bracketize_ipv6_addrs(
+ conf.return_values(['name-server']))
+
+ if conf.exists(['system']):
+ dns['system'] = True
+
+ if conf.exists(['listen-address']):
+ dns['listen_address'] = conf.return_values(['listen-address'])
+
+ if conf.exists(['dnssec']):
+ dns['dnssec'] = conf.return_value(['dnssec'])
+
+ if conf.exists(['dhcp']):
+ dns['dhcp_interfaces'] = conf.return_values(['dhcp'])
+
+ return dns
+
+def bracketize_ipv6_addrs(addrs):
+ """Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched."""
+ return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs]
+
+def verify(conf, dns):
+ # bail out early - looks like removal from running config
+ if dns is None:
+ return None
+
+ if not dns['listen_address']:
+ raise ConfigError(
+ "Error: DNS forwarding requires a listen-address")
+
+ if not dns['allow_from']:
+ raise ConfigError(
+ "Error: DNS forwarding requires an allow-from network")
+
+ if dns['domains']:
+ for domain in dns['domains']:
+ if not dns['domains'][domain]['nslist']:
+ raise ConfigError((
+ f'Error: No server configured for domain {domain}'))
+
+ no_system_nameservers = False
+ if dns['system'] and not (
+ conf.exists(['system', 'name-server']) or
+ conf.exists(['system', 'name-servers-dhcp']) ):
+ no_system_nameservers = True
+ print(("DNS forwarding warning: No 'system name-server' or "
+ "'system name-servers-dhcp' set\n"))
+
+ if (no_system_nameservers or not dns['system']) and not (
+ dns['name_servers'] or dns['dhcp_interfaces']):
+ print(("DNS forwarding warning: No 'dhcp', 'name-server' or 'system' "
+ "nameservers set. Forwarding will operate as a recursor.\n"))
+
+ return None
+
+def generate(dns):
+ # bail out early - looks like removal from running config
+ if dns is None:
+ return None
+
+ render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.tmpl',
+ dns, user=pdns_rec_user, group=pdns_rec_group)
+
+ render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.tmpl',
+ dns, user=pdns_rec_user, group=pdns_rec_group)
+
+ # if vyos-hostsd didn't create its files yet, create them (empty)
+ for f in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]:
+ with open(f, 'a'):
+ pass
+ chown(f, user=pdns_rec_user, group=pdns_rec_group)
+
+ return None
+
+def apply(dns):
+ if dns is None:
+ # DNS forwarding is removed in the commit
+ call("systemctl stop pdns-recursor.service")
+ if os.path.isfile(pdns_rec_config_file):
+ os.unlink(pdns_rec_config_file)
+ else:
+ ### first apply vyos-hostsd config
+ hc = hostsd_client()
+
+ # add static nameservers to hostsd so they can be joined with other
+ # sources
+ hc.delete_name_servers([hostsd_tag])
+ if dns['name_servers']:
+ hc.add_name_servers({hostsd_tag: dns['name_servers']})
+
+ # delete all nameserver tags
+ hc.delete_name_server_tags_recursor(hc.get_name_server_tags_recursor())
+
+ ## add nameserver tags - the order determines the nameserver order!
+ # our own tag (static)
+ hc.add_name_server_tags_recursor([hostsd_tag])
+
+ if dns['system']:
+ hc.add_name_server_tags_recursor(['system'])
+ else:
+ hc.delete_name_server_tags_recursor(['system'])
+
+ # add dhcp nameserver tags for configured interfaces
+ for intf in dns['dhcp_interfaces']:
+ hc.add_name_server_tags_recursor(['dhcp-' + intf, 'dhcpv6-' + intf ])
+
+ # hostsd will generate the forward-zones file
+ # the list and keys() are required as get returns a dict, not list
+ hc.delete_forward_zones(list(hc.get_forward_zones().keys()))
+ if dns['domains']:
+ hc.add_forward_zones(dns['domains'])
+
+ # call hostsd to generate forward-zones and its lua-config-file
+ hc.apply()
+
+ ### finally (re)start pdns-recursor
+ call("systemctl restart pdns-recursor.service")
+
+if __name__ == '__main__':
+ try:
+ conf = Config()
+ c = get_config(conf)
+ verify(conf, c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
new file mode 100755
index 000000000..5b1883c03
--- /dev/null
+++ b/src/conf_mode/dynamic_dns.py
@@ -0,0 +1,249 @@
+#!/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 sys import exit
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/ddclient/ddclient.conf'
+
+# Mapping of service name to service protocol
+default_service_protocol = {
+ 'afraid': 'freedns',
+ 'changeip': 'changeip',
+ 'cloudflare': 'cloudflare',
+ 'dnspark': 'dnspark',
+ 'dslreports': 'dslreports1',
+ 'dyndns': 'dyndns2',
+ 'easydns': 'easydns',
+ 'namecheap': 'namecheap',
+ 'noip': 'noip',
+ 'sitelutions': 'sitelutions',
+ 'zoneedit': 'zoneedit1'
+}
+
+default_config_data = {
+ 'interfaces': [],
+ 'deleted': False
+}
+
+def get_config():
+ dyndns = deepcopy(default_config_data)
+ conf = Config()
+ base_level = ['service', 'dns', 'dynamic']
+
+ if not conf.exists(base_level):
+ dyndns['deleted'] = True
+ return dyndns
+
+ for interface in conf.list_nodes(base_level + ['interface']):
+ node = {
+ 'interface': interface,
+ 'rfc2136': [],
+ 'service': [],
+ 'web_skip': '',
+ 'web_url': ''
+ }
+
+ # set config level to e.g. "service dns dynamic interface eth0"
+ conf.set_level(base_level + ['interface', interface])
+ # Handle RFC2136 - Dynamic Updates in the Domain Name System
+ for rfc2136 in conf.list_nodes(['rfc2136']):
+ rfc = {
+ 'name': rfc2136,
+ 'keyfile': '',
+ 'record': [],
+ 'server': '',
+ 'ttl': '600',
+ 'zone': ''
+ }
+
+ # set config level
+ conf.set_level(base_level + ['interface', interface, 'rfc2136', rfc2136])
+
+ if conf.exists(['key']):
+ rfc['keyfile'] = conf.return_value(['key'])
+
+ if conf.exists(['record']):
+ rfc['record'] = conf.return_values(['record'])
+
+ if conf.exists(['server']):
+ rfc['server'] = conf.return_value(['server'])
+
+ if conf.exists(['ttl']):
+ rfc['ttl'] = conf.return_value(['ttl'])
+
+ if conf.exists(['zone']):
+ rfc['zone'] = conf.return_value(['zone'])
+
+ node['rfc2136'].append(rfc)
+
+ # set config level to e.g. "service dns dynamic interface eth0"
+ conf.set_level(base_level + ['interface', interface])
+ # Handle DynDNS service providers
+ for service in conf.list_nodes(['service']):
+ srv = {
+ 'provider': service,
+ 'host': [],
+ 'login': '',
+ 'password': '',
+ 'protocol': '',
+ 'server': '',
+ 'custom' : False,
+ 'zone' : ''
+ }
+
+ # set config level
+ conf.set_level(base_level + ['interface', interface, 'service', service])
+
+ # preload protocol from default service mapping
+ if service in default_service_protocol.keys():
+ srv['protocol'] = default_service_protocol[service]
+ else:
+ srv['custom'] = True
+
+ if conf.exists(['login']):
+ srv['login'] = conf.return_value(['login'])
+
+ if conf.exists(['host-name']):
+ srv['host'] = conf.return_values(['host-name'])
+
+ if conf.exists(['protocol']):
+ srv['protocol'] = conf.return_value(['protocol'])
+
+ if conf.exists(['password']):
+ srv['password'] = conf.return_value(['password'])
+
+ if conf.exists(['server']):
+ srv['server'] = conf.return_value(['server'])
+
+ if conf.exists(['zone']):
+ srv['zone'] = conf.return_value(['zone'])
+ elif srv['provider'] == 'cloudflare':
+ # default populate zone entry with bar.tld if
+ # host-name is foo.bar.tld
+ srv['zone'] = srv['host'][0].split('.',1)[1]
+
+ node['service'].append(srv)
+
+ # 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']):
+ node['web_skip'] = conf.return_value(['use-web', 'skip'])
+
+ 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
+
+def verify(dyndns):
+ # bail out early - looks like removal from running config
+ if dyndns['deleted']:
+ return None
+
+ # A 'node' corresponds to an interface
+ for node in dyndns['interfaces']:
+
+ # RFC2136 - configuration validation
+ for rfc2136 in node['rfc2136']:
+ if not rfc2136['record']:
+ raise ConfigError('Set key for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface']))
+
+ if not rfc2136['zone']:
+ raise ConfigError('Set zone for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface']))
+
+ if not rfc2136['keyfile']:
+ raise ConfigError('Set keyfile for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface']))
+ else:
+ if not os.path.isfile(rfc2136['keyfile']):
+ raise ConfigError('Keyfile for service "{0}" to send DDNS updates for interface "{1}" does not exist'.format(rfc2136['name'], node['interface']))
+
+ if not rfc2136['server']:
+ raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface']))
+
+ # DynDNS service provider - configuration validation
+ for service in node['service']:
+ if not service['host']:
+ raise ConfigError('Set host-name for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface']))
+
+ if not service['login']:
+ raise ConfigError('Set login for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface']))
+
+ if not service['password']:
+ raise ConfigError('Set password for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface']))
+
+ if service['custom'] is True:
+ if not service['protocol']:
+ raise ConfigError('Set protocol for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface']))
+
+ if not service['server']:
+ raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface']))
+
+ if service['zone']:
+ if service['provider'] != 'cloudflare':
+ raise ConfigError('Zone option not allowed for "{0}", it can only be used for CloudFlare'.format(service['provider']))
+
+ return None
+
+def generate(dyndns):
+ # bail out early - looks like removal from running config
+ if dyndns['deleted']:
+ return None
+
+ render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns)
+
+ # Config file must be accessible only by its owner
+ os.chmod(config_file, S_IRUSR | S_IWUSR)
+
+ return None
+
+def apply(dyndns):
+ if dyndns['deleted']:
+ call('systemctl stop ddclient.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+
+ else:
+ call('systemctl restart ddclient.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/firewall_options.py b/src/conf_mode/firewall_options.py
new file mode 100755
index 000000000..71b2a98b3
--- /dev/null
+++ b/src/conf_mode/firewall_options.py
@@ -0,0 +1,147 @@
+#!/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 copy
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+default_config_data = {
+ 'intf_opts': [],
+ 'new_chain4': False,
+ 'new_chain6': False
+}
+
+def get_config():
+ opts = copy.deepcopy(default_config_data)
+ conf = Config()
+ if not conf.exists('firewall options'):
+ # bail out early
+ return opts
+ else:
+ conf.set_level('firewall options')
+
+ # Parse configuration of each individual instance
+ if conf.exists('interface'):
+ for intf in conf.list_nodes('interface'):
+ conf.set_level('firewall options interface {0}'.format(intf))
+ config = {
+ 'intf': intf,
+ 'disabled': False,
+ 'mss4': '',
+ 'mss6': ''
+ }
+
+ # Check if individual option is disabled
+ if conf.exists('disable'):
+ config['disabled'] = True
+
+ #
+ # Get MSS value IPv4
+ #
+ if conf.exists('adjust-mss'):
+ config['mss4'] = conf.return_value('adjust-mss')
+
+ # We need a marker that a new iptables chain needs to be generated
+ if not opts['new_chain4']:
+ opts['new_chain4'] = True
+
+ #
+ # Get MSS value IPv6
+ #
+ if conf.exists('adjust-mss6'):
+ config['mss6'] = conf.return_value('adjust-mss6')
+
+ # We need a marker that a new ip6tables chain needs to be generated
+ if not opts['new_chain6']:
+ opts['new_chain6'] = True
+
+ # Append interface options to global list
+ opts['intf_opts'].append(config)
+
+ return opts
+
+def verify(tcp):
+ # syntax verification is done via cli
+ return None
+
+def apply(tcp):
+ target = 'VYOS_FW_OPTIONS'
+
+ # always cleanup iptables
+ 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
+ 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']:
+ 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']
+ mss = opts['mss4']
+
+ # Check if this rule iis disabled
+ if opts['disabled']:
+ continue
+
+ # adjust TCP MSS per interface
+ if mss:
+ 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']:
+ 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']
+ mss = opts['mss6']
+
+ # Check if this rule iis disabled
+ if opts['disabled']:
+ continue
+
+ # adjust TCP MSS per interface
+ if 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
+
+if __name__ == '__main__':
+
+ try:
+ c = get_config()
+ verify(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
new file mode 100755
index 000000000..b7e73eaeb
--- /dev/null
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -0,0 +1,371 @@
+#!/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
+import re
+from sys import exit
+import ipaddress
+
+from ipaddress import ip_address
+from jinja2 import FileSystemLoader, Environment
+
+from vyos.ifconfig import Section
+from vyos.ifconfig import Interface
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import cmd
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+# default values
+default_sflow_server_port = 6343
+default_netflow_server_port = 2055
+default_plugin_pipe_size = 10
+default_captured_packet_size = 128
+default_netflow_version = '9'
+default_sflow_agentip = 'auto'
+uacctd_conf_path = '/etc/pmacct/uacctd.conf'
+iptables_nflog_table = 'raw'
+iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK'
+
+# helper functions
+# check if node exists and return True if this is true
+def _node_exists(path):
+ vyos_config = Config()
+ if vyos_config.exists(path):
+ return True
+
+# get sFlow agent-ip if agent-address is "auto" (default behaviour)
+def _sflow_default_agentip(config):
+ # check if any of BGP, OSPF, OSPFv3 protocols are configured and use router-id from there
+ if config.exists('protocols bgp'):
+ bgp_router_id = config.return_value("protocols bgp {} parameters router-id".format(config.list_nodes('protocols bgp')[0]))
+ if bgp_router_id:
+ return bgp_router_id
+ if config.return_value('protocols ospf parameters router-id'):
+ return config.return_value('protocols ospf parameters router-id')
+ if config.return_value('protocols ospfv3 parameters router-id'):
+ 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 Section.interfaces():
+ 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+$')
+ if regex_filter.search(address):
+ return regex_filter.search(address).group('ipaddr')
+
+ # return nothing by default
+ return None
+
+# get iptables rule dict for chain in table
+def _iptables_get_nflog():
+ # define list with rules
+ rules = []
+
+ # prepare regex for parsing rules
+ rule_pattern = "^-A (?P<rule_definition>{0} -i (?P<interface>[\w\.\*\-]+).*--comment FLOW_ACCOUNTING_RULE.* -j NFLOG.*$)".format(iptables_nflog_chain)
+ rule_re = re.compile(rule_pattern)
+
+ for iptables_variant in ['iptables', 'ip6tables']:
+ # run iptables, save output and split it by lines
+ iptables_command = f'{iptables_variant} -t {iptables_nflog_table} -S {iptables_nflog_chain}'
+ tmp = cmd(iptables_command, message='Failed to get flows list')
+
+ # parse each line and add information to list
+ for current_rule in tmp.splitlines():
+ current_rule_parsed = rule_re.search(current_rule)
+ if current_rule_parsed:
+ rules.append({ 'interface': current_rule_parsed.groupdict()["interface"], 'iptables_variant': iptables_variant, 'table': iptables_nflog_table, 'rule_definition': current_rule_parsed.groupdict()["rule_definition"] })
+
+ # return list with rules
+ return rules
+
+# modify iptables rules
+def _iptables_config(configured_ifaces):
+ # define list of iptables commands to modify settings
+ iptable_commands = []
+
+ # prepare extended list with configured interfaces
+ configured_ifaces_extended = []
+ for iface in configured_ifaces:
+ configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'iptables' })
+ configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'ip6tables' })
+
+ # 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:
+ iptables = rule['iptables_variant']
+ interface = rule['interface']
+ if interface not in configured_ifaces:
+ table = rule['table']
+ rule = rule['rule_definition']
+ iptable_commands.append(f'{iptables} -t {table} -D {rule}')
+ else:
+ active_nflog_ifaces.append({
+ 'iface': interface,
+ 'iptables_variant': iptables,
+ })
+
+ # do not create new rules for already configured interfaces
+ for iface in active_nflog_ifaces:
+ if iface in active_nflog_ifaces:
+ configured_ifaces_extended.remove(iface)
+
+ # create missed rules
+ for iface_extended in configured_ifaces_extended:
+ 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'{iptables} -t {iptables_nflog_table} -I {rule_definition}')
+
+ # change iptables
+ for command in iptable_commands:
+ cmd(command, raising=ConfigError)
+
+
+def get_config():
+ vc = Config()
+ vc.set_level('')
+ # Convert the VyOS config to an abstract internal representation
+ flow_config = {
+ 'flow-accounting-configured': vc.exists('system flow-accounting'),
+ 'buffer-size': vc.return_value('system flow-accounting buffer-size'),
+ 'disable-imt': _node_exists('system flow-accounting disable-imt'),
+ 'syslog-facility': vc.return_value('system flow-accounting syslog-facility'),
+ 'interfaces': None,
+ 'sflow': {
+ 'configured': vc.exists('system flow-accounting sflow'),
+ 'agent-address': vc.return_value('system flow-accounting sflow agent-address'),
+ 'sampling-rate': vc.return_value('system flow-accounting sflow sampling-rate'),
+ 'servers': None
+ },
+ 'netflow': {
+ 'configured': vc.exists('system flow-accounting netflow'),
+ 'engine-id': vc.return_value('system flow-accounting netflow engine-id'),
+ 'max-flows': vc.return_value('system flow-accounting netflow max-flows'),
+ 'sampling-rate': vc.return_value('system flow-accounting netflow sampling-rate'),
+ 'source-ip': vc.return_value('system flow-accounting netflow source-ip'),
+ 'version': vc.return_value('system flow-accounting netflow version'),
+ 'timeout': {
+ 'expint': vc.return_value('system flow-accounting netflow timeout expiry-interval'),
+ 'general': vc.return_value('system flow-accounting netflow timeout flow-generic'),
+ 'icmp': vc.return_value('system flow-accounting netflow timeout icmp'),
+ 'maxlife': vc.return_value('system flow-accounting netflow timeout max-active-life'),
+ 'tcp.fin': vc.return_value('system flow-accounting netflow timeout tcp-fin'),
+ 'tcp': vc.return_value('system flow-accounting netflow timeout tcp-generic'),
+ 'tcp.rst': vc.return_value('system flow-accounting netflow timeout tcp-rst'),
+ 'udp': vc.return_value('system flow-accounting netflow timeout udp')
+ },
+ 'servers': None
+ }
+ }
+
+ # get interfaces list
+ if vc.exists('system flow-accounting interface'):
+ flow_config['interfaces'] = vc.return_values('system flow-accounting interface')
+
+ # get sFlow collectors list
+ if vc.exists('system flow-accounting sflow server'):
+ flow_config['sflow']['servers'] = []
+ sflow_collectors = vc.list_nodes('system flow-accounting sflow server')
+ for collector in sflow_collectors:
+ port = default_sflow_server_port
+ if vc.return_value("system flow-accounting sflow server {} port".format(collector)):
+ port = vc.return_value("system flow-accounting sflow server {} port".format(collector))
+ flow_config['sflow']['servers'].append({ 'address': collector, 'port': port })
+
+ # get NetFlow collectors list
+ if vc.exists('system flow-accounting netflow server'):
+ flow_config['netflow']['servers'] = []
+ netflow_collectors = vc.list_nodes('system flow-accounting netflow server')
+ for collector in netflow_collectors:
+ port = default_netflow_server_port
+ if vc.return_value("system flow-accounting netflow server {} port".format(collector)):
+ port = vc.return_value("system flow-accounting netflow server {} port".format(collector))
+ flow_config['netflow']['servers'].append({ 'address': collector, 'port': port })
+
+ # get sflow agent-id
+ if flow_config['sflow']['agent-address'] == None or flow_config['sflow']['agent-address'] == 'auto':
+ flow_config['sflow']['agent-address'] = _sflow_default_agentip(vc)
+
+ # get NetFlow version
+ if not flow_config['netflow']['version']:
+ flow_config['netflow']['version'] = default_netflow_version
+
+ # convert NetFlow engine-id format, if this is necessary
+ if flow_config['netflow']['engine-id'] and flow_config['netflow']['version'] == '5':
+ regex_filter = re.compile('^\d+$')
+ if regex_filter.search(flow_config['netflow']['engine-id']):
+ flow_config['netflow']['engine-id'] = "{}:0".format(flow_config['netflow']['engine-id'])
+
+ # return dict with flow-accounting configuration
+ return flow_config
+
+def verify(config):
+ # Verify that configuration is valid
+ # skip all checks if flow-accounting was removed
+ if not config['flow-accounting-configured']:
+ return True
+
+ # check if at least one collector is enabled
+ if not (config['sflow']['configured'] or config['netflow']['configured'] or not config['disable-imt']):
+ raise ConfigError("You need to configure at least one sFlow or NetFlow protocol, or not set \"disable-imt\" for flow-accounting")
+
+ # Check if at least one interface is configured
+ if not config['interfaces']:
+ raise ConfigError("You need to configure at least one interface for flow-accounting")
+
+ # check that all configured interfaces exists in the system
+ for iface in config['interfaces']:
+ if not iface in Section.interfaces():
+ # 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))
+
+ # check sFlow configuration
+ if config['sflow']['configured']:
+ # check if at least one sFlow collector is configured if sFlow configuration is presented
+ if not config['sflow']['servers']:
+ raise ConfigError("You need to configure at least one sFlow server")
+
+ # check that all sFlow collectors use the same IP protocol version
+ sflow_collector_ipver = None
+ for sflow_collector in config['sflow']['servers']:
+ if sflow_collector_ipver:
+ 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 = 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 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 Section.interfaces():
+ 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+$')
+ if regex_filter.search(address):
+ if regex_filter.search(address).group('ipaddr') == config['sflow']['agent-address']:
+ agent_id_presented = True
+ break
+ if not agent_id_presented:
+ raise ConfigError("Your \"sflow agent-address\" does not exist in the system")
+
+ # check NetFlow configuration
+ if config['netflow']['configured']:
+ # check if at least one NetFlow collector is configured if NetFlow configuration is presented
+ if not config['netflow']['servers']:
+ raise ConfigError("You need to configure at least one NetFlow server")
+
+ # check if configured netflow source-ip exist in the system
+ if config['netflow']['source-ip']:
+ source_ip_presented = None
+ for iface in Section.interfaces():
+ for address in Interface(iface).get_addr():
+ # check an IP
+ regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
+ if regex_filter.search(address):
+ if regex_filter.search(address).group('ipaddr') == config['netflow']['source-ip']:
+ source_ip_presented = True
+ break
+ if not source_ip_presented:
+ raise ConfigError("Your \"netflow source-ip\" does not exist in the system")
+
+ # check if engine-id compatible with selected protocol version
+ if config['netflow']['engine-id']:
+ v5_filter = '^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$'
+ v9v10_filter = '^(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$'
+ if config['netflow']['version'] == '5':
+ regex_filter = re.compile(v5_filter)
+ if not regex_filter.search(config['netflow']['engine-id']):
+ raise ConfigError("You cannot use NetFlow engine-id {} together with NetFlow protocol version {}".format(config['netflow']['engine-id'], config['netflow']['version']))
+ else:
+ regex_filter = re.compile(v9v10_filter)
+ if not regex_filter.search(config['netflow']['engine-id']):
+ raise ConfigError("You cannot use NetFlow engine-id {} together with NetFlow protocol version {}".format(config['netflow']['engine-id'], config['netflow']['version']))
+
+ # return True if all checks were passed
+ return True
+
+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
+ config['plugin_pipe_size'] = int(config['buffer-size']) * 1024**2
+ else:
+ config['plugin_pipe_size'] = default_plugin_pipe_size * 1024**2
+ # transfer buffer size
+ # recommended value from pmacct developers 1/1000 of pipe size
+ config['plugin_buffer_size'] = int(config['plugin_pipe_size'] / 1000)
+
+ # Prepare a timeouts string
+ timeout_string = ''
+ for timeout_type, timeout_value in config['netflow']['timeout'].items():
+ if timeout_value:
+ if timeout_string == '':
+ timeout_string = "{}{}={}".format(timeout_string, timeout_type, timeout_value)
+ else:
+ timeout_string = "{}:{}={}".format(timeout_string, timeout_type, timeout_value)
+ config['netflow']['timeout_string'] = timeout_string
+
+ render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', {
+ 'templatecfg': config,
+ 'snaplen': default_captured_packet_size,
+ })
+
+
+def apply(config):
+ # define variables
+ command = None
+ # Check if flow-accounting was removed and define command
+ if not config['flow-accounting-configured']:
+ command = 'systemctl stop uacctd.service'
+ else:
+ command = 'systemctl restart uacctd.service'
+
+ # run command to start or stop flow-accounting
+ cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting')
+
+ # configure iptables rules for defined interfaces
+ if config['interfaces']:
+ _iptables_config(config['interfaces'])
+ else:
+ _iptables_config([])
+
+if __name__ == '__main__':
+ try:
+ config = get_config()
+ verify(config)
+ generate(config)
+ apply(config)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
new file mode 100755
index 000000000..9d66bd434
--- /dev/null
+++ b/src/conf_mode/host_name.py
@@ -0,0 +1,175 @@
+#!/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/>.
+
+"""
+conf-mode script for 'system host-name' and 'system domain-name'.
+"""
+
+import re
+import sys
+import copy
+
+import vyos.util
+import vyos.hostsd_client
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import cmd, call, process_named_running
+
+from vyos import airbag
+airbag.enable()
+
+default_config_data = {
+ 'hostname': 'vyos',
+ 'domain_name': '',
+ 'domain_search': [],
+ 'nameserver': [],
+ 'nameservers_dhcp_interfaces': [],
+ 'static_host_mapping': {}
+}
+
+hostsd_tag = 'system'
+
+def get_config():
+ conf = Config()
+
+ hosts = copy.deepcopy(default_config_data)
+
+ hosts['hostname'] = conf.return_value("system host-name")
+
+ # This may happen if the config is not loaded yet,
+ # e.g. if run by cloud-init
+ if not hosts['hostname']:
+ hosts['hostname'] = default_config_data['hostname']
+
+ if conf.exists("system domain-name"):
+ hosts['domain_name'] = conf.return_value("system domain-name")
+ hosts['domain_search'].append(hosts['domain_name'])
+
+ for search in conf.return_values("system domain-search domain"):
+ hosts['domain_search'].append(search)
+
+ hosts['nameserver'] = conf.return_values("system name-server")
+
+ hosts['nameservers_dhcp_interfaces'] = conf.return_values("system name-servers-dhcp")
+
+ # system static-host-mapping
+ for hn in conf.list_nodes('system static-host-mapping host-name'):
+ hosts['static_host_mapping'][hn] = {}
+ hosts['static_host_mapping'][hn]['address'] = conf.return_value(f'system static-host-mapping host-name {hn} inet')
+ hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(f'system static-host-mapping host-name {hn} alias')
+
+ return hosts
+
+
+def verify(hosts):
+ if hosts is None:
+ return None
+
+ # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)"
+ hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$")
+ if not hostname_regex.match(hosts['hostname']):
+ raise ConfigError('Invalid host name ' + hosts["hostname"])
+
+ # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length"
+ length = len(hosts['hostname'])
+ if length < 1 or length > 63:
+ raise ConfigError(
+ 'Invalid host-name length, must be less than 63 characters')
+
+ all_static_host_mapping_addresses = []
+ # static mappings alias hostname
+ for host, hostprops in hosts['static_host_mapping'].items():
+ if not hostprops['address']:
+ raise ConfigError(f'IP address required for static-host-mapping "{host}"')
+ all_static_host_mapping_addresses.append(hostprops['address'])
+ for a in hostprops['aliases']:
+ if not hostname_regex.match(a) and len(a) != 0:
+ raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"')
+
+ # TODO: add warnings for nameservers_dhcp_interfaces if interface doesn't
+ # exist or doesn't have address dhcp(v6)
+
+ return None
+
+
+def generate(config):
+ pass
+
+def apply(config):
+ if config is None:
+ return None
+
+ ## Send the updated data to vyos-hostsd
+ try:
+ hc = vyos.hostsd_client.Client()
+
+ hc.set_host_name(config['hostname'], config['domain_name'])
+
+ hc.delete_search_domains([hostsd_tag])
+ if config['domain_search']:
+ hc.add_search_domains({hostsd_tag: config['domain_search']})
+
+ hc.delete_name_servers([hostsd_tag])
+ if config['nameserver']:
+ hc.add_name_servers({hostsd_tag: config['nameserver']})
+
+ # add our own tag's (system) nameservers and search to resolv.conf
+ hc.delete_name_server_tags_system(hc.get_name_server_tags_system())
+ hc.add_name_server_tags_system([hostsd_tag])
+
+ # this will add the dhcp client nameservers to resolv.conf
+ for intf in config['nameservers_dhcp_interfaces']:
+ hc.add_name_server_tags_system([f'dhcp-{intf}', f'dhcpv6-{intf}'])
+
+ hc.delete_hosts([hostsd_tag])
+ if config['static_host_mapping']:
+ hc.add_hosts({hostsd_tag: config['static_host_mapping']})
+
+ hc.apply()
+ except vyos.hostsd_client.VyOSHostsdError as e:
+ raise ConfigError(str(e))
+
+ ## Actually update the hostname -- vyos-hostsd doesn't do that
+
+ # No domain name -- the Debian way.
+ hostname_new = config['hostname']
+
+ # rsyslog runs into a race condition at boot time with systemd
+ # restart rsyslog only if the hostname changed.
+ hostname_old = cmd('hostnamectl --static')
+ call(f'hostnamectl set-hostname --static {hostname_new}')
+
+ # Restart services that use the hostname
+ if hostname_new != hostname_old:
+ call("systemctl restart rsyslog.service")
+
+ # If SNMP is running, restart it too
+ if process_named_running('snmpd'):
+ call('systemctl restart snmpd.service')
+
+ return None
+
+
+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/http-api.py b/src/conf_mode/http-api.py
new file mode 100755
index 000000000..b8a084a40
--- /dev/null
+++ b/src/conf_mode/http-api.py
@@ -0,0 +1,113 @@
+#!/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
+import os
+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
+
+from vyos import airbag
+airbag.enable()
+
+config_file = '/etc/vyos/http-api.conf'
+
+vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode']
+
+# XXX: this model will need to be extended for tag nodes
+dependencies = [
+ 'https.py',
+]
+
+def get_config():
+ http_api = deepcopy(vyos.defaults.api_data)
+ x = http_api.get('api_keys')
+ if x is None:
+ default_key = None
+ else:
+ default_key = x[0]
+ keys_added = False
+
+ conf = Config()
+ if not conf.exists('service https api'):
+ return None
+ else:
+ conf.set_level('service https api')
+
+ if conf.exists('strict'):
+ http_api['strict'] = 'true'
+
+ if conf.exists('debug'):
+ http_api['debug'] = 'true'
+
+ if conf.exists('port'):
+ port = conf.return_value('port')
+ http_api['port'] = port
+
+ if conf.exists('keys'):
+ for name in conf.list_nodes('keys id'):
+ if conf.exists('keys id {0} key'.format(name)):
+ key = conf.return_value('keys id {0} key'.format(name))
+ new_key = { 'id': name, 'key': key }
+ http_api['api_keys'].append(new_key)
+ keys_added = True
+
+ if keys_added and default_key:
+ if default_key in http_api['api_keys']:
+ http_api['api_keys'].remove(default_key)
+
+ return http_api
+
+def verify(http_api):
+ return None
+
+def generate(http_api):
+ if http_api is None:
+ return None
+
+ if not os.path.exists('/etc/vyos'):
+ os.mkdir('/etc/vyos')
+
+ with open(config_file, 'w') as f:
+ json.dump(http_api, f, indent=2)
+
+ return None
+
+def apply(http_api):
+ if http_api is not None:
+ call('systemctl restart vyos-http-api.service')
+ else:
+ call('systemctl stop vyos-http-api.service')
+
+ for dep in dependencies:
+ cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
+
+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/https.py b/src/conf_mode/https.py
new file mode 100755
index 000000000..a13f131ab
--- /dev/null
+++ b/src/conf_mode/https.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 copy import deepcopy
+
+import vyos.defaults
+import vyos.certbot_util
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = '/etc/nginx/sites-available/default'
+certbot_dir = vyos.defaults.directories['certbot']
+
+# https config needs to coordinate several subsystems: api, certbot,
+# self-signed certificate, as well as the virtual hosts defined within the
+# https config definition itself. Consequently, one needs a general dict,
+# encompassing the https and other configs, and a list of such virtual hosts
+# (server blocks in nginx terminology) to pass to the jinja2 template.
+default_server_block = {
+ 'id' : '',
+ 'address' : '*',
+ 'port' : '443',
+ 'name' : ['_'],
+ 'api' : {},
+ 'vyos_cert' : {},
+ 'certbot' : False
+}
+
+def get_config():
+ conf = Config()
+ if not conf.exists('service https'):
+ return None
+
+ server_block_list = []
+ https_dict = conf.get_config_dict('service https', get_first_key=True)
+
+ # organize by vhosts
+
+ vhost_dict = https_dict.get('virtual-host', {})
+
+ if not vhost_dict:
+ # no specified virtual hosts (server blocks); use default
+ server_block_list.append(default_server_block)
+ else:
+ for vhost in list(vhost_dict):
+ server_block = deepcopy(default_server_block)
+ server_block['id'] = vhost
+ data = vhost_dict.get(vhost, {})
+ server_block['address'] = data.get('listen-address', '*')
+ server_block['port'] = data.get('listen-port', '443')
+ name = data.get('server-name', ['_'])
+ # XXX: T2636 workaround: convert string to a list with one element
+ if not isinstance(name, list):
+ name = [name]
+ server_block['name'] = name
+ server_block_list.append(server_block)
+
+ # get certificate data
+
+ cert_dict = https_dict.get('certificates', {})
+
+ # self-signed certificate
+
+ vyos_cert_data = {}
+ if 'system-generated-certificate' in list(cert_dict):
+ vyos_cert_data = vyos.defaults.vyos_cert_data
+ if vyos_cert_data:
+ for block in server_block_list:
+ block['vyos_cert'] = vyos_cert_data
+
+ # letsencrypt certificate using certbot
+
+ certbot = False
+ cert_domains = cert_dict.get('certbot', {}).get('domain-name', [])
+ if cert_domains:
+ # XXX: T2636 workaround: convert string to a list with one element
+ if not isinstance(cert_domains, list):
+ cert_domains = [cert_domains]
+ certbot = True
+ for domain in cert_domains:
+ sub_list = vyos.certbot_util.choose_server_block(server_block_list,
+ domain)
+ if sub_list:
+ for sb in sub_list:
+ sb['certbot'] = True
+ sb['certbot_dir'] = certbot_dir
+ # certbot organizes certificates by first domain
+ sb['certbot_domain_dir'] = cert_domains[0]
+
+ # get api data
+
+ api_set = False
+ api_data = {}
+ if 'api' in list(https_dict):
+ api_set = True
+ api_data = vyos.defaults.api_data
+ api_settings = https_dict.get('api', {})
+ if api_settings:
+ port = api_settings.get('port', '')
+ if port:
+ api_data['port'] = port
+ vhosts = https_dict.get('api-restrict', {}).get('virtual-host', [])
+ # XXX: T2636 workaround: convert string to a list with one element
+ if not isinstance(vhosts, list):
+ vhosts = [vhosts]
+ if vhosts:
+ api_data['vhost'] = vhosts[:]
+
+ if api_data:
+ vhost_list = api_data.get('vhost', [])
+ if not vhost_list:
+ 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
+
+ # return dict for use in template
+
+ https = {'server_block_list' : server_block_list,
+ 'api_set': api_set,
+ 'certbot': certbot}
+
+ return https
+
+def verify(https):
+ if https is None:
+ return None
+
+ if https['certbot']:
+ for sb in https['server_block_list']:
+ if sb['certbot']:
+ return None
+ raise ConfigError("At least one 'virtual-host <id> server-name' "
+ "matching the 'certbot domain-name' is required.")
+ return None
+
+def generate(https):
+ if https is None:
+ return None
+
+ if 'server_block_list' not in https or not https['server_block_list']:
+ https['server_block_list'] = [default_server_block]
+
+ render(config_file, 'https/nginx.default.tmpl', https, trim_blocks=True)
+
+ return None
+
+def apply(https):
+ if https is not None:
+ call('systemctl restart nginx.service')
+ else:
+ call('systemctl stop nginx.service')
+
+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/igmp_proxy.py b/src/conf_mode/igmp_proxy.py
new file mode 100755
index 000000000..49aea9b7f
--- /dev/null
+++ b/src/conf_mode/igmp_proxy.py
@@ -0,0 +1,142 @@
+#!/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 sys import exit
+from copy import deepcopy
+
+from netifaces import interfaces
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/igmpproxy.conf'
+
+default_config_data = {
+ 'disable': False,
+ 'disable_quickleave': False,
+ 'interfaces': [],
+}
+
+def get_config():
+ igmp_proxy = deepcopy(default_config_data)
+ conf = Config()
+ base = ['protocols', 'igmp-proxy']
+ if not conf.exists(base):
+ return None
+ else:
+ conf.set_level(base)
+
+ # Network interfaces to listen on
+ if conf.exists(['disable']):
+ igmp_proxy['disable'] = True
+
+ # Option to disable "quickleave"
+ if conf.exists(['disable-quickleave']):
+ igmp_proxy['disable_quickleave'] = True
+
+ for intf in conf.list_nodes(['interface']):
+ conf.set_level(base + ['interface', intf])
+ interface = {
+ 'name': intf,
+ 'alt_subnet': [],
+ 'role': 'downstream',
+ 'threshold': '1',
+ 'whitelist': []
+ }
+
+ 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(['threshold']):
+ interface['threshold'] = conf.return_value(['threshold'])
+
+ if conf.exists(['whitelist']):
+ interface['whitelist'] = conf.return_values(['whitelist'])
+
+ # Append interface configuration to global configuration list
+ igmp_proxy['interfaces'].append(interface)
+
+ return igmp_proxy
+
+def verify(igmp_proxy):
+ # bail out early - looks like removal from running config
+ if igmp_proxy is None:
+ return None
+
+ # bail out early - service is disabled
+ if igmp_proxy['disable']:
+ return None
+
+ # at least two interfaces are required, one upstream and one downstream
+ if len(igmp_proxy['interfaces']) < 2:
+ raise ConfigError('Must define an upstream and at least 1 downstream interface!')
+
+ upstream = 0
+ for interface in igmp_proxy['interfaces']:
+ if interface['name'] not in interfaces():
+ raise ConfigError('Interface "{}" does not exist'.format(interface['name']))
+ if "upstream" == interface['role']:
+ upstream += 1
+
+ if upstream == 0:
+ raise ConfigError('At least 1 upstream interface is required!')
+ elif upstream > 1:
+ raise ConfigError('Only 1 upstream interface allowed!')
+
+ return None
+
+def generate(igmp_proxy):
+ # bail out early - looks like removal from running config
+ if igmp_proxy is None:
+ return None
+
+ # bail out early - service is disabled, but inform user
+ if igmp_proxy['disable']:
+ print('Warning: IGMP Proxy will be deactivated because it is disabled')
+ return None
+
+ render(config_file, 'igmp-proxy/igmpproxy.conf.tmpl', igmp_proxy)
+ return None
+
+def apply(igmp_proxy):
+ if igmp_proxy is None or igmp_proxy['disable']:
+ # IGMP Proxy support is removed in the commit
+ call('systemctl stop igmpproxy.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ else:
+ call('systemctl restart igmpproxy.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/intel_qat.py b/src/conf_mode/intel_qat.py
new file mode 100755
index 000000000..742f09a54
--- /dev/null
+++ b/src/conf_mode/intel_qat.py
@@ -0,0 +1,103 @@
+#!/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
+import os
+import re
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import popen, run
+
+from vyos import airbag
+airbag.enable()
+
+# Define for recovering
+gl_ipsec_conf = None
+
+def get_config():
+ c = Config()
+ config_data = {
+ 'qat_conf' : None,
+ 'ipsec_conf' : None,
+ 'openvpn_conf' : None,
+ }
+
+ if c.exists('system acceleration qat'):
+ config_data['qat_conf'] = True
+
+ if c.exists('vpn ipsec '):
+ gl_ipsec_conf = True
+ config_data['ipsec_conf'] = True
+
+ if c.exists('interfaces openvpn'):
+ config_data['openvpn_conf'] = True
+
+ return config_data
+
+# 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:
+ return run('ipsec start')
+ return run(f'ipsec {action}')
+
+def verify(c):
+ # Check if QAT service installed
+ if not os.path.exists('/etc/init.d/qat_service'):
+ raise ConfigError("Warning: QAT init file not found")
+
+ if c['qat_conf'] == None:
+ return
+
+ # Check if QAT device exist
+ output, err = popen('lspci -nn', decode='utf-8')
+ if not err:
+ 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")
+ sys.exit(1)
+
+def apply(c):
+ if c['ipsec_conf']:
+ # Shutdown VPN service which can use QAT
+ vpn_control('stop')
+
+ # Disable QAT service
+ if c['qat_conf'] == None:
+ run('/etc/init.d/qat_service stop')
+ if c['ipsec_conf']:
+ vpn_control('start')
+ return
+
+ # Run qat init.d script
+ run('/etc/init.d/qat_service start')
+ if c['ipsec_conf']:
+ # Recovery VPN service
+ vpn_control('start')
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ vpn_control('restore')
+ sys.exit(1)
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
new file mode 100755
index 000000000..3b238f1ea
--- /dev/null
+++ b/src/conf_mode/interfaces-bonding.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_vlan_config
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import BondIf
+from vyos.validate import is_member
+from vyos.validate import has_address_configured
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_bond_mode(mode):
+ if mode == 'round-robin':
+ return 'balance-rr'
+ elif mode == 'active-backup':
+ return 'active-backup'
+ elif mode == 'xor-hash':
+ return 'balance-xor'
+ elif mode == 'broadcast':
+ return 'broadcast'
+ elif mode == '802.3ad':
+ return '802.3ad'
+ elif mode == 'transmit-load-balance':
+ return 'balance-tlb'
+ elif mode == 'adaptive-load-balance':
+ return 'balance-alb'
+ else:
+ raise ConfigError(f'invalid bond mode "{mode}"')
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'bonding']
+ bond = get_interface_dict(conf, base)
+
+ # To make our own life easier transfor the list of member interfaces
+ # into a dictionary - we will use this to add additional information
+ # later on for wach member
+ if 'member' in bond and 'interface' in bond['member']:
+ # first convert it to a list if only one member is given
+ if isinstance(bond['member']['interface'], str):
+ bond['member']['interface'] = [bond['member']['interface']]
+
+ tmp={}
+ for interface in bond['member']['interface']:
+ tmp.update({interface: {}})
+
+ bond['member']['interface'] = tmp
+
+ if 'mode' in bond:
+ bond['mode'] = get_bond_mode(bond['mode'])
+
+ tmp = leaf_node_changed(conf, ['mode'])
+ if tmp:
+ bond.update({'shutdown_required': ''})
+
+ # determine which members have been removed
+ tmp = leaf_node_changed(conf, ['member', 'interface'])
+ if tmp:
+ bond.update({'shutdown_required': ''})
+ if 'member' in bond:
+ bond['member'].update({'interface_remove': tmp })
+ else:
+ bond.update({'member': {'interface_remove': tmp }})
+
+ if 'member' in bond and 'interface' in bond['member']:
+ for interface, interface_config in bond['member']['interface'].items():
+ # Check if we are a member of another bond device
+ tmp = is_member(conf, interface, 'bridge')
+ if tmp:
+ interface_config.update({'is_bridge_member' : tmp})
+
+ # Check if we are a member of a bond device
+ tmp = is_member(conf, interface, 'bonding')
+ if tmp and tmp != bond['ifname']:
+ interface_config.update({'is_bond_member' : tmp})
+
+ # bond members must not have an assigned address
+ tmp = has_address_configured(conf, interface)
+ if tmp:
+ interface_config.update({'has_address' : ''})
+
+ return bond
+
+
+def verify(bond):
+ if 'deleted' in bond:
+ verify_bridge_delete(bond)
+ return None
+
+ if 'arp_monitor' in bond:
+ if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16:
+ raise ConfigError('The maximum number of arp-monitor targets is 16')
+
+ if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0:
+ if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
+ 'transmit-load-balance or adaptive-load-balance')
+
+ if 'primary' in bond:
+ if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('Option primary - mode dependency failed, not'
+ 'supported in mode {mode}!'.format(**bond))
+
+ verify_address(bond)
+ verify_dhcpv6(bond)
+ verify_vrf(bond)
+
+ # use common function to verify VLAN configuration
+ verify_vlan_config(bond)
+
+ bond_name = bond['ifname']
+ if 'member' in bond:
+ member = bond.get('member')
+ for interface, interface_config in member.get('interface', {}).items():
+ error_msg = f'Can not add interface "{interface}" to bond "{bond_name}", '
+
+ if interface == 'lo':
+ raise ConfigError('Loopback interface "lo" can not be added to a bond')
+
+ if interface not in interfaces():
+ raise ConfigError(error_msg + 'it does not exist!')
+
+ if 'is_bridge_member' in interface_config:
+ tmp = interface_config['is_bridge_member']
+ raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
+
+ if 'is_bond_member' in interface_config:
+ tmp = interface_config['is_bond_member']
+ raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
+
+ if 'has_address' in interface_config:
+ raise ConfigError(error_msg + 'it has an address assigned!')
+
+
+ if 'primary' in bond:
+ if bond['primary'] not in bond['member']['interface']:
+ raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface')
+
+ if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('primary interface only works for mode active-backup, ' \
+ 'transmit-load-balance or adaptive-load-balance')
+
+ return None
+
+def generate(bond):
+ return None
+
+def apply(bond):
+ b = BondIf(bond['ifname'])
+
+ if 'deleted' in bond:
+ # delete interface
+ b.remove()
+ else:
+ b.update(bond)
+
+ 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/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
new file mode 100755
index 000000000..ee8e85e73
--- /dev/null
+++ b/src/conf_mode/interfaces-bridge.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import node_changed
+from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import BridgeIf
+from vyos.validate import is_member, has_address_configured
+from vyos.xml import defaults
+
+from vyos.util import cmd
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'bridge']
+ bridge = get_interface_dict(conf, base)
+
+ # determine which members have been removed
+ tmp = node_changed(conf, ['member', 'interface'])
+ if tmp:
+ if 'member' in bridge:
+ bridge['member'].update({'interface_remove': tmp })
+ else:
+ bridge.update({'member': {'interface_remove': tmp }})
+
+ if 'member' in bridge and 'interface' in bridge['member']:
+ # XXX TT2665 we need a copy of the dict keys for iteration, else we will get:
+ # RuntimeError: dictionary changed size during iteration
+ for interface in list(bridge['member']['interface']):
+ for key in ['cost', 'priority']:
+ if interface == key:
+ del bridge['member']['interface'][key]
+ continue
+
+ # the default dictionary is not properly paged into the dict (see T2665)
+ # thus we will ammend it ourself
+ default_member_values = defaults(base + ['member', 'interface'])
+ for interface, interface_config in bridge['member']['interface'].items():
+ interface_config.update(default_member_values)
+
+ # Check if we are a member of another bridge device
+ tmp = is_member(conf, interface, 'bridge')
+ if tmp and tmp != bridge['ifname']:
+ interface_config.update({'is_bridge_member' : tmp})
+
+ # Check if we are a member of a bond device
+ tmp = is_member(conf, interface, 'bonding')
+ if tmp:
+ interface_config.update({'is_bond_member' : tmp})
+
+ # Bridge members must not have an assigned address
+ tmp = has_address_configured(conf, interface)
+ if tmp:
+ interface_config.update({'has_address' : ''})
+
+ return bridge
+
+def verify(bridge):
+ if 'deleted' in bridge:
+ return None
+
+ verify_dhcpv6(bridge)
+ verify_vrf(bridge)
+
+ if 'member' in bridge:
+ member = bridge.get('member')
+ bridge_name = bridge['ifname']
+ for interface, interface_config in member.get('interface', {}).items():
+ error_msg = f'Can not add interface "{interface}" to bridge "{bridge_name}", '
+
+ if interface == 'lo':
+ raise ConfigError('Loopback interface "lo" can not be added to a bridge')
+
+ if interface not in interfaces():
+ raise ConfigError(error_msg + 'it does not exist!')
+
+ if 'is_bridge_member' in interface_config:
+ tmp = interface_config['is_bridge_member']
+ raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
+
+ if 'is_bond_member' in interface_config:
+ tmp = interface_config['is_bond_member']
+ raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
+
+ if 'has_address' in interface_config:
+ raise ConfigError(error_msg + 'it has an address assigned!')
+
+ return None
+
+def generate(bridge):
+ return None
+
+def apply(bridge):
+ br = BridgeIf(bridge['ifname'])
+ if 'deleted' in bridge:
+ # delete interface
+ br.remove()
+ else:
+ br.update(bridge)
+
+ 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/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
new file mode 100755
index 000000000..8df86c8ea
--- /dev/null
+++ b/src/conf_mode/interfaces-dummy.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.ifconfig import DummyIf
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'dummy']
+ dummy = get_interface_dict(conf, base)
+ return dummy
+
+def verify(dummy):
+ if 'deleted' in dummy.keys():
+ verify_bridge_delete(dummy)
+ return None
+
+ verify_vrf(dummy)
+ verify_address(dummy)
+
+ return None
+
+def generate(dummy):
+ return None
+
+def apply(dummy):
+ d = DummyIf(dummy['ifname'])
+
+ # Remove dummy interface
+ if 'deleted' in dummy.keys():
+ d.remove()
+ else:
+ d.update(dummy)
+
+ 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/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
new file mode 100755
index 000000000..10758e35a
--- /dev/null
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_address
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_vlan_config
+from vyos.ifconfig import EthernetIf
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'ethernet']
+ ethernet = get_interface_dict(conf, base)
+ return ethernet
+
+def verify(ethernet):
+ if 'deleted' in ethernet:
+ return None
+
+ verify_interface_exists(ethernet)
+
+ if ethernet.get('speed', None) == 'auto':
+ if ethernet.get('duplex', None) != 'auto':
+ raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too')
+
+ if ethernet.get('duplex', None) == 'auto':
+ if ethernet.get('speed', None) != 'auto':
+ raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too')
+
+ verify_dhcpv6(ethernet)
+ verify_address(ethernet)
+ verify_vrf(ethernet)
+
+ if {'is_bond_member', 'mac'} <= set(ethernet):
+ print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" '
+ f'is a member of bond "{is_bond_member}"'.format(**ethernet))
+
+ # use common function to verify VLAN configuration
+ verify_vlan_config(ethernet)
+ return None
+
+def generate(ethernet):
+ return None
+
+def apply(ethernet):
+ e = EthernetIf(ethernet['ifname'])
+ if 'deleted' in ethernet:
+ # delete interface
+ e.remove()
+ else:
+ e.update(ethernet)
+
+
+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-geneve.py b/src/conf_mode/interfaces-geneve.py
new file mode 100755
index 000000000..1104bd3c0
--- /dev/null
+++ b/src/conf_mode/interfaces-geneve.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.ifconfig import GeneveIf
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'geneve']
+ geneve = get_interface_dict(conf, base)
+ return geneve
+
+def verify(geneve):
+ if 'deleted' in geneve:
+ verify_bridge_delete(geneve)
+ return None
+
+ verify_address(geneve)
+
+ if 'remote' not in geneve:
+ raise ConfigError('Remote side must be configured')
+
+ if 'vni' not in geneve:
+ raise ConfigError('VNI must be configured')
+
+ return None
+
+
+def generate(geneve):
+ return None
+
+
+def apply(geneve):
+ # Check if GENEVE interface already exists
+ if geneve['ifname'] in interfaces():
+ g = GeneveIf(geneve['ifname'])
+ # GENEVE is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ g.remove()
+
+ if 'deleted' not in geneve:
+ # GENEVE interface needs to be created on-block
+ # instead of passing a ton of arguments, I just use a dict
+ # that is managed by vyos.ifconfig
+ conf = deepcopy(GeneveIf.get_config())
+
+ # Assign GENEVE instance configuration parameters to config dict
+ conf['vni'] = geneve['vni']
+ conf['remote'] = geneve['remote']
+
+ # Finally create the new interface
+ g = GeneveIf(geneve['ifname'], **conf)
+ g.update(geneve)
+
+ 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/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
new file mode 100755
index 000000000..0978df5b6
--- /dev/null
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.ifconfig import L2TPv3If
+from vyos.util import check_kmod
+from vyos.validate import is_addr_assigned
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+k_mod = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6']
+
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'l2tpv3']
+ l2tpv3 = get_interface_dict(conf, base)
+
+ # L2TPv3 is "special" the default MTU is 1488 - update accordingly
+ # as the config_level is already st in get_interface_dict() - we can use []
+ tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+ if 'mtu' not in tmp:
+ l2tpv3['mtu'] = '1488'
+
+ # To delete an l2tpv3 interface we need the current tunnel and session-id
+ if 'deleted' in l2tpv3:
+ tmp = leaf_node_changed(conf, ['tunnel-id'])
+ l2tpv3.update({'tunnel_id': tmp})
+
+ tmp = leaf_node_changed(conf, ['session-id'])
+ l2tpv3.update({'session_id': tmp})
+
+ return l2tpv3
+
+def verify(l2tpv3):
+ if 'deleted' in l2tpv3:
+ verify_bridge_delete(l2tpv3)
+ return None
+
+ interface = l2tpv3['ifname']
+
+ for key in ['local_ip', 'remote_ip', 'tunnel_id', 'peer_tunnel_id',
+ 'session_id', 'peer_session_id']:
+ if key not in l2tpv3:
+ tmp = key.replace('_', '-')
+ raise ConfigError(f'L2TPv3 {tmp} must be configured!')
+
+ if not is_addr_assigned(l2tpv3['local_ip']):
+ raise ConfigError('L2TPv3 local-ip address '
+ '"{local_ip}" is not configured!'.format(**l2tpv3))
+
+ verify_address(l2tpv3)
+ return None
+
+def generate(l2tpv3):
+ 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
+ # vyos.ifconfig
+ conf = deepcopy(L2TPv3If.get_config())
+
+ # Check if L2TPv3 interface already exists
+ if l2tpv3['ifname'] in interfaces():
+ # L2TPv3 is picky when changing tunnels/sessions, thus we can simply
+ # always delete it first.
+ conf['session_id'] = l2tpv3['session_id']
+ conf['tunnel_id'] = l2tpv3['tunnel_id']
+ l = L2TPv3If(l2tpv3['ifname'], **conf)
+ l.remove()
+
+ if 'deleted' not in l2tpv3:
+ conf['peer_tunnel_id'] = l2tpv3['peer_tunnel_id']
+ conf['local_port'] = l2tpv3['source_port']
+ conf['remote_port'] = l2tpv3['destination_port']
+ conf['encapsulation'] = l2tpv3['encapsulation']
+ conf['local_address'] = l2tpv3['local_ip']
+ conf['remote_address'] = l2tpv3['remote_ip']
+ conf['session_id'] = l2tpv3['session_id']
+ conf['tunnel_id'] = l2tpv3['tunnel_id']
+ conf['peer_session_id'] = l2tpv3['peer_session_id']
+
+ # Finally create the new interface
+ l = L2TPv3If(l2tpv3['ifname'], **conf)
+ l.update(l2tpv3)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ check_kmod(k_mod)
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
new file mode 100755
index 000000000..0398cd591
--- /dev/null
+++ b/src/conf_mode/interfaces-loopback.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.ifconfig import LoopbackIf
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'loopback']
+ loopback = get_interface_dict(conf, base)
+ return loopback
+
+def verify(loopback):
+ return None
+
+def generate(loopback):
+ return None
+
+def apply(loopback):
+ l = LoopbackIf(loopback['ifname'])
+ if 'deleted' in loopback.keys():
+ l.remove()
+ else:
+ l.update(loopback)
+
+ 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/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
new file mode 100755
index 000000000..ca15212d4
--- /dev/null
+++ b/src/conf_mode/interfaces-macsec.py
@@ -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/>.
+
+import os
+
+from copy import deepcopy
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.ifconfig import MACsecIf
+from vyos.template import render
+from vyos.util import call
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_source_interface
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+# XXX: wpa_supplicant works on the source interface
+wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf'
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'macsec']
+ macsec = get_interface_dict(conf, base)
+
+ # Check if interface has been removed
+ if 'deleted' in macsec:
+ source_interface = conf.return_effective_value(
+ base + ['source-interface'])
+ macsec.update({'source_interface': source_interface})
+
+ return macsec
+
+
+def verify(macsec):
+ if 'deleted' in macsec:
+ verify_bridge_delete(macsec)
+ return None
+
+ verify_source_interface(macsec)
+ verify_vrf(macsec)
+ verify_address(macsec)
+
+ if not (('security' in macsec) and
+ ('cipher' in macsec['security'])):
+ raise ConfigError(
+ 'Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))
+
+ if (('security' in macsec) and
+ ('encrypt' in macsec['security'])):
+ tmp = macsec.get('security')
+
+ if not (('mka' in tmp) and
+ ('cak' in tmp['mka']) and
+ ('ckn' in tmp['mka'])):
+ raise ConfigError('Missing mandatory MACsec security '
+ 'keys as encryption is enabled!')
+
+ return None
+
+
+def generate(macsec):
+ render(wpa_suppl_conf.format(**macsec),
+ 'macsec/wpa_supplicant.conf.tmpl', macsec)
+ return None
+
+
+def apply(macsec):
+ # Remove macsec interface
+ if 'deleted' in macsec.keys():
+ call('systemctl stop wpa_supplicant-macsec@{source_interface}'
+ .format(**macsec))
+
+ MACsecIf(macsec['ifname']).remove()
+
+ # delete configuration on interface removal
+ if os.path.isfile(wpa_suppl_conf.format(**macsec)):
+ os.unlink(wpa_suppl_conf.format(**macsec))
+
+ else:
+ # MACsec interfaces require a configuration when they are added using
+ # iproute2. This static method will provide the configuration
+ # dictionary used by this class.
+
+ # XXX: subject of removal after completing T2653
+ conf = deepcopy(MACsecIf.get_config())
+ conf['source_interface'] = macsec['source_interface']
+ conf['security_cipher'] = macsec['security']['cipher']
+
+ # 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
+ i = MACsecIf(macsec['ifname'], **conf)
+ i.update(macsec)
+
+ call('systemctl restart wpa_supplicant-macsec@{source_interface}'
+ .format(**macsec))
+
+ 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/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
new file mode 100755
index 000000000..1420b4116
--- /dev/null
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -0,0 +1,1116 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 re
+
+from copy import deepcopy
+from sys import exit,stderr
+from ipaddress import ip_address,ip_network,IPv4Address,IPv4Network,IPv6Address,IPv6Network,summarize_address_range
+from netifaces import interfaces
+from time import sleep
+from shutil import rmtree
+
+from vyos.config import Config
+from vyos.configdict import list_diff
+from vyos.ifconfig import VTunIf
+from vyos.template import render
+from vyos.util import call, chown, chmod_600, chmod_755
+from vyos.validate import is_addr_assigned, is_member, is_ipv4
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+user = 'openvpn'
+group = 'openvpn'
+
+default_config_data = {
+ 'address': [],
+ 'auth_user': '',
+ 'auth_pass': '',
+ 'auth_user_pass_file': '',
+ 'auth': False,
+ 'compress_lzo': False,
+ 'deleted': False,
+ 'description': '',
+ 'disable': False,
+ 'disable_ncp': False,
+ 'encryption': '',
+ 'hash': '',
+ 'intf': '',
+ 'ipv6_accept_ra': 1,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
+ 'ipv6_local_address': [],
+ 'ipv6_remote_address': [],
+ 'is_bridge_member': False,
+ 'ping_restart': '60',
+ 'ping_interval': '10',
+ 'local_address': [],
+ 'local_address_subnet': '',
+ 'local_host': '',
+ 'local_port': '',
+ 'mode': '',
+ 'ncp_ciphers': '',
+ 'options': [],
+ 'persistent_tunnel': False,
+ 'protocol': 'udp',
+ 'protocol_real': '',
+ 'redirect_gateway': '',
+ 'remote_address': [],
+ 'remote_host': [],
+ 'remote_port': '',
+ 'client': [],
+ 'server_domain': '',
+ 'server_max_conn': '',
+ 'server_dns_nameserver': [],
+ 'server_pool': True,
+ 'server_pool_start': '',
+ 'server_pool_stop': '',
+ 'server_pool_netmask': '',
+ 'server_push_route': [],
+ 'server_reject_unconfigured': False,
+ 'server_subnet': [],
+ 'server_topology': '',
+ 'server_ipv6_dns_nameserver': [],
+ 'server_ipv6_local': '',
+ 'server_ipv6_prefixlen': '',
+ 'server_ipv6_remote': '',
+ 'server_ipv6_pool': True,
+ 'server_ipv6_pool_base': '',
+ 'server_ipv6_pool_prefixlen': '',
+ 'server_ipv6_push_route': [],
+ 'server_ipv6_subnet': [],
+ 'shared_secret_file': '',
+ 'tls': False,
+ 'tls_auth': '',
+ 'tls_ca_cert': '',
+ 'tls_cert': '',
+ 'tls_crl': '',
+ 'tls_dh': '',
+ 'tls_key': '',
+ 'tls_crypt': '',
+ 'tls_role': '',
+ 'tls_version_min': '',
+ 'type': 'tun',
+ 'uid': user,
+ 'gid': group,
+ 'vrf': ''
+}
+
+
+def get_config_name(intf):
+ cfg_file = f'/run/openvpn/{intf}.conf'
+ return cfg_file
+
+
+def checkCertHeader(header, filename):
+ """
+ Verify if filename contains specified header.
+ Returns True if match is found, False if no match or file is not found
+ """
+ if not os.path.isfile(filename):
+ return False
+
+ with open(filename, 'r') as f:
+ for line in f:
+ if re.match(header, line):
+ return True
+
+ return False
+
+def getDefaultServer(network, topology, devtype):
+ """
+ Gets the default server parameters for a IPv4 "server" directive.
+ Logic from openvpn's src/openvpn/helper.c.
+ Returns a dict with addresses or False if the input parameters were incorrect.
+ """
+ if not (devtype == 'tun' or devtype == 'tap'):
+ return False
+
+ if not network.version == 4:
+ return False
+ elif (devtype == 'tun' and network.prefixlen > 29) or (devtype == 'tap' and network.prefixlen > 30):
+ return False
+
+ server = {
+ 'local': '',
+ 'remote_netmask': '',
+ 'client_remote_netmask': '',
+ 'pool_start': '',
+ 'pool_stop': '',
+ 'pool_netmask': ''
+ }
+
+ if devtype == 'tun':
+ if topology == 'net30' or topology == 'point-to-point':
+ server['local'] = network[1]
+ server['remote_netmask'] = network[2]
+ server['client_remote_netmask'] = server['local']
+
+ # pool start is 4th host IP in subnet (.4 in a /24)
+ server['pool_start'] = network[4]
+
+ if network.prefixlen == 29:
+ server['pool_stop'] = network.broadcast_address
+ else:
+ # pool end is -4 from the broadcast address (.251 in a /24)
+ server['pool_stop'] = network[-5]
+
+ elif topology == 'subnet':
+ server['local'] = network[1]
+ server['remote_netmask'] = str(network.netmask)
+ server['client_remote_netmask'] = server['remote_netmask']
+ server['pool_start'] = network[2]
+ server['pool_stop'] = network[-3]
+ server['pool_netmask'] = server['remote_netmask']
+
+ elif devtype == 'tap':
+ server['local'] = network[1]
+ server['remote_netmask'] = str(network.netmask)
+ server['client_remote_netmask'] = server['remote_netmask']
+ server['pool_start'] = network[2]
+ server['pool_stop'] = network[-2]
+ server['pool_netmask'] = server['remote_netmask']
+
+ return server
+
+def get_config():
+ openvpn = 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')
+
+ openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw"
+
+ # check if interface is member of a bridge
+ openvpn['is_bridge_member'] = is_member(conf, openvpn['intf'], 'bridge')
+
+ # Check if interface instance has been removed
+ if not conf.exists('interfaces openvpn ' + openvpn['intf']):
+ openvpn['deleted'] = True
+ return openvpn
+
+ # bridged server should not have a pool by default (but can be specified manually)
+ if openvpn['is_bridge_member']:
+ openvpn['server_pool'] = False
+ openvpn['server_ipv6_pool'] = False
+
+ # set configuration level
+ conf.set_level('interfaces openvpn ' + openvpn['intf'])
+
+ # retrieve authentication options - username
+ if conf.exists('authentication username'):
+ openvpn['auth_user'] = conf.return_value('authentication username')
+ openvpn['auth'] = True
+
+ # retrieve authentication options - username
+ if conf.exists('authentication password'):
+ openvpn['auth_pass'] = conf.return_value('authentication password')
+ openvpn['auth'] = True
+
+ # retrieve interface description
+ if conf.exists('description'):
+ openvpn['description'] = conf.return_value('description')
+
+ # interface device-type
+ if conf.exists('device-type'):
+ openvpn['type'] = conf.return_value('device-type')
+
+ # disable interface
+ if conf.exists('disable'):
+ openvpn['disable'] = True
+
+ # data encryption algorithm cipher
+ if conf.exists('encryption cipher'):
+ openvpn['encryption'] = conf.return_value('encryption cipher')
+
+ # disable ncp-ciphers support
+ if conf.exists('encryption disable-ncp'):
+ openvpn['disable_ncp'] = True
+
+ # data encryption algorithm ncp-list
+ if conf.exists('encryption ncp-ciphers'):
+ _ncp_ciphers = []
+ for enc in conf.return_values('encryption ncp-ciphers'):
+ if enc == 'des':
+ _ncp_ciphers.append('des-cbc')
+ _ncp_ciphers.append('DES-CBC')
+ elif enc == '3des':
+ _ncp_ciphers.append('des-ede3-cbc')
+ _ncp_ciphers.append('DES-EDE3-CBC')
+ elif enc == 'aes128':
+ _ncp_ciphers.append('aes-128-cbc')
+ _ncp_ciphers.append('AES-128-CBC')
+ elif enc == 'aes128gcm':
+ _ncp_ciphers.append('aes-128-gcm')
+ _ncp_ciphers.append('AES-128-GCM')
+ elif enc == 'aes192':
+ _ncp_ciphers.append('aes-192-cbc')
+ _ncp_ciphers.append('AES-192-CBC')
+ elif enc == 'aes192gcm':
+ _ncp_ciphers.append('aes-192-gcm')
+ _ncp_ciphers.append('AES-192-GCM')
+ elif enc == 'aes256':
+ _ncp_ciphers.append('aes-256-cbc')
+ _ncp_ciphers.append('AES-256-CBC')
+ elif enc == 'aes256gcm':
+ _ncp_ciphers.append('aes-256-gcm')
+ _ncp_ciphers.append('AES-256-GCM')
+ openvpn['ncp_ciphers'] = ':'.join(_ncp_ciphers)
+
+ # hash algorithm
+ if conf.exists('hash'):
+ openvpn['hash'] = conf.return_value('hash')
+
+ # Maximum number of keepalive packet failures
+ if conf.exists('keep-alive failure-count') and conf.exists('keep-alive interval'):
+ fail_count = conf.return_value('keep-alive failure-count')
+ interval = conf.return_value('keep-alive interval')
+ openvpn['ping_interval' ] = interval
+ openvpn['ping_restart' ] = int(interval) * int(fail_count)
+
+ # Local IP address of tunnel - even as it is a tag node - we can only work
+ # on the first address
+ if conf.exists('local-address'):
+ for tmp in conf.list_nodes('local-address'):
+ tmp_ip = ip_address(tmp)
+ if tmp_ip.version == 4:
+ openvpn['local_address'].append(tmp)
+ if conf.exists('local-address {} subnet-mask'.format(tmp)):
+ openvpn['local_address_subnet'] = conf.return_value('local-address {} subnet-mask'.format(tmp))
+ elif tmp_ip.version == 6:
+ # input IPv6 address could be expanded so get the compressed version
+ openvpn['ipv6_local_address'].append(str(tmp_ip))
+
+ # Local IP address to accept connections
+ if conf.exists('local-host'):
+ openvpn['local_host'] = conf.return_value('local-host')
+
+ # Local port number to accept connections
+ 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 prefixes for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ openvpn['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Determine currently effective EUI64 addresses - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('ipv6 address eui64')
+ openvpn['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, openvpn['ipv6_eui64_prefix'])
+
+ # Remove the default link-local address if set.
+ if conf.exists('ipv6 address no-default-link-local'):
+ openvpn['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ openvpn['ipv6_eui64_prefix'].append('fe80::/64')
+
+ # 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'))
+
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if openvpn['ipv6_autoconf'] or 'dhcpv6' in openvpn['address']:
+ openvpn['ipv6_accept_ra'] = 2
+
+ # OpenVPN operation mode
+ if conf.exists('mode'):
+ openvpn['mode'] = conf.return_value('mode')
+
+ # Additional OpenVPN options
+ if conf.exists('openvpn-option'):
+ openvpn['options'] = conf.return_values('openvpn-option')
+
+ # Do not close and reopen interface
+ if conf.exists('persistent-tunnel'):
+ openvpn['persistent_tunnel'] = True
+
+ # Communication protocol
+ if conf.exists('protocol'):
+ openvpn['protocol'] = conf.return_value('protocol')
+
+ # IP address of remote end of tunnel
+ if conf.exists('remote-address'):
+ for tmp in conf.return_values('remote-address'):
+ tmp_ip = ip_address(tmp)
+ if tmp_ip.version == 4:
+ openvpn['remote_address'].append(tmp)
+ elif tmp_ip.version == 6:
+ openvpn['ipv6_remote_address'].append(str(tmp_ip))
+
+ # Remote host to connect to (dynamic if not set)
+ if conf.exists('remote-host'):
+ openvpn['remote_host'] = conf.return_values('remote-host')
+
+ # Remote port number to connect to
+ if conf.exists('remote-port'):
+ openvpn['remote_port'] = conf.return_value('remote-port')
+
+ # OpenVPN tunnel to be used as the default route
+ # see https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/
+ # redirect-gateway flags
+ if conf.exists('replace-default-route'):
+ openvpn['redirect_gateway'] = 'def1'
+
+ if conf.exists('replace-default-route local'):
+ openvpn['redirect_gateway'] = 'local def1'
+
+ # Topology for clients
+ if conf.exists('server topology'):
+ openvpn['server_topology'] = conf.return_value('server topology')
+
+ # Server-mode subnet (from which client IPs are allocated)
+ server_network_v4 = None
+ server_network_v6 = None
+ if conf.exists('server subnet'):
+ for tmp in conf.return_values('server subnet'):
+ tmp_ip = ip_network(tmp)
+ if tmp_ip.version == 4:
+ server_network_v4 = tmp_ip
+ # convert the network to format: "192.0.2.0 255.255.255.0" for later use in template
+ openvpn['server_subnet'].append(tmp_ip.with_netmask.replace(r'/', ' '))
+ elif tmp_ip.version == 6:
+ server_network_v6 = tmp_ip
+ openvpn['server_ipv6_subnet'].append(str(tmp_ip))
+
+ # Client-specific settings
+ for client in conf.list_nodes('server client'):
+ # set configuration level
+ conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client ' + client)
+ data = {
+ 'name': client,
+ 'disable': False,
+ 'ip': [],
+ 'ipv6_ip': [],
+ 'ipv6_remote': '',
+ 'ipv6_push_route': [],
+ 'ipv6_subnet': [],
+ 'push_route': [],
+ 'subnet': [],
+ 'remote_netmask': ''
+ }
+
+ # Option to disable client connection
+ if conf.exists('disable'):
+ data['disable'] = True
+
+ # IP address of the client
+ for tmp in conf.return_values('ip'):
+ tmp_ip = ip_address(tmp)
+ if tmp_ip.version == 4:
+ data['ip'].append(tmp)
+ elif tmp_ip.version == 6:
+ data['ipv6_ip'].append(str(tmp_ip))
+
+ # Route to be pushed to the client
+ for tmp in conf.return_values('push-route'):
+ tmp_ip = ip_network(tmp)
+ if tmp_ip.version == 4:
+ data['push_route'].append(tmp_ip.with_netmask.replace(r'/', ' '))
+ elif tmp_ip.version == 6:
+ data['ipv6_push_route'].append(str(tmp_ip))
+
+ # Subnet belonging to the client
+ for tmp in conf.return_values('subnet'):
+ tmp_ip = ip_network(tmp)
+ if tmp_ip.version == 4:
+ data['subnet'].append(tmp_ip.with_netmask.replace(r'/', ' '))
+ elif tmp_ip.version == 6:
+ data['ipv6_subnet'].append(str(tmp_ip))
+
+ # Append to global client list
+ openvpn['client'].append(data)
+
+ # re-set configuration level
+ conf.set_level('interfaces openvpn ' + openvpn['intf'])
+
+ # Server client IP pool
+ if conf.exists('server client-ip-pool'):
+ conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client-ip-pool')
+
+ # enable or disable server_pool where necessary
+ # default is enabled, or disabled in bridge mode
+ openvpn['server_pool'] = not conf.exists('disable')
+
+ if conf.exists('start'):
+ openvpn['server_pool_start'] = conf.return_value('start')
+
+ if conf.exists('stop'):
+ openvpn['server_pool_stop'] = conf.return_value('stop')
+
+ if conf.exists('netmask'):
+ openvpn['server_pool_netmask'] = conf.return_value('netmask')
+
+ conf.set_level('interfaces openvpn ' + openvpn['intf'])
+
+ # Server client IPv6 pool
+ if conf.exists('server client-ipv6-pool'):
+ conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client-ipv6-pool')
+ openvpn['server_ipv6_pool'] = not conf.exists('disable')
+ if conf.exists('base'):
+ tmp = conf.return_value('base').split('/')
+ openvpn['server_ipv6_pool_base'] = str(IPv6Address(tmp[0]))
+ if 1 < len(tmp):
+ openvpn['server_ipv6_pool_prefixlen'] = tmp[1]
+
+ conf.set_level('interfaces openvpn ' + openvpn['intf'])
+
+ # DNS suffix to be pushed to all clients
+ if conf.exists('server domain-name'):
+ openvpn['server_domain'] = conf.return_value('server domain-name')
+
+ # Number of maximum client connections
+ if conf.exists('server max-connections'):
+ openvpn['server_max_conn'] = conf.return_value('server max-connections')
+
+ # Domain Name Server (DNS)
+ if conf.exists('server name-server'):
+ for tmp in conf.return_values('server name-server'):
+ tmp_ip = ip_address(tmp)
+ if tmp_ip.version == 4:
+ openvpn['server_dns_nameserver'].append(tmp)
+ elif tmp_ip.version == 6:
+ openvpn['server_ipv6_dns_nameserver'].append(str(tmp_ip))
+
+ # Route to be pushed to all clients
+ if conf.exists('server push-route'):
+ for tmp in conf.return_values('server push-route'):
+ tmp_ip = ip_network(tmp)
+ if tmp_ip.version == 4:
+ openvpn['server_push_route'].append(tmp_ip.with_netmask.replace(r'/', ' '))
+ elif tmp_ip.version == 6:
+ openvpn['server_ipv6_push_route'].append(str(tmp_ip))
+
+ # Reject connections from clients that are not explicitly configured
+ if conf.exists('server reject-unconfigured-clients'):
+ openvpn['server_reject_unconfigured'] = True
+
+ # File containing TLS auth static key
+ if conf.exists('tls auth-file'):
+ openvpn['tls_auth'] = conf.return_value('tls auth-file')
+ openvpn['tls'] = True
+
+ # File containing certificate for Certificate Authority (CA)
+ if conf.exists('tls ca-cert-file'):
+ openvpn['tls_ca_cert'] = conf.return_value('tls ca-cert-file')
+ openvpn['tls'] = True
+
+ # File containing certificate for this host
+ if conf.exists('tls cert-file'):
+ openvpn['tls_cert'] = conf.return_value('tls cert-file')
+ openvpn['tls'] = True
+
+ # File containing certificate revocation list (CRL) for this host
+ if conf.exists('tls crl-file'):
+ openvpn['tls_crl'] = conf.return_value('tls crl-file')
+ openvpn['tls'] = True
+
+ # File containing Diffie Hellman parameters (server only)
+ if conf.exists('tls dh-file'):
+ openvpn['tls_dh'] = conf.return_value('tls dh-file')
+ openvpn['tls'] = True
+
+ # File containing this host's private key
+ if conf.exists('tls key-file'):
+ 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')
+ openvpn['tls'] = True
+
+ # 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')
+
+ 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'
+
+ # set default server topology to net30
+ if openvpn['mode'] == 'server' and not openvpn['server_topology']:
+ openvpn['server_topology'] = 'net30'
+
+ # Convert protocol to real protocol used by openvpn.
+ # To make openvpn listen on both IPv4 and IPv6 we must use *6 protocols
+ # (https://community.openvpn.net/openvpn/ticket/360), unless the local-host
+ # or each of the remote-host in client mode is IPv4
+ # in which case it must use the standard protocols.
+ if openvpn['protocol'] == 'tcp-active':
+ openvpn['protocol_real'] = 'tcp6-client'
+ elif openvpn['protocol'] == 'tcp-passive':
+ openvpn['protocol_real'] = 'tcp6-server'
+ else:
+ openvpn['protocol_real'] = 'udp6'
+
+ if ( is_ipv4(openvpn['local_host']) or
+ # in client mode test all the remotes instead
+ (openvpn['mode'] == 'client' and all([is_ipv4(h) for h in openvpn['remote_host']])) ):
+ # takes out the '6'
+ openvpn['protocol_real'] = openvpn['protocol_real'][:3] + openvpn['protocol_real'][4:]
+
+ # Set defaults where necessary.
+ # If any of the input parameters are wrong,
+ # this will return False and no defaults will be set.
+ if server_network_v4 and openvpn['server_topology'] and openvpn['type']:
+ default_server = None
+ default_server = getDefaultServer(server_network_v4, openvpn['server_topology'], openvpn['type'])
+ if default_server:
+ # server-bridge doesn't require a pool so don't set defaults for it
+ if openvpn['server_pool'] and not openvpn['is_bridge_member']:
+ if not openvpn['server_pool_start']:
+ openvpn['server_pool_start'] = default_server['pool_start']
+
+ if not openvpn['server_pool_stop']:
+ openvpn['server_pool_stop'] = default_server['pool_stop']
+
+ if not openvpn['server_pool_netmask']:
+ openvpn['server_pool_netmask'] = default_server['pool_netmask']
+
+ for client in openvpn['client']:
+ client['remote_netmask'] = default_server['client_remote_netmask']
+
+ if server_network_v6:
+ if not openvpn['server_ipv6_local']:
+ openvpn['server_ipv6_local'] = server_network_v6[1]
+ if not openvpn['server_ipv6_prefixlen']:
+ openvpn['server_ipv6_prefixlen'] = server_network_v6.prefixlen
+ if not openvpn['server_ipv6_remote']:
+ openvpn['server_ipv6_remote'] = server_network_v6[2]
+
+ if openvpn['server_ipv6_pool'] and server_network_v6.prefixlen < 112:
+ if not openvpn['server_ipv6_pool_base']:
+ openvpn['server_ipv6_pool_base'] = server_network_v6[0x1000]
+ if not openvpn['server_ipv6_pool_prefixlen']:
+ openvpn['server_ipv6_pool_prefixlen'] = openvpn['server_ipv6_prefixlen']
+
+ for client in openvpn['client']:
+ client['ipv6_remote'] = openvpn['server_ipv6_local']
+
+ if openvpn['redirect_gateway']:
+ openvpn['redirect_gateway'] += ' ipv6'
+
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ openvpn['vrf'] = conf.return_value('vrf')
+
+ return openvpn
+
+def verify(openvpn):
+ if openvpn['deleted']:
+ if openvpn['is_bridge_member']:
+ raise ConfigError((
+ f'Cannot delete interface "{openvpn["intf"]}" as it is a '
+ f'member of bridge "{openvpn["is_bridge_menber"]}"!'))
+ return None
+
+
+ if not openvpn['mode']:
+ raise ConfigError('Must specify OpenVPN operation mode')
+
+ # Check if we have disabled ncp and at the same time specified ncp-ciphers
+ if openvpn['disable_ncp'] and openvpn['ncp_ciphers']:
+ raise ConfigError('Cannot specify both "encryption disable-ncp" and "encryption ncp-ciphers"')
+ #
+ # OpenVPN client mode - VERIFY
+ #
+ if openvpn['mode'] == 'client':
+ if openvpn['local_port']:
+ raise ConfigError('Cannot specify "local-port" in client mode')
+
+ if openvpn['local_host']:
+ raise ConfigError('Cannot specify "local-host" in client mode')
+
+ if openvpn['protocol'] == 'tcp-passive':
+ raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
+
+ if not openvpn['remote_host']:
+ raise ConfigError('Must specify "remote-host" in client mode')
+
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
+ raise ConfigError('Cannot specify "tls dh-file" in client mode')
+
+ #
+ # OpenVPN site-to-site - VERIFY
+ #
+ if openvpn['mode'] == 'site-to-site':
+ if openvpn['ncp_ciphers']:
+ raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client')
+
+ if openvpn['mode'] == 'site-to-site' and not openvpn['is_bridge_member']:
+ if not (openvpn['local_address'] or openvpn['ipv6_local_address']):
+ raise ConfigError('Must specify "local-address" or add interface to bridge')
+
+ if len(openvpn['local_address']) > 1 or len(openvpn['ipv6_local_address']) > 1:
+ raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "local-address"')
+
+ if len(openvpn['remote_address']) > 1 or len(openvpn['ipv6_remote_address']) > 1:
+ raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "remote-address"')
+
+ for host in openvpn['remote_host']:
+ if host in openvpn['remote_address'] or host in openvpn['ipv6_remote_address']:
+ raise ConfigError('"remote-address" cannot be the same as "remote-host"')
+
+ if openvpn['local_address'] and not (openvpn['remote_address'] or openvpn['local_address_subnet']):
+ raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address" or IPv4 "local-address subnet"')
+
+ if openvpn['remote_address'] and not openvpn['local_address']:
+ raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"')
+
+ if openvpn['ipv6_local_address'] and not openvpn['ipv6_remote_address']:
+ raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"')
+
+ if openvpn['ipv6_remote_address'] and not openvpn['ipv6_local_address']:
+ raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"')
+
+ if openvpn['type'] == 'tun':
+ if not (openvpn['remote_address'] or openvpn['ipv6_remote_address']):
+ raise ConfigError('Must specify "remote-address"')
+
+ if ( (openvpn['local_address'] and openvpn['local_address'] == openvpn['remote_address']) or
+ (openvpn['ipv6_local_address'] and openvpn['ipv6_local_address'] == openvpn['ipv6_remote_address']) ):
+ raise ConfigError('"local-address" and "remote-address" cannot be the same')
+
+ if openvpn['local_host'] in openvpn['local_address'] or openvpn['local_host'] in openvpn['ipv6_local_address']:
+ raise ConfigError('"local-address" cannot be the same as "local-host"')
+
+ else:
+ # checks for client-server or site-to-site bridged
+ if openvpn['local_address'] or openvpn['ipv6_local_address'] or openvpn['remote_address'] or openvpn['ipv6_remote_address']:
+ raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server or bridge mode')
+
+ #
+ # OpenVPN server mode - VERIFY
+ #
+ if openvpn['mode'] == 'server':
+ if openvpn['protocol'] == 'tcp-active':
+ raise ConfigError('Protocol "tcp-active" is not valid in server mode')
+
+ if openvpn['remote_port']:
+ raise ConfigError('Cannot specify "remote-port" in server mode')
+
+ if openvpn['remote_host']:
+ raise ConfigError('Cannot specify "remote-host" in server mode')
+
+ 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'] 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 len(openvpn['server_subnet']) > 1 or len(openvpn['server_ipv6_subnet']) > 1:
+ raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 server subnet')
+
+ for client in openvpn['client']:
+ if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1:
+ raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
+
+ if openvpn['server_subnet']:
+ subnet = IPv4Network(openvpn['server_subnet'][0].replace(' ', '/'))
+
+ if openvpn['type'] == 'tun' and subnet.prefixlen > 29:
+ raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported')
+ elif openvpn['type'] == 'tap' and subnet.prefixlen > 30:
+ raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported')
+
+ for client in openvpn['client']:
+ if client['ip'] and not IPv4Address(client['ip'][0]) in subnet:
+ raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}')
+
+ else:
+ if not openvpn['is_bridge_member']:
+ raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
+
+ if openvpn['server_pool']:
+ if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']):
+ raise ConfigError('Server client-ip-pool requires both start and stop addresses in bridged mode')
+ else:
+ v4PoolStart = IPv4Address(openvpn['server_pool_start'])
+ v4PoolStop = IPv4Address(openvpn['server_pool_stop'])
+ if v4PoolStart > v4PoolStop:
+ raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}')
+
+ v4PoolSize = int(v4PoolStop) - int(v4PoolStart)
+ if v4PoolSize >= 65536:
+ raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.')
+
+ v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop))
+ for client in openvpn['client']:
+ if client['ip']:
+ for v4PoolNet in v4PoolNets:
+ if IPv4Address(client['ip'][0]) in v4PoolNet:
+ print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.',
+ file=stderr)
+
+ if openvpn['server_ipv6_subnet']:
+ if not openvpn['server_subnet']:
+ raise ConfigError('IPv6 server requires an IPv4 server subnet')
+
+ if openvpn['server_ipv6_pool']:
+ if not openvpn['server_pool']:
+ raise ConfigError('IPv6 server pool requires an IPv4 server pool')
+
+ if int(openvpn['server_ipv6_pool_prefixlen']) >= 112:
+ raise ConfigError('IPv6 server pool must be larger than /112')
+
+ v6PoolStart = IPv6Address(openvpn['server_ipv6_pool_base'])
+ v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple
+ v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536
+ if v6PoolSize < v4PoolSize:
+ raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})')
+
+ v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop))
+ for client in openvpn['client']:
+ if client['ipv6_ip']:
+ for v6PoolNet in v6PoolNets:
+ if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet:
+ print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.',
+ file=stderr)
+
+ else:
+ if openvpn['server_ipv6_push_route']:
+ raise ConfigError('IPv6 push-route requires an IPv6 server subnet')
+
+ for client in openvpn ['client']:
+ if client['ipv6_ip']:
+ raise ConfigError(f'Server client "{client["name"]}" IPv6 IP requires an IPv6 server subnet')
+ if client['ipv6_push_route']:
+ raise ConfigError(f'Server client "{client["name"]} IPv6 push-route requires an IPv6 server subnet"')
+ if client['ipv6_subnet']:
+ raise ConfigError(f'Server client "{client["name"]} IPv6 subnet requires an IPv6 server subnet"')
+
+ else:
+ # checks for both client and site-to-site go here
+ if openvpn['server_reject_unconfigured']:
+ raise ConfigError('reject-unconfigured-clients is only supported in OpenVPN server mode')
+
+ if openvpn['server_topology']:
+ raise ConfigError('The "topology" option is only valid in server mode')
+
+ if (not openvpn['remote_host']) and openvpn['redirect_gateway']:
+ raise ConfigError('Cannot set "replace-default-route" without "remote-host"')
+
+ #
+ # OpenVPN common verification section
+ # not depending on any operation mode
+ #
+
+ # verify specified IP address is present on any interface on this system
+ if openvpn['local_host']:
+ if not is_addr_assigned(openvpn['local_host']):
+ raise ConfigError('No interface on system with specified local-host IP address: {}'.format(openvpn['local_host']))
+
+ # TCP active
+ if openvpn['protocol'] == 'tcp-active':
+ if openvpn['local_port']:
+ raise ConfigError('Cannot specify "local-port" with "tcp-active"')
+
+ if not openvpn['remote_host']:
+ raise ConfigError('Must specify "remote-host" with "tcp-active"')
+
+ # shared secret and TLS
+ if not (openvpn['shared_secret_file'] or openvpn['tls']):
+ raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"')
+
+ if openvpn['shared_secret_file'] and openvpn['tls']:
+ raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"')
+
+ if openvpn['mode'] in ['client', 'server']:
+ if not openvpn['tls']:
+ raise ConfigError('Must specify "tls" in client-server mode')
+
+ #
+ # TLS/encryption
+ #
+ if openvpn['shared_secret_file']:
+ if openvpn['encryption'] in ['aes128gcm', 'aes192gcm', 'aes256gcm']:
+ raise ConfigError('GCM encryption with shared-secret-key-file is not supported')
+
+ if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['shared_secret_file']):
+ raise ConfigError('Specified shared-secret-key-file "{}" is not valid'.format(openvpn['shared_secret_file']))
+
+ if openvpn['tls']:
+ if not openvpn['tls_ca_cert']:
+ raise ConfigError('Must specify "tls ca-cert-file"')
+
+ if not (openvpn['mode'] == 'client' and openvpn['auth']):
+ if not openvpn['tls_cert']:
+ raise ConfigError('Must specify "tls cert-file"')
+
+ 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']))
+
+ if openvpn['tls_auth']:
+ if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_auth']):
+ raise ConfigError('Specified auth-file "{}" is invalid'.format(openvpn['tls_auth']))
+
+ if openvpn['tls_cert']:
+ if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_cert']):
+ raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert']))
+
+ if 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'] 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']))
+
+ if openvpn['tls_role']:
+ if openvpn['mode'] in ['client', 'server']:
+ if not openvpn['tls_auth']:
+ raise ConfigError('Cannot specify "tls role" in client-server mode')
+
+ if openvpn['tls_role'] == 'active':
+ if openvpn['protocol'] == 'tcp-passive':
+ raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
+
+ 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':
+ if openvpn['protocol'] == 'tcp-active':
+ raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
+
+ 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
+ #
+ if openvpn['auth']:
+ if not openvpn['auth_user']:
+ raise ConfigError('Username for authentication is missing')
+
+ if not openvpn['auth_pass']:
+ raise ConfigError('Password for authentication is missing')
+
+ if openvpn['vrf']:
+ if openvpn['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{openvpn["vrf"]}" does not exist')
+
+ if openvpn['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{openvpn["intf"]}" cannot be member of VRF '
+ f'"{openvpn["vrf"]}" and bridge "{openvpn["is_bridge_member"]}" '
+ f'at the same time!'))
+
+ return None
+
+def generate(openvpn):
+ interface = openvpn['intf']
+ directory = os.path.dirname(get_config_name(interface))
+
+ # we can't know in advance which clients have been removed,
+ # thus all client configs will be removed and re-added on demand
+ ccd_dir = os.path.join(directory, 'ccd', interface)
+ if os.path.isdir(ccd_dir):
+ rmtree(ccd_dir, ignore_errors=True)
+
+ if openvpn['deleted'] or openvpn['disable']:
+ return None
+
+ # create config directory on demand
+ directories = []
+ directories.append(f'{directory}/status')
+ directories.append(f'{directory}/ccd/{interface}')
+ for onedir in directories:
+ if not os.path.exists(onedir):
+ os.makedirs(onedir, 0o755)
+ chown(onedir, user, group)
+
+ # Fix file permissons for keys
+ fix_permissions = []
+ fix_permissions.append(openvpn['shared_secret_file'])
+ fix_permissions.append(openvpn['tls_key'])
+
+ # Generate User/Password authentication file
+ if openvpn['auth']:
+ with open(openvpn['auth_user_pass_file'], 'w') as f:
+ f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass']))
+ # also change permission on auth file
+ fix_permissions.append(openvpn['auth_user_pass_file'])
+
+ else:
+ # delete old auth file if present
+ if os.path.isfile(openvpn['auth_user_pass_file']):
+ os.remove(openvpn['auth_user_pass_file'])
+
+ # Generate client specific configuration
+ for client in openvpn['client']:
+ client_file = os.path.join(ccd_dir, client['name'])
+ render(client_file, 'openvpn/client.conf.tmpl', client)
+ chown(client_file, user, group)
+
+ # we need to support quoting of raw parameters from OpenVPN CLI
+ # see https://phabricator.vyos.net/T1632
+ render(get_config_name(interface), 'openvpn/server.conf.tmpl', openvpn,
+ formater=lambda _: _.replace("&quot;", '"'))
+ chown(get_config_name(interface), user, group)
+
+ # Fixup file permissions
+ for file in fix_permissions:
+ chmod_600(file)
+
+ return None
+
+def apply(openvpn):
+ interface = openvpn['intf']
+ call(f'systemctl stop openvpn@{interface}.service')
+
+ # Do some cleanup when OpenVPN is disabled/deleted
+ if openvpn['deleted'] or openvpn['disable']:
+ # cleanup old configuration files
+ cleanup = []
+ cleanup.append(get_config_name(interface))
+ cleanup.append(openvpn['auth_user_pass_file'])
+
+ for file in cleanup:
+ if os.path.isfile(file):
+ os.unlink(file)
+
+ return None
+
+ # On configuration change we need to wait for the 'old' interface to
+ # vanish from the Kernel, if it is not gone, OpenVPN will report:
+ # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16)
+ while interface in interfaces():
+ sleep(0.250) # 250ms
+
+ # No matching OpenVPN process running - maybe it got killed or none
+ # existed - nevertheless, spawn new OpenVPN process
+ call(f'systemctl start openvpn@{interface}.service')
+
+ # better late then sorry ... but we can only set interface alias after
+ # OpenVPN has been launched and created the interface
+ cnt = 0
+ while interface not in interfaces():
+ # If VPN tunnel can't be established because the peer/server isn't
+ # (temporarily) available, the vtun interface never becomes registered
+ # with the kernel, and the commit would hang if there is no bail out
+ # condition
+ 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
+ o = VTunIf(interface)
+ # update interface description used e.g. within SNMP
+ o.set_alias(openvpn['description'])
+ # IPv6 accept RA
+ o.set_ipv6_accept_ra(openvpn['ipv6_accept_ra'])
+ # IPv6 address autoconfiguration
+ o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
+ # 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'])
+
+ # IPv6 EUI-based addresses - only in TAP mode (TUN's have no MAC)
+ # If MAC has changed, old EUI64 addresses won't get deleted,
+ # but this isn't easy to solve, so leave them.
+ # This is even more difficult as openvpn uses a random MAC for the
+ # initial interface creation, unless set by 'lladdr'.
+ # NOTE: right now the interface is always deleted. For future
+ # compatibility when tap's are not deleted, leave the del_ in
+ if openvpn['mode'] == 'tap':
+ for addr in openvpn['ipv6_eui64_prefix_remove']:
+ o.del_ipv6_eui64_address(addr)
+ for addr in openvpn['ipv6_eui64_prefix']:
+ o.add_ipv6_eui64_address(addr)
+
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not openvpn['is_bridge_member']:
+ o.set_vrf(openvpn['vrf'])
+
+ except:
+ pass
+
+ # TAP interface needs to be brought up explicitly
+ if openvpn['type'] == 'tap':
+ if not openvpn['disable']:
+ VTunIf(interface).set_admin_state('up')
+
+ 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/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
new file mode 100755
index 000000000..901ea769c
--- /dev/null
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_vrf
+from vyos.template import render
+from vyos.util import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'pppoe']
+ pppoe = get_interface_dict(conf, base)
+
+ # PPPoE is "special" the default MTU is 1492 - update accordingly
+ # as the config_level is already st in get_interface_dict() - we can use []
+ tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+ if 'mtu' not in tmp:
+ pppoe['mtu'] = '1492'
+
+ return pppoe
+
+def verify(pppoe):
+ if 'deleted' in pppoe:
+ # bail out early
+ return None
+
+ verify_source_interface(pppoe)
+ verify_vrf(pppoe)
+
+ if {'connect_on_demand', 'vrf'} <= set(pppoe):
+ raise ConfigError('On-demand dialing and VRF can not be used at the same time')
+
+ return None
+
+def generate(pppoe):
+ # set up configuration file path variables where our templates will be
+ # rendered into
+ ifname = pppoe['ifname']
+ config_pppoe = f'/etc/ppp/peers/{ifname}'
+ script_pppoe_pre_up = f'/etc/ppp/ip-pre-up.d/1000-vyos-pppoe-{ifname}'
+ script_pppoe_ip_up = f'/etc/ppp/ip-up.d/1000-vyos-pppoe-{ifname}'
+ script_pppoe_ip_down = f'/etc/ppp/ip-down.d/1000-vyos-pppoe-{ifname}'
+ script_pppoe_ipv6_up = f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{ifname}'
+ config_wide_dhcp6c = f'/run/dhcp6c/dhcp6c.{ifname}.conf'
+
+ config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up,
+ script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c]
+
+ if 'deleted' in pppoe:
+ # stop DHCPv6-PD client
+ call(f'systemctl stop dhcp6c@{ifname}.service')
+ # Hang-up PPPoE connection
+ call(f'systemctl stop ppp@{ifname}.service')
+
+ # Delete PPP configuration files
+ for file in config_files:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ return None
+
+ # Create PPP configuration files
+ render(config_pppoe, 'pppoe/peer.tmpl',
+ pppoe, trim_blocks=True, permission=0o755)
+ # Create script for ip-pre-up.d
+ render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl',
+ pppoe, trim_blocks=True, permission=0o755)
+ # Create script for ip-up.d
+ render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl',
+ pppoe, trim_blocks=True, permission=0o755)
+ # Create script for ip-down.d
+ render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl',
+ pppoe, trim_blocks=True, permission=0o755)
+ # Create script for ipv6-up.d
+ render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl',
+ pppoe, trim_blocks=True, permission=0o755)
+
+ if 'dhcpv6_options' in pppoe and 'pd' in pppoe['dhcpv6_options']:
+ # ipv6.tmpl relies on ifname - this should be made consitent in the
+ # future better then double key-ing the same value
+ render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe, trim_blocks=True)
+
+ return None
+
+def apply(pppoe):
+ if 'deleted' in pppoe:
+ # bail out early
+ return None
+
+ if 'disable' not in pppoe:
+ # Dial PPPoE connection
+ call('systemctl restart ppp@{ifname}.service'.format(**pppoe))
+
+ 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/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
new file mode 100755
index 000000000..fe2d7b1be
--- /dev/null
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_vlan_config
+from vyos.ifconfig import MACVLANIf
+from vyos.validate import is_member
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at
+ least the interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'pseudo-ethernet']
+ peth = get_interface_dict(conf, base)
+
+ mode = leaf_node_changed(conf, ['mode'])
+ if mode:
+ peth.update({'mode_old' : mode})
+
+ # Check if source-interface is member of a bridge device
+ if 'source_interface' in peth:
+ bridge = is_member(conf, peth['source_interface'], 'bridge')
+ if bridge:
+ peth.update({'source_interface_is_bridge_member' : bridge})
+
+ # Check if we are a member of a bond device
+ bond = is_member(conf, peth['source_interface'], 'bonding')
+ if bond:
+ peth.update({'source_interface_is_bond_member' : bond})
+
+ return peth
+
+def verify(peth):
+ if 'deleted' in peth:
+ verify_bridge_delete(peth)
+ return None
+
+ verify_source_interface(peth)
+ verify_vrf(peth)
+ verify_address(peth)
+
+ if 'source_interface_is_bridge_member' in peth:
+ raise ConfigError(
+ 'Source interface "{source_interface}" can not be used as it is already a '
+ 'member of bridge "{source_interface_is_bridge_member}"!'.format(**peth))
+
+ if 'source_interface_is_bond_member' in peth:
+ raise ConfigError(
+ 'Source interface "{source_interface}" can not be used as it is already a '
+ 'member of bond "{source_interface_is_bond_member}"!'.format(**peth))
+
+ # use common function to verify VLAN configuration
+ verify_vlan_config(peth)
+ return None
+
+def generate(peth):
+ return None
+
+def apply(peth):
+ if 'deleted' in peth:
+ # delete interface
+ MACVLANIf(peth['ifname']).remove()
+ return None
+
+ # Check if MACVLAN interface already exists. Parameters like the underlaying
+ # source-interface device or mode can not be changed on the fly and the
+ # interface needs to be recreated from the bottom.
+ if 'mode_old' in peth:
+ MACVLANIf(peth['ifname']).remove()
+
+ # MACVLAN interface needs to be created on-block instead of passing a ton
+ # of arguments, I just use a dict that is managed by vyos.ifconfig
+ conf = deepcopy(MACVLANIf.get_config())
+
+ # Assign MACVLAN instance configuration parameters to config dict
+ 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['ifname'], **conf)
+ p.update(peth)
+ 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/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
new file mode 100755
index 000000000..ea15a7fb7
--- /dev/null
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -0,0 +1,718 @@
+#!/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 netifaces import interfaces
+
+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, is_member
+from vyos import ConfigError
+from vyos.dicts import FixedDict
+
+from vyos import airbag
+airbag.enable()
+
+
+class ConfigurationState(object):
+ """
+ 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_api() 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, configuration, section, default):
+ """
+ initialise the class for a given configuration path:
+
+ >>> conf = ConfigurationState(conf, 'interfaces ethernet eth1')
+ all further references to get_value(s) and get_effective(s)
+ will be for this part of the configuration (eth1)
+ """
+ self._conf = configuration
+
+ 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._conf.exists(section):
+ self.changes['section'] = 'delete'
+ elif self._conf.exists_effective(section):
+ self.changes['section'] = 'modify'
+ else:
+ self.changes['section'] = 'create'
+
+ self.set_level(section)
+
+ def set_level(self, lpath):
+ self.section = lpath
+ self._conf.set_level(lpath)
+
+ 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._conf.exists(section):
+ if self._conf.exists_effective(section):
+ if self._conf.return_value(section) != self._conf.return_effective_value(section):
+ return 'modify'
+ return 'static'
+ return 'create'
+ else:
+ if self._conf.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._conf.return_value)
+
+ def get_values(self, name, key, default=None):
+ """
+ >>> conf.get_values('addresses', 'address')
+ will place a list of the new IP present in 'interface dummy dum1 address'
+ into the dictionnary entry "-add" (here 'addresses-add') using
+ Config.return_values and will add the the one which were removed in into
+ the entry "-del" (here addresses-del')
+ """
+ add_name = f'{name}-add'
+
+ if self._action(add_name, key) in ('delete', 'absent'):
+ return
+
+ self._get(add_name, key, default, self._conf.return_values)
+
+ # get the effective values to determine which data is no longer valid
+ self.options['addresses-del'] = list_diff(
+ self._conf.return_effective_values('address'),
+ self.options['addresses-add']
+ )
+
+ 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._conf.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._conf.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 not self._conf.exists(option):
+ del self.options[option]
+ continue
+
+ if self._conf.return_value(option) == self.default[option]:
+ del self.options[option]
+ continue
+
+ if self._conf.return_values(option) == self.default[option]:
+ del self.options[option]
+ continue
+
+ def as_dict(self, lpath):
+ l = self._conf.get_level()
+ self._conf.set_level([])
+ d = self._conf.get_config_dict(lpath)
+ # XXX: that not what I would have expected from get_config_dict
+ if lpath:
+ d = d[lpath[-1]]
+ # XXX: it should have provided me the content and not the key
+ self._conf.set_level(l)
+ return d
+
+ def to_api(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': [],
+ 'arp_filter': 1,
+ 'arp_accept': 0,
+ 'arp_announce': 0,
+ 'arp_ignore': 0,
+ 'ipv6_accept_ra': 1,
+ 'ipv6_autoconf': 0,
+ 'ipv6_forwarding': 1,
+ 'ipv6_dad_transmits': 1,
+ # internal
+ 'interfaces': [],
+ 'tunnel': {},
+ 'bridge': '',
+ # the following names are exactly matching the name
+ # for the ip command and must not be changed
+ 'ifname': '',
+ 'type': '',
+ 'alias': '',
+ 'mtu': '1476',
+ 'local': '',
+ 'remote': '',
+ 'dev': '',
+ '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),
+ 'dev': ('source-interface', 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': ('address', True, None),
+ 'arp_filter': ('ip disable-arp-filter', False, 0),
+ 'arp_accept': ('ip enable-arp-accept', False, 1),
+ 'arp_announce': ('ip enable-arp-announce', False, 1),
+ 'arp_ignore': ('ip enable-arp-ignore', False, 1),
+ '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')
+
+ config = Config()
+ conf = ConfigurationState(config, ['interfaces', 'tunnel ', ifname], default_config_data)
+ options = conf.options
+ changes = conf.changes
+ options['ifname'] = ifname
+
+ if changes['section'] == 'delete':
+ conf.get_effective('type', mapping['type'][0])
+ config.set_level(['protocols', 'nhrp', 'tunnel'])
+ options['nhrp'] = config.list_nodes('')
+ return conf.to_api()
+
+ # 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'] = ''
+
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if options['ipv6_autoconf'] or 'dhcpv6' in options['addresses-add']:
+ options['ipv6_accept_ra'] = 2
+
+ # allmulticast fate is linked to multicast
+ options['allmulticast'] = options['multicast']
+
+ # check that per encapsulation all local-remote pairs are unique
+ ct = conf.as_dict(['interfaces', 'tunnel'])
+ options['tunnel'] = {}
+
+ # check for bridges
+ options['bridge'] = is_member(config, ifname, 'bridge')
+ options['interfaces'] = interfaces()
+
+ 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_api()
+
+
+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'Cannot delete interface tunnel {iftype} {ifname}, '
+ 'it is used by NHRP'))
+
+ if options['bridge']:
+ raise ConfigError((
+ f'Cannot delete interface "{options["ifname"]}" as it is a '
+ f'member of bridge "{options["bridge"]}"!'))
+
+ # 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', 'state']
+ valid += ['arp_filter', 'arp_accept', 'arp_announce', 'arp_ignore']
+ valid += ['ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits']
+
+ 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']
+ tun_dev = options['dev']
+
+ # 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}')
+
+ if tun_dev and iftype in ('gre-bridge', 'sit'):
+ raise ConfigError(f'source interface can not be used with {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}')
+
+ # vrf check
+ if options['vrf']:
+ if options['vrf'] not in options['interfaces']:
+ raise ConfigError(f'VRF "{options["vrf"]}" does not exist')
+
+ if options['bridge']:
+ raise ConfigError((
+ f'Interface "{options["ifname"]}" cannot be member of VRF '
+ f'"{options["vrf"]}" and bridge {options["bridge"]} '
+ f'at the same time!'))
+
+ # bridge and address check
+ if ( options['bridge']
+ and ( options['addresses-add']
+ or options['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{options["name"]}" '
+ f'as it is a member of bridge "{options["bridge"]}"!'))
+
+ # source-interface check
+
+ if tun_dev and tun_dev not in options['interfaces']:
+ raise ConfigError(f'device "{tun_dev}" does not exist')
+
+ # 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
+ if not options[option]:
+ # remote can be set to '' and it would generate an invalide command
+ continue
+ tunnel.set_interface(option, options[option])
+
+ # set other interface properties
+ for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast',
+ 'arp_accept', 'arp_filter', 'arp_announce', 'arp_ignore',
+ 'ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
+ if not options[option]:
+ # should never happen but better safe
+ continue
+ tunnel.set_interface(option, options[option])
+
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not options['bridge']:
+ tunnel.set_vrf(options['vrf'])
+
+ # 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
new file mode 100755
index 000000000..47c0bdcb8
--- /dev/null
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_source_interface
+from vyos.ifconfig import VXLANIf, Interface
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'vxlan']
+ vxlan = get_interface_dict(conf, base)
+
+ # VXLAN is "special" the default MTU is 1492 - update accordingly
+ # as the config_level is already st in get_interface_dict() - we can use []
+ tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+ if 'mtu' not in tmp:
+ vxlan['mtu'] = '1450'
+
+ return vxlan
+
+def verify(vxlan):
+ if 'deleted' in vxlan:
+ verify_bridge_delete(vxlan)
+ return None
+
+ if int(vxlan['mtu']) < 1500:
+ print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
+
+ if 'group' in vxlan:
+ if 'source_interface' not in vxlan:
+ raise ConfigError('Multicast VXLAN requires an underlaying interface ')
+
+ verify_source_interface(vxlan)
+
+ if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan):
+ raise ConfigError('Group, remote or source-address must be configured')
+
+ if 'vni' not in vxlan:
+ raise ConfigError('Must configure VNI for VXLAN')
+
+ if 'source_interface' in vxlan:
+ # 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['source_interface']).get_mtu())
+ if underlay_mtu < (int(vxlan['mtu']) + 50):
+ raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
+ f'MTU is to small ({underlay_mtu} bytes)')
+
+ verify_address(vxlan)
+ return None
+
+
+def generate(vxlan):
+ return None
+
+
+def apply(vxlan):
+ # Check if the VXLAN interface already exists
+ if vxlan['ifname'] in interfaces():
+ v = VXLANIf(vxlan['ifname'])
+ # VXLAN is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ v.remove()
+
+ if 'deleted' not in vxlan:
+ # VXLAN interface needs to be created on-block
+ # instead of passing a ton of arguments, I just use a dict
+ # that is managed by vyos.ifconfig
+ conf = deepcopy(VXLANIf.get_config())
+
+ # Assign VXLAN instance configuration parameters to config dict
+ for tmp in ['vni', 'group', 'source_address', 'source_interface', 'remote', 'port']:
+ if tmp in vxlan:
+ conf[tmp] = vxlan[tmp]
+
+ # Finally create the new interface
+ v = VXLANIf(vxlan['ifname'], **conf)
+ v.update(vxlan)
+
+ 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/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
new file mode 100755
index 000000000..8b64cde4d
--- /dev/null
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -0,0 +1,115 @@
+#!/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 sys import exit
+from copy import deepcopy
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configdict import get_interface_dict
+from vyos.configdict import node_changed
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.ifconfig import WireGuardIf
+from vyos.util import check_kmod
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'wireguard']
+ wireguard = get_interface_dict(conf, base)
+
+ # Wireguard is "special" the default MTU is 1420 - update accordingly
+ # as the config_level is already st in get_interface_dict() - we can use []
+ tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+ if 'mtu' not in tmp:
+ wireguard['mtu'] = '1420'
+
+ # Mangle private key - it has a default so its always valid
+ wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard)
+
+ # Determine which Wireguard peer has been removed.
+ # Peers can only be removed with their public key!
+ tmp = node_changed(conf, ['peer'])
+ if tmp:
+ dict = {}
+ for peer in tmp:
+ peer_config = leaf_node_changed(conf, ['peer', peer, 'pubkey'])
+ dict = dict_merge({'peer_remove' : {peer : {'pubkey' : peer_config}}}, dict)
+ wireguard.update(dict)
+
+ return wireguard
+
+def verify(wireguard):
+ if 'deleted' in wireguard:
+ verify_bridge_delete(wireguard)
+ return None
+
+ verify_address(wireguard)
+ verify_vrf(wireguard)
+
+ if not os.path.exists(wireguard['private_key']):
+ raise ConfigError('Wireguard private-key not found! Execute: ' \
+ '"run generate wireguard [default-keypair|named-keypairs]"')
+
+ if 'address' not in wireguard:
+ raise ConfigError('IP address required!')
+
+ if 'peer' not in wireguard:
+ raise ConfigError('At least one Wireguard peer is required!')
+
+ # run checks on individual configured WireGuard peer
+ for tmp in wireguard['peer']:
+ peer = wireguard['peer'][tmp]
+
+ if 'allowed_ips' not in peer:
+ raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
+
+ if 'pubkey' not in peer:
+ raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
+
+ if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
+ raise ConfigError('Both Wireguard port and address must be defined '
+ f'for peer "{tmp}" if either one of them is set!')
+
+def apply(wireguard):
+ if 'deleted' in wireguard:
+ WireGuardIf(wireguard['ifname']).remove()
+ return None
+
+ w = WireGuardIf(wireguard['ifname'])
+ w.update(wireguard)
+ return None
+
+if __name__ == '__main__':
+ try:
+ check_kmod('wireguard')
+ c = get_config()
+ verify(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
new file mode 100755
index 000000000..b6f247952
--- /dev/null
+++ b/src/conf_mode/interfaces-wireless.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 re import findall
+from copy import deepcopy
+from netaddr import EUI, mac_unix_expanded
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_vlan_config
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import WiFiIf
+from vyos.template import render
+from vyos.util import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+# XXX: wpa_supplicant works on the source interface
+wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
+hostapd_conf = '/run/hostapd/{ifname}.conf'
+
+def find_other_stations(conf, base, ifname):
+ """
+ Only one wireless interface per phy can be in station mode -
+ find all interfaces attached to a phy which run in station mode
+ """
+ old_level = conf.get_level()
+ conf.set_level(base)
+ dict = {}
+ for phy in os.listdir('/sys/class/ieee80211'):
+ list = []
+ for interface in conf.list_nodes([]):
+ if interface == ifname:
+ continue
+ # the following node is mandatory
+ if conf.exists([interface, 'physical-device', phy]):
+ tmp = conf.return_value([interface, 'type'])
+ if tmp == 'station':
+ list.append(interface)
+ if list:
+ dict.update({phy: list})
+ conf.set_level(old_level)
+ return dict
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'wireless']
+ wifi = get_interface_dict(conf, base)
+
+ if 'security' in wifi and 'wpa' in wifi['security']:
+ wpa_cipher = wifi['security']['wpa'].get('cipher')
+ wpa_mode = wifi['security']['wpa'].get('mode')
+ if not wpa_cipher:
+ tmp = None
+ if wpa_mode == 'wpa':
+ tmp = {'security': {'wpa': {'cipher' : ['TKIP', 'CCMP']}}}
+ elif wpa_mode == 'wpa2':
+ tmp = {'security': {'wpa': {'cipher' : ['CCMP']}}}
+ elif wpa_mode == 'both':
+ tmp = {'security': {'wpa': {'cipher' : ['CCMP', 'TKIP']}}}
+
+ if tmp: wifi = dict_merge(tmp, wifi)
+
+ # retrieve configured regulatory domain
+ conf.set_level(['system'])
+ if conf.exists(['wifi-regulatory-domain']):
+ wifi['country_code'] = conf.return_value(['wifi-regulatory-domain'])
+
+ # Only one wireless interface per phy can be in station mode
+ tmp = find_other_stations(conf, base, wifi['ifname'])
+ if tmp: wifi['station_interfaces'] = tmp
+
+ return wifi
+
+def verify(wifi):
+ if 'deleted' in wifi:
+ verify_bridge_delete(wifi)
+ return None
+
+ if 'physical_device' not in wifi:
+ raise ConfigError('You must specify a physical-device "phy"')
+
+ if 'type' not in wifi:
+ raise ConfigError('You must specify a WiFi mode')
+
+ if 'ssid' not in wifi and wifi['type'] != 'monitor':
+ raise ConfigError('SSID must be configured')
+
+ if wifi['type'] == 'access-point':
+ if 'country_code' not in wifi:
+ raise ConfigError('Wireless regulatory domain is mandatory,\n' \
+ 'use "set system wifi-regulatory-domain" for configuration.')
+
+ if 'channel' not in wifi:
+ raise ConfigError('Wireless channel must be configured!')
+
+ if 'security' in wifi:
+ if {'wep', 'wpa'} <= set(wifi.get('security', {})):
+ raise ConfigError('Must either use WEP or WPA security!')
+
+ if 'wep' in wifi['security']:
+ if 'key' in wifi['security']['wep'] and len(wifi['security']['wep']) > 4:
+ raise ConfigError('No more then 4 WEP keys configurable')
+ elif 'key' not in wifi['security']['wep']:
+ raise ConfigError('Security WEP configured - missing WEP keys!')
+
+ elif 'wpa' in wifi['security']:
+ wpa = wifi['security']['wpa']
+ if not any(i in ['passphrase', 'radius'] for i in wpa):
+ raise ConfigError('Misssing WPA key or RADIUS server')
+
+ if 'radius' in wpa:
+ if 'server' in wpa['radius']:
+ for server in wpa['radius']['server']:
+ if 'key' not in wpa['radius']['server'][server]:
+ raise ConfigError(f'Misssing RADIUS shared secret key for server: {server}')
+
+ if 'capabilities' in wifi:
+ capabilities = wifi['capabilities']
+ if 'vht' in capabilities:
+ if 'ht' not in capabilities:
+ raise ConfigError('Specify HT flags if you want to use VHT!')
+
+ if {'beamform', 'antenna_count'} <= set(capabilities.get('vht', {})):
+ if capabilities['vht']['antenna_count'] == '1':
+ raise ConfigError('Cannot use beam forming with just one antenna!')
+
+ if capabilities['vht']['beamform'] == 'single-user-beamformer':
+ if int(capabilities['vht']['antenna_count']) < 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 'station_interfaces' in wifi and wifi['type'] == 'station':
+ phy = wifi['physical_device']
+ if phy in wifi['station_interfaces']:
+ if len(wifi['station_interfaces'][phy]) > 0:
+ raise ConfigError('Only one station per wireless physical interface possible!')
+
+ verify_address(wifi)
+ verify_vrf(wifi)
+
+ # use common function to verify VLAN configuration
+ verify_vlan_config(wifi)
+
+ return None
+
+def generate(wifi):
+ interface = wifi['ifname']
+
+ # always stop hostapd service first before reconfiguring it
+ call(f'systemctl stop hostapd@{interface}.service')
+ # always stop wpa_supplicant service first before reconfiguring it
+ call(f'systemctl stop wpa_supplicant@{interface}.service')
+
+ # Delete config files if interface is removed
+ if 'deleted' in wifi:
+ if os.path.isfile(hostapd_conf.format(**wifi)):
+ os.unlink(hostapd_conf.format(**wifi))
+
+ if os.path.isfile(wpa_suppl_conf.format(**wifi)):
+ os.unlink(wpa_suppl_conf.format(**wifi))
+
+ return None
+
+ if 'mac' not in wifi:
+ # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd
+ # generate locally administered MAC address from used phy interface
+ with open('/sys/class/ieee80211/{physical_device}/addresses'.format(**wifi), '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+', interface)[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':
+ render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi, trim_blocks=True)
+
+ elif wifi['type'] == 'station':
+ render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi, trim_blocks=True)
+
+ return None
+
+def apply(wifi):
+ interface = wifi['ifname']
+ if 'deleted' in wifi:
+ WiFiIf(interface).remove()
+ else:
+ # 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['physical_device']
+
+ # Finally create the new interface
+ w = WiFiIf(interface, **conf)
+ w.update(wifi)
+
+ # Enable/Disable interface - interface is always placed in
+ # administrative down state in WiFiIf class
+ if 'disable' not in wifi:
+ # 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':
+ call(f'systemctl start hostapd@{interface}.service')
+
+ elif wifi['type'] == 'station':
+ call(f'systemctl start wpa_supplicant@{interface}.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/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
new file mode 100755
index 000000000..6d168d918
--- /dev/null
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -0,0 +1,127 @@
+#!/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 vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_vrf
+from vyos.template import render
+from vyos.util import call
+from vyos.util import check_kmod
+from vyos.util import find_device_file
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+k_mod = ['option', 'usb_wwan', 'usbserial']
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'wirelessmodem']
+ wwan = get_interface_dict(conf, base)
+ return wwan
+
+def verify(wwan):
+ if 'deleted' in wwan:
+ return None
+
+ if not 'apn' in wwan:
+ raise ConfigError('No APN configured for "{ifname}"'.format(**wwan))
+
+ if not 'device' in wwan:
+ raise ConfigError('Physical "device" must be 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(find_device_file(wwan['device'])):
+ raise ConfigError('Device "{device}" does not exist'.format(**wwan))
+
+ verify_vrf(wwan)
+
+ return None
+
+def generate(wwan):
+ # set up configuration file path variables where our templates will be
+ # rendered into
+ ifname = wwan['ifname']
+ config_wwan = f'/etc/ppp/peers/{ifname}'
+ config_wwan_chat = f'/etc/ppp/peers/chat.{ifname}'
+ script_wwan_pre_up = f'/etc/ppp/ip-pre-up.d/1010-vyos-wwan-{ifname}'
+ script_wwan_ip_up = f'/etc/ppp/ip-up.d/1010-vyos-wwan-{ifname}'
+ script_wwan_ip_down = f'/etc/ppp/ip-down.d/1010-vyos-wwan-{ifname}'
+
+ config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up,
+ script_wwan_ip_up, script_wwan_ip_down]
+
+ # Always hang-up WWAN connection prior generating new configuration file
+ call(f'systemctl stop ppp@{ifname}.service')
+
+ if 'deleted' in wwan:
+ # Delete PPP configuration files
+ for file in config_files:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ else:
+ wwan['device'] = find_device_file(wwan['device'])
+
+ # Create PPP configuration files
+ render(config_wwan, 'wwan/peer.tmpl', wwan)
+ # Create PPP chat script
+ render(config_wwan_chat, 'wwan/chat.tmpl', wwan)
+
+ # generated script file must be executable
+
+ # Create script for ip-pre-up.d
+ render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl',
+ wwan, permission=0o755)
+ # Create script for ip-up.d
+ render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl',
+ wwan, permission=0o755)
+ # Create script for ip-down.d
+ render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl',
+ wwan, permission=0o755)
+
+ return None
+
+def apply(wwan):
+ if 'deleted' in wwan:
+ # bail out early
+ return None
+
+ if not 'disable' in wwan:
+ # "dial" WWAN connection
+ call('systemctl start ppp@{ifname}.service'.format(**wwan))
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ check_kmod(k_mod)
+ 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
new file mode 100755
index 000000000..015d1a480
--- /dev/null
+++ b/src/conf_mode/ipsec-settings.py
@@ -0,0 +1,227 @@
+#!/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 re
+import os
+
+from time import sleep
+from sys import exit
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+ra_conn_name = "remote-access"
+charon_conf_file = "/etc/strongswan.d/charon.conf"
+ipsec_secrets_file = "/etc/ipsec.secrets"
+ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/"
+ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name
+ipsec_conf_file = "/etc/ipsec.conf"
+ca_cert_path = "/etc/ipsec.d/cacerts"
+server_cert_path = "/etc/ipsec.d/certs"
+server_key_path = "/etc/ipsec.d/private"
+delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###"
+delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###"
+charon_pidfile = "/var/run/charon.pid"
+
+def get_config():
+ config = Config()
+ data = {"install_routes": "yes"}
+
+ if config.exists("vpn ipsec options disable-route-autoinstall"):
+ data["install_routes"] = "no"
+
+ if config.exists("vpn ipsec ipsec-interfaces interface"):
+ data["ipsec_interfaces"] = config.return_values("vpn ipsec ipsec-interfaces interface")
+
+ # Init config variables
+ data["delim_ipsec_l2tp_begin"] = delim_ipsec_l2tp_begin
+ data["delim_ipsec_l2tp_end"] = delim_ipsec_l2tp_end
+ data["ipsec_ra_conn_file"] = ipsec_ra_conn_file
+ data["ra_conn_name"] = ra_conn_name
+ # Get l2tp ipsec settings
+ data["ipsec_l2tp"] = False
+ conf_ipsec_command = "vpn l2tp remote-access ipsec-settings " #last space is useful
+ if config.exists(conf_ipsec_command):
+ data["ipsec_l2tp"] = True
+
+ # Authentication params
+ if config.exists(conf_ipsec_command + "authentication mode"):
+ data["ipsec_l2tp_auth_mode"] = config.return_value(conf_ipsec_command + "authentication mode")
+ if config.exists(conf_ipsec_command + "authentication pre-shared-secret"):
+ data["ipsec_l2tp_secret"] = config.return_value(conf_ipsec_command + "authentication pre-shared-secret")
+
+ # mode x509
+ if config.exists(conf_ipsec_command + "authentication x509 ca-cert-file"):
+ data["ipsec_l2tp_x509_ca_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 ca-cert-file")
+ if config.exists(conf_ipsec_command + "authentication x509 crl-file"):
+ data["ipsec_l2tp_x509_crl_file"] = config.return_value(conf_ipsec_command + "authentication x509 crl-file")
+ if config.exists(conf_ipsec_command + "authentication x509 server-cert-file"):
+ data["ipsec_l2tp_x509_server_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")
+ data["server_cert_file_copied"] = server_cert_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")).group(0)
+ if config.exists(conf_ipsec_command + "authentication x509 server-key-file"):
+ data["ipsec_l2tp_x509_server_key_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-file")
+ data["server_key_file_copied"] = server_key_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-key-file")).group(0)
+ if config.exists(conf_ipsec_command + "authentication x509 server-key-password"):
+ data["ipsec_l2tp_x509_server_key_password"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-password")
+
+ # Common l2tp ipsec params
+ if config.exists(conf_ipsec_command + "ike-lifetime"):
+ data["ipsec_l2tp_ike_lifetime"] = config.return_value(conf_ipsec_command + "ike-lifetime")
+ else:
+ data["ipsec_l2tp_ike_lifetime"] = "3600"
+
+ if config.exists(conf_ipsec_command + "lifetime"):
+ data["ipsec_l2tp_lifetime"] = config.return_value(conf_ipsec_command + "lifetime")
+ else:
+ data["ipsec_l2tp_lifetime"] = "3600"
+
+ if config.exists("vpn l2tp remote-access outside-address"):
+ data['outside_addr'] = config.return_value('vpn l2tp remote-access outside-address')
+
+ return data
+
+def write_ipsec_secrets(c):
+ if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret":
+ secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end)
+ elif c.get("ipsec_l2tp_auth_mode") == "x509":
+ secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end)
+
+ old_umask = os.umask(0o077)
+ with open(ipsec_secrets_file, 'a+') as f:
+ f.write(secret_txt)
+ os.umask(old_umask)
+
+def write_ipsec_conf(c):
+ ipsec_confg_txt = "{0}\ninclude {1}\n{2}\n".format(delim_ipsec_l2tp_begin, ipsec_ra_conn_file, delim_ipsec_l2tp_end)
+
+ old_umask = os.umask(0o077)
+ with open(ipsec_conf_file, 'a+') as f:
+ f.write(ipsec_confg_txt)
+ os.umask(old_umask)
+
+### Remove config from file by delimiter
+def remove_confs(delim_begin, delim_end, conf_file):
+ call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file)
+
+
+### 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):
+ print("Warning: \"" + file_path + "\" lies outside of /config/auth directory. It will not get preserved during image upgrade.")
+ #Checking file existence
+ if not os.path.isfile(file_path):
+ raise ConfigError("L2TP VPN configuration error: Invalid "+cert_name+" \""+file_path+"\"")
+ else:
+ ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/
+ # todo make check
+ ret = call('cp -f '+file_path+' '+dts_path)
+ if ret:
+ raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path)
+
+def verify(data):
+ # l2tp ipsec check
+ if data["ipsec_l2tp"]:
+ # Checking dependecies for "authentication mode pre-shared-secret"
+ if data.get("ipsec_l2tp_auth_mode") == "pre-shared-secret":
+ if not data.get("ipsec_l2tp_secret"):
+ raise ConfigError("pre-shared-secret required")
+ if not data.get("outside_addr"):
+ raise ConfigError("outside-address not defined")
+
+ # Checking dependecies for "authentication mode x509"
+ if data.get("ipsec_l2tp_auth_mode") == "x509":
+ if not data.get("ipsec_l2tp_x509_server_key_file"):
+ raise ConfigError("L2TP VPN configuration error: \"server-key-file\" not defined.")
+ else:
+ check_cert_file_store("server-key-file", data['ipsec_l2tp_x509_server_key_file'], server_key_path)
+
+ if not data.get("ipsec_l2tp_x509_server_cert_file"):
+ raise ConfigError("L2TP VPN configuration error: \"server-cert-file\" not defined.")
+ else:
+ check_cert_file_store("server-cert-file", data['ipsec_l2tp_x509_server_cert_file'], server_cert_path)
+
+ if not data.get("ipsec_l2tp_x509_ca_cert_file"):
+ raise ConfigError("L2TP VPN configuration error: \"ca-cert-file\" must be defined for X.509")
+ else:
+ check_cert_file_store("ca-cert-file", data['ipsec_l2tp_x509_ca_cert_file'], ca_cert_path)
+
+ if not data.get('ipsec_interfaces'):
+ raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.")
+
+def generate(data):
+ render(charon_conf_file, 'ipsec/charon.tmpl', data, trim_blocks=True)
+
+ if data["ipsec_l2tp"]:
+ remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
+ # old_umask = os.umask(0o077)
+ # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data, trim_blocks=True)
+ # os.umask(old_umask)
+ ## Use this method while IPSec CLI handler won't be overwritten to python
+ write_ipsec_secrets(data)
+
+ 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)
+
+ render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data, trim_blocks=True)
+ os.umask(old_umask)
+
+ remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
+ # old_umask = os.umask(0o077)
+ # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data, trim_blocks=True)
+ # os.umask(old_umask)
+ ## Use this method while IPSec CLI handler won't be overwritten to python
+ write_ipsec_conf(data)
+
+ else:
+ if os.path.exists(ipsec_ra_conn_file):
+ remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file)
+ remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
+ remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
+
+def restart_ipsec():
+ call('ipsec restart >&/dev/null')
+ # counter for apply swanctl config
+ counter = 10
+ while counter <= 10:
+ if os.path.exists(charon_pidfile):
+ call('swanctl -q >&/dev/null')
+ break
+ counter -=1
+ sleep(1)
+ if counter == 0:
+ raise ConfigError('VPN configuration error: IPSec is not running.')
+
+def apply(data):
+ # Restart IPSec daemon
+ restart_ipsec()
+
+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/le_cert.py b/src/conf_mode/le_cert.py
new file mode 100755
index 000000000..755c89966
--- /dev/null
+++ b/src/conf_mode/le_cert.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 vyos.defaults
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import cmd
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
+vyos_certbot_dir = vyos.defaults.directories['certbot']
+
+dependencies = [
+ 'https.py',
+]
+
+def request_certbot(cert):
+ email = cert.get('email')
+ if email is not None:
+ email_flag = '-m {0}'.format(email)
+ else:
+ email_flag = ''
+
+ domains = cert.get('domains')
+ if domains is not None:
+ domain_flag = '-d ' + ' -d '.join(domains)
+ else:
+ domain_flag = ''
+
+ certbot_cmd = f'certbot certonly --config-dir {vyos_certbot_dir} -n --nginx --agree-tos --no-eff-email --expand {email_flag} {domain_flag}'
+
+ cmd(certbot_cmd,
+ raising=ConfigError,
+ message="The certbot request failed for the specified domains.")
+
+def get_config():
+ conf = Config()
+ if not conf.exists('service https certificates certbot'):
+ return None
+ else:
+ conf.set_level('service https certificates certbot')
+
+ cert = {}
+
+ if conf.exists('domain-name'):
+ cert['domains'] = conf.return_values('domain-name')
+
+ if conf.exists('email'):
+ cert['email'] = conf.return_value('email')
+
+ return cert
+
+def verify(cert):
+ if cert is None:
+ return None
+
+ if 'domains' not in cert:
+ raise ConfigError("At least one domain name is required to"
+ " request a letsencrypt certificate.")
+
+ if 'email' not in cert:
+ raise ConfigError("An email address is required to request"
+ " a letsencrypt certificate.")
+
+def generate(cert):
+ if cert is None:
+ return None
+
+ # certbot will attempt to reload nginx, even with 'certonly';
+ # start nginx if not active
+ ret = call('systemctl is-active --quiet nginx.service')
+ if ret:
+ call('systemctl start nginx.service')
+
+ request_certbot(cert)
+
+def apply(cert):
+ if cert is not None:
+ call('systemctl restart certbot.timer')
+ else:
+ call('systemctl stop certbot.timer')
+ return None
+
+ for dep in dependencies:
+ cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
+
+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/lldp.py b/src/conf_mode/lldp.py
new file mode 100755
index 000000000..1b539887a
--- /dev/null
+++ b/src/conf_mode/lldp.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 re
+
+from copy import deepcopy
+from sys import exit
+
+from vyos.config import Config
+from vyos.validate import is_addr_assigned,is_loopback_addr
+from vyos.version import get_version_data
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = "/etc/default/lldpd"
+vyos_config_file = "/etc/lldpd.d/01-vyos.conf"
+base = ['service', 'lldp']
+
+default_config_data = {
+ "options": '',
+ "interface_list": '',
+ "location": ''
+}
+
+def get_options(config):
+ options = {}
+ config.set_level(base)
+
+ options['listen_vlan'] = config.exists('listen-vlan')
+ options['mgmt_addr'] = []
+ for addr in config.return_values('management-address'):
+ if is_addr_assigned(addr) and not is_loopback_addr(addr):
+ options['mgmt_addr'].append(addr)
+ else:
+ message = 'WARNING: LLDP management address {0} invalid - '.format(addr)
+ if is_loopback_addr(addr):
+ message += '(loopback address).'
+ else:
+ message += 'address not found.'
+ print(message)
+
+ snmp = config.exists('snmp enable')
+ options["snmp"] = snmp
+ if snmp:
+ config.set_level('')
+ options["sys_snmp"] = config.exists('service snmp')
+ config.set_level(base)
+
+ 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
+ version_data = get_version_data()
+ options['description'] = version_data['version']
+ options['listen_on'] = []
+
+ return options
+
+def get_interface_list(config):
+ 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(base + ['interface', name])
+ disable = config.exists(['disable'])
+ intf = {
+ 'name': name,
+ 'disable': disable
+ }
+ interface_list.append(intf)
+ return interface_list
+
+
+def get_location_intf(config, name):
+ path = base + ['interface', name]
+ config.set_level(path)
+
+ config.set_level(path + ['location'])
+ elin = ''
+ coordinate_based = {}
+
+ if config.exists('elin'):
+ elin = config.return_value('elin')
+
+ if config.exists('coordinate-based'):
+ config.set_level(path + ['location', 'coordinate-based'])
+
+ 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'])
+
+ coordinate_based['datum'] = 'WGS84'
+ if config.exists(['datum']):
+ coordinate_based['datum'] = config.return_value(['datum'])
+
+ intf = {
+ 'name': name,
+ 'elin': elin,
+ 'coordinate_based': coordinate_based
+
+ }
+ return intf
+
+
+def get_location(config):
+ config.set_level(base)
+ intfs_names = config.list_nodes(['interface'])
+ if len(intfs_names) < 0:
+ return 0
+
+ if config.exists('disable'):
+ return 0
+
+ intfs_location = []
+ for name in intfs_names:
+ intf = get_location_intf(config, name)
+ intfs_location.append(intf)
+
+ return intfs_location
+
+
+def get_config():
+ lldp = deepcopy(default_config_data)
+ conf = Config()
+ if not conf.exists(base):
+ return None
+ else:
+ lldp['options'] = get_options(conf)
+ lldp['interface_list'] = get_interface_list(conf)
+ lldp['location'] = get_location(conf)
+
+ return lldp
+
+
+def verify(lldp):
+ # bail out early - looks like removal from running config
+ if lldp is None:
+ return
+
+ # check location
+ for location in lldp['location']:
+ # check coordinate-based
+ if len(location['coordinate_based']) > 0:
+ # check longitude and latitude
+ if not location['coordinate_based']['longitude']:
+ raise ConfigError('Must define longitude for interface {0}'.format(location['name']))
+
+ if not location['coordinate_based']['latitude']:
+ raise ConfigError('Must define latitude for interface {0}'.format(location['name']))
+
+ if not re.match(r'^(\d+)(\.\d+)?[nNsS]$', location['coordinate_based']['latitude']):
+ raise ConfigError('Invalid location for interface {0}:\n' \
+ 'latitude should be a number followed by S or N'.format(location['name']))
+
+ if not re.match(r'^(\d+)(\.\d+)?[eEwW]$', location['coordinate_based']['longitude']):
+ raise ConfigError('Invalid location for interface {0}:\n' \
+ 'longitude should be a number followed by E or W'.format(location['name']))
+
+ # check altitude and datum if exist
+ if location['coordinate_based']['altitude']:
+ if not re.match(r'^[-+0-9\.]+$', location['coordinate_based']['altitude']):
+ raise ConfigError('Invalid location for interface {0}:\n' \
+ 'altitude should be a positive or negative number'.format(location['name']))
+
+ if location['coordinate_based']['datum']:
+ if not re.match(r'^(WGS84|NAD83|MLLW)$', location['coordinate_based']['datum']):
+ raise ConfigError("Invalid location for interface {0}:\n' \
+ 'datum should be WGS84, NAD83, or MLLW".format(location['name']))
+
+ # check elin
+ elif location['elin']:
+ if not re.match(r'^[0-9]{10,25}$', location['elin']):
+ raise ConfigError('Invalid location for interface {0}:\n' \
+ 'ELIN number must be between 10-25 numbers'.format(location['name']))
+
+ # check options
+ if lldp['options']['snmp']:
+ if not lldp['options']['sys_snmp']:
+ raise ConfigError('SNMP must be configured to enable LLDP SNMP')
+
+
+def generate(lldp):
+ # bail out early - looks like removal from running config
+ if lldp is None:
+ return
+
+ # generate listen on interfaces
+ for intf in lldp['interface_list']:
+ tmp = ''
+ # add exclamation mark if interface is disabled
+ if intf['disable']:
+ tmp = '!'
+
+ tmp += intf['name']
+ lldp['options']['listen_on'].append(tmp)
+
+ # generate /etc/default/lldpd
+ render(config_file, 'lldp/lldpd.tmpl', lldp)
+ # generate /etc/lldpd.d/01-vyos.conf
+ render(vyos_config_file, 'lldp/vyos.conf.tmpl', lldp)
+
+
+def apply(lldp):
+ if lldp:
+ # start/restart lldp service
+ call('systemctl restart lldpd.service')
+ else:
+ # LLDP service has been terminated
+ call('systemctl stop lldpd.service')
+ os.unlink(config_file)
+ os.unlink(vyos_config_file)
+
+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/nat.py b/src/conf_mode/nat.py
new file mode 100755
index 000000000..dd34dfd66
--- /dev/null
+++ b/src/conf_mode/nat.py
@@ -0,0 +1,274 @@
+#!/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 jmespath
+import json
+import os
+
+from copy import deepcopy
+from sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call
+from vyos.util import cmd
+from vyos.util import check_kmod
+from vyos.validate import is_addr_assigned
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+k_mod = ['nft_nat', 'nft_chain_nat_ipv4']
+
+default_config_data = {
+ 'deleted': False,
+ 'destination': [],
+ 'helper_functions': None,
+ 'pre_ct_helper': '',
+ 'pre_ct_conntrack': '',
+ 'out_ct_helper': '',
+ 'out_ct_conntrack': '',
+ 'source': []
+}
+
+iptables_nat_config = '/tmp/vyos-nat-rules.nft'
+
+def get_handler(json, chain, target):
+ """ Get nftable rule handler number of given chain/target combination.
+ Handler is required when adding NAT/Conntrack helper targets """
+ for x in json:
+ if x['chain'] != chain:
+ continue
+ if x['target'] != target:
+ continue
+ return x['handle']
+
+ return None
+
+
+def verify_rule(rule, err_msg):
+ """ Common verify steps used for both source and destination NAT """
+ if rule['translation_port'] or rule['dest_port'] or rule['source_port']:
+ if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
+ proto = rule['protocol']
+ raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")')
+
+ if '/' in rule['translation_address']:
+ raise ConfigError(f'{err_msg}\n' \
+ 'Cannot use ports with an IPv4net type translation address as it\n' \
+ 'statically maps a whole network of addresses onto another\n' \
+ 'network of addresses')
+
+
+def parse_configuration(conf, source_dest):
+ """ Common wrapper to read in both NAT source and destination CLI """
+ ruleset = []
+ base_level = ['nat', source_dest]
+ conf.set_level(base_level)
+ for number in conf.list_nodes(['rule']):
+ rule = {
+ 'description': '',
+ 'dest_address': '',
+ 'dest_port': '',
+ 'disabled': False,
+ 'exclude': False,
+ 'interface_in': '',
+ 'interface_out': '',
+ 'log': False,
+ 'protocol': 'all',
+ 'number': number,
+ 'source_address': '',
+ 'source_prefix': '',
+ 'source_port': '',
+ 'translation_address': '',
+ 'translation_prefix': '',
+ 'translation_port': ''
+ }
+ conf.set_level(base_level + ['rule', number])
+
+ if conf.exists(['description']):
+ rule['description'] = conf.return_value(['description'])
+
+ if conf.exists(['destination', 'address']):
+ tmp = conf.return_value(['destination', 'address'])
+ if tmp.startswith('!'):
+ tmp = tmp.replace('!', '!=')
+ rule['dest_address'] = tmp
+
+ if conf.exists(['destination', 'port']):
+ tmp = conf.return_value(['destination', 'port'])
+ if tmp.startswith('!'):
+ tmp = tmp.replace('!', '!=')
+ rule['dest_port'] = tmp
+
+ if conf.exists(['disable']):
+ rule['disabled'] = True
+
+ if conf.exists(['exclude']):
+ rule['exclude'] = True
+
+ if conf.exists(['inbound-interface']):
+ rule['interface_in'] = conf.return_value(['inbound-interface'])
+
+ if conf.exists(['outbound-interface']):
+ rule['interface_out'] = conf.return_value(['outbound-interface'])
+
+ if conf.exists(['log']):
+ rule['log'] = True
+
+ if conf.exists(['protocol']):
+ rule['protocol'] = conf.return_value(['protocol'])
+
+ if conf.exists(['source', 'address']):
+ tmp = conf.return_value(['source', 'address'])
+ if tmp.startswith('!'):
+ tmp = tmp.replace('!', '!=')
+ rule['source_address'] = tmp
+
+ if conf.exists(['source', 'prefix']):
+ rule['source_prefix'] = conf.return_value(['source', 'prefix'])
+
+ if conf.exists(['source', 'port']):
+ tmp = conf.return_value(['source', 'port'])
+ if tmp.startswith('!'):
+ tmp = tmp.replace('!', '!=')
+ rule['source_port'] = tmp
+
+ if conf.exists(['translation', 'address']):
+ rule['translation_address'] = conf.return_value(['translation', 'address'])
+
+ if conf.exists(['translation', 'prefix']):
+ rule['translation_prefix'] = conf.return_value(['translation', 'prefix'])
+
+ if conf.exists(['translation', 'port']):
+ rule['translation_port'] = conf.return_value(['translation', 'port'])
+
+ ruleset.append(rule)
+
+ return ruleset
+
+def get_config():
+ nat = deepcopy(default_config_data)
+ conf = Config()
+
+ # read in current nftable (once) for further processing
+ tmp = cmd('nft -j list table raw')
+ nftable_json = json.loads(tmp)
+
+ # condense the full JSON table into a list with only relevand informations
+ pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}'
+ condensed_json = jmespath.search(pattern, nftable_json)
+
+ if not conf.exists(['nat']):
+ nat['helper_functions'] = 'remove'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')
+
+ nat['deleted'] = True
+
+ return nat
+
+ # check if NAT connection tracking helpers need to be set up - this has to
+ # be done only once
+ if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'):
+ nat['helper_functions'] = 'add'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
+
+ # set config level for parsing in NAT configuration
+ conf.set_level(['nat'])
+
+ # use a common wrapper function to read in the source / destination
+ # tree from the config - thus we do not need to replicate almost the
+ # same code :-)
+ for tgt in ['source', 'destination', 'nptv6']:
+ nat[tgt] = parse_configuration(conf, tgt)
+
+ return nat
+
+def verify(nat):
+ if nat['deleted']:
+ # no need to verify the CLI as NAT is going to be deactivated
+ return None
+
+ if nat['helper_functions']:
+ if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']):
+ raise Exception('could not determine nftable ruleset handlers')
+
+ for rule in nat['source']:
+ interface = rule['interface_out']
+ err_msg = f'Source NAT configuration error in rule "{rule["number"]}":'
+
+ if interface and interface not in 'any' and interface not in interfaces():
+ print(f'Warning: rule "{rule["number"]}" interface "{interface}" does not exist on this system')
+
+ if not rule['interface_out']:
+ raise ConfigError(f'{err_msg} outbound-interface not specified')
+
+ if rule['translation_address']:
+ addr = rule['translation_address']
+ if addr != 'masquerade' and not is_addr_assigned(addr):
+ print(f'Warning: IP address {addr} does not exist on the system!')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ for rule in nat['destination']:
+ interface = rule['interface_in']
+ err_msg = f'Destination NAT configuration error in rule "{rule["number"]}":'
+
+ if interface and interface not in 'any' and interface not in interfaces():
+ print(f'Warning: rule "{rule["number"]}" interface "{interface}" does not exist on this system')
+
+ if not rule['interface_in']:
+ raise ConfigError(f'{err_msg} inbound-interface not specified')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ return None
+
+def generate(nat):
+ render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755)
+ return None
+
+def apply(nat):
+ cmd(f'{iptables_nat_config}')
+ if os.path.isfile(iptables_nat_config):
+ os.unlink(iptables_nat_config)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ check_kmod(k_mod)
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
new file mode 100755
index 000000000..bba8f87a4
--- /dev/null
+++ b/src/conf_mode/ntp.py
@@ -0,0 +1,82 @@
+#!/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 vyos.config import Config
+from vyos.configverify import verify_vrf
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/ntp.conf'
+systemd_override = r'/etc/systemd/system/ntp.service.d/override.conf'
+
+def get_config():
+ conf = Config()
+ base = ['system', 'ntp']
+
+ ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ return ntp
+
+def verify(ntp):
+ # bail out early - looks like removal from running config
+ if not ntp:
+ return None
+
+ if len(ntp.get('allow_clients', {})) and not (len(ntp.get('server', {})) > 0):
+ raise ConfigError('NTP server not configured')
+
+ verify_vrf(ntp)
+ return None
+
+def generate(ntp):
+ # bail out early - looks like removal from running config
+ if not ntp:
+ return None
+
+ render(config_file, 'ntp/ntp.conf.tmpl', ntp, trim_blocks=True)
+ render(systemd_override, 'ntp/override.conf.tmpl', ntp, trim_blocks=True)
+
+ return None
+
+def apply(ntp):
+ if not ntp:
+ # NTP support is removed in the commit
+ call('systemctl stop ntp.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ if os.path.isfile(systemd_override):
+ os.unlink(systemd_override)
+
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+ if ntp:
+ call('systemctl restart ntp.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/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
new file mode 100755
index 000000000..c8e791c78
--- /dev/null
+++ b/src/conf_mode/protocols_bfd.py
@@ -0,0 +1,216 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 vyos.config import Config
+from vyos.validate import is_ipv6_link_local, is_ipv6
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/tmp/bfd.frr'
+
+default_config_data = {
+ 'new_peers': [],
+ 'old_peers' : []
+}
+
+# get configuration for BFD peer from proposed or effective configuration
+def get_bfd_peer_config(peer, conf_mode="proposed"):
+ conf = Config()
+ conf.set_level('protocols bfd peer {0}'.format(peer))
+
+ bfd_peer = {
+ 'remote': peer,
+ 'shutdown': False,
+ 'src_if': '',
+ 'src_addr': '',
+ 'multiplier': '3',
+ 'rx_interval': '300',
+ 'tx_interval': '300',
+ 'multihop': False,
+ 'echo_interval': '',
+ 'echo_mode': False,
+ }
+
+ # Check if individual peer is disabled
+ if conf_mode == "effective" and conf.exists_effective('shutdown'):
+ bfd_peer['shutdown'] = True
+ if conf_mode == "proposed" and conf.exists('shutdown'):
+ bfd_peer['shutdown'] = True
+
+ # Check if peer has a local source interface configured
+ if conf_mode == "effective" and conf.exists_effective('source interface'):
+ bfd_peer['src_if'] = conf.return_effective_value('source interface')
+ if conf_mode == "proposed" and conf.exists('source interface'):
+ bfd_peer['src_if'] = conf.return_value('source interface')
+
+ # Check if peer has a local source address configured - this is mandatory for IPv6
+ if conf_mode == "effective" and conf.exists_effective('source address'):
+ bfd_peer['src_addr'] = conf.return_effective_value('source address')
+ if conf_mode == "proposed" and conf.exists('source address'):
+ bfd_peer['src_addr'] = conf.return_value('source address')
+
+ # Tell BFD daemon that we should expect packets with TTL less than 254
+ # (because it will take more than one hop) and to listen on the multihop
+ # port (4784)
+ if conf_mode == "effective" and conf.exists_effective('multihop'):
+ bfd_peer['multihop'] = True
+ if conf_mode == "proposed" and conf.exists('multihop'):
+ bfd_peer['multihop'] = True
+
+ # Configures the minimum interval that this system is capable of receiving
+ # control packets. The default value is 300 milliseconds.
+ if conf_mode == "effective" and conf.exists_effective('interval receive'):
+ bfd_peer['rx_interval'] = conf.return_effective_value('interval receive')
+ if conf_mode == "proposed" and conf.exists('interval receive'):
+ bfd_peer['rx_interval'] = conf.return_value('interval receive')
+
+ # The minimum transmission interval (less jitter) that this system wants
+ # to use to send BFD control packets.
+ if conf_mode == "effective" and conf.exists_effective('interval transmit'):
+ bfd_peer['tx_interval'] = conf.return_effective_value('interval transmit')
+ if conf_mode == "proposed" and conf.exists('interval transmit'):
+ bfd_peer['tx_interval'] = conf.return_value('interval transmit')
+
+ # Configures the detection multiplier to determine packet loss. The remote
+ # transmission interval will be multiplied by this value to determine the
+ # connection loss detection timer. The default value is 3.
+ if conf_mode == "effective" and conf.exists_effective('interval multiplier'):
+ bfd_peer['multiplier'] = conf.return_effective_value('interval multiplier')
+ if conf_mode == "proposed" and conf.exists('interval multiplier'):
+ bfd_peer['multiplier'] = conf.return_value('interval multiplier')
+
+ # Configures the minimal echo receive transmission interval that this system is capable of handling
+ if conf_mode == "effective" and conf.exists_effective('interval echo-interval'):
+ bfd_peer['echo_interval'] = conf.return_effective_value('interval echo-interval')
+ if conf_mode == "proposed" and conf.exists('interval echo-interval'):
+ bfd_peer['echo_interval'] = conf.return_value('interval echo-interval')
+
+ # Enables or disables the echo transmission mode
+ if conf_mode == "effective" and conf.exists_effective('echo-mode'):
+ bfd_peer['echo_mode'] = True
+ if conf_mode == "proposed" and conf.exists('echo-mode'):
+ bfd_peer['echo_mode'] = True
+
+ return bfd_peer
+
+def get_config():
+ bfd = deepcopy(default_config_data)
+ conf = Config()
+ if not (conf.exists('protocols bfd') or conf.exists_effective('protocols bfd')):
+ return None
+ else:
+ conf.set_level('protocols bfd')
+
+ # as we have to use vtysh to talk to FRR we also need to know
+ # which peers are gone due to a config removal - thus we read in
+ # all peers (active or to delete)
+ for peer in conf.list_effective_nodes('peer'):
+ bfd['old_peers'].append(get_bfd_peer_config(peer, "effective"))
+
+ for peer in conf.list_nodes('peer'):
+ bfd['new_peers'].append(get_bfd_peer_config(peer))
+
+ # find deleted peers
+ set_new_peers = set(conf.list_nodes('peer'))
+ set_old_peers = set(conf.list_effective_nodes('peer'))
+ bfd['deleted_peers'] = set_old_peers - set_new_peers
+
+ return bfd
+
+def verify(bfd):
+ if bfd is None:
+ return None
+
+ # some variables to use later
+ conf = Config()
+
+ for peer in bfd['new_peers']:
+ # IPv6 link local peers require an explicit local address/interface
+ 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 is_ipv6(peer['remote']):
+ if not peer['src_addr']:
+ raise ConfigError('BFD IPv6 peers require explicit local address setting')
+
+ # multihop require source address
+ if peer['multihop'] and not peer['src_addr']:
+ raise ConfigError('Multihop require source address')
+
+ # multihop and echo-mode cannot be used together
+ if peer['multihop'] and peer['echo_mode']:
+ raise ConfigError('Multihop and echo-mode cannot be used together')
+
+ # multihop doesn't accept interface names
+ if peer['multihop'] and peer['src_if']:
+ raise ConfigError('Multihop and source interface cannot be used together')
+
+ # echo interval can be configured only with enabled echo-mode
+ if peer['echo_interval'] != '' and not peer['echo_mode']:
+ raise ConfigError('echo-interval can be configured only with enabled echo-mode')
+
+ # check if we deleted peers are not used in configuration
+ if conf.exists('protocols bgp'):
+ bgp_as = conf.list_nodes('protocols bgp')[0]
+
+ # check BGP neighbors
+ for peer in bfd['deleted_peers']:
+ if conf.exists('protocols bgp {0} neighbor {1} bfd'.format(bgp_as, peer)):
+ raise ConfigError('Cannot delete BFD peer {0}: it is used in BGP configuration'.format(peer))
+ if conf.exists('protocols bgp {0} neighbor {1} peer-group'.format(bgp_as, peer)):
+ peer_group = conf.return_value('protocols bgp {0} neighbor {1} peer-group'.format(bgp_as, peer))
+ if conf.exists('protocols bgp {0} peer-group {1} bfd'.format(bgp_as, peer_group)):
+ raise ConfigError('Cannot delete BFD peer {0}: it belongs to BGP peer-group {1} with enabled BFD'.format(peer, peer_group))
+
+ return None
+
+def generate(bfd):
+ if bfd is None:
+ return None
+
+ render(config_file, 'frr/bfd.frr.tmpl', bfd)
+ return None
+
+def apply(bfd):
+ if bfd is None:
+ return None
+
+ call("vtysh -d bfdd -f " + config_file)
+ if os.path.exists(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_bgp.py b/src/conf_mode/protocols_bgp.py
new file mode 100755
index 000000000..3aa76d866
--- /dev/null
+++ b/src/conf_mode/protocols_bgp.py
@@ -0,0 +1,102 @@
+#!/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 jmespath
+
+from copy import deepcopy
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos import ConfigError, airbag
+airbag.enable()
+
+config_file = r'/tmp/bgp.frr'
+
+default_config_data = {
+ 'as_number': ''
+}
+
+def get_config():
+ bgp = deepcopy(default_config_data)
+ conf = Config()
+
+ # this lives in the "nbgp" tree until we switch over
+ base = ['protocols', 'nbgp']
+ if not conf.exists(base):
+ return None
+
+ bgp = deepcopy(default_config_data)
+ # Get full BGP configuration as dictionary - output the configuration for development
+ #
+ # vyos@vyos# commit
+ # [ protocols nbgp 65000 ]
+ # {'nbgp': {'65000': {'address-family': {'ipv4-unicast': {'aggregate-address': {'1.1.0.0/16': {},
+ # '2.2.2.0/24': {}}},
+ # 'ipv6-unicast': {'aggregate-address': {'2001:db8::/32': {}}}},
+ # 'neighbor': {'192.0.2.1': {'password': 'foo',
+ # 'remote-as': '100'}}}}}
+ #
+ tmp = conf.get_config_dict(base)
+
+ # extract base key from dict as this is our AS number
+ bgp['as_number'] = jmespath.search('nbgp | keys(@) [0]', tmp)
+
+ # adjust level of dictionary returned by get_config_dict()
+ # by using jmesgpath and update dictionary
+ bgp.update(jmespath.search('nbgp.* | [0]', tmp))
+
+ from pprint import pprint
+ pprint(bgp)
+ # resulting in e.g.
+ # vyos@vyos# commit
+ # [ protocols nbgp 65000 ]
+ # {'address-family': {'ipv4-unicast': {'aggregate-address': {'1.1.0.0/16': {},
+ # '2.2.2.0/24': {}}},
+ # 'ipv6-unicast': {'aggregate-address': {'2001:db8::/32': {}}}},
+ # 'as_number': '65000',
+ # 'neighbor': {'192.0.2.1': {'password': 'foo', 'remote-as': '100'}},
+ # 'timers': {'holdtime': '5'}}
+
+ return bgp
+
+def verify(bgp):
+ # bail out early - looks like removal from running config
+ if not bgp:
+ return None
+
+ return None
+
+def generate(bgp):
+ # bail out early - looks like removal from running config
+ if not bgp:
+ return None
+
+ render(config_file, 'frr/bgp.frr.tmpl', bgp)
+ return None
+
+def apply(bgp):
+ 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_igmp.py b/src/conf_mode/protocols_igmp.py
new file mode 100755
index 000000000..ca148fd6a
--- /dev/null
+++ b/src/conf_mode/protocols_igmp.py
@@ -0,0 +1,113 @@
+#!/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 sys import exit
+
+from vyos import ConfigError
+from vyos.config import Config
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+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
+
+ render(config_file, 'frr/igmp.frr.tmpl', igmp)
+ return None
+
+def apply(igmp):
+ if igmp is None:
+ return None
+
+ if os.path.exists(config_file):
+ call(f'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..bcb16fa04
--- /dev/null
+++ b/src/conf_mode/protocols_mpls.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+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,
+ 'hello_holdtime' : None,
+ 'hello_interval' : None
+ },
+ 'ldp' : {
+ 'interfaces' : [],
+ 'neighbors' : {},
+ 'd_transp_ipv4' : None,
+ 'd_transp_ipv6' : None,
+ 'hello_holdtime' : None,
+ 'hello_interval' : 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 hello holdtime
+ if conf.exists_effective('discovery hello-holdtime'):
+ mpls_conf['old_ldp']['hello_holdtime'] = conf.return_effective_value('discovery hello-holdtime')
+
+ if conf.exists('discovery hello-holdtime'):
+ mpls_conf['ldp']['hello_holdtime'] = conf.return_value('discovery hello-holdtime')
+
+ # Get hello interval
+ if conf.exists_effective('discovery hello-interval'):
+ mpls_conf['old_ldp']['hello_interval'] = conf.return_effective_value('discovery hello-interval')
+
+ if conf.exists('discovery hello-interval'):
+ mpls_conf['ldp']['hello_interval'] = conf.return_value('discovery hello-interval')
+
+ # 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
+
+ render(config_file, 'frr/ldpd.frr.tmpl', mpls)
+ 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(f'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..8aa324bac
--- /dev/null
+++ b/src/conf_mode/protocols_pim.py
@@ -0,0 +1,140 @@
+#!/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 sys import exit
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+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
+
+ render(config_file, 'frr/pimd.frr.tmpl', pim)
+ 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/protocols_rip.py b/src/conf_mode/protocols_rip.py
new file mode 100755
index 000000000..4f8816d61
--- /dev/null
+++ b/src/conf_mode/protocols_rip.py
@@ -0,0 +1,317 @@
+#!/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 vyos import ConfigError
+from vyos.config import Config
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/tmp/ripd.frr'
+
+def get_config():
+ conf = Config()
+ base = ['protocols', 'rip']
+ rip_conf = {
+ 'rip_conf' : False,
+ 'default_distance' : [],
+ 'default_originate' : False,
+ 'old_rip' : {
+ 'default_metric' : [],
+ 'distribute' : {},
+ 'neighbors' : {},
+ 'networks' : {},
+ 'net_distance' : {},
+ 'passive_iface' : {},
+ 'redist' : {},
+ 'route' : {},
+ 'ifaces' : {},
+ 'timer_garbage' : 120,
+ 'timer_timeout' : 180,
+ 'timer_update' : 30
+ },
+ 'rip' : {
+ 'default_metric' : None,
+ 'distribute' : {},
+ 'neighbors' : {},
+ 'networks' : {},
+ 'net_distance' : {},
+ 'passive_iface' : {},
+ 'redist' : {},
+ 'route' : {},
+ 'ifaces' : {},
+ 'timer_garbage' : 120,
+ 'timer_timeout' : 180,
+ 'timer_update' : 30
+ }
+ }
+
+ if not (conf.exists(base) or conf.exists_effective(base)):
+ return None
+
+ if conf.exists(base):
+ rip_conf['rip_conf'] = True
+
+ conf.set_level(base)
+
+ # Get default distance
+ if conf.exists_effective('default-distance'):
+ rip_conf['old_default_distance'] = conf.return_effective_value('default-distance')
+
+ if conf.exists('default-distance'):
+ rip_conf['default_distance'] = conf.return_value('default-distance')
+
+ # Get default information originate (originate default route)
+ if conf.exists_effective('default-information originate'):
+ rip_conf['old_default_originate'] = True
+
+ if conf.exists('default-information originate'):
+ rip_conf['default_originate'] = True
+
+ # Get default-metric
+ if conf.exists_effective('default-metric'):
+ rip_conf['old_rip']['default_metric'] = conf.return_effective_value('default-metric')
+
+ if conf.exists('default-metric'):
+ rip_conf['rip']['default_metric'] = conf.return_value('default-metric')
+
+ # Get distribute list interface old_rip
+ for dist_iface in conf.list_effective_nodes('distribute-list interface'):
+ # Set level 'distribute-list interface ethX'
+ conf.set_level((str(base)) + ' distribute-list interface ' + dist_iface)
+ rip_conf['rip']['distribute'].update({
+ dist_iface : {
+ 'iface_access_list_in': conf.return_effective_value('access-list in'.format(dist_iface)),
+ 'iface_access_list_out': conf.return_effective_value('access-list out'.format(dist_iface)),
+ 'iface_prefix_list_in': conf.return_effective_value('prefix-list in'.format(dist_iface)),
+ 'iface_prefix_list_out': conf.return_effective_value('prefix-list out'.format(dist_iface))
+ }
+ })
+
+ # Access-list in old_rip
+ if conf.exists_effective('access-list in'.format(dist_iface)):
+ rip_conf['old_rip']['iface_access_list_in'] = conf.return_effective_value('access-list in'.format(dist_iface))
+ # Access-list out old_rip
+ if conf.exists_effective('access-list out'.format(dist_iface)):
+ rip_conf['old_rip']['iface_access_list_out'] = conf.return_effective_value('access-list out'.format(dist_iface))
+ # Prefix-list in old_rip
+ if conf.exists_effective('prefix-list in'.format(dist_iface)):
+ rip_conf['old_rip']['iface_prefix_list_in'] = conf.return_effective_value('prefix-list in'.format(dist_iface))
+ # Prefix-list out old_rip
+ if conf.exists_effective('prefix-list out'.format(dist_iface)):
+ rip_conf['old_rip']['iface_prefix_list_out'] = conf.return_effective_value('prefix-list out'.format(dist_iface))
+
+ conf.set_level(base)
+
+ # Get distribute list interface
+ for dist_iface in conf.list_nodes('distribute-list interface'):
+ # Set level 'distribute-list interface ethX'
+ conf.set_level((str(base)) + ' distribute-list interface ' + dist_iface)
+ rip_conf['rip']['distribute'].update({
+ dist_iface : {
+ 'iface_access_list_in': conf.return_value('access-list in'.format(dist_iface)),
+ 'iface_access_list_out': conf.return_value('access-list out'.format(dist_iface)),
+ 'iface_prefix_list_in': conf.return_value('prefix-list in'.format(dist_iface)),
+ 'iface_prefix_list_out': conf.return_value('prefix-list out'.format(dist_iface))
+ }
+ })
+
+ # Access-list in
+ if conf.exists('access-list in'.format(dist_iface)):
+ rip_conf['rip']['iface_access_list_in'] = conf.return_value('access-list in'.format(dist_iface))
+ # Access-list out
+ if conf.exists('access-list out'.format(dist_iface)):
+ rip_conf['rip']['iface_access_list_out'] = conf.return_value('access-list out'.format(dist_iface))
+ # Prefix-list in
+ if conf.exists('prefix-list in'.format(dist_iface)):
+ rip_conf['rip']['iface_prefix_list_in'] = conf.return_value('prefix-list in'.format(dist_iface))
+ # Prefix-list out
+ if conf.exists('prefix-list out'.format(dist_iface)):
+ rip_conf['rip']['iface_prefix_list_out'] = conf.return_value('prefix-list out'.format(dist_iface))
+
+ conf.set_level((str(base)) + ' distribute-list')
+
+ # Get distribute list, access-list in
+ if conf.exists_effective('access-list in'):
+ rip_conf['old_rip']['dist_acl_in'] = conf.return_effective_value('access-list in')
+
+ if conf.exists('access-list in'):
+ rip_conf['rip']['dist_acl_in'] = conf.return_value('access-list in')
+
+ # Get distribute list, access-list out
+ if conf.exists_effective('access-list out'):
+ rip_conf['old_rip']['dist_acl_out'] = conf.return_effective_value('access-list out')
+
+ if conf.exists('access-list out'):
+ rip_conf['rip']['dist_acl_out'] = conf.return_value('access-list out')
+
+ # Get ditstribute list, prefix-list in
+ if conf.exists_effective('prefix-list in'):
+ rip_conf['old_rip']['dist_prfx_in'] = conf.return_effective_value('prefix-list in')
+
+ if conf.exists('prefix-list in'):
+ rip_conf['rip']['dist_prfx_in'] = conf.return_value('prefix-list in')
+
+ # Get distribute list, prefix-list out
+ if conf.exists_effective('prefix-list out'):
+ rip_conf['old_rip']['dist_prfx_out'] = conf.return_effective_value('prefix-list out')
+
+ if conf.exists('prefix-list out'):
+ rip_conf['rip']['dist_prfx_out'] = conf.return_value('prefix-list out')
+
+ conf.set_level(base)
+
+ # Get network Interfaces
+ if conf.exists_effective('interface'):
+ rip_conf['old_rip']['ifaces'] = conf.return_effective_values('interface')
+
+ if conf.exists('interface'):
+ rip_conf['rip']['ifaces'] = conf.return_values('interface')
+
+ # Get neighbors
+ if conf.exists_effective('neighbor'):
+ rip_conf['old_rip']['neighbors'] = conf.return_effective_values('neighbor')
+
+ if conf.exists('neighbor'):
+ rip_conf['rip']['neighbors'] = conf.return_values('neighbor')
+
+ # Get networks
+ if conf.exists_effective('network'):
+ rip_conf['old_rip']['networks'] = conf.return_effective_values('network')
+
+ if conf.exists('network'):
+ rip_conf['rip']['networks'] = conf.return_values('network')
+
+ # Get network-distance old_rip
+ for net_dist in conf.list_effective_nodes('network-distance'):
+ rip_conf['old_rip']['net_distance'].update({
+ net_dist : {
+ 'access_list' : conf.return_effective_value('network-distance {0} access-list'.format(net_dist)),
+ 'distance' : conf.return_effective_value('network-distance {0} distance'.format(net_dist)),
+ }
+ })
+
+ # Get network-distance
+ for net_dist in conf.list_nodes('network-distance'):
+ rip_conf['rip']['net_distance'].update({
+ net_dist : {
+ 'access_list' : conf.return_value('network-distance {0} access-list'.format(net_dist)),
+ 'distance' : conf.return_value('network-distance {0} distance'.format(net_dist)),
+ }
+ })
+
+ # Get passive-interface
+ if conf.exists_effective('passive-interface'):
+ rip_conf['old_rip']['passive_iface'] = conf.return_effective_values('passive-interface')
+
+ if conf.exists('passive-interface'):
+ rip_conf['rip']['passive_iface'] = conf.return_values('passive-interface')
+
+ # Get redistribute for old_rip
+ for protocol in conf.list_effective_nodes('redistribute'):
+ rip_conf['old_rip']['redist'].update({
+ protocol : {
+ 'metric' : conf.return_effective_value('redistribute {0} metric'.format(protocol)),
+ 'route_map' : conf.return_effective_value('redistribute {0} route-map'.format(protocol)),
+ }
+ })
+
+ # Get redistribute
+ for protocol in conf.list_nodes('redistribute'):
+ rip_conf['rip']['redist'].update({
+ protocol : {
+ 'metric' : conf.return_value('redistribute {0} metric'.format(protocol)),
+ 'route_map' : conf.return_value('redistribute {0} route-map'.format(protocol)),
+ }
+ })
+
+ conf.set_level(base)
+
+ # Get route
+ if conf.exists_effective('route'):
+ rip_conf['old_rip']['route'] = conf.return_effective_values('route')
+
+ if conf.exists('route'):
+ rip_conf['rip']['route'] = conf.return_values('route')
+
+ # Get timers garbage
+ if conf.exists_effective('timers garbage-collection'):
+ rip_conf['old_rip']['timer_garbage'] = conf.return_effective_value('timers garbage-collection')
+
+ if conf.exists('timers garbage-collection'):
+ rip_conf['rip']['timer_garbage'] = conf.return_value('timers garbage-collection')
+
+ # Get timers timeout
+ if conf.exists_effective('timers timeout'):
+ rip_conf['old_rip']['timer_timeout'] = conf.return_effective_value('timers timeout')
+
+ if conf.exists('timers timeout'):
+ rip_conf['rip']['timer_timeout'] = conf.return_value('timers timeout')
+
+ # Get timers update
+ if conf.exists_effective('timers update'):
+ rip_conf['old_rip']['timer_update'] = conf.return_effective_value('timers update')
+
+ if conf.exists('timers update'):
+ rip_conf['rip']['timer_update'] = conf.return_value('timers update')
+
+ return rip_conf
+
+def verify(rip):
+ if rip is None:
+ return None
+
+ # Check for network. If network-distance acl is set and distance not set
+ for net in rip['rip']['net_distance']:
+ if not rip['rip']['net_distance'][net]['distance']:
+ raise ConfigError(f"Must specify distance for network {net}")
+
+def generate(rip):
+ if rip is None:
+ return None
+
+ render(config_file, 'frr/rip.frr.tmpl', rip)
+ return None
+
+def apply(rip):
+ if rip is None:
+ return None
+
+ if os.path.exists(config_file):
+ call(f'vtysh -d ripd -f {config_file}')
+ os.remove(config_file)
+ else:
+ print("File {0} not found".format(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_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
new file mode 100755
index 000000000..232d1e181
--- /dev/null
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -0,0 +1,117 @@
+#!/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 sys import exit
+
+from vyos import ConfigError
+from vyos.config import Config
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/tmp/static_mcast.frr'
+
+# Get configuration for static multicast route
+def get_config():
+ conf = Config()
+ mroute = {
+ 'old_mroute' : {},
+ 'mroute' : {}
+ }
+
+ base_path = "protocols static multicast"
+
+ if not (conf.exists(base_path) or conf.exists_effective(base_path)):
+ return None
+
+ conf.set_level(base_path)
+
+ # Get multicast effective routes
+ for route in conf.list_effective_nodes('route'):
+ mroute['old_mroute'][route] = {}
+ for next_hop in conf.list_effective_nodes('route {0} next-hop'.format(route)):
+ mroute['old_mroute'][route].update({
+ next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop))
+ })
+
+ # Get multicast effective interface-routes
+ for route in conf.list_effective_nodes('interface-route'):
+ if not route in mroute['old_mroute']:
+ mroute['old_mroute'][route] = {}
+ for next_hop in conf.list_effective_nodes('interface-route {0} next-hop-interface'.format(route)):
+ mroute['old_mroute'][route].update({
+ next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop))
+ })
+
+ # Get multicast routes
+ for route in conf.list_nodes('route'):
+ mroute['mroute'][route] = {}
+ for next_hop in conf.list_nodes('route {0} next-hop'.format(route)):
+ mroute['mroute'][route].update({
+ next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop))
+ })
+
+ # Get multicast interface-routes
+ for route in conf.list_nodes('interface-route'):
+ if not route in mroute['mroute']:
+ mroute['mroute'][route] = {}
+ for next_hop in conf.list_nodes('interface-route {0} next-hop-interface'.format(route)):
+ mroute['mroute'][route].update({
+ next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop))
+ })
+
+ return mroute
+
+def verify(mroute):
+ if mroute is None:
+ return None
+
+ for route in mroute['mroute']:
+ route = route.split('/')
+ if IPv4Address(route[0]) < IPv4Address('224.0.0.0'):
+ raise ConfigError(route + " not a multicast network")
+
+def generate(mroute):
+ if mroute is None:
+ return None
+
+ render(config_file, 'frr/static_mcast.frr.tmpl', mroute)
+ return None
+
+def apply(mroute):
+ if mroute is None:
+ return None
+
+ if os.path.exists(config_file):
+ call(f'vtysh -d staticd -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..3343d1247
--- /dev/null
+++ b/src/conf_mode/salt-minion.py
@@ -0,0 +1,123 @@
+#!/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 socket import gethostname
+from sys import exit
+from urllib3 import PoolManager
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, chown
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/salt/minion'
+master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'
+
+default_config_data = {
+ 'hash': 'sha256',
+ 'log_level': 'warning',
+ 'master' : 'salt',
+ 'user': 'minion',
+ 'group': 'vyattacfg',
+ 'salt_id': gethostname(),
+ 'mine_interval': '60',
+ 'verify_master_pubkey_sign': 'false',
+ 'master_key': ''
+}
+
+def get_config():
+ salt = deepcopy(default_config_data)
+ conf = Config()
+ base = ['service', 'salt-minion']
+
+ if not conf.exists(base):
+ return None
+ else:
+ conf.set_level(base)
+
+ if conf.exists(['hash']):
+ salt['hash'] = conf.return_value(['hash'])
+
+ if conf.exists(['master']):
+ salt['master'] = conf.return_values(['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(['interval']):
+ salt['interval'] = conf.return_value(['interval'])
+
+ if conf.exists(['master-key']):
+ salt['master_key'] = conf.return_value(['master-key'])
+ salt['verify_master_pubkey_sign'] = 'true'
+
+ return salt
+
+def verify(salt):
+ return None
+
+def generate(salt):
+ if not salt:
+ return None
+
+ render(config_file, 'salt-minion/minion.tmpl', salt,
+ user=salt['user'], group=salt['group'])
+
+ if not os.path.exists(master_keyfile):
+ if salt['master_key']:
+ req = PoolManager().request('GET', salt['master_key'], preload_content=False)
+
+ with open(master_keyfile, 'wb') as f:
+ while True:
+ data = req.read(1024)
+ if not data:
+ break
+ f.write(data)
+
+ req.release_conn()
+ chown(master_keyfile, salt['user'], salt['group'])
+
+ return None
+
+def apply(salt):
+ if not salt:
+ # Salt removed from running config
+ call('systemctl stop salt-minion.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ else:
+ call('systemctl restart salt-minion.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/service_console-server.py b/src/conf_mode/service_console-server.py
new file mode 100755
index 000000000..613ec6879
--- /dev/null
+++ b/src/conf_mode/service_console-server.py
@@ -0,0 +1,103 @@
+#!/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 sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import call
+from vyos.xml import defaults
+from vyos import ConfigError
+
+config_file = r'/run/conserver/conserver.cf'
+
+def get_config():
+ conf = Config()
+ base = ['service', 'console-server']
+
+ # Retrieve CLI representation as dictionary
+ proxy = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
+ # The retrieved dictionary will look something like this:
+ #
+ # {'device': {'usb0b2.4p1.0': {'speed': '9600'},
+ # 'usb0b2.4p1.1': {'data_bits': '8',
+ # 'parity': 'none',
+ # 'speed': '115200',
+ # 'stop_bits': '2'}}}
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base + ['device'])
+ if 'device' in proxy:
+ for device in proxy['device']:
+ tmp = dict_merge(default_values, proxy['device'][device])
+ proxy['device'][device] = tmp
+
+ return proxy
+
+def verify(proxy):
+ if not proxy:
+ return None
+
+ if 'device' in proxy:
+ for device in proxy['device']:
+ if 'speed' not in proxy['device'][device]:
+ raise ConfigError(f'Serial port speed must be defined for "{device}"!')
+
+ if 'ssh' in proxy['device'][device]:
+ if 'port' not in proxy['device'][device]['ssh']:
+ raise ConfigError(f'SSH port must be defined for "{device}"!')
+
+ return None
+
+def generate(proxy):
+ if not proxy:
+ return None
+
+ render(config_file, 'conserver/conserver.conf.tmpl', proxy)
+ return None
+
+def apply(proxy):
+ call('systemctl stop dropbear@*.service conserver-server.service')
+
+ if not proxy:
+ if os.path.isfile(config_file):
+ os.unlink(config_file)
+ return None
+
+ call('systemctl restart conserver-server.service')
+
+ if 'device' in proxy:
+ for device in proxy['device']:
+ if 'ssh' in proxy['device'][device]:
+ port = proxy['device'][device]['ssh']['port']
+ call(f'systemctl restart dropbear@{device}.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/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py
new file mode 100755
index 000000000..d46f9578e
--- /dev/null
+++ b/src/conf_mode/service_ids_fastnetmon.py
@@ -0,0 +1,89 @@
+#!/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 sys import exit
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/fastnetmon.conf'
+networks_list = r'/etc/networks_list'
+
+def get_config():
+ conf = Config()
+ base = ['service', 'ids', 'ddos-protection']
+ fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ return fastnetmon
+
+def verify(fastnetmon):
+ if not fastnetmon:
+ return None
+
+ if not "mode" in fastnetmon:
+ raise ConfigError('ddos-protection mode is mandatory!')
+
+ if not "network" in fastnetmon:
+ raise ConfigError('Required define network!')
+
+ if not "listen_interface" in fastnetmon:
+ raise ConfigError('Define listen-interface is mandatory!')
+
+ if "alert_script" in fastnetmon:
+ if os.path.isfile(fastnetmon["alert_script"]):
+ # Check script permissions
+ if not os.access(fastnetmon["alert_script"], os.X_OK):
+ raise ConfigError('Script {0} does not have permissions for execution'.format(fastnetmon["alert_script"]))
+ else:
+ raise ConfigError('File {0} does not exists!'.format(fastnetmon["alert_script"]))
+
+def generate(fastnetmon):
+ if not fastnetmon:
+ if os.path.isfile(config_file):
+ os.unlink(config_file)
+ if os.path.isfile(networks_list):
+ os.unlink(networks_list)
+
+ return
+
+ render(config_file, 'ids/fastnetmon.tmpl', fastnetmon, trim_blocks=True)
+ render(networks_list, 'ids/fastnetmon_networks_list.tmpl', fastnetmon, trim_blocks=True)
+
+ return None
+
+def apply(fastnetmon):
+ if not fastnetmon:
+ # Stop fastnetmon service if removed
+ call('systemctl stop fastnetmon.service')
+ else:
+ call('systemctl restart fastnetmon.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/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
new file mode 100755
index 000000000..553cc2e97
--- /dev/null
+++ b/src/conf_mode/service_ipoe-server.py
@@ -0,0 +1,309 @@
+#!/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
+import re
+
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+ipoe_conf = '/run/accel-pppd/ipoe.conf'
+ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets'
+
+default_config_data = {
+ 'auth_mode': 'local',
+ 'auth_interfaces': [],
+ 'chap_secrets_file': ipoe_chap_secrets, # used in Jinja2 template
+ 'interfaces': [],
+ 'dnsv4': [],
+ 'dnsv6': [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
+ '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': '',
+ 'thread_cnt': get_half_cpus()
+}
+
+def get_config():
+ conf = Config()
+ base_path = ['service', 'ipoe-server']
+ if not conf.exists(base_path):
+ return None
+
+ conf.set_level(base_path)
+ ipoe = deepcopy(default_config_data)
+
+ for interface in conf.list_nodes(['interface']):
+ tmp = {
+ 'mode': 'L2',
+ 'name': interface,
+ 'shared': '1',
+ # may need a config option, can be dhcpv4 or up for unclassified pkts
+ 'sess_start': 'dhcpv4',
+ 'range': None,
+ 'ifcfg': '1',
+ 'vlan_mon': []
+ }
+
+ conf.set_level(base_path + ['interface', interface])
+
+ if conf.exists(['network-mode']):
+ tmp['mode'] = conf.return_value(['network-mode'])
+
+ if conf.exists(['network']):
+ mode = conf.return_value(['network'])
+ if mode == 'vlan':
+ tmp['shared'] = '0'
+
+ if conf.exists(['vlan-id']):
+ tmp['vlan_mon'] += conf.return_values(['vlan-id'])
+
+ if conf.exists(['vlan-range']):
+ tmp['vlan_mon'] += conf.return_values(['vlan-range'])
+
+ if conf.exists(['client-subnet']):
+ tmp['range'] = conf.return_value(['client-subnet'])
+
+ ipoe['interfaces'].append(tmp)
+
+ conf.set_level(base_path)
+
+ if conf.exists(['name-server']):
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ ipoe['dnsv4'].append(name_server)
+ else:
+ ipoe['dnsv6'].append(name_server)
+
+ if conf.exists(['authentication', 'mode']):
+ ipoe['auth_mode'] = conf.return_value(['authentication', 'mode'])
+
+ if conf.exists(['authentication', 'interface']):
+ for interface in conf.list_nodes(['authentication', 'interface']):
+ tmp = {
+ 'name': interface,
+ 'mac': []
+ }
+ for mac in conf.list_nodes(['authentication', 'interface', interface, 'mac-address']):
+ client = {
+ 'address': mac,
+ 'rate_download': '',
+ 'rate_upload': '',
+ 'vlan_id': ''
+ }
+ conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', mac])
+
+ if conf.exists(['rate-limit', 'download']):
+ client['rate_download'] = conf.return_value(['rate-limit', 'download'])
+
+ if conf.exists(['rate-limit', 'upload']):
+ client['rate_upload'] = conf.return_value(['rate-limit', 'upload'])
+
+ if conf.exists(['vlan-id']):
+ client['vlan'] = conf.return_value(['vlan-id'])
+
+ tmp['mac'].append(client)
+
+ ipoe['auth_interfaces'].append(tmp)
+
+ conf.set_level(base_path)
+
+ #
+ # authentication mode radius servers and settings
+ if conf.exists(['authentication', 'mode', 'radius']):
+ for server in conf.list_nodes(['authentication', 'radius', 'server']):
+ radius = {
+ 'server' : server,
+ 'key' : '',
+ 'fail_time' : 0,
+ 'port' : '1812',
+ 'acct_port' : '1813'
+ }
+
+ conf.set_level(base_path + ['authentication', 'radius', 'server', server])
+
+ if conf.exists(['fail-time']):
+ radius['fail_time'] = conf.return_value(['fail-time'])
+
+ if conf.exists(['port']):
+ radius['port'] = conf.return_value(['port'])
+
+ if conf.exists(['acct-port']):
+ radius['acct_port'] = conf.return_value(['acct-port'])
+
+ if conf.exists(['key']):
+ radius['key'] = conf.return_value(['key'])
+
+ if not conf.exists(['disable']):
+ ipoe['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+ if conf.exists(['acct-timeout']):
+ ipoe['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ ipoe['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ ipoe['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ ipoe['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ ipoe['radius_source_address'] = conf.return_value(['source-address'])
+
+ # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA)
+ if conf.exists(['dynamic-author']):
+ dae = {
+ 'port' : '',
+ 'server' : '',
+ 'key' : ''
+ }
+
+ if conf.exists(['dynamic-author', 'server']):
+ dae['server'] = conf.return_value(['dynamic-author', 'server'])
+
+ if conf.exists(['dynamic-author', 'port']):
+ dae['port'] = conf.return_value(['dynamic-author', 'port'])
+
+ if conf.exists(['dynamic-author', 'key']):
+ dae['key'] = conf.return_value(['dynamic-author', 'key'])
+
+ ipoe['radius_dynamic_author'] = dae
+
+
+ conf.set_level(base_path)
+ if conf.exists(['client-ipv6-pool', 'prefix']):
+ for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': '64'
+ }
+
+ if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']):
+ tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask'])
+
+ ipoe['client_ipv6_pool'].append(tmp)
+
+
+ if conf.exists(['client-ipv6-pool', 'delegate']):
+ for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix'])
+
+ ipoe['client_ipv6_delegate_prefix'].append(tmp)
+
+ return ipoe
+
+
+def verify(ipoe):
+ if not ipoe:
+ return None
+
+ if not ipoe['interfaces']:
+ raise ConfigError('No IPoE interface configured')
+
+ for interface in ipoe['interfaces']:
+ if not interface['range']:
+ raise ConfigError(f'No IPoE client subnet defined on interface "{ interface }"')
+
+ if len(ipoe['dnsv4']) > 2:
+ raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+
+ if len(ipoe['dnsv6']) > 3:
+ raise ConfigError('Not more then three IPv6 DNS name-servers can be configured')
+
+ if ipoe['auth_mode'] == 'radius':
+ if len(ipoe['radius_server']) == 0:
+ raise ConfigError('RADIUS authentication requires at least one server')
+
+ for radius in ipoe['radius_server']:
+ if not radius['key']:
+ server = radius['server']
+ raise ConfigError(f'Missing RADIUS secret key for server "{ server }"')
+
+ if ipoe['client_ipv6_delegate_prefix'] and not ipoe['client_ipv6_pool']:
+ raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!')
+
+ return None
+
+
+def generate(ipoe):
+ if not ipoe:
+ return None
+
+ render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True)
+
+ if ipoe['auth_mode'] == 'local':
+ render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.tmpl', ipoe)
+ os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+
+ else:
+ if os.path.exists(ipoe_chap_secrets):
+ os.unlink(ipoe_chap_secrets)
+
+ return None
+
+
+def apply(ipoe):
+ if ipoe == None:
+ call('systemctl stop accel-ppp@ipoe.service')
+ for file in [ipoe_conf, ipoe_chap_secrets]:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ return None
+
+ call('systemctl restart accel-ppp@ipoe.service')
+
+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/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py
new file mode 100755
index 000000000..1a6b2c328
--- /dev/null
+++ b/src/conf_mode/service_mdns-repeater.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 netifaces import ifaddresses, interfaces, AF_INET
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/default/mdns-repeater'
+
+def get_config():
+ conf = Config()
+ base = ['service', 'mdns', 'repeater']
+ mdns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ return mdns
+
+def verify(mdns):
+ if not mdns:
+ return None
+
+ if 'disable' in mdns:
+ return None
+
+ # We need at least two interfaces to repeat mDNS advertisments
+ if 'interface' not in mdns or len(mdns['interface']) < 2:
+ raise ConfigError('mDNS repeater requires at least 2 configured interfaces!')
+
+ # For mdns-repeater to work it is essential that the interfaces has
+ # an IPv4 address assigned
+ for interface in mdns['interface']:
+ if interface not in interfaces():
+ raise ConfigError(f'Interface "{interface}" does not exist!')
+
+ if AF_INET not in ifaddresses(interface):
+ raise ConfigError('mDNS repeater requires an IPv4 address to be '
+ f'configured on interface "{interface}"')
+
+ return None
+
+def generate(mdns):
+ if not mdns:
+ return None
+
+ if 'disable' in mdns:
+ print('Warning: mDNS repeater will be deactivated because it is disabled')
+ return None
+
+ render(config_file, 'mdns-repeater/mdns-repeater.tmpl', mdns)
+ return None
+
+def apply(mdns):
+ if not mdns or 'disable' in mdns:
+ call('systemctl stop mdns-repeater.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ else:
+ call('systemctl restart mdns-repeater.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/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
new file mode 100755
index 000000000..39d34a7e2
--- /dev/null
+++ b/src/conf_mode/service_pppoe-server.py
@@ -0,0 +1,473 @@
+#!/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
+import re
+
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+pppoe_conf = r'/run/accel-pppd/pppoe.conf'
+pppoe_chap_secrets = r'/run/accel-pppd/pppoe.chap-secrets'
+
+default_config_data = {
+ 'auth_mode': 'local',
+ 'auth_proto': ['auth_mschap_v2', 'auth_mschap_v1', 'auth_chap_md5', 'auth_pap'],
+ 'chap_secrets_file': pppoe_chap_secrets, # used in Jinja2 template
+ 'client_ip_pool': '',
+ 'client_ip_subnets': [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
+ 'concentrator': 'vyos-ac',
+ 'interfaces': [],
+ 'local_users' : [],
+
+ 'svc_name': [],
+ 'dnsv4': [],
+ 'dnsv6': [],
+ 'wins': [],
+ 'mtu': '1492',
+
+ 'limits_burst': '',
+ 'limits_connections': '',
+ 'limits_timeout': '',
+
+ 'pado_delay': '',
+ 'ppp_ccp': False,
+ 'ppp_gw': '',
+ 'ppp_ipv4': '',
+ 'ppp_ipv6': '',
+ 'ppp_ipv6_accept_peer_intf_id': False,
+ 'ppp_ipv6_intf_id': '',
+ 'ppp_ipv6_peer_intf_id': '',
+ 'ppp_echo_failure': '3',
+ 'ppp_echo_interval': '30',
+ 'ppp_echo_timeout': '0',
+ 'ppp_min_mtu': '',
+ 'ppp_mppe': 'prefer',
+ 'ppp_mru': '',
+
+ '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': '',
+ 'sesscrtl': 'replace',
+ 'snmp': False,
+ 'thread_cnt': get_half_cpus()
+}
+
+def get_config():
+ conf = Config()
+ base_path = ['service', 'pppoe-server']
+ if not conf.exists(base_path):
+ return None
+
+ conf.set_level(base_path)
+ pppoe = deepcopy(default_config_data)
+
+ # general options
+ if conf.exists(['access-concentrator']):
+ pppoe['concentrator'] = conf.return_value(['access-concentrator'])
+
+ if conf.exists(['service-name']):
+ pppoe['svc_name'] = conf.return_values(['service-name'])
+
+ if conf.exists(['interface']):
+ for interface in conf.list_nodes(['interface']):
+ conf.set_level(base_path + ['interface', interface])
+ tmp = {
+ 'name': interface,
+ 'vlans': []
+ }
+
+ if conf.exists(['vlan-id']):
+ tmp['vlans'] += conf.return_values(['vlan-id'])
+
+ if conf.exists(['vlan-range']):
+ tmp['vlans'] += conf.return_values(['vlan-range'])
+
+ pppoe['interfaces'].append(tmp)
+
+ conf.set_level(base_path)
+
+ if conf.exists(['local-ip']):
+ pppoe['ppp_gw'] = conf.return_value(['local-ip'])
+
+ if conf.exists(['name-server']):
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ pppoe['dnsv4'].append(name_server)
+ else:
+ pppoe['dnsv6'].append(name_server)
+
+ if conf.exists(['wins-server']):
+ pppoe['wins'] = conf.return_values(['wins-server'])
+
+
+ if conf.exists(['client-ip-pool']):
+ if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']):
+ start = conf.return_value(['client-ip-pool', 'start'])
+ stop = conf.return_value(['client-ip-pool', 'stop'])
+ pppoe['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0)
+
+ if conf.exists(['client-ip-pool', 'subnet']):
+ pppoe['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet'])
+
+
+ if conf.exists(['client-ipv6-pool', 'prefix']):
+ for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': '64'
+ }
+
+ if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']):
+ tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask'])
+
+ pppoe['client_ipv6_pool'].append(tmp)
+
+
+ if conf.exists(['client-ipv6-pool', 'delegate']):
+ for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix'])
+
+ pppoe['client_ipv6_delegate_prefix'].append(tmp)
+
+
+ if conf.exists(['limits']):
+ if conf.exists(['limits', 'burst']):
+ pppoe['limits_burst'] = conf.return_value(['limits', 'burst'])
+
+ if conf.exists(['limits', 'connection-limit']):
+ pppoe['limits_connections'] = conf.return_value(['limits', 'connection-limit'])
+
+ if conf.exists(['limits', 'timeout']):
+ pppoe['limits_timeout'] = conf.return_value(['limits', 'timeout'])
+
+
+ if conf.exists(['snmp']):
+ pppoe['snmp'] = True
+
+ if conf.exists(['snmp', 'master-agent']):
+ pppoe['snmp'] = 'enable-ma'
+
+ # authentication mode local
+ if conf.exists(['authentication', 'mode']):
+ pppoe['auth_mode'] = conf.return_value(['authentication', 'mode'])
+
+ 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'])
+
+ pppoe['local_users'].append(user)
+
+ conf.set_level(base_path)
+
+ if conf.exists(['authentication', 'protocols']):
+ auth_mods = {
+ 'mschap-v2': 'auth_mschap_v2',
+ 'mschap': 'auth_mschap_v1',
+ 'chap': 'auth_chap_md5',
+ 'pap': 'auth_pap'
+ }
+
+ pppoe['auth_proto'] = []
+ for proto in conf.return_values(['authentication', 'protocols']):
+ pppoe['auth_proto'].append(auth_mods[proto])
+
+ #
+ # authentication mode radius servers and settings
+ if conf.exists(['authentication', 'mode', 'radius']):
+
+ for server in conf.list_nodes(['authentication', 'radius', 'server']):
+ radius = {
+ 'server' : server,
+ 'key' : '',
+ 'fail_time' : 0,
+ 'port' : '1812',
+ 'acct_port' : '1813'
+ }
+
+ conf.set_level(base_path + ['authentication', 'radius', 'server', server])
+
+ if conf.exists(['fail-time']):
+ radius['fail_time'] = conf.return_value(['fail-time'])
+
+ if conf.exists(['port']):
+ radius['port'] = conf.return_value(['port'])
+
+ if conf.exists(['acct-port']):
+ radius['acct_port'] = conf.return_value(['acct-port'])
+
+ if conf.exists(['key']):
+ radius['key'] = conf.return_value(['key'])
+
+ if not conf.exists(['disable']):
+ pppoe['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+
+ if conf.exists(['acct-timeout']):
+ pppoe['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ pppoe['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ pppoe['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ pppoe['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ pppoe['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ pppoe['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'])
+
+ pppoe['radius_dynamic_author'] = dae
+
+ # RADIUS based rate-limiter
+ if conf.exists(['rate-limit', 'enable']):
+ pppoe['radius_shaper_attr'] = 'Filter-Id'
+ c_attr = ['rate-limit', 'enable', 'attribute']
+ if conf.exists(c_attr):
+ pppoe['radius_shaper_attr'] = conf.return_value(c_attr)
+
+ c_vendor = ['rate-limit', 'enable', 'vendor']
+ if conf.exists(c_vendor):
+ pppoe['radius_shaper_vendor'] = conf.return_value(c_vendor)
+
+ # re-set config level
+ conf.set_level(base_path)
+
+ if conf.exists(['mtu']):
+ pppoe['mtu'] = conf.return_value(['mtu'])
+
+ if conf.exists(['session-control']):
+ pppoe['sesscrtl'] = conf.return_value(['session-control'])
+
+ # ppp_options
+ if conf.exists(['ppp-options']):
+ conf.set_level(base_path + ['ppp-options'])
+
+ if conf.exists(['ccp']):
+ pppoe['ppp_ccp'] = True
+
+ if conf.exists(['ipv4']):
+ pppoe['ppp_ipv4'] = conf.return_value(['ipv4'])
+
+ if conf.exists(['ipv6']):
+ pppoe['ppp_ipv6'] = conf.return_value(['ipv6'])
+
+ if conf.exists(['ipv6-accept-peer-intf-id']):
+ pppoe['ppp_ipv6_peer_intf_id'] = True
+
+ if conf.exists(['ipv6-intf-id']):
+ pppoe['ppp_ipv6_intf_id'] = conf.return_value(['ipv6-intf-id'])
+
+ if conf.exists(['ipv6-peer-intf-id']):
+ pppoe['ppp_ipv6_peer_intf_id'] = conf.return_value(['ipv6-peer-intf-id'])
+
+ if conf.exists(['lcp-echo-failure']):
+ pppoe['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure'])
+
+ if conf.exists(['lcp-echo-failure']):
+ pppoe['ppp_echo_interval'] = conf.return_value(['lcp-echo-failure'])
+
+ if conf.exists(['lcp-echo-timeout']):
+ pppoe['ppp_echo_timeout'] = conf.return_value(['lcp-echo-timeout'])
+
+ if conf.exists(['min-mtu']):
+ pppoe['ppp_min_mtu'] = conf.return_value(['min-mtu'])
+
+ if conf.exists(['mppe']):
+ pppoe['ppp_mppe'] = conf.return_value(['mppe'])
+
+ if conf.exists(['mru']):
+ pppoe['ppp_mru'] = conf.return_value(['mru'])
+
+ if conf.exists(['pado-delay']):
+ pppoe['pado_delay'] = '0'
+ a = {}
+ for id in conf.list_nodes(['pado-delay']):
+ if not conf.return_value(['pado-delay', id, 'sessions']):
+ a[id] = 0
+ else:
+ a[id] = conf.return_value(['pado-delay', id, 'sessions'])
+
+ for k in sorted(a.keys()):
+ if k != sorted(a.keys())[-1]:
+ pppoe['pado_delay'] += ",{0}:{1}".format(k, a[k])
+ else:
+ pppoe['pado_delay'] += ",{0}:{1}".format('-1', a[k])
+
+ return pppoe
+
+
+def verify(pppoe):
+ if not pppoe:
+ return None
+
+ # vertify auth settings
+ if pppoe['auth_mode'] == 'local':
+ if not pppoe['local_users']:
+ raise ConfigError('PPPoE local auth mode requires local users to be configured!')
+
+ for user in pppoe['local_users']:
+ username = user['name']
+ if not user['password']:
+ raise ConfigError(f'Password required for local user "{username}"')
+
+ # 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 local user "{username}"')
+
+ if user['download'] and not user['upload']:
+ raise ConfigError(f'Upload speed value required for local user "{username}"')
+
+ elif pppoe['auth_mode'] == 'radius':
+ if len(pppoe['radius_server']) == 0:
+ raise ConfigError('RADIUS authentication requires at least one server')
+
+ for radius in pppoe['radius_server']:
+ if not radius['key']:
+ server = radius['server']
+ raise ConfigError(f'Missing RADIUS secret key for server "{ server }"')
+
+ if len(pppoe['wins']) > 2:
+ raise ConfigError('Not more then two IPv4 WINS name-servers can be configured')
+
+ if len(pppoe['dnsv4']) > 2:
+ raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+
+ if len(pppoe['dnsv6']) > 3:
+ raise ConfigError('Not more then three IPv6 DNS name-servers can be configured')
+
+ if not pppoe['interfaces']:
+ raise ConfigError('At least one listen interface must be defined!')
+
+ # local ippool and gateway settings config checks
+ if pppoe['client_ip_subnets'] or pppoe['client_ip_pool']:
+ if not pppoe['ppp_gw']:
+ raise ConfigError('PPPoE server requires local IP to be configured')
+
+ if pppoe['ppp_gw'] and not pppoe['client_ip_subnets'] and not pppoe['client_ip_pool']:
+ print("Warning: No PPPoE client pool defined")
+
+ return None
+
+
+def generate(pppoe):
+ if not pppoe:
+ return None
+
+ render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True)
+
+ if pppoe['local_users']:
+ render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pppoe, trim_blocks=True)
+ os.chmod(pppoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+ else:
+ if os.path.exists(pppoe_chap_secrets):
+ os.unlink(pppoe_chap_secrets)
+
+ return None
+
+
+def apply(pppoe):
+ if not pppoe:
+ call('systemctl stop accel-ppp@pppoe.service')
+ for file in [pppoe_conf, pppoe_chap_secrets]:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ return None
+
+ call('systemctl restart accel-ppp@pppoe.service')
+
+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/service_router-advert.py b/src/conf_mode/service_router-advert.py
new file mode 100755
index 000000000..4e1c432ab
--- /dev/null
+++ b/src/conf_mode/service_router-advert.py
@@ -0,0 +1,117 @@
+#!/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 sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import call
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/radvd/radvd.conf'
+
+def get_config():
+ conf = Config()
+ base = ['service', 'router-advert']
+ rtradv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_interface_values = defaults(base + ['interface'])
+ # we deal with prefix defaults later on
+ if 'prefix' in default_interface_values:
+ del default_interface_values['prefix']
+
+ default_prefix_values = defaults(base + ['interface', 'prefix'])
+
+ if 'interface' in rtradv:
+ for interface in rtradv['interface']:
+ rtradv['interface'][interface] = dict_merge(
+ default_interface_values, rtradv['interface'][interface])
+
+ if 'prefix' in rtradv['interface'][interface]:
+ for prefix in rtradv['interface'][interface]['prefix']:
+ rtradv['interface'][interface]['prefix'][prefix] = dict_merge(
+ default_prefix_values, rtradv['interface'][interface]['prefix'][prefix])
+
+ if 'name_server' in rtradv['interface'][interface]:
+ # always use a list when dealing with nameservers - eases the template generation
+ if isinstance(rtradv['interface'][interface]['name_server'], str):
+ rtradv['interface'][interface]['name_server'] = [
+ rtradv['interface'][interface]['name_server']]
+
+ return rtradv
+
+def verify(rtradv):
+ if not rtradv:
+ return None
+
+ if 'interface' not in rtradv:
+ return None
+
+ for interface in rtradv['interface']:
+ interface = rtradv['interface'][interface]
+ if 'prefix' in interface:
+ for prefix in interface['prefix']:
+ prefix = interface['prefix'][prefix]
+ valid_lifetime = prefix['valid_lifetime']
+ if valid_lifetime == 'infinity':
+ valid_lifetime = 4294967295
+
+ preferred_lifetime = prefix['preferred_lifetime']
+ if preferred_lifetime == 'infinity':
+ preferred_lifetime = 4294967295
+
+ if not (int(valid_lifetime) > int(preferred_lifetime)):
+ raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime')
+
+ return None
+
+def generate(rtradv):
+ if not rtradv:
+ return None
+
+ render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, trim_blocks=True, permission=0o644)
+ return None
+
+def apply(rtradv):
+ if not rtradv:
+ # 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
new file mode 100755
index 000000000..e9806ef47
--- /dev/null
+++ b/src/conf_mode/snmp.py
@@ -0,0 +1,581 @@
+#!/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 sys import exit
+
+from vyos.config import Config
+from vyos.configverify import verify_vrf
+from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random
+from vyos.template import render
+from vyos.util import call
+from vyos.validate import is_ipv4, is_addr_assigned
+from vyos.version import get_version_data
+from vyos import ConfigError, airbag
+airbag.enable()
+
+config_file_client = r'/etc/snmp/snmp.conf'
+config_file_daemon = r'/etc/snmp/snmpd.conf'
+config_file_access = r'/usr/share/snmp/snmpd.conf'
+config_file_user = r'/var/lib/snmp/snmpd.conf'
+default_script_dir = r'/config/user-data/'
+systemd_override = r'/etc/systemd/system/snmpd.service.d/override.conf'
+
+# SNMP OIDs used to mark auth/priv type
+OIDs = {
+ 'md5' : '.1.3.6.1.6.3.10.1.1.2',
+ 'sha' : '.1.3.6.1.6.3.10.1.1.3',
+ 'aes' : '.1.3.6.1.6.3.10.1.2.4',
+ 'des' : '.1.3.6.1.6.3.10.1.2.2',
+ 'none': '.1.3.6.1.6.3.10.1.2.1'
+}
+
+default_config_data = {
+ 'listen_on': [],
+ 'listen_address': [],
+ 'ipv6_enabled': 'True',
+ 'communities': [],
+ 'smux_peers': [],
+ 'location' : '',
+ 'description' : '',
+ 'contact' : '',
+ 'trap_source': '',
+ 'trap_targets': [],
+ 'vyos_user': '',
+ 'vyos_user_pass': '',
+ 'version': '',
+ 'v3_enabled': 'False',
+ 'v3_engineid': '',
+ 'v3_groups': [],
+ 'v3_traps': [],
+ 'v3_users': [],
+ 'v3_views': [],
+ 'script_ext': []
+}
+
+def rmfile(file):
+ if os.path.isfile(file):
+ os.unlink(file)
+
+def get_config():
+ snmp = default_config_data
+ conf = 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 = get_version_data()
+ snmp['version'] = version_data['version']
+
+ # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx'
+ snmp['vyos_user'] = 'vyos' + random(8)
+ snmp['vyos_user_pass'] = random(16)
+
+ if conf.exists('community'):
+ for name in conf.list_nodes('community'):
+ community = {
+ 'name': name,
+ 'authorization': 'ro',
+ 'network_v4': [],
+ 'network_v6': [],
+ 'has_source' : False
+ }
+
+ if conf.exists('community {0} authorization'.format(name)):
+ community['authorization'] = conf.return_value('community {0} authorization'.format(name))
+
+ # 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 is_ipv4(addr):
+ community['network_v4'].append(addr)
+ else:
+ community['network_v6'].append(addr)
+
+ # 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 is_ipv4(addr):
+ community['network_v4'].append(addr)
+ else:
+ community['network_v6'].append(addr)
+
+ if (len(community['network_v4']) > 0) or (len(community['network_v6']) > 0):
+ community['has_source'] = True
+
+ snmp['communities'].append(community)
+
+ if conf.exists('contact'):
+ snmp['contact'] = conf.return_value('contact')
+
+ if conf.exists('description'):
+ snmp['description'] = conf.return_value('description')
+
+ if conf.exists('listen-address'):
+ for addr in conf.list_nodes('listen-address'):
+ port = '161'
+ if conf.exists('listen-address {0} port'.format(addr)):
+ port = conf.return_value('listen-address {0} port'.format(addr))
+
+ snmp['listen_address'].append((addr, port))
+
+ # Always listen on localhost if an explicit address has been configured
+ # This is a safety measure to not end up with invalid listen addresses
+ # that are not configured on this system. See https://phabricator.vyos.net/T850
+ if not '127.0.0.1' in conf.list_nodes('listen-address'):
+ snmp['listen_address'].append(('127.0.0.1', '161'))
+
+ if not '::1' in conf.list_nodes('listen-address'):
+ snmp['listen_address'].append(('::1', '161'))
+
+ if conf.exists('location'):
+ snmp['location'] = conf.return_value('location')
+
+ if conf.exists('smux-peer'):
+ snmp['smux_peers'] = conf.return_values('smux-peer')
+
+ if conf.exists('trap-source'):
+ snmp['trap_source'] = conf.return_value('trap-source')
+
+ if conf.exists('trap-target'):
+ for target in conf.list_nodes('trap-target'):
+ trap_tgt = {
+ 'target': target,
+ 'community': '',
+ 'port': ''
+ }
+
+ if conf.exists('trap-target {0} community'.format(target)):
+ trap_tgt['community'] = conf.return_value('trap-target {0} community'.format(target))
+
+ if conf.exists('trap-target {0} port'.format(target)):
+ trap_tgt['port'] = conf.return_value('trap-target {0} port'.format(target))
+
+ snmp['trap_targets'].append(trap_tgt)
+
+ if conf.exists('script-extensions'):
+ for extname in conf.list_nodes('script-extensions extension-name'):
+ conf_script = conf.return_value('script-extensions extension-name {} script'.format(extname))
+ # if script has not absolute path, use pre configured path
+ if "/" not in conf_script:
+ conf_script = default_script_dir + conf_script
+
+ extension = {
+ 'name': extname,
+ 'script' : conf_script
+ }
+
+ snmp['script_ext'].append(extension)
+
+ if conf.exists('vrf'):
+ # Append key to dict but don't place it in the default dictionary.
+ # This is required to make the override.conf.tmpl work until we
+ # migrate to get_config_dict().
+ snmp['vrf'] = conf.return_value('vrf')
+
+
+ #########################################################################
+ # ____ _ _ __ __ ____ _____ #
+ # / ___|| \ | | \/ | _ \ __ _|___ / #
+ # \___ \| \| | |\/| | |_) | \ \ / / |_ \ #
+ # ___) | |\ | | | | __/ \ V / ___) | #
+ # |____/|_| \_|_| |_|_| \_/ |____/ #
+ # #
+ # now take care about the fancy SNMP v3 stuff, or bail out eraly #
+ #########################################################################
+ if not conf.exists('v3'):
+ return snmp
+ else:
+ snmp['v3_enabled'] = True
+
+ # 'set service snmp v3 engineid'
+ if conf.exists('v3 engineid'):
+ snmp['v3_engineid'] = conf.return_value('v3 engineid')
+
+ # 'set service snmp v3 group'
+ if conf.exists('v3 group'):
+ for group in conf.list_nodes('v3 group'):
+ v3_group = {
+ 'name': group,
+ 'mode': 'ro',
+ 'seclevel': 'auth',
+ 'view': ''
+ }
+
+ if conf.exists('v3 group {0} mode'.format(group)):
+ v3_group['mode'] = conf.return_value('v3 group {0} mode'.format(group))
+
+ if conf.exists('v3 group {0} seclevel'.format(group)):
+ v3_group['seclevel'] = conf.return_value('v3 group {0} seclevel'.format(group))
+
+ if conf.exists('v3 group {0} view'.format(group)):
+ v3_group['view'] = conf.return_value('v3 group {0} view'.format(group))
+
+ snmp['v3_groups'].append(v3_group)
+
+ # 'set service snmp v3 trap-target'
+ if conf.exists('v3 trap-target'):
+ for trap in conf.list_nodes('v3 trap-target'):
+ trap_cfg = {
+ 'ipAddr': trap,
+ 'secName': '',
+ 'authProtocol': 'md5',
+ 'authPassword': '',
+ 'authMasterKey': '',
+ 'privProtocol': 'des',
+ 'privPassword': '',
+ 'privMasterKey': '',
+ 'ipProto': 'udp',
+ 'ipPort': '162',
+ 'type': '',
+ 'secLevel': 'noAuthNoPriv'
+ }
+
+ if conf.exists('v3 trap-target {0} user'.format(trap)):
+ # Set the securityName used for authenticated SNMPv3 messages.
+ trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap))
+
+ if conf.exists('v3 trap-target {0} auth type'.format(trap)):
+ # Set the authentication protocol (MD5 or SHA) used for authenticated SNMPv3 messages
+ # cmdline option '-a'
+ trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap))
+
+ if conf.exists('v3 trap-target {0} auth plaintext-password'.format(trap)):
+ # Set the authentication pass phrase used for authenticated SNMPv3 messages.
+ # cmdline option '-A'
+ trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-password'.format(trap))
+
+ if conf.exists('v3 trap-target {0} auth encrypted-password'.format(trap)):
+ # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys.
+ # cmdline option '-3m'
+ trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-password'.format(trap))
+
+ if conf.exists('v3 trap-target {0} privacy type'.format(trap)):
+ # Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages.
+ # cmdline option '-x'
+ trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap))
+
+ if conf.exists('v3 trap-target {0} privacy plaintext-password'.format(trap)):
+ # Set the privacy pass phrase used for encrypted SNMPv3 messages.
+ # cmdline option '-X'
+ trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-password'.format(trap))
+
+ if conf.exists('v3 trap-target {0} privacy encrypted-password'.format(trap)):
+ # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys.
+ # cmdline option '-3M'
+ trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-password'.format(trap))
+
+ if conf.exists('v3 trap-target {0} protocol'.format(trap)):
+ trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap))
+
+ if conf.exists('v3 trap-target {0} port'.format(trap)):
+ trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap))
+
+ if conf.exists('v3 trap-target {0} type'.format(trap)):
+ trap_cfg['type'] = conf.return_value('v3 trap-target {0} type'.format(trap))
+
+ # Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv).
+ # Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv.
+ if trap_cfg['authPassword'] or trap_cfg['authMasterKey']:
+ if trap_cfg['privProtocol'] or trap_cfg['privPassword']:
+ trap_cfg['secLevel'] = 'authPriv'
+ else:
+ trap_cfg['secLevel'] = 'authNoPriv'
+
+ snmp['v3_traps'].append(trap_cfg)
+
+ # 'set service snmp v3 user'
+ if conf.exists('v3 user'):
+ for user in conf.list_nodes('v3 user'):
+ user_cfg = {
+ 'name': user,
+ 'authMasterKey': '',
+ 'authPassword': '',
+ 'authProtocol': 'md5',
+ 'authOID': 'none',
+ 'group': '',
+ 'mode': 'ro',
+ 'privMasterKey': '',
+ 'privPassword': '',
+ 'privOID': '',
+ 'privProtocol': 'des'
+ }
+
+ # v3 user {0} auth
+ if conf.exists('v3 user {0} auth encrypted-password'.format(user)):
+ user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-password'.format(user))
+
+ if conf.exists('v3 user {0} auth plaintext-password'.format(user)):
+ user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-password'.format(user))
+
+ # load default value
+ type = user_cfg['authProtocol']
+ if conf.exists('v3 user {0} auth type'.format(user)):
+ type = conf.return_value('v3 user {0} auth type'.format(user))
+
+ # (re-)update with either default value or value from CLI
+ user_cfg['authProtocol'] = type
+ user_cfg['authOID'] = OIDs[type]
+
+ # v3 user {0} group
+ if conf.exists('v3 user {0} group'.format(user)):
+ user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user))
+
+ # v3 user {0} mode
+ if conf.exists('v3 user {0} mode'.format(user)):
+ user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user))
+
+ # v3 user {0} privacy
+ if conf.exists('v3 user {0} privacy encrypted-password'.format(user)):
+ user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-password'.format(user))
+
+ if conf.exists('v3 user {0} privacy plaintext-password'.format(user)):
+ user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-password'.format(user))
+
+ # load default value
+ type = user_cfg['privProtocol']
+ if conf.exists('v3 user {0} privacy type'.format(user)):
+ type = conf.return_value('v3 user {0} privacy type'.format(user))
+
+ # (re-)update with either default value or value from CLI
+ user_cfg['privProtocol'] = type
+ user_cfg['privOID'] = OIDs[type]
+
+ snmp['v3_users'].append(user_cfg)
+
+ # 'set service snmp v3 view'
+ if conf.exists('v3 view'):
+ for view in conf.list_nodes('v3 view'):
+ view_cfg = {
+ 'name': view,
+ 'oids': []
+ }
+
+ if conf.exists('v3 view {0} oid'.format(view)):
+ for oid in conf.list_nodes('v3 view {0} oid'.format(view)):
+ oid_cfg = {
+ 'oid': oid
+ }
+ view_cfg['oids'].append(oid_cfg)
+ snmp['v3_views'].append(view_cfg)
+
+ return snmp
+
+def verify(snmp):
+ if snmp is None:
+ # we can not delete SNMP when LLDP is configured with SNMP
+ conf = Config()
+ if conf.exists('service lldp snmp enable'):
+ raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!')
+
+ return None
+
+ ### check if the configured script actually exist
+ if snmp['script_ext']:
+ for ext in snmp['script_ext']:
+ if not os.path.isfile(ext['script']):
+ print ("WARNING: script: {} doesn't exist".format(ext['script']))
+ else:
+ chmod_755(ext['script'])
+
+ for listen in snmp['listen_address']:
+ addr = listen[0]
+ port = listen[1]
+
+ if is_ipv4(addr):
+ # example: udp:127.0.0.1:161
+ listen = 'udp:' + addr + ':' + port
+ 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 is_addr_assigned(addr):
+ snmp['listen_on'].append(listen)
+ else:
+ print('WARNING: SNMP listen address {0} not configured!'.format(addr))
+
+ verify_vrf(snmp)
+
+ # bail out early if SNMP v3 is not configured
+ if not snmp['v3_enabled']:
+ return None
+
+ if 'v3_groups' in snmp.keys():
+ for group in snmp['v3_groups']:
+ #
+ # A view must exist prior to mapping it into a group
+ #
+ if 'view' in group.keys():
+ error = True
+ if 'v3_views' in snmp.keys():
+ for view in snmp['v3_views']:
+ if view['name'] == group['view']:
+ error = False
+ if error:
+ raise ConfigError('You must create view "{0}" first'.format(group['view']))
+ else:
+ raise ConfigError('"view" must be specified')
+
+ if not 'mode' in group.keys():
+ raise ConfigError('"mode" must be specified')
+
+ if not 'seclevel' in group.keys():
+ raise ConfigError('"seclevel" must be specified')
+
+ if 'v3_traps' in snmp.keys():
+ for trap in snmp['v3_traps']:
+ if trap['authPassword'] and trap['authMasterKey']:
+ raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap auth')
+
+ if trap['authPassword'] == '' and trap['authMasterKey'] == '':
+ raise ConfigError('Must specify encrypted-password or plaintext-key for trap auth')
+
+ if trap['privPassword'] and trap['privMasterKey']:
+ raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap privacy')
+
+ if trap['privPassword'] == '' and trap['privMasterKey'] == '':
+ raise ConfigError('Must specify encrypted-password or plaintext-key for trap privacy')
+
+ if not 'type' in trap.keys():
+ raise ConfigError('v3 trap: "type" must be specified')
+
+ if not 'authPassword' and 'authMasterKey' in trap.keys():
+ raise ConfigError('v3 trap: "auth" must be specified')
+
+ if not 'authProtocol' in trap.keys():
+ raise ConfigError('v3 trap: "protocol" must be specified')
+
+ if not 'privPassword' and 'privMasterKey' in trap.keys():
+ raise ConfigError('v3 trap: "user" must be specified')
+
+ if 'v3_users' in snmp.keys():
+ for user in snmp['v3_users']:
+ #
+ # Group must exist prior to mapping it into a group
+ # seclevel will be extracted from group
+ #
+ if user['group']:
+ error = True
+ if 'v3_groups' in snmp.keys():
+ for group in snmp['v3_groups']:
+ if group['name'] == user['group']:
+ seclevel = group['seclevel']
+ error = False
+
+ if error:
+ raise ConfigError('You must create group "{0}" first'.format(user['group']))
+
+ # Depending on the configured security level the user has to provide additional info
+ if (not user['authPassword'] and not user['authMasterKey']):
+ raise ConfigError('Must specify encrypted-password or plaintext-key for user auth')
+
+ if user['privPassword'] == '' and user['privMasterKey'] == '':
+ raise ConfigError('Must specify encrypted-password or plaintext-key for user privacy')
+
+ if user['mode'] == '':
+ raise ConfigError('Must specify user mode ro/rw')
+
+ if 'v3_views' in snmp.keys():
+ for view in snmp['v3_views']:
+ if not view['oids']:
+ raise ConfigError('Must configure an oid')
+
+ return None
+
+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
+ call('systemctl stop snmpd.service')
+ config_files = [config_file_client, config_file_daemon, config_file_access,
+ config_file_user, systemd_override]
+ for file in config_files:
+ rmfile(file)
+
+ if not snmp:
+ return None
+
+ if 'v3_users' in snmp.keys():
+ # net-snmp is now regenerating the configuration file in the background
+ # thus we need to re-open and re-read the file as the content changed.
+ # After that we can no read the encrypted password from the config and
+ # replace the CLI plaintext password with its encrypted version.
+ os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos"
+
+ for user in snmp['v3_users']:
+ if user['authProtocol'] == 'sha':
+ hash = plaintext_to_sha1
+ else:
+ hash = plaintext_to_md5
+
+ if user['authPassword']:
+ user['authMasterKey'] = hash(user['authPassword'], snmp['v3_engineid'])
+ user['authPassword'] = ''
+
+ call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" auth encrypted-password "{authMasterKey}" > /dev/null'.format(**user))
+ call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" auth plaintext-password > /dev/null'.format(**user))
+
+ if user['privPassword']:
+ user['privMasterKey'] = hash(user['privPassword'], snmp['v3_engineid'])
+ user['privPassword'] = ''
+
+ call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" privacy encrypted-password "{privMasterKey}" > /dev/null'.format(**user))
+ call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" privacy plaintext-password > /dev/null'.format(**user))
+
+ # Write client config file
+ render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp)
+ # Write server config file
+ render(config_file_daemon, 'snmp/etc.snmpd.conf.tmpl', snmp)
+ # Write access rights config file
+ render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp)
+ # Write access rights config file
+ render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp)
+ # Write daemon configuration file
+ render(systemd_override, 'snmp/override.conf.tmpl', snmp)
+
+ return None
+
+def apply(snmp):
+ # Always reload systemd manager configuration
+ call('systemctl daemon-reload')
+
+ if not snmp:
+ return None
+
+ # start SNMP daemon
+ call('systemctl restart snmpd.service')
+
+ # Enable AgentX in FRR
+ call('vtysh -c "configure terminal" -c "agentx" >/dev/null')
+
+ 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/ssh.py b/src/conf_mode/ssh.py
new file mode 100755
index 000000000..7b262565a
--- /dev/null
+++ b/src/conf_mode/ssh.py
@@ -0,0 +1,94 @@
+#!/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 netifaces import interfaces
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+from vyos.xml import defaults
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/ssh/sshd_config'
+systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf'
+
+def get_config():
+ conf = Config()
+ base = ['service', 'ssh']
+ if not conf.exists(base):
+ return None
+
+ ssh = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ ssh = dict_merge(default_values, ssh)
+ # pass config file path - used in override template
+ ssh['config_file'] = config_file
+
+ return ssh
+
+def verify(ssh):
+ if not ssh:
+ return None
+
+ if 'vrf' in ssh.keys() and ssh['vrf'] not in interfaces():
+ raise ConfigError('VRF "{vrf}" does not exist'.format(**ssh))
+
+ return None
+
+def generate(ssh):
+ if not ssh:
+ if os.path.isfile(config_file):
+ os.unlink(config_file)
+ if os.path.isfile(systemd_override):
+ os.unlink(systemd_override)
+
+ return None
+
+ render(config_file, 'ssh/sshd_config.tmpl', ssh, trim_blocks=True)
+ render(systemd_override, 'ssh/override.conf.tmpl', ssh, trim_blocks=True)
+
+ return None
+
+def apply(ssh):
+ if not ssh:
+ # SSH access is removed in the commit
+ call('systemctl stop ssh.service')
+
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+
+ if ssh:
+ call('systemctl restart ssh.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/system-ip.py b/src/conf_mode/system-ip.py
new file mode 100755
index 000000000..85f1e3771
--- /dev/null
+++ b/src/conf_mode/system-ip.py
@@ -0,0 +1,85 @@
+#!/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 sys import exit
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+default_config_data = {
+ 'arp_table': 8192,
+ 'ipv4_forward': '1',
+ 'mp_unreach_nexthop': '0',
+ 'mp_layer4_hashing': '0'
+}
+
+def sysctl(name, value):
+ call('sysctl -wq {}={}'.format(name, value))
+
+def get_config():
+ ip_opt = deepcopy(default_config_data)
+ conf = Config()
+ conf.set_level('system ip')
+ if conf.exists(''):
+ if conf.exists('arp table-size'):
+ ip_opt['arp_table'] = int(conf.return_value('arp table-size'))
+
+ if conf.exists('disable-forwarding'):
+ ip_opt['ipv4_forward'] = '0'
+
+ if conf.exists('multipath ignore-unreachable-nexthops'):
+ ip_opt['mp_unreach_nexthop'] = '1'
+
+ if conf.exists('multipath layer4-hashing'):
+ ip_opt['mp_layer4_hashing'] = '1'
+
+ return ip_opt
+
+def verify(ip_opt):
+ pass
+
+def generate(ip_opt):
+ pass
+
+def apply(ip_opt):
+ # apply ARP threshold values
+ sysctl('net.ipv4.neigh.default.gc_thresh3', ip_opt['arp_table'])
+ sysctl('net.ipv4.neigh.default.gc_thresh2', ip_opt['arp_table'] // 2)
+ sysctl('net.ipv4.neigh.default.gc_thresh1', ip_opt['arp_table'] // 8)
+
+ # enable/disable IPv4 forwarding
+ with open('/proc/sys/net/ipv4/conf/all/forwarding', 'w') as f:
+ f.write(ip_opt['ipv4_forward'])
+
+ # configure multipath
+ sysctl('net.ipv4.fib_multipath_use_neigh', ip_opt['mp_unreach_nexthop'])
+ sysctl('net.ipv4.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing'])
+
+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/system-ipv6.py b/src/conf_mode/system-ipv6.py
new file mode 100755
index 000000000..3417c609d
--- /dev/null
+++ b/src/conf_mode/system-ipv6.py
@@ -0,0 +1,113 @@
+#!/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 sys
+
+from sys import exit
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf'
+
+default_config_data = {
+ 'reboot_message': False,
+ 'ipv6_forward': '1',
+ 'disable_addr_assignment': False,
+ 'mp_layer4_hashing': '0',
+ 'neighbor_cache': 8192,
+ 'strict_dad': '1'
+
+}
+
+def sysctl(name, value):
+ call('sysctl -wq {}={}'.format(name, value))
+
+def get_config():
+ ip_opt = deepcopy(default_config_data)
+ conf = Config()
+ conf.set_level('system ipv6')
+ if conf.exists(''):
+ ip_opt['disable_addr_assignment'] = conf.exists('disable')
+ if conf.exists_effective('disable') != conf.exists('disable'):
+ ip_opt['reboot_message'] = True
+
+ if conf.exists('disable-forwarding'):
+ ip_opt['ipv6_forward'] = '0'
+
+ if conf.exists('multipath layer4-hashing'):
+ ip_opt['mp_layer4_hashing'] = '1'
+
+ if conf.exists('neighbor table-size'):
+ ip_opt['neighbor_cache'] = int(conf.return_value('neighbor table-size'))
+
+ if conf.exists('strict-dad'):
+ ip_opt['strict_dad'] = 2
+
+ return ip_opt
+
+def verify(ip_opt):
+ pass
+
+def generate(ip_opt):
+ pass
+
+def apply(ip_opt):
+ # disable IPv6 address assignment
+ if ip_opt['disable_addr_assignment']:
+ with open(ipv6_disable_file, 'w') as f:
+ f.write('options ipv6 disable_ipv6=1')
+ else:
+ if os.path.exists(ipv6_disable_file):
+ os.unlink(ipv6_disable_file)
+
+ if ip_opt['reboot_message']:
+ print('Changing IPv6 disable parameter will only take affect\n' \
+ 'when the system is rebooted.')
+
+ # configure multipath
+ sysctl('net.ipv6.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing'])
+
+ # apply neighbor table threshold values
+ sysctl('net.ipv6.neigh.default.gc_thresh3', ip_opt['neighbor_cache'])
+ sysctl('net.ipv6.neigh.default.gc_thresh2', ip_opt['neighbor_cache'] // 2)
+ sysctl('net.ipv6.neigh.default.gc_thresh1', ip_opt['neighbor_cache'] // 8)
+
+ # enable/disable IPv6 forwarding
+ with open('/proc/sys/net/ipv6/conf/all/forwarding', 'w') as f:
+ f.write(ip_opt['ipv6_forward'])
+
+ # configure IPv6 strict-dad
+ for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'):
+ for name in files:
+ if name == "accept_dad":
+ with open(os.path.join(root, name), 'w') as f:
+ f.write(str(ip_opt['strict_dad']))
+
+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/system-login-banner.py b/src/conf_mode/system-login-banner.py
new file mode 100755
index 000000000..5c0adc921
--- /dev/null
+++ b/src/conf_mode/system-login-banner.py
@@ -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/>.
+
+from sys import exit
+from vyos.config import Config
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+motd="""
+The programs included with the Debian GNU/Linux system are free software;
+the exact distribution terms for each program are described in the
+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'
+PRELOGIN_NET_FILE = r'/etc/issue.net'
+POSTLOGIN_FILE = r'/etc/motd'
+
+default_config_data = {
+ 'issue': 'Welcome to VyOS - \n \l\n',
+ 'issue_net': 'Welcome to VyOS\n',
+ 'motd': motd
+}
+
+def get_config():
+ banner = default_config_data
+ conf = Config()
+ base_level = ['system', 'login', 'banner']
+
+ if not conf.exists(base_level):
+ return banner
+ else:
+ conf.set_level(base_level)
+
+ # Post-Login banner
+ if conf.exists(['post-login']):
+ tmp = conf.return_value(['post-login'])
+ # 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'])
+ # 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
+
+def verify(banner):
+ pass
+
+def generate(banner):
+ pass
+
+def apply(banner):
+ with open(PRELOGIN_FILE, 'w') as f:
+ f.write(banner['issue'])
+
+ with open(PRELOGIN_NET_FILE, 'w') as f:
+ f.write(banner['issue_net'])
+
+ with open(POSTLOGIN_FILE, 'w') as f:
+ f.write(banner['motd'])
+
+ 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/system-login.py b/src/conf_mode/system-login.py
new file mode 100755
index 000000000..b1dd583b5
--- /dev/null
+++ b/src/conf_mode/system-login.py
@@ -0,0 +1,403 @@
+#!/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 crypt import crypt, METHOD_SHA512
+from netifaces import interfaces
+from psutil import users
+from pwd import getpwall, getpwnam
+from spwd import getspnam
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import cmd, call, DEVNULL, chmod_600, chmod_755
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+radius_config_file = "/etc/pam_radius_auth.conf"
+
+default_config_data = {
+ 'deleted': False,
+ 'add_users': [],
+ 'del_users': [],
+ 'radius_server': [],
+ 'radius_source_address': '',
+ 'radius_vrf': ''
+}
+
+
+def get_local_users():
+ """Return list of dynamically allocated users (see Debian Policy Manual)"""
+ local_users = []
+ for p in getpwall():
+ username = p[0]
+ uid = getpwnam(username).pw_uid
+ if uid in range(1000, 29999):
+ if username not in ['radius_user', 'radius_priv_user']:
+ local_users.append(username)
+
+ return local_users
+
+
+def get_config():
+ login = default_config_data
+ conf = Config()
+ base_level = ['system', 'login']
+
+ # We do not need to check if the nodes exist or not and bail out early
+ # ... this would interrupt the following logic on determine which users
+ # should be deleted and which users should stay.
+ #
+ # All fine so far!
+
+ # Read in all local users and store to list
+ for username in conf.list_nodes(base_level + ['user']):
+ user = {
+ 'name': username,
+ 'password_plaintext': '',
+ 'password_encrypted': '!',
+ 'public_keys': [],
+ 'full_name': '',
+ 'home_dir': '/home/' + username,
+ }
+ conf.set_level(base_level + ['user', username])
+
+ # Plaintext password
+ if conf.exists(['authentication', 'plaintext-password']):
+ user['password_plaintext'] = conf.return_value(
+ ['authentication', 'plaintext-password'])
+
+ # Encrypted password
+ if conf.exists(['authentication', 'encrypted-password']):
+ user['password_encrypted'] = conf.return_value(
+ ['authentication', 'encrypted-password'])
+
+ # User real name
+ if conf.exists(['full-name']):
+ user['full_name'] = conf.return_value(['full-name'])
+
+ # User home-directory
+ if conf.exists(['home-directory']):
+ user['home_dir'] = conf.return_value(['home-directory'])
+
+ # Read in public keys
+ for id in conf.list_nodes(['authentication', 'public-keys']):
+ key = {
+ 'name': id,
+ 'key': '',
+ 'options': '',
+ 'type': ''
+ }
+ conf.set_level(base_level + ['user', username, 'authentication',
+ 'public-keys', id])
+
+ # Public Key portion
+ if conf.exists(['key']):
+ key['key'] = conf.return_value(['key'])
+
+ # Options for individual public key
+ if conf.exists(['options']):
+ key['options'] = conf.return_value(['options'])
+
+ # Type of public key
+ if conf.exists(['type']):
+ key['type'] = conf.return_value(['type'])
+
+ # Append individual public key to list of user keys
+ user['public_keys'].append(key)
+
+ login['add_users'].append(user)
+
+ #
+ # RADIUS configuration
+ #
+ conf.set_level(base_level + ['radius'])
+
+ if conf.exists(['source-address']):
+ login['radius_source_address'] = conf.return_value(['source-address'])
+
+ # retrieve VRF instance
+ if conf.exists(['vrf']):
+ login['radius_vrf'] = conf.return_value(['vrf'])
+
+ # Read in all RADIUS servers and store to list
+ for server in conf.list_nodes(['server']):
+ server_cfg = {
+ 'address': server,
+ 'disabled': False,
+ 'key': '',
+ 'port': '1812',
+ 'timeout': '2',
+ 'priority': 255
+ }
+ conf.set_level(base_level + ['radius', 'server', server])
+
+ # Check if RADIUS server was temporary disabled
+ if conf.exists(['disable']):
+ server_cfg['disabled'] = True
+
+ # RADIUS shared secret
+ if conf.exists(['key']):
+ server_cfg['key'] = conf.return_value(['key'])
+
+ # RADIUS authentication port
+ if conf.exists(['port']):
+ server_cfg['port'] = conf.return_value(['port'])
+
+ # RADIUS session timeout
+ if conf.exists(['timeout']):
+ server_cfg['timeout'] = conf.return_value(['timeout'])
+
+ # Check if RADIUS server has priority
+ if conf.exists(['priority']):
+ server_cfg['priority'] = int(conf.return_value(['priority']))
+
+ # Append individual RADIUS server configuration to global server list
+ login['radius_server'].append(server_cfg)
+
+ # users no longer existing in the running configuration need to be deleted
+ local_users = get_local_users()
+ cli_users = [tmp['name'] for tmp in login['add_users']]
+ # create a list of all users, cli and users
+ all_users = list(set(local_users+cli_users))
+
+ # Remove any normal users that dos not exist in the current configuration.
+ # This can happen if user is added but configuration was not saved and
+ # system is rebooted.
+ login['del_users'] = [tmp for tmp in all_users if tmp not in cli_users]
+
+ return login
+
+
+def verify(login):
+ cur_user = os.environ['SUDO_USER']
+ 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 "{name}"!'.format(**key))
+
+ if not key['key']:
+ raise ConfigError(
+ 'SSH public key for id "{name}" missing!'.format(**key))
+
+ # At lease one RADIUS server must not be disabled
+ if len(login['radius_server']) > 0:
+ fail = True
+ for server in login['radius_server']:
+ if not server['disabled']:
+ fail = False
+ if fail:
+ raise ConfigError('At least one RADIUS server must be active.')
+
+ vrf_name = login['radius_vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
+ return None
+
+
+def generate(login):
+ # calculate users encrypted password
+ for user in login['add_users']:
+ if user['password_plaintext']:
+ user['password_encrypted'] = crypt(
+ user['password_plaintext'], METHOD_SHA512)
+ user['password_plaintext'] = ''
+
+ # remove old plaintext password and set new encrypted password
+ env = os.environ.copy()
+ env['vyos_libexec_dir'] = '/usr/libexec/vyos'
+
+ call("/opt/vyatta/sbin/my_set system login user '{name}' "
+ "authentication plaintext-password '{password_plaintext}'"
+ .format(**user), env=env)
+
+ call("/opt/vyatta/sbin/my_set system login user '{name}' "
+ "authentication encrypted-password '{password_encrypted}'"
+ .format(**user), env=env)
+
+ else:
+ try:
+ if getspnam(user['name']).sp_pwdp == user['password_encrypted']:
+ # If the current encrypted bassword matches the encrypted password
+ # from the config - do not update it. This will remove the encrypted
+ # value from the system logs.
+ #
+ # The encrypted password will be set only once during the first boot
+ # after an image upgrade.
+ user['password_encrypted'] = ''
+ except:
+ pass
+
+ if len(login['radius_server']) > 0:
+ render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl',
+ login, trim_blocks=True)
+
+ uid = getpwnam('root').pw_uid
+ gid = getpwnam('root').pw_gid
+ os.chown(radius_config_file, uid, gid)
+ chmod_600(radius_config_file)
+ else:
+ if os.path.isfile(radius_config_file):
+ os.unlink(radius_config_file)
+
+ return None
+
+
+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)
+ command = "useradd -m -N"
+ # check if user already exists:
+ if user['name'] in get_local_users():
+ # update existing account
+ command = "usermod"
+
+ # all accounts use /bin/vbash
+ command += " -s /bin/vbash"
+ # 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
+ if user['password_encrypted']:
+ command += " -p '{}'".format(user['password_encrypted'])
+
+ if user['full_name']:
+ command += " -c '{}'".format(user['full_name'])
+
+ if user['home_dir']:
+ command += " -d '{}'".format(user['home_dir'])
+
+ command += " -G frrvty,vyattacfg,sudo,adm,dip,disk"
+ command += " {}".format(user['name'])
+
+ try:
+ cmd(command)
+
+ uid = getpwnam(user['name']).pw_uid
+ gid = getpwnam(user['name']).pw_gid
+
+ # we should not rely on the value stored in user['home_dir'], as a
+ # crazy user will choose username root or any other system user
+ # which will fail. Should we deny using root at all?
+ home_dir = getpwnam(user['name']).pw_dir
+
+ # install ssh keys
+ 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)
+ chmod_755(ssh_key_dir)
+
+ 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")
+
+ for id in user['public_keys']:
+ line = ''
+ if id['options']:
+ line = '{} '.format(id['options'])
+
+ line += '{} {} {}\n'.format(id['type'],
+ id['key'], id['name'])
+ f.write(line)
+
+ os.chown(ssh_key_file, uid, gid)
+ chmod_600(ssh_key_file)
+
+ except Exception as e:
+ print(e)
+ raise ConfigError('Adding user "{name}" raised exception'
+ .format(**user))
+
+ for user in login['del_users']:
+ try:
+ # 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))
+ call('pkill -HUP -u {}'.format(user))
+
+ # Remove user account but leave home directory to be safe
+ call(f'userdel -r {user}', stderr=DEVNULL)
+
+ except Exception as e:
+ raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
+
+ #
+ # RADIUS configuration
+ #
+ if len(login['radius_server']) > 0:
+ try:
+ env = os.environ.copy()
+ env['DEBIAN_FRONTEND'] = 'noninteractive'
+ # Enable RADIUS in PAM
+ cmd("pam-auth-update --package --enable radius", env=env)
+
+ # Make NSS system aware of RADIUS, too
+ command = "sed -i -e \'/\smapname/b\' \
+ -e \'/^passwd:/s/\s\s*/&mapuid /\' \
+ -e \'/^passwd:.*#/s/#.*/mapname &/\' \
+ -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \
+ -e \'/^group:.*#/s/#.*/ mapname &/\' \
+ -e \'/^group:[^#]*$/s/: */&mapname /\' \
+ /etc/nsswitch.conf"
+
+ cmd(command)
+
+ except Exception as e:
+ raise ConfigError('RADIUS configuration failed: {}'.format(e))
+
+ else:
+ try:
+ env = os.environ.copy()
+ env['DEBIAN_FRONTEND'] = 'noninteractive'
+
+ # Disable RADIUS in PAM
+ cmd("pam-auth-update --package --remove radius", env=env)
+
+ 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"
+
+ cmd(command)
+
+ except Exception as e:
+ raise ConfigError(
+ 'Removing RADIUS configuration failed.\n{}'.format(e))
+
+ 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/system-options.py b/src/conf_mode/system-options.py
new file mode 100755
index 000000000..0aacd19d8
--- /dev/null
+++ b/src/conf_mode/system-options.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 netifaces import interfaces
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call
+from vyos.validate import is_addr_assigned
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+curlrc_config = r'/etc/curlrc'
+ssh_config = r'/etc/ssh/ssh_config'
+systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target'
+
+def get_config():
+ conf = Config()
+ base = ['system', 'options']
+ options = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ return options
+
+def verify(options):
+ if 'http_client' in options:
+ config = options['http_client']
+ if 'source_interface' in config:
+ if not config['source_interface'] in interfaces():
+ raise ConfigError(f'Source interface {source_interface} does not '
+ f'exist'.format(**config))
+
+ if {'source_address', 'source_interface'} <= set(config):
+ raise ConfigError('Can not define both HTTP source-interface and source-address')
+
+ if 'source_address' in config:
+ if not is_addr_assigned(config['source_address']):
+ raise ConfigError('No interface with give address specified!')
+
+ if 'ssh_client' in options:
+ config = options['ssh_client']
+ if 'source_address' in config:
+ if not is_addr_assigned(config['source_address']):
+ raise ConfigError('No interface with give address specified!')
+
+ return None
+
+def generate(options):
+ render(curlrc_config, 'system/curlrc.tmpl', options, trim_blocks=True)
+ render(ssh_config, 'system/ssh_config.tmpl', options, trim_blocks=True)
+ return None
+
+def apply(options):
+ # Beep action
+ if 'beep_if_fully_booted' in options.keys():
+ call('systemctl enable vyos-beep.service')
+ else:
+ call('systemctl disable vyos-beep.service')
+
+ # Ctrl-Alt-Delete action
+ if os.path.exists(systemd_action_file):
+ os.unlink(systemd_action_file)
+
+ if 'ctrl_alt_del_action' in options:
+ if options['ctrl_alt_del_action'] == 'reboot':
+ os.symlink('/lib/systemd/system/reboot.target', systemd_action_file)
+ elif options['ctrl_alt_del_action'] == 'poweroff':
+ os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file)
+
+ if 'http_client' not in options:
+ if os.path.exists(curlrc_config):
+ os.unlink(curlrc_config)
+
+ if 'ssh_client' not in options:
+ if os.path.exists(ssh_config):
+ os.unlink(ssh_config)
+
+ # Reboot system on kernel panic
+ with open('/proc/sys/kernel/panic', 'w') as f:
+ if 'reboot_on_panic' in options.keys():
+ f.write('60')
+ else:
+ f.write('0')
+
+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/system-proxy.py b/src/conf_mode/system-proxy.py
new file mode 100755
index 000000000..02536c2ab
--- /dev/null
+++ b/src/conf_mode/system-proxy.py
@@ -0,0 +1,95 @@
+#!/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
+
+from vyos import ConfigError
+from vyos.config import Config
+
+from vyos import airbag
+airbag.enable()
+
+proxy_def = r'/etc/profile.d/vyos-system-proxy.sh'
+
+
+def get_config():
+ c = Config()
+ if not c.exists('system proxy'):
+ return None
+
+ c.set_level('system proxy')
+
+ cnf = {
+ 'url': None,
+ 'port': None,
+ 'usr': None,
+ 'passwd': None
+ }
+
+ if c.exists('url'):
+ cnf['url'] = c.return_value('url')
+ if c.exists('port'):
+ cnf['port'] = c.return_value('port')
+ if c.exists('username'):
+ cnf['usr'] = c.return_value('username')
+ if c.exists('password'):
+ cnf['passwd'] = c.return_value('password')
+
+ return cnf
+
+
+def verify(c):
+ if not c:
+ return None
+ if not c['url'] or not c['port']:
+ raise ConfigError("proxy url and port requires a value")
+ elif c['usr'] and not c['passwd']:
+ raise ConfigError("proxy password requires a value")
+ elif not c['usr'] and c['passwd']:
+ raise ConfigError("proxy username requires a value")
+
+
+def generate(c):
+ if not c:
+ return None
+ if not c['usr']:
+ return str("export http_proxy={url}:{port}\nexport https_proxy=$http_proxy\nexport ftp_proxy=$http_proxy"
+ .format(url=c['url'], port=c['port']))
+ else:
+ return str("export http_proxy=http://{usr}:{passwd}@{url}:{port}\nexport https_proxy=$http_proxy\nexport ftp_proxy=$http_proxy"
+ .format(url=re.sub('http://', '', c['url']), port=c['port'], usr=c['usr'], passwd=c['passwd']))
+
+
+def apply(ln):
+ if not ln and os.path.exists(proxy_def):
+ os.remove(proxy_def)
+ else:
+ open(proxy_def, 'w').write(
+ "# generated by system-proxy.py\n{}\n".format(ln))
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ ln = generate(c)
+ apply(ln)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
new file mode 100755
index 000000000..cfc1ca55f
--- /dev/null
+++ b/src/conf_mode/system-syslog.py
@@ -0,0 +1,259 @@
+#!/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
+import re
+
+from sys import exit
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import run
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ c = Config()
+ if not c.exists('system syslog'):
+ return None
+ c.set_level('system syslog')
+
+ config_data = {
+ 'files': {},
+ 'console': {},
+ 'hosts': {},
+ 'user': {}
+ }
+
+ #
+ # /etc/rsyslog.d/vyos-rsyslog.conf
+ # 'set system syslog global'
+ #
+ config_data['files'].update(
+ {
+ 'global': {
+ 'log-file': '/var/log/messages',
+ 'max-size': 262144,
+ 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog',
+ 'selectors': '*.notice;local7.debug',
+ 'max-files': '5',
+ 'preserver_fqdn': False
+ }
+ }
+ )
+
+ if c.exists('global marker'):
+ config_data['files']['global']['marker'] = True
+ if c.exists('global marker interval'):
+ config_data['files']['global'][
+ 'marker-interval'] = c.return_value('global marker interval')
+ if c.exists('global facility'):
+ config_data['files']['global'][
+ 'selectors'] = generate_selectors(c, 'global facility')
+ if c.exists('global archive size'):
+ config_data['files']['global']['max-size'] = int(
+ c.return_value('global archive size')) * 1024
+ if c.exists('global archive file'):
+ config_data['files']['global'][
+ 'max-files'] = c.return_value('global archive file')
+ if c.exists('global preserve-fqdn'):
+ config_data['files']['global']['preserver_fqdn'] = True
+
+ #
+ # set system syslog file
+ #
+
+ if c.exists('file'):
+ filenames = c.list_nodes('file')
+ for filename in filenames:
+ config_data['files'].update(
+ {
+ filename: {
+ 'log-file': '/var/log/user/' + filename,
+ 'max-files': '5',
+ 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename,
+ 'selectors': '*.err',
+ 'max-size': 262144
+ }
+ }
+ )
+
+ if c.exists('file ' + filename + ' facility'):
+ config_data['files'][filename]['selectors'] = generate_selectors(
+ c, 'file ' + filename + ' facility')
+ if c.exists('file ' + filename + ' archive size'):
+ config_data['files'][filename]['max-size'] = int(
+ c.return_value('file ' + filename + ' archive size')) * 1024
+ if c.exists('file ' + filename + ' archive files'):
+ config_data['files'][filename]['max-files'] = c.return_value(
+ 'file ' + filename + ' archive files')
+
+ # set system syslog console
+ if c.exists('console'):
+ config_data['console'] = {
+ '/dev/console': {
+ 'selectors': '*.err'
+ }
+ }
+
+ for f in c.list_nodes('console facility'):
+ if c.exists('console facility ' + f + ' level'):
+ config_data['console'] = {
+ '/dev/console': {
+ 'selectors': generate_selectors(c, 'console facility')
+ }
+ }
+
+ # set system syslog host
+ if c.exists('host'):
+ rhosts = c.list_nodes('host')
+ proto = 'udp'
+ for rhost in rhosts:
+ for fac in c.list_nodes('host ' + rhost + ' facility'):
+ if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'):
+ proto = c.return_value(
+ 'host ' + rhost + ' facility ' + fac + ' protocol')
+ else:
+ proto = 'udp'
+
+ config_data['hosts'].update(
+ {
+ rhost: {
+ 'selectors': generate_selectors(c, 'host ' + rhost + ' facility'),
+ 'proto': proto
+ }
+ }
+ )
+ if c.exists('host ' + rhost + ' port'):
+ config_data['hosts'][rhost][
+ 'port'] = c.return_value(['host', rhost, 'port'])
+
+ # set system syslog user
+ if c.exists('user'):
+ usrs = c.list_nodes('user')
+ for usr in usrs:
+ config_data['user'].update(
+ {
+ usr: {
+ 'selectors': generate_selectors(c, 'user ' + usr + ' facility')
+ }
+ }
+ )
+
+ return config_data
+
+
+def generate_selectors(c, config_node):
+# protocols and security are being mapped here
+# for backward compatibility with old configs
+# security and protocol mappings can be removed later
+ nodes = c.list_nodes(config_node)
+ selectors = ""
+ for node in nodes:
+ lvl = c.return_value(config_node + ' ' + node + ' level')
+ if lvl == None:
+ lvl = "err"
+ if lvl == 'all':
+ lvl = '*'
+ if node == 'all' and node != nodes[-1]:
+ selectors += "*." + lvl + ";"
+ elif node == 'all':
+ selectors += "*." + lvl
+ elif node != nodes[-1]:
+ if node == 'protocols':
+ node = 'local7'
+ if node == 'security':
+ node = 'auth'
+ selectors += node + "." + lvl + ";"
+ else:
+ if node == 'protocols':
+ node = 'local7'
+ if node == 'security':
+ node = 'auth'
+ selectors += node + "." + lvl
+ return selectors
+
+
+def generate(c):
+ if c == None:
+ return None
+
+ conf = '/etc/rsyslog.d/vyos-rsyslog.conf'
+ render(conf, 'syslog/rsyslog.conf.tmpl', c, trim_blocks=True)
+
+ # eventually write for each file its own logrotate file, since size is
+ # defined it shouldn't matter
+ conf = '/etc/logrotate.d/vyos-rsyslog'
+ render(conf, 'syslog/logrotate.tmpl', c, trim_blocks=True)
+
+
+def verify(c):
+ if c == None:
+ return None
+
+ # may be obsolete
+ # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf)
+ # it interferes with the global logging, to make sure we are using a single base, template is enforced here
+ #
+ if not os.path.islink('/etc/rsyslog.conf'):
+ os.remove('/etc/rsyslog.conf')
+ os.symlink(
+ '/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf')
+
+ # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there
+ # is a chance that someone still needs it, so I don't automatically remove
+ # them
+ #
+
+ if c == None:
+ return None
+
+ fac = [
+ '*', 'auth', 'authpriv', 'cron', 'daemon', 'kern', 'lpr', 'mail', 'mark', 'news', 'protocols', 'security',
+ 'syslog', 'user', 'uucp', 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7']
+ lvl = ['emerg', 'alert', 'crit', 'err',
+ 'warning', 'notice', 'info', 'debug', '*']
+
+ for conf in c:
+ if c[conf]:
+ for item in c[conf]:
+ for s in c[conf][item]['selectors'].split(";"):
+ f = re.sub("\..*$", "", s)
+ if f not in fac:
+ raise ConfigError(
+ 'Invalid facility ' + s + ' set in ' + conf + ' ' + item)
+ l = re.sub("^.+\.", "", s)
+ if l not in lvl:
+ raise ConfigError(
+ 'Invalid logging level ' + s + ' set in ' + conf + ' ' + item)
+
+
+def apply(c):
+ if not c:
+ return run('systemctl stop syslog.service')
+ return run('systemctl restart syslog.service')
+
+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/system-timezone.py b/src/conf_mode/system-timezone.py
new file mode 100755
index 000000000..0f4513122
--- /dev/null
+++ b/src/conf_mode/system-timezone.py
@@ -0,0 +1,57 @@
+#!/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
+import os
+
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+default_config_data = {
+ 'name': 'UTC'
+}
+
+def get_config():
+ tz = deepcopy(default_config_data)
+ conf = Config()
+ if conf.exists('system time-zone'):
+ tz['name'] = conf.return_value('system time-zone')
+
+ return tz
+
+def verify(tz):
+ pass
+
+def generate(tz):
+ pass
+
+def apply(tz):
+ call('/usr/bin/timedatectl set-timezone {}'.format(tz['name']))
+
+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/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py
new file mode 100755
index 000000000..30ea89098
--- /dev/null
+++ b/src/conf_mode/system-wifi-regdom.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 sys import exit
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_80211_file='/etc/modprobe.d/cfg80211.conf'
+config_crda_file='/etc/default/crda'
+
+default_config_data = {
+ 'regdom' : '',
+ 'deleted' : False
+}
+
+def get_config():
+ regdom = deepcopy(default_config_data)
+ conf = Config()
+ base = ['system', 'wifi-regulatory-domain']
+
+ # Check if interface has been removed
+ if not conf.exists(base):
+ regdom['deleted'] = True
+ return regdom
+ else:
+ regdom['regdom'] = conf.return_value(base)
+
+ return regdom
+
+def verify(regdom):
+ if regdom['deleted']:
+ return None
+
+ if not regdom['regdom']:
+ raise ConfigError("Wireless regulatory domain is mandatory.")
+
+ return None
+
+def generate(regdom):
+ print("Changing the wireless regulatory domain requires a system reboot.")
+
+ if regdom['deleted']:
+ if os.path.isfile(config_80211_file):
+ os.unlink(config_80211_file)
+
+ if os.path.isfile(config_crda_file):
+ os.unlink(config_crda_file)
+
+ return None
+
+ render(config_80211_file, 'wifi/cfg80211.conf.tmpl', regdom)
+ render(config_crda_file, 'wifi/crda.tmpl', regdom)
+ return None
+
+def apply(regdom):
+ 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/system_console.py b/src/conf_mode/system_console.py
new file mode 100755
index 000000000..6f83335f3
--- /dev/null
+++ b/src/conf_mode/system_console.py
@@ -0,0 +1,141 @@
+#!/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 re
+
+from fileinput import input as replace_in_file
+from vyos.config import Config
+from vyos.util import call
+from vyos.template import render
+from vyos import ConfigError, airbag
+airbag.enable()
+
+by_bus_dir = '/dev/serial/by-bus'
+
+def get_config():
+ conf = Config()
+ base = ['system', 'console']
+
+ # retrieve configuration at once
+ console = conf.get_config_dict(base, get_first_key=True)
+
+ # bail out early if no serial console is configured
+ if 'device' not in console.keys():
+ return console
+
+ # convert CLI values to system values
+ for device in console['device'].keys():
+ # no speed setting has been configured - use default value
+ if not 'speed' in console['device'][device].keys():
+ tmp = { 'speed': '' }
+ if device.startswith('hvc'):
+ tmp['speed'] = 38400
+ else:
+ tmp['speed'] = 115200
+
+ console['device'][device].update(tmp)
+
+ if device.startswith('usb'):
+ # It is much easiert to work with the native ttyUSBn name when using
+ # getty, but that name may change across reboots - depending on the
+ # amount of connected devices. We will resolve the fixed device name
+ # to its dynamic device file - and create a new dict entry for it.
+ by_bus_device = f'{by_bus_dir}/{device}'
+ if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device):
+ tmp = os.path.basename(os.readlink(by_bus_device))
+ # updating the dict must come as last step in the loop!
+ console['device'][tmp] = console['device'].pop(device)
+
+ return console
+
+def verify(console):
+ return None
+
+def generate(console):
+ base_dir = '/etc/systemd/system'
+ # Remove all serial-getty configuration files in advance
+ for root, dirs, files in os.walk(base_dir):
+ for basename in files:
+ if 'serial-getty' in basename:
+ call(f'systemctl stop {basename}')
+ os.unlink(os.path.join(root, basename))
+
+ if not console:
+ return None
+
+ for device in console['device'].keys():
+ config_file = base_dir + f'/serial-getty@{device}.service'
+ getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service'
+
+ render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device])
+ os.symlink(config_file, getty_wants_symlink)
+
+ # GRUB
+ # For existing serial line change speed (if necessary)
+ # Only applys to ttyS0
+ if 'ttyS0' not in console['device'].keys():
+ return None
+
+ speed = console['device']['ttyS0']['speed']
+ grub_config = '/boot/grub/grub.cfg'
+ if not os.path.isfile(grub_config):
+ return None
+
+ # stdin/stdout are redirected in replace_in_file(), thus print() is fine
+ p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$')
+ for line in replace_in_file(grub_config, inplace=True):
+ if line.startswith('serial --unit'):
+ line = f'serial --unit=0 --speed={speed}\n'
+ elif p.match(line):
+ line = '{},{}{}\n'.format(p.search(line)[1], speed, p.search(line)[2])
+
+ print(line, end='')
+
+ return None
+
+def apply(console):
+ # reset screen blanking
+ call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux </dev/tty1 >/dev/tty1 2>&1')
+
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+
+ if not console:
+ return None
+
+ if 'powersave' in console.keys():
+ # Configure screen blank powersaving on VGA console
+ call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1')
+
+ # Start getty process on configured serial interfaces
+ for device in console['device'].keys():
+ # Only start console if it exists on the running system. If a user
+ # detaches a USB serial console and reboots - it should not fail!
+ if os.path.exists(f'/dev/{device}'):
+ call(f'systemctl start serial-getty@{device}.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/system_lcd.py b/src/conf_mode/system_lcd.py
new file mode 100755
index 000000000..31a09252d
--- /dev/null
+++ b/src/conf_mode/system_lcd.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+#
+# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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 vyos.config import Config
+from vyos.util import call
+from vyos.util import find_device_file
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+lcdd_conf = '/run/LCDd/LCDd.conf'
+lcdproc_conf = '/run/lcdproc/lcdproc.conf'
+
+def get_config():
+ conf = Config()
+ base = ['system', 'lcd']
+ lcd = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
+ # Return (possibly empty) dictionary
+ return lcd
+
+def verify(lcd):
+ if not lcd:
+ return None
+
+ if 'model' in lcd and lcd['model'] in ['sdec']:
+ # This is a fixed LCD display, no device needed - bail out early
+ return None
+
+ if not {'device', 'model'} <= set(lcd):
+ raise ConfigError('Both device and driver must be set!')
+
+ return None
+
+def generate(lcd):
+ if not lcd:
+ return None
+
+ if 'device' in lcd:
+ lcd['device'] = find_device_file(lcd['device'])
+
+ # Render config file for daemon LCDd
+ render(lcdd_conf, 'lcd/LCDd.conf.tmpl', lcd, trim_blocks=True)
+ # Render config file for client lcdproc
+ render(lcdproc_conf, 'lcd/lcdproc.conf.tmpl', lcd, trim_blocks=True)
+
+ return None
+
+def apply(lcd):
+ if not lcd:
+ call('systemctl stop lcdproc.service LCDd.service')
+
+ for file in [lcdd_conf, lcdproc_conf]:
+ if os.path.exists(file):
+ os.remove(file)
+ else:
+ # Restart server
+ call('systemctl restart LCDd.service lcdproc.service')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ config_dict = get_config()
+ verify(config_dict)
+ generate(config_dict)
+ apply(config_dict)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/task_scheduler.py b/src/conf_mode/task_scheduler.py
new file mode 100755
index 000000000..51d8684cb
--- /dev/null
+++ b/src/conf_mode/task_scheduler.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017 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 re
+import sys
+
+from vyos.config import Config
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+crontab_file = "/etc/cron.d/vyos-crontab"
+
+
+def format_task(minute="*", hour="*", day="*", dayofweek="*", month="*", user="root", rawspec=None, command=""):
+ fmt_full = "{minute} {hour} {day} {month} {dayofweek} {user} {command}\n"
+ fmt_raw = "{spec} {user} {command}\n"
+
+ if rawspec is None:
+ s = fmt_full.format(minute=minute, hour=hour, day=day,
+ dayofweek=dayofweek, month=month, command=command, user=user)
+ else:
+ s = fmt_raw.format(spec=rawspec, user=user, command=command)
+
+ return s
+
+def split_interval(s):
+ result = re.search(r"(\d+)([mdh]?)", s)
+ value = int(result.group(1))
+ suffix = result.group(2)
+ return( (value, suffix) )
+
+def make_command(executable, arguments):
+ if arguments:
+ return("sg vyattacfg \"{0} {1}\"".format(executable, arguments))
+ else:
+ return("sg vyattacfg \"{0}\"".format(executable))
+
+def get_config():
+ conf = Config()
+ conf.set_level("system task-scheduler task")
+ task_names = conf.list_nodes("")
+ tasks = []
+
+ for name in task_names:
+ interval = conf.return_value("{0} interval".format(name))
+ spec = conf.return_value("{0} crontab-spec".format(name))
+ executable = conf.return_value("{0} executable path".format(name))
+ args = conf.return_value("{0} executable arguments".format(name))
+ task = {
+ "name": name,
+ "interval": interval,
+ "spec": spec,
+ "executable": executable,
+ "args": args
+ }
+ tasks.append(task)
+
+ return tasks
+
+def verify(tasks):
+ for task in tasks:
+ if not task["interval"] and not task["spec"]:
+ raise ConfigError("Invalid task {0}: must define either interval or crontab-spec".format(task["name"]))
+
+ if task["interval"]:
+ if task["spec"]:
+ raise ConfigError("Invalid task {0}: cannot use interval and crontab-spec at the same time".format(task["name"]))
+
+ if not re.match(r"^\d+[mdh]?$", task["interval"]):
+ raise(ConfigError("Invalid interval {0} in task {1}: interval should be a number optionally followed by m, h, or d".format(task["name"], task["interval"])))
+ else:
+ # Check if values are within allowed range
+ value, suffix = split_interval(task["interval"])
+
+ if not suffix or suffix == "m":
+ if value > 60:
+ raise ConfigError("Invalid task {0}: interval in minutes must not exceed 60".format(task["name"]))
+ elif suffix == "h":
+ if value > 24:
+ raise ConfigError("Invalid task {0}: interval in hours must not exceed 24".format(task["name"]))
+ elif suffix == "d":
+ if value > 31:
+ raise ConfigError("Invalid task {0}: interval in days must not exceed 31".format(task["name"]))
+
+ if not task["executable"]:
+ raise ConfigError("Invalid task {0}: executable is not defined".format(task["name"]))
+ else:
+ # Check if executable exists and is executable
+ if not (os.path.isfile(task["executable"]) and os.access(task["executable"], os.X_OK)):
+ raise ConfigError("Invalid task {0}: file {1} does not exist or is not executable".format(task["name"], task["executable"]))
+
+def generate(tasks):
+ crontab_header = "### Generated by vyos-update-crontab.py ###\n"
+ if len(tasks) == 0:
+ if os.path.exists(crontab_file):
+ os.remove(crontab_file)
+ else:
+ pass
+ else:
+ crontab_lines = []
+ for task in tasks:
+ command = make_command(task["executable"], task["args"])
+ if task["spec"]:
+ line = format_task(command=command, rawspec=task["spec"])
+ else:
+ value, suffix = split_interval(task["interval"])
+ if not suffix or suffix == "m":
+ line = format_task(command=command, minute="*/{0}".format(value))
+ elif suffix == "h":
+ line = format_task(command=command, minute="0", hour="*/{0}".format(value))
+ elif suffix == "d":
+ line = format_task(command=command, minute="0", hour="0", day="*/{0}".format(value))
+ crontab_lines.append(line)
+
+ with open(crontab_file, 'w') as f:
+ f.write(crontab_header)
+ f.writelines(crontab_lines)
+
+def apply(config):
+ # No daemon restarts etc. needed for cron
+ pass
+
+
+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/tftp_server.py b/src/conf_mode/tftp_server.py
new file mode 100755
index 000000000..d31851bef
--- /dev/null
+++ b/src/conf_mode/tftp_server.py
@@ -0,0 +1,149 @@
+#!/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
+import stat
+import pwd
+
+from copy import deepcopy
+from glob import glob
+from sys import exit
+
+from vyos.config import Config
+from vyos.validate import is_ipv4, is_addr_assigned
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/default/tftpd'
+
+default_config_data = {
+ 'directory': '',
+ 'allow_upload': False,
+ 'port': '69',
+ 'listen': []
+}
+
+def get_config():
+ tftpd = deepcopy(default_config_data)
+ conf = Config()
+ base = ['service', 'tftp-server']
+ if not conf.exists(base):
+ return None
+ else:
+ conf.set_level(base)
+
+ if conf.exists(['directory']):
+ tftpd['directory'] = conf.return_value(['directory'])
+
+ if conf.exists(['allow-upload']):
+ tftpd['allow_upload'] = True
+
+ if conf.exists(['port']):
+ tftpd['port'] = conf.return_value(['port'])
+
+ if conf.exists(['listen-address']):
+ tftpd['listen'] = conf.return_values(['listen-address'])
+
+ return tftpd
+
+def verify(tftpd):
+ # bail out early - looks like removal from running config
+ if tftpd is None:
+ return None
+
+ # Configuring allowed clients without a server makes no sense
+ if not tftpd['directory']:
+ raise ConfigError('TFTP root directory must be configured!')
+
+ if not tftpd['listen']:
+ raise ConfigError('TFTP server listen address must be configured!')
+
+ for addr in tftpd['listen']:
+ if not is_addr_assigned(addr):
+ print('WARNING: TFTP server listen address {0} not assigned to any interface!'.format(addr))
+
+ return None
+
+def generate(tftpd):
+ # cleanup any available configuration file
+ # files will be recreated on demand
+ for i in glob(config_file + '*'):
+ os.unlink(i)
+
+ # bail out early - looks like removal from running config
+ if tftpd is None:
+ return None
+
+ idx = 0
+ for listen in tftpd['listen']:
+ config = deepcopy(tftpd)
+ if is_ipv4(listen):
+ config['listen'] = [listen + ":" + tftpd['port'] + " -4"]
+ else:
+ config['listen'] = ["[" + listen + "]" + tftpd['port'] + " -6"]
+
+ file = config_file + str(idx)
+ render(file, 'tftp-server/default.tmpl', config)
+
+ idx = idx + 1
+
+ return None
+
+def apply(tftpd):
+ # stop all services first - then we will decide
+ call('systemctl stop tftpd@{0..20}.service')
+
+ # bail out early - e.g. service deletion
+ if tftpd is None:
+ return None
+
+ tftp_root = tftpd['directory']
+ if not os.path.exists(tftp_root):
+ os.makedirs(tftp_root)
+ os.chmod(tftp_root, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
+
+ # get UNIX uid for user 'tftp'
+ tftp_uid = pwd.getpwnam('tftp').pw_uid
+ tftp_gid = pwd.getpwnam('tftp').pw_gid
+
+ # get UNIX uid for tftproot directory
+ dir_uid = os.stat(tftp_root).st_uid
+ dir_gid = os.stat(tftp_root).st_gid
+
+ # adjust uid/gid of tftproot directory if files don't belong to user tftp
+ if (tftp_uid != dir_uid) or (tftp_gid != dir_gid):
+ os.chown(tftp_root, tftp_uid, tftp_gid)
+
+ idx = 0
+ for listen in tftpd['listen']:
+ call('systemctl restart tftpd@{0}.service'.format(idx))
+ idx = idx + 1
+
+ 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/vpn_anyconnect.py b/src/conf_mode/vpn_anyconnect.py
new file mode 100755
index 000000000..158e1a117
--- /dev/null
+++ b/src/conf_mode/vpn_anyconnect.py
@@ -0,0 +1,135 @@
+#!/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 sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.xml import defaults
+from vyos.template import render
+from vyos.util import call
+from vyos import ConfigError
+from crypt import crypt, mksalt, METHOD_SHA512
+
+from vyos import airbag
+airbag.enable()
+
+cfg_dir = '/run/ocserv'
+ocserv_conf = cfg_dir + '/ocserv.conf'
+ocserv_passwd = cfg_dir + '/ocpasswd'
+radius_cfg = cfg_dir + '/radiusclient.conf'
+radius_servers = cfg_dir + '/radius_servers'
+
+
+# Generate hash from user cleartext password
+def get_hash(password):
+ return crypt(password, mksalt(METHOD_SHA512))
+
+
+def get_config():
+ conf = Config()
+ base = ['vpn', 'anyconnect']
+ if not conf.exists(base):
+ return None
+
+ ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ default_values = defaults(base)
+ ocserv = dict_merge(default_values, ocserv)
+ return ocserv
+
+
+def verify(ocserv):
+ if ocserv is None:
+ return None
+
+ # Check authentication
+ if "authentication" in ocserv:
+ if "mode" in ocserv["authentication"]:
+ if "local" in ocserv["authentication"]["mode"]:
+ if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]:
+ raise ConfigError('Anyconect mode local required at leat one user')
+ else:
+ for user in ocserv["authentication"]["local_users"]["username"]:
+ if not "password" in ocserv["authentication"]["local_users"]["username"][user]:
+ raise ConfigError(f'password required for user {user}')
+ else:
+ raise ConfigError('anyconnect authentication mode required')
+ else:
+ raise ConfigError('anyconnect authentication credentials required')
+
+ # Check ssl
+ if "ssl" in ocserv:
+ req_cert = ['ca_cert_file', 'cert_file', 'key_file']
+ for cert in req_cert:
+ if not cert in ocserv["ssl"]:
+ raise ConfigError('anyconnect ssl {0} required'.format(cert.replace('_', '-')))
+ else:
+ raise ConfigError('anyconnect ssl required')
+
+ # Check network settings
+ if "network_settings" in ocserv:
+ if "push_route" in ocserv["network_settings"]:
+ # Replace default route
+ if "0.0.0.0/0" in ocserv["network_settings"]["push_route"]:
+ ocserv["network_settings"]["push_route"].remove("0.0.0.0/0")
+ ocserv["network_settings"]["push_route"].append("default")
+ else:
+ ocserv["network_settings"]["push_route"] = "default"
+ else:
+ raise ConfigError('anyconnect network settings required')
+
+
+def generate(ocserv):
+ if not ocserv:
+ return None
+
+ if "radius" in ocserv["authentication"]["mode"]:
+ # Render radius client configuration
+ render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"], trim_blocks=True)
+ # Render radius servers
+ render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"], trim_blocks=True)
+ else:
+ if "local_users" in ocserv["authentication"]:
+ for user in ocserv["authentication"]["local_users"]["username"]:
+ ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"])
+ # Render local users
+ render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"], trim_blocks=True)
+
+ # Render config
+ render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv, trim_blocks=True)
+
+
+
+def apply(ocserv):
+ if not ocserv:
+ call('systemctl stop ocserv.service')
+ for file in [ocserv_conf, ocserv_passwd]:
+ if os.path.exists(file):
+ os.unlink(file)
+ else:
+ call('systemctl restart ocserv.service')
+
+
+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/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
new file mode 100755
index 000000000..26ad1af84
--- /dev/null
+++ b/src/conf_mode/vpn_l2tp.py
@@ -0,0 +1,381 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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 re
+
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from sys import exit
+from time import sleep
+
+from ipaddress import ip_network
+
+from vyos.config import Config
+from vyos.util import call, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+l2tp_conf = '/run/accel-pppd/l2tp.conf'
+l2tp_chap_secrets = '/run/accel-pppd/l2tp.chap-secrets'
+
+default_config_data = {
+ 'auth_mode': 'local',
+ 'auth_ppp_mppe': 'prefer',
+ 'auth_proto': ['auth_mschap_v2'],
+ 'chap_secrets_file': l2tp_chap_secrets, # used in Jinja2 template
+ 'client_ip_pool': None,
+ 'client_ip_subnets': [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
+ 'dnsv4': [],
+ 'dnsv6': [],
+ 'gateway_address': '10.255.255.0',
+ 'local_users' : [],
+ 'mtu': '1436',
+ 'outside_addr': '',
+ 'ppp_mppe': 'prefer',
+ 'ppp_echo_failure' : '3',
+ 'ppp_echo_interval' : '30',
+ 'ppp_echo_timeout': '0',
+ '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': '',
+ 'wins': [],
+ 'ip6_column': [],
+ 'thread_cnt': get_half_cpus()
+}
+
+def get_config():
+ conf = Config()
+ base_path = ['vpn', 'l2tp', 'remote-access']
+ if not conf.exists(base_path):
+ return None
+
+ conf.set_level(base_path)
+ l2tp = deepcopy(default_config_data)
+
+ ### general options ###
+ if conf.exists(['name-server']):
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ l2tp['dnsv4'].append(name_server)
+ else:
+ l2tp['dnsv6'].append(name_server)
+
+ if conf.exists(['wins-server']):
+ l2tp['wins'] = conf.return_values(['wins-server'])
+
+ if conf.exists('outside-address'):
+ l2tp['outside_addr'] = conf.return_value('outside-address')
+
+ if conf.exists(['authentication', 'mode']):
+ l2tp['auth_mode'] = conf.return_value(['authentication', 'mode'])
+
+ if conf.exists(['authentication', '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(['authentication', 'protocols']):
+ l2tp['auth_proto'].append(auth_mods[proto])
+
+ if conf.exists(['authentication', 'mppe']):
+ l2tp['auth_ppp_mppe'] = conf.return_value(['authentication', 'mppe'])
+
+ #
+ # 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'])
+
+ l2tp['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',
+ 'acct_port' : '1813'
+ }
+
+ conf.set_level(base_path + ['authentication', 'radius', 'server', server])
+
+ if conf.exists(['fail-time']):
+ radius['fail_time'] = conf.return_value(['fail-time'])
+
+ if conf.exists(['port']):
+ radius['port'] = conf.return_value(['port'])
+
+ if conf.exists(['acct-port']):
+ radius['acct_port'] = conf.return_value(['acct-port'])
+
+ if conf.exists(['key']):
+ radius['key'] = conf.return_value(['key'])
+
+ if not conf.exists(['disable']):
+ l2tp['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+
+ if conf.exists(['acct-timeout']):
+ l2tp['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ l2tp['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ l2tp['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ l2tp['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ l2tp['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ l2tp['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'])
+
+ l2tp['radius_dynamic_author'] = dae
+
+ if conf.exists(['rate-limit', 'enable']):
+ l2tp['radius_shaper_attr'] = 'Filter-Id'
+ c_attr = ['rate-limit', 'enable', 'attribute']
+ if conf.exists(c_attr):
+ l2tp['radius_shaper_attr'] = conf.return_value(c_attr)
+
+ c_vendor = ['rate-limit', 'enable', 'vendor']
+ if conf.exists(c_vendor):
+ l2tp['radius_shaper_vendor'] = conf.return_value(c_vendor)
+
+ conf.set_level(base_path)
+ if conf.exists(['client-ip-pool']):
+ if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']):
+ start = conf.return_value(['client-ip-pool', 'start'])
+ stop = conf.return_value(['client-ip-pool', 'stop'])
+ l2tp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0)
+
+ if conf.exists(['client-ip-pool', 'subnet']):
+ l2tp['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet'])
+
+ if conf.exists(['client-ipv6-pool', 'prefix']):
+ l2tp['ip6_column'].append('ip6')
+ for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': '64'
+ }
+
+ if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']):
+ tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask'])
+
+ l2tp['client_ipv6_pool'].append(tmp)
+
+ if conf.exists(['client-ipv6-pool', 'delegate']):
+ l2tp['ip6_column'].append('ip6-db')
+ for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix'])
+
+ l2tp['client_ipv6_delegate_prefix'].append(tmp)
+
+ if conf.exists(['mtu']):
+ l2tp['mtu'] = conf.return_value(['mtu'])
+
+ # gateway address
+ if conf.exists(['gateway-address']):
+ l2tp['gateway_address'] = conf.return_value(['gateway-address'])
+ else:
+ # calculate gw-ip-address
+ if conf.exists(['client-ip-pool', 'start']):
+ # use start ip as gw-ip-address
+ l2tp['gateway_address'] = conf.return_value(['client-ip-pool', 'start'])
+
+ elif conf.exists(['client-ip-pool', 'subnet']):
+ # use first ip address from first defined pool
+ subnet = conf.return_values(['client-ip-pool', 'subnet'])[0]
+ subnet = ip_network(subnet)
+ l2tp['gateway_address'] = str(list(subnet.hosts())[0])
+
+ # LNS secret
+ if conf.exists(['lns', 'shared-secret']):
+ l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret'])
+
+ if conf.exists(['ccp-disable']):
+ l2tp['ccp_disable'] = True
+
+ # PPP options
+ if conf.exists(['idle']):
+ l2tp['ppp_echo_timeout'] = conf.return_value(['idle'])
+
+ if conf.exists(['ppp-options', 'lcp-echo-failure']):
+ l2tp['ppp_echo_failure'] = conf.return_value(['ppp-options', 'lcp-echo-failure'])
+
+ if conf.exists(['ppp-options', 'lcp-echo-interval']):
+ l2tp['ppp_echo_interval'] = conf.return_value(['ppp-options', 'lcp-echo-interval'])
+
+ return l2tp
+
+
+def verify(l2tp):
+ if not l2tp:
+ return None
+
+ if l2tp['auth_mode'] == 'local':
+ if not l2tp['local_users']:
+ raise ConfigError('L2TP local auth mode requires local users to be configured!')
+
+ for user in l2tp['local_users']:
+ if not user['password']:
+ raise ConfigError(f"Password required for user {user['name']}")
+
+ elif l2tp['auth_mode'] == 'radius':
+ if len(l2tp['radius_server']) == 0:
+ raise ConfigError("RADIUS authentication requires at least one server")
+
+ for radius in l2tp['radius_server']:
+ if not radius['key']:
+ raise ConfigError(f"Missing RADIUS secret for server { radius['key'] }")
+
+ # check for the existence of a client ip pool
+ if not (l2tp['client_ip_pool'] or l2tp['client_ip_subnets']):
+ raise ConfigError(
+ "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool")
+
+ # check ipv6
+ if l2tp['client_ipv6_delegate_prefix'] and not l2tp['client_ipv6_pool']:
+ raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix')
+
+ for prefix in l2tp['client_ipv6_delegate_prefix']:
+ if not prefix['mask']:
+ raise ConfigError('Delegation-prefix required for individual delegated networks')
+
+ if len(l2tp['wins']) > 2:
+ raise ConfigError('Not more then two IPv4 WINS name-servers can be configured')
+
+ if len(l2tp['dnsv4']) > 2:
+ raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+
+ if len(l2tp['dnsv6']) > 3:
+ raise ConfigError('Not more then three IPv6 DNS name-servers can be configured')
+
+ return None
+
+
+def generate(l2tp):
+ if not l2tp:
+ return None
+
+ render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True)
+
+ if l2tp['auth_mode'] == 'local':
+ render(l2tp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', l2tp)
+ os.chmod(l2tp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+
+ else:
+ if os.path.exists(l2tp_chap_secrets):
+ os.unlink(l2tp_chap_secrets)
+
+ return None
+
+
+def apply(l2tp):
+ if not l2tp:
+ call('systemctl stop accel-ppp@l2tp.service')
+ for file in [l2tp_chap_secrets, l2tp_conf]:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ return None
+
+ call('systemctl restart accel-ppp@l2tp.service')
+
+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/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
new file mode 100755
index 000000000..32cbadd74
--- /dev/null
+++ b/src/conf_mode/vpn_pptp.py
@@ -0,0 +1,286 @@
+#!/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
+import re
+
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, get_half_cpus
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+pptp_conf = '/run/accel-pppd/pptp.conf'
+pptp_chap_secrets = '/run/accel-pppd/pptp.chap-secrets'
+
+default_pptp = {
+ 'auth_mode' : 'local',
+ 'local_users' : [],
+ 'radius_server' : [],
+ 'radius_acct_tmo' : '30',
+ 'radius_max_try' : '3',
+ 'radius_timeout' : '30',
+ 'radius_nas_id' : '',
+ 'radius_nas_ip' : '',
+ 'radius_source_address' : '',
+ 'radius_shaper_attr' : '',
+ 'radius_shaper_vendor': '',
+ 'radius_dynamic_author' : '',
+ 'chap_secrets_file': pptp_chap_secrets, # used in Jinja2 template
+ 'outside_addr': '',
+ 'dnsv4': [],
+ 'wins': [],
+ 'client_ip_pool': '',
+ 'mtu': '1436',
+ 'auth_proto' : ['auth_mschap_v2'],
+ 'ppp_mppe' : 'prefer',
+ 'thread_cnt': get_half_cpus()
+}
+
+def get_config():
+ conf = Config()
+ base_path = ['vpn', 'pptp', 'remote-access']
+ if not conf.exists(base_path):
+ return None
+
+ pptp = deepcopy(default_pptp)
+ conf.set_level(base_path)
+
+ if conf.exists(['name-server']):
+ pptp['dnsv4'] = conf.return_values(['name-server'])
+
+ if conf.exists(['wins-server']):
+ pptp['wins'] = conf.return_values(['wins-server'])
+
+ if conf.exists(['outside-address']):
+ pptp['outside_addr'] = conf.return_value(['outside-address'])
+
+ if conf.exists(['authentication', 'mode']):
+ pptp['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' : '*',
+ }
+
+ 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 not conf.exists(['disable']):
+ pptp['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',
+ 'acct_port' : '1813'
+ }
+
+ conf.set_level(base_path + ['authentication', 'radius', 'server', server])
+
+ if conf.exists(['fail-time']):
+ radius['fail_time'] = conf.return_value(['fail-time'])
+
+ if conf.exists(['port']):
+ radius['port'] = conf.return_value(['port'])
+
+ if conf.exists(['acct-port']):
+ radius['acct_port'] = conf.return_value(['acct-port'])
+
+ if conf.exists(['key']):
+ radius['key'] = conf.return_value(['key'])
+
+ if not conf.exists(['disable']):
+ pptp['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+
+ if conf.exists(['acct-timeout']):
+ pptp['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ pptp['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ pptp['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ pptp['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ pptp['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ pptp['radius_source_address'] = conf.return_value(['source-address'])
+
+ # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA)
+ if conf.exists(['dae-server']):
+ dae = {
+ 'port' : '',
+ 'server' : '',
+ 'key' : ''
+ }
+
+ if conf.exists(['dynamic-author', 'ip-address']):
+ dae['server'] = conf.return_value(['dynamic-author', 'ip-address'])
+
+ 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'])
+
+ pptp['radius_dynamic_author'] = dae
+
+ if conf.exists(['rate-limit', 'enable']):
+ pptp['radius_shaper_attr'] = 'Filter-Id'
+ c_attr = ['rate-limit', 'enable', 'attribute']
+ if conf.exists(c_attr):
+ pptp['radius_shaper_attr'] = conf.return_value(c_attr)
+
+ c_vendor = ['rate-limit', 'enable', 'vendor']
+ if conf.exists(c_vendor):
+ pptp['radius_shaper_vendor'] = conf.return_value(c_vendor)
+
+ conf.set_level(base_path)
+ if conf.exists(['client-ip-pool']):
+ if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']):
+ start = conf.return_value(['client-ip-pool', 'start'])
+ stop = conf.return_value(['client-ip-pool', 'stop'])
+ pptp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0)
+
+ if conf.exists(['mtu']):
+ pptp['mtu'] = conf.return_value(['mtu'])
+
+ # gateway address
+ if conf.exists(['gateway-address']):
+ pptp['gw_ip'] = conf.return_value(['gateway-address'])
+ else:
+ # calculate gw-ip-address
+ if conf.exists(['client-ip-pool', 'start']):
+ # use start ip as gw-ip-address
+ pptp['gateway_address'] = conf.return_value(['client-ip-pool', 'start'])
+
+ if conf.exists(['authentication', 'require']):
+ # clear default list content, now populate with actual CLI values
+ pptp['auth_proto'] = []
+ auth_mods = {
+ 'pap': 'auth_pap',
+ 'chap': 'auth_chap_md5',
+ 'mschap': 'auth_mschap_v1',
+ 'mschap-v2': 'auth_mschap_v2'
+ }
+
+ for proto in conf.return_values(['authentication', 'require']):
+ pptp['auth_proto'].append(auth_mods[proto])
+
+ if conf.exists(['authentication', 'mppe']):
+ pptp['ppp_mppe'] = conf.return_value(['authentication', 'mppe'])
+
+ return pptp
+
+
+def verify(pptp):
+ if not pptp:
+ return None
+
+ if pptp['auth_mode'] == 'local':
+ if not pptp['local_users']:
+ raise ConfigError('PPTP local auth mode requires local users to be configured!')
+
+ for user in pptp['local_users']:
+ username = user['name']
+ if not user['password']:
+ raise ConfigError(f'Password required for local user "{username}"')
+
+ elif pptp['auth_mode'] == 'radius':
+ if len(pptp['radius_server']) == 0:
+ raise ConfigError('RADIUS authentication requires at least one server')
+
+ for radius in pptp['radius_server']:
+ if not radius['key']:
+ server = radius['server']
+ raise ConfigError(f'Missing RADIUS secret key for server "{ server }"')
+
+ if len(pptp['dnsv4']) > 2:
+ raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+
+ if len(pptp['wins']) > 2:
+ raise ConfigError('Not more then two IPv4 WINS name-servers can be configured')
+
+
+def generate(pptp):
+ if not pptp:
+ return None
+
+ render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True)
+
+ if pptp['local_users']:
+ render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp, trim_blocks=True)
+ os.chmod(pptp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+ else:
+ if os.path.exists(pptp_chap_secrets):
+ os.unlink(pptp_chap_secrets)
+
+
+def apply(pptp):
+ if not pptp:
+ call('systemctl stop accel-ppp@pptp.service')
+ for file in [pptp_conf, pptp_chap_secrets]:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ return None
+
+ call('systemctl restart accel-ppp@pptp.service')
+
+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/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
new file mode 100755
index 000000000..ddb499bf4
--- /dev/null
+++ b/src/conf_mode/vpn_sstp.py
@@ -0,0 +1,387 @@
+#!/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 copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, run, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+sstp_conf = '/run/accel-pppd/sstp.conf'
+sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
+
+default_config_data = {
+ 'local_users' : [],
+ 'auth_mode' : 'local',
+ 'auth_proto' : ['auth_mschap_v2'],
+ 'chap_secrets_file': sstp_chap_secrets, # used in Jinja2 template
+ 'client_ip_pool' : [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
+ 'client_gateway': '',
+ 'dnsv4' : [],
+ 'dnsv6' : [],
+ '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' : '',
+ 'mtu' : '',
+ 'ppp_mppe' : 'prefer',
+ 'ppp_echo_failure' : '',
+ 'ppp_echo_interval' : '',
+ 'ppp_echo_timeout' : '',
+ 'thread_cnt' : get_half_cpus()
+}
+
+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)
+
+ 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',
+ 'acct_port' : '1813'
+ }
+
+ conf.set_level(base_path + ['authentication', 'radius', 'server', server])
+
+ if conf.exists(['fail-time']):
+ radius['fail_time'] = conf.return_value(['fail-time'])
+
+ if conf.exists(['port']):
+ radius['port'] = conf.return_value(['port'])
+
+ if conf.exists(['acct-port']):
+ radius['acct_port'] = conf.return_value(['acct-port'])
+
+ if conf.exists(['key']):
+ radius['key'] = conf.return_value(['key'])
+
+ if not conf.exists(['disable']):
+ 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']):
+ # clear default list content, now populate with actual CLI values
+ sstp['auth_proto'] = []
+ 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])
+
+ #
+ # 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 IPv4 pool
+ 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 client IPv6 pool
+ conf.set_level(base_path + ['network-settings', 'client-ipv6-pool'])
+ if conf.exists(['prefix']):
+ for prefix in conf.list_nodes(['prefix']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': '64'
+ }
+
+ if conf.exists(['prefix', prefix, 'mask']):
+ tmp['mask'] = conf.return_value(['prefix', prefix, 'mask'])
+
+ sstp['client_ipv6_pool'].append(tmp)
+
+ if conf.exists(['delegate']):
+ for prefix in conf.list_nodes(['delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['delegate', prefix, 'delegation-prefix'])
+
+ sstp['client_ipv6_delegate_prefix'].append(tmp)
+
+ #
+ # read in network settings
+ conf.set_level(base_path + ['network-settings'])
+ if conf.exists(['name-server']):
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ sstp['dnsv4'].append(name_server)
+ else:
+ sstp['dnsv6'].append(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 local auth mode requires local users to be configured!')
+
+ for user in sstp['local_users']:
+ username = user['name']
+ if not user['password']:
+ raise ConfigError(f'Password required for local user "{username}"')
+
+ # 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 local user "{username}"')
+
+ if user['download'] and not user['upload']:
+ raise ConfigError(f'Upload speed value required for local user "{username}"')
+
+ 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('Not more then two IPv4 DNS name-servers can be configured')
+
+ # check ipv6
+ if sstp['client_ipv6_delegate_prefix'] and not sstp['client_ipv6_pool']:
+ raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix')
+
+ for prefix in sstp['client_ipv6_delegate_prefix']:
+ if not prefix['mask']:
+ raise ConfigError('Delegation-prefix required for individual delegated networks')
+
+ 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']):
+ file = sstp['ssl_ca']
+ raise ConfigError(f'SSL CA certificate file "{file}" does not exist')
+
+ if not os.path.exists(sstp['ssl_cert']):
+ file = sstp['ssl_cert']
+ raise ConfigError(f'SSL public key file "{file}" does not exist')
+
+ if not os.path.exists(sstp['ssl_key']):
+ file = sstp['ssl_key']
+ raise ConfigError(f'SSL private key file "{file}" 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']:
+ server = radius['server']
+ raise ConfigError(f'Missing RADIUS secret key for server "{ server }"')
+
+def generate(sstp):
+ if not sstp:
+ return None
+
+ # accel-cmd reload doesn't work so any change results in a restart of the daemon
+ render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp, trim_blocks=True)
+
+ if sstp['local_users']:
+ render(sstp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', sstp, trim_blocks=True)
+ os.chmod(sstp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+ else:
+ if os.path.exists(sstp_chap_secrets):
+ os.unlink(sstp_chap_secrets)
+
+ return sstp
+
+def apply(sstp):
+ if not sstp:
+ call('systemctl stop accel-ppp@sstp.service')
+ for file in [sstp_chap_secrets, sstp_conf]:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ return None
+
+ call('systemctl restart accel-ppp@sstp.service')
+
+
+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..56ca813ff
--- /dev/null
+++ b/src/conf_mode/vrf.py
@@ -0,0 +1,269 @@
+#!/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 json import loads
+
+from vyos.config import Config
+from vyos.configdict import list_diff
+from vyos.ifconfig import Interface
+from vyos.util import read_file, cmd
+from vyos import ConfigError
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+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([], get_first_key=True)
+ 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):
+ render(config_file, 'vrf/vrf.conf.tmpl', vrf_config)
+ 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'ip -4 route del vrf {name} unreachable default metric 4278198272')
+ _cmd(f'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
new file mode 100755
index 000000000..292eb0c78
--- /dev/null
+++ b/src/conf_mode/vrrp.py
@@ -0,0 +1,256 @@
+#!/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 sys import exit
+from ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address
+from json import dumps
+from pathlib import Path
+
+import vyos.config
+
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos.ifconfig.vrrp import VRRP
+
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ vrrp_groups = []
+ sync_groups = []
+
+ config = vyos.config.Config()
+
+ # Get the VRRP groups
+ for group_name in config.list_nodes("high-availability vrrp group"):
+ config.set_level("high-availability vrrp group {0}".format(group_name))
+
+ # Retrieve the values
+ group = {"preempt": True, "use_vmac": False, "disable": False}
+
+ if config.exists("disable"):
+ group["disable"] = True
+
+ group["name"] = group_name
+ group["vrid"] = config.return_value("vrid")
+ group["interface"] = config.return_value("interface")
+ group["description"] = config.return_value("description")
+ group["advertise_interval"] = config.return_value("advertise-interval")
+ group["priority"] = config.return_value("priority")
+ group["hello_source"] = config.return_value("hello-source-address")
+ group["peer_address"] = config.return_value("peer-address")
+ group["sync_group"] = config.return_value("sync-group")
+ group["preempt_delay"] = config.return_value("preempt-delay")
+ group["virtual_addresses"] = config.return_values("virtual-address")
+
+ group["auth_password"] = config.return_value("authentication password")
+ group["auth_type"] = config.return_value("authentication type")
+
+ group["health_check_script"] = config.return_value("health-check script")
+ group["health_check_interval"] = config.return_value("health-check interval")
+ group["health_check_count"] = config.return_value("health-check failure-count")
+
+ group["master_script"] = config.return_value("transition-script master")
+ group["backup_script"] = config.return_value("transition-script backup")
+ group["fault_script"] = config.return_value("transition-script fault")
+ group["stop_script"] = config.return_value("transition-script stop")
+
+ if config.exists("no-preempt"):
+ group["preempt"] = False
+ if config.exists("rfc3768-compatibility"):
+ group["use_vmac"] = True
+
+ # Substitute defaults where applicable
+ if not group["advertise_interval"]:
+ group["advertise_interval"] = 1
+ if not group["priority"]:
+ group["priority"] = 100
+ if not group["preempt_delay"]:
+ group["preempt_delay"] = 0
+ if not group["health_check_interval"]:
+ group["health_check_interval"] = 60
+ if not group["health_check_count"]:
+ group["health_check_count"] = 3
+
+ # FIXUP: translate our option for auth type to keepalived's syntax
+ # for simplicity
+ if group["auth_type"]:
+ if group["auth_type"] == "plaintext-password":
+ group["auth_type"] = "PASS"
+ else:
+ group["auth_type"] = "AH"
+
+ vrrp_groups.append(group)
+
+ config.set_level("")
+
+ # Get the sync group used for conntrack-sync
+ conntrack_sync_group = None
+ if config.exists("service conntrack-sync failover-mechanism vrrp"):
+ conntrack_sync_group = config.return_value("service conntrack-sync failover-mechanism vrrp sync-group")
+
+ # Get the sync groups
+ for sync_group_name in config.list_nodes("high-availability vrrp sync-group"):
+ config.set_level("high-availability vrrp sync-group {0}".format(sync_group_name))
+
+ sync_group = {"conntrack_sync": False}
+ sync_group["name"] = sync_group_name
+ sync_group["members"] = config.return_values("member")
+ if conntrack_sync_group:
+ 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(VRRP.location['vyos']), '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
+
+ for group in vrrp_groups:
+ # Check required fields
+ if not group["vrid"]:
+ raise ConfigError("vrid is required but not set in VRRP group {0}".format(group["name"]))
+ if not group["interface"]:
+ raise ConfigError("interface is required but not set in VRRP group {0}".format(group["name"]))
+ if not group["virtual_addresses"]:
+ raise ConfigError("virtual-address is required but not set in VRRP group {0}".format(group["name"]))
+
+ if group["auth_password"] and (not group["auth_type"]):
+ raise ConfigError("authentication type is required but not set in VRRP group {0}".format(group["name"]))
+
+ # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
+
+ # 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: 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 = 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 = 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 = 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 = 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
+ _groups = sorted(vrrp_groups, key=(lambda x: x["interface"]))
+ count = len(_groups) - 1
+ index = 0
+ while (index < count):
+ if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]):
+ raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format(
+ _groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"]))
+ else:
+ index += 1
+
+ # Check sync groups
+ vrrp_group_names = list(map(lambda x: x["name"], vrrp_groups))
+
+ for sync_group in sync_groups:
+ for m in sync_group["members"]:
+ 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):
+ vrrp_groups, sync_groups = data
+
+ # Remove disabled groups from the sync group member lists
+ for sync_group in sync_groups:
+ for member in sync_group["members"]:
+ g = list(filter(lambda x: x["name"] == member, vrrp_groups))[0]
+ 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"] is not True, vrrp_groups))
+
+ render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl',
+ {"groups": vrrp_groups, "sync_groups": sync_groups})
+ render(VRRP.location['daemon'], 'vrrp/daemon.tmpl', {})
+ 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(VRRP.location['vyos']))
+ dict_file.rename(Path(VRRP.location['vyos']))
+ except Exception as err:
+ print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err))
+
+ if not VRRP.is_running():
+ print("Starting the VRRP process")
+ ret = call("systemctl restart keepalived.service")
+ else:
+ print("Reloading the VRRP process")
+ ret = call("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")
+ call("systemctl stop keepalived.service")
+ os.unlink(VRRP.location['daemon'])
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print("VRRP error: {0}".format(str(e)))
+ exit(1)
diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py
new file mode 100755
index 000000000..fb4644d5a
--- /dev/null
+++ b/src/conf_mode/vyos_cert.py
@@ -0,0 +1,144 @@
+#!/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
+import os
+import tempfile
+import pathlib
+import ssl
+
+import vyos.defaults
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import cmd
+
+from vyos import airbag
+airbag.enable()
+
+vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
+
+# XXX: this model will need to be extended for tag nodes
+dependencies = [
+ 'https.py',
+]
+
+def status_self_signed(cert_data):
+# check existence and expiration date
+ path = pathlib.Path(cert_data['conf'])
+ if not path.is_file():
+ return False
+ path = pathlib.Path(cert_data['crt'])
+ if not path.is_file():
+ return False
+ path = pathlib.Path(cert_data['key'])
+ if not path.is_file():
+ return False
+
+ # 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)
+ try:
+ cmd(checkend_cmd, message='Called process error')
+ return True
+ except OSError as err:
+ if err.errno == 1:
+ return False
+ print(err)
+ # XXX: This seems wrong to continue on failure
+ # implicitely returning None
+
+def generate_self_signed(cert_data):
+ san_config = None
+
+ if ssl.OPENSSL_VERSION_INFO < (1, 1, 1, 0, 0):
+ san_config = tempfile.NamedTemporaryFile()
+ with open(san_config.name, 'w') as fd:
+ fd.write('[req]\n')
+ fd.write('distinguished_name=req\n')
+ fd.write('[san]\n')
+ fd.write('subjectAltName=DNS:vyos\n')
+
+ openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
+ '-newkey rsa:4096 -keyout {key} -out {crt} '
+ '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
+ '-extensions san -config {san_conf}'
+ ''.format(san_conf=san_config.name,
+ **cert_data))
+
+ else:
+ openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
+ '-newkey rsa:4096 -keyout {key} -out {crt} '
+ '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
+ '-addext "subjectAltName=DNS:vyos"'
+ ''.format(**cert_data))
+
+ try:
+ 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)
+
+ with open('{conf}'.format(**cert_data), 'w') as f:
+ f.write('ssl_certificate {crt};\n'.format(**cert_data))
+ f.write('ssl_certificate_key {key};\n'.format(**cert_data))
+
+ if san_config:
+ san_config.close()
+
+def get_config():
+ vyos_cert = vyos.defaults.vyos_cert_data
+
+ conf = Config()
+ if not conf.exists('service https certificates system-generated-certificate'):
+ return None
+ else:
+ conf.set_level('service https certificates system-generated-certificate')
+
+ if conf.exists('lifetime'):
+ lifetime = conf.return_value('lifetime')
+ vyos_cert['lifetime'] = lifetime
+
+ return vyos_cert
+
+def verify(vyos_cert):
+ return None
+
+def generate(vyos_cert):
+ if vyos_cert is None:
+ return None
+
+ if not status_self_signed(vyos_cert):
+ generate_self_signed(vyos_cert)
+
+def apply(vyos_cert):
+ for dep in dependencies:
+ command = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
+ cmd(command, raising=ConfigError)
+
+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/etc/dhcp/dhclient-enter-hooks.d/01-vyos-logging b/src/etc/dhcp/dhclient-enter-hooks.d/01-vyos-logging
new file mode 100644
index 000000000..121fb21be
--- /dev/null
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/01-vyos-logging
@@ -0,0 +1,20 @@
+# enable logging
+LOG_ENABLE="yes"
+LOG_STDERR="no"
+LOG_TAG="dhclient-script-vyos"
+
+function logmsg () {
+ # log message to journal
+ case $1 in
+ error) LOG_PRIO="daemon.err" ;;
+ info) LOG_PRIO="daemon.info" ;;
+ esac
+
+ if [ "${LOG_ENABLE}" == "yes" ] ; then
+ if [ "${LOG_STDERR}" == "yes" ] ; then
+ /usr/bin/logger -e --id=$$ -s -p ${LOG_PRIO} -t ${LOG_TAG} "${@:2}"
+ else
+ /usr/bin/logger -e --id=$$ -p ${LOG_PRIO} -t ${LOG_TAG} "${@:2}"
+ fi
+ fi
+}
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient
new file mode 100644
index 000000000..d5d90632c
--- /dev/null
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient
@@ -0,0 +1,27 @@
+# skip all of this if dhclient-script running by stop command defined below
+if [ -z ${CONTROLLED_STOP} ] ; then
+ # stop dhclient for this interface, if it is not current one
+ # get PID for current dhclient
+ current_dhclient=`ps --no-headers --format ppid --pid $$ | awk '{ print $1 }'`
+
+ # get PID for master process (current can be a fork)
+ master_dhclient=`ps --no-headers --format ppid --pid $current_dhclient | awk '{ print $1 }'`
+
+ # get IP version for current dhclient
+ ipversion_arg=`ps --no-headers --format args --pid $current_dhclient | awk '{ print $2 }'`
+
+ # get list of all dhclient running for current interface
+ dhclients_pids=(`ps --no-headers --format pid,args -C dhclient | awk -v IFACE="/sbin/dhclient $ipversion_arg .*$interface$" '$0 ~ IFACE { print $1 }'`)
+
+ logmsg info "Current dhclient PID: $current_dhclient, Parent PID: $master_dhclient, IP version: $ipversion_arg, All dhclients for interface $interface: ${dhclients_pids[@]}"
+ # stop all dhclients for current interface, except current one
+ for dhclient in ${dhclients_pids[@]}; do
+ if ([ $dhclient -ne $current_dhclient ] && [ $dhclient -ne $master_dhclient ]); then
+ logmsg info "Stopping dhclient with PID: ${dhclient}"
+ # get path to PID-file of dhclient process
+ local dhclient_pidfile=`ps --no-headers --format args --pid $dhclient | awk 'match($0, ".*-pf (/.*pid) .*", PF) { print PF[1] }'`
+ # stop dhclient with native command - this will run dhclient-script with correct reason unlike simple kill
+ dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile
+ fi
+ done
+fi
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
new file mode 100644
index 000000000..d1161e704
--- /dev/null
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
@@ -0,0 +1,87 @@
+# redefine ip command to use FRR when it is available
+
+# get status of FRR
+function frr_alive () {
+ /usr/lib/frr/watchfrr.sh all_status
+ if [ "$?" -eq "0" ] ; then
+ logmsg info "FRR status: running"
+ return 0
+ else
+ logmsg info "FRR status: not running"
+ return 1
+ fi
+}
+
+# convert ip route command to vtysh
+function iptovtysh () {
+ # prepare variables for vtysh command
+ local VTYSH_DISTANCE="210"
+ local VTYSH_TAG="210"
+ local VTYSH_NETADDR=""
+ local VTYSH_GATEWAY=""
+ local VTYSH_DEV=""
+ # convert default route to 0.0.0.0/0
+ if [ "$4" == "default" ] ; then
+ VTYSH_NETADDR="0.0.0.0/0"
+ else
+ VTYSH_NETADDR=$4
+ fi
+ # add /32 to ip addresses without netmasks
+ if [[ ! $VTYSH_NETADDR =~ ^.*/[[:digit:]]+$ ]] ; then
+ VTYSH_NETADDR="$VTYSH_NETADDR/32"
+ fi
+ # get gateway address
+ if [ "$5" == "via" ] ; then
+ VTYSH_GATEWAY=$6
+ fi
+ # get device name
+ if [ "$5" == "dev" ]; then
+ VTYSH_DEV=$6
+ elif [ "$7" == "dev" ]; then
+ VTYSH_DEV=$8
+ fi
+
+ # Add route to VRF routing table
+ local VTYSH_VRF_NAME=$(basename /sys/class/net/$VTYSH_DEV/upper_* | sed -e 's/upper_//')
+ if [ -n $VTYSH_VRF_NAME ]; then
+ VTYSH_VRF="vrf $VTYSH_VRF_NAME"
+ fi
+ VTYSH_CMD="ip route $VTYSH_NETADDR $VTYSH_GATEWAY $VTYSH_DEV tag $VTYSH_TAG $VTYSH_DISTANCE $VTYSH_VRF"
+
+ # delete route if the command is "del"
+ if [ "$3" == "del" ] ; then
+ VTYSH_CMD="no $VTYSH_CMD"
+ fi
+ logmsg info "Converted vtysh command: \"$VTYSH_CMD\""
+}
+
+# delete the same route from kernel before adding new one
+function delroute () {
+ logmsg info "Checking if the route presented in kernel: $@"
+ if /usr/sbin/ip route show $@ | grep -qx "$1 " ; then
+ logmsg info "Deleting IP route: \"/usr/sbin/ip route del $@\""
+ /usr/sbin/ip route del $@
+ fi
+}
+
+# replace ip command with this wrapper
+function ip () {
+ # pass comand to system `ip` if this is not related to routes change
+ if [ "$2" != "route" ] ; then
+ logmsg info "Passing command to /usr/sbin/ip: \"$@\""
+ /usr/sbin/ip $@
+ else
+ # if we want to work with routes, try to use FRR first
+ if frr_alive ; then
+ delroute ${@:4}
+ iptovtysh $@
+ logmsg info "Sending command to vtysh"
+ vtysh -c "conf t" -c "$VTYSH_CMD"
+ else
+ # add ip route to kernel
+ logmsg info "Modifying routes in kernel: \"/usr/sbin/ip $@\""
+ /usr/sbin/ip $@
+ fi
+ fi
+}
+
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
new file mode 100644
index 000000000..24090e2a8
--- /dev/null
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
@@ -0,0 +1,44 @@
+# modified make_resolv_conf () for VyOS
+make_resolv_conf() {
+ hostsd_client="/usr/bin/vyos-hostsd-client"
+ hostsd_changes=
+
+ if [ -n "$new_domain_name" ]; then
+ logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-search-domains --tag "dhcp-$interface"
+ logmsg info "Adding domain name \"$new_domain_name\" as search domain with tag \"dhcp-$interface\" via vyos-hostsd-client"
+ $hostsd_client --add-search-domains "$new_domain_name" --tag "dhcp-$interface"
+ hostsd_changes=y
+ fi
+
+ if [ -n "$new_dhcp6_domain_search" ]; then
+ logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-search-domains --tag "dhcpv6-$interface"
+ logmsg info "Adding search domain \"$new_dhcp6_domain_search\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --add-search-domains "$new_dhcp6_domain_search" --tag "dhcpv6-$interface"
+ hostsd_changes=y
+ fi
+
+ if [ -n "$new_domain_name_servers" ]; then
+ logmsg info "Deleting nameservers with tag \"dhcp-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-name-servers --tag "dhcp-$interface"
+ logmsg info "Adding nameservers \"$new_domain_name_servers\" with tag \"dhcp-$interface\" via vyos-hostsd-client"
+ $hostsd_client --add-name-servers $new_domain_name_servers --tag "dhcp-$interface"
+ hostsd_changes=y
+ fi
+
+ if [ -n "$new_dhcp6_name_servers" ]; then
+ logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-name-servers --tag "dhcpv6-$interface"
+ logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface"
+ hostsd_changes=y
+ fi
+
+ if [ $hostsd_changes ]; then
+ logmsg info "Applying changes via vyos-hostsd-client"
+ $hostsd_client --apply
+ else
+ logmsg info "No changes to apply via vyos-hostsd-client"
+ fi
+}
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/05-vyos-mtureplace b/src/etc/dhcp/dhclient-enter-hooks.d/05-vyos-mtureplace
new file mode 100644
index 000000000..4a08765ba
--- /dev/null
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/05-vyos-mtureplace
@@ -0,0 +1,38 @@
+# replace MTU with value from configuration
+
+# get MTU value via Python
+# as configuration is not available to cli-shell-api at the first boot, we must use vyos.config, which contain workaround for this, instead clean shell
+function get_mtu {
+python3 - <<PYEND
+from vyos.config import Config
+import os
+import re
+
+# check if interface is not VLAN and fix name if necessary
+interface_name = os.getenv('interface', '')
+regex_filter = re.compile('^(?P<interface>\w+)\.(?P<vid>\d+)$')
+if regex_filter.search(interface_name):
+ iface = regex_filter.search(interface_name).group('interface')
+ vid = regex_filter.search(interface_name).group('vid')
+ interface_name = "{} vif {}".format(iface, vid)
+
+# initialize config
+config = Config()
+if config.exists('interfaces'):
+ iface_types = config.list_nodes('interfaces')
+ for iface_type in iface_types:
+ # check if configuration contain MTU value for interface and return (print) it
+ if config.exists("interfaces {} {} mtu".format(iface_type, interface_name)):
+ print(format(config.return_value("interfaces {} {} mtu".format(iface_type, interface_name))))
+PYEND
+}
+
+# check if DHCP server return MTU value
+if [ -n "$new_interface_mtu" ]; then
+ # try to get MTU from config and replace original one
+ configured_mtu="$(get_mtu)"
+ if [[ -n $configured_mtu ]] ; then
+ logmsg info "Replacing MTU value for $interface with preconfigured one: $configured_mtu"
+ new_interface_mtu="$configured_mtu"
+ fi
+fi
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
new file mode 100644
index 000000000..b768e1ae5
--- /dev/null
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -0,0 +1,104 @@
+##
+## VyOS cleanup
+##
+# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state
+hostsd_client="/usr/bin/vyos-hostsd-client"
+hostsd_changes=
+
+if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
+ # delete search domains and nameservers via vyos-hostsd
+ logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-search-domains --tag "dhcp-$interface"
+ logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client"
+ $hostsd_client --delete-name-servers --tag "dhcp-${interface}"
+ hostsd_changes=y
+
+ # try to delete default ip route
+ for router in $old_routers; do
+ # check if we are bound to a VRF
+ local vrf_name=$(basename /sys/class/net/${interface}/upper_* | sed -e 's/upper_//')
+ if [ -n $vrf_name ]; then
+ vrf="vrf $vrf_name"
+ fi
+
+ logmsg info "Deleting default route: via $router dev ${interface} ${vrf}"
+ ip -4 route del default via $router dev ${interface} ${vrf}
+ done
+
+ # delete rfc3442 routes
+ if [ -n "$old_rfc3442_classless_static_routes" ]; then
+ set -- $old_rfc3442_classless_static_routes
+ while [ $# -gt 0 ]; do
+ net_length=$1
+ via_arg=''
+ case $net_length in
+ 32|31|30|29|28|27|26|25)
+ if [ $# -lt 9 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.${5}"
+ gateway="${6}.${7}.${8}.${9}"
+ shift 9
+ ;;
+ 24|23|22|21|20|19|18|17)
+ if [ $# -lt 8 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.0"
+ gateway="${5}.${6}.${7}.${8}"
+ shift 8
+ ;;
+ 16|15|14|13|12|11|10|9)
+ if [ $# -lt 7 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.0.0"
+ gateway="${4}.${5}.${6}.${7}"
+ shift 7
+ ;;
+ 8|7|6|5|4|3|2|1)
+ if [ $# -lt 6 ]; then
+ return 1
+ fi
+ net_address="${2}.0.0.0"
+ gateway="${3}.${4}.${5}.${6}"
+ shift 6
+ ;;
+ 0) # default route
+ if [ $# -lt 5 ]; then
+ return 1
+ fi
+ net_address="0.0.0.0"
+ gateway="${2}.${3}.${4}.${5}"
+ shift 5
+ ;;
+ *) # error
+ return 1
+ ;;
+ esac
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ fi
+ # delete route (ip detects host routes automatically)
+ ip -4 route del "${net_address}/${net_length}" \
+ ${via_arg} dev "${interface}" >/dev/null 2>&1
+ done
+ fi
+fi
+
+if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then
+ # delete search domains and nameservers via vyos-hostsd
+ logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-search-domains --tag "dhcpv6-$interface"
+ logmsg info "Deleting nameservers with tag \"dhcpv6-${interface}\" via vyos-hostsd-client"
+ $hostsd_client --delete-name-servers --tag "dhcpv6-${interface}"
+ hostsd_changes=y
+fi
+
+if [ $hostsd_changes ]; then
+ logmsg info "Applying changes via vyos-hostsd-client"
+ $hostsd_client --apply
+else
+ logmsg info "No changes to apply via vyos-hostsd-client"
+fi
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442
new file mode 100644
index 000000000..9202fe72d
--- /dev/null
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442
@@ -0,0 +1,148 @@
+# support for RFC3442 routes in DHCP RENEW
+
+function convert_to_cidr () {
+ cidr=""
+ set -- $1
+ while [ $# -gt 0 ]; do
+ net_length=$1
+
+ case $net_length in
+ 32|31|30|29|28|27|26|25)
+ if [ $# -lt 9 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.${5}"
+ gateway="${6}.${7}.${8}.${9}"
+ shift 9
+ ;;
+ 24|23|22|21|20|19|18|17)
+ if [ $# -lt 8 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.0"
+ gateway="${5}.${6}.${7}.${8}"
+ shift 8
+ ;;
+ 16|15|14|13|12|11|10|9)
+ if [ $# -lt 7 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.0.0"
+ gateway="${4}.${5}.${6}.${7}"
+ shift 7
+ ;;
+ 8|7|6|5|4|3|2|1)
+ if [ $# -lt 6 ]; then
+ return 1
+ fi
+ net_address="${2}.0.0.0"
+ gateway="${3}.${4}.${5}.${6}"
+ shift 6
+ ;;
+ 0) # default route
+ if [ $# -lt 5 ]; then
+ return 1
+ fi
+ net_address="0.0.0.0"
+ gateway="${2}.${3}.${4}.${5}"
+ shift 5
+ ;;
+ *) # error
+ return 1
+ ;;
+ esac
+
+ cidr+="${net_address}/${net_length}:${gateway} "
+ done
+}
+
+# main script starts here
+
+RUN="yes"
+
+if [ "$RUN" = "yes" ]; then
+ convert_to_cidr "$old_rfc3442_classless_static_routes"
+ old_cidr=$cidr
+ convert_to_cidr "$new_rfc3442_classless_static_routes"
+ new_cidr=$cidr
+
+ if [ "$reason" = "RENEW" ]; then
+ if [ "$new_rfc3442_classless_static_routes" != "$old_rfc3442_classless_static_routes" ]; then
+ logmsg info "RFC3442 route change detected, old_routes: $old_rfc3442_classless_static_routes"
+ logmsg info "RFC3442 route change detected, new_routes: $new_rfc3442_classless_static_routes"
+ if [ -z "$new_rfc3442_classless_static_routes" ]; then
+ # delete all routes from the old_rfc3442_classless_static_routes
+ for route in $old_cidr; do
+ network=$(printf "${route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ done
+ elif [ -z "$old_rfc3442_classless_static_routes" ]; then
+ # add all routes from the new_rfc3442_classless_static_routes
+ for route in $new_cidr; do
+ network=$(printf "${route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ done
+ else
+ # update routes
+ # delete old
+ for old_route in $old_cidr; do
+ match="false"
+ for new_route in $new_cidr; do
+ if [[ "$old_route" == "$new_route" ]]; then
+ match="true"
+ break
+ fi
+ done
+ if [[ "$match" == "false" ]]; then
+ # delete old_route
+ network=$(printf "${old_route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${old_route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ fi
+ done
+ # add new
+ for new_route in $new_cidr; do
+ match="false"
+ for old_route in $old_cidr; do
+ if [[ "$new_route" == "$old_route" ]]; then
+ match="true"
+ break
+ fi
+ done
+ if [[ "$match" == "false" ]]; then
+ # add new_route
+ network=$(printf "${new_route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${new_route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ fi
+ done
+ fi
+ fi
+ fi
+fi
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook
new file mode 100644
index 000000000..eeb8b0782
--- /dev/null
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook
@@ -0,0 +1,44 @@
+#!/bin/sh
+
+# Author: Stig Thormodsrud <stig@vyatta.com>
+# Date: 2007
+# Description: dhcp client hook
+
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2006, 2007, 2008 Vyatta, Inc.
+# All Rights Reserved.
+# **** End License ****
+
+# To enable this script set the following variable to "yes"
+RUN="yes"
+
+proto=""
+if [[ $reason =~ (REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6) ]]; then
+ proto="v6"
+fi
+
+if [ "$RUN" = "yes" ]; then
+ LOG=/var/lib/dhcp/dhclient_"$interface"."$proto"lease
+ echo `date` > $LOG
+
+ for i in reason interface new_expiry new_dhcp_lease_time medium \
+ alias_ip_address new_ip_address new_broadcast_address \
+ new_subnet_mask new_domain_name new_network_number \
+ new_domain_name_servers new_routers new_static_routes \
+ new_dhcp_server_identifier new_dhcp_message_type \
+ old_ip_address old_subnet_mask old_domain_name \
+ old_domain_name_servers old_routers \
+ old_static_routes; do
+ echo $i=\'${!i}\' >> $LOG
+ done
+fi
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/sysctl.d/31-vyos-addr_gen_mode.conf b/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf
new file mode 100644
index 000000000..07a0d1584
--- /dev/null
+++ b/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf
@@ -0,0 +1,14 @@
+### Added by vyos-1x ###
+#
+# addr_gen_mode - INTEGER
+# Defines how link-local and autoconf addresses are generated.
+#
+# 0: generate address based on EUI64 (default)
+# 1: do no generate a link-local address, use EUI64 for addresses generated
+# from autoconf
+# 2: generate stable privacy addresses, using the secret from
+# stable_secret (RFC7217)
+# 3: generate stable privacy addresses, using a random secret if unset
+#
+net.ipv6.conf.all.addr_gen_mode = 1
+net.ipv6.conf.default.addr_gen_mode = 1
diff --git a/src/etc/systemd/system/LCDd.service.d/override.conf b/src/etc/systemd/system/LCDd.service.d/override.conf
new file mode 100644
index 000000000..5f3f0dc95
--- /dev/null
+++ b/src/etc/systemd/system/LCDd.service.d/override.conf
@@ -0,0 +1,8 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/LCDd -c /run/LCDd/LCDd.conf
+
diff --git a/src/etc/systemd/system/conserver-server.service.d/override.conf b/src/etc/systemd/system/conserver-server.service.d/override.conf
new file mode 100644
index 000000000..3c753f572
--- /dev/null
+++ b/src/etc/systemd/system/conserver-server.service.d/override.conf
@@ -0,0 +1,10 @@
+[Unit]
+After=
+After=vyos-router.service
+ConditionPathExists=/run/conserver/conserver.cf
+
+[Service]
+Type=simple
+ExecStart=
+ExecStart=/usr/sbin/conserver -M localhost -C /run/conserver/conserver.cf
+Restart=on-failure
diff --git a/src/etc/systemd/system/hostapd@.service.d/override.conf b/src/etc/systemd/system/hostapd@.service.d/override.conf
new file mode 100644
index 000000000..bb8e81d7a
--- /dev/null
+++ b/src/etc/systemd/system/hostapd@.service.d/override.conf
@@ -0,0 +1,10 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=/run/hostapd
+EnvironmentFile=
+ExecStart=
+ExecStart=/usr/sbin/hostapd -B -P /run/hostapd/%i.pid /run/hostapd/%i.conf
+PIDFile=/run/hostapd/%i.pid
diff --git a/src/etc/systemd/system/keepalived.service.d/override.conf b/src/etc/systemd/system/keepalived.service.d/override.conf
new file mode 100644
index 000000000..9fcabf652
--- /dev/null
+++ b/src/etc/systemd/system/keepalived.service.d/override.conf
@@ -0,0 +1,2 @@
+[Service]
+KillMode=process
diff --git a/src/etc/systemd/system/ocserv.service.d/override.conf b/src/etc/systemd/system/ocserv.service.d/override.conf
new file mode 100644
index 000000000..89dbb153f
--- /dev/null
+++ b/src/etc/systemd/system/ocserv.service.d/override.conf
@@ -0,0 +1,14 @@
+[Unit]
+RequiresMountsFor=/run
+ConditionPathExists=/run/ocserv/ocserv.conf
+After=
+After=vyos-router.service
+After=dbus.service
+
+[Service]
+WorkingDirectory=/run/ocserv
+PIDFile=
+PIDFile=/run/ocserv/ocserv.pid
+ExecStart=
+ExecStart=/usr/sbin/ocserv --foreground --pid-file /run/ocserv/ocserv.pid --config /run/ocserv/ocserv.conf
+
diff --git a/src/etc/systemd/system/openvpn@.service.d/override.conf b/src/etc/systemd/system/openvpn@.service.d/override.conf
new file mode 100644
index 000000000..7946484a3
--- /dev/null
+++ b/src/etc/systemd/system/openvpn@.service.d/override.conf
@@ -0,0 +1,9 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=
+WorkingDirectory=/run/openvpn
+ExecStart=
+ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid
diff --git a/src/etc/systemd/system/pdns-recursor.service.d/override.conf b/src/etc/systemd/system/pdns-recursor.service.d/override.conf
new file mode 100644
index 000000000..158bac02b
--- /dev/null
+++ b/src/etc/systemd/system/pdns-recursor.service.d/override.conf
@@ -0,0 +1,8 @@
+[Service]
+WorkingDirectory=
+WorkingDirectory=/run/powerdns
+RuntimeDirectory=
+RuntimeDirectory=powerdns
+RuntimeDirectoryPreserve=yes
+ExecStart=
+ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir=/run/powerdns --socket-dir=/run/powerdns
diff --git a/src/etc/systemd/system/radvd.service.d/override.conf b/src/etc/systemd/system/radvd.service.d/override.conf
new file mode 100644
index 000000000..c2f640cf5
--- /dev/null
+++ b/src/etc/systemd/system/radvd.service.d/override.conf
@@ -0,0 +1,17 @@
+[Unit]
+ConditionPathExists=/run/radvd/radvd.conf
+After=
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=
+WorkingDirectory=/run/radvd
+ExecStartPre=
+ExecStartPre=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf
+ExecStart=
+ExecStart=/usr/sbin/radvd --logmethod stderr_clean --config /run/radvd/radvd.conf --pidfile /run/radvd/radvd.pid
+ExecReload=
+ExecReload=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf
+ExecReload=/bin/kill -HUP $MAINPID
+PIDFile=
+PIDFile=/run/radvd/radvd.pid
diff --git a/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf
new file mode 100644
index 000000000..a895e675f
--- /dev/null
+++ b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf
@@ -0,0 +1,10 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=
+WorkingDirectory=/run/wpa_supplicant
+PIDFile=/run/wpa_supplicant/%I.pid
+ExecStart=
+ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dnl80211,wext -i%I
diff --git a/src/etc/udev/rules.d/90-vyos-serial.rules b/src/etc/udev/rules.d/90-vyos-serial.rules
new file mode 100644
index 000000000..3f10f4924
--- /dev/null
+++ b/src/etc/udev/rules.d/90-vyos-serial.rules
@@ -0,0 +1,28 @@
+# do not edit this file, it will be overwritten on update
+
+ACTION=="remove", GOTO="serial_end"
+SUBSYSTEM!="tty", GOTO="serial_end"
+
+SUBSYSTEMS=="pci", ENV{ID_BUS}="pci", ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{device}"
+SUBSYSTEMS=="pci", IMPORT{builtin}="hwdb --subsystem=pci"
+SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb"
+
+# /dev/serial/by-path/, /dev/serial/by-id/ for USB devices
+KERNEL!="ttyUSB[0-9]*|ttyACM[0-9]*", GOTO="serial_end"
+
+SUBSYSTEMS=="usb-serial", ENV{.ID_PORT}="$attr{port_number}"
+
+IMPORT{builtin}="path_id", IMPORT{builtin}="usb_id"
+
+# Change the name of the usb id to a "more" human redable format.
+#
+# - $env{ID_PATH} usually is a name like: "pci-0000:00:10.0-usb-0:2.3.3.4:1.0-port0" so we strip the "pci-*"
+# portion and only use the usb part
+# - Transform the USB "speach" to the tree like structure so we start with "usb0" as root-complex 0.
+# (tr -d -) does the replacement
+# - Replace the first group after ":" to represent the bus relation (sed -e 0,/:/s//b/) indicated by "b"
+# - Replace the next group after ":" to represent the port relation (sed -e 0,/:/s//p/) indicated by "p"
+ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", PROGRAM="/bin/sh -c 'D=$env{ID_PATH}; echo ${D:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result"
+ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", PROGRAM="/bin/sh -c 'D=$env{ID_PATH}; echo ${D:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result"
+
+LABEL="serial_end"
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
new file mode 100755
index 000000000..dc751c45c
--- /dev/null
+++ b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
@@ -0,0 +1,66 @@
+#!/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 sys
+import syslog as sl
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import run
+
+
+def get_config():
+ c = Config()
+ interfaces = dict()
+ for intf in c.list_effective_nodes('interfaces ethernet'):
+ # 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):
+ continue
+
+ # get addresses configured on the interface
+ intf_addresses = c.return_effective_values(
+ "interfaces ethernet {} address".format(intf)
+ )
+ 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))
+ run(cmd)
+
+ # add configured addresses to interface
+ for addr in addresses:
+ if addr == "dhcp":
+ cmd = ["dhclient", intf]
+ else:
+ cmd = ["ip", "address", "add", addr, "dev", intf]
+ sl.syslog(sl.LOG_NOTICE, " ".join(cmd))
+ run(cmd)
+
+
+if __name__ == '__main__':
+ try:
+ config = get_config()
+ apply(config)
+ 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
new file mode 100755
index 000000000..cc7166c22
--- /dev/null
+++ b/src/helpers/run-config-migration.py
@@ -0,0 +1,86 @@
+#!/usr/bin/python3
+
+# 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 sys
+import argparse
+import datetime
+
+from vyos.util import cmd
+from vyos.migrator import Migrator, VirtualMigrator
+
+def main():
+ argparser = argparse.ArgumentParser(
+ formatter_class=argparse.RawTextHelpFormatter)
+ argparser.add_argument('config_file', type=str,
+ help="configuration file to migrate")
+ argparser.add_argument('--force', action='store_true',
+ help="Force calling of all migration scripts.")
+ argparser.add_argument('--set-vintage', type=str,
+ choices=['vyatta', 'vyos'],
+ help="Set the format for the config version footer in config"
+ " file:\n"
+ "set to 'vyatta':\n"
+ "(for '/* === vyatta-config-version ... */' format)\n"
+ "or 'vyos':\n"
+ "(for '// vyos-config-version ...' format).")
+ argparser.add_argument('--virtual', action='store_true',
+ help="Update the format of the trailing comments in"
+ " config file,\nfrom 'vyatta' to 'vyos'; no migration"
+ " scripts are run.")
+ args = argparser.parse_args()
+
+ config_file_name = args.config_file
+ force_on = args.force
+ vintage = args.set_vintage
+ virtual = args.virtual
+
+ if not os.access(config_file_name, os.R_OK):
+ print("Read error: {}.".format(config_file_name))
+ sys.exit(1)
+
+ if not os.access(config_file_name, os.W_OK):
+ print("Write error: {}.".format(config_file_name))
+ sys.exit(1)
+
+ separator = "."
+ backup_file_name = separator.join([config_file_name,
+ '{0:%Y-%m-%d-%H%M%S}'.format(datetime.datetime.now()),
+ 'pre-migration'])
+
+ cmd(f'cp -p {config_file_name} {backup_file_name}')
+
+ if not virtual:
+ 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:
+ virtual_migration = VirtualMigrator(config_file_name,
+ set_vintage=vintage)
+
+ virtual_migration.run()
+
+ if not virtual_migration.config_changed():
+ os.remove(backup_file_name)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py
new file mode 100755
index 000000000..c33e41d79
--- /dev/null
+++ b/src/helpers/system-versions-foot.py
@@ -0,0 +1,39 @@
+#!/usr/bin/python3
+
+# 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 sys
+import vyos.formatversions as formatversions
+import vyos.systemversions as systemversions
+import vyos.defaults
+import vyos.version
+
+sys_versions = systemversions.get_system_versions()
+
+component_string = formatversions.format_versions_string(sys_versions)
+
+os_version_string = vyos.version.get_version()
+
+sys.stdout.write("\n\n")
+if vyos.defaults.cfg_vintage == 'vyos':
+ formatversions.write_vyos_versions_foot(None, component_string,
+ os_version_string)
+elif vyos.defaults.cfg_vintage == 'vyatta':
+ formatversions.write_vyatta_versions_foot(None, component_string,
+ os_version_string)
+else:
+ formatversions.write_vyatta_versions_foot(None, component_string,
+ os_version_string)
diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py
new file mode 100755
index 000000000..c5bf22f10
--- /dev/null
+++ b/src/helpers/vyos-boot-config-loader.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
+import sys
+import pwd
+import grp
+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:
+ LOG_DIR = '/var/log/vyatta'
+
+LOG_FILE = LOG_DIR + '/vyos-boot-config-loader.log'
+
+try:
+ with open('/proc/cmdline', 'r') as f:
+ 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))
+
+def write_config_status(status):
+ try:
+ with open(STATUS_FILE, 'w') as f:
+ f.write('{0}\n'.format(status))
+ except Exception as e:
+ print('{0}'.format(e))
+
+def trace_to_file(trace_file_name):
+ try:
+ with open(trace_file_name, 'w') as trace_file:
+ traceback.print_exc(file=trace_file)
+ except Exception as e:
+ print('{0}'.format(e))
+
+def failsafe(config_file_name):
+ fail_msg = """
+ !!!!!
+ There were errors loading the configuration
+ Please examine the errors in
+ {0}
+ and correct
+ !!!!!
+ """.format(TRACE_FILE)
+
+ print(fail_msg, file=sys.stderr)
+
+ users = [x[0] for x in pwd.getpwall()]
+ if 'vyos' in users:
+ return
+
+ try:
+ with open(config_file_name, 'r') as f:
+ config_file = f.read()
+ except Exception as e:
+ print("Catastrophic: no default config file "
+ "'{0}'".format(config_file_name))
+ sys.exit(1)
+
+ config = ConfigTree(config_file)
+ if not config.exists(['system', 'login', 'user', 'vyos',
+ 'authentication', 'encrypted-password']):
+ print("No password entry in default config file;")
+ print("unable to recover password for user 'vyos'.")
+ sys.exit(1)
+ else:
+ passwd = config.return_value(['system', 'login', 'user', 'vyos',
+ 'authentication',
+ 'encrypted-password'])
+
+ cmd(f"useradd -s /bin/bash -G 'users,sudo' -m -N -p '{passwd}' vyos")
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Must specify boot config file.")
+ sys.exit(1)
+ else:
+ file_name = sys.argv[1]
+
+ # Set user and group options, so that others will be able to commit
+ # Currently, the only caller does 'sg CFG_GROUP', but that may change
+ cfg_group = grp.getgrnam(CFG_GROUP)
+ os.setgid(cfg_group.gr_gid)
+
+ # Need to set file permissions to 775 so that every vyattacfg group
+ # member has write access to the running config
+ os.umask(0o002)
+
+ session = ConfigSession(os.getpid(), 'vyos-boot-config-loader')
+ env = session.get_session_env()
+
+ default_file_name = env['vyatta_sysconfdir'] + '/config.boot.default'
+
+ try:
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+ except Exception:
+ write_config_status(1)
+ if trace_config:
+ failsafe(default_file_name)
+ trace_to_file(TRACE_FILE)
+ sys.exit(1)
+
+ try:
+ time_begin_load = datetime.now()
+ load_out = session.load_config(file_name)
+ time_end_load = datetime.now()
+ time_begin_commit = datetime.now()
+ commit_out = session.commit()
+ time_end_commit = datetime.now()
+ write_config_status(0)
+ except ConfigSessionError:
+ # 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)
+ if trace_config:
+ failsafe(default_file_name)
+ trace_to_file(TRACE_FILE)
+ sys.exit(1)
+
+ time_elapsed_load = time_end_load - time_begin_load
+ time_elapsed_commit = time_end_commit - time_begin_commit
+
+ try:
+ if not os.path.exists(LOG_DIR):
+ os.mkdir(LOG_DIR)
+ with open(LOG_FILE, 'a') as f:
+ f.write('\n\n')
+ f.write('{0} Begin config load\n'
+ ''.format(time_begin_load))
+ f.write(load_out)
+ f.write('{0} End config load\n'
+ ''.format(time_end_load))
+ f.write('Elapsed time for config load: {0}\n'
+ ''.format(time_elapsed_load))
+ f.write('{0} Begin config commit\n'
+ ''.format(time_begin_commit))
+ f.write(commit_out)
+ f.write('{0} End config commit\n'
+ ''.format(time_end_commit))
+ f.write('Elapsed time for config commit: {0}\n'
+ ''.format(time_elapsed_commit))
+ except Exception as e:
+ print('{0}'.format(e))
diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py
new file mode 100755
index 000000000..097d28d85
--- /dev/null
+++ b/src/helpers/vyos-bridge-sync.py
@@ -0,0 +1,51 @@
+#!/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/>.
+#
+
+# Script is used to synchronize configured bridge interfaces.
+# one can add a non existing interface to a bridge group (e.g. VLAN)
+# but the vlan interface itself does yet not exist. It should be added
+# to the bridge automatically once it's available
+
+import argparse
+from sys import exit
+from time import sleep
+
+from vyos.config import Config
+from vyos.util import cmd, run
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-i', '--interface', action='store', help='Interface name which should be added to bridge it is configured for', required=True)
+ args, unknownargs = parser.parse_known_args()
+
+ conf = Config()
+ if not conf.list_nodes('interfaces bridge'):
+ # no bridge interfaces exist .. bail out early
+ exit(0)
+ else:
+ 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:
+ command = 'brctl addif "{}" "{}"'.format(bridge, args.interface)
+ # let interfaces etc. settle - especially required for OpenVPN bridged interfaces
+ sleep(4)
+ # 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
new file mode 100755
index 000000000..c2da1bb11
--- /dev/null
+++ b/src/helpers/vyos-load-config.py
@@ -0,0 +1,90 @@
+#!/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/>.
+#
+#
+
+"""Load config file from within config session.
+Config file specified by URI or path (without scheme prefix).
+Example: load https://somewhere.net/some.config
+ or
+ load /tmp/some.config
+"""
+
+import sys
+import tempfile
+import vyos.defaults
+import vyos.remote
+from vyos.configsource import ConfigSourceSession, VyOSError
+from vyos.migrator import Migrator, VirtualMigrator, MigratorError
+
+class LoadConfig(ConfigSourceSession):
+ """A subclass for calling 'loadFile'.
+ This does not belong in configsource.py, and only has a single caller.
+ """
+ def load_config(self, path):
+ return self._run(['/bin/cli-shell-api','loadFile',path])
+
+
+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:
+ sys.exit("No config file by that name.")
+else:
+ canonical_path = '{0}/{1}'.format(configdir, file_name)
+ try:
+ with open(canonical_path, 'r') as f:
+ config_file = f.read()
+ except OSError as err1:
+ try:
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+ except OSError as err2:
+ sys.exit('{0}\n{1}'.format(err1, err2))
+
+config = LoadConfig()
+
+print("Loading configuration from '{}'".format(file_name))
+
+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()
+ except MigratorError as err:
+ sys.exit('{}'.format(err))
+
+ try:
+ config.load_config(fp.name)
+ except VyOSError as err:
+ sys.exit('{}'.format(err))
+
+if config.session_changed():
+ print("Load complete. Use 'commit' to make changes effective.")
+else:
+ print("No configuration changes to commit.")
diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py
new file mode 100755
index 000000000..14df2734b
--- /dev/null
+++ b/src/helpers/vyos-merge-config.py
@@ -0,0 +1,111 @@
+#!/usr/bin/python3
+
+# 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 sys
+import os
+import tempfile
+import vyos.defaults
+import vyos.remote
+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):
+ print("Need config file name to merge.")
+ print("Usage: merge <config file> [config path]")
+ sys.exit(0)
+
+file_name = sys.argv[1]
+
+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:
+ sys.exit("No config file by that name.")
+else:
+ canonical_path = "{0}/{1}".format(configdir, file_name)
+ first_err = None
+ try:
+ with open(canonical_path, 'r') as f:
+ config_file = f.read()
+ except Exception as err:
+ first_err = err
+ try:
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+ except Exception as err:
+ print(first_err)
+ print(err)
+ sys.exit(1)
+
+with tempfile.NamedTemporaryFile() as file_to_migrate:
+ with open(file_to_migrate.name, 'w') as fd:
+ fd.write(config_file)
+
+ virtual_migration = VirtualMigrator(file_to_migrate.name)
+ virtual_migration.run()
+
+ migration = Migrator(file_to_migrate.name)
+ migration.run()
+
+ 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()
+effective_config_tree = effective_config._running_config
+
+effective_cmds = effective_config_tree.to_commands()
+merge_cmds = merge_config_tree.to_commands()
+
+effective_cmd_list = effective_cmds.splitlines()
+merge_cmd_list = merge_cmds.splitlines()
+
+effective_cmd_set = set(effective_cmd_list)
+add_cmds = [ cmd for cmd in merge_cmd_list if cmd not in effective_cmd_set ]
+
+path = None
+if (len(sys.argv) > 2):
+ path = sys.argv[2:]
+ if (not effective_config_tree.exists(path) and not
+ merge_config_tree.exists(path)):
+ print("path {} does not exist in either effective or merge"
+ " config; will use root.".format(path))
+ path = None
+ else:
+ path = " ".join(path)
+
+if path:
+ add_cmds = [ cmd for cmd in add_cmds if path in cmd ]
+
+for add in add_cmds:
+ try:
+ 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.")
+else:
+ print("No configuration changes to commit.")
diff --git a/src/helpers/vyos-sudo.py b/src/helpers/vyos-sudo.py
new file mode 100755
index 000000000..3e4c196d9
--- /dev/null
+++ b/src/helpers/vyos-sudo.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+
+# 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 sys
+
+from vyos.util import is_admin
+
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print('Missing command argument')
+ sys.exit(1)
+
+ if not is_admin():
+ print('This account is not authorized to run this command')
+ sys.exit(1)
+
+ os.execvp('sudo', ['sudo'] + sys.argv[1:])
diff --git a/src/migration-scripts/config-management/0-to-1 b/src/migration-scripts/config-management/0-to-1
new file mode 100755
index 000000000..344359110
--- /dev/null
+++ b/src/migration-scripts/config-management/0-to-1
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+
+# Add commit-revisions option if it doesn't exist
+
+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 config.exists(['system', 'config-management', 'commit-revisions']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ config.set(['system', 'config-management', 'commit-revisions'], value='200')
+
+ 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/dhcp-relay/1-to-2 b/src/migration-scripts/dhcp-relay/1-to-2
new file mode 100755
index 000000000..b72da1028
--- /dev/null
+++ b/src/migration-scripts/dhcp-relay/1-to-2
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+
+# Delete "set service dhcp-relay relay-options port" option
+# Delete "set service dhcpv6-relay listen-port" option
+
+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(['service', 'dhcp-relay', 'relay-options', 'port']) or config.exists(['service', 'dhcpv6-relay', 'listen-port'])):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Delete abandoned node
+ config.delete(['service', 'dhcp-relay', 'relay-options', 'port'])
+ # Delete abandoned node
+ config.delete(['service', 'dhcpv6-relay', 'listen-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))
+ sys.exit(1)
diff --git a/src/migration-scripts/dhcp-server/4-to-5 b/src/migration-scripts/dhcp-server/4-to-5
new file mode 100755
index 000000000..313b5279a
--- /dev/null
+++ b/src/migration-scripts/dhcp-server/4-to-5
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+
+# Removes boolean operator from:
+# - "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 ip-forwarding enable (true|false)"
+# - "set service dhcp-server shared-network-name <xyz> authoritative (true|false)"
+# - "set service dhcp-server disabled (true|false)"
+
+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(['service', 'dhcp-server']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ base = ['service', 'dhcp-server']
+ # Make node "set service dhcp-server dynamic-dns-update enable (true|false)" valueless
+ if config.exists(base + ['dynamic-dns-update']):
+ bool_val = config.return_value(base + ['dynamic-dns-update', 'enable'])
+
+ # Delete the node with the old syntax
+ config.delete(base + ['dynamic-dns-update'])
+ if str(bool_val) == 'true':
+ # Enable dynamic-dns-update with new syntax
+ config.set(base + ['dynamic-dns-update'], value=None)
+
+ # Make node "set service dhcp-server disabled (true|false)" valueless
+ if config.exists(base + ['disabled']):
+ bool_val = config.return_value(base + ['disabled'])
+
+ # Delete the node with the old syntax
+ config.delete(base + ['disabled'])
+ if str(bool_val) == 'true':
+ # Now disable DHCP server with the new syntax
+ config.set(base + ['disable'], value=None)
+
+ # Make node "set service dhcp-server hostfile-update (enable|disable) valueless
+ if config.exists(base + ['hostfile-update']):
+ bool_val = config.return_value(base + ['hostfile-update'])
+
+ # Delete the node with the old syntax incl. all subnodes
+ config.delete(base + ['hostfile-update'])
+ if str(bool_val) == 'enable':
+ # Enable hostfile update with new syntax
+ config.set(base + ['hostfile-update'], value=None)
+
+ # Run this for every instance if 'shared-network-name'
+ for network in config.list_nodes(base + ['shared-network-name']):
+ base_network = base + ['shared-network-name', network]
+ # format as tag node to avoid loading problems
+ config.set_tag(base + ['shared-network-name'])
+
+ # Run this for every specified 'subnet'
+ for subnet in config.list_nodes(base_network + ['subnet']):
+ base_subnet = base_network + ['subnet', subnet]
+ # format as tag node to avoid loading problems
+ config.set_tag(base_network + ['subnet'])
+
+ # Make node "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 ip-forwarding enable" valueless
+ if config.exists(base_subnet + ['ip-forwarding', 'enable']):
+ bool_val = config.return_value(base_subnet + ['ip-forwarding', 'enable'])
+ # Delete the node with the old syntax
+ config.delete(base_subnet + ['ip-forwarding'])
+ if str(bool_val) == 'true':
+ # Recreate node with new syntax
+ config.set(base_subnet + ['ip-forwarding'], value=None)
+
+ # Rename node "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 start <172.16.0.4> stop <172.16.0.9>
+ if config.exists(base_subnet + ['start']):
+ # This is the new "range" id for DHCP lease ranges
+ r_id = 0
+ for range in config.list_nodes(base_subnet + ['start']):
+ range_start = range
+ range_stop = config.return_value(base_subnet + ['start', range_start, 'stop'])
+
+ # Delete the node with the old syntax
+ config.delete(base_subnet + ['start', range_start])
+
+ # Create the node for the new syntax
+ # Note: range is a tag node, counter is its child, not a value
+ config.set(base_subnet + ['range', r_id])
+ config.set(base_subnet + ['range', r_id, 'start'], value=range_start)
+ config.set(base_subnet + ['range', r_id, 'stop'], value=range_stop)
+
+ # format as tag node to avoid loading problems
+ config.set_tag(base_subnet + ['range'])
+
+ # increment range id for possible next range definition
+ r_id += 1
+
+ # Delete the node with the old syntax
+ config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'subnet', subnet, 'start'])
+
+
+ # Make node "set service dhcp-server shared-network-name <xyz> authoritative" valueless
+ if config.exists(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']):
+ authoritative = config.return_value(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative'])
+
+ # Delete the node with the old syntax
+ config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative'])
+
+ # Recreate node with new syntax - if required
+ if authoritative == "enable":
+ config.set(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative'])
+
+ 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/dhcpv6-server/0-to-1 b/src/migration-scripts/dhcpv6-server/0-to-1
new file mode 100755
index 000000000..6f1150da1
--- /dev/null
+++ b/src/migration-scripts/dhcpv6-server/0-to-1
@@ -0,0 +1,61 @@
+#!/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/>.
+
+# combine both sip-server-address and sip-server-name nodes to common sip-server
+
+from sys import argv, exit
+from vyos.configtree import ConfigTree
+
+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 = ['service', 'dhcpv6-server', 'shared-network-name']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+ # we need to run this for every configured network
+ for network in config.list_nodes(base):
+ for subnet in config.list_nodes(base + [network, 'subnet']):
+ sip_server = []
+
+ # Do we have 'sip-server-address' configured?
+ if config.exists(base + [network, 'subnet', subnet, 'sip-server-address']):
+ sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-address'])
+ config.delete(base + [network, 'subnet', subnet, 'sip-server-address'])
+
+ # Do we have 'sip-server-name' configured?
+ if config.exists(base + [network, 'subnet', subnet, 'sip-server-name']):
+ sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-name'])
+ config.delete(base + [network, 'subnet', subnet, 'sip-server-name'])
+
+ # Write new CLI value for sip-server
+ for server in sip_server:
+ config.set(base + [network, 'subnet', subnet, 'sip-server'], value=server, replace=False)
+
+ 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/dns-forwarding/0-to-1 b/src/migration-scripts/dns-forwarding/0-to-1
new file mode 100755
index 000000000..6e8720eef
--- /dev/null
+++ b/src/migration-scripts/dns-forwarding/0-to-1
@@ -0,0 +1,50 @@
+#!/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/>.
+#
+
+# This migration script will check if there is a allow-from directive configured
+# for the dns forwarding service - if not, the node will be created with the old
+# default values of 0.0.0.0/0 and ::/0
+
+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)
+
+base = ['service', 'dns', 'forwarding']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ if not config.exists(base + ['allow-from']):
+ config.set(base + ['allow-from'], value='0.0.0.0/0', replace=False)
+ config.set(base + ['allow-from'], value='::/0', replace=False)
+
+ 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/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2
new file mode 100755
index 000000000..8c4f4b5c7
--- /dev/null
+++ b/src/migration-scripts/dns-forwarding/1-to-2
@@ -0,0 +1,83 @@
+#!/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/>.
+#
+
+# This migration script will remove the deprecated 'listen-on' statement
+# from the dns forwarding service and will add the corresponding
+# listen-address nodes instead. This is required as PowerDNS can only listen
+# on interface addresses and not on interface names.
+
+from ipaddress import ip_interface
+from sys import argv, exit
+from vyos.ifconfig import Interface
+from vyos.configtree import ConfigTree
+
+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 = ['service', 'dns', 'forwarding']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+if config.exists(base + ['listen-on']):
+ listen_intf = config.return_values(base + ['listen-on'])
+ # Delete node with abandoned command
+ config.delete(base + ['listen-on'])
+
+ # retrieve interface addresses for every configured listen-on interface
+ listen_addr = []
+ for intf in listen_intf:
+ # we need to evaluate the interface section before manipulating the 'intf' variable
+ section = Interface.section(intf)
+ if not section:
+ raise ValueError(f'Invalid interface name {intf}')
+
+ # we need to treat vif and vif-s interfaces differently,
+ # both "real interfaces" use dots for vlan identifiers - those
+ # need to be exchanged with vif and vif-s identifiers
+ if intf.count('.') == 1:
+ # this is a regular VLAN interface
+ intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1]
+ elif intf.count('.') == 2:
+ # this is a QinQ VLAN interface
+ intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2]
+
+ # retrieve corresponding interface addresses in CIDR format
+ # those need to be converted in pure IP addresses without network information
+ path = ['interfaces', section, intf, 'address']
+ for addr in config.return_values(path):
+ listen_addr.append( ip_interface(addr).ip )
+
+ for addr in listen_addr:
+ config.set(base + ['listen-address'], value=addr, replace=False)
+
+ 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)
+
+exit(0)
diff --git a/src/migration-scripts/dns-forwarding/2-to-3 b/src/migration-scripts/dns-forwarding/2-to-3
new file mode 100755
index 000000000..01e445b22
--- /dev/null
+++ b/src/migration-scripts/dns-forwarding/2-to-3
@@ -0,0 +1,51 @@
+#!/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/>.
+#
+
+# Sets the new options "addnta" and "recursion-desired" for all
+# 'dns forwarding domain' as this is usually desired
+
+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)
+
+base = ['service', 'dns', 'forwarding']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+if config.exists(base + ['domain']):
+ for domain in config.list_nodes(base + ['domain']):
+ domain_base = base + ['domain', domain]
+ config.set(domain_base + ['addnta'])
+ config.set(domain_base + ['recursion-desired'])
+
+ 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/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/0-to-1 b/src/migration-scripts/interfaces/0-to-1
new file mode 100755
index 000000000..ee4d6b82c
--- /dev/null
+++ b/src/migration-scripts/interfaces/0-to-1
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+
+# Change syntax of bridge interface
+# - move interface based bridge-group to actual bridge (de-nest)
+# - make stp and igmp-snooping nodes valueless
+# https://phabricator.vyos.net/T1556
+
+import sys
+from vyos.configtree import ConfigTree
+
+def migrate_bridge(config, tree, intf):
+ # check if bridge-group exists
+ tree_bridge = tree + ['bridge-group']
+ if config.exists(tree_bridge):
+ bridge = config.return_value(tree_bridge + ['bridge'])
+ # create new bridge member interface
+ config.set(base + [bridge, 'member', 'interface', intf])
+ # format as tag node to avoid loading problems
+ config.set_tag(base + [bridge, 'member', 'interface'])
+
+ # cost: migrate if configured
+ tree_cost = tree + ['bridge-group', 'cost']
+ if config.exists(tree_cost):
+ cost = config.return_value(tree_cost)
+ # set new node
+ config.set(base + [bridge, 'member', 'interface', intf, 'cost'], value=cost)
+
+ # priority: migrate if configured
+ tree_priority = tree + ['bridge-group', 'priority']
+ if config.exists(tree_priority):
+ priority = config.return_value(tree_priority)
+ # set new node
+ config.set(base + [bridge, 'member', 'interface', intf, 'priority'], value=priority)
+
+ # Delete the old bridge-group assigned to an interface
+ config.delete(tree_bridge)
+
+
+if __name__ == '__main__':
+ 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 = ['interfaces', 'bridge']
+
+ if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+ else:
+ #
+ # make stp and igmp-snooping nodes valueless
+ #
+ for br in config.list_nodes(base):
+ # STP: check if enabled
+ if config.exists(base + [br, 'stp']):
+ stp_val = config.return_value(base + [br, 'stp'])
+ # STP: delete node with old syntax
+ config.delete(base + [br, 'stp'])
+ # STP: set new node - if enabled
+ if stp_val == "true":
+ config.set(base + [br, 'stp'], value=None)
+
+ # igmp-snooping: check if enabled
+ if config.exists(base + [br, 'igmp-snooping', 'querier']):
+ igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier'])
+ # igmp-snooping: delete node with old syntax
+ config.delete(base + [br, 'igmp-snooping', 'querier'])
+ # igmp-snooping: set new node - if enabled
+ if igmp_val == "enable":
+ config.set(base + [br, 'igmp', 'querier'], value=None)
+
+ #
+ # move interface based bridge-group to actual bridge (de-nest)
+ #
+ bridge_types = ['bonding', 'ethernet', 'l2tpv3', 'openvpn', 'vxlan', 'wireless']
+ for type in bridge_types:
+ if not config.exists(['interfaces', type]):
+ continue
+
+ for interface in config.list_nodes(['interfaces', type]):
+ # check if bridge-group exists
+ bridge_group = ['interfaces', type, interface]
+ if config.exists(bridge_group + ['bridge-group']):
+ migrate_bridge(config, bridge_group, interface)
+
+ # We also need to migrate VLAN interfaces
+ vlan_base = ['interfaces', type, interface, 'vif']
+ if config.exists(vlan_base):
+ for vlan in config.list_nodes(vlan_base):
+ intf = "{}.{}".format(interface, vlan)
+ migrate_bridge(config, vlan_base + [vlan], intf)
+
+ # And then we have service VLANs (vif-s) interfaces
+ vlan_base = ['interfaces', type, interface, 'vif-s']
+ if config.exists(vlan_base):
+ for vif_s in config.list_nodes(vlan_base):
+ intf = "{}.{}".format(interface, vif_s)
+ migrate_bridge(config, vlan_base + [vif_s], intf)
+
+ # Every service VLAN can have multiple customer VLANs (vif-c)
+ vlan_c = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c']
+ if config.exists(vlan_c):
+ for vif_c in config.list_nodes(vlan_c):
+ intf = "{}.{}.{}".format(interface, vif_s, vif_c)
+ migrate_bridge(config, vlan_c + [vif_c], intf)
+
+ 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/1-to-2 b/src/migration-scripts/interfaces/1-to-2
new file mode 100755
index 000000000..050137318
--- /dev/null
+++ b/src/migration-scripts/interfaces/1-to-2
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+
+# Change syntax of bond interface
+# - move interface based bond-group to actual bond (de-nest)
+# https://phabricator.vyos.net/T1614
+
+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)
+base = ['interfaces', 'bonding']
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ #
+ # move interface based bond-group to actual bond (de-nest)
+ #
+ for intf in config.list_nodes(['interfaces', 'ethernet']):
+ # check if bond-group exists
+ if config.exists(['interfaces', 'ethernet', intf, 'bond-group']):
+ # get configured bond interface
+ bond = config.return_value(['interfaces', 'ethernet', intf, 'bond-group'])
+ # delete old interface asigned (nested) bond group
+ config.delete(['interfaces', 'ethernet', intf, 'bond-group'])
+ # create new bond member interface
+ config.set(base + [bond, 'member', 'interface'], value=intf, replace=False)
+
+ #
+ # some combinations were allowed in the past from a CLI perspective
+ # but the kernel overwrote them - remove from CLI to not confuse the users.
+ # In addition new consitency checks are in place so users can't repeat the
+ # mistake. One of those nice issues is https://phabricator.vyos.net/T532
+ for bond in config.list_nodes(base):
+ if config.exists(base + [bond, 'arp-monitor', 'interval']) and config.exists(base + [bond, 'mode']):
+ mode = config.return_value(base + [bond, 'mode'])
+ if mode in ['802.3ad', 'transmit-load-balance', 'adaptive-load-balance']:
+ intvl = int(config.return_value(base + [bond, 'arp-monitor', 'interval']))
+ if intvl > 0:
+ # this is not allowed and the linux kernel replies with:
+ # option arp_interval: mode dependency failed, not supported in mode 802.3ad(4)
+ # option arp_interval: mode dependency failed, not supported in mode balance-alb(6)
+ # option arp_interval: mode dependency failed, not supported in mode balance-tlb(5)
+ #
+ # so we simply disable arp_interval by setting it to 0 and miimon will take care about the link
+ config.set(base + [bond, 'arp-monitor', 'interval'], value='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/interfaces/10-to-11 b/src/migration-scripts/interfaces/10-to-11
new file mode 100755
index 000000000..6b8e49ed9
--- /dev/null
+++ b/src/migration-scripts/interfaces/10-to-11
@@ -0,0 +1,55 @@
+#!/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 WWAN (wirelessmodem) serial interface from non persistent ttyUSB2 to
+# a bus like name, e.g. "usb0b1.3p1.3"
+
+import os
+
+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', 'wirelessmodem']
+ if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+ for wwan in config.list_nodes(base):
+ if config.exists(base + [wwan, 'device']):
+ device = config.return_value(base + [wwan, 'device'])
+
+ for root, dirs, files in os.walk('/dev/serial/by-bus'):
+ for file in files:
+ device_file = os.path.realpath(os.path.join(root, file))
+ if os.path.basename(device_file) == device:
+ config.set(base + [wwan, 'device'], value=file, replace=True)
+
+ 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/11-to-12 b/src/migration-scripts/interfaces/11-to-12
new file mode 100755
index 000000000..0dad24642
--- /dev/null
+++ b/src/migration-scripts/interfaces/11-to-12
@@ -0,0 +1,58 @@
+#!/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 'dhcpv6-options prefix-delegation' from single node to a new tag node
+# 'dhcpv6-options pd 0'
+# - delete 'sla-len' from CLI - value is calculated on demand
+
+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 type in config.list_nodes(['interfaces']):
+ for interface in config.list_nodes(['interfaces', type]):
+ # cache current config tree
+ base_path = ['interfaces', type, interface, 'dhcpv6-options']
+ old_base = base_path + ['prefix-delegation']
+ new_base = base_path + ['pd']
+ if config.exists(old_base):
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(old_base, new_base + ['0'])
+ config.delete(old_base)
+
+ for pd in config.list_nodes(new_base):
+ for tmp in config.list_nodes(new_base + [pd, 'interface']):
+ sla_config = new_base + [pd, 'interface', tmp, 'sla-len']
+ if config.exists(sla_config):
+ config.delete(sla_config)
+
+ 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/2-to-3 b/src/migration-scripts/interfaces/2-to-3
new file mode 100755
index 000000000..a63a54cdf
--- /dev/null
+++ b/src/migration-scripts/interfaces/2-to-3
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+
+# Change syntax of openvpn encryption settings
+# - move cipher from encryption to encryption cipher
+# https://phabricator.vyos.net/T1704
+
+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)
+base = ['interfaces', 'openvpn']
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ #
+ # move cipher from "encryption" to "encryption cipher"
+ #
+ for intf in config.list_nodes(['interfaces', 'openvpn']):
+ # Check if encryption is set
+ if config.exists(['interfaces', 'openvpn', intf, 'encryption']):
+ # Get cipher used
+ cipher = config.return_value(['interfaces', 'openvpn', intf, 'encryption'])
+ # Delete old syntax
+ config.delete(['interfaces', 'openvpn', intf, 'encryption'])
+ # Add new syntax to config
+ config.set(['interfaces', 'openvpn', intf, 'encryption', 'cipher'], value=cipher)
+ 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/3-to-4 b/src/migration-scripts/interfaces/3-to-4
new file mode 100755
index 000000000..e3bd25a68
--- /dev/null
+++ b/src/migration-scripts/interfaces/3-to-4
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+
+# Change syntax of wireless interfaces
+# Migrate boolean nodes to valueless
+
+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)
+base = ['interfaces', 'wireless']
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ for wifi in config.list_nodes(base):
+ # as converting a node to bool is always the same, we can script it
+ to_bool_nodes = ['capabilities ht 40MHz-incapable',
+ 'capabilities ht auto-powersave',
+ 'capabilities ht delayed-block-ack',
+ 'capabilities ht dsss-cck-40',
+ 'capabilities ht greenfield',
+ 'capabilities ht ldpc',
+ 'capabilities ht lsig-protection',
+ 'capabilities ht stbc tx',
+ 'capabilities require-ht',
+ 'capabilities require-vht',
+ 'capabilities vht antenna-pattern-fixed',
+ 'capabilities vht ldpc',
+ 'capabilities vht stbc tx',
+ 'capabilities vht tx-powersave',
+ 'capabilities vht vht-cf',
+ 'expunge-failing-stations',
+ 'isolate-stations']
+
+ for node in to_bool_nodes:
+ if config.exists(base + [wifi, node]):
+ tmp = config.return_value(base + [wifi, node])
+ # delete old node
+ config.delete(base + [wifi, node])
+ # set new node if it was enabled
+ if tmp == 'true':
+ # OLD CLI used camel casing in 40MHz-incapable which is
+ # not supported in the new backend. Convert all to lower-case
+ config.set(base + [wifi, node.lower()])
+
+ # Remove debug node
+ if config.exists(base + [wifi, 'debug']):
+ config.delete(base + [wifi, 'debug'])
+
+ # RADIUS servers
+ if config.exists(base + [wifi, 'security', 'wpa', 'radius-server']):
+ for server in config.list_nodes(base + [wifi, 'security', 'wpa', 'radius-server']):
+ base_server = base + [wifi, 'security', 'wpa', 'radius-server', server]
+
+ # Migrate RADIUS shared secret
+ if config.exists(base_server + ['secret']):
+ key = config.return_value(base_server + ['secret'])
+ # write new configuration node
+ config.set(base + [wifi, 'security', 'wpa', 'radius', 'server', server, 'key'], value=key)
+ # format as tag node
+ config.set_tag(base + [wifi, 'security', 'wpa', 'radius', 'server'])
+
+ # Migrate RADIUS port
+ if config.exists(base_server + ['port']):
+ port = config.return_value(base_server + ['port'])
+ # write new configuration node
+ config.set(base + [wifi, 'security', 'wpa', 'radius', 'server', server, 'port'], value=port)
+ # format as tag node
+ config.set_tag(base + [wifi, 'security', 'wpa', 'radius', 'server'])
+
+ # Migrate RADIUS accounting
+ if config.exists(base_server + ['accounting']):
+ port = config.return_value(base_server + ['accounting'])
+ # write new configuration node
+ config.set(base + [wifi, 'security', 'wpa', 'radius', 'server', server, 'accounting'])
+ # format as tag node
+ config.set_tag(base + [wifi, 'security', 'wpa', 'radius', 'server'])
+
+ # delete old radius-server nodes
+ config.delete(base + [wifi, 'security', 'wpa', 'radius-server'])
+
+ 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/4-to-5 b/src/migration-scripts/interfaces/4-to-5
new file mode 100755
index 000000000..2a42c60ff
--- /dev/null
+++ b/src/migration-scripts/interfaces/4-to-5
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+
+# De-nest PPPoE interfaces
+# Migrate boolean nodes to valueless
+
+import sys
+from vyos.configtree import ConfigTree
+
+def migrate_dialer(config, tree, intf):
+ for pppoe in config.list_nodes(tree):
+ # assemble string, 0 -> pppoe0
+ new_base = ['interfaces', 'pppoe']
+ pppoe_base = new_base + ['pppoe' + pppoe]
+ config.set(new_base)
+ # format as tag node to avoid loading problems
+ config.set_tag(new_base)
+
+ # Copy the entire old node to the new one before migrating individual
+ # parts
+ config.copy(tree + [pppoe], pppoe_base)
+
+ # Instead of letting the user choose between auto and none
+ # where auto is default, it makes more sesne to just offer
+ # an option to disable the default behavior (declutter CLI)
+ if config.exists(pppoe_base + ['name-server']):
+ tmp = config.return_value(pppoe_base + ['name-server'])
+ if tmp == "none":
+ config.set(pppoe_base + ['no-peer-dns'])
+ config.delete(pppoe_base + ['name-server'])
+
+ # Migrate user-id and password nodes under an 'authentication'
+ # node
+ if config.exists(pppoe_base + ['user-id']):
+ user = config.return_value(pppoe_base + ['user-id'])
+ config.set(pppoe_base + ['authentication', 'user'], value=user)
+ config.delete(pppoe_base + ['user-id'])
+
+ if config.exists(pppoe_base + ['password']):
+ pwd = config.return_value(pppoe_base + ['password'])
+ config.set(pppoe_base + ['authentication', 'password'], value=pwd)
+ config.delete(pppoe_base + ['password'])
+
+ # remove enable-ipv6 node and rather place it under ipv6 node
+ if config.exists(pppoe_base + ['enable-ipv6']):
+ config.set(pppoe_base + ['ipv6', 'enable'])
+ config.delete(pppoe_base + ['enable-ipv6'])
+
+ # Source interface migration
+ config.set(pppoe_base + ['source-interface'], value=intf)
+
+ # Remove IPv6 router-advert nodes as this makes no sense on a
+ # client diale rinterface to send RAs back into the network
+ # https://phabricator.vyos.net/T2055
+ ipv6_ra = pppoe_base + ['ipv6', 'router-advert']
+ if config.exists(ipv6_ra):
+ config.delete(ipv6_ra)
+
+
+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)
+ pppoe_links = ['bonding', 'ethernet']
+
+ for link_type in pppoe_links:
+ if not config.exists(['interfaces', link_type]):
+ continue
+
+ for interface in config.list_nodes(['interfaces', link_type]):
+ # check if PPPoE exists
+ base_if = ['interfaces', link_type, interface]
+ pppoe_if = base_if + ['pppoe']
+ if config.exists(pppoe_if):
+ for dialer in config.list_nodes(pppoe_if):
+ migrate_dialer(config, pppoe_if, interface)
+
+ # Delete old PPPoE interface
+ config.delete(pppoe_if)
+
+ # bail out early if there are no VLAN interfaces to migrate
+ if not config.exists(base_if + ['vif']):
+ continue
+
+ # Migrate PPPoE interfaces attached to a VLAN
+ for vlan in config.list_nodes(base_if + ['vif']):
+ vlan_if = base_if + ['vif', vlan]
+ pppoe_if = vlan_if + ['pppoe']
+ if config.exists(pppoe_if):
+ for dialer in config.list_nodes(pppoe_if):
+ intf = "{}.{}".format(interface, vlan)
+ migrate_dialer(config, pppoe_if, intf)
+
+ # Delete old PPPoE interface
+ config.delete(pppoe_if)
+
+ # Add interface description that this is required for PPPoE
+ if not config.exists(vlan_if + ['description']):
+ config.set(vlan_if + ['description'], value='PPPoE link 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))
+ 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..1291751d8
--- /dev/null
+++ b/src/migration-scripts/interfaces/5-to-6
@@ -0,0 +1,123 @@
+#!/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):
+ if c.exists(prefix_base + [prefix, 'autonomous-flag']):
+ tmp = c.return_value(prefix_base + [prefix, 'autonomous-flag'])
+ c.delete(prefix_base + [prefix, 'autonomous-flag'])
+ if tmp == 'false':
+ c.set(prefix_base + [prefix, 'no-autonomous-flag'])
+
+ if c.exists(prefix_base + [prefix, 'on-link-flag']):
+ tmp = c.return_value(prefix_base + [prefix, 'on-link-flag'])
+ c.delete(prefix_base + [prefix, 'on-link-flag'])
+ if tmp == 'true':
+ c.set(prefix_base + [prefix, 'on-link-flag'])
+
+ # 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..a4051301f
--- /dev/null
+++ b/src/migration-scripts/interfaces/7-to-8
@@ -0,0 +1,76 @@
+#!/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
+
+import os
+
+from sys import exit, argv
+from vyos.configtree import ConfigTree
+from vyos.util import chown, chmod_750
+
+def migrate_default_keys():
+ kdir = r'/config/auth/wireguard'
+ 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')
+
+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']
+
+ migrate_default_keys()
+
+ 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..2d1efd418
--- /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
+ continue
+
+ # 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/interfaces/9-to-10 b/src/migration-scripts/interfaces/9-to-10
new file mode 100755
index 000000000..4aa2c42b5
--- /dev/null
+++ b/src/migration-scripts/interfaces/9-to-10
@@ -0,0 +1,64 @@
+#!/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 CLI node 'dhcpv6-options delgate' to 'dhcpv6-options prefix-delegation
+# interface'
+# - rename CLI node 'interface-id' for prefix-delegation to 'address' as it
+# represents the local interface IPv6 address assigned by DHCPv6-PD
+
+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 intf_type in config.list_nodes(['interfaces']):
+ for intf in config.list_nodes(['interfaces', intf_type]):
+ # cache current config tree
+ base_path = ['interfaces', intf_type, intf, 'dhcpv6-options',
+ 'delegate']
+
+ if config.exists(base_path):
+ # cache new config tree
+ new_path = ['interfaces', intf_type, intf, 'dhcpv6-options',
+ 'prefix-delegation']
+ if not config.exists(new_path):
+ config.set(new_path)
+
+ # copy to new node
+ config.copy(base_path, new_path + ['interface'])
+
+ # rename interface-id to address
+ for interface in config.list_nodes(new_path + ['interface']):
+ config.rename(new_path + ['interface', interface, 'interface-id'], 'address')
+
+ # delete old noe
+ config.delete(base_path)
+
+ 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/ipoe-server/0-to-1 b/src/migration-scripts/ipoe-server/0-to-1
new file mode 100755
index 000000000..f328ebced
--- /dev/null
+++ b/src/migration-scripts/ipoe-server/0-to-1
@@ -0,0 +1,133 @@
+#!/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 primary/secondary identifier from nameserver
+# - Unifi RADIUS configuration by placing it all under "authentication radius" node
+
+import os
+import sys
+
+from sys import argv, exit
+from vyos.configtree import ConfigTree
+
+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 = ['service', 'ipoe-server']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+
+ # Migrate IPv4 DNS servers
+ dns_base = base + ['dns-server']
+ if config.exists(dns_base):
+ for server in ['server-1', 'server-2']:
+ if config.exists(dns_base + [server]):
+ dns = config.return_value(dns_base + [server])
+ config.set(base + ['name-server'], value=dns, replace=False)
+
+ config.delete(dns_base)
+
+ # Migrate IPv6 DNS servers
+ dns_base = base + ['dnsv6-server']
+ if config.exists(dns_base):
+ for server in ['server-1', 'server-2', 'server-3']:
+ if config.exists(dns_base + [server]):
+ dns = config.return_value(dns_base + [server])
+ config.set(base + ['name-server'], value=dns, replace=False)
+
+ config.delete(dns_base)
+
+ # Migrate radius-settings node to RADIUS and use this as base for the
+ # later migration of the RADIUS servers - this will save a lot of code
+ radius_settings = base + ['authentication', 'radius-settings']
+ if config.exists(radius_settings):
+ config.rename(radius_settings, 'radius')
+
+ # Migrate RADIUS dynamic author / change of authorisation server
+ dae_old = base + ['authentication', 'radius', 'dae-server']
+ if config.exists(dae_old):
+ config.rename(dae_old, 'dynamic-author')
+ dae_new = base + ['authentication', 'radius', 'dynamic-author']
+
+ if config.exists(dae_new + ['ip-address']):
+ config.rename(dae_new + ['ip-address'], 'server')
+
+ if config.exists(dae_new + ['secret']):
+ config.rename(dae_new + ['secret'], 'key')
+
+ # Migrate RADIUS server
+ radius_server = base + ['authentication', 'radius-server']
+ if config.exists(radius_server):
+ new_base = base + ['authentication', 'radius', 'server']
+ config.set(new_base)
+ config.set_tag(new_base)
+ for server in config.list_nodes(radius_server):
+ old_base = radius_server + [server]
+ config.copy(old_base, new_base + [server])
+
+ # migrate key
+ if config.exists(new_base + [server, 'secret']):
+ config.rename(new_base + [server, 'secret'], 'key')
+
+ # remove old req-limit node
+ if config.exists(new_base + [server, 'req-limit']):
+ config.delete(new_base + [server, 'req-limit'])
+
+ config.delete(radius_server)
+
+ # Migrate IPv6 prefixes
+ ipv6_base = base + ['client-ipv6-pool']
+ if config.exists(ipv6_base + ['prefix']):
+ prefix_old = config.return_values(ipv6_base + ['prefix'])
+ # delete old prefix CLI nodes
+ config.delete(ipv6_base + ['prefix'])
+ # create ned prefix tag node
+ config.set(ipv6_base + ['prefix'])
+ config.set_tag(ipv6_base + ['prefix'])
+
+ for p in prefix_old:
+ prefix = p.split(',')[0]
+ mask = p.split(',')[1]
+ config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask)
+
+ if config.exists(ipv6_base + ['delegate-prefix']):
+ prefix_old = config.return_values(ipv6_base + ['delegate-prefix'])
+ # delete old delegate prefix CLI nodes
+ config.delete(ipv6_base + ['delegate-prefix'])
+ # create ned delegation tag node
+ config.set(ipv6_base + ['delegate'])
+ config.set_tag(ipv6_base + ['delegate'])
+
+ for p in prefix_old:
+ prefix = p.split(',')[0]
+ mask = p.split(',')[1]
+ config.set(ipv6_base + ['delegate', prefix, 'delegation-prefix'], value=mask)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/ipsec/4-to-5 b/src/migration-scripts/ipsec/4-to-5
new file mode 100755
index 000000000..b64aa8462
--- /dev/null
+++ b/src/migration-scripts/ipsec/4-to-5
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+
+# log-modes have changed, keyword all to any
+
+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()
+
+ctree = ConfigTree(config_file)
+
+if not ctree.exists(['vpn', 'ipsec', 'logging','log-modes']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ lmodes = ctree.return_values(['vpn', 'ipsec', 'logging','log-modes'])
+ for mode in lmodes:
+ if mode == 'all':
+ ctree.set(['vpn', 'ipsec', 'logging','log-modes'], value='any', replace=True)
+
+ try:
+ open(file_name,'w').write(ctree.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/l2tp/0-to-1 b/src/migration-scripts/l2tp/0-to-1
new file mode 100755
index 000000000..686ebc655
--- /dev/null
+++ b/src/migration-scripts/l2tp/0-to-1
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+
+# Unclutter L2TP VPN configuiration - move radius-server top level tag
+# nodes to a regular node which now also configures the radius source address
+# used when querying a radius server
+
+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)
+
+cfg_base = ['vpn', 'l2tp', 'remote-access', 'authentication']
+if not config.exists(cfg_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Migrate "vpn l2tp authentication radius-source-address" to new
+ # "vpn l2tp authentication radius source-address"
+ if config.exists(cfg_base + ['radius-source-address']):
+ address = config.return_value(cfg_base + ['radius-source-address'])
+ # delete old configuration node
+ config.delete(cfg_base + ['radius-source-address'])
+ # write new configuration node
+ config.set(cfg_base + ['radius', 'source-address'], value=address)
+
+ # Migrate "vpn l2tp authentication radius-server" tag node to new
+ # "vpn l2tp authentication radius server" tag node
+ if config.exists(cfg_base + ['radius-server']):
+ for server in config.list_nodes(cfg_base + ['radius-server']):
+ base_server = cfg_base + ['radius-server', server]
+ key = config.return_value(base_server + ['key'])
+
+ # delete old configuration node
+ config.delete(base_server)
+ # write new configuration node
+ config.set(cfg_base + ['radius', 'server', server, 'key'], value=key)
+
+ # format as tag node
+ config.set_tag(cfg_base + ['radius', 'server'])
+
+ # delete top level tag node
+ if config.exists(cfg_base + ['radius-server']):
+ config.delete(cfg_base + ['radius-server'])
+
+ 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/l2tp/1-to-2 b/src/migration-scripts/l2tp/1-to-2
new file mode 100755
index 000000000..c46eba8f8
--- /dev/null
+++ b/src/migration-scripts/l2tp/1-to-2
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+
+# Delete depricated outside-nexthop address
+
+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)
+
+cfg_base = ['vpn', 'l2tp', 'remote-access']
+if not config.exists(cfg_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ if config.exists(cfg_base + ['outside-nexthop']):
+ config.delete(cfg_base + ['outside-nexthop'])
+
+ 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/l2tp/2-to-3 b/src/migration-scripts/l2tp/2-to-3
new file mode 100755
index 000000000..3472ee3ed
--- /dev/null
+++ b/src/migration-scripts/l2tp/2-to-3
@@ -0,0 +1,111 @@
+#!/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 primary/secondary identifier from nameserver
+# - TODO: remove radius server req-limit
+
+import os
+import sys
+
+from sys import argv, exit
+from vyos.configtree import ConfigTree
+
+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 = ['vpn', 'l2tp', 'remote-access']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+
+ # Migrate IPv4 DNS servers
+ dns_base = base + ['dns-servers']
+ if config.exists(dns_base):
+ for server in ['server-1', 'server-2']:
+ if config.exists(dns_base + [server]):
+ dns = config.return_value(dns_base + [server])
+ config.set(base + ['name-server'], value=dns, replace=False)
+
+ config.delete(dns_base)
+
+ # Migrate IPv6 DNS servers
+ dns_base = base + ['dnsv6-servers']
+ if config.exists(dns_base):
+ for server in config.return_values(dns_base):
+ config.set(base + ['name-server'], value=server, replace=False)
+
+ config.delete(dns_base)
+
+ # Migrate IPv4 WINS servers
+ wins_base = base + ['wins-servers']
+ if config.exists(wins_base):
+ for server in ['server-1', 'server-2']:
+ if config.exists(wins_base + [server]):
+ wins = config.return_value(wins_base + [server])
+ config.set(base + ['wins-server'], value=wins, replace=False)
+
+ config.delete(wins_base)
+
+
+ # Remove RADIUS server req-limit node
+ radius_base = base + ['authentication', 'radius']
+ if config.exists(radius_base):
+ for server in config.list_nodes(radius_base + ['server']):
+ if config.exists(radius_base + ['server', server, 'req-limit']):
+ config.delete(radius_base + ['server', server, 'req-limit'])
+
+ # Migrate IPv6 prefixes
+ ipv6_base = base + ['client-ipv6-pool']
+ if config.exists(ipv6_base + ['prefix']):
+ prefix_old = config.return_values(ipv6_base + ['prefix'])
+ # delete old prefix CLI nodes
+ config.delete(ipv6_base + ['prefix'])
+ # create ned prefix tag node
+ config.set(ipv6_base + ['prefix'])
+ config.set_tag(ipv6_base + ['prefix'])
+
+ for p in prefix_old:
+ prefix = p.split(',')[0]
+ mask = p.split(',')[1]
+ config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask)
+
+ if config.exists(ipv6_base + ['delegate-prefix']):
+ prefix_old = config.return_values(ipv6_base + ['delegate-prefix'])
+ # delete old delegate prefix CLI nodes
+ config.delete(ipv6_base + ['delegate-prefix'])
+ # create ned delegation tag node
+ config.set(ipv6_base + ['delegate'])
+ config.set_tag(ipv6_base + ['delegate'])
+
+ for p in prefix_old:
+ prefix = p.split(',')[0]
+ mask = p.split(',')[1]
+ config.set(ipv6_base + ['delegate', prefix, 'delegate-prefix'], value=mask)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/lldp/0-to-1 b/src/migration-scripts/lldp/0-to-1
new file mode 100755
index 000000000..5f66570e7
--- /dev/null
+++ b/src/migration-scripts/lldp/0-to-1
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+
+# Delete "set service lldp interface <interface> location civic-based" option
+# as it was broken most of the time anyways
+
+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)
+base = ['service', 'lldp', 'interface']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Delete nodes with abandoned CLI syntax
+ for interface in config.list_nodes(base):
+ if config.exists(base + [interface, 'location', 'civic-based']):
+ config.delete(base + [interface, 'location', 'civic-based'])
+
+ 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/nat/4-to-5 b/src/migration-scripts/nat/4-to-5
new file mode 100755
index 000000000..dda191719
--- /dev/null
+++ b/src/migration-scripts/nat/4-to-5
@@ -0,0 +1,58 @@
+#!/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/>.
+
+# Drop the enable/disable from the nat "log" node. If log node is specified
+# it is "enabled"
+
+from sys import argv,exit
+from vyos.configtree import ConfigTree
+
+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)
+
+if not config.exists(['nat']):
+ # Nothing to do
+ exit(0)
+else:
+ for direction in ['source', 'destination']:
+ if not config.exists(['nat', direction]):
+ continue
+
+ for rule in config.list_nodes(['nat', direction, 'rule']):
+ base = ['nat', direction, 'rule', rule]
+
+ # Check if the log node exists and if log is enabled,
+ # migrate it to the new valueless 'log' node
+ if config.exists(base + ['log']):
+ tmp = config.return_value(base + ['log'])
+ config.delete(base + ['log'])
+ if tmp == 'enable':
+ config.set(base + ['log'])
+
+ 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/ntp/0-to-1 b/src/migration-scripts/ntp/0-to-1
new file mode 100755
index 000000000..9c66f3109
--- /dev/null
+++ b/src/migration-scripts/ntp/0-to-1
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+
+# Delete "set system ntp server <n> dynamic" option
+
+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(['system', 'ntp']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Delete abandoned leaf node if found inside tag node for
+ # "set system ntp server <n> dynamic"
+ base = ['system', 'ntp', 'server']
+ for server in config.list_nodes(base):
+ if config.exists(base + [server, 'dynamic']):
+ config.delete(base + [server, 'dynamic'])
+
+ 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/pppoe-server/0-to-1 b/src/migration-scripts/pppoe-server/0-to-1
new file mode 100755
index 000000000..bb24211b6
--- /dev/null
+++ b/src/migration-scripts/pppoe-server/0-to-1
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+# Convert "service pppoe-server authentication radius-server node key"
+# to:
+# "service pppoe-server authentication radius-server node secret"
+
+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()
+
+ctree = ConfigTree(config_file)
+
+
+if not ctree.exists(['service', 'pppoe-server', 'authentication','radius-server']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ nodes = ctree.list_nodes(['service', 'pppoe-server', 'authentication','radius-server'])
+ for node in nodes:
+ if ctree.exists(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'key']):
+ val = ctree.return_value(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'key'])
+ ctree.set(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'secret'], value=val, replace=False)
+ ctree.delete(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'key'])
+ try:
+ open(file_name,'w').write(ctree.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/pppoe-server/1-to-2 b/src/migration-scripts/pppoe-server/1-to-2
new file mode 100755
index 000000000..fa83896d3
--- /dev/null
+++ b/src/migration-scripts/pppoe-server/1-to-2
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+
+# Convert "service pppoe-server interface ethX"
+# to:
+# "service pppoe-server interface ethX {}"
+
+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()
+
+ctree = ConfigTree(config_file)
+cbase = ['service', 'pppoe-server','interface']
+
+if not ctree.exists(cbase):
+ sys.exit(0)
+else:
+ nics = ctree.return_values(cbase)
+ # convert leafNode to a tagNode
+ ctree.set(cbase)
+ ctree.set_tag(cbase)
+ for nic in nics:
+ ctree.set(cbase + [nic])
+
+ try:
+ open(file_name,'w').write(ctree.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
+
diff --git a/src/migration-scripts/pppoe-server/2-to-3 b/src/migration-scripts/pppoe-server/2-to-3
new file mode 100755
index 000000000..5f9730a41
--- /dev/null
+++ b/src/migration-scripts/pppoe-server/2-to-3
@@ -0,0 +1,141 @@
+#!/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 primary/secondary identifier from nameserver
+
+import os
+
+from sys import argv, exit
+from vyos.configtree import ConfigTree
+
+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 = ['service', 'pppoe-server']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+
+ # Migrate IPv4 DNS servers
+ dns_base = base + ['dns-servers']
+ if config.exists(dns_base):
+ for server in ['server-1', 'server-2']:
+ if config.exists(dns_base + [server]):
+ dns = config.return_value(dns_base + [server])
+ config.set(base + ['name-server'], value=dns, replace=False)
+
+ config.delete(dns_base)
+
+ # Migrate IPv6 DNS servers
+ dns_base = base + ['dnsv6-servers']
+ if config.exists(dns_base):
+ for server in ['server-1', 'server-2', 'server-3']:
+ if config.exists(dns_base + [server]):
+ dns = config.return_value(dns_base + [server])
+ config.set(base + ['name-server'], value=dns, replace=False)
+
+ config.delete(dns_base)
+
+ # Migrate IPv4 WINS servers
+ wins_base = base + ['wins-servers']
+ if config.exists(wins_base):
+ for server in ['server-1', 'server-2']:
+ if config.exists(wins_base + [server]):
+ wins = config.return_value(wins_base + [server])
+ config.set(base + ['wins-server'], value=wins, replace=False)
+
+ config.delete(wins_base)
+
+ # Migrate radius-settings node to RADIUS and use this as base for the
+ # later migration of the RADIUS servers - this will save a lot of code
+ radius_settings = base + ['authentication', 'radius-settings']
+ if config.exists(radius_settings):
+ config.rename(radius_settings, 'radius')
+
+ # Migrate RADIUS dynamic author / change of authorisation server
+ dae_old = base + ['authentication', 'radius', 'dae-server']
+ if config.exists(dae_old):
+ config.rename(dae_old, 'dynamic-author')
+ dae_new = base + ['authentication', 'radius', 'dynamic-author']
+
+ if config.exists(dae_new + ['ip-address']):
+ config.rename(dae_new + ['ip-address'], 'server')
+
+ if config.exists(dae_new + ['secret']):
+ config.rename(dae_new + ['secret'], 'key')
+
+ # Migrate RADIUS server
+ radius_server = base + ['authentication', 'radius-server']
+ if config.exists(radius_server):
+ new_base = base + ['authentication', 'radius', 'server']
+ config.set(new_base)
+ config.set_tag(new_base)
+ for server in config.list_nodes(radius_server):
+ old_base = radius_server + [server]
+ config.copy(old_base, new_base + [server])
+
+ # migrate key
+ if config.exists(new_base + [server, 'secret']):
+ config.rename(new_base + [server, 'secret'], 'key')
+
+ # remove old req-limit node
+ if config.exists(new_base + [server, 'req-limit']):
+ config.delete(new_base + [server, 'req-limit'])
+
+ config.delete(radius_server)
+
+ # Migrate IPv6 prefixes
+ ipv6_base = base + ['client-ipv6-pool']
+ if config.exists(ipv6_base + ['prefix']):
+ prefix_old = config.return_values(ipv6_base + ['prefix'])
+ # delete old prefix CLI nodes
+ config.delete(ipv6_base + ['prefix'])
+ # create ned prefix tag node
+ config.set(ipv6_base + ['prefix'])
+ config.set_tag(ipv6_base + ['prefix'])
+
+ for p in prefix_old:
+ prefix = p.split(',')[0]
+ mask = p.split(',')[1]
+ config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask)
+
+ if config.exists(ipv6_base + ['delegate-prefix']):
+ prefix_old = config.return_values(ipv6_base + ['delegate-prefix'])
+ # delete old delegate prefix CLI nodes
+ config.delete(ipv6_base + ['delegate-prefix'])
+ # create ned delegation tag node
+ config.set(ipv6_base + ['delegate'])
+ config.set_tag(ipv6_base + ['delegate'])
+
+ for p in prefix_old:
+ prefix = p.split(',')[0]
+ mask = p.split(',')[1]
+ config.set(ipv6_base + ['delegate', prefix, 'delegation-prefix'], value=mask)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/pppoe-server/3-to-4 b/src/migration-scripts/pppoe-server/3-to-4
new file mode 100755
index 000000000..ed5a01625
--- /dev/null
+++ b/src/migration-scripts/pppoe-server/3-to-4
@@ -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/>.
+
+# change mppe node to a leaf node with value prefer
+
+import os
+
+from sys import argv, exit
+from vyos.configtree import ConfigTree
+
+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 = ['service', 'pppoe-server']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+ mppe_base = base + ['ppp-options', 'mppe']
+ if config.exists(mppe_base):
+ # drop node first ...
+ config.delete(mppe_base)
+ # ... and set new default
+ config.set(mppe_base, value='prefer')
+
+ print(config.to_string())
+ exit(1)
+
+ 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/pptp/0-to-1 b/src/migration-scripts/pptp/0-to-1
new file mode 100755
index 000000000..d0c7a83b5
--- /dev/null
+++ b/src/migration-scripts/pptp/0-to-1
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+
+# Unclutter PPTP VPN configuiration - move radius-server top level tag
+# nodes to a regular node which now also configures the radius source address
+# used when querying a radius server
+
+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)
+
+cfg_base = ['vpn', 'pptp', 'remote-access', 'authentication']
+if not config.exists(cfg_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Migrate "vpn pptp authentication radius-source-address" to new
+ # "vpn pptp authentication radius source-address"
+ if config.exists(cfg_base + ['radius-source-address']):
+ address = config.return_value(cfg_base + ['radius-source-address'])
+ # delete old configuration node
+ config.delete(cfg_base + ['radius-source-address'])
+ # write new configuration node
+ config.set(cfg_base + ['radius', 'source-address'], value=address)
+
+ # Migrate "vpn pptp authentication radius-server" tag node to new
+ # "vpn pptp authentication radius server" tag node
+ for server in config.list_nodes(cfg_base + ['radius-server']):
+ base_server = cfg_base + ['radius-server', server]
+ key = config.return_value(base_server + ['key'])
+
+ # delete old configuration node
+ config.delete(base_server)
+ # write new configuration node
+ config.set(cfg_base + ['radius', 'server', server, 'key'], value=key)
+
+ # format as tag node
+ config.set_tag(cfg_base + ['radius', 'server'])
+
+ # delete top level tag node
+ if config.exists(cfg_base + ['radius-server']):
+ config.delete(cfg_base + ['radius-server'])
+
+ 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/pptp/1-to-2 b/src/migration-scripts/pptp/1-to-2
new file mode 100755
index 000000000..a13cc3a4f
--- /dev/null
+++ b/src/migration-scripts/pptp/1-to-2
@@ -0,0 +1,71 @@
+#!/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 dns-servers node to common name-servers
+# - remove radios req-limit node
+
+from sys import argv, exit
+
+from vyos.configtree import ConfigTree
+
+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 = ['vpn', 'pptp', 'remote-access']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+ # Migrate IPv4 DNS servers
+ dns_base = base + ['dns-servers']
+ if config.exists(dns_base):
+ for server in ['server-1', 'server-2']:
+ if config.exists(dns_base + [server]):
+ dns = config.return_value(dns_base + [server])
+ config.set(base + ['name-server'], value=dns, replace=False)
+
+ config.delete(dns_base)
+
+ # Migrate IPv4 WINS servers
+ wins_base = base + ['wins-servers']
+ if config.exists(wins_base):
+ for server in ['server-1', 'server-2']:
+ if config.exists(wins_base + [server]):
+ wins = config.return_value(wins_base + [server])
+ config.set(base + ['wins-server'], value=wins, replace=False)
+
+ config.delete(wins_base)
+
+ # Remove RADIUS server req-limit node
+ radius_base = base + ['authentication', 'radius']
+ if config.exists(radius_base):
+ for server in config.list_nodes(radius_base + ['server']):
+ if config.exists(radius_base + ['server', server, 'req-limit']):
+ config.delete(radius_base + ['server', server, 'req-limit'])
+
+ 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/2-to-3 b/src/migration-scripts/quagga/2-to-3
new file mode 100755
index 000000000..4c1cd86a3
--- /dev/null
+++ b/src/migration-scripts/quagga/2-to-3
@@ -0,0 +1,203 @@
+#!/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
+
+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)
+
+def migrate_neighbor(config, neighbor_path, neighbor):
+ if config.exists(neighbor_path):
+ neighbors = config.list_nodes(neighbor_path)
+ for neighbor in neighbors:
+ # Move the valueless options: as-override, next-hop-self, route-reflector-client, route-server-client,
+ # remove-private-as
+ for valueless_option in ['as-override', 'nexthop-self', 'route-reflector-client', 'route-server-client',
+ 'remove-private-as']:
+ if config.exists(neighbor_path + [neighbor, valueless_option]):
+ config.set(neighbor_path + [neighbor] + af_path + [valueless_option])
+ config.delete(neighbor_path + [neighbor, valueless_option])
+
+ # Move filter options: distribute-list, filter-list, prefix-list, and route-map
+ # They share the same syntax inside so we can group them
+ for filter_type in ['distribute-list', 'filter-list', 'prefix-list', 'route-map']:
+ if config.exists(neighbor_path + [neighbor, filter_type]):
+ for filter_dir in ['import', 'export']:
+ if config.exists(neighbor_path + [neighbor, filter_type, filter_dir]):
+ filter_name = config.return_value(neighbor_path + [neighbor, filter_type, filter_dir])
+ config.set(neighbor_path + [neighbor] + af_path + [filter_type, filter_dir], value=filter_name)
+ config.delete(neighbor_path + [neighbor, filter_type])
+
+ # Move simple leaf node options: maximum-prefix, unsuppress-map, weight
+ for leaf_option in ['maximum-prefix', 'unsuppress-map', 'weight']:
+ if config.exists(neighbor_path + [neighbor, leaf_option]):
+ if config.exists(neighbor_path + [neighbor, leaf_option]):
+ leaf_opt_value = config.return_value(neighbor_path + [neighbor, leaf_option])
+ config.set(neighbor_path + [neighbor] + af_path + [leaf_option], value=leaf_opt_value)
+ config.delete(neighbor_path + [neighbor, leaf_option])
+
+ # The rest is special cases, for better or worse
+
+ # Move allowas-in
+ if config.exists(neighbor_path + [neighbor, 'allowas-in']):
+ if config.exists(neighbor_path + [neighbor, 'allowas-in', 'number']):
+ allowas_in = config.return_value(neighbor_path + [neighbor, 'allowas-in', 'number'])
+ config.set(neighbor_path + [neighbor] + af_path + ['allowas-in', 'number'], value=allowas_in)
+ config.delete(neighbor_path + [neighbor, 'allowas-in'])
+
+ # Move attribute-unchanged options
+ if config.exists(neighbor_path + [neighbor, 'attribute-unchanged']):
+ for attr in ['as-path', 'med', 'next-hop']:
+ if config.exists(neighbor_path + [neighbor, 'attribute-unchanged', attr]):
+ config.set(neighbor_path + [neighbor] + af_path + ['attribute-unchanged', attr])
+ config.delete(neighbor_path + [neighbor, 'attribute-unchanged', attr])
+ config.delete(neighbor_path + [neighbor, 'attribute-unchanged'])
+
+ # Move capability options
+ if config.exists(neighbor_path + [neighbor, 'capability']):
+ # "capability dynamic" is a peer-global option, we only migrate ORF
+ if config.exists(neighbor_path + [neighbor, 'capability', 'orf']):
+ if config.exists(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list']):
+ for orf in ['send', 'receive']:
+ if config.exists(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list', orf]):
+ config.set(neighbor_path + [neighbor] + af_path + ['capability', 'orf', 'prefix-list', orf])
+ config.delete(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list', orf])
+ config.delete(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list'])
+ config.delete(neighbor_path + [neighbor, 'capability', 'orf'])
+
+ # Move default-originate
+ if config.exists(neighbor_path + [neighbor, 'default-originate']):
+ if config.exists(neighbor_path + [neighbor, 'default-originate', 'route-map']):
+ route_map = config.return_value(neighbor_path + [neighbor, 'default-originate', 'route-map'])
+ config.set(neighbor_path + [neighbor] + af_path + ['default-originate', 'route-map'], value=route_map)
+ else:
+ # Empty default-originate node is meaningful so we re-create it
+ config.set(neighbor_path + [neighbor] + af_path + ['default-originate'])
+ config.delete(neighbor_path + [neighbor, 'default-originate'])
+
+ # Move soft-reconfiguration
+ if config.exists(neighbor_path + [neighbor, 'soft-reconfiguration']):
+ if config.exists(neighbor_path + [neighbor, 'soft-reconfiguration', 'inbound']):
+ config.set(neighbor_path + [neighbor] + af_path + ['soft-reconfiguration', 'inbound'])
+ # Empty soft-reconfiguration is meaningless, so we just remove it
+ config.delete(neighbor_path + [neighbor, 'soft-reconfiguration'])
+
+ # Move disable-send-community
+ if config.exists(neighbor_path + [neighbor, 'disable-send-community']):
+ for comm_type in ['standard', 'extended']:
+ if config.exists(neighbor_path + [neighbor, 'disable-send-community', comm_type]):
+ config.set(neighbor_path + [neighbor] + af_path + ['disable-send-community', comm_type])
+ config.delete(neighbor_path + [neighbor, 'disable-send-community', comm_type])
+ config.delete(neighbor_path + [neighbor, 'disable-send-community'])
+
+
+if not config.exists(['protocols', 'bgp']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Just to avoid writing it so many times
+ af_path = ['address-family', 'ipv4-unicast']
+
+ # 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]
+ bgp_path = ['protocols', 'bgp', asn]
+ else:
+ # There's actually no BGP, just its empty shell
+ sys.exit(0)
+
+ ## Move global IPv4-specific BGP options to "address-family ipv4-unicast"
+
+ # Move networks
+ network_path = ['protocols', 'bgp', asn, 'network']
+ if config.exists(network_path):
+ config.set(bgp_path + af_path + ['network'])
+ config.set_tag(bgp_path + af_path + ['network'])
+
+ networks = config.list_nodes(network_path)
+ for network in networks:
+ config.set(bgp_path + af_path + ['network', network])
+ if config.exists(network_path + [network, 'route-map']):
+ route_map = config.return_value(network_path + [network, 'route-map'])
+ config.set(bgp_path + af_path + ['network', network, 'route-map'], value=route_map)
+ config.delete(network_path)
+
+ # Move aggregate-address statements
+ aggregate_path = ['protocols', 'bgp', asn, 'aggregate-address']
+ if config.exists(aggregate_path):
+ config.set(bgp_path + af_path + ['aggregate-address'])
+ config.set_tag(bgp_path + af_path + ['aggregate-address'])
+
+ aggregates = config.list_nodes(aggregate_path)
+ for aggregate in aggregates:
+ config.set(bgp_path + af_path + ['aggregate-address', aggregate])
+ if config.exists(aggregate_path + [aggregate, 'as-set']):
+ config.set(bgp_path + af_path + ['aggregate-address', aggregate, 'as-set'])
+ if config.exists(aggregate_path + [aggregate, 'summary-only']):
+ config.set(bgp_path + af_path + ['aggregate-address', aggregate, 'summary-only'])
+ config.delete(aggregate_path)
+
+ ## Migrate neighbor options
+ neighbor_path = ['protocols', 'bgp', asn, 'neighbor']
+ if config.exists(neighbor_path):
+ neighbors = config.list_nodes(neighbor_path)
+ for neighbor in neighbors:
+ migrate_neighbor(config, neighbor_path, neighbor)
+
+ peer_group_path = ['protocols', 'bgp', asn, 'peer-group']
+ if config.exists(peer_group_path):
+ peer_groups = config.list_nodes(peer_group_path)
+ for peer_group in peer_groups:
+ migrate_neighbor(config, peer_group_path, peer_group)
+
+ ## Migrate redistribute statements
+ redistribute_path = ['protocols', 'bgp', asn, 'redistribute']
+ if config.exists(redistribute_path):
+ config.set(bgp_path + af_path + ['redistribute'])
+
+ redistributes = config.list_nodes(redistribute_path)
+ for redistribute in redistributes:
+ config.set(bgp_path + af_path + ['redistribute', redistribute])
+ if config.exists(redistribute_path + [redistribute, 'metric']):
+ redist_metric = config.return_value(redistribute_path + [redistribute, 'metric'])
+ config.set(bgp_path + af_path + ['redistribute', redistribute, 'metric'], value=redist_metric)
+ if config.exists(redistribute_path + [redistribute, 'route-map']):
+ redist_route_map = config.return_value(redistribute_path + [redistribute, 'route-map'])
+ config.set(bgp_path + af_path + ['redistribute', redistribute, 'route-map'], value=redist_route_map)
+
+ config.delete(redistribute_path)
+
+ 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/3-to-4 b/src/migration-scripts/quagga/3-to-4
new file mode 100755
index 000000000..be3528391
--- /dev/null
+++ b/src/migration-scripts/quagga/3-to-4
@@ -0,0 +1,76 @@
+#!/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/>.
+#
+#
+
+# 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
+
+
+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 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)
+
+ # 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/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/salt/0-to-1 b/src/migration-scripts/salt/0-to-1
new file mode 100755
index 000000000..79053c056
--- /dev/null
+++ b/src/migration-scripts/salt/0-to-1
@@ -0,0 +1,58 @@
+#!/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/>.
+
+# Delete log_file, log_level and user nodes
+# rename hash_type to hash
+# rename mine_interval to interval
+
+from sys import argv,exit
+
+from vyos.configtree import ConfigTree
+
+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 = ['service', 'salt-minion']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+
+ # delete nodes which are now populated with sane defaults
+ for node in ['log_file', 'log_level', 'user']:
+ if config.exists(base + [node]):
+ config.delete(base + [node])
+
+ if config.exists(base + ['hash_type']):
+ config.rename(base + ['hash_type'], 'hash')
+
+ if config.exists(base + ['mine_interval']):
+ config.rename(base + ['mine_interval'], 'interval')
+
+ 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/snmp/0-to-1 b/src/migration-scripts/snmp/0-to-1
new file mode 100755
index 000000000..a836f7011
--- /dev/null
+++ b/src/migration-scripts/snmp/0-to-1
@@ -0,0 +1,56 @@
+#!/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)
+config_base = ['service', 'snmp', 'v3']
+
+if not config.exists(config_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # we no longer support a per trap target engine ID (https://phabricator.vyos.net/T818)
+ if config.exists(config_base + ['v3', 'trap-target']):
+ for target in config.list_nodes(config_base + ['v3', 'trap-target']):
+ config.delete(config_base + ['v3', 'trap-target', target, 'engineid'])
+
+ # we no longer support a per user engine ID (https://phabricator.vyos.net/T818)
+ if config.exists(config_base + ['v3', 'user']):
+ for user in config.list_nodes(config_base + ['v3', 'user']):
+ config.delete(config_base + ['v3', 'user', user, 'engineid'])
+
+ # we drop TSM support as there seem to be no users and this code is untested
+ # https://phabricator.vyos.net/T1769
+ if config.exists(config_base + ['v3', 'tsm']):
+ config.delete(config_base + ['v3', 'tsm'])
+
+ 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/snmp/1-to-2 b/src/migration-scripts/snmp/1-to-2
new file mode 100755
index 000000000..466a624e6
--- /dev/null
+++ b/src/migration-scripts/snmp/1-to-2
@@ -0,0 +1,89 @@
+#!/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 sys import argv, exit
+from vyos.configtree import ConfigTree
+
+def migrate_keys(config, path):
+ # authentication: rename node 'encrypted-key' -> 'encrypted-password'
+ config_path_auth = path + ['auth', 'encrypted-key']
+ if config.exists(config_path_auth):
+ config.rename(config_path_auth, 'encrypted-password')
+ config_path_auth = path + ['auth', 'encrypted-password']
+
+ # remove leading '0x' from string if present
+ tmp = config.return_value(config_path_auth)
+ if tmp.startswith(prefix):
+ tmp = tmp.replace(prefix, '')
+ config.set(config_path_auth, value=tmp)
+
+ # privacy: rename node 'encrypted-key' -> 'encrypted-password'
+ config_path_priv = path + ['privacy', 'encrypted-key']
+ if config.exists(config_path_priv):
+ config.rename(config_path_priv, 'encrypted-password')
+ config_path_priv = path + ['privacy', 'encrypted-password']
+
+ # remove leading '0x' from string if present
+ tmp = config.return_value(config_path_priv)
+ if tmp.startswith(prefix):
+ tmp = tmp.replace(prefix, '')
+ config.set(config_path_priv, value=tmp)
+
+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)
+ config_base = ['service', 'snmp', 'v3']
+
+ if not config.exists(config_base):
+ # Nothing to do
+ exit(0)
+ else:
+ # We no longer support hashed values prefixed with '0x' to unclutter
+ # CLI and also calculate the hases in advance instead of retrieving
+ # them after service startup - which was always a bad idea
+ prefix = '0x'
+
+ config_engineid = config_base + ['engineid']
+ if config.exists(config_engineid):
+ tmp = config.return_value(config_engineid)
+ if tmp.startswith(prefix):
+ tmp = tmp.replace(prefix, '')
+ config.set(config_engineid, value=tmp)
+
+ config_user = config_base + ['user']
+ if config.exists(config_user):
+ for user in config.list_nodes(config_user):
+ migrate_keys(config, config_user + [user])
+
+ config_trap = config_base + ['trap-target']
+ if config.exists(config_trap):
+ for trap in config.list_nodes(config_trap):
+ migrate_keys(config, config_trap + [trap])
+
+ 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/ssh/0-to-1 b/src/migration-scripts/ssh/0-to-1
new file mode 100755
index 000000000..91b832276
--- /dev/null
+++ b/src/migration-scripts/ssh/0-to-1
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+# Delete "service ssh allow-root" option
+
+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(['service', 'ssh', 'allow-root']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Delete node with abandoned command
+ config.delete(['service', 'ssh', 'allow-root'])
+
+ 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/ssh/1-to-2 b/src/migration-scripts/ssh/1-to-2
new file mode 100755
index 000000000..bc8815753
--- /dev/null
+++ b/src/migration-scripts/ssh/1-to-2
@@ -0,0 +1,55 @@
+#!/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/>.
+
+# VyOS 1.2 crux allowed configuring a lower or upper case loglevel. This
+# is no longer supported as the input data is validated and will lead to
+# an error. If user specifies an upper case logleve, make it lowercase
+
+from sys import argv,exit
+from vyos.configtree import ConfigTree
+
+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()
+
+base = ['service', 'ssh', 'loglevel']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+ # red in configured loglevel and convert it to lower case
+ tmp = config.return_value(base).lower()
+
+ # VyOS 1.2 had no proper value validation on the CLI thus the
+ # user could use any arbitrary values - sanitize them
+ if tmp not in ['quiet', 'fatal', 'error', 'info', 'verbose']:
+ tmp = 'info'
+
+ config.set(base, value=tmp)
+
+ 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/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/10-to-11 b/src/migration-scripts/system/10-to-11
new file mode 100755
index 000000000..1a0233c7d
--- /dev/null
+++ b/src/migration-scripts/system/10-to-11
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+
+# Unclutter RADIUS configuration
+#
+# Move radius-server top level tag nodes to a regular node which allows us
+# to specify additional general features for the RADIUS client.
+
+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)
+cfg_base = ['system', 'login']
+if not (config.exists(cfg_base + ['radius-server']) or config.exists(cfg_base + ['radius-source-address'])):
+ # Nothing to do
+ sys.exit(0)
+else:
+ #
+ # Migrate "system login radius-source-address" to "system login radius"
+ #
+ if config.exists(cfg_base + ['radius-source-address']):
+ address = config.return_value(cfg_base + ['radius-source-address'])
+ # delete old configuration node
+ config.delete(cfg_base + ['radius-source-address'])
+ # write new configuration node
+ config.set(cfg_base + ['radius', 'source-address'], value=address)
+
+ #
+ # Migrate "system login radius-server" tag node to new
+ # "system login radius server" tag node and also rename the "secret" node to "key"
+ #
+ for server in config.list_nodes(cfg_base + ['radius-server']):
+ base_server = cfg_base + ['radius-server', server]
+ # "key" node is mandatory
+ key = config.return_value(base_server + ['secret'])
+ config.set(cfg_base + ['radius', 'server', server, 'key'], value=key)
+
+ # "port" is optional
+ if config.exists(base_server + ['port']):
+ port = config.return_value(base_server + ['port'])
+ config.set(cfg_base + ['radius', 'server', server, 'port'], value=port)
+
+ # "timeout is optional"
+ if config.exists(base_server + ['timeout']):
+ timeout = config.return_value(base_server + ['timeout'])
+ config.set(cfg_base + ['radius', 'server', server, 'timeout'], value=timeout)
+
+ # format as tag node
+ config.set_tag(cfg_base + ['radius', 'server'])
+
+ # delete old configuration node
+ config.delete(base_server)
+
+ # delete top level tag node
+ if config.exists(cfg_base + ['radius-server']):
+ config.delete(cfg_base + ['radius-server'])
+
+ 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/11-to-12 b/src/migration-scripts/system/11-to-12
new file mode 100755
index 000000000..0c92a0746
--- /dev/null
+++ b/src/migration-scripts/system/11-to-12
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+
+# converts 'set system syslog host <address>:<port>'
+# to 'set system syslog host <address> port <port>'
+
+import sys
+import re
+
+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)
+cbase = ['system', 'syslog', 'host']
+
+if not config.exists(cbase):
+ sys.exit(0)
+
+for host in config.list_nodes(cbase):
+ if re.search(':[0-9]{1,5}$',host):
+ h = re.search('^[a-zA-Z\-0-9\.]+', host).group(0)
+ p = re.sub(':', '', re.search(':[0-9]+$', host).group(0))
+ config.set(cbase + [h])
+ config.set(cbase + [h, 'port'], value=p)
+ for fac in config.list_nodes(cbase + [host, 'facility']):
+ config.set(cbase + [h, 'facility', fac])
+ config.set_tag(cbase + [h, 'facility'])
+ if config.exists(cbase + [host, 'facility', fac, 'protocol']):
+ proto = config.return_value(cbase + [host, 'facility', fac, 'protocol'])
+ config.set(cbase + [h, 'facility', fac, 'protocol'], value=proto)
+ if config.exists(cbase + [host, 'facility', fac, 'level']):
+ lvl = config.return_value(cbase + [host, 'facility', fac, 'level'])
+ config.set(cbase + [h, 'facility', fac, 'level'], value=lvl)
+ config.delete(cbase + [host])
+
+ try:
+ open(file_name,'w').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
new file mode 100755
index 000000000..5b068f4fc
--- /dev/null
+++ b/src/migration-scripts/system/12-to-13
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+
+# Fixup non existent time-zones. Some systems have time-zone set to: Los*
+# (Los_Angeles), Den* (Denver), New* (New_York) ... but those are no real IANA
+# assigned time zones. In the past they have been silently remapped.
+#
+# Time to clean it up!
+#
+# Migrate all configured timezones to real IANA assigned timezones!
+
+import re
+import sys
+
+from vyos.configtree import ConfigTree
+from vyos.util import cmd
+
+
+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)
+tz_base = ['system', 'time-zone']
+if not config.exists(tz_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ tz = config.return_value(tz_base)
+
+ # retrieve all valid timezones
+ 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'
+ elif re.match(r'[Dd][Ee][Nn].+', tz):
+ tz = 'America/Denver'
+ elif re.match(r'[Hh][Oo][Nn][Oo].+', tz):
+ tz = 'Pacific/Honolulu'
+ elif re.match(r'[Nn][Ee][Ww].+', tz):
+ tz = 'America/New_York'
+ elif re.match(r'[Cc][Hh][Ii][Cc]*.+', tz):
+ tz = 'America/Chicago'
+ elif re.match(r'[Aa][Nn][Cc].+', tz):
+ tz = 'America/Anchorage'
+ elif re.match(r'[Pp][Hh][Oo].+', tz):
+ tz = 'America/Phoenix'
+ elif re.match(r'GMT(.+)?', tz):
+ tz = 'Etc/' + tz
+ elif tz not in tz_data:
+ # assign default UTC timezone
+ tz = 'UTC'
+
+ # replace timezone data is required
+ config.set(tz_base, value=tz)
+
+ 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/13-to-14 b/src/migration-scripts/system/13-to-14
new file mode 100755
index 000000000..c055dad1f
--- /dev/null
+++ b/src/migration-scripts/system/13-to-14
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+#
+# Delete 'system ipv6 blacklist' option as the IPv6 module can no longer be
+# blacklisted as it is required by e.g. WireGuard and thus will always be
+# loaded.
+
+import os
+import sys
+
+ipv6_blacklist_file = '/etc/modprobe.d/vyatta_blacklist_ipv6.conf'
+
+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)
+ip_base = ['system', 'ipv6']
+if not config.exists(ip_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # delete 'system ipv6 blacklist' node
+ if config.exists(ip_base + ['blacklist']):
+ config.delete(ip_base + ['blacklist'])
+ if os.path.isfile(ipv6_blacklist_file):
+ os.unlink(ipv6_blacklist_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/system/14-to-15 b/src/migration-scripts/system/14-to-15
new file mode 100755
index 000000000..2491e3d0d
--- /dev/null
+++ b/src/migration-scripts/system/14-to-15
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+#
+# Make 'system options reboot-on-panic' valueless
+
+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)
+base = ['system', 'options']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ if config.exists(base + ['reboot-on-panic']):
+ reboot = config.return_value(base + ['reboot-on-panic'])
+ config.delete(base + ['reboot-on-panic'])
+ # create new valueless node if action was true
+ if reboot == "true":
+ config.set(base + ['reboot-on-panic'])
+
+ 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/15-to-16 b/src/migration-scripts/system/15-to-16
new file mode 100755
index 000000000..e70893d55
--- /dev/null
+++ b/src/migration-scripts/system/15-to-16
@@ -0,0 +1,55 @@
+#!/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 "system login user <user> group" node, Why should be add a user to a
+# 3rd party group when the system is fully managed by CLI?
+# * remove "system login user <user> level" node
+# This is the only privilege level left and also the default, what is the
+# sense in keeping this orphaned node?
+
+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)
+base = ['system', 'login', 'user']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ for user in config.list_nodes(base):
+ if config.exists(base + [user, 'group']):
+ config.delete(base + [user, 'group'])
+
+ if config.exists(base + [user, 'level']):
+ config.delete(base + [user, 'level'])
+
+ 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/16-to-17 b/src/migration-scripts/system/16-to-17
new file mode 100755
index 000000000..8f762c0e2
--- /dev/null
+++ b/src/migration-scripts/system/16-to-17
@@ -0,0 +1,76 @@
+#!/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 "system console netconsole"
+# remove "system console device <device> modem"
+
+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)
+base = ['system', 'console']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # remove "system console netconsole" (T2561)
+ if config.exists(base + ['netconsole']):
+ config.delete(base + ['netconsole'])
+
+ if config.exists(base + ['device']):
+ for device in config.list_nodes(base + ['device']):
+ dev_path = base + ['device', device]
+ # remove "system console device <device> modem" (T2570)
+ if config.exists(dev_path + ['modem']):
+ config.delete(dev_path + ['modem'])
+
+ # Only continue on USB based serial consoles
+ if not 'ttyUSB' in device:
+ continue
+
+ # A serial console has been configured but it does no longer
+ # exist on the system - cleanup
+ if not os.path.exists(f'/dev/{device}'):
+ config.delete(dev_path)
+ continue
+
+ # migrate from ttyUSB device to new device in /dev/serial/by-bus
+ for root, dirs, files in os.walk('/dev/serial/by-bus'):
+ for usb_device in files:
+ device_file = os.path.realpath(os.path.join(root, usb_device))
+ # migrate to new USB device names (T2529)
+ if os.path.basename(device_file) == device:
+ config.copy(dev_path, base + ['device', usb_device])
+ # Delete old USB node from config
+ config.delete(dev_path)
+
+ 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/17-to-18 b/src/migration-scripts/system/17-to-18
new file mode 100755
index 000000000..dd2abce00
--- /dev/null
+++ b/src/migration-scripts/system/17-to-18
@@ -0,0 +1,105 @@
+#!/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 disable-dhcp-nameservers (boolean) to name-servers-dhcp <interface>
+# if disable-dhcp-nameservers is set, just remove it
+# else retrieve all interface names that have configured dhcp(v6) address and
+# add them to the new name-servers-dhcp node
+
+from sys import argv, exit
+from vyos.ifconfig import Interface
+from vyos.configtree import ConfigTree
+
+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 = ['system']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+if config.exists(base + ['disable-dhcp-nameservers']):
+ config.delete(base + ['disable-dhcp-nameservers'])
+else:
+ dhcp_interfaces = []
+
+ # go through all interfaces searching for 'address dhcp(v6)?'
+ for sect in Interface.sections():
+ sect_base = ['interfaces', sect]
+
+ if not config.exists(sect_base):
+ continue
+
+ for intf in config.list_nodes(sect_base):
+ intf_base = sect_base + [intf]
+
+ # try without vlans
+ if config.exists(intf_base + ['address']):
+ for addr in config.return_values(intf_base + ['address']):
+ if addr in ['dhcp', 'dhcpv6']:
+ dhcp_interfaces.append(intf)
+
+ # try vif
+ if config.exists(intf_base + ['vif']):
+ for vif in config.list_nodes(intf_base + ['vif']):
+ vif_base = intf_base + ['vif', vif]
+ if config.exists(vif_base + ['address']):
+ for addr in config.return_values(vif_base + ['address']):
+ if addr in ['dhcp', 'dhcpv6']:
+ dhcp_interfaces.append(f'{intf}.{vif}')
+
+ # try vif-s
+ if config.exists(intf_base + ['vif-s']):
+ for vif_s in config.list_nodes(intf_base + ['vif-s']):
+ vif_s_base = intf_base + ['vif-s', vif_s]
+ if config.exists(vif_s_base + ['address']):
+ for addr in config.return_values(vif_s_base + ['address']):
+ if addr in ['dhcp', 'dhcpv6']:
+ dhcp_interfaces.append(f'{intf}.{vif_s}')
+
+ # try vif-c
+ if config.exists(intf_base + ['vif-c', vif_c]):
+ for vif_c in config.list_nodes(vif_s_base + ['vif-c', vif_c]):
+ vif_c_base = vif_s_base + ['vif-c', vif_c]
+ if config.exists(vif_c_base + ['address']):
+ for addr in config.return_values(vif_c_base + ['address']):
+ if addr in ['dhcp', 'dhcpv6']:
+ dhcp_interfaces.append(f'{intf}.{vif_s}.{vif_c}')
+
+ # set new config nodes
+ for intf in dhcp_interfaces:
+ config.set(base + ['name-servers-dhcp'], value=intf, replace=False)
+
+ # delete old node
+ config.delete(base + ['disable-dhcp-nameservers'])
+
+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)
+
+exit(0)
diff --git a/src/migration-scripts/system/6-to-7 b/src/migration-scripts/system/6-to-7
new file mode 100755
index 000000000..bf07abf3a
--- /dev/null
+++ b/src/migration-scripts/system/6-to-7
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+
+# Change smp_affinity to smp-affinity
+
+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)
+
+update_required = False
+
+intf_types = config.list_nodes(["interfaces"])
+
+for intf_type in intf_types:
+ intf_type_path = ["interfaces", intf_type]
+ intfs = config.list_nodes(intf_type_path)
+
+ for intf in intfs:
+ intf_path = intf_type_path + [intf]
+ if not config.exists(intf_path + ["smp_affinity"]):
+ # Nothing to do.
+ continue
+ else:
+ # Rename the node.
+ old_smp_affinity_path = intf_path + ["smp_affinity"]
+ config.rename(old_smp_affinity_path, "smp-affinity")
+ update_required = True
+
+if update_required:
+ 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/7-to-8 b/src/migration-scripts/system/7-to-8
new file mode 100755
index 000000000..4cbb21f17
--- /dev/null
+++ b/src/migration-scripts/system/7-to-8
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+
+# Converts "system gateway-address" option to "protocols static route 0.0.0.0/0 next-hop $gw"
+
+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(['system', 'gateway-address']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Save the address
+ gw = config.return_value(['system', 'gateway-address'])
+
+ # Create the node for the new syntax
+ # Note: next-hop is a tag node, gateway address is its child, not a value
+ config.set(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', gw])
+
+ # Delete the node with the old syntax
+ config.delete(['system', 'gateway-address'])
+
+ # Now, the interesting part. Both route and next-hop are supposed to be tag nodes,
+ # which you can verify with "cli-shell-api isTag $configPath".
+ # They must be formatted as such to load correctly.
+ config.set_tag(['protocols', 'static', 'route'])
+ config.set_tag(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop'])
+
+ 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/8-to-9 b/src/migration-scripts/system/8-to-9
new file mode 100755
index 000000000..db3fefdea
--- /dev/null
+++ b/src/migration-scripts/system/8-to-9
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+# Deletes "system package" option as it is deprecated
+
+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(['system', 'package']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # Delete the node with the old syntax
+ config.delete(['system', 'package'])
+
+ 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/9-to-10 b/src/migration-scripts/system/9-to-10
new file mode 100755
index 000000000..3c49f0d95
--- /dev/null
+++ b/src/migration-scripts/system/9-to-10
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+
+# Operator accounts have been deprecated due to a security issue. Those accounts
+# will be converted to regular admin accounts.
+
+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)
+base_level = ['system', 'login', 'user']
+
+if not config.exists(base_level):
+ # Nothing to do, which shouldn't happen anyway
+ # only if you wipe the config and reboot.
+ sys.exit(0)
+else:
+ for user in config.list_nodes(base_level):
+ if config.exists(base_level + [user, 'level']):
+ if config.return_value(base_level + [user, 'level']) == 'operator':
+ config.set(base_level + [user, 'level'], value="admin", replace=True)
+
+ try:
+ open(file_name,'w').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/vrrp/1-to-2 b/src/migration-scripts/vrrp/1-to-2
new file mode 100755
index 000000000..b2e61dd38
--- /dev/null
+++ b/src/migration-scripts/vrrp/1-to-2
@@ -0,0 +1,270 @@
+#!/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 re
+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)
+
+# Convert the old VRRP syntax to the new syntax
+
+# The old approach was to put VRRP groups inside interfaces,
+# as in "interfaces ethernet eth0 vrrp vrrp-group 10 ...".
+# It was supported only under ethernet and bonding and their
+# respective vif, vif-s, and vif-c subinterfaces
+
+def get_vrrp_group(path):
+ group = {"preempt": True, "rfc_compatibility": False, "disable": False}
+
+ if config.exists(path + ["advertise-interval"]):
+ group["advertise_interval"] = config.return_value(path + ["advertise-interval"])
+
+ if config.exists(path + ["description"]):
+ group["description"] = config.return_value(path + ["description"])
+
+ if config.exists(path + ["disable"]):
+ group["disable"] = True
+
+ if config.exists(path + ["hello-source-address"]):
+ group["hello_source"] = config.return_value(path + ["hello-source-address"])
+
+ # 1.1.8 didn't have it, but earlier 1.2.0 did, we don't want to break
+ # configs of early adopters!
+ if config.exists(path + ["peer-address"]):
+ group["peer_address"] = config.return_value(path + ["peer-address"])
+
+ if config.exists(path + ["preempt"]):
+ preempt = config.return_value(path + ["preempt"])
+ if preempt == "false":
+ group["preempt"] = False
+
+ if config.exists(path + ["rfc3768-compatibility"]):
+ group["rfc_compatibility"] = True
+
+ if config.exists(path + ["preempt-delay"]):
+ group["preempt_delay"] = config.return_value(path + ["preempt-delay"])
+
+ if config.exists(path + ["priority"]):
+ group["priority"] = config.return_value(path + ["priority"])
+
+ if config.exists(path + ["sync-group"]):
+ group["sync_group"] = config.return_value(path + ["sync-group"])
+
+ if config.exists(path + ["authentication", "type"]):
+ group["auth_type"] = config.return_value(path + ["authentication", "type"])
+
+ if config.exists(path + ["authentication", "password"]):
+ group["auth_password"] = config.return_value(path + ["authentication", "password"])
+
+ if config.exists(path + ["virtual-address"]):
+ group["virtual_addresses"] = config.return_values(path + ["virtual-address"])
+
+ if config.exists(path + ["run-transition-scripts"]):
+ if config.exists(path + ["run-transition-scripts", "master"]):
+ group["master_script"] = config.return_value(path + ["run-transition-scripts", "master"])
+ if config.exists(path + ["run-transition-scripts", "backup"]):
+ group["backup_script"] = config.return_value(path + ["run-transition-scripts", "backup"])
+ if config.exists(path + ["run-transition-scripts", "fault"]):
+ group["fault_script"] = config.return_value(path + ["run-transition-scripts", "fault"])
+
+ # Also not present in 1.1.8, but supported by earlier 1.2.0
+ if config.exists(path + ["health-check"]):
+ if config.exists(path + ["health-check", "interval"]):
+ group["health_check_interval"] = config.return_value(path + ["health-check", "interval"])
+ if config.exists(path + ["health-check", "failure-count"]):
+ group["health_check_count"] = config.return_value(path + ["health-check", "failure-count"])
+ if config.exists(path + ["health-check", "script"]):
+ group["health_check_script"] = config.return_value(path + ["health-check", "script"])
+
+ return group
+
+# Since VRRP is all over the place, there's no way to just check a path and exit early
+# if it doesn't exist, we have to walk all interfaces and collect VRRP settings from them.
+# Only if no data is collected from any interface we can conclude that VRRP is not configured
+# and exit.
+
+groups = []
+base_paths = []
+
+if config.exists(["interfaces", "ethernet"]):
+ base_paths.append("ethernet")
+if config.exists(["interfaces", "bonding"]):
+ base_paths.append("bonding")
+
+for bp in base_paths:
+ parent_path = ["interfaces", bp]
+
+ parent_intfs = config.list_nodes(parent_path)
+
+ for pi in parent_intfs:
+ # Extract VRRP groups from the parent interface
+ vg_path =[pi, "vrrp", "vrrp-group"]
+ if config.exists(parent_path + vg_path):
+ pgroups = config.list_nodes(parent_path + vg_path)
+ for pg in pgroups:
+ g = get_vrrp_group(parent_path + vg_path + [pg])
+ g["interface"] = pi
+ g["vrid"] = pg
+ groups.append(g)
+
+ # Delete the VRRP subtree
+ # If left in place, configs will not load correctly
+ config.delete(parent_path + [pi, "vrrp"])
+
+ # Extract VRRP groups from 802.1q VLAN interfaces
+ if config.exists(parent_path + [pi, "vif"]):
+ vifs = config.list_nodes(parent_path + [pi, "vif"])
+ for vif in vifs:
+ vif_vg_path = [pi, "vif", vif, "vrrp", "vrrp-group"]
+ if config.exists(parent_path + vif_vg_path):
+ vifgroups = config.list_nodes(parent_path + vif_vg_path)
+ for vif_group in vifgroups:
+ g = get_vrrp_group(parent_path + vif_vg_path + [vif_group])
+ g["interface"] = "{0}.{1}".format(pi, vif)
+ g["vrid"] = vif_group
+ groups.append(g)
+
+ config.delete(parent_path + [pi, "vif", vif, "vrrp"])
+
+ # Extract VRRP groups from 802.3ad QinQ service VLAN interfaces
+ if config.exists(parent_path + [pi, "vif-s"]):
+ vif_ss = config.list_nodes(parent_path + [pi, "vif-s"])
+ for vif_s in vif_ss:
+ vifs_vg_path = [pi, "vif-s", vif_s, "vrrp", "vrrp-group"]
+ if config.exists(parent_path + vifs_vg_path):
+ vifsgroups = config.list_nodes(parent_path + vifs_vg_path)
+ for vifs_group in vifsgroups:
+ g = get_vrrp_group(parent_path + vifs_vg_path + [vifs_group])
+ g["interface"] = "{0}.{1}".format(pi, vif_s)
+ g["vrid"] = vifs_group
+ groups.append(g)
+
+ config.delete(parent_path + [pi, "vif-s", vif_s, "vrrp"])
+
+ # Extract VRRP groups from QinQ client VLAN interfaces nested in the vif-s
+ if config.exists(parent_path + [pi, "vif-s", vif_s, "vif-c"]):
+ vif_cs = config.list_nodes(parent_path + [pi, "vif-s", vif_s, "vif-c"])
+ for vif_c in vif_cs:
+ vifc_vg_path = [pi, "vif-s", vif_s, "vif-c", vif_c, "vrrp", "vrrp-group"]
+ vifcgroups = config.list_nodes(parent_path + vifc_vg_path)
+ for vifc_group in vifcgroups:
+ g = get_vrrp_group(parent_path + vifc_vg_path + [vifc_group])
+ g["interface"] = "{0}.{1}.{2}".format(pi, vif_s, vif_c)
+ g["vrid"] = vifc_group
+ groups.append(g)
+
+ config.delete(parent_path + [pi, "vif-s", vif_s, "vif-c", vif_c, "vrrp"])
+
+# If nothing was collected before this point, it means the config has no VRRP setup
+if not groups:
+ sys.exit(0)
+
+# Otherwise, there is VRRP to convert
+
+# Now convert the collected groups to the new syntax
+base_group_path = ["high-availability", "vrrp", "group"]
+sync_path = ["high-availability", "vrrp", "sync-group"]
+
+for g in groups:
+ group_name = "{0}-{1}".format(g["interface"], g["vrid"])
+ group_path = base_group_path + [group_name]
+
+ config.set(group_path + ["interface"], value=g["interface"])
+ config.set(group_path + ["vrid"], value=g["vrid"])
+
+ if "advertise_interval" in g:
+ config.set(group_path + ["advertise-interval"], value=g["advertise_interval"])
+
+ if "priority" in g:
+ config.set(group_path + ["priority"], value=g["priority"])
+
+ if not g["preempt"]:
+ config.set(group_path + ["no-preempt"], value=None)
+
+ if "preempt_delay" in g:
+ config.set(group_path + ["preempt-delay"], value=g["preempt_delay"])
+
+ if g["rfc_compatibility"]:
+ config.set(group_path + ["rfc3768-compatibility"], value=None)
+
+ if g["disable"]:
+ config.set(group_path + ["disable"], value=None)
+
+ if "hello_source" in g:
+ config.set(group_path + ["hello-source-address"], value=g["hello_source"])
+
+ if "peer_address" in g:
+ config.set(group_path + ["peer-address"], value=g["peer_address"])
+
+ if "auth_password" in g:
+ config.set(group_path + ["authentication", "password"], value=g["auth_password"])
+ if "auth_type" in g:
+ config.set(group_path + ["authentication", "type"], value=g["auth_type"])
+
+ if "master_script" in g:
+ config.set(group_path + ["transition-script", "master"], value=g["master_script"])
+ if "backup_script" in g:
+ config.set(group_path + ["transition-script", "backup"], value=g["backup_script"])
+ if "fault_script" in g:
+ config.set(group_path + ["transition-script", "fault"], value=g["fault_script"])
+
+ if "health_check_interval" in g:
+ config.set(group_path + ["health-check", "interval"], value=g["health_check_interval"])
+ if "health_check_count" in g:
+ config.set(group_path + ["health-check", "failure-count"], value=g["health_check_count"])
+ if "health_check_script" in g:
+ config.set(group_path + ["health-check", "script"], value=g["health_check_script"])
+
+ # Not that it should ever be absent...
+ if "virtual_addresses" in g:
+ # The new CLI disallows addresses without prefix length
+ # Pre-rewrite configs didn't support IPv6 VRRP, but handle it anyway
+ for va in g["virtual_addresses"]:
+ if not re.search(r'/', va):
+ if re.search(r':', va):
+ va = "{0}/128".format(va)
+ else:
+ va = "{0}/32".format(va)
+ config.set(group_path + ["virtual-address"], value=va, replace=False)
+
+ # Sync group
+ if "sync_group" in g:
+ config.set(sync_path + [g["sync_group"], "member"], value=group_name, replace=False)
+
+# Set the tag flag
+config.set_tag(base_group_path)
+if config.exists(sync_path):
+ config.set_tag(sync_path)
+
+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/webproxy/1-to-2 b/src/migration-scripts/webproxy/1-to-2
new file mode 100755
index 000000000..070ff356d
--- /dev/null
+++ b/src/migration-scripts/webproxy/1-to-2
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+
+# migrate old style `webproxy proxy-bypass 1.2.3.4/24`
+# to new style `webproxy whitelist destination-address 1.2.3.4/24`
+
+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)
+
+cfg_webproxy_base = ['service', 'webproxy']
+if not config.exists(cfg_webproxy_base + ['proxy-bypass']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ bypass_addresses = config.return_values(cfg_webproxy_base + ['proxy-bypass'])
+ # delete old configuration node
+ config.delete(cfg_webproxy_base + ['proxy-bypass'])
+ for bypass_address in bypass_addresses:
+ # add data to new configuration node
+ config.set(cfg_webproxy_base + ['whitelist', 'destination-address'], value=bypass_address, replace=False)
+
+ # save updated configuration
+ 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/op_mode/anyconnect-control.py b/src/op_mode/anyconnect-control.py
new file mode 100755
index 000000000..6382016b7
--- /dev/null
+++ b/src/op_mode/anyconnect-control.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 sys
+import argparse
+import json
+
+from vyos.config import Config
+from vyos.util import popen, run, DEVNULL
+from tabulate import tabulate
+
+occtl = '/usr/bin/occtl'
+occtl_socket = '/run/ocserv/occtl.socket'
+
+def show_sessions():
+ out, code = popen("sudo {0} -j -s {1} show users".format(occtl, occtl_socket),stderr=DEVNULL)
+ if code:
+ sys.exit('Cannot get anyconnect users information')
+ else:
+ headers = ["interface", "username", "ip", "remote IP", "RX", "TX", "state", "uptime"]
+ sessions = json.loads(out)
+ ses_list = []
+ for ses in sessions:
+ ses_list.append([ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"], ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"]])
+ if len(ses_list) > 0:
+ print(tabulate(ses_list, headers))
+ else:
+ print("No active anyconnect sessions")
+
+def is_ocserv_configured():
+ if not Config().exists_effective('vpn anyconnect'):
+ print("vpn anyconnect server is not configured")
+ sys.exit(1)
+
+def main():
+ #parese args
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--action', help='Control action', required=True)
+ parser.add_argument('--selector', help='Selector username|ifname|sid', required=False)
+ parser.add_argument('--target', help='Target must contain username|ifname|sid', required=False)
+ args = parser.parse_args()
+
+
+ # Check is IPoE configured
+ is_ocserv_configured()
+
+ if args.action == "restart":
+ run("systemctl restart ocserv")
+ sys.exit(0)
+ elif args.action == "show_sessions":
+ show_sessions()
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py
new file mode 100755
index 000000000..423694187
--- /dev/null
+++ b/src/op_mode/clear_conntrack.py
@@ -0,0 +1,26 @@
+#!/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.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:
+ 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
new file mode 100755
index 000000000..a773aa28e
--- /dev/null
+++ b/src/op_mode/connect_disconnect.py
@@ -0,0 +1,87 @@
+#!/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 argparse
+
+from sys import exit
+from psutil import process_iter
+from time import strftime, localtime, time
+
+from vyos.util import call
+
+def check_interface(interface):
+ if not os.path.isfile(f'/etc/ppp/peers/{interface}'):
+ print(f'Interface {interface}: invalid!')
+ exit(1)
+
+def check_ppp_running(interface):
+ """
+ Check if ppp process is running in the interface in question
+ """
+ for p in process_iter():
+ if "pppd" in p.name():
+ if interface in p.cmdline():
+ return True
+
+ return False
+
+def connect(interface):
+ """
+ Connect PPP interface
+ """
+ check_interface(interface)
+
+ # Check if interface is already dialed
+ if os.path.isdir(f'/sys/class/net/{interface}'):
+ print(f'Interface {interface}: already connected!')
+ elif check_ppp_running(interface):
+ print(f'Interface {interface}: connection is beeing established!')
+ else:
+ print(f'Interface {interface}: connecting...')
+ call(f'systemctl restart ppp@{interface}.service')
+
+def disconnect(interface):
+ """
+ Disconnect PPP interface
+ """
+ check_interface(interface)
+
+ # Check if interface is already down
+ if not check_ppp_running(interface):
+ print(f'Interface {interface}: connection is already down')
+ else:
+ print(f'Interface {interface}: disconnecting...')
+ call(f'systemctl stop ppp@{interface}.service')
+
+def main():
+ parser = argparse.ArgumentParser()
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument("--connect", help="Bring up a connection-oriented network interface", action="store")
+ group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store")
+ args = parser.parse_args()
+
+ if args.connect:
+ connect(args.connect)
+ elif args.disconnect:
+ disconnect(args.disconnect)
+ else:
+ parser.print_help()
+
+ exit(0)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py
new file mode 100755
index 000000000..cfd321522
--- /dev/null
+++ b/src/op_mode/cpu_summary.py
@@ -0,0 +1,36 @@
+#!/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 re
+from vyos.util import colon_separated_to_dict
+
+FILE_NAME = '/proc/cpuinfo'
+
+with open(FILE_NAME, 'r') as f:
+ data_raw = f.read()
+
+data = colon_separated_to_dict(data_raw)
+
+# Accumulate all data in a dict for future support for machine-readable output
+cpu_data = {}
+cpu_data['cpu_number'] = len(data['processor'])
+cpu_data['models'] = list(set(data['model name']))
+
+# Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that
+cpu_data['models'] = map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models'])
+
+print("CPU(s): {0}".format(cpu_data['cpu_number']))
+print("CPU model(s): {0}".format(",".join(cpu_data['models'])))
diff --git a/src/op_mode/dns_forwarding_reset.py b/src/op_mode/dns_forwarding_reset.py
new file mode 100755
index 000000000..bfc640a26
--- /dev/null
+++ b/src/op_mode/dns_forwarding_reset.py
@@ -0,0 +1,54 @@
+#!/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/>.
+#
+# File: vyos-show-version
+# Purpose:
+# Displays image version and system information.
+# Used by the "run show version" command.
+
+
+import os
+import argparse
+
+from sys import exit
+from vyos.config import Config
+from vyos.util import call
+
+PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns'
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-a", "--all", action="store_true", help="Reset all cache")
+parser.add_argument("domain", type=str, nargs="?", help="Domain to reset cache entries for")
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective(['service', 'dns', 'forwarding']):
+ print("DNS forwarding is not configured")
+ exit(0)
+
+ if args.all:
+ call(f"{PDNS_CMD} wipe-cache \'.$\'")
+ exit(0)
+
+ elif args.domain:
+ call(f"{PDNS_CMD} wipe-cache \'{0}$\'".format(args.domain))
+
+ else:
+ parser.print_help()
+ exit(1)
diff --git a/src/op_mode/dns_forwarding_restart.sh b/src/op_mode/dns_forwarding_restart.sh
new file mode 100755
index 000000000..64cc92115
--- /dev/null
+++ b/src/op_mode/dns_forwarding_restart.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if cli-shell-api existsEffective service dns forwarding; then
+ echo "Restarting the DNS forwarding service"
+ systemctl restart pdns-recursor.service
+else
+ echo "DNS forwarding is not configured"
+fi
diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py
new file mode 100755
index 000000000..1fb61d263
--- /dev/null
+++ b/src/op_mode/dns_forwarding_statistics.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+import jinja2
+from sys import exit
+
+from vyos.config import Config
+from vyos.util import cmd
+
+PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns'
+
+OUT_TMPL_SRC = """
+DNS forwarding statistics:
+
+Cache entries: {{ cache_entries -}}
+Cache size: {{ cache_size }} kbytes
+
+"""
+
+if __name__ == '__main__':
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service dns forwarding'):
+ print("DNS forwarding is not configured")
+ exit(0)
+
+ data = {}
+
+ 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
new file mode 100755
index 000000000..021acfd73
--- /dev/null
+++ b/src/op_mode/dynamic_dns.py
@@ -0,0 +1,104 @@
+#!/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
+import argparse
+import jinja2
+import sys
+import time
+
+from vyos.config import Config
+from vyos.util import call
+
+cache_file = r'/run/ddclient/ddclient.cache'
+
+OUT_TMPL_SRC = """
+{%- for entry in hosts -%}
+ip address : {{ entry.ip }}
+host-name : {{ entry.host }}
+last update : {{ entry.time }}
+update-status: {{ entry.status }}
+
+{% endfor -%}
+"""
+
+def show_status():
+ data = {
+ 'hosts': []
+ }
+
+ with open(cache_file, 'r') as f:
+ for line in f:
+ if line.startswith('#'):
+ continue
+
+ outp = {
+ 'host': '',
+ 'ip': '',
+ 'time': ''
+ }
+
+ if 'host=' in line:
+ host = line.split('host=')[1]
+ if host:
+ outp['host'] = host.split(',')[0]
+
+ if 'ip=' in line:
+ ip = line.split('ip=')[1]
+ if ip:
+ outp['ip'] = ip.split(',')[0]
+
+ if 'atime=' in line:
+ atime = line.split('atime=')[1]
+ if atime:
+ tmp = atime.split(',')[0]
+ outp['time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(int(tmp, base=10)))
+
+ if 'status=' in line:
+ status = line.split('status=')[1]
+ if status:
+ outp['status'] = status.split(',')[0]
+
+ data['hosts'].append(outp)
+
+ tmpl = jinja2.Template(OUT_TMPL_SRC)
+ print(tmpl.render(data))
+
+
+def update_ddns():
+ call('systemctl stop ddclient.service')
+ if os.path.exists(cache_file):
+ os.remove(cache_file)
+ call('systemctl start ddclient.service')
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument("--status", help="Show DDNS status", action="store_true")
+ group.add_argument("--update", help="Update DDNS on a given interface", action="store_true")
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service dns dynamic'):
+ print("Dynamic DNS not configured")
+ sys.exit(1)
+
+ if args.status:
+ show_status()
+ elif args.update:
+ update_ddns()
diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py
new file mode 100755
index 000000000..6586cbceb
--- /dev/null
+++ b/src/op_mode/flow_accounting_op.py
@@ -0,0 +1,252 @@
+#!/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 argparse
+import re
+import ipaddress
+import os.path
+from tabulate import tabulate
+from json import loads
+from vyos.util import cmd, run
+from vyos.logger import syslog
+
+# some default values
+uacctd_pidfile = '/var/run/uacctd.pid'
+uacctd_pipefile = '/tmp/uacctd.pipe'
+
+def parse_port(port):
+ try:
+ port_num = int(port)
+ if (port_num >= 0) and (port_num <= 65535):
+ return port_num
+ else:
+ raise ValueError("out of the 0-65535 range".format(port))
+ except ValueError as e:
+ raise ValueError("Incorrect port number \'{0}\': {1}".format(port, e))
+
+def parse_ports(arg):
+ if re.match(r'^\d+$', arg):
+ # Single port
+ port = parse_port(arg)
+ return {"type": "single", "value": port}
+ elif re.match(r'^\d+\-\d+$', arg):
+ # Port range
+ ports = arg.split("-")
+ ports = list(map(parse_port, ports))
+ if ports[0] > ports[1]:
+ raise ValueError("Malformed port range \'{0}\': lower end is greater than the higher".format(arg))
+ else:
+ return {"type": "range", "value": (ports[0], ports[1])}
+ elif re.match(r'^\d+,.*\d$', arg):
+ # Port list
+ ports = re.split(r',+', arg) # This allows duplicate commad like '1,,2,3,4'
+ ports = list(map(parse_port, ports))
+ return {"type": "list", "value": ports}
+ else:
+ raise ValueError("Malformed port spec \'{0}\'".format(arg))
+
+# check if host argument have correct format
+def check_host(host):
+ # define regex for checking
+ if not ipaddress.ip_address(host):
+ raise ValueError("Invalid host \'{}\', must be a valid IP or IPv6 address".format(host))
+
+# check if flow-accounting running
+def _uacctd_running():
+ command = 'systemctl status uacctd.service > /dev/null'
+ return run(command) == 0
+
+
+# get list of interfaces
+def _get_ifaces_dict():
+ # run command to get ifaces list
+ out = cmd('/bin/ip link show')
+
+ # read output
+ ifaces_out = out.splitlines()
+
+ # make a dictionary with interfaces and indexes
+ ifaces_dict = {}
+ regex_filter = re.compile(r'^(?P<iface_index>\d+):\ (?P<iface_name>[\w\d\.]+)[:@].*$')
+ for iface_line in ifaces_out:
+ if regex_filter.search(iface_line):
+ ifaces_dict[int(regex_filter.search(iface_line).group('iface_index'))] = regex_filter.search(iface_line).group('iface_name')
+
+ # return dictioanry
+ return ifaces_dict
+
+
+# get list of flows
+def _get_flows_list():
+ # run command to get flows list
+ out = cmd(f'/usr/bin/pmacct -s -O json -T flows -p {uacctd_pipefile}',
+ message='Failed to get flows list')
+
+ # read output
+ flows_out = out.splitlines()
+
+ # make a list with flows
+ flows_list = []
+ for flow_line in flows_out:
+ try:
+ flows_list.append(loads(flow_line))
+ except Exception as err:
+ syslog.error('Unable to read flow info: {}'.format(err))
+
+ # return list of flows
+ return flows_list
+
+
+# filter and format flows
+def _flows_filter(flows, ifaces):
+ # predefine filtered flows list
+ flows_filtered = []
+
+ # add interface names to flows
+ for flow in flows:
+ if flow['iface_in'] in ifaces:
+ flow['iface_in_name'] = ifaces[flow['iface_in']]
+ else:
+ flow['iface_in_name'] = 'unknown'
+
+ # iterate through flows list
+ for flow in flows:
+ # filter by interface
+ if cmd_args.interface:
+ if flow['iface_in_name'] != cmd_args.interface:
+ continue
+ # filter by host
+ if cmd_args.host:
+ if flow['ip_src'] != cmd_args.host and flow['ip_dst'] != cmd_args.host:
+ continue
+ # filter by ports
+ if cmd_args.ports:
+ if cmd_args.ports['type'] == 'single':
+ if flow['port_src'] != cmd_args.ports['value'] and flow['port_dst'] != cmd_args.ports['value']:
+ continue
+ else:
+ if flow['port_src'] not in cmd_args.ports['value'] and flow['port_dst'] not in cmd_args.ports['value']:
+ continue
+ # add filtered flows to new list
+ flows_filtered.append(flow)
+
+ # stop adding if we already reached top count
+ if cmd_args.top:
+ if len(flows_filtered) == cmd_args.top:
+ break
+
+ # return filtered flows
+ return flows_filtered
+
+
+# print flow table
+def _flows_table_print(flows):
+ # define headers and body
+ table_headers = ['IN_IFACE', 'SRC_MAC', 'DST_MAC', 'SRC_IP', 'DST_IP', 'SRC_PORT', 'DST_PORT', 'PROTOCOL', 'TOS', 'PACKETS', 'FLOWS', 'BYTES']
+ table_body = []
+ # convert flows to list
+ for flow in flows:
+ table_line = [
+ flow.get('iface_in_name'),
+ flow.get('mac_src'),
+ flow.get('mac_dst'),
+ flow.get('ip_src'),
+ flow.get('ip_dst'),
+ flow.get('port_src'),
+ flow.get('port_dst'),
+ flow.get('ip_proto'),
+ flow.get('tos'),
+ flow.get('packets'),
+ flow.get('flows'),
+ flow.get('bytes')
+ ]
+ table_body.append(table_line)
+ # configure and fill table
+ table = tabulate(table_body, table_headers, tablefmt="simple")
+
+ # print formatted table
+ try:
+ print(table)
+ except IOError:
+ sys.exit(0)
+ except KeyboardInterrupt:
+ sys.exit(0)
+
+
+# check if in-memory table is active
+def _check_imt():
+ if not os.path.exists(uacctd_pipefile):
+ print("In-memory table is not available")
+ sys.exit(1)
+
+
+# define program arguments
+cmd_args_parser = argparse.ArgumentParser(description='show flow-accounting')
+cmd_args_parser.add_argument('--action', choices=['show', 'clear', 'restart'], required=True, help='command to flow-accounting daemon')
+cmd_args_parser.add_argument('--filter', choices=['interface', 'host', 'ports', 'top'], required=False, nargs='*', help='filter flows to display')
+cmd_args_parser.add_argument('--interface', required=False, help='interface name for output filtration')
+cmd_args_parser.add_argument('--host', type=str, required=False, help='host address for output filtering')
+cmd_args_parser.add_argument('--ports', type=str, required=False, help='port number, range or list for output filtering')
+cmd_args_parser.add_argument('--top', type=int, required=False, help='top records for output filtering')
+# parse arguments
+cmd_args = cmd_args_parser.parse_args()
+
+try:
+ if cmd_args.host:
+ check_host(cmd_args.host)
+
+ if cmd_args.ports:
+ cmd_args.ports = parse_ports(cmd_args.ports)
+except ValueError as e:
+ print(e)
+ sys.exit(1)
+
+# main logic
+# do nothing if uacctd daemon is not running
+if not _uacctd_running():
+ print("flow-accounting is not active")
+ sys.exit(1)
+
+# restart pmacct daemon
+if cmd_args.action == 'restart':
+ # run command to restart flow-accounting
+ cmd('systemctl restart uacctd.service',
+ message='Failed to restart flow-accounting')
+
+# clear in-memory collected flows
+if cmd_args.action == 'clear':
+ _check_imt()
+ # run command to clear flows
+ cmd(f'/usr/bin/pmacct -e -p {uacctd_pipefile}',
+ message='Failed to clear flows')
+
+# show table with flows
+if cmd_args.action == 'show':
+ _check_imt()
+ # get interfaces index and names
+ ifaces_dict = _get_ifaces_dict()
+ # get flows
+ flows_list = _get_flows_list()
+
+ # filter and format flows
+ tabledata = _flows_filter(flows_list, ifaces_dict)
+
+ # print flows
+ _flows_table_print(tabledata)
+
+sys.exit(0)
diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py
new file mode 100755
index 000000000..df4486bce
--- /dev/null
+++ b/src/op_mode/format_disk.py
@@ -0,0 +1,143 @@
+#!/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 argparse
+import os
+import re
+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()
+ with open('/proc/partitions') as partitions_file:
+ for line in partitions_file:
+ fields = line.strip().split()
+ if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name':
+ disks.add(fields[3])
+ return disks
+
+
+def is_busy(disk: str):
+ """Check if given disk device is busy by re-reading it's partition table"""
+ return call(f'sudo blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0
+
+
+def backup_partitions(disk: str):
+ """Save sfdisk partitions output to a backup file"""
+
+ device_path = '/dev/' + disk
+ backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M')
+ backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts)
+ cmd(f'sudo /sbin/sfdisk -d {device_path} > {backup_file}')
+
+
+def list_partitions(disk: str):
+ """List partition numbers of a given disk"""
+
+ parts = set()
+ part_num_expr = re.compile(disk + '([0-9]+)')
+ with open('/proc/partitions') as partitions_file:
+ for line in partitions_file:
+ fields = line.strip().split()
+ if len(fields) == 4 and fields[3] != 'name' and part_num_expr.match(fields[3]):
+ part_idx = part_num_expr.match(fields[3]).group(1)
+ parts.add(int(part_idx))
+ return parts
+
+
+def delete_partition(disk: str, partition_idx: int):
+ cmd(f'sudo /sbin/parted /dev/{disk} rm {partition_idx}')
+
+
+def format_disk_like(target: str, proto: str):
+ cmd(f'sudo /sbin/sfdisk -d /dev/{proto} | sudo /sbin/sfdisk --force /dev/{target}')
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ group = parser.add_argument_group()
+ group.add_argument('-t', '--target', type=str, required=True, help='Target device to format')
+ group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference')
+ args = parser.parse_args()
+
+ if not is_admin():
+ print('Must be admin or root to format disk')
+ sys.exit(1)
+
+ target_disk = args.target
+ eligible_target_disks = list_disks()
+
+ proto_disk = args.proto
+ eligible_proto_disks = eligible_target_disks.copy()
+ eligible_proto_disks.remove(target_disk)
+
+ fmt = {
+ 'target_disk': target_disk,
+ 'proto_disk': proto_disk,
+ }
+
+ if proto_disk == target_disk:
+ print('The two disk drives must be different.')
+ sys.exit(1)
+
+ if not os.path.exists('/dev/' + proto_disk):
+ print('Device /dev/{proto_disk} does not exist'.format_map(fmt))
+ sys.exit(1)
+
+ if not os.path.exists('/dev/' + target_disk):
+ print('Device /dev/{target_disk} does not exist'.format_map(fmt))
+ sys.exit(1)
+
+ if target_disk not in eligible_target_disks:
+ print('Device {target_disk} can not be formatted'.format_map(fmt))
+ sys.exit(1)
+
+ if proto_disk not in eligible_proto_disks:
+ print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt))
+ sys.exit(1)
+
+ if is_busy(target_disk):
+ print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt))
+ sys.exit(1)
+
+ print('This will re-format disk {target_disk} so that it has the same disk\n'
+ 'partion sizes and offsets as {proto_disk}. This will not copy\n'
+ 'data from {proto_disk} to {target_disk}. But this will erase all\n'
+ 'data on {target_disk}.\n'.format_map(fmt))
+
+ if not ask_yes_no("Do you wish to proceed?"):
+ print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt))
+ sys.exit(0)
+
+ print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt))
+
+ print('Making backup copy of partitions...')
+ backup_partitions(target_disk)
+ sleep(1)
+
+ print('Deleting old partitions...')
+ for p in list_partitions(target_disk):
+ delete_partition(disk=target_disk, partition_idx=p)
+
+ print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt))
+ format_disk_like(target=target_disk, proto=proto_disk)
+ print('Done.')
diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py
new file mode 100755
index 000000000..cbc9ef973
--- /dev/null
+++ b/src/op_mode/generate_ssh_server_key.py
@@ -0,0 +1,26 @@
+#!/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/>.
+
+from sys import exit
+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?'):
+ exit(0)
+
+cmd('rm -v /etc/ssh/ssh_host_*')
+cmd('dpkg-reconfigure openssh-server')
+cmd('systemctl restart ssh.service')
diff --git a/src/op_mode/ipoe-control.py b/src/op_mode/ipoe-control.py
new file mode 100755
index 000000000..7111498b2
--- /dev/null
+++ b/src/op_mode/ipoe-control.py
@@ -0,0 +1,65 @@
+#!/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 sys
+import argparse
+
+from vyos.config import Config
+from vyos.util import popen, run
+
+cmd_dict = {
+ 'cmd_base' : '/usr/bin/accel-cmd -p 2002 ',
+ 'selector' : ['if', 'username', 'sid'],
+ 'actions' : {
+ 'show_sessions' : 'show sessions',
+ 'show_stat' : 'show stat',
+ 'terminate' : 'teminate'
+ }
+}
+
+def is_ipoe_configured():
+ if not Config().exists_effective('service ipoe-server'):
+ print("Service IPoE is not configured")
+ sys.exit(1)
+
+def main():
+ #parese args
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--action', help='Control action', required=True)
+ parser.add_argument('--selector', help='Selector username|ifname|sid', required=False)
+ parser.add_argument('--target', help='Target must contain username|ifname|sid', required=False)
+ args = parser.parse_args()
+
+
+ # Check is IPoE configured
+ is_ipoe_configured()
+
+ if args.action == "restart":
+ run(cmd_dict['cmd_base'] + "restart")
+ sys.exit(0)
+
+ if args.action in cmd_dict['actions']:
+ if args.selector in cmd_dict['selector'] and args.target:
+ run(cmd_dict['cmd_base'] + "{0} {1} {2}".format(args.action, args.selector, args.target))
+ else:
+ output, err = popen(cmd_dict['cmd_base'] + cmd_dict['actions'][args.action], decode='utf-8')
+ if not err:
+ print(output)
+ else:
+ print("IPoE server is not running")
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py
new file mode 100755
index 000000000..06958c605
--- /dev/null
+++ b/src/op_mode/lldp_op.py
@@ -0,0 +1,125 @@
+#!/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 argparse
+import jinja2
+import json
+
+from sys import exit
+from tabulate import tabulate
+
+from vyos.util import cmd
+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("-d", "--detail", action="store_true", help="Show detailes LLDP neighbor information on all interfaces")
+parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface")
+
+# Please be careful if you edit the template.
+lldp_out = """Capability Codes: R - Router, B - Bridge, W - Wlan r - Repeater, S - Station
+ D - Docsis, T - Telephone, O - Other
+
+Device ID Local Proto Cap Platform Port ID
+--------- ----- ----- --- -------- -------
+{% for neighbor in neighbors %}
+{% for local_if, info in neighbor.items() %}
+{{ "%-25s" | format(info.chassis) }} {{ "%-9s" | format(local_if) }} {{ "%-6s" | format(info.proto) }} {{ "%-5s" | format(info.capabilities) }} {{ "%-20s" | format(info.platform[:18]) }} {{ info.remote_if }}
+{% endfor %}
+{% endfor %}
+"""
+
+def get_neighbors():
+ return cmd('/usr/sbin/lldpcli -f json show neighbors')
+
+def parse_data(data):
+ output = []
+ for tmp in data:
+ for local_if, values in tmp.items():
+ for chassis, c_value in values.get('chassis', {}).items():
+ capabilities = c_value['capability']
+ if isinstance(capabilities, dict):
+ capabilities = [capabilities]
+
+ cap = ''
+ for capability in capabilities:
+ if capability['enabled']:
+ if capability['type'] == 'Router':
+ cap += 'R'
+ if capability['type'] == 'Bridge':
+ cap += 'B'
+ if capability['type'] == 'Wlan':
+ cap += 'W'
+ if capability['type'] == 'Station':
+ cap += 'S'
+ if capability['type'] == 'Repeater':
+ cap += 'r'
+ if capability['type'] == 'Telephone':
+ cap += 'T'
+ if capability['type'] == 'Docsis':
+ cap += 'D'
+ if capability['type'] == 'Other':
+ cap += 'O'
+
+
+ remote_if = 'Unknown'
+ if 'descr' in values.get('port', {}):
+ remote_if = values.get('port', {}).get('descr')
+ elif 'id' in values.get('port', {}):
+ remote_if = values.get('port', {}).get('id').get('value', 'Unknown')
+
+ output.append({local_if: {'chassis': chassis,
+ 'remote_if': remote_if,
+ 'proto': values.get('via','Unknown'),
+ 'platform': c_value.get('descr', 'Unknown'),
+ 'capabilities': cap}})
+
+
+ output = {'neighbors': output}
+ return output
+
+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.detail:
+ print(cmd('/usr/sbin/lldpctl -f plain'))
+ exit(0)
+ elif args.all or args.interface:
+ tmp = json.loads(get_neighbors())
+
+ if args.all:
+ neighbors = tmp['lldp']['interface']
+ elif args.interface:
+ neighbors = []
+ for neighbor in tmp['lldp']['interface']:
+ if args.interface in neighbor:
+ neighbors.append(neighbor)
+
+ else:
+ parser.print_help()
+ exit(1)
+
+ tmpl = jinja2.Template(lldp_out, trim_blocks=True)
+ config_text = tmpl.render(parse_data(neighbors))
+ print(config_text)
+
+ exit(0)
diff --git a/src/op_mode/maya_date.py b/src/op_mode/maya_date.py
new file mode 100755
index 000000000..847b543e0
--- /dev/null
+++ b/src/op_mode/maya_date.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2013, 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
+
+class MayaDate(object):
+ """ Converts number of days since UNIX epoch
+ to the Maya calendar date.
+
+ Ancient Maya people used three independent calendars for
+ different purposes.
+
+ The long count calendar is for recording historical events.
+ It represents the number of days passed
+ since some date in the past the Maya believed is the day
+ our world was created.
+
+ Tzolkin calendar is for religious purposes, it has
+ two independent cycles of 13 and 20 days, where 13 day
+ cycle days are numbered, and 20 day cycle days are named.
+
+ Haab calendar is for agriculture and daily life, it's a
+ 365 day calendar with 18 months 20 days each, and 5
+ nameless days.
+
+ The smallest unit of the long count calendar is one day (kin).
+ """
+
+ """ The long count calendar uses five different base 18 or base 20
+ cycles. Modern scholars write long count calendar dates in a dot separated format
+ from longest to shortest cycle,
+ <baktun>.<katun>.<tun>.<winal>.<kin>
+ for example, "13.0.0.9.2".
+
+ Classic version actually used by the ancient Maya wraps around
+ every 13th baktun, but modern historians often use longer cycles
+ such as piktun = 20 baktun.
+
+ """
+ kin = 1
+ winal = 20 # 20 kin
+ tun = 360 # 18 winal
+ katun = 7200 # 20 tun
+ baktun = 144000 # 20 katun
+
+ """ Tzolk'in date is composed of two independent cycles.
+ Dates repeat every 260 days, 13 Ajaw is considered the end
+ of tzolk'in.
+
+ Every day of the 20 day cycle has unique name, we number
+ them from zero so it's easier to map the remainder to day:
+ """
+ tzolkin_days = { 0: "Imix'",
+ 1: "Ik'",
+ 2: "Ak'b'al",
+ 3: "K'an",
+ 4: "Chikchan",
+ 5: "Kimi",
+ 6: "Manik'",
+ 7: "Lamat",
+ 8: "Muluk",
+ 9: "Ok",
+ 10: "Chuwen",
+ 11: "Eb'",
+ 12: "B'en",
+ 13: "Ix",
+ 14: "Men",
+ 15: "Kib'",
+ 16: "Kab'an",
+ 17: "Etz'nab'",
+ 18: "Kawak",
+ 19: "Ajaw" }
+
+ """ As said above, haab (year) has 19 months. Only 18 are
+ true months of 20 days each, the remaining 5 days called "wayeb"
+ do not really belong to any month, but we think of them as a pseudo-month
+ for convenience.
+
+ Also, note that days of the month are actually numbered from 0, not from 1,
+ it's not for technical reasons.
+ """
+ haab_months = { 0: "Pop",
+ 1: "Wo'",
+ 2: "Sip",
+ 3: "Sotz'",
+ 4: "Sek",
+ 5: "Xul",
+ 6: "Yaxk'in'",
+ 7: "Mol",
+ 8: "Ch'en",
+ 9: "Yax",
+ 10: "Sak'",
+ 11: "Keh",
+ 12: "Mak",
+ 13: "K'ank'in",
+ 14: "Muwan'",
+ 15: "Pax",
+ 16: "K'ayab",
+ 17: "Kumk'u",
+ 18: "Wayeb'" }
+
+ """ Now we need to map the beginning of UNIX epoch
+ (Jan 1 1970 00:00 UTC) to the beginning of the long count
+ calendar (0.0.0.0.0, 4 Ajaw, 8 Kumk'u).
+
+ The problem with mapping the long count calendar to
+ any other is that its start date is not known exactly.
+
+ The most widely accepted hypothesis suggests it was
+ August 11, 3114 BC gregorian date. In this case UNIX epoch
+ starts on 12.17.16.7.5, 13 Chikchan, 3 K'ank'in
+
+ It's known as Goodman-Martinez-Thompson (GMT) correlation
+ constant.
+ """
+ start_days = 1856305
+
+ """ Seconds in day, for conversion from timestamp """
+ seconds_in_day = 60 * 60 * 24
+
+ def __init__(self, timestamp):
+ if timestamp is None:
+ self.days = self.start_days
+ else:
+ self.days = self.start_days + (int(timestamp) // self.seconds_in_day)
+
+ def long_count_date(self):
+ """ Returns long count date string """
+ days = self.days
+
+ cur_baktun = days // self.baktun
+ days = days % self.baktun
+
+ cur_katun = days // self.katun
+ days = days % self.katun
+
+ cur_tun = days // self.tun
+ days = days % self.tun
+
+ cur_winal = days // self.winal
+ days = days % self.winal
+
+ cur_kin = days
+
+ longcount_string = "{0}.{1}.{2}.{3}.{4}".format( cur_baktun,
+ cur_katun,
+ cur_tun,
+ cur_winal,
+ cur_kin )
+ return(longcount_string)
+
+ def tzolkin_date(self):
+ """ Returns tzolkin date string """
+ days = self.days
+
+ """ The start date is not the beginning of both cycles,
+ it's 4 Ajaw. So we need to add 4 to the 13 days cycle day,
+ and substract 1 from the 20 day cycle to get correct result.
+ """
+ tzolkin_13 = (days + 4) % 13
+ tzolkin_20 = (days - 1) % 20
+
+ tzolkin_string = "{0} {1}".format(tzolkin_13, self.tzolkin_days[tzolkin_20])
+
+ return(tzolkin_string)
+
+ def haab_date(self):
+ """ Returns haab date string.
+
+ The time start on 8 Kumk'u rather than 0 Pop, which is
+ 17 days before the new haab, so we need to substract 17
+ from the current date to get correct result.
+ """
+ days = self.days
+
+ haab_day = (days - 17) % 365
+ haab_month = haab_day // 20
+ haab_day_of_month = haab_day % 20
+
+ haab_string = "{0} {1}".format(haab_day_of_month, self.haab_months[haab_month])
+
+ return(haab_string)
+
+ def date(self):
+ return("{0}, {1}, {2}".format( self.long_count_date(), self.tzolkin_date(), self.haab_date() ))
+
+if __name__ == '__main__':
+ try:
+ timestamp = sys.argv[1]
+ except:
+ print("Please specify timestamp in the argument")
+ sys.exit(1)
+
+ maya_date = MayaDate(timestamp)
+ print(maya_date.date())
diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py
new file mode 100755
index 000000000..29b430d53
--- /dev/null
+++ b/src/op_mode/ping.py
@@ -0,0 +1,230 @@
+#! /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 sys
+import socket
+import ipaddress
+
+options = {
+ 'audible': {
+ 'ping': '{command} -a',
+ 'type': 'noarg',
+ 'help': 'Make a noise on ping'
+ },
+ 'adaptive': {
+ 'ping': '{command} -A',
+ 'type': 'noarg',
+ 'help': 'Adativly set interpacket interval'
+ },
+ 'allow-broadcast': {
+ 'ping': '{command} -b',
+ 'type': 'noarg',
+ 'help': 'Ping broadcast address'
+ },
+ 'bypass-route': {
+ 'ping': '{command} -r',
+ 'type': 'noarg',
+ 'help': 'Bypass normal routing tables'
+ },
+ 'count': {
+ 'ping': '{command} -c {value}',
+ 'type': '<requests>',
+ 'help': 'Number of requests to send'
+ },
+ 'deadline': {
+ 'ping': '{command} -w {value}',
+ 'type': '<seconds>',
+ 'help': 'Number of seconds before ping exits'
+ },
+ 'flood': {
+ 'ping': 'sudo {command} -f',
+ 'type': 'noarg',
+ 'help': 'Send 100 requests per second'
+ },
+ 'interface': {
+ 'ping': '{command} -I {value}',
+ 'type': '<interface> <X.X.X.X> <h:h:h:h:h:h:h:h>',
+ 'help': 'Interface to use as source for ping'
+ },
+ 'interval': {
+ 'ping': '{command} -i {value}',
+ 'type': '<seconds>',
+ 'help': 'Number of seconds to wait between requests'
+ },
+ 'mark': {
+ 'ping': '{command} -m {value}',
+ 'type': '<fwmark>',
+ 'help': 'Mark request for special processing'
+ },
+ 'numeric': {
+ 'ping': '{command} -n',
+ 'type': 'noarg',
+ 'help': 'Do not resolve DNS names'
+ },
+ 'no-loopback': {
+ 'ping': '{command} -L',
+ 'type': 'noarg',
+ 'help': 'Supress loopback of multicast pings'
+ },
+ 'pattern': {
+ 'ping': '{command} -p {value}',
+ 'type': '<pattern>',
+ 'help': 'Pattern to fill out the packet'
+ },
+ 'timestamp': {
+ 'ping': '{command} -D',
+ 'type': 'noarg',
+ 'help': 'Print timestamp of output'
+ },
+ 'tos': {
+ 'ping': '{command} -Q {value}',
+ 'type': '<tos>',
+ 'help': 'Mark packets with specified TOS'
+ },
+ 'quiet': {
+ 'ping': '{command} -q',
+ 'type': 'noarg',
+ 'help': 'Only print summary lines'
+ },
+ 'record-route': {
+ 'ping': '{command} -R',
+ 'type': 'noarg',
+ 'help': 'Record route the packet takes'
+ },
+ 'size': {
+ 'ping': '{command} -s {value}',
+ 'type': '<bytes>',
+ 'help': 'Number of bytes to send'
+ },
+ 'ttl': {
+ 'ping': '{command} -t {value}',
+ 'type': '<ttl>',
+ 'help': 'Maximum packet lifetime'
+ },
+ 'vrf': {
+ 'ping': 'sudo ip vrf exec {value} {command}',
+ 'type': '<vrf>',
+ 'help': 'Use specified VRF table',
+ 'dflt': 'default',
+ },
+ 'verbose': {
+ 'ping': '{command} -v',
+ 'type': 'noarg',
+ 'help': 'Verbose output'}
+}
+
+ping = {
+ 4: '/bin/ping',
+ 6: '/bin/ping6',
+}
+
+
+class List (list):
+ def first (self):
+ return self.pop(0) if self else ''
+
+ def last(self):
+ return self.pop() if self else ''
+
+ def prepend(self,value):
+ self.insert(0,value)
+
+
+def expension_failure(option, completions):
+ reason = 'Ambiguous' if completions else 'Invalid'
+ sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option))
+ if completions:
+ sys.stderr.write(' Possible completions:\n ')
+ sys.stderr.write('\n '.join(completions))
+ sys.stderr.write('\n')
+ sys.stdout.write('<nocomps>')
+ sys.exit(1)
+
+
+def complete(prefix):
+ return [o for o in options if o.startswith(prefix)]
+
+
+def convert(command, args):
+ while args:
+ shortname = args.first()
+ longnames = complete(shortname)
+ if len(longnames) != 1:
+ expension_failure(shortname, longnames)
+ longname = longnames[0]
+ if options[longname]['type'] == 'noarg':
+ command = options[longname]['ping'].format(
+ command=command, value='')
+ elif not args:
+ sys.exit(f'ping: missing argument for {longname} option')
+ else:
+ command = options[longname]['ping'].format(
+ command=command, value=args.first())
+ return command
+
+
+if __name__ == '__main__':
+ args = List(sys.argv[1:])
+ host = args.first()
+
+ if not host:
+ sys.exit("ping: Missing host")
+
+ if host == '--get-options':
+ args.first() # pop ping
+ args.first() # pop IP
+ while args:
+ option = args.first()
+
+ matched = complete(option)
+ if not args:
+ sys.stdout.write(' '.join(matched))
+ sys.exit(0)
+
+ if len(matched) > 1 :
+ sys.stdout.write(' '.join(matched))
+ sys.exit(0)
+
+ if options[matched[0]]['type'] == 'noarg':
+ continue
+
+ value = args.first()
+ if not args:
+ matched = complete(option)
+ sys.stdout.write(options[matched[0]]['type'])
+ sys.exit(0)
+
+ for name,option in options.items():
+ if 'dflt' in option and name not in args:
+ args.append(name)
+ args.append(option['dflt'])
+
+ try:
+ ip = socket.gethostbyname(host)
+ except socket.gaierror:
+ ip = host
+
+ try:
+ version = ipaddress.ip_address(ip).version
+ except ValueError:
+ sys.exit(f'ping: Unknown host: {host}')
+
+ command = convert(ping[version],args)
+
+ # print(f'{command} {host}')
+ os.system(f'{command} {host}')
+
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
new file mode 100755
index 000000000..69af427ec
--- /dev/null
+++ b/src/op_mode/powerctrl.py
@@ -0,0 +1,193 @@
+#!/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 re
+
+from argparse import ArgumentParser
+from datetime import datetime, timedelta, time as type_time, date as type_date
+from sys import exit
+from time import time
+
+from vyos.util import ask_yes_no, cmd, call, run, STDOUT
+
+systemd_sched_file = "/run/systemd/shutdown/scheduled"
+
+def utc2local(datetime):
+ now = time()
+ offs = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now)
+ return datetime + offs
+
+def parse_time(s):
+ try:
+ if re.match(r'^\d{1,2}$', s):
+ return datetime.strptime(s, "%M").time()
+ else:
+ return datetime.strptime(s, "%H:%M").time()
+ except ValueError:
+ return None
+
+
+def parse_date(s):
+ for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]:
+ try:
+ return datetime.strptime(s, fmt).date()
+ except ValueError:
+ continue
+ # If nothing matched...
+ return None
+
+
+def get_shutdown_status():
+ 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()
+ if output and 'MODE' in output:
+ dt = datetime.strptime(output['DATETIME'], '%Y-%m-%d %H:%M:%S')
+ if output['MODE'] == 'reboot':
+ print("Reboot is scheduled", utc2local(dt))
+ elif output['MODE'] == 'poweroff':
+ print("Poweroff is scheduled", utc2local(dt))
+ else:
+ print("Reboot or poweroff is not scheduled")
+
+
+def cancel_shutdown():
+ output = get_shutdown_status()
+ if output and 'MODE' in output:
+ timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ try:
+ run('/sbin/shutdown -c --no-wall')
+ except OSError as e:
+ exit("Could not cancel a reboot or poweroff: %s" % e)
+
+ message = 'Scheduled {} has been cancelled {}'.format(output['MODE'], timenow)
+ run(f'wall {message} > /dev/null 2>&1')
+ else:
+ print("Reboot or poweroff is not scheduled")
+
+
+def execute_shutdown(time, reboot=True, ask=True):
+ if not ask:
+ action = "reboot" if reboot else "poweroff"
+ if not ask_yes_no("Are you sure you want to %s this system?" % action):
+ exit(0)
+
+ action = "-r" if reboot else "-P"
+
+ if len(time) == 0:
+ # T870 legacy reboot job support
+ chk_vyatta_based_reboots()
+ ###
+
+ out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT)
+ print(out.split(",", 1)[0])
+ return
+ elif len(time) == 1:
+ # Assume the argument is just time
+ ts = parse_time(time[0])
+ if ts:
+ cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT)
+ else:
+ exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
+ elif len(time) == 2:
+ # Assume it's date and time
+ ts = parse_time(time[0])
+ ds = parse_date(time[1])
+ if ts and ds:
+ t = datetime.combine(ds, ts)
+ td = t - datetime.now()
+ t2 = 1 + int(td.total_seconds())//60 # Get total minutes
+ cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT)
+ else:
+ if not ts:
+ exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
+ else:
+ exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1]))
+ else:
+ exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM")
+ check_shutdown()
+
+
+def chk_vyatta_based_reboots():
+ # T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed
+ # legacy scheduled reboot s are using at and store the is as /var/run/<name>.job
+ # name is the node of scheduled the job, commit-confirm checks for that
+
+ f = r'/var/run/confirm.job'
+ if os.path.exists(f):
+ jid = open(f).read().strip()
+ if jid != 0:
+ call(f'sudo atrm {jid}')
+ os.remove(f)
+
+
+def main():
+ parser = ArgumentParser()
+ parser.add_argument("--yes", "-y",
+ help="Do not ask for confirmation",
+ action="store_true",
+ dest="yes")
+ action = parser.add_mutually_exclusive_group(required=True)
+ action.add_argument("--reboot", "-r",
+ help="Reboot the system",
+ nargs="*",
+ metavar="Minutes|HH:MM")
+
+ action.add_argument("--poweroff", "-p",
+ help="Poweroff the system",
+ nargs="*",
+ metavar="Minutes|HH:MM")
+
+ action.add_argument("--cancel", "-c",
+ help="Cancel pending shutdown",
+ action="store_true")
+
+ action.add_argument("--check",
+ help="Check pending chutdown",
+ action="store_true")
+ args = parser.parse_args()
+
+ try:
+ if args.reboot is not None:
+ execute_shutdown(args.reboot, reboot=True, ask=args.yes)
+ if args.poweroff is not None:
+ execute_shutdown(args.poweroff, reboot=False, ask=args.yes)
+ if args.cancel:
+ cancel_shutdown()
+ if args.check:
+ check_shutdown()
+ except KeyboardInterrupt:
+ exit("Interrupted")
+
+if __name__ == "__main__":
+ main()
diff --git a/src/op_mode/ppp-server-ctrl.py b/src/op_mode/ppp-server-ctrl.py
new file mode 100755
index 000000000..171107b4a
--- /dev/null
+++ b/src/op_mode/ppp-server-ctrl.py
@@ -0,0 +1,71 @@
+#!/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
+import argparse
+
+from vyos.config import Config
+from vyos.util import popen, DEVNULL
+
+cmd_dict = {
+ 'cmd_base' : '/usr/bin/accel-cmd -p {} ',
+ 'vpn_types' : {
+ 'pppoe' : 2001,
+ 'pptp' : 2003,
+ 'l2tp' : 2004,
+ 'sstp' : 2005
+ },
+ 'conf_proto' : {
+ 'pppoe' : 'service pppoe-server',
+ 'pptp' : 'vpn pptp remote-access',
+ 'l2tp' : 'vpn l2tp remote-access',
+ 'sstp' : 'vpn sstp'
+ }
+}
+
+def is_service_configured(proto):
+ if not Config().exists_effective(cmd_dict['conf_proto'][proto]):
+ print("Service {} is not configured".format(proto))
+ sys.exit(1)
+
+def main():
+ #parese args
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--proto', help='Possible protocols pppoe|pptp|l2tp|sstp', required=True)
+ parser.add_argument('--action', help='Action command', required=True)
+ args = parser.parse_args()
+
+ if args.proto in cmd_dict['vpn_types'] and args.action:
+ # Check is service configured
+ is_service_configured(args.proto)
+
+ if args.action == "show sessions":
+ ses_pattern = " ifname,username,ip,ip6,ip6-dp,calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes"
+ else:
+ ses_pattern = ""
+
+ output, err = popen(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][args.proto]) + args.action + ses_pattern, stderr=DEVNULL, decode='utf-8')
+ if not err:
+ print(output)
+ else:
+ print("{} server is not running".format(args.proto))
+
+ else:
+ print("Param --proto and --action required")
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py
new file mode 100755
index 000000000..dbd3eb4d1
--- /dev/null
+++ b/src/op_mode/reset_openvpn.py
@@ -0,0 +1,31 @@
+#!/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 sys import argv, exit
+from vyos.util import call
+
+if __name__ == '__main__':
+ if (len(argv) < 1):
+ print('Must specify OpenVPN interface name!')
+ exit(1)
+
+ interface = argv[1]
+ if os.path.isfile(f'/run/openvpn/{interface}.conf'):
+ call(f'systemctl restart openvpn@{interface}.service')
+ else:
+ print(f'OpenVPN interface "{interface}" does not exist!')
+ exit(1)
diff --git a/src/op_mode/reset_vpn.py b/src/op_mode/reset_vpn.py
new file mode 100755
index 000000000..3a0ad941c
--- /dev/null
+++ b/src/op_mode/reset_vpn.py
@@ -0,0 +1,72 @@
+#!/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
+import argparse
+
+from vyos.util import run
+
+cmd_dict = {
+ 'cmd_base' : '/usr/bin/accel-cmd -p {} terminate {} {}',
+ 'vpn_types' : {
+ 'pptp' : 2003,
+ 'l2tp' : 2004,
+ 'sstp' : 2005
+ }
+}
+
+def terminate_sessions(username='', interface='', protocol=''):
+
+ # Reset vpn connections by username
+ if protocol in cmd_dict['vpn_types']:
+ if username == "all_users":
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'all', ''))
+ else:
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'username', username))
+
+ # Reset vpn connections by ifname
+ elif interface:
+ for proto in cmd_dict['vpn_types']:
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'if', interface))
+
+ elif username:
+ # Reset all vpn connections
+ if username == "all_users":
+ for proto in cmd_dict['vpn_types']:
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'all', ''))
+ else:
+ for proto in cmd_dict['vpn_types']:
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'username', username))
+
+def main():
+ #parese args
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--username', help='Terminate by username (all_users used for disconnect all users)', required=False)
+ parser.add_argument('--interface', help='Terminate by interface', required=False)
+ parser.add_argument('--protocol', help='Set protocol (pptp|l2tp|sstp)', required=False)
+ args = parser.parse_args()
+
+ if args.username or args.interface:
+ terminate_sessions(username=args.username, interface=args.interface, protocol=args.protocol)
+ else:
+ print("Param --username or --interface required")
+ sys.exit(1)
+
+ terminate_sessions()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py
new file mode 100755
index 000000000..af4fb2d15
--- /dev/null
+++ b/src/op_mode/restart_dhcp_relay.py
@@ -0,0 +1,55 @@
+#!/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/>.
+
+# File: restart_dhcp_relay.py
+# Purpose:
+# Restart IPv4 and IPv6 DHCP relay instances of dhcrelay service
+
+import sys
+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")
+parser.add_argument("--ipv6", action="store_true", help="Restart IPv6 DHCP relay")
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+ c = vyos.config.Config()
+
+ if args.ipv4:
+ # Do nothing if service is not configured
+ if not c.exists_effective('service dhcp-relay'):
+ print("DHCP relay service not configured")
+ else:
+ call('systemctl restart isc-dhcp-server.service')
+
+ sys.exit(0)
+ elif args.ipv6:
+ # Do nothing if service is not configured
+ if not c.exists_effective('service dhcpv6-relay'):
+ print("DHCPv6 relay service not configured")
+ else:
+ call('systemctl restart isc-dhcp-server6.service')
+
+ sys.exit(0)
+ else:
+ parser.print_help()
+ sys.exit(1)
diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py
new file mode 100755
index 000000000..d1b66b33f
--- /dev/null
+++ b/src/op_mode/restart_frr.py
@@ -0,0 +1,197 @@
+#!/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
+import argparse
+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'
+frrconfig_tmp = '/tmp/frr_restart'
+
+# configure logging
+logger = logging.getLogger(__name__)
+logs_handler = SysLogHandler('/dev/log')
+logs_handler.setFormatter(logging.Formatter('%(filename)s: %(message)s'))
+logger.addHandler(logs_handler)
+logger.setLevel(logging.INFO)
+
+# check if it is safe to restart FRR
+def _check_safety():
+ try:
+ # print warning
+ answer = input("WARNING: This is a potentially unsafe function! You may lose the connection to the router or active configuration after running this command. Use it at your own risk! Continue? [y/N]: ")
+ if not answer.lower() == "y":
+ logger.error("User aborted command")
+ return False
+
+ # check if another restart process already running
+ if len([process for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']) if 'python' in process.info['name'] and 'restart_frr.py' in process.info['cmdline'][1]]) > 1:
+ logger.error("Another restart_frr.py already running")
+ answer = input("Another restart_frr.py process is already running. It is unsafe to continue. Do you want to process anyway? [y/N]: ")
+ if not answer.lower() == "y":
+ return False
+
+ # check if watchfrr.sh is running
+ for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']):
+ if 'bash' in process.info['name'] and watchfrr in process.info['cmdline']:
+ logger.error("Another {} already running".format(watchfrr))
+ answer = input("Another {} process is already running. It is unsafe to continue. Do you want to process anyway? [y/N]: ".format(watchfrr))
+ if not answer.lower() == "y":
+ return False
+
+ # check if vtysh is running
+ for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']):
+ if 'vtysh' in process.info['name']:
+ logger.error("The vtysh is running by another task")
+ answer = input("The vtysh is running by another task. It is unsafe to continue. Do you want to process anyway? [y/N]: ")
+ if not answer.lower() == "y":
+ return False
+
+ # check if temporary directory exists
+ if Path(frrconfig_tmp).exists():
+ logger.error("The temporary directory \"{}\" already exists".format(frrconfig_tmp))
+ answer = input("The temporary directory \"{}\" already exists. It is unsafe to continue. Do you want to process anyway? [y/N]: ".format(frrconfig_tmp))
+ if not answer.lower() == "y":
+ return False
+ except:
+ logger.error("Something goes wrong in _check_safety()")
+ return False
+
+ # return True if all check was passed or user confirmed to ignore they results
+ return True
+
+# write active config to file
+def _write_config():
+ # create temporary directory
+ 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 = call(command)
+ if not return_code == 0:
+ logger.error("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code))
+ return False
+ logger.info("Active config saved to {}".format(frrconfig_tmp))
+ return True
+
+# clear and remove temporary directory
+def _cleanup():
+ tmpdir = Path(frrconfig_tmp)
+ try:
+ if tmpdir.exists():
+ for file in tmpdir.iterdir():
+ file.unlink()
+ tmpdir.rmdir()
+ except:
+ logger.error("Failed to remove temporary directory {}".format(frrconfig_tmp))
+ print("Failed to remove temporary directory {}".format(frrconfig_tmp))
+
+# check if daemon is running
+def _daemon_check(daemon):
+ command = "{} print_status {}".format(watchfrr, daemon)
+ return_code = call(command)
+ if not return_code == 0:
+ logger.error("Daemon \"{}\" is not running".format(daemon))
+ return False
+
+ # return True if all checks were passed
+ return True
+
+# restart daemon
+def _daemon_restart(daemon):
+ command = "{} restart {}".format(watchfrr, daemon)
+ return_code = call(command)
+ if not return_code == 0:
+ logger.error("Failed to restart daemon \"{}\"".format(daemon))
+ return False
+
+ # return True if restarted successfully
+ logger.info("Daemon \"{}\" restarted".format(daemon))
+ return True
+
+# reload old config
+def _reload_config(daemon):
+ if daemon != '':
+ command = "{} -n -b --config_dir {} -d {} 2> /dev/null".format(vtysh, frrconfig_tmp, daemon)
+ else:
+ command = "{} -n -b --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp)
+
+ return_code = call(command)
+ if not return_code == 0:
+ logger.error("Failed to reinstall configuration")
+ return False
+
+ # return True if restarted successfully
+ logger.info("Configuration reinstalled successfully")
+ return True
+
+# check all daemons if they are running
+def _check_args_daemon(daemons):
+ for daemon in daemons:
+ if not _daemon_check(daemon):
+ return False
+ return True
+
+# define program arguments
+cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons')
+cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons')
+cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons')
+# parse arguments
+cmd_args = cmd_args_parser.parse_args()
+
+
+# main logic
+# restart daemon
+if cmd_args.action == 'restart':
+ # check if it is safe to restart FRR
+ if not _check_safety():
+ print("\nOne of the safety checks was failed or user aborted command. Exiting.")
+ sys.exit(1)
+
+ if not _write_config():
+ print("Failed to save active config")
+ _cleanup()
+ sys.exit(1)
+
+ # a little trick to make further commands more clear
+ if not cmd_args.daemon:
+ cmd_args.daemon = ['']
+
+ # check all daemons if they are running
+ if cmd_args.daemon != ['']:
+ if not _check_args_daemon(cmd_args.daemon):
+ print("Warning: some of listed daemons are not running")
+
+ # run command to restart daemon
+ for daemon in cmd_args.daemon:
+ if not _daemon_restart(daemon):
+ print("Failed to restart daemon: {}".format(daemon))
+ _cleanup()
+ sys.exit(1)
+ # reinstall old configuration
+ _reload_config(daemon)
+
+ # cleanup after all actions
+ _cleanup()
+
+sys.exit(0)
diff --git a/src/op_mode/show_acceleration.py b/src/op_mode/show_acceleration.py
new file mode 100755
index 000000000..752db3deb
--- /dev/null
+++ b/src/op_mode/show_acceleration.py
@@ -0,0 +1,116 @@
+#!/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
+import os
+import re
+import argparse
+
+from vyos.config import Config
+from vyos.util import popen
+from vyos.util import call
+
+
+def detect_qat_dev():
+ output, err = popen('sudo lspci -nn', decode='utf-8')
+ if not err:
+ data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output)
+ #If QAT devices found
+ if data:
+ return
+ print("\t No QAT device found")
+ sys.exit(1)
+
+def show_qat_status():
+ detect_qat_dev()
+
+ # Check QAT service
+ if not os.path.exists('/etc/init.d/qat_service'):
+ print("\t QAT service not installed")
+ sys.exit(1)
+
+ # Show QAT service
+ call('sudo /etc/init.d/qat_service status')
+
+# Return QAT devices
+def get_qat_devices():
+ data_st, err = popen('sudo /etc/init.d/qat_service status', decode='utf-8')
+ if not err:
+ elm_lst = re.findall('qat_dev\d', data_st)
+ print('\n'.join(elm_lst))
+
+# Return QAT path in sysfs
+def get_qat_proc_path(qat_dev):
+ q_type = ""
+ q_bsf = ""
+ output, err = popen('sudo /etc/init.d/qat_service status', decode='utf-8')
+ if not err:
+ # Parse QAT service output
+ 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(", ")
+ for elm in range(len(elm_list)):
+ if re.search('type', elm_list[elm]):
+ q_list = elm_list[elm].split(": ")
+ q_type=q_list[1]
+ elif re.search('bsf', elm_list[elm]):
+ q_list = elm_list[elm].split(": ")
+ q_bsf = q_list[1]
+ return "/sys/kernel/debug/qat_"+q_type+"_"+q_bsf+"/"
+
+# Check if QAT service confgured
+def check_qat_if_conf():
+ if not Config().exists_effective('system acceleration qat'):
+ print("\t system acceleration qat is not configured")
+ sys.exit(1)
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("--hw", action="store_true", help="Show Intel QAT HW")
+group.add_argument("--dev_list", action="store_true", help="Return Intel QAT devices")
+group.add_argument("--flow", action="store_true", help="Show Intel QAT flows")
+group.add_argument("--interrupts", action="store_true", help="Show Intel QAT interrupts")
+group.add_argument("--status", action="store_true", help="Show Intel QAT status")
+group.add_argument("--conf", action="store_true", help="Show Intel QAT configuration")
+
+parser.add_argument("--dev", type=str, help="Selected QAT device")
+
+args = parser.parse_args()
+
+if args.hw:
+ detect_qat_dev()
+ # Show availible Intel QAT devices
+ call('sudo lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'')
+elif args.flow and args.dev:
+ check_qat_if_conf()
+ call('sudo cat '+get_qat_proc_path(args.dev)+"fw_counters")
+elif args.interrupts:
+ check_qat_if_conf()
+ # Delete _dev from args.dev
+ 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()
+ 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)
diff --git a/src/op_mode/show_configuration_files.sh b/src/op_mode/show_configuration_files.sh
new file mode 100755
index 000000000..ad8e0747c
--- /dev/null
+++ b/src/op_mode/show_configuration_files.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# Wrapper script for the show configuration files command
+find ${vyatta_sysconfdir}/config/ \
+ -type f \
+ -not -name ".*" \
+ -not -name "config.boot.*" \
+ -printf "%f\t(%Tc)\t%T@\n" \
+ | sort -r -k3 \
+ | awk -F"\t" '{printf ("%-20s\t%s\n", $1,$2) ;}'
diff --git a/src/op_mode/show_cpu.py b/src/op_mode/show_cpu.py
new file mode 100755
index 000000000..0a540da1d
--- /dev/null
+++ b/src/op_mode/show_cpu.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016-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 json
+
+from jinja2 import Template
+from sys import exit
+from vyos.util import popen, DEVNULL
+
+OUT_TMPL_SRC = """
+{%- if cpu -%}
+{% if 'vendor' in cpu %}CPU Vendor: {{cpu.vendor}}{%- endif %}
+{% if 'model' in cpu %}Model: {{cpu.model}}{%- endif %}
+{% if 'cpus' in cpu %}Total CPUs: {{cpu.cpus}}{%- endif %}
+{% if 'sockets' in cpu %}Sockets: {{cpu.sockets}}{%- endif %}
+{% if 'cores' in cpu %}Cores: {{cpu.cores}}{%- endif %}
+{% if 'threads' in cpu %}Threads: {{cpu.threads}}{%- endif %}
+{% if 'mhz' in cpu %}Current MHz: {{cpu.mhz}}{%- endif %}
+{% if 'mhz_min' in cpu %}Minimum MHz: {{cpu.mhz_min}}{%- endif %}
+{% if 'mhz_max' in cpu %}Maximum MHz: {{cpu.mhz_max}}{%- endif %}
+{% endif %}
+"""
+
+cpu = {}
+cpu_json, code = popen('lscpu -J', stderr=DEVNULL)
+
+if code == 0:
+ cpu_info = json.loads(cpu_json)
+ if len(cpu_info) > 0 and 'lscpu' in cpu_info:
+ for prop in cpu_info['lscpu']:
+ if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data']
+ if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data']
+ if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data']
+ if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data']
+ if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data']
+ if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data']
+ if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data']
+ if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data']
+ if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data']
+
+if len(cpu) > 0:
+ tmp = { 'cpu':cpu }
+ tmpl = Template(OUT_TMPL_SRC)
+ print(tmpl.render(tmp))
+ exit(0)
+else:
+ print('CPU information could not be determined\n')
+ exit(1)
diff --git a/src/op_mode/show_current_user.sh b/src/op_mode/show_current_user.sh
new file mode 100755
index 000000000..93e6efa61
--- /dev/null
+++ b/src/op_mode/show_current_user.sh
@@ -0,0 +1,18 @@
+#! /bin/bash
+
+echo -n "login : " ; who -m
+
+if [ -n "$VYATTA_USER_LEVEL_DIR" ]
+then
+ echo -n "level : "
+ basename $VYATTA_USER_LEVEL_DIR
+fi
+
+echo -n "user : " ; id -un
+echo -n "groups : " ; id -Gn
+
+if id -Z >/dev/null 2>&1
+then
+ echo -n "context : "
+ id -Z
+fi
diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py
new file mode 100755
index 000000000..ff1e3cc56
--- /dev/null
+++ b/src/op_mode/show_dhcp.py
@@ -0,0 +1,264 @@
+#!/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/>.
+#
+# TODO: merge with show_dhcpv6.py
+
+from json import dumps
+from argparse import ArgumentParser
+from ipaddress import ip_address
+from tabulate import tabulate
+from sys import exit
+from collections import OrderedDict
+from datetime import datetime
+
+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"
+
+lease_display_fields = OrderedDict()
+lease_display_fields['ip'] = 'IP address'
+lease_display_fields['hardware_address'] = 'Hardware address'
+lease_display_fields['state'] = 'State'
+lease_display_fields['start'] = 'Lease start'
+lease_display_fields['end'] = 'Lease expiration'
+lease_display_fields['remaining'] = 'Remaining'
+lease_display_fields['pool'] = 'Pool'
+lease_display_fields['hostname'] = 'Hostname'
+
+lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup']
+
+def in_pool(lease, pool):
+ if pool_key in lease.sets:
+ if lease.sets[pool_key] == pool:
+ return True
+
+ return False
+
+def utc_to_local(utc_dt):
+ return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds())
+
+def get_lease_data(lease):
+ data = {}
+
+ # isc-dhcp lease times are in UTC so we need to convert them to local time to display
+ try:
+ data["start"] = utc_to_local(lease.start).strftime("%Y/%m/%d %H:%M:%S")
+ except:
+ data["start"] = ""
+
+ try:
+ data["end"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S")
+ except:
+ data["end"] = ""
+
+ try:
+ data["remaining"] = lease.end - datetime.utcnow()
+ # negative timedelta prints wrong so bypass it
+ if (data["remaining"].days >= 0):
+ # substraction gives us a timedelta object which can't be formatted with strftime
+ # so we use str(), split gets rid of the microseconds
+ data["remaining"] = str(data["remaining"]).split('.')[0]
+ else:
+ data["remaining"] = ""
+ except:
+ data["remaining"] = ""
+
+ # currently not used but might come in handy
+ # todo: parse into datetime string
+ for prop in ['tstp', 'tsfp', 'atsfp', 'cltt']:
+ if prop in lease.data:
+ data[prop] = lease.data[prop]
+ else:
+ data[prop] = ''
+
+ data["hardware_address"] = lease.ethernet
+ data["hostname"] = lease.hostname
+
+ data["state"] = lease.binding_state
+ data["ip"] = lease.ip
+
+ try:
+ data["pool"] = lease.sets[pool_key]
+ except:
+ data["pool"] = ""
+
+ return data
+
+def get_leases(config, leases, state, pool=None, sort='ip'):
+ # get leases from file
+ leases = IscDhcpLeases(lease_file).get()
+
+ # filter leases by state
+ if 'all' not in state:
+ leases = list(filter(lambda x: x.binding_state in state, leases))
+
+ # filter leases by pool name
+ if pool is not None:
+ if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)):
+ leases = list(filter(lambda x: in_pool(x, pool), leases))
+ else:
+ print("Pool {0} does not exist.".format(pool))
+ exit(0)
+
+ # should maybe filter all state=active by lease.valid here?
+
+ # sort by start time to dedupe (newest lease overrides older)
+ leases = sorted(leases, key = lambda lease: lease.start)
+
+ # dedupe by converting to dict
+ leases_dict = {}
+ for lease in leases:
+ # dedupe by IP
+ leases_dict[lease.ip] = lease
+
+ # convert the lease data
+ leases = list(map(get_lease_data, leases_dict.values()))
+
+ # apply output/display sort
+ if sort == 'ip':
+ leases = sorted(leases, key = lambda lease: int(ip_address(lease['ip'])))
+ else:
+ leases = sorted(leases, key = lambda lease: lease[sort])
+
+ return leases
+
+def show_leases(leases):
+ lease_list = []
+ for l in leases:
+ lease_list_params = []
+ for k in lease_display_fields.keys():
+ lease_list_params.append(l[k])
+ lease_list.append(lease_list_params)
+
+ output = tabulate(lease_list, lease_display_fields.values())
+
+ print(output)
+
+def get_pool_size(config, pool):
+ size = 0
+ subnets = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet".format(pool))
+ for s in subnets:
+ ranges = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet {1} range".format(pool, s))
+ for r in ranges:
+ start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r))
+ stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r))
+
+ # Add +1 because both range boundaries are inclusive
+ size += int(ip_address(stop)) - int(ip_address(start)) + 1
+
+ return size
+
+def show_pool_stats(stats):
+ headers = ["Pool", "Size", "Leases", "Available", "Usage"]
+ output = tabulate(stats, headers)
+
+ print(output)
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases")
+ group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics")
+ group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument")
+
+ parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool")
+ parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by")
+ parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)")
+ parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output")
+
+ args = parser.parse_args()
+
+ conf = Config()
+
+ if args.allowed == 'pool':
+ if conf.exists_effective('service dhcp-server'):
+ print(' '.join(conf.list_effective_nodes("service dhcp-server shared-network-name")))
+ exit(0)
+ elif args.allowed == 'sort':
+ print(' '.join(lease_display_fields.keys()))
+ exit(0)
+ elif args.allowed == 'state':
+ print(' '.join(lease_valid_states))
+ exit(0)
+ elif args.allowed:
+ parser.print_help()
+ exit(1)
+
+ if args.sort not in lease_display_fields.keys():
+ print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}')
+ exit(0)
+
+ if not set(args.state) < set(lease_valid_states):
+ print(f'Invalid lease state, choose from: {lease_valid_states}')
+ exit(0)
+
+ # Do nothing if service is not configured
+ if not conf.exists_effective('service dhcp-server'):
+ print("DHCP service is not configured.")
+ exit(0)
+
+ # if dhcp server is down, inactive leases may still be shown as active, so warn the user.
+ if call('systemctl -q is-active isc-dhcp-server.service') != 0:
+ print("WARNING: DHCP server is configured but not started. Data may be stale.")
+
+ if args.leases:
+ leases = get_leases(conf, lease_file, args.state, args.pool, args.sort)
+
+ if args.json:
+ print(dumps(leases, indent=4))
+ else:
+ show_leases(leases)
+
+ elif args.statistics:
+ pools = []
+
+ # Get relevant pools
+ if args.pool:
+ pools = [args.pool]
+ else:
+ pools = conf.list_effective_nodes("service dhcp-server shared-network-name")
+
+ # Get pool usage stats
+ stats = []
+ for p in pools:
+ size = get_pool_size(conf, p)
+ leases = len(get_leases(conf, lease_file, state='active', pool=p))
+
+ use_percentage = round(leases / size * 100) if size != 0 else 0
+
+ if args.json:
+ pool_stats = {"pool": p, "size": size, "leases": leases,
+ "available": (size - leases), "percentage": use_percentage}
+ else:
+ # For tabulate
+ pool_stats = [p, size, leases, size - leases, "{0}%".format(use_percentage)]
+ stats.append(pool_stats)
+
+ # Print stats
+ if args.json:
+ print(dumps(stats, indent=4))
+ else:
+ show_pool_stats(stats)
+
+ else:
+ parser.print_help()
+ exit(1)
diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py
new file mode 100755
index 000000000..ac211fb0a
--- /dev/null
+++ b/src/op_mode/show_dhcpv6.py
@@ -0,0 +1,219 @@
+#!/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/>.
+#
+# TODO: merge with show_dhcp.py
+
+from json import dumps
+from argparse import ArgumentParser
+from ipaddress import ip_address
+from tabulate import tabulate
+from sys import exit
+from collections import OrderedDict
+from datetime import datetime
+
+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"
+
+lease_display_fields = OrderedDict()
+lease_display_fields['ip'] = 'IPv6 address'
+lease_display_fields['state'] = 'State'
+lease_display_fields['last_comm'] = 'Last communication'
+lease_display_fields['expires'] = 'Lease expiration'
+lease_display_fields['remaining'] = 'Remaining'
+lease_display_fields['type'] = 'Type'
+lease_display_fields['pool'] = 'Pool'
+lease_display_fields['iaid_duid'] = 'IAID_DUID'
+
+lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup']
+
+def in_pool(lease, pool):
+ if pool_key in lease.sets:
+ if lease.sets[pool_key] == pool:
+ return True
+
+ return False
+
+def format_hex_string(in_str):
+ out_str = ""
+
+ # if input is divisible by 2, add : every 2 chars
+ if len(in_str) > 0 and len(in_str) % 2 == 0:
+ out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2]))
+ else:
+ out_str = in_str
+
+ return out_str
+
+def utc_to_local(utc_dt):
+ return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds())
+
+def get_lease_data(lease):
+ data = {}
+
+ # isc-dhcp lease times are in UTC so we need to convert them to local time to display
+ try:
+ data["expires"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S")
+ except:
+ data["expires"] = ""
+
+ try:
+ data["last_comm"] = utc_to_local(lease.last_communication).strftime("%Y/%m/%d %H:%M:%S")
+ except:
+ data["last_comm"] = ""
+
+ try:
+ data["remaining"] = lease.end - datetime.utcnow()
+ # negative timedelta prints wrong so bypass it
+ if (data["remaining"].days >= 0):
+ # substraction gives us a timedelta object which can't be formatted with strftime
+ # so we use str(), split gets rid of the microseconds
+ data["remaining"] = str(data["remaining"]).split('.')[0]
+ else:
+ data["remaining"] = ""
+ except:
+ data["remaining"] = ""
+
+ # isc-dhcp records lease declarations as ia_{na|ta|pd} IAID_DUID {...}
+ # where IAID_DUID is the combined IAID and DUID
+ data["iaid_duid"] = format_hex_string(lease.host_identifier_string)
+
+ lease_types_long = {"na": "non-temporary", "ta": "temporary", "pd": "prefix delegation"}
+ data["type"] = lease_types_long[lease.type]
+
+ data["state"] = lease.binding_state
+ data["ip"] = lease.ip
+
+ try:
+ data["pool"] = lease.sets[pool_key]
+ except:
+ data["pool"] = ""
+
+ return data
+
+def get_leases(config, leases, state, pool=None, sort='ip'):
+ leases = IscDhcpLeases(lease_file).get()
+
+ # filter leases by state
+ if 'all' not in state:
+ leases = list(filter(lambda x: x.binding_state in state, leases))
+
+ # filter leases by pool name
+ if pool is not None:
+ if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)):
+ leases = list(filter(lambda x: in_pool(x, pool), leases))
+ else:
+ print("Pool {0} does not exist.".format(pool))
+ exit(0)
+
+ # should maybe filter all state=active by lease.valid here?
+
+ # sort by last_comm time to dedupe (newest lease overrides older)
+ leases = sorted(leases, key = lambda lease: lease.last_communication)
+
+ # dedupe by converting to dict
+ leases_dict = {}
+ for lease in leases:
+ # dedupe by IP
+ leases_dict[lease.ip] = lease
+
+ # convert the lease data
+ leases = list(map(get_lease_data, leases_dict.values()))
+
+ # apply output/display sort
+ if sort == 'ip':
+ leases = sorted(leases, key = lambda k: int(ip_address(k['ip'])))
+ else:
+ leases = sorted(leases, key = lambda k: k[sort])
+
+ return leases
+
+def show_leases(leases):
+ lease_list = []
+ for l in leases:
+ lease_list_params = []
+ for k in lease_display_fields.keys():
+ lease_list_params.append(l[k])
+ lease_list.append(lease_list_params)
+
+ output = tabulate(lease_list, lease_display_fields.values())
+
+ print(output)
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument("-l", "--leases", action="store_true", help="Show DHCPv6 leases")
+ group.add_argument("-s", "--statistics", action="store_true", help="Show DHCPv6 statistics")
+ group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument")
+
+ parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool")
+ parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by")
+ parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)")
+ parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output")
+
+ args = parser.parse_args()
+
+ conf = Config()
+
+ if args.allowed == 'pool':
+ if conf.exists_effective('service dhcpv6-server'):
+ print(' '.join(conf.list_effective_nodes("service dhcpv6-server shared-network-name")))
+ exit(0)
+ elif args.allowed == 'sort':
+ print(' '.join(lease_display_fields.keys()))
+ exit(0)
+ elif args.allowed == 'state':
+ print(' '.join(lease_valid_states))
+ exit(0)
+ elif args.allowed:
+ parser.print_help()
+ exit(1)
+
+ if args.sort not in lease_display_fields.keys():
+ print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}')
+ exit(0)
+
+ if not set(args.state) < set(lease_valid_states):
+ print(f'Invalid lease state, choose from: {lease_valid_states}')
+ exit(0)
+
+ # Do nothing if service is not configured
+ if not conf.exists_effective('service dhcpv6-server'):
+ print("DHCPv6 service is not configured")
+ exit(0)
+
+ # if dhcp server is down, inactive leases may still be shown as active, so warn the user.
+ if call('systemctl -q is-active isc-dhcp-server6.service') != 0:
+ print("WARNING: DHCPv6 server is configured but not started. Data may be stale.")
+
+ if args.leases:
+ leases = get_leases(conf, lease_file, args.state, args.pool, args.sort)
+
+ if args.json:
+ print(dumps(leases, indent=4))
+ else:
+ show_leases(leases)
+ elif args.statistics:
+ print("DHCPv6 statistics option is not available")
+ else:
+ parser.print_help()
+ exit(1)
diff --git a/src/op_mode/show_disk_format.sh b/src/op_mode/show_disk_format.sh
new file mode 100755
index 000000000..61b15a52b
--- /dev/null
+++ b/src/op_mode/show_disk_format.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+disk_dev="/dev/$1"
+if [ ! -b "$disk_dev" ];then
+ echo "$3 is not a disk device"
+ exit 1
+fi
+sudo /sbin/fdisk -l "$disk_dev"
diff --git a/src/op_mode/show_igmpproxy.py b/src/op_mode/show_igmpproxy.py
new file mode 100755
index 000000000..5ccc16287
--- /dev/null
+++ b/src/op_mode/show_igmpproxy.py
@@ -0,0 +1,241 @@
+#!/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/>.
+
+# File: show_igmpproxy.py
+# Purpose:
+# Display istatistics from IPv4 IGMP proxy.
+# Used by the "run show ip multicast" command tree.
+
+import sys
+import jinja2
+import argparse
+import ipaddress
+import socket
+
+import vyos.config
+
+# Output Template for "show ip multicast interface" command
+#
+# Example:
+# Interface BytesIn PktsIn BytesOut PktsOut Local
+# eth0 0.0b 0 0.0b 0 xxx.xxx.xxx.65
+# eth1 0.0b 0 0.0b 0 xxx.xxx.xx.201
+# eth0.3 0.0b 0 0.0b 0 xxx.xxx.x.7
+# tun1 0.0b 0 0.0b 0 xxx.xxx.xxx.2
+vif_out_tmpl = """
+{%- for r in data %}
+{{ "%-10s"|format(r.interface) }} {{ "%-12s"|format(r.bytes_in) }} {{ "%-12s"|format(r.pkts_in) }} {{ "%-12s"|format(r.bytes_out) }} {{ "%-12s"|format(r.pkts_out) }} {{ "%-15s"|format(r.loc) }}
+{%- endfor %}
+"""
+
+# Output Template for "show ip multicast mfc" command
+#
+# Example:
+# Group Origin In Out Pkts Bytes Wrong
+# xxx.xxx.xxx.250 xxx.xx.xxx.75 --
+# xxx.xxx.xx.124 xx.xxx.xxx.26 --
+mfc_out_tmpl = """
+{%- for r in data %}
+{{ "%-15s"|format(r.group) }} {{ "%-15s"|format(r.origin) }} {{ "%-12s"|format(r.pkts) }} {{ "%-12s"|format(r.bytes) }} {{ "%-12s"|format(r.wrong) }} {{ "%-10s"|format(r.iif) }} {{ "%-20s"|format(r.oifs|join(', ')) }}
+{%- endfor %}
+"""
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--interface", action="store_true", help="Interface Statistics")
+parser.add_argument("--mfc", action="store_true", help="Multicast Forwarding Cache")
+
+def byte_string(size):
+ # convert size to integer
+ size = int(size)
+
+ # One Terrabyte
+ s_TB = 1024 * 1024 * 1024 * 1024
+ # One Gigabyte
+ s_GB = 1024 * 1024 * 1024
+ # One Megabyte
+ s_MB = 1024 * 1024
+ # One Kilobyte
+ s_KB = 1024
+ # One Byte
+ s_B = 1
+
+ if size > s_TB:
+ return str(round((size/s_TB), 2)) + 'TB'
+ elif size > s_GB:
+ return str(round((size/s_GB), 2)) + 'GB'
+ elif size > s_MB:
+ return str(round((size/s_MB), 2)) + 'MB'
+ elif size > s_KB:
+ return str(round((size/s_KB), 2)) + 'KB'
+ else:
+ return str(round((size/s_B), 2)) + 'b'
+
+ return None
+
+def kernel2ip(addr):
+ """
+ Convert any given addr from Linux Kernel to a proper, IPv4 address
+ using the correct host byte order.
+ """
+
+ # Convert from hex 'FE000A0A' to decimal '4261415434'
+ addr = int(addr, 16)
+ # Kernel ABI _always_ uses network byteorder
+ addr = socket.ntohl(addr)
+
+ return ipaddress.IPv4Address( addr )
+
+def do_mr_vif():
+ """
+ Read contents of file /proc/net/ip_mr_vif and print a more human
+ friendly version to the command line. IPv4 addresses present as
+ 32bit integers in hex format are converted to IPv4 notation, too.
+ """
+
+ with open('/proc/net/ip_mr_vif', 'r') as f:
+ lines = len(f.readlines())
+ if lines < 2:
+ return None
+
+ result = {
+ 'data': []
+ }
+
+ # Build up table format string
+ table_format = {
+ 'interface': 'Interface',
+ 'pkts_in' : 'PktsIn',
+ 'pkts_out' : 'PktsOut',
+ 'bytes_in' : 'BytesIn',
+ 'bytes_out': 'BytesOut',
+ 'loc' : 'Local'
+ }
+ result['data'].append(table_format)
+
+ # read and parse information from /proc filesystema
+ with open('/proc/net/ip_mr_vif', 'r') as f:
+ header_line = next(f)
+ for line in f:
+ data = {
+ 'interface': line.split()[1],
+ 'pkts_in' : line.split()[3],
+ 'pkts_out' : line.split()[5],
+
+ # convert raw byte number to something more human readable
+ # Note: could be replaced by Python3 hurry.filesize module
+ 'bytes_in' : byte_string( line.split()[2] ),
+ 'bytes_out': byte_string( line.split()[4] ),
+
+ # convert IP address from hex 'FE000A0A' to decimal '4261415434'
+ 'loc' : kernel2ip( line.split()[7] ),
+ }
+ result['data'].append(data)
+
+ return result
+
+def do_mr_mfc():
+ """
+ Read contents of file /proc/net/ip_mr_cache and print a more human
+ friendly version to the command line. IPv4 addresses present as
+ 32bit integers in hex format are converted to IPv4 notation, too.
+ """
+
+ with open('/proc/net/ip_mr_cache', 'r') as f:
+ lines = len(f.readlines())
+ if lines < 2:
+ return None
+
+ # We need this to convert from interface index to a real interface name
+ # Thus we also skip the format identifier on list index 0
+ vif = do_mr_vif()['data'][1:]
+
+ result = {
+ 'data': []
+ }
+
+ # Build up table format string
+ table_format = {
+ 'group' : 'Group',
+ 'origin': 'Origin',
+ 'iif' : 'In',
+ 'oifs' : ['Out'],
+ 'pkts' : 'Pkts',
+ 'bytes' : 'Bytes',
+ 'wrong' : 'Wrong'
+ }
+ result['data'].append(table_format)
+
+ # read and parse information from /proc filesystem
+ with open('/proc/net/ip_mr_cache', 'r') as f:
+ header_line = next(f)
+ for line in f:
+ data = {
+ # convert IP address from hex 'FE000A0A' to decimal '4261415434'
+ 'group' : kernel2ip( line.split()[0] ),
+ 'origin': kernel2ip( line.split()[1] ),
+
+ 'iif' : '--',
+ 'pkts' : '',
+ 'bytes' : '',
+ 'wrong' : '',
+ 'oifs' : []
+ }
+
+ iif = int( line.split()[2] )
+ if not ((iif == -1) or (iif == 65535)):
+ data['pkts'] = line.split()[3]
+ data['bytes'] = byte_string( line.split()[4] )
+ data['wrong'] = line.split()[5]
+
+ # convert index to real interface name
+ data['iif'] = vif[iif]['interface']
+
+ # convert each output interface index to a real interface name
+ for oif in line.split()[6:]:
+ idx = int( oif.split(':')[0] )
+ data['oifs'].append( vif[idx]['interface'] )
+
+ result['data'].append(data)
+
+ return result
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = vyos.config.Config()
+ if not c.exists_effective('protocols igmp-proxy'):
+ print("IGMP proxy is not configured")
+ sys.exit(0)
+
+ if args.interface:
+ data = do_mr_vif()
+ if data:
+ tmpl = jinja2.Template(vif_out_tmpl)
+ print(tmpl.render(data))
+
+ sys.exit(0)
+ elif args.mfc:
+ data = do_mr_mfc()
+ if data:
+ tmpl = jinja2.Template(mfc_out_tmpl)
+ print(tmpl.render(data))
+
+ sys.exit(0)
+ else:
+ parser.print_help()
+ sys.exit(1)
+
diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py
new file mode 100755
index 000000000..d4dae3cd1
--- /dev/null
+++ b/src/op_mode/show_interfaces.py
@@ -0,0 +1,304 @@
+#!/usr/bin/env python3
+
+# Copyright 2017, 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 sys
+import glob
+import datetime
+import argparse
+import netifaces
+
+from vyos.ifconfig import Section
+from vyos.ifconfig import Interface
+from vyos.ifconfig import VRRP
+from vyos.util import cmd
+
+
+# interfaces = Sections.reserved()
+interfaces = ['eno', 'ens', 'enp', 'enx', 'eth', 'vmnet', 'lo', 'tun', 'wan', 'pppoe', 'pppoa', 'adsl']
+glob_ifnames = '/sys/class/net/({})*'.format('|'.join(interfaces))
+
+
+actions = {}
+def register (name):
+ """
+ decorator to register a function into actions with a name
+ it allows to use actions[name] to call the registered function
+ """
+ def _register(function):
+ actions[name] = function
+ return function
+ return _register
+
+
+def filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ """
+ get all the interfaces from the OS and returns them
+ ifnames can be used to filter which interfaces should be considered
+
+ ifnames: a list of interfaces names to consider, empty do not filter
+ return an instance of the interface class
+ """
+ allnames = Section.interfaces()
+
+ vrrp_interfaces = VRRP.active_interfaces() if vrrp else []
+
+ for ifname in allnames:
+ if ifnames and ifname not in ifnames:
+ continue
+
+ # return the class which can handle this interface name
+ klass = Section.klass(ifname)
+ # connect to the interface
+ interface = klass(ifname, create=False, debug=False)
+
+ if iftypes and interface.definition['section'] not in iftypes:
+ continue
+
+ if vif and not '.' in ifname:
+ continue
+
+ if vrrp and ifname not in vrrp_interfaces:
+ continue
+
+ yield interface
+
+
+def split_text(text, used=0):
+ """
+ take a string and attempt to split it to fit with the width of the screen
+
+ text: the string to split
+ used: number of characted already used in the screen
+ """
+ returned = cmd('stty size')
+ if len(returned) == 2:
+ rows, columns = [int(_) for _ in returned]
+ else:
+ rows, columns = (40, 80)
+
+ desc_len = columns - used
+
+ line = ''
+ for word in text.split():
+ if len(line) + len(word) < desc_len:
+ line = f'{line} {word}'
+ continue
+ if line:
+ yield line[1:]
+ else:
+ line = f'{line} {word}'
+
+ yield line[1:]
+
+
+def get_vrrp_intf():
+ return [intf for intf in Section.interfaces() if intf.is_vrrp()]
+
+
+def get_counter_val(clear, now):
+ """
+ attempt to correct a counter if it wrapped, copied from perl
+
+ clear: previous counter
+ now: the current counter
+ """
+ # This function has to deal with both 32 and 64 bit counters
+ if clear == 0:
+ return now
+
+ # device is using 64 bit values assume they never wrap
+ value = now - clear
+ if (now >> 32) != 0:
+ return value
+
+ # The counter has rolled. If the counter has rolled
+ # multiple times since the clear value, then this math
+ # is meaningless.
+ if (value < 0):
+ value = (4294967296 - clear) + now
+
+ return value
+
+
+@register('help')
+def usage(*args):
+ print(f"Usage: {sys.argv[0]} [intf=NAME|intf-type=TYPE|vif|vrrp] action=ACTION")
+ print(f" NAME = " + ' | '.join(Section.interfaces()))
+ print(f" TYPE = " + ' | '.join(Section.sections()))
+ print(f" ACTION = " + ' | '.join(actions))
+ sys.exit(1)
+
+
+@register('allowed')
+def run_allowed(**kwarg):
+ sys.stdout.write(' '.join(Section.interfaces()))
+
+
+def pppoe(ifname):
+ out = cmd(f'ps -C pppd -f')
+ if ifname in out:
+ return 'C'
+ elif ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]:
+ return 'D'
+ return ''
+
+
+@register('show')
+def run_show_intf(ifnames, iftypes, vif, vrrp):
+ handled = []
+ for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ handled.append(interface.ifname)
+ cache = interface.operational.load_counters()
+
+ out = cmd(f'ip addr show {interface.ifname}')
+ out = re.sub(f'^\d+:\s+','',out)
+ if re.search("link/tunnel6", out):
+ tunnel = cmd(f'ip -6 tun show {interface.ifname}')
+ # tun0: ip/ipv6 remote ::2 local ::1 encaplimit 4 hoplimit 64 tclass inherit flowlabel inherit (flowinfo 0x00000000)
+ tunnel = re.sub('.*encap', 'encap', tunnel)
+ out = re.sub('(\n\s+)(link/tunnel6)', f'\g<1>{tunnel}\g<1>\g<2>', out)
+
+ print(out)
+
+ timestamp = int(cache.get('timestamp', 0))
+ if timestamp:
+ when = interface.operational.strtime(timestamp)
+ print(f' Last clear: {when}')
+
+ description = interface.get_alias()
+ if description:
+ print(f' Description: {description}')
+
+ print()
+ print(interface.operational.formated_stats())
+
+ for ifname in ifnames:
+ if ifname not in handled and ifname.startswith('pppoe'):
+ state = pppoe(ifname)
+ if not state:
+ continue
+ string = {
+ 'C': 'Coming up',
+ 'D': 'Link down',
+ }[state]
+ print('{}: {}'.format(ifname, string))
+
+
+@register('show-brief')
+def run_show_intf_brief(ifnames, iftypes, vif, vrrp):
+ format1 = '%-16s %-33s %-4s %s'
+ format2 = '%-16s %s'
+
+ print('Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down')
+ print(format1 % ("Interface", "IP Address", "S/L", "Description"))
+ print(format1 % ("---------", "----------", "---", "-----------"))
+
+ handled = []
+ for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ handled.append(interface.ifname)
+
+ oper_state = interface.operational.get_state()
+ admin_state = interface.get_admin_state()
+
+ intf = [interface.ifname,]
+ oper = ['u', ] if oper_state in ('up', 'unknown') else ['A', ]
+ admin = ['u', ] if oper_state in ('up', 'unknown') else ['D', ]
+ addrs = [_ for _ in interface.get_addr() if not _.startswith('fe80::')] or ['-', ]
+ descs = list(split_text(interface.get_alias(),0))
+
+ while intf or oper or admin or addrs or descs:
+ i = intf.pop(0) if intf else ''
+ a = addrs.pop(0) if addrs else ''
+ d = descs.pop(0) if descs else ''
+ s = [oper.pop(0)] if oper else []
+ l = [admin.pop(0)] if admin else []
+ if len(a) < 33:
+ print(format1 % (i, a, '/'.join(s+l), d))
+ else:
+ print(format2 % (i, a))
+ print(format1 % ('', '', '/'.join(s+l), d))
+
+ for ifname in ifnames:
+ if ifname not in handled and ifname.startswith('pppoe'):
+ state = pppoe(ifname)
+ if not state:
+ continue
+ string = {
+ 'C': 'u/D',
+ 'D': 'A/D',
+ }[state]
+ print(format1 % (ifname, '', string, ''))
+
+
+@register('show-count')
+def run_show_counters(ifnames, iftypes, vif, vrrp):
+ formating = '%-12s %10s %10s %10s %10s'
+ print(formating % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes'))
+
+ for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ oper = interface.operational.get_state()
+
+ if oper not in ('up','unknown'):
+ continue
+
+ stats = interface.operational.get_stats()
+ cache = interface.operational.load_counters()
+ print(formating % (
+ interface.ifname,
+ get_counter_val(cache['rx_packets'], stats['rx_packets']),
+ get_counter_val(cache['rx_bytes'], stats['rx_bytes']),
+ get_counter_val(cache['tx_packets'], stats['tx_packets']),
+ get_counter_val(cache['tx_bytes'], stats['tx_bytes']),
+ ))
+
+
+@register('clear')
+def run_clear_intf(ifnames, iftypes, vif, vrrp):
+ for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ print(f'Clearing {interface.ifname}')
+ interface.operational.clear_counters()
+
+
+@register('reset')
+def run_reset_intf(ifnames, iftypes, vif, vrrp):
+ for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ interface.operational.reset_counters()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(add_help=False, description='Show interface information')
+ parser.add_argument('--intf', action="store", type=str, default='', help='only show the specified interface(s)')
+ parser.add_argument('--intf-type', action="store", type=str, default='', help='only show the specified interface type')
+ parser.add_argument('--action', action="store", type=str, default='show', help='action to perform')
+ parser.add_argument('--vif', action='store_true', default=False, help="only show vif interfaces")
+ parser.add_argument('--vrrp', action='store_true', default=False, help="only show vrrp interfaces")
+ parser.add_argument('--help', action='store_true', default=False, help="show help")
+
+ args = parser.parse_args()
+
+ def missing(*args):
+ print('Invalid action [{args.action}]')
+ usage()
+
+ actions.get(args.action, missing)(
+ [_ for _ in args.intf.split(' ') if _],
+ [_ for _ in args.intf_type.split(' ') if _],
+ args.vif,
+ args.vrrp
+ )
diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py
new file mode 100755
index 000000000..e319cc38d
--- /dev/null
+++ b/src/op_mode/show_ipsec_sa.py
@@ -0,0 +1,111 @@
+#!/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 re
+import sys
+
+import vici
+import tabulate
+import hurry.filesize
+
+import vyos.util
+
+
+try:
+ session = vici.Session()
+ sas = session.list_sas()
+except PermissionError:
+ print("You do not have a permission to connect to the IPsec daemon")
+ sys.exit(1)
+except ConnectionRefusedError:
+ print("IPsec is not runing")
+ sys.exit(1)
+except Exception as e:
+ print("An error occured: {0}".format(e))
+ sys.exit(1)
+
+sa_data = []
+
+for sa in sas:
+ # list_sas() returns a list of single-item dicts
+ for peer in sa:
+ parent_sa = sa[peer]
+
+ if parent_sa["state"] == b"ESTABLISHED":
+ state = "up"
+ else:
+ state = "down"
+
+ if state == "up":
+ uptime = vyos.util.seconds_to_human(parent_sa["established"].decode())
+ else:
+ uptime = "N/A"
+
+ remote_host = parent_sa["remote-host"].decode()
+ remote_id = parent_sa["remote-id"].decode()
+
+ if remote_host == remote_id:
+ remote_id = "N/A"
+
+ # The counters can only be obtained from the child SAs
+ child_sas = parent_sa["child-sas"]
+ installed_sas = {k: v for k, v in child_sas.items() if v["state"] == b"INSTALLED"}
+
+ if not installed_sas:
+ data = [peer, state, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"]
+ sa_data.append(data)
+ else:
+ for csa in installed_sas:
+ isa = installed_sas[csa]
+
+ bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode()))
+ bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode()))
+ bytes_str = "{0}/{1}".format(bytes_in, bytes_out)
+
+ pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si)
+ pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si)
+ pkts_str = "{0}/{1}".format(pkts_in, pkts_out)
+ # Remove B from <1K values
+ pkts_str = re.sub(r'B', r'', pkts_str)
+
+ enc = isa["encr-alg"].decode()
+ if "encr-keysize" in isa:
+ key_size = isa["encr-keysize"].decode()
+ else:
+ key_size = ""
+ if "integ-alg" in isa:
+ hash = isa["integ-alg"].decode()
+ else:
+ hash = ""
+ if "dh-group" in isa:
+ dh_group = isa["dh-group"].decode()
+ else:
+ dh_group = ""
+
+ proposal = enc
+ if key_size:
+ proposal = "{0}_{1}".format(proposal, key_size)
+ if hash:
+ proposal = "{0}/{1}".format(proposal, hash)
+ if dh_group:
+ proposal = "{0}/{1}".format(proposal, dh_group)
+
+ data = [peer, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal]
+ sa_data.append(data)
+
+headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"]
+output = tabulate.tabulate(sa_data, headers)
+print(output)
diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py
new file mode 100755
index 000000000..0b53112f2
--- /dev/null
+++ b/src/op_mode/show_nat_statistics.py
@@ -0,0 +1,63 @@
+#!/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 jmespath
+import json
+
+from argparse import ArgumentParser
+from jinja2 import Template
+from sys import exit
+from vyos.util import cmd
+
+OUT_TMPL_SRC="""
+rule pkts bytes interface
+---- ---- ----- ---------
+{% for r in output %}
+{%- if r.comment -%}
+{%- set packets = r.counter.packets -%}
+{%- set bytes = r.counter.bytes -%}
+{%- set interface = r.interface -%}
+{# remove rule comment prefix #}
+{%- set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') | replace(' tcp_udp', '') -%}
+{{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }}
+{%- endif %}
+{% endfor %}
+"""
+
+parser = ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true")
+group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true")
+args = parser.parse_args()
+
+if args.source or args.destination:
+ tmp = cmd('sudo nft -j list table nat')
+ tmp = json.loads(tmp)
+
+ source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
+ destination = r"nftables[?rule.chain=='PREROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
+ data = {
+ 'output' : jmespath.search(source if args.source else destination, tmp),
+ 'direction' : 'source' if args.source else 'destination'
+ }
+
+ tmpl = Template(OUT_TMPL_SRC, lstrip_blocks=True)
+ print(tmpl.render(data))
+ exit(0)
+else:
+ parser.print_help()
+ exit(1)
+
diff --git a/src/op_mode/show_nat_translations.py b/src/op_mode/show_nat_translations.py
new file mode 100755
index 000000000..3af33b78e
--- /dev/null
+++ b/src/op_mode/show_nat_translations.py
@@ -0,0 +1,200 @@
+#!/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/>.
+
+'''
+show nat translations
+'''
+
+import os
+import sys
+import ipaddress
+import argparse
+import xmltodict
+
+from vyos.util import popen
+from vyos.util import DEVNULL
+
+conntrack = '/usr/sbin/conntrack'
+
+verbose_format = "%-20s %-18s %-20s %-18s"
+normal_format = "%-20s %-20s %-4s %-8s %s"
+
+
+def headers(verbose, pipe):
+ if verbose:
+ return verbose_format % ('Pre-NAT src', 'Pre-NAT dst', 'Post-NAT src', 'Post-NAT dst')
+ return normal_format % ('Pre-NAT', 'Post-NAT', 'Prot', 'Timeout', 'Type' if pipe else '')
+
+
+def command(srcdest, proto, ipaddr):
+ command = f'{conntrack} -o xml -L'
+
+ if proto:
+ command += f' -p {proto}'
+
+ if srcdest == 'source':
+ command += ' -n'
+ if ipaddr:
+ command += f' --orig-src {ipaddr}'
+ if srcdest == 'destination':
+ command += ' -g'
+
+ return command
+
+
+def run(command):
+ xml, code = popen(command,stderr=DEVNULL)
+ if code:
+ sys.exit('conntrack failed')
+ return xml
+
+
+def content(xmlfile):
+ xml = ''
+ with open(xmlfile,'r') as r:
+ xml += r.read()
+ return xml
+
+
+def pipe():
+ xml = ''
+ while True:
+ line = sys.stdin.readline()
+ xml += line
+ if '</conntrack>' in line:
+ break
+
+ sys.stdin = open('/dev/tty')
+ return xml
+
+
+def process(data, stats, protocol, pipe, verbose, flowtype=''):
+ if not data:
+ return
+
+ parsed = xmltodict.parse(data)
+
+ print(headers(verbose, pipe))
+
+ # to help the linter to detect typos
+ ORIGINAL = 'original'
+ REPLY = 'reply'
+ INDEPENDANT = 'independent'
+ SPORT = 'sport'
+ DPORT = 'dport'
+ SRC = 'src'
+ DST = 'dst'
+
+ for rule in parsed['conntrack']['flow']:
+ src, dst, sport, dport, proto = {}, {}, {}, {}, {}
+ packet_count, byte_count = {}, {}
+ timeout, use = 0, 0
+
+ rule_type = rule.get('type', '')
+
+ for meta in rule['meta']:
+ # print(meta)
+ direction = meta['@direction']
+
+ if direction in (ORIGINAL, REPLY):
+ if 'layer3' in meta:
+ l3 = meta['layer3']
+ src[direction] = l3[SRC]
+ dst[direction] = l3[DST]
+
+ if 'layer4' in meta:
+ l4 = meta['layer4']
+ sp = l4.get(SPORT, '')
+ dp = l4.get(DPORT, '')
+ if sp:
+ sport[direction] = sp
+ if dp:
+ dport[direction] = dp
+ proto[direction] = l4.get('@protoname','')
+
+ if stats and 'counters' in meta:
+ packet_count[direction] = meta['packets']
+ byte_count[direction] = meta['bytes']
+ continue
+
+ if direction == INDEPENDANT:
+ timeout = meta['timeout']
+ use = meta['use']
+ continue
+
+ in_src = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if ORIGINAL in sport else src[ORIGINAL]
+ in_dst = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if ORIGINAL in dport else dst[ORIGINAL]
+
+ # inverted the the perl code !!?
+ out_dst = '%s:%s' % (dst[REPLY], dport[REPLY]) if REPLY in dport else dst[REPLY]
+ out_src = '%s:%s' % (src[REPLY], sport[REPLY]) if REPLY in sport else src[REPLY]
+
+ if flowtype == 'source':
+ v = ORIGINAL in sport and REPLY in dport
+ f = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if v else src[ORIGINAL]
+ t = '%s:%s' % (dst[REPLY], dport[REPLY]) if v else dst[REPLY]
+ else:
+ v = ORIGINAL in dport and REPLY in sport
+ f = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if v else dst[ORIGINAL]
+ t = '%s:%s' % (src[REPLY], sport[REPLY]) if v else src[REPLY]
+
+ # Thomas: I do not believe proto should be an option
+ p = proto.get('original', '')
+ if protocol and p != protocol:
+ continue
+
+ if verbose:
+ msg = verbose_format % (in_src, in_dst, out_dst, out_src)
+ p = f'{p}: ' if p else ''
+ msg += f'\n {p}{f} ==> {t}'
+ msg += f' timeout: {timeout}' if timeout else ''
+ msg += f' use: {use} ' if use else ''
+ msg += f' type: {rule_type}' if rule_type else ''
+ print(msg)
+ else:
+ print(normal_format % (f, t, p, timeout, rule_type if rule_type else ''))
+
+ if stats:
+ for direction in ('original', 'reply'):
+ if direction in packet_count:
+ print(' %-8s: packets %s, bytes %s' % direction, packet_count[direction], byte_count[direction])
+
+
+def main():
+ parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__)
+ parser.add_argument('--verbose', help='provide more details about the flows', action='store_true')
+ parser.add_argument('--proto', help='filter by protocol', default='', type=str)
+ parser.add_argument('--file', help='read the conntrack xml from a file', type=str)
+ parser.add_argument('--stats', help='add usage statistics', action='store_true')
+ parser.add_argument('--type', help='NAT type (source, destination)', required=True, type=str)
+ parser.add_argument('--ipaddr', help='source ip address to filter on', type=ipaddress.ip_address)
+ parser.add_argument('--pipe', help='read conntrack xml data from stdin', action='store_true')
+
+ arg = parser.parse_args()
+
+ if arg.type not in ('source', 'destination'):
+ sys.exit('Unknown NAT type!')
+
+ if arg.pipe:
+ process(pipe(), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type)
+ elif arg.file:
+ process(content(arg.file), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type)
+ else:
+ process(run(command(arg.type, arg.proto, arg.ipaddr)), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
new file mode 100755
index 000000000..32918ddce
--- /dev/null
+++ b/src/op_mode/show_openvpn.py
@@ -0,0 +1,178 @@
+#!/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 jinja2
+import argparse
+
+from sys import exit
+from vyos.config import Config
+
+outp_tmpl = """
+{% if clients %}
+OpenVPN status on {{ intf }}
+
+Client CN Remote Host Local Host TX bytes RX bytes Connected Since
+--------- ----------- ---------- -------- -------- ---------------
+{%- for c in clients %}
+{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-21s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.rx_bytes) }} {{ c.online_since }}
+{%- endfor %}
+{% endif %}
+"""
+
+def bytes2HR(size):
+ # we need to operate in integers
+ size = int(size)
+
+ suff = ['B', 'KB', 'MB', 'GB', 'TB']
+ suffIdx = 0
+
+ while size > 1024:
+ # incr. suffix index
+ suffIdx += 1
+ # divide
+ size = size/1024.0
+
+ output="{0:.1f} {1}".format(size, suff[suffIdx])
+ return output
+
+def get_status(mode, interface):
+ status_file = '/opt/vyatta/etc/openvpn/status/{}.status'.format(interface)
+ # this is an empirical value - I assume we have no more then 999999
+ # current OpenVPN connections
+ routing_table_line = 999999
+
+ data = {
+ 'mode': mode,
+ 'intf': interface,
+ 'local': 'N/A',
+ 'date': '',
+ '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):
+ # remove trailing newline character first
+ line = line.rstrip('\n')
+
+ # check first line header
+ if line_no == 0:
+ if mode == 'server':
+ if not line == 'OpenVPN CLIENT LIST':
+ raise NameError('Expected "OpenVPN CLIENT LIST"')
+ else:
+ if not line == 'OpenVPN STATISTICS':
+ raise NameError('Expected "OpenVPN STATISTICS"')
+
+ continue
+
+ # second line informs us when the status file has been last updated
+ if line_no == 1:
+ data['date'] = line.lstrip('Updated,').rstrip('\n')
+ continue
+
+ if mode == 'server':
+ # followed by line3 giving output information and the actual output data
+ #
+ # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
+ # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019
+ # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019
+ # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019
+ if (line_no >= 3) and (line_no < routing_table_line):
+ # indicator that there are no more clients and we will continue with the
+ # routing table
+ if line == 'ROUTING TABLE':
+ routing_table_line = line_no
+ continue
+
+ client = {
+ 'name': line.split(',')[0],
+ 'remote': line.split(',')[1],
+ 'rx_bytes': bytes2HR(line.split(',')[2]),
+ 'tx_bytes': bytes2HR(line.split(',')[3]),
+ 'online_since': line.split(',')[4]
+ }
+
+ data['clients'].append(client)
+ continue
+ else:
+ if line_no == 2:
+ client = {
+ 'name': 'N/A',
+ 'remote': 'N/A',
+ 'rx_bytes': bytes2HR(line.split(',')[1]),
+ 'tx_bytes': '',
+ 'online_since': 'N/A'
+ }
+ continue
+
+ if line_no == 3:
+ client['tx_bytes'] = bytes2HR(line.split(',')[1])
+ data['clients'].append(client)
+ break
+
+ return data
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-m', '--mode', help='OpenVPN operation mode (server, client, site-2-site)', required=True)
+
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ config = Config()
+ if len(config.list_effective_nodes('interfaces openvpn')) == 0:
+ print("No OpenVPN interfaces configured")
+ exit(0)
+
+ # search all OpenVPN interfaces and add those with a matching mode to our
+ # interfaces list
+ interfaces = []
+ for intf in config.list_effective_nodes('interfaces openvpn'):
+ # get interface type (server, client, site-to-site)
+ mode = config.return_effective_value('interfaces openvpn {} mode'.format(intf))
+ if args.mode == mode:
+ interfaces.append(intf)
+
+ for intf in interfaces:
+ data = get_status(args.mode, intf)
+ local_host = config.return_effective_value('interfaces openvpn {} local-host'.format(intf))
+ local_port = config.return_effective_value('interfaces openvpn {} local-port'.format(intf))
+ if local_host and local_port:
+ data['local'] = local_host + ':' + local_port
+
+ if args.mode in ['client', 'site-to-site']:
+ for client in data['clients']:
+ if config.exists_effective('interfaces openvpn {} shared-secret-key-file'.format(intf)):
+ client['name'] = "None (PSK)"
+
+ remote_host = config.return_effective_values('interfaces openvpn {} remote-host'.format(intf))
+ remote_port = config.return_effective_value('interfaces openvpn {} remote-port'.format(intf))
+
+ if not remote_port:
+ remote_port = '1194'
+
+ if len(remote_host) >= 1:
+ client['remote'] = str(remote_host[0]) + ':' + remote_port
+
+ tmpl = jinja2.Template(outp_tmpl)
+ print(tmpl.render(data))
+
diff --git a/src/op_mode/show_raid.sh b/src/op_mode/show_raid.sh
new file mode 100755
index 000000000..ba4174692
--- /dev/null
+++ b/src/op_mode/show_raid.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+raid_set_name=$1
+raid_sets=`cat /proc/partitions | grep md | awk '{ print $4 }'`
+valid_set=`echo $raid_sets | grep $raid_set_name`
+if [ -z $valid_set ]; then
+ echo "$raid_set_name is not a RAID set"
+else
+ if [ -r /dev/${raid_set_name} ]; then
+ # This should work without sudo because we have read
+ # access to the dev, but for some reason mdadm must be
+ # run as root in order to succeed.
+ sudo /sbin/mdadm --detail /dev/${raid_set_name}
+ else
+ echo "Must be administrator or root to display RAID status"
+ fi
+fi
diff --git a/src/op_mode/show_ram.sh b/src/op_mode/show_ram.sh
new file mode 100755
index 000000000..b013e16f8
--- /dev/null
+++ b/src/op_mode/show_ram.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+#
+# Module: vyos-show-ram.sh
+# Displays memory usage information in minimalistic format
+#
+# 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 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/>.
+
+MB_DIVISOR=1024
+
+TOTAL=$(cat /proc/meminfo | grep -E "^MemTotal:" | awk -F ' ' '{print $2}')
+FREE=$(cat /proc/meminfo | grep -E "^MemFree:" | awk -F ' ' '{print $2}')
+BUFFERS=$(cat /proc/meminfo | grep -E "^Buffers:" | awk -F ' ' '{print $2}')
+CACHED=$(cat /proc/meminfo | grep -E "^Cached:" | awk -F ' ' '{print $2}')
+
+DISPLAY_FREE=$(( ($FREE + $BUFFERS + $CACHED) / $MB_DIVISOR ))
+DISPLAY_TOTAL=$(( $TOTAL / $MB_DIVISOR ))
+DISPLAY_USED=$(( $DISPLAY_TOTAL - $DISPLAY_FREE ))
+
+echo "Total: $DISPLAY_TOTAL"
+echo "Free: $DISPLAY_FREE"
+echo "Used: $DISPLAY_USED"
diff --git a/src/op_mode/show_sensors.py b/src/op_mode/show_sensors.py
new file mode 100755
index 000000000..6ae477647
--- /dev/null
+++ b/src/op_mode/show_sensors.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+
+import re
+import sys
+from vyos.util import popen
+from vyos.util import DEVNULL
+output,retcode = popen("sensors --no-adapter", stderr=DEVNULL)
+if retcode == 0:
+ print (output)
+ sys.exit(0)
+else:
+ output,retcode = popen("sensors-detect --auto",stderr=DEVNULL)
+ match = re.search(r'#----cut here----(.*)#----cut here----',output, re.DOTALL)
+ if match:
+ for module in match.group(0).split('\n'):
+ if not module.startswith("#"):
+ popen("modprobe {}".format(module.strip()))
+ output,retcode = popen("sensors --no-adapter", stderr=DEVNULL)
+ if retcode == 0:
+ print (output)
+ sys.exit(0)
+
+
+print ("No sensors found")
+sys.exit(1)
+
+
diff --git a/src/op_mode/show_usb_serial.py b/src/op_mode/show_usb_serial.py
new file mode 100755
index 000000000..776898c25
--- /dev/null
+++ b/src/op_mode/show_usb_serial.py
@@ -0,0 +1,57 @@
+#!/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
+
+from jinja2 import Template
+from pyudev import Context, Devices
+from sys import exit
+
+OUT_TMPL_SRC = """Device Model Vendor
+------ ------ ------
+{%- for d in devices %}
+{{ "%-16s" | format(d.device) }} {{ "%-19s" | format(d.model)}} {{d.vendor}}
+{%- endfor %}
+
+"""
+
+data = {
+ 'devices': []
+}
+
+
+base_directory = '/dev/serial/by-bus'
+if not os.path.isdir(base_directory):
+ print("No USB to serial converter connected")
+ exit(0)
+
+context = Context()
+for root, dirs, files in os.walk(base_directory):
+ for basename in files:
+ os.path.join(root, basename)
+ device = Devices.from_device_file(context, os.path.join(root, basename))
+ tmp = {
+ 'device': basename,
+ 'model': device.properties.get('ID_MODEL'),
+ 'vendor': device.properties.get('ID_VENDOR_FROM_DATABASE')
+ }
+ data['devices'].append(tmp)
+
+data['devices'] = sorted(data['devices'], key = lambda i: i['device'])
+tmpl = Template(OUT_TMPL_SRC)
+print(tmpl.render(data))
+
+exit(0)
diff --git a/src/op_mode/show_users.py b/src/op_mode/show_users.py
new file mode 100755
index 000000000..8e4f12851
--- /dev/null
+++ b/src/op_mode/show_users.py
@@ -0,0 +1,111 @@
+#!/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 argparse
+import pwd
+import spwd
+import struct
+import sys
+from time import ctime
+
+from tabulate import tabulate
+from vyos.config import Config
+
+
+class UserInfo:
+ def __init__(self, uid, name, user_type, is_locked, login_time, tty, host):
+ self.uid = uid
+ self.name = name
+ self.user_type = user_type
+ self.is_locked = is_locked
+ self.login_time = login_time
+ self.tty = tty
+ self.host = host
+
+
+filters = {
+ 'default': lambda user: not user.is_locked, # Default is everything but locked accounts
+ 'vyos': lambda user: user.user_type == 'vyos',
+ 'other': lambda user: user.user_type != 'vyos',
+ 'locked': lambda user: user.is_locked,
+ 'all': lambda user: True
+}
+
+
+def is_locked(user_name: str) -> bool:
+ """Check if a given user has password in shadow db"""
+
+ try:
+ encrypted_password = spwd.getspnam(user_name)[1]
+ return encrypted_password == '*' or encrypted_password.startswith('!')
+ except (KeyError, PermissionError):
+ print('Cannot access shadow database, ensure this script is run with sufficient permissions')
+ sys.exit(1)
+
+
+def decode_lastlog(lastlog_file, uid: int):
+ """Decode last login info of a given user uid from the lastlog file"""
+
+ struct_fmt = '=L32s256s'
+ recordsize = struct.calcsize(struct_fmt)
+ lastlog_file.seek(recordsize * uid)
+ buf = lastlog_file.read(recordsize)
+ if len(buf) < recordsize:
+ return None
+ (time, tty, host) = struct.unpack(struct_fmt, buf)
+ time = 'never logged in' if time == 0 else ctime(time)
+ tty = tty.strip(b'\x00')
+ host = host.strip(b'\x00')
+ return time, tty, host
+
+
+def list_users():
+ cfg = Config()
+ vyos_users = cfg.list_effective_nodes('system login user')
+ users = []
+ with open('/var/log/lastlog', 'rb') as lastlog_file:
+ for (name, _, uid, _, _, _, _) in pwd.getpwall():
+ lastlog_info = decode_lastlog(lastlog_file, uid)
+ if lastlog_info is None:
+ continue
+ user_info = UserInfo(
+ uid, name,
+ user_type='vyos' if name in vyos_users else 'other',
+ is_locked=is_locked(name),
+ login_time=lastlog_info[0],
+ tty=lastlog_info[1],
+ host=lastlog_info[2])
+ users.append(user_info)
+ return users
+
+
+def main():
+ parser = argparse.ArgumentParser(prog=sys.argv[0], add_help=False)
+ parser.add_argument('type', nargs='?', choices=['all', 'vyos', 'other', 'locked'])
+ args = parser.parse_args()
+
+ filter_type = args.type if args.type is not None else 'default'
+ filter_expr = filters[filter_type]
+
+ headers = ['Username', 'Type', 'Locked', 'Tty', 'From', 'Last login']
+ table_data = []
+ for user in list_users():
+ if filter_expr(user):
+ table_data.append([user.name, user.user_type, user.is_locked, user.tty, user.host, user.login_time])
+ print(tabulate(table_data, headers, tablefmt='simple'))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py
new file mode 100755
index 000000000..d0d5c6785
--- /dev/null
+++ b/src/op_mode/show_version.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016-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/>.
+#
+# Purpose:
+# Displays image version and system information.
+# Used by the "run show version" command.
+
+import argparse
+import vyos.version
+import vyos.limericks
+
+from jinja2 import Template
+from sys import exit
+from vyos.util import call
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-a", "--all", action="store_true", help="Include individual package versions")
+parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
+parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
+
+version_output_tmpl = """
+Version: VyOS {{version}}
+Release Train: {{release_train}}
+
+Built by: {{built_by}}
+Built on: {{built_on}}
+Build UUID: {{build_uuid}}
+Build Commit ID: {{build_git}}
+
+Architecture: {{system_arch}}
+Boot via: {{boot_via}}
+System type: {{system_type}}
+
+Hardware vendor: {{hardware_vendor}}
+Hardware model: {{hardware_model}}
+Hardware S/N: {{hardware_serial}}
+Hardware UUID: {{hardware_uuid}}
+
+Copyright: VyOS maintainers and contributors
+"""
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ version_data = vyos.version.get_full_version_data()
+
+ if args.json:
+ import json
+ print(json.dumps(version_data))
+ exit(0)
+
+ tmpl = Template(version_output_tmpl)
+ print(tmpl.render(version_data))
+
+ if args.all:
+ print("Package versions:")
+ call("dpkg -l")
+
+ if args.funny:
+ print(vyos.limericks.get_random())
diff --git a/src/op_mode/show_vpn_ra.py b/src/op_mode/show_vpn_ra.py
new file mode 100755
index 000000000..73688c4ea
--- /dev/null
+++ b/src/op_mode/show_vpn_ra.py
@@ -0,0 +1,56 @@
+#!/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 sys
+import re
+
+from vyos.util import popen
+
+# chech connection to pptp and l2tp daemon
+def get_sessions():
+ absent_pptp = False
+ absent_l2tp = False
+ pptp_cmd = "accel-cmd -p 2003 show sessions"
+ l2tp_cmd = "accel-cmd -p 2004 show sessions"
+ err_pattern = "^Connection.+failed$"
+ # This value for chack only output header without sessions.
+ len_def_header = 170
+
+ # Check pptp
+ 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
+ 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
+
+ if absent_l2tp and absent_pptp:
+ print("No active remote access VPN sessions")
+
+
+def main():
+ get_sessions()
+
+
+if __name__ == '__main__':
+ main()
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
new file mode 100755
index 000000000..b5ee3aee1
--- /dev/null
+++ b/src/op_mode/show_wireless.py
@@ -0,0 +1,156 @@
+#!/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 argparse
+import re
+
+from sys import exit
+from copy import deepcopy
+
+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 show_brief():
+ config = Config()
+ if len(config.list_effective_nodes('interfaces wireless')) == 0:
+ print("No Wireless interfaces configured")
+ exit(0)
+
+ interfaces = []
+ for intf in config.list_effective_nodes('interfaces wireless'):
+ config.set_level('interfaces wireless {}'.format(intf))
+ data = {
+ 'name': intf,
+ 'type': '',
+ 'ssid': '',
+ 'channel': ''
+ }
+ data['type'] = config.return_effective_value('type')
+ data['ssid'] = config.return_effective_value('ssid')
+ data['channel'] = config.return_effective_value('channel')
+
+ interfaces.append(data)
+
+ return interfaces
+
+def ssid_scan(intf):
+ # XXX: This ignores errors
+ tmp, _ = popen(f'/sbin/iw dev {intf} scan ap-force')
+ networks = []
+ data = {
+ 'ssid': '',
+ 'mac': '',
+ 'channel': '',
+ 'signal': ''
+ }
+ re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})')
+ for line in tmp.splitlines():
+ if line.startswith('BSS '):
+ ssid = deepcopy(data)
+ ssid['mac'] = re.search(re_mac, line).group()
+
+ elif line.lstrip().startswith('SSID: '):
+ # SSID can be " SSID: WLAN-57 6405", thus strip all leading whitespaces
+ ssid['ssid'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('signal: '):
+ # Siganl can be " signal: -67.00 dBm", thus strip all leading whitespaces
+ ssid['signal'] = line.lstrip().split(':')[-1].split()[0]
+
+ elif line.lstrip().startswith('DS Parameter set: channel'):
+ # Channel can be " DS Parameter set: channel 6" , thus
+ # strip all leading whitespaces
+ ssid['channel'] = line.lstrip().split(':')[-1].split()[-1]
+ networks.append(ssid)
+ continue
+
+ return networks
+
+def show_clients(intf):
+ # XXX: This ignores errors
+ tmp, _ = popen(f'/sbin/iw dev {intf} station dump')
+ clients = []
+ data = {
+ 'mac': '',
+ 'signal': '',
+ 'rx_bytes': '',
+ 'rx_packets': '',
+ 'tx_bytes': '',
+ 'tx_packets': ''
+ }
+ re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})')
+ for line in tmp.splitlines():
+ if line.startswith('Station'):
+ client = deepcopy(data)
+ client['mac'] = re.search(re_mac, line).group()
+
+ elif line.lstrip().startswith('signal avg:'):
+ client['signal'] = line.lstrip().split(':')[-1].lstrip().split()[0]
+
+ elif line.lstrip().startswith('rx bytes:'):
+ client['rx_bytes'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('rx packets:'):
+ client['rx_packets'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('tx bytes:'):
+ client['tx_bytes'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('tx packets:'):
+ client['tx_packets'] = line.lstrip().split(':')[-1].lstrip()
+ clients.append(client)
+ continue
+
+ return clients
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ if args.scan:
+ print("Address SSID Channel Signal (dbm)")
+ for network in ssid_scan(args.scan):
+ print("{:<17} {:<32} {:>3} {}".format(network['mac'],
+ network['ssid'],
+ network['channel'],
+ network['signal']))
+ exit(0)
+
+ elif args.brief:
+ print("Interface Type SSID Channel")
+ for intf in show_brief():
+ print("{:<9} {:<12} {:<32} {:>3}".format(intf['name'],
+ intf['type'],
+ intf['ssid'],
+ intf['channel']))
+ exit(0)
+
+ elif args.stations:
+ print("Station Signal RX: bytes packets TX: bytes packets")
+ for client in show_clients(args.stations):
+ print("{:<17} {:>3} {:>15} {:>9} {:>15} {:>10} ".format(client['mac'],
+ client['signal'], client['rx_bytes'], client['rx_packets'], client['tx_bytes'], client['tx_packets']))
+
+ exit(0)
+
+ else:
+ parser.print_help()
+ exit(1)
diff --git a/src/op_mode/snmp.py b/src/op_mode/snmp.py
new file mode 100755
index 000000000..5fae67881
--- /dev/null
+++ b/src/op_mode/snmp.py
@@ -0,0 +1,78 @@
+#!/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/>.
+#
+# File: snmp.py
+# Purpose:
+# Show SNMP community/remote hosts
+# Used by the "run show snmp community" commands.
+
+import os
+import sys
+import argparse
+
+from vyos.config import Config
+from vyos.util import call
+
+config_file_daemon = r'/etc/snmp/snmpd.conf'
+
+parser = argparse.ArgumentParser(description='Retrieve infomration from running SNMP daemon')
+parser.add_argument('--allowed', action="store_true", help='Show available SNMP communities')
+parser.add_argument('--community', action="store", help='Show status of given SNMP community', type=str)
+parser.add_argument('--host', action="store", help='SNMP host to connect to', type=str, default='localhost')
+
+config = {
+ 'communities': [],
+}
+
+def read_config():
+ with open(config_file_daemon, 'r') as f:
+ for line in f:
+ # Only get configured SNMP communitie
+ if line.startswith('rocommunity') or line.startswith('rwcommunity'):
+ string = line.split(' ')
+ # append community to the output list only once
+ c = string[1]
+ if c not in config['communities']:
+ config['communities'].append(c)
+
+def show_all():
+ if len(config['communities']) > 0:
+ print(' '.join(config['communities']))
+
+def show_community(c, h):
+ print('Status of SNMP community {0} on {1}'.format(c, h), flush=True)
+ call('/usr/bin/snmpstatus -t1 -v1 -c {0} {1}'.format(c, h))
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service snmp'):
+ print("SNMP service is not configured")
+ sys.exit(0)
+
+ read_config()
+
+ if args.allowed:
+ show_all()
+ sys.exit(1)
+ elif args.community:
+ show_community(args.community, args.host)
+ sys.exit(1)
+ else:
+ parser.print_help()
+ sys.exit(1)
diff --git a/src/op_mode/snmp_ifmib.py b/src/op_mode/snmp_ifmib.py
new file mode 100755
index 000000000..2479936bd
--- /dev/null
+++ b/src/op_mode/snmp_ifmib.py
@@ -0,0 +1,121 @@
+#!/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/>.
+#
+# File: snmp_ifmib.py
+# Purpose:
+# Show SNMP MIB information
+# Used by the "run show snmp mib" commands.
+
+import sys
+import argparse
+import netifaces
+
+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(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(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 = ''
+ dev_id = ''
+
+ try:
+ with open(r'/sys/class/net/' + i + '/device/vendor', 'r') as f:
+ ven_id = f.read().replace('\n', '')
+ except FileNotFoundError:
+ pass
+
+ try:
+ with open(r'/sys/class/net/' + i + '/device/device', 'r') as f:
+ dev_id = f.read().replace('\n', '')
+ except FileNotFoundError:
+ pass
+
+ if ven_id == '' and dev_id == '':
+ ret = 'ifDescr = {0}'.format(i)
+ return ret
+
+ device = str(ven_id) + ':' + str(dev_id)
+ out, err = popen(f'/usr/bin/lspci -mm -d {device}', decode='utf-8')
+
+ vendor = ""
+ device = ""
+
+ # convert output to string
+ string = out.split('"')
+ if len(string) > 3:
+ vendor = string[3]
+
+ if len(string) > 5:
+ device = string[5]
+
+ ret = 'ifDescr = {0} {1}'.format(vendor, device)
+ return ret.replace('\n', '')
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service snmp'):
+ print("SNMP service is not configured")
+ sys.exit(0)
+
+ if args.ifindex:
+ if args.ifindex == 'all':
+ for i in netifaces.interfaces():
+ print('{0}: {1}'.format(i, show_ifindex(i)))
+ else:
+ print('{0}: {1}'.format(args.ifindex, show_ifindex(args.ifindex)))
+
+ elif args.ifalias:
+ if args.ifalias == 'all':
+ for i in netifaces.interfaces():
+ print('{0}: {1}'.format(i, show_ifalias(i)))
+ else:
+ print('{0}: {1}'.format(args.ifalias, show_ifalias(args.ifalias)))
+
+ elif args.ifdescr:
+ if args.ifdescr == 'all':
+ for i in netifaces.interfaces():
+ print('{0}: {1}'.format(i, show_ifdescr(i)))
+ else:
+ print('{0}: {1}'.format(args.ifdescr, show_ifdescr(args.ifdescr)))
+
+ else:
+ #eth0: ifIndex = 2
+ # ifAlias = NET-MYBLL-MUCI-BACKBONE
+ # ifDescr = VMware VMXNET3 Ethernet Controller
+ #lo: ifIndex = 1
+ for i in netifaces.interfaces():
+ print('{0}:\t{1}'.format(i, show_ifindex(i)))
+ print('\t{0}'.format(show_ifalias(i)))
+ print('\t{0}'.format(show_ifdescr(i)))
+
+ sys.exit(1)
diff --git a/src/op_mode/snmp_v3.py b/src/op_mode/snmp_v3.py
new file mode 100755
index 000000000..92601f15e
--- /dev/null
+++ b/src/op_mode/snmp_v3.py
@@ -0,0 +1,180 @@
+#!/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/>.
+#
+# File: snmp_v3.py
+# Purpose:
+# Show SNMP v3 information
+# Used by the "run show snmp v3" commands.
+
+import sys
+import jinja2
+import argparse
+
+from vyos.config import Config
+
+parser = argparse.ArgumentParser(description='Retrieve SNMP v3 information')
+parser.add_argument('--all', action="store_true", help='Show all available information')
+parser.add_argument('--group', action="store_true", help='Show the list of configured groups')
+parser.add_argument('--trap', action="store_true", help='Show the list of configured targets')
+parser.add_argument('--user', action="store_true", help='Show the list of configured users')
+parser.add_argument('--view', action="store_true", help='Show the list of configured views')
+
+GROUP_OUTP_TMPL_SRC = """
+SNMPv3 Groups:
+
+ Group View
+ ----- ----
+ {% if group -%}{% for g in group -%}
+ {{ "%-20s" | format(g.name) }}{{ g.view }}({{ g.mode }})
+ {% endfor %}{% endif %}
+"""
+
+TRAPTGT_OUTP_TMPL_SRC = """
+SNMPv3 Trap-targets:
+
+ Tpap-target Port Protocol Auth Priv Type EngineID User
+ ----------- ---- -------- ---- ---- ---- -------- ----
+ {% if trap -%}{% for t in trap -%}
+ {{ "%-20s" | format(t.name) }} {{ t.port }} {{ t.proto }} {{ t.auth }} {{ t.priv }} {{ t.type }} {{ "%-32s" | format(t.engID) }} {{ t.user }}
+ {% endfor %}{% endif %}
+"""
+
+USER_OUTP_TMPL_SRC = """
+SNMPv3 Users:
+
+ User Auth Priv Mode Group
+ ---- ---- ---- ---- -----
+ {% if user -%}{% for u in user -%}
+ {{ "%-20s" | format(u.name) }}{{ u.auth }} {{ u.priv }} {{ u.mode }} {{ u.group }}
+ {% endfor %}{% endif %}
+"""
+
+VIEW_OUTP_TMPL_SRC = """
+SNMPv3 Views:
+ {% if view -%}{% for v in view %}
+ View : {{ v.name }}
+ OIDs : .{{ v.oids | join("\n .")}}
+ {% endfor %}{% endif %}
+"""
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service snmp v3'):
+ print("SNMP v3 is not configured")
+ sys.exit(0)
+
+ data = {
+ 'group': [],
+ 'trap': [],
+ 'user': [],
+ 'view': []
+ }
+
+ if c.exists_effective('service snmp v3 group'):
+ for g in c.list_effective_nodes('service snmp v3 group'):
+ group = {
+ 'name': g,
+ 'mode': '',
+ 'view': ''
+ }
+ group['mode'] = c.return_effective_value('service snmp v3 group {0} mode'.format(g))
+ group['view'] = c.return_effective_value('service snmp v3 group {0} view'.format(g))
+
+ data['group'].append(group)
+
+ if c.exists_effective('service snmp v3 user'):
+ for u in c.list_effective_nodes('service snmp v3 user'):
+ user = {
+ 'name' : u,
+ 'mode' : '',
+ 'auth' : '',
+ 'priv' : '',
+ 'group': ''
+ }
+ user['mode'] = c.return_effective_value('service snmp v3 user {0} mode'.format(u))
+ user['auth'] = c.return_effective_value('service snmp v3 user {0} auth type'.format(u))
+ user['priv'] = c.return_effective_value('service snmp v3 user {0} privacy type'.format(u))
+ user['group'] = c.return_effective_value('service snmp v3 user {0} group'.format(u))
+
+ data['user'].append(user)
+
+ if c.exists_effective('service snmp v3 view'):
+ for v in c.list_effective_nodes('service snmp v3 view'):
+ view = {
+ 'name': v,
+ 'oids': []
+ }
+ view['oids'] = c.list_effective_nodes('service snmp v3 view {0} oid'.format(v))
+
+ data['view'].append(view)
+
+ if c.exists_effective('service snmp v3 trap-target'):
+ for t in c.list_effective_nodes('service snmp v3 trap-target'):
+ trap = {
+ 'name' : t,
+ 'port' : '',
+ 'proto': '',
+ 'auth' : '',
+ 'priv' : '',
+ 'type' : '',
+ 'engID': '',
+ 'user' : ''
+ }
+ trap['port'] = c.return_effective_value('service snmp v3 trap-target {0} port'.format(t))
+ trap['proto'] = c.return_effective_value('service snmp v3 trap-target {0} protocol'.format(t))
+ trap['auth'] = c.return_effective_value('service snmp v3 trap-target {0} auth type'.format(t))
+ trap['priv'] = c.return_effective_value('service snmp v3 trap-target {0} privacy type'.format(t))
+ trap['type'] = c.return_effective_value('service snmp v3 trap-target {0} type'.format(t))
+ trap['engID'] = c.return_effective_value('service snmp v3 trap-target {0} engineid'.format(t))
+ trap['user'] = c.return_effective_value('service snmp v3 trap-target {0} user'.format(t))
+
+ data['trap'].append(trap)
+
+ print(data)
+ if args.all:
+ # Special case, print all templates !
+ tmpl = jinja2.Template(GROUP_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+ tmpl = jinja2.Template(TRAPTGT_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+ tmpl = jinja2.Template(USER_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+ tmpl = jinja2.Template(VIEW_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.group:
+ tmpl = jinja2.Template(GROUP_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.trap:
+ tmpl = jinja2.Template(TRAPTGT_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.user:
+ tmpl = jinja2.Template(USER_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.view:
+ tmpl = jinja2.Template(VIEW_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ else:
+ parser.print_help()
+
+ sys.exit(1)
diff --git a/src/op_mode/snmp_v3_showcerts.sh b/src/op_mode/snmp_v3_showcerts.sh
new file mode 100755
index 000000000..015b2e662
--- /dev/null
+++ b/src/op_mode/snmp_v3_showcerts.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+files=`sudo ls /etc/snmp/tls/certs/ 2> /dev/null`;
+if [ -n "$files" ]; then
+ sudo /usr/bin/net-snmp-cert showcerts --subject --fingerprint
+else
+ echo "You don't have any certificates. Put it in '/etc/snmp/tls/certs/' folder."
+fi
diff --git a/src/op_mode/system_integrity.py b/src/op_mode/system_integrity.py
new file mode 100755
index 000000000..c0e3d1095
--- /dev/null
+++ b/src/op_mode/system_integrity.py
@@ -0,0 +1,70 @@
+#!/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 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 = cmd('/usr/libexec/vyos/op_mode/version.py')
+ if re.search('^Built on:.+',a, re.M) == None:
+ return None
+
+ dt = ( re.sub('Built on: +','', re.search('^Built on:.+',a, re.M).group(0)) )
+ return datetime.strptime(dt,'%a %d %b %Y %H:%M %Z')
+
+def check_pkgs(dt):
+ pkg_diffs = {
+ 'buildtime' : str(dt),
+ 'pkg' : {}
+ }
+
+ pkg_info = os.listdir('/var/lib/dpkg/info/')
+ for file in pkg_info:
+ if re.search('\.list$', file):
+ fts = os.stat('/var/lib/dpkg/info/' + file).st_mtime
+ dt_str = (datetime.utcfromtimestamp(fts).strftime('%Y-%m-%d %H:%M:%S'))
+ fdt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S')
+ if fdt > dt:
+ pkg_diffs['pkg'].update( { str(re.sub('\.list','',file)) : str(fdt)})
+
+ if len(pkg_diffs['pkg']) != 0:
+ return pkg_diffs
+ else:
+ return None
+
+def main():
+ dt = get_sys_build_version()
+ pkgs = check_pkgs(dt)
+ if pkgs != None:
+ print ("The following packages don\'t fit the image creation time\nbuild time:\t" + pkgs['buildtime'])
+ for k, v in pkgs['pkg'].items():
+ print ("installed: " + v + '\t' + k)
+
+if __name__ == '__main__':
+ sys.exit( main() )
+
diff --git a/src/op_mode/toggle_help_binding.sh b/src/op_mode/toggle_help_binding.sh
new file mode 100755
index 000000000..a8708f3da
--- /dev/null
+++ b/src/op_mode/toggle_help_binding.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+# 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/>.
+
+# Script for [un-]binding the question mark key for getting help
+if [ "$1" == 'disable' ]; then
+ sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc
+ echo "bind '\"?\": self-insert' # vyatta key binding" >> $HOME/.bashrc
+ bind '"?": self-insert'
+else
+ sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc
+ bind '"?": possible-completions'
+fi
diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py
new file mode 100755
index 000000000..2c1db20bf
--- /dev/null
+++ b/src/op_mode/vrrp.py
@@ -0,0 +1,55 @@
+#!/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 time
+import argparse
+import json
+import tabulate
+
+import vyos.util
+
+from vyos.ifconfig.vrrp import VRRP
+from vyos.ifconfig.vrrp import VRRPError, VRRPNoData
+
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("-s", "--summary", action="store_true", help="Print VRRP summary")
+group.add_argument("-t", "--statistics", action="store_true", help="Print VRRP statistics")
+group.add_argument("-d", "--data", action="store_true", help="Print detailed VRRP data")
+
+args = parser.parse_args()
+
+# Exit early if VRRP is dead or not configured
+if not VRRP.is_running():
+ print('VRRP is not running')
+ sys.exit(0)
+
+try:
+ if args.summary:
+ print(VRRP.format(VRRP.collect('json')))
+ elif args.statistics:
+ print(VRRP.collect('stats'))
+ elif args.data:
+ print(VRRP.collect('state'))
+ else:
+ parser.print_help()
+ sys.exit(1)
+except VRRPNoData as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py
new file mode 100755
index 000000000..e08bc983a
--- /dev/null
+++ b/src/op_mode/wireguard.py
@@ -0,0 +1,159 @@
+#!/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 argparse
+import os
+import sys
+import shutil
+import syslog as sl
+import re
+
+from vyos.config import Config
+from vyos.ifconfig import WireGuardIf
+from vyos.util import cmd
+from vyos.util import run
+from vyos.util import check_kmod
+from vyos import ConfigError
+
+dir = r'/config/auth/wireguard'
+psk = dir + '/preshared.key'
+
+k_mod = 'wireguard'
+
+def generate_keypair(pk, pub):
+ """ generates a keypair which is stored in /config/auth/wireguard """
+ old_umask = os.umask(0o027)
+ if run(f'wg genkey | tee {pk} | wg pubkey > {pub}') != 0:
+ raise ConfigError("wireguard key-pair generation failed")
+ else:
+ sl.syslog(
+ sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir)
+ os.umask(old_umask)
+
+
+def genkey(location):
+ """ helper function to check, regenerate the keypair """
+ pk = "{}/private.key".format(location)
+ pub = "{}/public.key".format(location)
+ old_umask = os.umask(0o027)
+ if os.path.exists(pk) and os.path.exists(pub):
+ try:
+ choice = input(
+ "You already have a wireguard key-pair, do you want to re-generate? [y/n] ")
+ if choice == 'y' or choice == 'Y':
+ generate_keypair(pk, pub)
+ except KeyboardInterrupt:
+ sys.exit(0)
+ else:
+ """ if keypair is bing executed from a running iso """
+ if not os.path.exists(location):
+ 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)
+
+
+def showkey(key):
+ """ helper function to show privkey or pubkey """
+ if os.path.exists(key):
+ print (open(key).read().strip())
+ else:
+ print ("{} not found".format(key))
+
+
+def genpsk():
+ """
+ generates a preshared key and shows it on stdout,
+ it's stored only in the cli config
+ """
+
+ psk = cmd('wg genpsk')
+ print(psk)
+
+def list_key_dirs():
+ """ lists all dirs under /config/auth/wireguard """
+ if os.path.exists(dir):
+ nks = next(os.walk(dir))[1]
+ for nk in nks:
+ print (nk)
+
+def del_key_dir(kname):
+ """ deletes /config/auth/wireguard/<kname> """
+ kdir = "{0}/{1}".format(dir,kname)
+ if not os.path.isdir(kdir):
+ print ("named keypair {} not found".format(kname))
+ return 1
+ shutil.rmtree(kdir)
+
+
+if __name__ == '__main__':
+ check_kmod(k_mod)
+ parser = argparse.ArgumentParser(description='wireguard key management')
+ parser.add_argument(
+ '--genkey', action="store_true", help='generate key-pair')
+ parser.add_argument(
+ '--showpub', action="store_true", help='shows public key')
+ parser.add_argument(
+ '--showpriv', action="store_true", help='shows private key')
+ parser.add_argument(
+ '--genpsk', action="store_true", help='generates preshared-key')
+ parser.add_argument(
+ '--location', action="store", help='key location within {}'.format(dir))
+ parser.add_argument(
+ '--listkdir', action="store_true", help='lists named keydirectories')
+ parser.add_argument(
+ '--delkdir', action="store_true", help='removes named keydirectories')
+ parser.add_argument(
+ '--showinterface', action="store", help='shows interface details')
+ args = parser.parse_args()
+
+ try:
+ if args.genkey:
+ if args.location:
+ genkey("{0}/{1}".format(dir, args.location))
+ else:
+ genkey("{}/default".format(dir))
+ if args.showpub:
+ if args.location:
+ showkey("{0}/{1}/public.key".format(dir, args.location))
+ else:
+ showkey("{}/default/public.key".format(dir))
+ if args.showpriv:
+ if args.location:
+ showkey("{0}/{1}/private.key".format(dir, args.location))
+ else:
+ showkey("{}/default/private.key".format(dir))
+ if args.genpsk:
+ genpsk()
+ if args.listkdir:
+ list_key_dirs()
+ if args.showinterface:
+ try:
+ intf = WireGuardIf(args.showinterface, create=False, debug=False)
+ print(intf.operational.show_interface())
+ # the interface does not exists
+ except Exception:
+ pass
+ if args.delkdir:
+ if args.location:
+ del_key_dir(args.location)
+ else:
+ del_key_dir("default")
+
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/pam-configs/radius b/src/pam-configs/radius
new file mode 100644
index 000000000..0e2c71e38
--- /dev/null
+++ b/src/pam-configs/radius
@@ -0,0 +1,20 @@
+Name: RADIUS authentication
+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-hostsd b/src/services/vyos-hostsd
new file mode 100755
index 000000000..0079f7e5c
--- /dev/null
+++ b/src/services/vyos-hostsd
@@ -0,0 +1,618 @@
+#!/usr/bin/env python3
+#
+# 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
+# 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/>.
+#
+#########
+# USAGE #
+#########
+# This daemon listens on its socket for JSON messages.
+# The received message format is:
+#
+# { 'type': '<message type>',
+# 'op': '<message operation>',
+# 'data': <data list or dict>
+# }
+#
+# For supported message types, see below.
+# 'op' can be 'add', delete', 'get', 'set' or 'apply'.
+# Different message types support different sets of operations and different
+# data formats.
+#
+# Changes to configuration made via add or delete don't take effect immediately,
+# they are remembered in a state variable and saved to disk to a state file.
+# State is remembered across daemon restarts but not across system reboots
+# as it's saved in a temporary filesystem (/run).
+#
+# 'apply' is a special operation that applies the configuration from the cached
+# state, rendering all config files and reloading relevant daemons (currently
+# just pdns-recursor via rec-control).
+#
+# note: 'add' operation also acts as 'update' as it uses dict.update, if the
+# 'data' dict item value is a dict. If it is a list, it uses list.append.
+#
+### tags
+# Tags can be arbitrary, but they are generally in this format:
+# 'static', 'system', 'dhcp(v6)-<intf>' or 'dhcp-server-<client ip>'
+# They are used to distinguish entries created by different scripts so they can
+# be removed and recreated without having to track what needs to be changed.
+# They are also used as a way to control which tags settings (e.g. nameservers)
+# get added to various config files via name_server_tags_(recursor|system)
+#
+### name_server_tags_(recursor|system)
+# A list of tags whose nameservers and search domains is used to generate
+# /etc/resolv.conf and pdns-recursor config.
+# system list is used to generate resolv.conf.
+# recursor list is used to generate pdns-rec forward-zones.
+# When generating each file, the order of nameservers is as per the order of
+# name_server_tags (the order in which tags were added), then the order in
+# which the name servers for each tag were added.
+#
+#### Message types
+#
+### name_servers
+#
+# { 'type': 'name_servers',
+# 'op': 'add',
+# 'data': {
+# '<str tag>': ['<str nameserver>', ...],
+# ...
+# }
+# }
+#
+# { 'type': 'name_servers',
+# 'op': 'delete',
+# 'data': ['<str tag>', ...]
+# }
+#
+# { 'type': 'name_servers',
+# 'op': 'get',
+# 'tag_regex': '<str regex>'
+# }
+# response:
+# { 'data': {
+# '<str tag>': ['<str nameserver>', ...],
+# ...
+# }
+# }
+#
+### name_server_tags
+#
+# { 'type': 'name_server_tags',
+# 'op': 'add',
+# 'data': ['<str tag>', ...]
+# }
+#
+# { 'type': 'name_server_tags',
+# 'op': 'delete',
+# 'data': ['<str tag>', ...]
+# }
+#
+# { 'type': 'name_server_tags',
+# 'op': 'get',
+# }
+# response:
+# { 'data': ['<str tag>', ...] }
+#
+### forward_zones
+## Additional zones added to pdns-recursor forward-zones-file.
+## If recursion-desired is true, '+' will be prepended to the zone line.
+## If addNTA is true, a NTA will be added via lua-config-file.
+#
+# { 'type': 'forward_zones',
+# 'op': 'add',
+# 'data': {
+# '<str zone>': {
+# 'nslist': ['<str nameserver>', ...],
+# 'addNTA': <bool>,
+# 'recursion-desired': <bool>
+# }
+# ...
+# }
+# }
+#
+# { 'type': 'forward_zones',
+# 'op': 'delete',
+# 'data': ['<str zone>', ...]
+# }
+#
+# { 'type': 'forward_zones',
+# 'op': 'get',
+# }
+# response:
+# { 'data': {
+# '<str zone>': { ... },
+# ...
+# }
+# }
+#
+#
+### search_domains
+#
+# { 'type': 'search_domains',
+# 'op': 'add',
+# 'data': {
+# '<str tag>': ['<str domain>', ...],
+# ...
+# }
+# }
+#
+# { 'type': 'search_domains',
+# 'op': 'delete',
+# 'data': ['<str tag>', ...]
+# }
+#
+# { 'type': 'search_domains',
+# 'op': 'get',
+# }
+# response:
+# { 'data': {
+# '<str tag>': ['<str domain>', ...],
+# ...
+# }
+# }
+#
+### hosts
+#
+# { 'type': 'hosts',
+# 'op': 'add',
+# 'data': {
+# '<str tag>': {
+# '<str host>': {
+# 'address': '<str address>',
+# 'aliases': ['<str alias>, ...]
+# },
+# ...
+# },
+# ...
+# }
+# }
+#
+# { 'type': 'hosts',
+# 'op': 'delete',
+# 'data': ['<str tag>', ...]
+# }
+#
+# { 'type': 'hosts',
+# 'op': 'get'
+# 'tag_regex': '<str regex>'
+# }
+# response:
+# { 'data': {
+# '<str tag>': {
+# '<str host>': {
+# 'address': '<str address>',
+# 'aliases': ['<str alias>, ...]
+# },
+# ...
+# },
+# ...
+# }
+# }
+### host_name
+#
+# { 'type': 'host_name',
+# 'op': 'set',
+# 'data': {
+# 'host_name': '<str hostname>'
+# 'domain_name': '<str domainname>'
+# }
+# }
+
+import os
+import sys
+import time
+import json
+import signal
+import traceback
+import re
+import logging
+import zmq
+from voluptuous import Schema, MultipleInvalid, Required, Any
+from collections import OrderedDict
+from vyos.util import popen, chown, chmod_755, makedir, process_named_running
+from vyos.template import render
+
+debug = True
+
+# Configure logging
+logger = logging.getLogger(__name__)
+# set stream as output
+logs_handler = logging.StreamHandler()
+logger.addHandler(logs_handler)
+
+if debug:
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
+
+RUN_DIR = "/run/vyos-hostsd"
+STATE_FILE = os.path.join(RUN_DIR, "vyos-hostsd.state")
+SOCKET_PATH = "ipc://" + os.path.join(RUN_DIR, 'vyos-hostsd.sock')
+
+RESOLV_CONF_FILE = '/etc/resolv.conf'
+HOSTS_FILE = '/etc/hosts'
+
+PDNS_REC_USER = PDNS_REC_GROUP = 'pdns'
+PDNS_REC_RUN_DIR = '/run/powerdns'
+PDNS_REC_LUA_CONF_FILE = f'{PDNS_REC_RUN_DIR}/recursor.vyos-hostsd.conf.lua'
+PDNS_REC_ZONES_FILE = f'{PDNS_REC_RUN_DIR}/recursor.forward-zones.conf'
+
+STATE = {
+ "name_servers": {},
+ "name_server_tags_recursor": [],
+ "name_server_tags_system": [],
+ "forward_zones": {},
+ "hosts": {},
+ "host_name": "vyos",
+ "domain_name": "",
+ "search_domains": {},
+ "changes": 0
+ }
+
+# the base schema that every received message must be in
+base_schema = Schema({
+ Required('op'): Any('add', 'delete', 'set', 'get', 'apply'),
+ 'type': Any('name_servers',
+ 'name_server_tags_recursor', 'name_server_tags_system',
+ 'forward_zones', 'search_domains', 'hosts', 'host_name'),
+ 'data': Any(list, dict),
+ 'tag': str,
+ 'tag_regex': str
+ })
+
+# more specific schemas
+op_schema = Schema({
+ 'op': str,
+ }, required=True)
+
+op_type_schema = op_schema.extend({
+ 'type': str,
+ }, required=True)
+
+host_name_add_schema = op_type_schema.extend({
+ 'data': {
+ 'host_name': str,
+ 'domain_name': Any(str, None)
+ }
+ }, required=True)
+
+data_dict_list_schema = op_type_schema.extend({
+ 'data': {
+ str: [str]
+ }
+ }, required=True)
+
+data_list_schema = op_type_schema.extend({
+ 'data': [str]
+ }, required=True)
+
+tag_regex_schema = op_type_schema.extend({
+ 'tag_regex': str
+ }, required=True)
+
+forward_zone_add_schema = op_type_schema.extend({
+ 'data': {
+ str: {
+ 'nslist': [str],
+ 'addNTA': bool,
+ 'recursion-desired': bool
+ }
+ }
+ }, required=True)
+
+hosts_add_schema = op_type_schema.extend({
+ 'data': {
+ str: {
+ str: {
+ 'address': str,
+ 'aliases': [str]
+ }
+ }
+ }
+ }, required=True)
+
+
+# op and type to schema mapping
+msg_schema_map = {
+ 'name_servers': {
+ 'add': data_dict_list_schema,
+ 'delete': data_list_schema,
+ 'get': tag_regex_schema
+ },
+ 'name_server_tags_recursor': {
+ 'add': data_list_schema,
+ 'delete': data_list_schema,
+ 'get': op_type_schema
+ },
+ 'name_server_tags_system': {
+ 'add': data_list_schema,
+ 'delete': data_list_schema,
+ 'get': op_type_schema
+ },
+ 'forward_zones': {
+ 'add': forward_zone_add_schema,
+ 'delete': data_list_schema,
+ 'get': op_type_schema
+ },
+ 'search_domains': {
+ 'add': data_dict_list_schema,
+ 'delete': data_list_schema,
+ 'get': tag_regex_schema
+ },
+ 'hosts': {
+ 'add': hosts_add_schema,
+ 'delete': data_list_schema,
+ 'get': tag_regex_schema
+ },
+ 'host_name': {
+ 'set': host_name_add_schema
+ },
+ None: {
+ 'apply': op_schema
+ }
+ }
+
+def validate_schema(data):
+ base_schema(data)
+
+ try:
+ schema = msg_schema_map[data['type'] if 'type' in data else None][data['op']]
+ schema(data)
+ except KeyError:
+ raise ValueError((
+ 'Invalid or unknown combination: '
+ f'op: "{data["op"]}", type: "{data["type"]}"'))
+
+
+def pdns_rec_control(command):
+ # pdns-r process name is NOT equal to the name shown in ps
+ if not process_named_running('pdns-r/worker'):
+ logger.info(f'pdns_recursor not running, not sending "{command}"')
+ return
+
+ logger.info(f'Running "rec_control {command}"')
+ (ret,ret_code) = popen((
+ f"rec_control --socket-dir={PDNS_REC_RUN_DIR} {command}"))
+ if ret_code > 0:
+ logger.exception((
+ f'"rec_control {command}" failed with exit status {ret_code}, '
+ f'output: "{ret}"'))
+
+def make_resolv_conf(state):
+ logger.info(f"Writing {RESOLV_CONF_FILE}")
+ render(RESOLV_CONF_FILE, 'vyos-hostsd/resolv.conf.tmpl', state,
+ user='root', group='root')
+
+def make_hosts(state):
+ logger.info(f"Writing {HOSTS_FILE}")
+ render(HOSTS_FILE, 'vyos-hostsd/hosts.tmpl', state,
+ user='root', group='root')
+
+def make_pdns_rec_conf(state):
+ logger.info(f"Writing {PDNS_REC_LUA_CONF_FILE}")
+
+ # on boot, /run/powerdns does not exist, so create it
+ makedir(PDNS_REC_RUN_DIR, user=PDNS_REC_USER, group=PDNS_REC_GROUP)
+ chmod_755(PDNS_REC_RUN_DIR)
+
+ render(PDNS_REC_LUA_CONF_FILE,
+ 'dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl',
+ state, user=PDNS_REC_USER, group=PDNS_REC_GROUP)
+
+ logger.info(f"Writing {PDNS_REC_ZONES_FILE}")
+ render(PDNS_REC_ZONES_FILE,
+ 'dns-forwarding/recursor.forward-zones.conf.tmpl',
+ state, user=PDNS_REC_USER, group=PDNS_REC_GROUP)
+
+def set_host_name(state, data):
+ if data['host_name']:
+ state['host_name'] = data['host_name']
+ if 'domain_name' in data:
+ state['domain_name'] = data['domain_name']
+
+def add_items_to_dict(_dict, items):
+ """
+ Dedupes and preserves sort order.
+ """
+ assert isinstance(_dict, dict)
+ assert isinstance(items, dict)
+
+ if not items:
+ return
+
+ _dict.update(items)
+
+def add_items_to_dict_as_keys(_dict, items):
+ """
+ Added item values are converted to OrderedDict with the value as keys
+ and null values. This is to emulate a list but with inherent deduplication.
+ Dedupes and preserves sort order.
+ """
+ assert isinstance(_dict, dict)
+ assert isinstance(items, dict)
+
+ if not items:
+ return
+
+ for item, item_val in items.items():
+ if item not in _dict:
+ _dict[item] = OrderedDict({})
+ _dict[item].update(OrderedDict.fromkeys(item_val))
+
+def add_items_to_list(_list, items):
+ """
+ Dedupes and preserves sort order.
+ """
+ assert isinstance(_list, list)
+ assert isinstance(items, list)
+
+ if not items:
+ return
+
+ for item in items:
+ if item not in _list:
+ _list.append(item)
+
+def delete_items_from_dict(_dict, items):
+ """
+ items is a list of keys to delete.
+ Doesn't error if the key doesn't exist.
+ """
+ assert isinstance(_dict, dict)
+ assert isinstance(items, list)
+
+ for item in items:
+ if item in _dict:
+ del _dict[item]
+
+def delete_items_from_list(_list, items):
+ """
+ items is a list of items to remove.
+ Doesn't error if the key doesn't exist.
+ """
+ assert isinstance(_list, list)
+ assert isinstance(items, list)
+
+ for item in items:
+ if item in _list:
+ _list.remove(item)
+
+def get_items_from_dict_regex(_dict, item_regex_string):
+ """
+ Returns the items whose keys match item_regex_string.
+ """
+ assert isinstance(_dict, dict)
+ assert isinstance(item_regex_string, str)
+
+ tmp = {}
+ regex = re.compile(item_regex_string)
+ for item in _dict:
+ if regex.match(item):
+ tmp[item] = _dict[item]
+ return tmp
+
+def get_option(msg, key):
+ if key in msg:
+ return msg[key]
+ else:
+ raise ValueError("Missing required option \"{0}\"".format(key))
+
+def handle_message(msg):
+ result = None
+ op = get_option(msg, 'op')
+
+ if op in ['add', 'delete', 'set']:
+ STATE['changes'] += 1
+
+ if op == 'delete':
+ _type = get_option(msg, 'type')
+ data = get_option(msg, 'data')
+ if _type in ['name_servers', 'forward_zones', 'search_domains', 'hosts']:
+ delete_items_from_dict(STATE[_type], data)
+ elif _type in ['name_server_tags_recursor', 'name_server_tags_system']:
+ delete_items_from_list(STATE[_type], data)
+ else:
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
+ elif op == 'add':
+ _type = get_option(msg, 'type')
+ data = get_option(msg, 'data')
+ if _type in ['name_servers', 'search_domains']:
+ add_items_to_dict_as_keys(STATE[_type], data)
+ elif _type in ['forward_zones', 'hosts']:
+ add_items_to_dict(STATE[_type], data)
+ # maybe we need to rec_control clear-nta each domain that was removed here?
+ elif _type in ['name_server_tags_recursor', 'name_server_tags_system']:
+ add_items_to_list(STATE[_type], data)
+ else:
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
+ elif op == 'set':
+ _type = get_option(msg, 'type')
+ data = get_option(msg, 'data')
+ if _type == 'host_name':
+ set_host_name(STATE, data)
+ else:
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
+ elif op == 'get':
+ _type = get_option(msg, 'type')
+ if _type in ['name_servers', 'search_domains', 'hosts']:
+ tag_regex = get_option(msg, 'tag_regex')
+ result = get_items_from_dict_regex(STATE[_type], tag_regex)
+ elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'forward_zones']:
+ result = STATE[_type]
+ else:
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
+ elif op == 'apply':
+ logger.info(f"Applying {STATE['changes']} changes")
+ make_resolv_conf(STATE)
+ make_hosts(STATE)
+ make_pdns_rec_conf(STATE)
+ pdns_rec_control('reload-lua-config')
+ pdns_rec_control('reload-zones')
+ logger.info("Success")
+ result = {'message': f'Applied {STATE["changes"]} changes'}
+ STATE['changes'] = 0
+
+ else:
+ raise ValueError(f"Unknown operation {op}")
+
+ logger.debug(f"Saving state to {STATE_FILE}")
+ with open(STATE_FILE, 'w') as f:
+ json.dump(STATE, f)
+
+ return result
+
+if __name__ == '__main__':
+ # Create a directory for state checkpoints
+ os.makedirs(RUN_DIR, exist_ok=True)
+ if os.path.exists(STATE_FILE):
+ with open(STATE_FILE, 'r') as f:
+ try:
+ STATE = json.load(f)
+ except:
+ logger.exception(traceback.format_exc())
+ logger.exception("Failed to load the state file, using default")
+
+ context = zmq.Context()
+ socket = context.socket(zmq.REP)
+
+ # Set the right permissions on the socket, then change it back
+ o_mask = os.umask(0o007)
+ socket.bind(SOCKET_PATH)
+ os.umask(o_mask)
+
+ while True:
+ # Wait for next request from client
+ msg_json = socket.recv().decode()
+ logger.debug(f"Request data: {msg_json}")
+
+ try:
+ msg = json.loads(msg_json)
+ validate_schema(msg)
+
+ resp = {}
+ resp['data'] = handle_message(msg)
+ except ValueError as e:
+ resp['error'] = str(e)
+ except MultipleInvalid as e:
+ # raised by schema
+ resp['error'] = f'Invalid message: {str(e)}'
+ logger.exception(resp['error'])
+ except:
+ logger.exception(traceback.format_exc())
+ resp['error'] = "Internal error"
+
+ # Send reply back to client
+ socket.send(json.dumps(resp).encode())
+ logger.debug(f"Sent response: {resp}")
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
new file mode 100755
index 000000000..d5730d86c
--- /dev/null
+++ b/src/services/vyos-http-api-server
@@ -0,0 +1,400 @@
+#!/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 sys
+import grp
+import json
+import traceback
+import threading
+import signal
+
+import vyos.config
+
+from flask import Flask, request
+from waitress import serve
+
+from functools import wraps
+
+from vyos.configsession import ConfigSession, ConfigSessionError
+
+
+DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
+CFG_GROUP = 'vyattacfg'
+
+app = Flask(__name__)
+
+# Giant lock!
+lock = threading.Lock()
+
+def load_server_config():
+ with open(DEFAULT_CONFIG_FILE) as f:
+ config = json.load(f)
+ return config
+
+def check_auth(key_list, key):
+ id = None
+ for k in key_list:
+ if k['key'] == key:
+ id = k['id']
+ return id
+
+def error(code, msg):
+ resp = {"success": False, "error": msg, "data": None}
+ return json.dumps(resp), code
+
+def success(data):
+ resp = {"success": True, "data": data, "error": None}
+ return json.dumps(resp)
+
+def get_command(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ cmd = request.form.get("data")
+ if not cmd:
+ return error(400, "Non-empty data field is required")
+ try:
+ cmd = json.loads(cmd)
+ except Exception as e:
+ return error(400, "Failed to parse JSON: {0}".format(e))
+ return f(cmd, *args, **kwargs)
+
+ return decorated_function
+
+def auth_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ key = request.form.get("key")
+ api_keys = app.config['vyos_keys']
+ id = check_auth(api_keys, key)
+ if not id:
+ return error(401, "Valid API key is required")
+ return f(*args, **kwargs)
+
+ return decorated_function
+
+@app.route('/configure', methods=['POST'])
+@get_command
+@auth_required
+def configure_op(commands):
+ session = app.config['vyos_session']
+ env = session.get_session_env()
+ config = vyos.config.Config(session_env=env)
+
+ strict_field = request.form.get("strict")
+ if strict_field == "true":
+ strict = True
+ else:
+ strict = False
+
+ # Allow users to pass just one command
+ if not isinstance(commands, list):
+ commands = [commands]
+
+ # We don't want multiple people/apps to be able to commit at once,
+ # or modify the shared session while someone else is doing the same,
+ # so the lock is really global
+ lock.acquire()
+
+ status = 200
+ error_msg = None
+ try:
+ for c in commands:
+ # What we've got may not even be a dict
+ if not isinstance(c, dict):
+ raise ConfigSessionError("Malformed command \"{0}\": any command must be a dict".format(json.dumps(c)))
+
+ # Missing op or path is a show stopper
+ if not ('op' in c):
+ raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c)))
+ if not ('path' in c):
+ raise ConfigSessionError("Malformed command \"{0}\": missing \"path\" field".format(json.dumps(c)))
+
+ # Missing value is fine, substitute for empty string
+ if 'value' in c:
+ value = c['value']
+ else:
+ value = ""
+
+ op = c['op']
+ path = c['path']
+
+ if not path:
+ raise ConfigSessionError("Malformed command \"{0}\": empty path".format(json.dumps(c)))
+
+ # Type checking
+ if not isinstance(path, list):
+ raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(c)))
+
+ if not isinstance(value, str):
+ raise ConfigSessionError("Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(c)))
+
+ # Account for the case when value field is present and set to null
+ if not value:
+ value = ""
+
+ # For vyos.configsessios calls that have no separate value arguments,
+ # and for type checking too
+ try:
+ cfg_path = " ".join(path + [value]).strip()
+ except TypeError:
+ raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(c)))
+
+ if op == 'set':
+ # XXX: it would be nice to do a strict check for "path already exists",
+ # but there's probably no way to do that
+ session.set(path, value=value)
+ elif op == 'delete':
+ if strict and not config.exists(cfg_path):
+ raise ConfigSessionError("Cannot delete [{0}]: path/value does not exist".format(cfg_path))
+ session.delete(path, value=value)
+ elif op == 'comment':
+ session.comment(path, value=value)
+ else:
+ raise ConfigSessionError("\"{0}\" is not a valid operation".format(op))
+ # end for
+ session.commit()
+ print("Configuration modified via HTTP API using key \"{0}\"".format(id))
+ except ConfigSessionError as e:
+ session.discard()
+ status = 400
+ if app.config['vyos_debug']:
+ print(traceback.format_exc(), file=sys.stderr)
+ error_msg = str(e)
+ except Exception as e:
+ session.discard()
+ print(traceback.format_exc(), file=sys.stderr)
+ status = 500
+
+ # Don't give the details away to the outer world
+ error_msg = "An internal error occured. Check the logs for details."
+ finally:
+ lock.release()
+
+ if status != 200:
+ return error(status, error_msg)
+ else:
+ return success(None)
+
+@app.route('/retrieve', methods=['POST'])
+@get_command
+@auth_required
+def retrieve_op(command):
+ session = app.config['vyos_session']
+ env = session.get_session_env()
+ config = vyos.config.Config(session_env=env)
+
+ try:
+ op = command['op']
+ path = " ".join(command['path'])
+ except KeyError:
+ return error(400, "Missing required field. \"op\" and \"path\" fields are required")
+
+ try:
+ if op == 'returnValue':
+ res = config.return_value(path)
+ elif op == 'returnValues':
+ res = config.return_values(path)
+ elif op == 'exists':
+ res = config.exists(path)
+ elif op == 'showConfig':
+ config_format = 'json'
+ if 'configFormat' in command:
+ config_format = command['configFormat']
+
+ res = session.show_config(path=command['path'])
+ if config_format == 'json':
+ config_tree = vyos.configtree.ConfigTree(res)
+ res = json.loads(config_tree.to_json())
+ elif config_format == 'json_ast':
+ config_tree = vyos.configtree.ConfigTree(res)
+ res = json.loads(config_tree.to_json_ast())
+ elif config_format == 'raw':
+ pass
+ else:
+ return error(400, "\"{0}\" is not a valid config format".format(config_format))
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except ConfigSessionError 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('/config-file', methods=['POST'])
+@get_command
+@auth_required
+def config_file_op(command):
+ session = app.config['vyos_session']
+
+ try:
+ op = command['op']
+ except KeyError:
+ return error(400, "Missing required field \"op\"")
+
+ try:
+ if op == 'save':
+ try:
+ path = command['file']
+ except KeyError:
+ path = '/config/config.boot'
+ res = session.save_config(path)
+ elif op == 'load':
+ try:
+ path = command['file']
+ except KeyError:
+ return error(400, "Missing required field \"file\"")
+ res = session.load_config(path)
+ res = session.commit()
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except ConfigSessionError 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('/image', methods=['POST'])
+@get_command
+@auth_required
+def image_op(command):
+ session = app.config['vyos_session']
+
+ try:
+ op = command['op']
+ except KeyError:
+ return error(400, "Missing required field \"op\"")
+
+ try:
+ if op == 'add':
+ try:
+ url = command['url']
+ except KeyError:
+ return error(400, "Missing required field \"url\"")
+ res = session.install_image(url)
+ elif op == 'delete':
+ try:
+ name = command['name']
+ except KeyError:
+ return error(400, "Missing required field \"name\"")
+ res = session.remove_image(name)
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except ConfigSessionError 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('/generate', methods=['POST'])
+@get_command
+@auth_required
+def generate_op(command):
+ session = app.config['vyos_session']
+
+ try:
+ op = command['op']
+ path = command['path']
+ except KeyError:
+ return error(400, "Missing required field. \"op\" and \"path\" fields are required")
+
+ if not isinstance(path, list):
+ return error(400, "Malformed command: \"path\" field must be a list of strings")
+
+ try:
+ if op == 'generate':
+ res = session.generate(path)
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except ConfigSessionError 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', methods=['POST'])
+@get_command
+@auth_required
+def show_op(command):
+ session = app.config['vyos_session']
+
+ try:
+ op = command['op']
+ path = command['path']
+ except KeyError:
+ return error(400, "Missing required field. \"op\" and \"path\" fields are required")
+
+ if not isinstance(path, list):
+ return error(400, "Malformed command: \"path\" field must be a list of strings")
+
+ try:
+ if op == 'show':
+ res = session.show(path)
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except ConfigSessionError 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)
+
+def shutdown():
+ raise KeyboardInterrupt
+
+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
+ cfg_group = grp.getgrnam(CFG_GROUP)
+ os.setgid(cfg_group.gr_gid)
+
+ # Need to set file permissions to 775 too so that every vyattacfg group member
+ # has write access to the running config
+ os.umask(0o002)
+
+ try:
+ server_config = load_server_config()
+ except Exception as e:
+ print("Failed to load the HTTP API server config: {0}".format(e))
+
+ session = ConfigSession(os.getpid())
+
+ app.config['vyos_session'] = session
+ app.config['vyos_keys'] = server_config['api_keys']
+ app.config['vyos_debug'] = server_config['debug']
+
+ def sig_handler(signum, frame):
+ shutdown()
+
+ signal.signal(signal.SIGTERM, sig_handler)
+
+ try:
+ serve(app, host=server_config["listen_address"],
+ port=server_config["port"])
+ except OSError as e:
+ print(f"OSError {e}")
diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
new file mode 100755
index 000000000..7e2076820
--- /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)
+ 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/normalize-ip b/src/system/normalize-ip
new file mode 100755
index 000000000..08f922a8e
--- /dev/null
+++ b/src/system/normalize-ip
@@ -0,0 +1,43 @@
+#!/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/>.
+#
+#
+
+# Normalizes IPv6 addresses so that they can be passed to iproute2,
+# since iproute2 will not take an address with leading zeroes for an argument
+
+import re
+import sys
+import ipaddress
+
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Argument required")
+ sys.exit(1)
+
+ address_string, prefix_length = re.match(r'(.+)/(.+)', sys.argv[1]).groups()
+
+ try:
+ address = ipaddress.IPv6Address(address_string)
+ normalized_address = address.compressed
+ except ipaddress.AddressValueError:
+ # It's likely an IPv4 address, do nothing
+ normalized_address = address_string
+
+ print("{0}/{1}".format(normalized_address, prefix_length))
+ sys.exit(0)
+
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
new file mode 100755
index 000000000..a062dc810
--- /dev/null
+++ b/src/system/on-dhcp-event.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+
+# This script came from ubnt.com forum user "bradd" in the following post
+# http://community.ubnt.com/t5/EdgeMAX/Automatic-DNS-resolution-of-DHCP-client-names/td-p/651311
+# It has been modified by Ubiquiti to update the /etc/host file
+# instead of adding to the CLI.
+# Thanks to forum user "itsmarcos" for bug fix & improvements
+# Thanks to forum user "ruudboon" for multiple domain fix
+# Thanks to forum user "chibby85" for expire patch and static-mapping
+
+if [ $# -lt 5 ]; then
+ echo Invalid args
+ logger -s -t on-dhcp-event "Invalid args \"$@\""
+ exit 1
+fi
+
+action=$1
+client_name=$2
+client_ip=$3
+client_mac=$4
+domain=$5
+hostsd_client="/usr/bin/vyos-hostsd-client"
+
+if [ -z "$client_name" ]; then
+ logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
+ client_name=$(echo "client-"$client_mac | tr : -)
+fi
+
+if [ "$domain" == "..YYZ!" ]; then
+ client_fqdn_name=$client_name
+ client_search_expr=$client_name
+else
+ client_fqdn_name=$client_name.$domain
+ client_search_expr="$client_name\\.$domain"
+fi
+
+case "$action" in
+ commit) # add mapping for new lease
+ $hostsd_client --add-hosts "$client_fqdn_name,$client_ip" --tag "dhcp-server-$client_ip" --apply
+ exit 0
+ ;;
+
+ release) # delete mapping for released address
+ $hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply
+ exit 0
+ ;;
+
+ *)
+ logger -s -t on-dhcp-event "Invalid command \"$1\""
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/src/system/post-upgrade b/src/system/post-upgrade
new file mode 100755
index 000000000..41b7c01ba
--- /dev/null
+++ b/src/system/post-upgrade
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+chown -R root:vyattacfg /config
diff --git a/src/system/unpriv-ip b/src/system/unpriv-ip
new file mode 100755
index 000000000..1ea0d626a
--- /dev/null
+++ b/src/system/unpriv-ip
@@ -0,0 +1,2 @@
+#!/bin/sh
+sudo /sbin/ip $*
diff --git a/src/systemd/accel-ppp@.service b/src/systemd/accel-ppp@.service
new file mode 100644
index 000000000..256112769
--- /dev/null
+++ b/src/systemd/accel-ppp@.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=Accel-PPP - High performance VPN server application for Linux
+RequiresMountsFor=/run
+ConditionPathExists=/run/accel-pppd/%i.conf
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=/run/accel-pppd
+ExecStart=/usr/sbin/accel-pppd -d -p /run/accel-pppd/%i.pid -c /run/accel-pppd/%i.conf
+ExecReload=/bin/kill -SIGUSR1 $MAINPID
+PIDFile=/run/accel-pppd/%i.pid
+Type=forking
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/ddclient.service b/src/systemd/ddclient.service
new file mode 100644
index 000000000..a4d55827a
--- /dev/null
+++ b/src/systemd/ddclient.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Dynamic DNS Update Client
+RequiresMountsFor=/run
+ConditionPathExists=/run/ddclient/ddclient.conf
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=/run/ddclient
+Type=forking
+PIDFile=/run/ddclient/ddclient.pid
+ExecStart=/usr/sbin/ddclient -cache /run/ddclient/ddclient.cache -pid /run/ddclient/ddclient.pid -file /run/ddclient/ddclient.conf
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service
new file mode 100644
index 000000000..2ced1038a
--- /dev/null
+++ b/src/systemd/dhclient@.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=DHCP client on %i
+Documentation=man:dhclient(8)
+ConditionPathExists=/var/lib/dhcp/dhclient_%i.conf
+ConditionPathExists=/var/lib/dhcp/dhclient_%i.options
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=/var/lib/dhcp
+Type=exec
+EnvironmentFile=-/var/lib/dhcp/dhclient_%i.options
+PIDFile=/var/lib/dhcp/dhclient_%i.pid
+ExecStart=/sbin/dhclient -4 $DHCLIENT_OPTS
+ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/dhcp6c@.service b/src/systemd/dhcp6c@.service
new file mode 100644
index 000000000..9a97ee261
--- /dev/null
+++ b/src/systemd/dhcp6c@.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=WIDE DHCPv6 client on %i
+Documentation=man:dhcp6c(8) man:dhcp6c.conf(5)
+ConditionPathExists=/run/dhcp6c/dhcp6c.%i.conf
+After=vyos-router.service
+StartLimitIntervalSec=0
+
+[Service]
+WorkingDirectory=/run/dhcp6c
+Type=forking
+PIDFile=/run/dhcp6c/dhcp6c.%i.pid
+ExecStart=/usr/sbin/dhcp6c -D -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i
+Restart=on-failure
+RestartSec=20
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/dropbear@.service b/src/systemd/dropbear@.service
new file mode 100644
index 000000000..606a7ea6d
--- /dev/null
+++ b/src/systemd/dropbear@.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Dropbear SSH per-connection server
+Requires=dropbearkey.service
+Wants=conserver-server.service
+ConditionPathExists=/run/conserver/conserver.cf
+After=dropbearkey.service vyos-router.service conserver-server.service
+
+[Service]
+Type=forking
+ExecStartPre=/usr/bin/bash -c '/usr/bin/systemctl set-environment PORT=$(cli-shell-api returnActiveValue service console-server device "%I" ssh port)'
+ExecStart=-/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -c "/usr/bin/console %I" -P /run/conserver/dropbear.%I.pid -p ${PORT}
+PIDFile=/run/conserver/dropbear.%I.pid
+KillMode=process
+Restart=on-failure
diff --git a/src/systemd/dropbearkey.service b/src/systemd/dropbearkey.service
new file mode 100644
index 000000000..770641c8b
--- /dev/null
+++ b/src/systemd/dropbearkey.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Dropbear SSH Key Generation
+ConditionPathExists=|!/etc/dropbear/dropbear_rsa_host_key
+
+[Service]
+ExecStart=/usr/bin/dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/src/systemd/isc-dhcp-relay.service b/src/systemd/isc-dhcp-relay.service
new file mode 100644
index 000000000..56bcec840
--- /dev/null
+++ b/src/systemd/isc-dhcp-relay.service
@@ -0,0 +1,20 @@
+[Unit]
+Description=ISC DHCP IPv4 relay
+Documentation=man:dhcrelay(8)
+Wants=network-online.target
+RequiresMountsFor=/run
+ConditionPathExists=/run/dhcp-relay/dhcp.conf
+After=vyos-router.service
+
+[Service]
+Type=forking
+WorkingDirectory=/run/dhcp-relay
+RuntimeDirectory=dhcp-relay
+RuntimeDirectoryPreserve=yes
+EnvironmentFile=/run/dhcp-relay/dhcp.conf
+PIDFile=/run/dhcp-relay/dhcrelay.pid
+ExecStart=/usr/sbin/dhcrelay -4 -pf /run/dhcp-relay/dhcrelay.pid $OPTIONS
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-relay6.service b/src/systemd/isc-dhcp-relay6.service
new file mode 100644
index 000000000..85ff16e41
--- /dev/null
+++ b/src/systemd/isc-dhcp-relay6.service
@@ -0,0 +1,20 @@
+[Unit]
+Description=ISC DHCP IPv6 relay
+Documentation=man:dhcrelay(8)
+Wants=network-online.target
+RequiresMountsFor=/run
+ConditionPathExists=/run/dhcp-relay/dhcpv6.conf
+After=vyos-router.service
+
+[Service]
+Type=forking
+WorkingDirectory=/run/dhcp-relay
+RuntimeDirectory=dhcp-relay
+RuntimeDirectoryPreserve=yes
+EnvironmentFile=/run/dhcp-relay/dhcpv6.conf
+PIDFile=/run/dhcp-relay/dhcrelayv6.pid
+ExecStart=/usr/sbin/dhcrelay -6 -pf /run/dhcp-relay/dhcrelayv6.pid $OPTIONS
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service
new file mode 100644
index 000000000..9aa70a7cc
--- /dev/null
+++ b/src/systemd/isc-dhcp-server.service
@@ -0,0 +1,24 @@
+[Unit]
+Description=ISC DHCP IPv4 server
+Documentation=man:dhcpd(8)
+RequiresMountsFor=/run
+ConditionPathExists=/run/dhcp-server/dhcpd.conf
+After=vyos-router.service
+
+[Service]
+Type=forking
+WorkingDirectory=/run/dhcp-server
+RuntimeDirectory=dhcp-server
+RuntimeDirectoryPreserve=yes
+Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhcpd.conf LEASE_FILE=/config/dhcpd.leases
+PIDFile=/run/dhcp-server/dhcpd.pid
+ExecStartPre=/bin/sh -ec '\
+touch ${LEASE_FILE}; \
+chown dhcpd:nogroup ${LEASE_FILE}* ; \
+chmod 664 ${LEASE_FILE}* ; \
+/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
+ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service
new file mode 100644
index 000000000..1345c5fc5
--- /dev/null
+++ b/src/systemd/isc-dhcp-server6.service
@@ -0,0 +1,24 @@
+[Unit]
+Description=ISC DHCP IPv6 server
+Documentation=man:dhcpd(8)
+RequiresMountsFor=/run
+ConditionPathExists=/run/dhcp-server/dhcpdv6.conf
+After=vyos-router.service
+
+[Service]
+Type=forking
+WorkingDirectory=/run/dhcp-server
+RuntimeDirectory=dhcp-server
+RuntimeDirectoryPreserve=yes
+Environment=PID_FILE=/run/dhcp-server/dhcpdv6.pid CONFIG_FILE=/run/dhcp-server/dhcpdv6.conf LEASE_FILE=/config/dhcpdv6.leases
+PIDFile=/run/dhcp-server/dhcpdv6.pid
+ExecStartPre=/bin/sh -ec '\
+touch ${LEASE_FILE}; \
+chown nobody:nogroup ${LEASE_FILE}* ; \
+chmod 664 ${LEASE_FILE}* ; \
+/usr/sbin/dhcpd -6 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
+ExecStart=/usr/sbin/dhcpd -6 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/lcdproc.service b/src/systemd/lcdproc.service
new file mode 100644
index 000000000..ef717667a
--- /dev/null
+++ b/src/systemd/lcdproc.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=LCDproc system status information viewer on %I
+Documentation=man:lcdproc(8) http://www.lcdproc.org/
+After=vyos-router.service LCDd.service
+Requires=LCDd.service
+
+[Service]
+User=root
+ExecStart=/usr/bin/lcdproc -f -c /run/lcdproc/lcdproc.conf
+PIDFile=/run/lcdproc/lcdproc.pid
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/ppp@.service b/src/systemd/ppp@.service
new file mode 100644
index 000000000..bb4622034
--- /dev/null
+++ b/src/systemd/ppp@.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Dialing PPP connection %I
+After=vyos-router.service
+
+[Service]
+ExecStart=/usr/sbin/pppd call %I nodetach nolog
+Restart=on-failure
+RestartSec=5s
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/tftpd@.service b/src/systemd/tftpd@.service
new file mode 100644
index 000000000..266bc0962
--- /dev/null
+++ b/src/systemd/tftpd@.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=TFTP server
+After=vyos-router.service
+RequiresMountsFor=/run
+
+[Service]
+Type=forking
+#NotifyAccess=main
+EnvironmentFile=-/etc/default/tftpd%I
+ExecStart=/usr/sbin/in.tftpd "$DAEMON_ARGS"
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/vyos-beep.service b/src/systemd/vyos-beep.service
new file mode 100644
index 000000000..78baa544c
--- /dev/null
+++ b/src/systemd/vyos-beep.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Beep after system start
+DefaultDependencies=no
+After=vyos.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/beep -f 130 -l 100 -n -f 262 -l 100 -n -f 330 -l 100 -n -f 392 -l 100 -n -f 523 -l 100 -n -f 660 -l 100 -n -f 784 -l 300 -n -f 660 -l 300
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service
new file mode 100644
index 000000000..b77335778
--- /dev/null
+++ b/src/systemd/vyos-hostsd.service
@@ -0,0 +1,34 @@
+[Unit]
+Description=VyOS DNS configuration keeper
+
+# Without this option, lots of default dependencies are added,
+# among them network.target, which creates a dependency cycle
+DefaultDependencies=no
+
+# Seemingly sensible way to say "as early as the system is ready"
+# All vyos-hostsd needs is read/write mounted root
+After=systemd-remount-fs.service
+
+[Service]
+WorkingDirectory=/run/vyos-hostsd
+RuntimeDirectory=vyos-hostsd
+RuntimeDirectoryPreserve=yes
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd
+Type=idle
+KillMode=process
+
+SyslogIdentifier=vyos-hostsd
+SyslogFacility=daemon
+
+Restart=on-failure
+
+# Does't work in Jessie but leave it here
+User=root
+Group=hostsd
+
+[Install]
+
+# Note: After= doesn't actually create a dependency,
+# it just sets order for the case when both services are to start,
+# and without RequiredBy it *does not* set vyos-hostsd to start.
+RequiredBy=cloud-init-local.service vyos-router.service
diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service
new file mode 100644
index 000000000..4fa68b4ff
--- /dev/null
+++ b/src/systemd/vyos-http-api.service
@@ -0,0 +1,24 @@
+[Unit]
+Description=VyOS HTTP API service
+After=auditd.service systemd-user-sessions.service time-sync.target vyos-router.service
+Requires=vyos-router.service
+
+[Service]
+ExecStartPre=/usr/libexec/vyos/init/vyos-config
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-http-api-server
+Type=idle
+KillMode=process
+
+SyslogIdentifier=vyos-http-api
+SyslogFacility=daemon
+
+Restart=on-failure
+
+# Does't work but leave it here
+User=root
+Group=vyattacfg
+
+[Install]
+# Installing in a earlier target leaves ExecStartPre waiting
+WantedBy=getty.target
+
diff --git a/src/systemd/wpa_supplicant-macsec@.service b/src/systemd/wpa_supplicant-macsec@.service
new file mode 100644
index 000000000..7e0bee8e1
--- /dev/null
+++ b/src/systemd/wpa_supplicant-macsec@.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=WPA supplicant daemon (macsec-specific version)
+Requires=sys-subsystem-net-devices-%i.device
+ConditionPathExists=/run/wpa_supplicant/%I.conf
+After=vyos-router.service
+RequiresMountsFor=/run
+
+# NetworkManager users will probably want the dbus version instead.
+
+[Service]
+Type=simple
+WorkingDirectory=/run/wpa_supplicant
+PIDFile=/run/wpa_supplicant/%I.pid
+ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -i%I
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/tests/helper.py b/src/tests/helper.py
new file mode 100644
index 000000000..a7e4f201c
--- /dev/null
+++ b/src/tests/helper.py
@@ -0,0 +1,27 @@
+#!/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 importlib.util
+
+
+def prepare_module(file_path='', module_name=''):
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ sys.modules[module_name] = module
diff --git a/src/tests/test_config_parser.py b/src/tests/test_config_parser.py
new file mode 100644
index 000000000..e47770a7f
--- /dev/null
+++ b/src/tests/test_config_parser.py
@@ -0,0 +1,61 @@
+#!/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 tempfile
+import unittest
+from unittest import TestCase, mock
+
+import vyos.configtree
+
+
+class TestConfigParser(TestCase):
+ def setUp(self):
+ with open('tests/data/config.valid', 'r') as f:
+ config_string = f.read()
+ self.config = vyos.configtree.ConfigTree(config_string)
+
+ def test_top_level_valueless(self):
+ self.assertTrue(self.config.exists(["top-level-valueless-node"]))
+
+ def test_top_level_leaf(self):
+ self.assertTrue(self.config.exists(["top-level-leaf-node"]))
+ self.assertEqual(self.config.return_value(["top-level-leaf-node"]), "foo")
+
+ def test_top_level_tag(self):
+ self.assertTrue(self.config.exists(["top-level-tag-node"]))
+ # No sorting is intentional, child order must be preserved
+ self.assertEqual(self.config.list_nodes(["top-level-tag-node"]), ["foo", "bar"])
+
+ def test_copy(self):
+ self.config.copy(["top-level-tag-node", "bar"], ["top-level-tag-node", "baz"])
+ print(self.config.to_string())
+ self.assertTrue(self.config.exists(["top-level-tag-node", "baz"]))
+
+ def test_copy_duplicate(self):
+ with self.assertRaises(vyos.configtree.ConfigTreeError):
+ self.config.copy(["top-level-tag-node", "foo"], ["top-level-tag-node", "bar"])
+
+ def test_rename(self):
+ self.config.rename(["top-level-tag-node", "bar"], "quux")
+ print(self.config.to_string())
+ self.assertTrue(self.config.exists(["top-level-tag-node", "quux"]))
+
+ def test_rename_duplicate(self):
+ with self.assertRaises(vyos.configtree.ConfigTreeError):
+ self.config.rename(["top-level-tag-node", "foo"], "bar")
diff --git a/src/tests/test_initial_setup.py b/src/tests/test_initial_setup.py
new file mode 100644
index 000000000..1597025e8
--- /dev/null
+++ b/src/tests/test_initial_setup.py
@@ -0,0 +1,105 @@
+#!/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 tempfile
+import unittest
+from unittest import TestCase, mock
+
+from vyos import xml
+import vyos.configtree
+import vyos.initialsetup as vis
+
+
+class TestInitialSetup(TestCase):
+ def setUp(self):
+ with open('tests/data/config.boot.default', 'r') as f:
+ config_string = f.read()
+ self.config = vyos.configtree.ConfigTree(config_string)
+ self.xml = xml.load_configuration()
+
+ def test_set_user_password(self):
+ vis.set_user_password(self.config, 'vyos', 'vyosvyos')
+
+ # Old password hash from the default config
+ old_pw = '$6$QxPS.uk6mfo$9QBSo8u1FkH16gMyAVhus6fU3LOzvLR9Z9.82m3tiHFAxTtIkhaZSWssSgzt4v4dGAL8rhVQxTg0oAG9/q11h/'
+ new_pw = self.config.return_value(["system", "login", "user", "vyos", "authentication", "encrypted-password"])
+
+ # Just check it changed the hash, don't try to check if hash is good
+ self.assertNotEqual(old_pw, new_pw)
+
+ def test_disable_user_password(self):
+ vis.disable_user_password(self.config, 'vyos')
+ new_pw = self.config.return_value(["system", "login", "user", "vyos", "authentication", "encrypted-password"])
+
+ self.assertEqual(new_pw, '!')
+
+ def test_set_ssh_key_with_name(self):
+ test_ssh_key = " ssh-rsa fakedata vyos@vyos "
+ vis.set_user_ssh_key(self.config, 'vyos', test_ssh_key)
+
+ key_type = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos@vyos", "type"])
+ key_data = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos@vyos", "key"])
+
+ self.assertEqual(key_type, 'ssh-rsa')
+ self.assertEqual(key_data, 'fakedata')
+ self.assertTrue(self.xml.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"]))
+
+ def test_set_ssh_key_without_name(self):
+ # If key file doesn't include a name, the function will use user name for the key name
+
+ test_ssh_key = " ssh-rsa fakedata "
+ vis.set_user_ssh_key(self.config, 'vyos', test_ssh_key)
+
+ key_type = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos", "type"])
+ key_data = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos", "key"])
+
+ self.assertEqual(key_type, 'ssh-rsa')
+ self.assertEqual(key_data, 'fakedata')
+ self.assertTrue(self.xml.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"]))
+
+ def test_create_user(self):
+ vis.create_user(self.config, 'jrandomhacker', password='qwerty', key=" ssh-rsa fakedata jrandomhacker@foovax ")
+
+ self.assertTrue(self.config.exists(["system", "login", "user", "jrandomhacker"]))
+ self.assertTrue(self.config.exists(["system", "login", "user", "jrandomhacker", "authentication", "public-keys", "jrandomhacker@foovax"]))
+ self.assertTrue(self.config.exists(["system", "login", "user", "jrandomhacker", "authentication", "encrypted-password"]))
+ self.assertEqual(self.config.return_value(["system", "login", "user", "jrandomhacker", "level"]), "admin")
+
+ def test_set_hostname(self):
+ vis.set_host_name(self.config, "vyos-test")
+
+ self.assertEqual(self.config.return_value(["system", "host-name"]), "vyos-test")
+
+ def test_set_name_servers(self):
+ vis.set_name_servers(self.config, ["192.0.2.10", "203.0.113.20"])
+ servers = self.config.return_values(["system", "name-server"])
+
+ self.assertIn("192.0.2.10", servers)
+ self.assertIn("203.0.113.20", servers)
+
+ def test_set_gateway(self):
+ vis.set_default_gateway(self.config, '192.0.2.1')
+
+ self.assertTrue(self.config.exists(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '192.0.2.1']))
+ self.assertTrue(self.xml.is_tag(['protocols', 'static', 'multicast', 'route', '0.0.0.0/0', 'next-hop']))
+ self.assertTrue(self.xml.is_tag(['protocols', 'static', 'multicast', 'route']))
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/src/tests/test_task_scheduler.py b/src/tests/test_task_scheduler.py
new file mode 100644
index 000000000..084bd868c
--- /dev/null
+++ b/src/tests/test_task_scheduler.py
@@ -0,0 +1,130 @@
+#!/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 tempfile
+import unittest
+
+from vyos import ConfigError
+try:
+ from src.conf_mode import task_scheduler
+except ModuleNotFoundError: # for unittest.main()
+ import sys
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+ from src.conf_mode import task_scheduler
+
+
+class TestUpdateCrontab(unittest.TestCase):
+
+ def test_verify(self):
+ tests = [
+ {'name': 'one_task',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': None
+ },
+ {'name': 'has_interval_and_spec',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '0 * * * *', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'has_no_interval_and_spec',
+ 'tasks': [{'name': 'aaa', 'interval': '', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval',
+ 'tasks': [{'name': 'aaa', 'interval': '1y', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval_min',
+ 'tasks': [{'name': 'aaa', 'interval': '61m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval_hour',
+ 'tasks': [{'name': 'aaa', 'interval': '25h', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval_day',
+ 'tasks': [{'name': 'aaa', 'interval': '32d', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'no_executable',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '', 'args': ''}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_executable',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/aaa', 'args': ''}],
+ 'expected': ConfigError
+ }
+ ]
+ for t in tests:
+ with self.subTest(msg=t['name'], tasks=t['tasks'], expected=t['expected']):
+ if t['expected'] is not None:
+ with self.assertRaises(t['expected']):
+ task_scheduler.verify(t['tasks'])
+ else:
+ task_scheduler.verify(t['tasks'])
+
+ def test_generate(self):
+ tests = [
+ {'name': 'zero_task',
+ 'tasks': [],
+ 'expected': []
+ },
+ {'name': 'one_task',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '*/60 * * * * root sg vyattacfg \"/bin/ls -l\"']
+ },
+ {'name': 'one_task_with_hour',
+ 'tasks': [{'name': 'aaa', 'interval': '10h', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '0 */10 * * * root sg vyattacfg \"/bin/ls -l\"']
+ },
+ {'name': 'one_task_with_day',
+ 'tasks': [{'name': 'aaa', 'interval': '10d', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '0 0 */10 * * root sg vyattacfg \"/bin/ls -l\"']
+ },
+ {'name': 'multiple_tasks',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'},
+ {'name': 'bbb', 'interval': '', 'spec': '0 0 * * *', 'executable': '/bin/ls', 'args': '-ltr'}
+ ],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '*/60 * * * * root sg vyattacfg \"/bin/ls -l\"',
+ '0 0 * * * root sg vyattacfg \"/bin/ls -ltr\"']
+ }
+ ]
+ for t in tests:
+ with self.subTest(msg=t['name'], tasks=t['tasks'], expected=t['expected']):
+ task_scheduler.crontab_file = tempfile.mkstemp()[1]
+ task_scheduler.generate(t['tasks'])
+ if len(t['expected']) > 0:
+ self.assertTrue(os.path.isfile(task_scheduler.crontab_file))
+ with open(task_scheduler.crontab_file) as f:
+ actual = f.read()
+ self.assertEqual(t['expected'], actual.splitlines())
+ os.remove(task_scheduler.crontab_file)
+ else:
+ self.assertFalse(os.path.isfile(task_scheduler.crontab_file))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/tests/test_util.py b/src/tests/test_util.py
new file mode 100644
index 000000000..0e56a67a8
--- /dev/null
+++ b/src/tests/test_util.py
@@ -0,0 +1,34 @@
+#!/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 unittest
+from unittest import TestCase
+
+import vyos.util
+
+
+class TestVyOSUtil(TestCase):
+ def setUp(self):
+ pass
+
+ def test_key_mangline(self):
+ data = {"foo-bar": {"baz-quux": None}}
+ expected_data = {"foo_bar": {"baz_quux": None}}
+ new_data = vyos.util.mangle_dict_keys(data, '-', '_')
+ self.assertEqual(new_data, expected_data)
+
diff --git a/src/utils/initial-setup b/src/utils/initial-setup
new file mode 100644
index 000000000..37fc45751
--- /dev/null
+++ b/src/utils/initial-setup
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+import argparse
+
+import vyos.configtree
+
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument("--ssh", help="Enable SSH", action="store_true")
+parser.add_argument("--ssh-port", help="SSH port", type=int, action="store", default=22)
+
+parser.add_argument("--intf-address", help="Set interface address", type=str, action="append")
+
+parser.add_argument("config_file", help="Configuration file to modify", type=str)
+
+args = parser.parse_args()
+
+# Load the config file
+with open(args.config_file, 'r') as f:
+ config_file = f.read()
+
+config = vyos.configtree.ConfigTree(config_file)
+
+
+# Interface names and addresses are comma-separated,
+# we need to split them
+intf_addrs = list(map(lambda s: s.split(","), args.intf_address))
+
+# Enable SSH, if requested
+if args.ssh:
+ config.set(["service", "ssh", "port"], value=str(args.ssh_port))
+
+# Assign addresses to interfaces
+if intf_addrs:
+ for a in intf_addrs:
+ config.set(["interfaces", "ethernet", a[0], "address"], value=a[1])
+ config.set_tag(["interfaces", "ethernet"])
+
+print( config.to_string() )
diff --git a/src/utils/vyos-config-file-query b/src/utils/vyos-config-file-query
new file mode 100755
index 000000000..a10c7e9b3
--- /dev/null
+++ b/src/utils/vyos-config-file-query
@@ -0,0 +1,100 @@
+#!/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 re
+import os
+import sys
+import json
+import argparse
+
+import vyos.configtree
+
+
+arg_parser = argparse.ArgumentParser()
+arg_parser.add_argument('-p', '--path', type=str,
+ help="VyOS config node, e.g. \"system config-management commit-revisions\"", required=True)
+arg_parser.add_argument('-f', '--file', type=str, help="VyOS config file, e.g. /config/config.boot", required=True)
+
+arg_parser.add_argument('-s', '--separator', type=str, default=' ', help="Value separator for the plain format")
+arg_parser.add_argument('-j', '--json', action='store_true')
+
+op_group = arg_parser.add_mutually_exclusive_group(required=True)
+op_group.add_argument('--return-value', action='store_true', help="Return a single node value")
+op_group.add_argument('--return-values', action='store_true', help="Return all values of a multi-value node")
+op_group.add_argument('--list-nodes', action='store_true', help="List children of a node")
+op_group.add_argument('--exists', action='store_true', help="Check if a node exists")
+
+args = arg_parser.parse_args()
+
+
+try:
+ with open(args.file, 'r') as f:
+ config_file = f.read()
+except OSError as e:
+ print("Could not read the config file: {0}".format(e))
+ sys.exit(1)
+
+try:
+ config = vyos.configtree.ConfigTree(config_file)
+except Exception as e:
+ print(e)
+ sys.exit(1)
+
+
+path = re.split(r'\s+', args.path)
+values = None
+
+if args.exists:
+ if config.exists(path):
+ sys.exit(0)
+ else:
+ sys.exit(1)
+elif args.return_value:
+ try:
+ values = [config.return_value(path)]
+ except vyos.configtree.ConfigTreeError as e:
+ print(e)
+ sys.exit(1)
+elif args.return_values:
+ try:
+ values = config.return_values(path)
+ except vyos.configtree.ConfigTreeError as e:
+ print(e)
+ sys.exit(1)
+elif args.list_nodes:
+ values = config.list_nodes(path)
+ if not values:
+ values = []
+else:
+ # Can't happen
+ print("Operation required")
+ sys.exit(1)
+
+
+if values:
+ if args.json:
+ print(json.dumps(values))
+ else:
+ if len(values) == 1:
+ print(values[0])
+ else:
+ # XXX: assuming values never contain quotes
+ values = list(map(lambda s: "\'{0}\'".format(s), values))
+ values_str = args.separator.join(values)
+ print(values_str)
+
+sys.exit(0)
diff --git a/src/utils/vyos-config-to-commands b/src/utils/vyos-config-to-commands
new file mode 100755
index 000000000..8b50f7c5d
--- /dev/null
+++ b/src/utils/vyos-config-to-commands
@@ -0,0 +1,29 @@
+#!/usr/bin/python3
+
+import sys
+
+from signal import signal, SIGPIPE, SIG_DFL
+from vyos.configtree import ConfigTree
+
+signal(SIGPIPE,SIG_DFL)
+
+config_string = None
+if (len(sys.argv) == 1):
+ # If no argument given, act as a pipe
+ config_string = sys.stdin.read()
+else:
+ file_name = sys.argv[1]
+ try:
+ with open(file_name, 'r') as f:
+ config_string = f.read()
+ except OSError as e:
+ print("Could not read config file {0}: {1}".format(file_name, e), file=sys.stderr)
+
+try:
+ config = ConfigTree(config_string)
+ commands = config.to_commands()
+except ValueError as e:
+ print("Could not parse the config file: {0}".format(e), file=sys.stderr)
+ sys.exit(1)
+
+print(commands)
diff --git a/src/utils/vyos-config-to-json b/src/utils/vyos-config-to-json
new file mode 100755
index 000000000..e03fd6a59
--- /dev/null
+++ b/src/utils/vyos-config-to-json
@@ -0,0 +1,40 @@
+#!/usr/bin/python3
+
+import sys
+import json
+
+from signal import signal, SIGPIPE, SIG_DFL
+from vyos.configtree import ConfigTree
+
+signal(SIGPIPE,SIG_DFL)
+
+config_string = None
+if (len(sys.argv) == 1):
+ # If no argument given, act as a pipe
+ config_string = sys.stdin.read()
+else:
+ file_name = sys.argv[1]
+ try:
+ with open(file_name, 'r') as f:
+ config_string = f.read()
+ except OSError as e:
+ print("Could not read config file {0}: {1}".format(file_name, e), file=sys.stderr)
+
+# This script is usually called with the output of "cli-shell-api showCfg", which does not
+# escape backslashes. "ConfigTree()" expects escaped backslashes when parsing a config
+# string (and also prints them itself). Therefore this script would fail.
+# Manually escape backslashes here to handle backslashes in any configuration strings
+# properly. The alternative would be to modify the output of "cli-shell-api showCfg",
+# but that may be break other things who rely on that specific output.
+config_string = config_string.replace("\\", "\\\\")
+
+try:
+ config = ConfigTree(config_string)
+ json_str = config.to_json()
+ # Pretty print
+ json_str = json.dumps(json.loads(json_str), indent=4, sort_keys=True)
+except ValueError as e:
+ print("Could not parse the config file: {0}".format(e), file=sys.stderr)
+ sys.exit(1)
+
+print(json_str)
diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client
new file mode 100755
index 000000000..48ebc83f7
--- /dev/null
+++ b/src/utils/vyos-hostsd-client
@@ -0,0 +1,165 @@
+#!/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
+import argparse
+
+import vyos.hostsd_client
+
+parser = argparse.ArgumentParser(allow_abbrev=False)
+group = parser.add_mutually_exclusive_group()
+
+group.add_argument('--add-name-servers', type=str, nargs='*')
+group.add_argument('--delete-name-servers', action='store_true')
+group.add_argument('--get-name-servers', type=str, const='.*', nargs='?')
+
+group.add_argument('--add-name-server-tags-recursor', type=str, nargs='*')
+group.add_argument('--delete-name-server-tags-recursor', type=str, nargs='*')
+group.add_argument('--get-name-server-tags-recursor', action='store_true')
+
+group.add_argument('--add-name-server-tags-system', type=str, nargs='*')
+group.add_argument('--delete-name-server-tags-system', type=str, nargs='*')
+group.add_argument('--get-name-server-tags-system', action='store_true')
+
+group.add_argument('--add-forward-zone', type=str, nargs='?')
+group.add_argument('--delete-forward-zones', type=str, nargs='*')
+group.add_argument('--get-forward-zones', action='store_true')
+
+group.add_argument('--add-search-domains', type=str, nargs='*')
+group.add_argument('--delete-search-domains', action='store_true')
+group.add_argument('--get-search-domains', type=str, const='.*', nargs='?')
+
+group.add_argument('--add-hosts', type=str, nargs='*')
+group.add_argument('--delete-hosts', action='store_true')
+group.add_argument('--get-hosts', type=str, const='.*', nargs='?')
+
+group.add_argument('--set-host-name', type=str)
+
+# for --set-host-name
+parser.add_argument('--domain-name', type=str)
+
+# for forward zones
+parser.add_argument('--nameservers', type=str, nargs='*')
+parser.add_argument('--addnta', action='store_true')
+parser.add_argument('--recursion-desired', action='store_true')
+
+parser.add_argument('--tag', type=str)
+
+# users must call --apply either in the same command or after they're done
+parser.add_argument('--apply', action="store_true")
+
+args = parser.parse_args()
+
+try:
+ client = vyos.hostsd_client.Client()
+ ops = 1
+
+ if args.add_name_servers:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ client.add_name_servers({args.tag: args.add_name_servers})
+ elif args.delete_name_servers:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ client.delete_name_servers([args.tag])
+ elif args.get_name_servers:
+ print(client.get_name_servers(args.get_name_servers))
+
+ elif args.add_name_server_tags_recursor:
+ client.add_name_server_tags_recursor(args.add_name_server_tags_recursor)
+ elif args.delete_name_server_tags_recursor:
+ client.delete_name_server_tags_recursor(args.delete_name_server_tags_recursor)
+ elif args.get_name_server_tags_recursor:
+ print(client.get_name_server_tags_recursor())
+
+ elif args.add_name_server_tags_system:
+ client.add_name_server_tags_system(args.add_name_server_tags_system)
+ elif args.delete_name_server_tags_system:
+ client.delete_name_server_tags_system(args.delete_name_server_tags_system)
+ elif args.get_name_server_tags_system:
+ print(client.get_name_server_tags_system())
+
+ elif args.add_forward_zone:
+ if not args.nameservers:
+ raise ValueError("--nameservers is required for this operation")
+ client.add_forward_zones(
+ { args.add_forward_zone: {
+ 'nslist': args.nameservers,
+ 'addNTA': args.addnta,
+ 'recursion-desired': args.recursion_desired
+ }
+ })
+ elif args.delete_forward_zones:
+ client.delete_forward_zones(args.delete_forward_zones)
+ elif args.get_forward_zones:
+ print(client.get_forward_zones())
+
+ elif args.add_search_domains:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ client.add_search_domains({args.tag: args.add_search_domains})
+ elif args.delete_search_domains:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ client.delete_search_domains([args.tag])
+ elif args.get_search_domains:
+ print(client.get_search_domains(args.get_search_domains))
+
+ elif args.add_hosts:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ data = {}
+ for h in args.add_hosts:
+ entry = {}
+ params = h.split(",")
+ if len(params) < 2:
+ raise ValueError("Malformed host entry")
+ entry['address'] = params[1]
+ entry['aliases'] = params[2:]
+ data[params[0]] = entry
+ client.add_hosts({args.tag: data})
+ elif args.delete_hosts:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ client.delete_hosts([args.tag])
+ elif args.get_hosts:
+ print(client.get_hosts(args.get_hosts))
+
+ elif args.set_host_name:
+ if not args.domain_name:
+ raise ValueError('--domain-name is required for this operation')
+ client.set_host_name({'host_name': args.set_host_name, 'domain_name': args.domain_name})
+
+ elif args.apply:
+ pass
+ else:
+ ops = 0
+
+ if args.apply:
+ client.apply()
+
+ if ops == 0:
+ raise ValueError("Operation required")
+
+except ValueError as e:
+ print("Incorrect options: {0}".format(e))
+ sys.exit(1)
+except vyos.hostsd_client.VyOSHostsdError as e:
+ print("Server returned an error: {0}".format(e))
+ sys.exit(1)
+
diff --git a/src/validators/dotted-decimal b/src/validators/dotted-decimal
new file mode 100755
index 000000000..652110346
--- /dev/null
+++ b/src/validators/dotted-decimal
@@ -0,0 +1,33 @@
+#!/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
+import sys
+
+area = sys.argv[1]
+
+res = re.match(r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$', area)
+if not res:
+ print("\'{0}\' is not a valid dotted decimal value".format(area))
+ sys.exit(1)
+else:
+ components = res.groups()
+ for n in range(0, 4):
+ if (int(components[n]) > 255):
+ print("Invalid component of a dotted decimal value: {0} exceeds 255".format(components[n]))
+ sys.exit(1)
+
+sys.exit(0)
diff --git a/src/validators/file-exists b/src/validators/file-exists
new file mode 100755
index 000000000..5cef6b199
--- /dev/null
+++ b/src/validators/file-exists
@@ -0,0 +1,61 @@
+#!/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/>.
+#
+# Description:
+# Check if a given file exists on the system. Used for files that
+# are referenced from the CLI and need to be preserved during an image upgrade.
+# Warn the user if these aren't under /config
+
+import os
+import sys
+import argparse
+
+
+def exit(strict, message):
+ if strict:
+ sys.exit(f'ERROR: {message}')
+ print(f'WARNING: {message}', file=sys.stderr)
+ sys.exit()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-d", "--directory", type=str, help="File must be present in this directory.")
+ parser.add_argument("-e", "--error", action="store_true", help="Tread warnings as errors - change exit code to '1'")
+ parser.add_argument("file", type=str, help="Path of file to validate")
+
+ args = parser.parse_args()
+
+ #
+ # Always check if the given file exists
+ #
+ if not os.path.exists(args.file):
+ exit(args.error, f"File '{args.file}' not found")
+
+ #
+ # Optional check if the file is under a certain directory path
+ #
+ if args.directory:
+ # remove directory path from path to verify
+ rel_filename = args.file.replace(args.directory, '').lstrip('/')
+
+ if not os.path.exists(args.directory + '/' + rel_filename):
+ exit(args.error,
+ f"'{args.file}' lies outside of '{args.directory}' directory.\n"
+ "It will not get preserved during image upgrade!"
+ )
+
+ sys.exit()
diff --git a/src/validators/fqdn b/src/validators/fqdn
new file mode 100755
index 000000000..347ffda42
--- /dev/null
+++ b/src/validators/fqdn
@@ -0,0 +1,30 @@
+#!/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
+import sys
+
+
+# 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 __name__ == '__main__':
+ if len(sys.argv) != 2:
+ sys.exit(1)
+ if not re.match(pattern, sys.argv[1]):
+ sys.exit(1)
+ sys.exit(0)
diff --git a/src/validators/interface-address b/src/validators/interface-address
new file mode 100755
index 000000000..4c203956b
--- /dev/null
+++ b/src/validators/interface-address
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4-host $1 || ipaddrcheck --is-ipv6-host $1
diff --git a/src/validators/ip-address b/src/validators/ip-address
new file mode 100755
index 000000000..51fb72c85
--- /dev/null
+++ b/src/validators/ip-address
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-any-single $1
diff --git a/src/validators/ip-cidr b/src/validators/ip-cidr
new file mode 100755
index 000000000..987bf84ca
--- /dev/null
+++ b/src/validators/ip-cidr
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-any-cidr $1
diff --git a/src/validators/ip-host b/src/validators/ip-host
new file mode 100755
index 000000000..f2906e8cf
--- /dev/null
+++ b/src/validators/ip-host
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-any-host $1
diff --git a/src/validators/ip-prefix b/src/validators/ip-prefix
new file mode 100755
index 000000000..e58aad395
--- /dev/null
+++ b/src/validators/ip-prefix
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-any-net $1
diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol
new file mode 100755
index 000000000..078f8e319
--- /dev/null
+++ b/src/validators/ip-protocol
@@ -0,0 +1,41 @@
+#!/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 argv,exit
+
+if __name__ == '__main__':
+ if len(argv) != 2:
+ exit(1)
+
+ input = argv[1]
+ try:
+ # IP protocol can be in the range 0 - 255, thus the range must end with 256
+ if int(input) in range(0, 256):
+ exit(0)
+ except ValueError:
+ pass
+
+ pattern = "!?\\b(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|" \
+ "tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|" \
+ "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|" \
+ "ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|" \
+ "encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|" \
+ "udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc)\\b"
+ if re.match(pattern, input):
+ exit(0)
+
+ exit(1)
diff --git a/src/validators/ipv4-address b/src/validators/ipv4-address
new file mode 100755
index 000000000..872a7645a
--- /dev/null
+++ b/src/validators/ipv4-address
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4-single $1
diff --git a/src/validators/ipv4-address-exclude b/src/validators/ipv4-address-exclude
new file mode 100755
index 000000000..80ad17d45
--- /dev/null
+++ b/src/validators/ipv4-address-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-address "${arg:1}"
diff --git a/src/validators/ipv4-host b/src/validators/ipv4-host
new file mode 100755
index 000000000..f42feffa4
--- /dev/null
+++ b/src/validators/ipv4-host
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4-host $1
diff --git a/src/validators/ipv4-prefix b/src/validators/ipv4-prefix
new file mode 100755
index 000000000..8ec8a2c45
--- /dev/null
+++ b/src/validators/ipv4-prefix
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4-net $1
diff --git a/src/validators/ipv4-prefix-exclude b/src/validators/ipv4-prefix-exclude
new file mode 100755
index 000000000..4f7de400a
--- /dev/null
+++ b/src/validators/ipv4-prefix-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-prefix "${arg:1}"
diff --git a/src/validators/ipv4-range b/src/validators/ipv4-range
new file mode 100755
index 000000000..ae3f3f163
--- /dev/null
+++ b/src/validators/ipv4-range
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+# snippet from https://stackoverflow.com/questions/10768160/ip-address-converter
+ip2dec () {
+ local a b c d ip=$@
+ IFS=. read -r a b c d <<< "$ip"
+ printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))"
+}
+
+# Only run this if there is a hypen present in $1
+if [[ "$1" =~ "-" ]]; then
+ # This only works with real bash (<<<) - split IP addresses into array with
+ # hyphen as delimiter
+ readarray -d - -t strarr <<< $1
+
+ ipaddrcheck --is-ipv4-single ${strarr[0]}
+ if [ $? -gt 0 ]; then
+ exit 1
+ fi
+
+ ipaddrcheck --is-ipv4-single ${strarr[1]}
+ if [ $? -gt 0 ]; then
+ exit 1
+ fi
+
+ start=$(ip2dec ${strarr[0]})
+ stop=$(ip2dec ${strarr[1]})
+ if [ $start -ge $stop ]; then
+ exit 1
+ fi
+fi
+
+exit 0
diff --git a/src/validators/ipv4-range-exclude b/src/validators/ipv4-range-exclude
new file mode 100755
index 000000000..3787b4dec
--- /dev/null
+++ b/src/validators/ipv4-range-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-range "${arg:1}"
diff --git a/src/validators/ipv6 b/src/validators/ipv6
new file mode 100755
index 000000000..f18d4a63e
--- /dev/null
+++ b/src/validators/ipv6
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv6 $1
diff --git a/src/validators/ipv6-address b/src/validators/ipv6-address
new file mode 100755
index 000000000..e5d68d756
--- /dev/null
+++ b/src/validators/ipv6-address
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv6-single $1
diff --git a/src/validators/ipv6-host b/src/validators/ipv6-host
new file mode 100755
index 000000000..f7a745077
--- /dev/null
+++ b/src/validators/ipv6-host
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv6-host $1
diff --git a/src/validators/ipv6-prefix b/src/validators/ipv6-prefix
new file mode 100755
index 000000000..e43616350
--- /dev/null
+++ b/src/validators/ipv6-prefix
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv6-net $1
diff --git a/src/validators/mac-address b/src/validators/mac-address
new file mode 100755
index 000000000..b2d3496f4
--- /dev/null
+++ b/src/validators/mac-address
@@ -0,0 +1,29 @@
+#!/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 re
+import sys
+
+
+pattern = "^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$"
+
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ sys.exit(1)
+ if not re.match(pattern, sys.argv[1]):
+ sys.exit(1)
+ sys.exit(0)
diff --git a/src/validators/script b/src/validators/script
new file mode 100755
index 000000000..2665ec1f6
--- /dev/null
+++ b/src/validators/script
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+#
+# numeric value validator
+#
+# 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
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# 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 shlex
+
+import vyos.util
+
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ sys.exit('Please specify script file to check')
+
+ # if the "script" is a script+ stowaway arguments, this removes the aguements
+ script = shlex.split(sys.argv[1])[0]
+
+ if not os.path.exists(script):
+ sys.exit(f'File {script} does not exist')
+
+ if not (os.path.isfile(script) and os.access(script, os.X_OK)):
+ sys.exit('File {script} is not an executable file')
+
+ # File outside the config dir is just a warning
+ if not vyos.util.file_is_persistent(script):
+ sys.exit(
+ 'Warning: file {path} is outside the / config directory\n'
+ 'It will not be automatically migrated to a new image on system update'
+ )
diff --git a/src/validators/timezone b/src/validators/timezone
new file mode 100755
index 000000000..baf5abca2
--- /dev/null
+++ b/src/validators/timezone
@@ -0,0 +1,33 @@
+#!/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 argparse
+import sys
+
+from vyos.util import cmd
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--validate", action="store", required=True, help="Check if timezone is valid")
+ args = parser.parse_args()
+
+ 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 args.validate not in tz_data:
+ sys.exit("the timezone can't be found in the timezone list")
+ sys.exit()
diff --git a/src/validators/vrf-name b/src/validators/vrf-name
new file mode 100755
index 000000000..7b6313888
--- /dev/null
+++ b/src/validators/vrf-name
@@ -0,0 +1,41 @@
+#!/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 argv, exit
+
+if __name__ == '__main__':
+ if len(argv) != 2:
+ exit(1)
+
+ vrf = argv[1]
+ length = len(vrf)
+
+ if length not in range(1, 16):
+ 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 vrf == "lo":
+ exit(1)
+
+ 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 not re.match(pattern, vrf):
+ exit(1)
+
+ exit(0)
diff --git a/src/validators/wireless-phy b/src/validators/wireless-phy
new file mode 100755
index 000000000..513a902de
--- /dev/null
+++ b/src/validators/wireless-phy
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+# 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/>.
+
+if [ ! -d /sys/class/ieee80211 ]; then
+ echo No IEEE 802.11 physical interfaces detected
+ exit 1
+fi
+
+if [ ! -e /sys/class/ieee80211/$1 ]; then
+ echo Device interface "$1" does not exist
+ exit 1
+fi
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 000000000..9348520b5
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,5 @@
+python/
+lxml
+pylint
+nose
+coverage
diff --git a/tests/data/config.boot.default b/tests/data/config.boot.default
new file mode 100644
index 000000000..0a75716b8
--- /dev/null
+++ b/tests/data/config.boot.default
@@ -0,0 +1,40 @@
+system {
+ host-name vyos
+ login {
+ user vyos {
+ authentication {
+ encrypted-password $6$QxPS.uk6mfo$9QBSo8u1FkH16gMyAVhus6fU3LOzvLR9Z9.82m3tiHFAxTtIkhaZSWssSgzt4v4dGAL8rhVQxTg0oAG9/q11h/
+ plaintext-password ""
+ }
+ level admin
+ }
+ }
+ syslog {
+ global {
+ facility all {
+ level notice
+ }
+ facility protocols {
+ level debug
+ }
+ }
+ }
+ ntp {
+ server "0.pool.ntp.org"
+ server "1.pool.ntp.org"
+ server "2.pool.ntp.org"
+ }
+ console {
+ device ttyS0 {
+ speed 9600
+ }
+ }
+ config-management {
+ commit-revisions 100
+ }
+}
+
+interfaces {
+ loopback lo {
+ }
+}
diff --git a/tests/data/config.valid b/tests/data/config.valid
new file mode 100644
index 000000000..1fbdd1505
--- /dev/null
+++ b/tests/data/config.valid
@@ -0,0 +1,39 @@
+/* top level leaf node */
+top-level-leaf-node foo
+
+top-level-valueless-node
+
+top-level-tag-node foo {
+ top-level-tag-node-child some-value
+}
+
+top-level-tag-node bar {
+ top-level-tag-node-child another-value
+}
+
+normal-node {
+ normal-node-child {
+ valueless-node
+ multi-node value1
+ /* valueless node comment */
+ another-valueless-node
+ multi-node value1
+ tag-node foo {
+ }
+ one-more-valueless-node
+ tag-node bar {
+ some-option some-value
+ }
+ }
+ option-with-quoted-value "some-value"
+}
+
+trailing-leaf-node-option some-value
+
+empty-node {
+}
+
+trailing-leaf-node-without-value
+
+// Trailing comment
+// Another trailing comment
diff --git a/tests/data/interface-definitions/test-op.xml b/tests/data/interface-definitions/test-op.xml
new file mode 100644
index 000000000..50bd686ae
--- /dev/null
+++ b/tests/data/interface-definitions/test-op.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="foo">
+ <properties>
+ <help>foo</help>
+ </properties>
+ <children>
+ <leafNode name="bar">
+ <command>/usr/bin/bar</command>
+ <properties>
+ <help>bar</help>
+ <completionHelp>
+ <list>foo bar</list>
+ <path>interfaces tunnel</path>
+ <script>/usr/bin/foo</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/tests/data/interface-definitions/test.xml b/tests/data/interface-definitions/test.xml
new file mode 100644
index 000000000..fbb302e70
--- /dev/null
+++ b/tests/data/interface-definitions/test.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="foo">
+ <properties>
+ <help>foo</help>
+ </properties>
+ <children>
+ <leafNode name="bar">
+ <properties>
+ <help>bar</help>
+ <valueHelp>
+ <format>bar</format>
+ <description>bar</description>
+ </valueHelp>
+ <completionHelp>
+ <list>foo bar</list>
+ <path>interfaces tunnel</path>
+ <script>/usr/bin/foo</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>