diff options
81 files changed, 1440 insertions, 986 deletions
diff --git a/data/configd-include.json b/data/configd-include.json index 0c75657e0..2711a29b8 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -2,6 +2,7 @@ "bcast_relay.py", "dhcp_relay.py", "dhcpv6_relay.py", +"dns_forwarding.py", "dynamic_dns.py", "firewall_options.py", "host_name.py", diff --git a/data/templates/dns-forwarding/recursor.conf.tmpl b/data/templates/dns-forwarding/recursor.conf.tmpl index d233b8abc..b0ae3cc61 100644 --- a/data/templates/dns-forwarding/recursor.conf.tmpl +++ b/data/templates/dns-forwarding/recursor.conf.tmpl @@ -21,7 +21,7 @@ max-cache-entries={{ cache_size }} max-negative-ttl={{ negative_ttl }} # ignore-hosts-file -export-etc-hosts={{ export_hosts_file }} +export-etc-hosts={{ 'no' if ignore_hosts_file is defined else 'yes' }} # listen-address local-address={{ listen_address | join(',') }} diff --git a/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl b/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl index de5eaee00..90f35ae1c 100644 --- a/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl +++ b/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl @@ -3,26 +3,26 @@ # 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 %} +{# 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 %} +{% endfor %} -{%- if n.dot_zone_ns %} +{% if n.dot_zone_ns %} +.={{ n.dot_zone_ns }} -{%- endif %} +{% endif %} -{% if forward_zones -%} +{% if forward_zones is defined %} # 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 %} +{% for zone, zonedata in forward_zones.items() %} +{{ "+" if zonedata['recursion_desired'] is defined }}{{ zone }}={{ zonedata['server']|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 index b0d99d9ae..784d5c360 100644 --- a/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl +++ b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl @@ -1,24 +1,24 @@ -- Autogenerated by VyOS (vyos-hostsd) -- -- Do not edit, your changes will get overwritten -- -{% if hosts -%} +{% if hosts %} -- from 'system static-host-mapping' and DHCP server -{%- for tag, taghosts in hosts.items() %} -{%- for host, hostprops in taghosts.items() %} +{% for tag, taghosts in hosts.items() %} +{% for host, hostprops in taghosts.items() %} addNTA("{{ host }}.", "{{ tag }}") -{%- for a in hostprops['aliases'] %} +{% for a in hostprops['aliases'] %} addNTA("{{ a }}.", "{{ tag }} alias") -{%- endfor %} -{%- endfor %} -{%- endfor %} -{%- endif %} +{% endfor %} +{% endfor %} +{% endfor %} +{% endif %} -{% if forward_zones -%} +{% if forward_zones is defined %} -- from 'service dns forwarding domain' -{%- for zone, zonedata in forward_zones.items() %} -{%- if zonedata['addNTA'] %} +{% for zone, zonedata in forward_zones.items() %} +{% if zonedata['addnta'] is defined %} addNTA("{{ zone }}", "static") -{%- endif %} -{%- endfor %} -{%- endif %} +{% endif %} +{% endfor %} +{% endif %} diff --git a/data/templates/macsec/wpa_supplicant.conf.tmpl b/data/templates/macsec/wpa_supplicant.conf.tmpl index 1731bf160..5b353def8 100644 --- a/data/templates/macsec/wpa_supplicant.conf.tmpl +++ b/data/templates/macsec/wpa_supplicant.conf.tmpl @@ -1,4 +1,4 @@ -# autogenerated by interfaces-macsec.py +### Autogenerated by interfaces-macsec.py ### # see full documentation: # https://w1.fi/cgit/hostap/plain/wpa_supplicant/wpa_supplicant.conf diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index 401f8e04b..fea310236 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -181,7 +181,11 @@ dh {{ tls_dh }} {%- endif %} {%- if tls_auth %} -tls-auth {{tls_auth}} +{%- if mode == 'client' %} +tls-auth {{tls_auth}} 1 +{%- elif mode == 'server' %} +tls-auth {{tls_auth}} 0 +{%- endif %} {%- endif %} {%- if tls_role %} @@ -196,7 +200,9 @@ tls-server # Encryption options {%- if encryption %} -{% if encryption == 'des' -%} +{% if encryption == 'none' -%} +cipher none +{%- elif encryption == 'des' -%} cipher des-cbc {%- elif encryption == '3des' -%} cipher des-ede3-cbc diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl index 132c4ce40..c5e4240d1 100644 --- a/data/templates/wifi/hostapd.conf.tmpl +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -11,6 +11,21 @@ device_name={{ description | truncate(32, True) }} # command line parameter. interface={{ ifname }} +{% if is_bridge_member is defined %} +# In case of atheros and nl80211 driver interfaces, an additional +# configuration parameter, bridge, may be used to notify hostapd if the +# interface is included in a bridge. This parameter is not used with Host AP +# driver. If the bridge parameter is not set, the drivers will automatically +# figure out the bridge interface (assuming sysfs is enabled and mounted to +# /sys) and this parameter may not be needed. +# +# For nl80211, this parameter can be used to request the AP interface to be +# added to the bridge automatically (brctl may refuse to do this before hostapd +# has been started to change the interface mode). If needed, the bridge +# interface is also created. +bridge={{ is_bridge_member }} +{% endif %} + # 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 @@ -433,6 +448,14 @@ macaddr_acl=0 max_num_sta={{ max_stations }} {% endif %} +{% if wds is defined %} +# WDS (4-address frame) mode with per-station virtual interfaces +# (only supported with driver=nl80211) +# This mode allows associated stations to use 4-address frames to allow layer 2 +# bridging to be used. +wds_sta=1 +{% 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. diff --git a/data/templates/wifi/wpa_supplicant.conf.tmpl b/data/templates/wifi/wpa_supplicant.conf.tmpl index 9ddad35fd..f84892dc0 100644 --- a/data/templates/wifi/wpa_supplicant.conf.tmpl +++ b/data/templates/wifi/wpa_supplicant.conf.tmpl @@ -1,7 +1,13 @@ -# WPA supplicant config +### Autogenerated by interfaces-macsec.py ### + +# see full documentation: +# https://w1.fi/cgit/hostap/plain/wpa_supplicant/wpa_supplicant.conf + network={ ssid="{{ ssid }}" + scan_ssid=1 {% if security is defined and security.wpa is defined and security.wpa.passphrase is defined %} + key_mgmt=WPA-PSK psk="{{ security.wpa.passphrase }}" {% else %} key_mgmt=NONE diff --git a/data/templates/wwan/peer.tmpl b/data/templates/wwan/peer.tmpl index aa759f741..e23881bf8 100644 --- a/data/templates/wwan/peer.tmpl +++ b/data/templates/wwan/peer.tmpl @@ -21,7 +21,7 @@ noauth crtscts lock persist -{{ "demand" if ondemand is defined }} +{{ "demand" if connect_on_demand is defined }} connect '/usr/sbin/chat -v -t6 -f /etc/ppp/peers/chat.{{ ifname }}' diff --git a/debian/control b/debian/control index 0777ecc52..ebcfc6c43 100644 --- a/debian/control +++ b/debian/control @@ -18,7 +18,7 @@ Build-Depends: Standards-Version: 3.9.6 Package: vyos-1x -Architecture: all +Architecture: amd64 arm64 Depends: accel-ppp, beep, @@ -117,7 +117,7 @@ Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything Package: vyos-1x-vmware -Architecture: amd64 i386 +Architecture: amd64 Depends: vyos-1x, open-vm-tools diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index aaf8bb27d..07e63d54a 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -16,7 +16,7 @@ <children> <leafNode name="cache-size"> <properties> - <help>DNS forwarding cache size</help> + <help>DNS forwarding cache size (default: 10000)</help> <valueHelp> <format>0-10000</format> <description>DNS forwarding cache size</description> @@ -25,6 +25,7 @@ <validator name="numeric" argument="--range 0-10000"/> </constraint> </properties> + <defaultValue>10000</defaultValue> </leafNode> <leafNode name="dhcp"> <properties> @@ -37,7 +38,7 @@ </leafNode> <leafNode name="dnssec"> <properties> - <help>DNSSEC mode</help> + <help>DNSSEC mode (default: process-no-validate)</help> <completionHelp> <list>off process-no-validate process log-fail validate</list> </completionHelp> @@ -62,9 +63,10 @@ <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> + <regex>^(off|process-no-validate|process|log-fail|validate)$</regex> </constraint> </properties> + <defaultValue>process-no-validate</defaultValue> </leafNode> <tagNode name="domain"> <properties> @@ -146,7 +148,7 @@ </leafNode> <leafNode name="negative-ttl"> <properties> - <help>Maximum amount of time negative entries are cached</help> + <help>Maximum amount of time negative entries are cached (default: 3600)</help> <valueHelp> <format>0-7200</format> <description>Seconds to cache NXDOMAIN entries</description> @@ -155,6 +157,7 @@ <validator name="numeric" argument="--range 0-7200"/> </constraint> </properties> + <defaultValue>3600</defaultValue> </leafNode> <leafNode name="name-server"> <properties> diff --git a/interface-definitions/include/accel-client-ipv6-pool.xml.in b/interface-definitions/include/accel-client-ipv6-pool.xml.i index 455ada6ef..455ada6ef 100644 --- a/interface-definitions/include/accel-client-ipv6-pool.xml.in +++ b/interface-definitions/include/accel-client-ipv6-pool.xml.i diff --git a/interface-definitions/include/accel-name-server.xml.in b/interface-definitions/include/accel-name-server.xml.i index 82ed6771d..82ed6771d 100644 --- a/interface-definitions/include/accel-name-server.xml.in +++ b/interface-definitions/include/accel-name-server.xml.i diff --git a/interface-definitions/include/accel-radius-additions.xml.in b/interface-definitions/include/accel-radius-additions.xml.i index e37b68514..e37b68514 100644 --- a/interface-definitions/include/accel-radius-additions.xml.in +++ b/interface-definitions/include/accel-radius-additions.xml.i diff --git a/interface-definitions/include/interface-dial-on-demand.xml.i b/interface-definitions/include/interface-dial-on-demand.xml.i new file mode 100644 index 000000000..c14ddf6f5 --- /dev/null +++ b/interface-definitions/include/interface-dial-on-demand.xml.i @@ -0,0 +1,6 @@ +<leafNode name="connect-on-demand"> + <properties> + <help>Establishment connection automatically when traffic is sent</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/vif-s.xml.i b/interface-definitions/include/vif-s.xml.i index a6d7c81ce..cd0afe742 100644 --- a/interface-definitions/include/vif-s.xml.i +++ b/interface-definitions/include/vif-s.xml.i @@ -13,25 +13,26 @@ #include <include/dhcpv6-options.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> - <leafNode name="ethertype"> + <leafNode name="protocol"> <properties> - <help>Set Ethertype</help> + <help>Protocol used for service VLAN (default: 802.1ad)</help> <completionHelp> - <list>0x88A8 0x8100</list> + <list>802.1ad 802.1q</list> </completionHelp> <valueHelp> - <format>0x88A8</format> - <description>802.1ad</description> + <format>802.1ad</format> + <description>Provider Bridging (IEEE 802.1ad, Q-inQ), ethertype 0x88a8</description> </valueHelp> <valueHelp> - <format>0x8100</format> - <description>802.1q</description> + <format>802.1q</format> + <description>VLAN-tagged frame (IEEE 802.1q), ethertype 0x8100</description> </valueHelp> <constraint> - <regex>(0x88A8|0x8100)</regex> + <regex>(802.1q|802.1ad)</regex> </constraint> - <constraintErrorMessage>Ethertype must be 0x88A8 or 0x8100</constraintErrorMessage> + <constraintErrorMessage>Ethertype must be 802.1ad or 802.1q</constraintErrorMessage> </properties> + <defaultValue>802.1ad</defaultValue> </leafNode> <node name="ip"> <children> diff --git a/interface-definitions/include/vif.xml.i b/interface-definitions/include/vif.xml.i index 5a4e52122..919e4d493 100644 --- a/interface-definitions/include/vif.xml.i +++ b/interface-definitions/include/vif.xml.i @@ -50,6 +50,7 @@ #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"> diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in index dfef387d2..9384726cc 100644 --- a/interface-definitions/interfaces-macsec.xml.in +++ b/interface-definitions/interfaces-macsec.xml.in @@ -107,8 +107,9 @@ </node> #include <include/interface-description.xml.i> #include <include/interface-disable.xml.i> - #include <include/interface-vrf.xml.i> + #include <include/interface-mtu-68-9000.xml.i> #include <include/source-interface-ethernet.xml.i> + #include <include/interface-vrf.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index 905c76507..5675379d5 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -63,9 +63,13 @@ <properties> <help>Standard Data Encryption Algorithm</help> <completionHelp> - <list>des 3des bf128 bf256 aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list> + <list>none des 3des bf128 bf256 aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list> </completionHelp> <valueHelp> + <format>none</format> + <description>Disable encryption</description> + </valueHelp> + <valueHelp> <format>des</format> <description>DES algorithm</description> </valueHelp> @@ -106,7 +110,7 @@ <description>AES algorithm with 256-bit key GCM</description> </valueHelp> <constraint> - <regex>(des|3des|bf128|bf256|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex> + <regex>(none|des|3des|bf128|bf256|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex> </constraint> </properties> </leafNode> @@ -114,9 +118,13 @@ <properties> <help>Cipher negotiation list for use in server or client mode</help> <completionHelp> - <list>des 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list> + <list>none des 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list> </completionHelp> <valueHelp> + <format>none</format> + <description>Disable encryption</description> + </valueHelp> + <valueHelp> <format>des</format> <description>DES algorithm</description> </valueHelp> @@ -149,7 +157,7 @@ <description>AES algorithm with 256-bit key GCM</description> </valueHelp> <constraint> - <regex>(des|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex> + <regex>(none|des|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex> </constraint> <multi/> </properties> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index 8a6c61312..b6208e0b9 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -42,12 +42,7 @@ </leafNode> </children> </node> - <leafNode name="connect-on-demand"> - <properties> - <help>Automatic establishment of PPPOE connection when traffic is sent</help> - <valueless/> - </properties> - </leafNode> + #include <include/interface-dial-on-demand.xml.i> <leafNode name="default-route"> <properties> <help>Default route insertion behaviour (default: auto)</help> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index a0caf810f..8c594e758 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -770,6 +770,12 @@ </leafNode> #include <include/vif.xml.i> #include <include/vif-s.xml.i> + <leafNode name="wds"> + <properties> + <help>Enable WDS (Wireless Distribution System)</help> + <valueless/> + </properties> + </leafNode> </children> </tagNode> </children> diff --git a/interface-definitions/interfaces-wirelessmodem.xml.in b/interface-definitions/interfaces-wirelessmodem.xml.in index d375b808d..96604ff00 100644 --- a/interface-definitions/interfaces-wirelessmodem.xml.in +++ b/interface-definitions/interfaces-wirelessmodem.xml.in @@ -80,12 +80,7 @@ <valueless/> </properties> </leafNode> - <leafNode name="ondemand"> - <properties> - <help>Only dial when traffic is available</help> - <valueless/> - </properties> - </leafNode> + #include <include/interface-dial-on-demand.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in index 9ee5d5156..ee09d01d6 100644 --- a/interface-definitions/service_ipoe-server.xml.in +++ b/interface-definitions/service_ipoe-server.xml.in @@ -111,8 +111,8 @@ </leafNode> </children> </tagNode> - #include <include/accel-name-server.xml.in> - #include <include/accel-client-ipv6-pool.xml.in> + #include <include/accel-name-server.xml.i> + #include <include/accel-client-ipv6-pool.xml.i> <node name="authentication"> <properties> <help>Client authentication methods</help> @@ -198,7 +198,7 @@ </children> </tagNode> #include <include/radius-server.xml.i> - #include <include/accel-radius-additions.xml.in> + #include <include/accel-radius-additions.xml.i> </children> </node> </children> diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in index 605f47b37..64fd6e4ef 100644 --- a/interface-definitions/service_pppoe-server.xml.in +++ b/interface-definitions/service_pppoe-server.xml.in @@ -109,7 +109,7 @@ </node> #include <include/accel-auth-mode.xml.i> #include <include/radius-server.xml.i> - #include <include/accel-radius-additions.xml.in> + #include <include/accel-radius-additions.xml.i> <node name="radius"> <children> <node name="rate-limit"> @@ -200,8 +200,8 @@ </leafNode> </children> </node> - #include <include/accel-client-ipv6-pool.xml.in> - #include <include/accel-name-server.xml.in> + #include <include/accel-client-ipv6-pool.xml.i> + #include <include/accel-name-server.xml.i> <tagNode name="interface"> <properties> <help>interface(s) to listen on</help> diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in index 702ef8b5a..4de28d2bd 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn_l2tp.xml.in @@ -36,7 +36,7 @@ </constraint> </properties> </leafNode> - #include <include/accel-name-server.xml.in> + #include <include/accel-name-server.xml.i> <node name="lns"> <properties> <help>L2TP Network Server (LNS)</help> @@ -203,7 +203,7 @@ </leafNode> </children> </node> - #include <include/accel-client-ipv6-pool.xml.in> + #include <include/accel-client-ipv6-pool.xml.i> <leafNode name="description"> <properties> <help>Description for L2TP remote-access settings</help> diff --git a/interface-definitions/vpn_pptp.xml.in b/interface-definitions/vpn_pptp.xml.in index 032455b4d..f37c9bd01 100644 --- a/interface-definitions/vpn_pptp.xml.in +++ b/interface-definitions/vpn_pptp.xml.in @@ -153,7 +153,7 @@ </children> </node> #include <include/radius-server.xml.i> - #include <include/accel-radius-additions.xml.in> + #include <include/accel-radius-additions.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in index f0c93b882..5da2f8f24 100644 --- a/interface-definitions/vpn_sstp.xml.in +++ b/interface-definitions/vpn_sstp.xml.in @@ -96,7 +96,7 @@ </properties> </leafNode> #include <include/radius-server.xml.i> - #include <include/accel-radius-additions.xml.in> + #include <include/accel-radius-additions.xml.i> <node name="radius"> <children> <node name="rate-limit"> @@ -207,8 +207,8 @@ </leafNode> </children> </node> - #include <include/accel-client-ipv6-pool.xml.in> - #include <include/accel-name-server.xml.in> + #include <include/accel-client-ipv6-pool.xml.i> + #include <include/accel-name-server.xml.i> #include <include/interface-mtu-68-1500.xml.i> </children> </node> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index bfc70b772..58ecd3f17 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -18,9 +18,12 @@ 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.util import vyos_dict_search +from vyos.xml import defaults +from vyos.xml import is_tag +from vyos.xml import is_leaf from vyos import ConfigError def retrieve_config(path_hash, base_path, config): @@ -104,47 +107,6 @@ def list_diff(first, second): 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 @@ -207,16 +169,100 @@ def get_removed_vlans(conf, dict): return dict +def T2665_set_dhcpv6pd_defaults(config_dict): + """ Properly configure DHCPv6 default options in the dictionary. If there is + no DHCPv6 configured at all, it is safe to remove the entire configuration. + """ + # As this is the same for every interface type it is safe to assume this + # for ethernet + pd_defaults = defaults(['interfaces', 'ethernet', 'dhcpv6-options', 'pd']) -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) + if vyos_dict_search('dhcpv6_options.pd.length', config_dict): + del config_dict['dhcpv6_options']['pd']['length'] + + for pd in (vyos_dict_search('dhcpv6_options.pd', config_dict) or []): + config_dict['dhcpv6_options']['pd'][pd] = dict_merge(pd_defaults, + config_dict['dhcpv6_options']['pd'][pd]) return config_dict +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 + intftypes = ['bonding', 'bridge'] + if intftype not in intftypes + [None]: + raise ValueError(( + f'unknown interface type "{intftype}" or it cannot ' + f'have member interfaces')) + + intftype = intftypes 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 is_tag(memberintf): + if interface in conf.list_nodes(memberintf): + ret_val = intf + break + elif 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 is_source_interface(conf, interface, intftype=None): + """ + Checks if passed interface is configured as source-interface of other + interfaces of specified type. intftype is optional, if not passed it will + search all known types (currently pppoe, macsec, pseudo-ethernet, tunnel + and vxlan) + + 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 + intftypes = ['macsec', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan'] + if intftype not in intftypes + [None]: + raise ValueError(f'unknown interface type "{intftype}" or it can not ' + 'have a source-interface') + + intftype = intftypes 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): + lower_intf = base + [intf, 'source-interface'] + if conf.exists(lower_intf) and interface in conf.return_values(lower_intf): + ret_val = intf + break + + old_level = conf.set_level(old_level) + return ret_val + def get_interface_dict(config, base, ifname=''): """ Common utility function to retrieve and mandgle the interfaces available @@ -225,10 +271,6 @@ def get_interface_dict(config, base, ifname=''): 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: @@ -238,6 +280,12 @@ def get_interface_dict(config, base, ifname=''): # retrieve interface default values default_values = defaults(base) + # We take care about VLAN (vif, vif-s, vif-c) default values later on when + # parsing vlans in default dict and merge the "proper" values in correctly, + # see T2665. + for vif in ['vif', 'vif_s']: + if vif in default_values: del default_values[vif] + # 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) @@ -249,24 +297,42 @@ def get_interface_dict(config, base, ifname=''): # Add interface instance name into dictionary dict.update({'ifname': ifname}) + # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely + # remove the default values from the dict. + if 'dhcpv6_options' not in dict: + if 'dhcpv6_options' in default_values: + del default_values['dhcpv6_options'] + # 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) + # XXX: T2665: blend in proper DHCPv6-PD default values + dict = T2665_set_dhcpv6pd_defaults(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}) + 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}) + if bond: dict.update({'is_bond_member' : bond}) + + # Some interfaces come with a source_interface which must also not be part + # of any other bond or bridge interface as it is exclusivly assigned as the + # Kernels "lower" interface to this new "virtual/upper" interface. + if 'source_interface' in dict: + # Check if source interface is member of another bridge + tmp = is_member(config, dict['source_interface'], 'bridge') + if tmp: dict.update({'source_interface_is_bridge_member' : tmp}) + + # Check if source interface is member of another bridge + tmp = is_member(config, dict['source_interface'], 'bonding') + if tmp: dict.update({'source_interface_is_bond_member' : tmp}) mac = leaf_node_changed(config, ['mac']) - if mac: - dict.update({'mac_old' : mac}) + if mac: dict.update({'mac_old' : mac}) eui64 = leaf_node_changed(config, ['ipv6', 'address', 'eui64']) if eui64: @@ -276,36 +342,49 @@ def get_interface_dict(config, base, ifname=''): 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)) + default_vif_values = defaults(base + ['vif']) + # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely + # remove the default values from the dict. + if not 'dhcpv6_options' in vif_config: + del default_vif_values['dhcpv6_options'] + + dict['vif'][vif] = dict_merge(default_vif_values, vif_config) + # XXX: T2665: blend in proper DHCPv6-PD default values + dict['vif'][vif] = T2665_set_dhcpv6pd_defaults(dict['vif'][vif]) 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)) + default_vif_s_values = defaults(base + ['vif-s']) + # XXX: T2665: we only wan't the vif-s defaults - do not care about vif-c + if 'vif_c' in default_vif_s_values: del default_vif_s_values['vif_c'] + + # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely + # remove the default values from the dict. + if not 'dhcpv6_options' in vif_s_config: + del default_vif_s_values['dhcpv6_options'] + + dict['vif_s'][vif_s] = dict_merge(default_vif_s_values, vif_s_config) + # XXX: T2665: blend in proper DHCPv6-PD default values + dict['vif_s'][vif_s] = T2665_set_dhcpv6pd_defaults( + dict['vif_s'][vif_s]) + 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)) + default_vif_c_values = defaults(base + ['vif-s', 'vif-c']) + + # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely + # remove the default values from the dict. + if not 'dhcpv6_options' in vif_c_config: + del default_vif_c_values['dhcpv6_options'] + + dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_merge( + default_vif_c_values, vif_c_config) + # XXX: T2665: blend in proper DHCPv6-PD default values + dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_set_dhcpv6pd_defaults( + dict['vif_s'][vif_s]['vif_c'][vif_c]) # Check vif, vif-s/vif-c VLAN interfaces for removal dict = get_removed_vlans(config, dict) - return dict - diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 7e1930878..944fc4294 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -23,6 +23,57 @@ from vyos import ConfigError +def verify_mtu(config): + """ + Common helper function used by interface implementations to perform + recurring validation if the specified MTU can be used by the underlaying + hardware. + """ + from vyos.ifconfig import Interface + if 'mtu' in config: + mtu = int(config['mtu']) + + tmp = Interface(config['ifname']) + min_mtu = tmp.get_min_mtu() + max_mtu = tmp.get_max_mtu() + + if mtu < min_mtu: + raise ConfigError(f'Interface MTU too low, ' \ + f'minimum supported MTU is {min_mtu}!') + if mtu > max_mtu: + raise ConfigError(f'Interface MTU too high, ' \ + f'maximum supported MTU is {max_mtu}!') + +def verify_mtu_ipv6(config): + """ + Common helper function used by interface implementations to perform + recurring validation if the specified MTU can be used when IPv6 is + configured on the interface. IPv6 requires a 1280 bytes MTU. + """ + from vyos.validate import is_ipv6 + from vyos.util import vyos_dict_search + # IPv6 minimum required link mtu + min_mtu = 1280 + + if int(config['mtu']) < min_mtu: + interface = config['ifname'] + error_msg = f'IPv6 address will be configured on interface "{interface}" ' \ + f'thus the minimum MTU requirement is {min_mtu}!' + + if not vyos_dict_search('ipv6.address.no_default_link_local', config): + raise ConfigError('link-local ' + error_msg) + + for address in (vyos_dict_search('address', config) or []): + if address in ['dhcpv6'] or is_ipv6(address): + raise ConfigError(error_msg) + + if vyos_dict_search('ipv6.address.autoconf', config): + raise ConfigError(error_msg) + + if vyos_dict_search('ipv6.address.eui64', config): + raise ConfigError(error_msg) + + def verify_vrf(config): """ Common helper function used by interface implementations to perform @@ -82,9 +133,20 @@ def verify_source_interface(config): 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)) + raise ConfigError('Specified source-interface {source_interface} does ' + 'not exist'.format(**config)) + + if 'source_interface_is_bridge_member' in config: + raise ConfigError('Invalid source-interface {source_interface}. Interface ' + 'is already a member of bridge ' + '{source_interface_is_bridge_member}'.format(**config)) + + if 'source_interface_is_bond_member' in config: + raise ConfigError('Invalid source-interface {source_interface}. Interface ' + 'is already a member of bond ' + '{source_interface_is_bond_member}'.format(**config)) def verify_dhcpv6(config): """ @@ -102,6 +164,11 @@ def verify_dhcpv6(config): # assigned IPv6 subnet from a delegated prefix for pd in vyos_dict_search('dhcpv6_options.pd', config): sla_ids = [] + + if not vyos_dict_search(f'dhcpv6_options.pd.{pd}.interface', config): + raise ConfigError('DHCPv6-PD requires an interface where to assign ' + 'the delegated prefix!') + 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) diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 4c76fe996..c133a56fc 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. +from netifaces import interfaces + from vyos.ifconfig.interface import Interface from vyos.ifconfig.stp import STP from vyos.validate import assert_boolean @@ -228,8 +230,8 @@ class BridgeIf(Interface): # remove interface from bridge tmp = vyos_dict_search('member.interface_remove', config) - if tmp: - for member in tmp: + for member in (tmp or []): + if member in interfaces(): self.del_port(member) STPBridgeIf = STP.enable(BridgeIf) diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py index 0a13043cc..5c4597be8 100644 --- a/python/vyos/ifconfig/geneve.py +++ b/python/vyos/ifconfig/geneve.py @@ -13,7 +13,6 @@ # 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 @@ -51,18 +50,6 @@ class GeneveIf(Interface): # 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 diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index be97b411b..d200fc7a8 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -50,14 +50,6 @@ 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 @@ -86,6 +78,14 @@ class Interface(Control): 'shellcmd': 'ip -json link show dev {ifname}', 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down', }, + 'min_mtu': { + 'shellcmd': 'ip -json -detail link list dev {ifname}', + 'format': lambda j: jmespath.search('[*].min_mtu | [0]', json.loads(j)), + }, + 'max_mtu': { + 'shellcmd': 'ip -json -detail link list dev {ifname}', + 'format': lambda j: jmespath.search('[*].max_mtu | [0]', json.loads(j)), + }, } _command_set = { @@ -182,6 +182,15 @@ class Interface(Control): def exists(cls, ifname): return os.path.exists(f'/sys/class/net/{ifname}') + @classmethod + def get_config(cls): + """ + Some but not all interfaces require a configuration when they are added + using iproute2. This method will provide the configuration dictionary + used by this class. + """ + return deepcopy(cls.default) + def __init__(self, ifname, **kargs): """ This is the base interface class which supports basic IP/MAC address @@ -281,6 +290,28 @@ class Interface(Control): cmd = 'ip link del dev {ifname}'.format(**self.config) return self._cmd(cmd) + def get_min_mtu(self): + """ + Get hardware minimum supported MTU + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_min_mtu() + '60' + """ + return int(self.get_interface('min_mtu')) + + def get_max_mtu(self): + """ + Get hardware maximum supported MTU + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_max_mtu() + '9000' + """ + return int(self.get_interface('max_mtu')) + def get_mtu(self): """ Get/set interface mtu in bytes. @@ -290,7 +321,7 @@ class Interface(Control): >>> Interface('eth0').get_mtu() '1500' """ - return self.get_interface('mtu') + return int(self.get_interface('mtu')) def set_mtu(self, mtu): """ @@ -1013,7 +1044,7 @@ class Interface(Control): # create/update 802.1ad (Q-in-Q VLANs) for vif_s_id, vif_s_config in config.get('vif_s', {}).items(): tmp = deepcopy(VLANIf.get_config()) - tmp['ethertype'] = get_ethertype(vif_s_config.get('ethertype', '0x88A8')) + tmp['protocol'] = vif_s_config['protocol'] tmp['source_interface'] = ifname tmp['vlan_id'] = vif_s_id @@ -1061,13 +1092,13 @@ class VLANIf(Interface): 'type': 'vlan', 'source_interface': '', 'vlan_id': '', - 'ethertype': '', + 'protocol': '', 'ingress_qos': '', 'egress_qos': '', } options = Interface.options + \ - ['source_interface', 'vlan_id', 'ethertype', 'ingress_qos', 'egress_qos'] + ['source_interface', 'vlan_id', 'protocol', 'ingress_qos', 'egress_qos'] def remove(self): """ @@ -1092,12 +1123,12 @@ class VLANIf(Interface): def _create(self): # bail out early if interface already exists - if os.path.exists(f'/sys/class/net/{self.ifname}'): + if self.exists(f'{self.ifname}'): return cmd = 'ip link add link {source_interface} name {ifname} type vlan id {vlan_id}' - if self.config['ethertype']: - cmd += ' proto {ethertype}' + if self.config['protocol']: + cmd += ' protocol {protocol}' if self.config['ingress_qos']: cmd += ' ingress-qos-map {ingress_qos}' if self.config['egress_qos']: @@ -1108,20 +1139,6 @@ class VLANIf(Interface): # 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 = VLANIf().get_config() - """ - config = deepcopy(__class__.default) - del config['type'] - return config - def set_admin_state(self, state): """ Set interface administrative state to be 'up' or 'down' diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py index 33740921e..5fd90f9cf 100644 --- a/python/vyos/ifconfig/l2tpv3.py +++ b/python/vyos/ifconfig/l2tpv3.py @@ -13,7 +13,6 @@ # 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 @@ -28,6 +27,15 @@ class L2TPv3If(Interface): default = { 'type': 'l2tp', + 'peer_tunnel_id': '', + 'local_port': 0, + 'remote_port': 0, + 'encapsulation': 'udp', + 'local_address': '', + 'remote_address': '', + 'session_id': '', + 'tunnel_id': '', + 'peer_session_id': '' } definition = { **Interface.definition, @@ -73,7 +81,7 @@ class L2TPv3If(Interface): >>> i.remove() """ - if os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): + if self.exists(self.config['ifname']): # interface is always A/D down. It needs to be enabled explicitly self.set_admin_state('down') @@ -86,25 +94,3 @@ class L2TPv3If(Interface): 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/macsec.py b/python/vyos/ifconfig/macsec.py index 6f570d162..456686ea6 100644 --- a/python/vyos/ifconfig/macsec.py +++ b/python/vyos/ifconfig/macsec.py @@ -56,22 +56,6 @@ class MACsecIf(Interface): # 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 diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index 9c1d09c1c..2447fec77 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# 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 @@ -13,7 +13,6 @@ # 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 @@ -53,18 +52,6 @@ class MACVLANIf(Interface): 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 diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index dba62b61a..ad1f605ed 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -13,8 +13,6 @@ # 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 @@ -97,18 +95,6 @@ class VXLANIf(Interface): 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 diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py index 346577119..deca68bf0 100644 --- a/python/vyos/ifconfig/wireless.py +++ b/python/vyos/ifconfig/wireless.py @@ -23,8 +23,10 @@ class WiFiIf(Interface): default = { 'type': 'wifi', - 'phy': 'phy0' + 'phy': '', + 'wds': 'off', } + definition = { **Interface.definition, **{ @@ -33,12 +35,19 @@ class WiFiIf(Interface): 'bridgeable': True, } } + options = Interface.options + \ ['phy', 'op_mode'] + _command_set = {**Interface._command_set, **{ + '4addr': { + 'shellcmd': 'iw dev {ifname} set 4addr {value}', + }, + }} + def _create(self): # all interfaces will be added in monitor mode - cmd = 'iw phy {phy} interface add {ifname} type monitor' \ + cmd = 'iw phy {phy} interface add {ifname} type monitor 4addr {wds}' \ .format(**self.config) self._cmd(cmd) @@ -50,21 +59,8 @@ class WiFiIf(Interface): .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 set_4aadr_mode(self, state): + return self.set_interface('4addr', state) def update(self, config): """ General helper function which works on a dictionary retrived by @@ -72,22 +68,11 @@ class WiFiIf(Interface): interface setup code and provide a single point of entry when workin on any interface. """ - # We can not call add_to_bridge() until wpa_supplicant is running, thus - # we will remove the key from the config dict and react to this specal - # case in thie derived class. - # re-add ourselves to any bridge we might have fallen out of - bridge_member = '' - if 'is_bridge_member' in config: - bridge_member = config['is_bridge_member'] - del config['is_bridge_member'] + self.set_4aadr_mode('on' if 'wds' in config else 'off') # call base class first super().update(config) - # re-add ourselves to any bridge we might have fallen out of - if bridge_member: - self.add_to_bridge(bridge_member) - # 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 diff --git a/python/vyos/validate.py b/python/vyos/validate.py index ceeb6888a..691cf3c8e 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -19,7 +19,6 @@ import netifaces import ipaddress from vyos.util import cmd -from vyos import xml # Important note when you are adding new validation functions: # @@ -267,46 +266,6 @@ def assert_mac(m): 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. diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 14ec7e137..047c19dd0 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -14,12 +14,15 @@ import os import unittest +import json from netifaces import ifaddresses, AF_INET, AF_INET6 from vyos.configsession import ConfigSession from vyos.ifconfig import Interface from vyos.util import read_file +from vyos.util import cmd +from vyos.util import vyos_dict_search from vyos.validate import is_intf_addr_assigned, is_ipv6_link_local class BasicInterfaceTest: @@ -212,8 +215,12 @@ class BasicInterfaceTest: self.session.set(base + ['address', address]) self.session.commit() + for interface in self._interfaces: for vif_s in self._qinq_range: + tmp = json.loads(cmd(f'ip -d -j link show dev {interface}.{vif_s}'))[0] + self.assertEqual(vyos_dict_search('linkinfo.info_data.protocol', tmp), '802.1ad') + for vif_c in self._vlan_range: vif = f'{interface}.{vif_s}.{vif_c}' for address in self._test_addr: diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py index 0f1b6486d..6d1be86ba 100755 --- a/smoketest/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -16,15 +16,17 @@ import re import unittest -from psutil import process_iter -from vyos.ifconfig import Section from base_interfaces_test import BasicInterfaceTest +from netifaces import interfaces + from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section from vyos.util import read_file +from vyos.util import process_named_running -def get_config_value(intf, key): - tmp = read_file(f'/run/wpa_supplicant/{intf}.conf') +def get_config_value(interface, key): + tmp = read_file(f'/run/wpa_supplicant/{interface}.conf') tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp) return tmp[0] @@ -32,71 +34,123 @@ class MACsecInterfaceTest(BasicInterfaceTest.BaseTest): def setUp(self): super().setUp() self._base_path = ['interfaces', 'macsec'] - self._options = { - 'macsec0': ['source-interface eth0', - 'security cipher gcm-aes-128'] - } + self._options = { 'macsec0': ['source-interface eth0', 'security cipher gcm-aes-128'] } # if we have a physical eth1 interface, add a second macsec instance if 'eth1' in Section.interfaces("ethernet"): - macsec = { 'macsec1': ['source-interface eth1', 'security cipher gcm-aes-128'] } + macsec = { 'macsec1': [f'source-interface eth1', 'security cipher gcm-aes-128'] } self._options.update(macsec) self._interfaces = list(self._options) def test_encryption(self): - """ MACsec can be operating in authentication and encryption - mode - both using different mandatory settings, lets test - encryption as the basic authentication test has been performed - using the base class tests """ - intf = 'macsec0' - src_intf = 'eth0' + """ MACsec can be operating in authentication and encryption mode - both + using different mandatory settings, lets test encryption as the basic + authentication test has been performed using the base class tests """ + mak_cak = '232e44b7fda6f8e2d88a07bf78a7aff4' mak_ckn = '40916f4b23e3d548ad27eedd2d10c6f98c2d21684699647d63d41b500dfe8836' replay_window = '64' - self.session.set(self._base_path + [intf, 'security', 'encrypt']) - # check validate() - Cipher suite must be set for MACsec - with self.assertRaises(ConfigSessionError): - self.session.commit() - self.session.set(self._base_path + [intf, 'security', 'cipher', 'gcm-aes-128']) + for interface, option_value in self._options.items(): + for option in option_value: + if option.split()[0] == 'source-interface': + src_interface = option.split()[1] - # check validate() - Physical source interface must be set for MACsec - with self.assertRaises(ConfigSessionError): + self.session.set(self._base_path + [interface] + option.split()) + + # Encrypt link + self.session.set(self._base_path + [interface, 'security', 'encrypt']) + + # check validate() - Physical source interface MTU must be higher then our MTU + self.session.set(self._base_path + [interface, 'mtu', '1500']) + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.delete(self._base_path + [interface, 'mtu']) + + # check validate() - MACsec security keys mandartory when encryption is enabled + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(self._base_path + [interface, 'security', 'mka', 'cak', mak_cak]) + + # check validate() - MACsec security keys mandartory when encryption is enabled + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.set(self._base_path + [interface, 'security', 'mka', 'ckn', mak_ckn]) + + self.session.set(self._base_path + [interface, 'security', 'replay-window', replay_window]) + + # final commit of settings self.session.commit() - self.session.set(self._base_path + [intf, 'source-interface', src_intf]) - # check validate() - MACsec security keys mandartory when encryption is enabled + tmp = get_config_value(src_interface, 'macsec_integ_only') + self.assertTrue("0" in tmp) + + tmp = get_config_value(src_interface, 'mka_cak') + self.assertTrue(mak_cak in tmp) + + tmp = get_config_value(src_interface, 'mka_ckn') + self.assertTrue(mak_ckn in tmp) + + # check that the default priority of 255 is programmed + tmp = get_config_value(src_interface, 'mka_priority') + self.assertTrue("255" in tmp) + + tmp = get_config_value(src_interface, 'macsec_replay_window') + self.assertTrue(replay_window in tmp) + + tmp = read_file(f'/sys/class/net/{interface}/mtu') + self.assertEqual(tmp, '1460') + + # Check for running process + self.assertTrue(process_named_running('wpa_supplicant')) + + def test_mandatory_toptions(self): + interface = 'macsec1' + self.session.set(self._base_path + [interface]) + + # check validate() - source interface is mandatory with self.assertRaises(ConfigSessionError): self.session.commit() - self.session.set(self._base_path + [intf, 'security', 'mka', 'cak', mak_cak]) + self.session.set(self._base_path + [interface, 'source-interface', 'eth0']) - # check validate() - MACsec security keys mandartory when encryption is enabled + # check validate() - cipher is mandatory with self.assertRaises(ConfigSessionError): self.session.commit() - self.session.set(self._base_path + [intf, 'security', 'mka', 'ckn', mak_ckn]) + self.session.set(self._base_path + [interface, 'security', 'cipher', 'gcm-aes-128']) - self.session.set(self._base_path + [intf, 'security', 'replay-window', replay_window]) + # final commit and verify self.session.commit() + self.assertIn(interface, interfaces()) - tmp = get_config_value(src_intf, 'macsec_integ_only') - self.assertTrue("0" in tmp) + def test_source_interface(self): + """ Ensure source-interface can bot be part of any other bond or bridge """ - tmp = get_config_value(src_intf, 'mka_cak') - self.assertTrue(mak_cak in tmp) + base_bridge = ['interfaces', 'bridge', 'br200'] + base_bond = ['interfaces', 'bonding', 'bond200'] - tmp = get_config_value(src_intf, 'mka_ckn') - self.assertTrue(mak_ckn in tmp) + for interface, option_value in self._options.items(): + for option in option_value: + self.session.set(self._base_path + [interface] + option.split()) + if option.split()[0] == 'source-interface': + src_interface = option.split()[1] - # check that the default priority of 255 is programmed - tmp = get_config_value(src_intf, 'mka_priority') - self.assertTrue("255" in tmp) + self.session.set(base_bridge + ['member', 'interface', src_interface]) + # check validate() - Source interface must not already be a member of a bridge + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.delete(base_bridge) - tmp = get_config_value(src_intf, 'macsec_replay_window') - self.assertTrue(replay_window in tmp) + self.session.set(base_bond + ['member', 'interface', src_interface]) + # check validate() - Source interface must not already be a member of a bridge + with self.assertRaises(ConfigSessionError): + self.session.commit() + self.session.delete(base_bond) - # Check for running process - self.assertTrue("wpa_supplicant" in (p.name() for p in process_iter())) + # final commit and verify + self.session.commit() + self.assertIn(interface, interfaces()) if __name__ == '__main__': unittest.main() + diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index 691f633b7..0e93b6432 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -19,8 +19,7 @@ import re import unittest from base_interfaces_test import BasicInterfaceTest -from psutil import process_iter - +from vyos.util import process_named_running from vyos.util import check_kmod from vyos.util import read_file @@ -54,10 +53,10 @@ class WirelessInterfaceTest(BasicInterfaceTest.BaseTest): for option, option_value in self._options.items(): if 'type access-point' in option_value: # Check for running process - self.assertIn('hostapd', (p.name() for p in process_iter())) + self.assertTrue(process_named_running('hostapd')) elif 'type station' in option_value: # Check for running process - self.assertIn('wpa_supplicant', (p.name() for p in process_iter())) + self.assertTrue(process_named_running('wpa_supplicant')) else: self.assertTrue(False) @@ -137,8 +136,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.BaseTest): self.assertIn(value, tmp) # Check for running process - self.assertIn('hostapd', (p.name() for p in process_iter())) - + self.assertTrue(process_named_running('hostapd')) if __name__ == '__main__': check_kmod('mac80211_hwsim') diff --git a/smoketest/scripts/cli/test_interfaces_wirelessmodem.py b/smoketest/scripts/cli/test_interfaces_wirelessmodem.py index 40cd03b93..efc9c0e98 100755 --- a/smoketest/scripts/cli/test_interfaces_wirelessmodem.py +++ b/smoketest/scripts/cli/test_interfaces_wirelessmodem.py @@ -43,7 +43,7 @@ class WWANInterfaceTest(unittest.TestCase): def test_wlm_1(self): for interface in self._interfaces: self.session.set(base_path + [interface, 'no-peer-dns']) - self.session.set(base_path + [interface, 'ondemand']) + self.session.set(base_path + [interface, 'connect-on-demand']) # check validate() - APN must be configure with self.assertRaises(ConfigSessionError): diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index be52360ed..c7ac87135 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -19,10 +19,12 @@ import os import unittest from getpass import getuser -from psutil import process_iter -from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError from vyos.util import read_file +from vyos.util import process_named_running +PROCESS_NAME = 'ddclient' DDCLIENT_CONF = '/run/ddclient/ddclient.conf' base_path = ['service', 'dns', 'dynamic'] @@ -32,17 +34,6 @@ def get_config_value(key): tmp = tmp[0].rstrip(',') return tmp -def check_process(): - """ - Check for running process, process name changes dynamically e.g. - "ddclient - sleeping for 270 seconds", thus we need a different approach - """ - running = False - for p in process_iter(): - if "ddclient" in p.name(): - running = True - return running - class TestServiceDDNS(unittest.TestCase): def setUp(self): self.session = ConfigSession(os.getpid()) @@ -104,8 +95,7 @@ class TestServiceDDNS(unittest.TestCase): self.assertTrue(pwd == "'" + password + "'") # Check for running process - self.assertTrue(check_process()) - + self.assertTrue(process_named_running(PROCESS_NAME)) def test_rfc2136(self): """ Check if DDNS service can be configured and runs """ @@ -135,7 +125,7 @@ class TestServiceDDNS(unittest.TestCase): # TODO: inspect generated configuration file # Check for running process - self.assertTrue(check_process()) + self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': unittest.main() diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py new file mode 100755 index 000000000..5e2f3dfbd --- /dev/null +++ b/smoketest/scripts/cli/test_service_dns_forwarding.py @@ -0,0 +1,192 @@ +#!/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 re +import os +import unittest + +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.util import read_file +from vyos.util import process_named_running + +CONFIG_FILE = '/run/powerdns/recursor.conf' +FORWARD_FILE = '/run/powerdns/recursor.forward-zones.conf' +HOSTSD_FILE = '/run/powerdns/recursor.vyos-hostsd.conf.lua' +PROCESS_NAME= 'pdns-r/worker' + +base_path = ['service', 'dns', 'forwarding'] + +allow_from = ['192.0.2.0/24', '2001:db8::/32'] +listen_adress = ['127.0.0.1', '::1'] + +def get_config_value(key, file=CONFIG_FILE): + tmp = read_file(file) + tmp = re.findall(r'\n{}=+(.*)'.format(key), tmp) + return tmp[0] + +class TestServicePowerDNS(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + + def tearDown(self): + # Delete DNS forwarding configuration + self.session.delete(base_path) + self.session.commit() + del self.session + + def test_basic_forwarding(self): + """ Check basic DNS forwarding settings """ + cache_size = '20' + negative_ttl = '120' + + self.session.set(base_path + ['cache-size', cache_size]) + self.session.set(base_path + ['negative-ttl', negative_ttl]) + + # check validate() - allow from must be defined + with self.assertRaises(ConfigSessionError): + self.session.commit() + for network in allow_from: + self.session.set(base_path + ['allow-from', network]) + + # check validate() - listen-address must be defined + with self.assertRaises(ConfigSessionError): + self.session.commit() + for address in listen_adress: + self.session.set(base_path + ['listen-address', address]) + + # configure DNSSEC + self.session.set(base_path + ['dnssec', 'validate']) + + # Do not use local /etc/hosts file in name resolution + self.session.set(base_path + ['ignore-hosts-file']) + + # commit changes + self.session.commit() + + # Check configured cache-size + tmp = get_config_value('max-cache-entries') + self.assertEqual(tmp, cache_size) + + # Networks allowed to query this server + tmp = get_config_value('allow-from') + self.assertEqual(tmp, ','.join(allow_from)) + + # Addresses to listen for DNS queries + tmp = get_config_value('local-address') + self.assertEqual(tmp, ','.join(listen_adress)) + + # Maximum amount of time negative entries are cached + tmp = get_config_value('max-negative-ttl') + self.assertEqual(tmp, negative_ttl) + + # Do not use local /etc/hosts file in name resolution + tmp = get_config_value('export-etc-hosts') + self.assertEqual(tmp, 'no') + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + def test_dnssec(self): + """ DNSSEC option testing """ + + for network in allow_from: + self.session.set(base_path + ['allow-from', network]) + for address in listen_adress: + self.session.set(base_path + ['listen-address', address]) + + options = ['off', 'process-no-validate', 'process', 'log-fail', 'validate'] + for option in options: + self.session.set(base_path + ['dnssec', option]) + + # commit changes + self.session.commit() + + tmp = get_config_value('dnssec') + self.assertEqual(tmp, option) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + def test_external_nameserver(self): + """ Externe Domain Name Servers (DNS) addresses """ + + for network in allow_from: + self.session.set(base_path + ['allow-from', network]) + for address in listen_adress: + self.session.set(base_path + ['listen-address', address]) + + nameservers = ['192.0.2.1', '192.0.2.2'] + for nameserver in nameservers: + self.session.set(base_path + ['name-server', nameserver]) + + # commit changes + self.session.commit() + + tmp = get_config_value(r'\+.', file=FORWARD_FILE) + self.assertEqual(tmp, ', '.join(nameservers)) + + # Do not use local /etc/hosts file in name resolution + # default: yes + tmp = get_config_value('export-etc-hosts') + self.assertEqual(tmp, 'yes') + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + def test_domain_forwarding(self): + """ Externe Domain Name Servers (DNS) addresses """ + + for network in allow_from: + self.session.set(base_path + ['allow-from', network]) + for address in listen_adress: + self.session.set(base_path + ['listen-address', address]) + + domains = ['vyos.io', 'vyos.net', 'vyos.com'] + nameservers = ['192.0.2.1', '192.0.2.2'] + for domain in domains: + for nameserver in nameservers: + self.session.set(base_path + ['domain', domain, 'server', nameserver]) + + # Test 'recursion-desired' flag for only one domain + if domain == domains[0]: + self.session.set(base_path + ['domain', domain, 'recursion-desired']) + + # Test 'negative trust anchor' flag for the second domain only + if domain == domains[1]: + self.session.set(base_path + ['domain', domain, 'addnta']) + + # commit changes + self.session.commit() + + # Test configured name-servers + hosts_conf = read_file(HOSTSD_FILE) + for domain in domains: + # Test 'recursion-desired' flag for the first domain only + if domain == domains[0]: key =f'\+{domain}' + else: key =f'{domain}' + tmp = get_config_value(key, file=FORWARD_FILE) + self.assertEqual(tmp, ', '.join(nameservers)) + + # Test 'negative trust anchor' flag for the second domain only + if domain == domains[1]: + self.assertIn(f'addNTA("{domain}", "static")', hosts_conf) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + +if __name__ == '__main__': + unittest.main() + diff --git a/smoketest/scripts/cli/test_service_mdns-repeater.py b/smoketest/scripts/cli/test_service_mdns-repeater.py index 18900b6d2..de73b9914 100755 --- a/smoketest/scripts/cli/test_service_mdns-repeater.py +++ b/smoketest/scripts/cli/test_service_mdns-repeater.py @@ -17,8 +17,8 @@ import os import unittest -from psutil import process_iter from vyos.configsession import ConfigSession +from vyos.util import process_named_running base_path = ['service', 'mdns', 'repeater'] intf_base = ['interfaces', 'dummy'] @@ -45,7 +45,7 @@ class TestServiceMDNSrepeater(unittest.TestCase): self.session.commit() # Check for running process - self.assertTrue("mdns-repeater" in (p.name() for p in process_iter())) + self.assertTrue(process_named_running('mdns-repeater')) if __name__ == '__main__': unittest.main() diff --git a/smoketest/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py index 901ca792d..3a6b12ef4 100755 --- a/smoketest/scripts/cli/test_service_pppoe-server.py +++ b/smoketest/scripts/cli/test_service_pppoe-server.py @@ -14,15 +14,15 @@ # 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 unittest from configparser import ConfigParser -from psutil import process_iter from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError +from vyos.util import process_named_running +process_name = 'accel-pppd' base_path = ['service', 'pppoe-server'] local_if = ['interfaces', 'dummy', 'dum667'] pppoe_conf = '/run/accel-pppd/pppoe.conf' @@ -116,7 +116,7 @@ class TestServicePPPoEServer(unittest.TestCase): self.assertEqual(conf['connlimit']['limit'], '20/min') # Check for running process - self.assertTrue('accel-pppd' in (p.name() for p in process_iter())) + self.assertTrue(process_named_running(process_name)) def test_radius_auth(self): """ Test configuration of RADIUS authentication for PPPoE server """ @@ -161,7 +161,7 @@ class TestServicePPPoEServer(unittest.TestCase): self.assertFalse(conf['ppp'].getboolean('ccp')) # Check for running process - self.assertTrue('accel-pppd' in (p.name() for p in process_iter())) + self.assertTrue(process_named_running(process_name)) if __name__ == '__main__': unittest.main() diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index ec2110c8a..238f59e6d 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -18,9 +18,9 @@ import re import os import unittest -from psutil import process_iter from vyos.configsession import ConfigSession from vyos.util import read_file +from vyos.util import process_named_running RADVD_CONF = '/run/radvd/radvd.conf' @@ -90,10 +90,8 @@ class TestServiceRADVD(unittest.TestCase): tmp = get_config_value('AdvOnLink') self.assertEqual(tmp, 'off') - - # Check for running process - self.assertTrue('radvd' in (p.name() for p in process_iter())) + self.assertTrue(process_named_running('radvd')) if __name__ == '__main__': unittest.main() diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py index fb5f5393f..067a3c76b 100755 --- a/smoketest/scripts/cli/test_service_snmp.py +++ b/smoketest/scripts/cli/test_service_snmp.py @@ -19,12 +19,15 @@ import re import unittest from vyos.validate import is_ipv4 -from psutil import process_iter -from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError from vyos.util import read_file +from vyos.util import process_named_running +PROCESS_NAME = 'snmpd' SNMPD_CONF = '/etc/snmp/snmpd.conf' + base_path = ['service', 'snmp'] def get_config_value(key): @@ -78,7 +81,7 @@ class TestSNMPService(unittest.TestCase): self.assertTrue(expected in config) # Check for running process - self.assertTrue("snmpd" in (p.name() for p in process_iter())) + self.assertTrue(process_named_running(PROCESS_NAME)) def test_snmpv3_sha(self): @@ -113,7 +116,7 @@ class TestSNMPService(unittest.TestCase): # TODO: read in config file and check values # Check for running process - self.assertTrue("snmpd" in (p.name() for p in process_iter())) + self.assertTrue(process_named_running(PROCESS_NAME)) def test_snmpv3_md5(self): """ Check if SNMPv3 can be configured with MD5 authentication and service runs""" @@ -147,8 +150,7 @@ class TestSNMPService(unittest.TestCase): # TODO: read in config file and check values # Check for running process - self.assertTrue("snmpd" in (p.name() for p in process_iter())) - + self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': unittest.main() diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index 79850fe44..0cd00ccce 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -18,10 +18,12 @@ import re import os import unittest -from psutil import process_iter -from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError from vyos.util import read_file +from vyos.util import process_named_running +PROCESS_NAME = 'sshd' SSHD_CONF = '/run/ssh/sshd_config' base_path = ['service', 'ssh'] @@ -30,9 +32,6 @@ def get_config_value(key): tmp = re.findall(f'\n?{key}\s+(.*)', tmp) return tmp -def is_service_running(): - return 'sshd' in (p.name() for p in process_iter()) - class TestServiceSSH(unittest.TestCase): def setUp(self): self.session = ConfigSession(os.getpid()) @@ -62,7 +61,7 @@ class TestServiceSSH(unittest.TestCase): self.assertEqual('22', port) # Check for running process - self.assertTrue(is_service_running()) + self.assertTrue(process_named_running(PROCESS_NAME)) def test_ssh_single(self): """ Check if SSH service can be configured and runs """ @@ -101,7 +100,7 @@ class TestServiceSSH(unittest.TestCase): self.assertTrue("100" in keepalive) # Check for running process - self.assertTrue(is_service_running()) + self.assertTrue(process_named_running(PROCESS_NAME)) def test_ssh_multi(self): """ Check if SSH service can be configured and runs with multiple @@ -128,7 +127,7 @@ class TestServiceSSH(unittest.TestCase): self.assertIn(address, tmp) # Check for running process - self.assertTrue(is_service_running()) + self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': unittest.main() diff --git a/smoketest/scripts/cli/test_system_lcd.py b/smoketest/scripts/cli/test_system_lcd.py index 931a91c53..9385799b0 100755 --- a/smoketest/scripts/cli/test_system_lcd.py +++ b/smoketest/scripts/cli/test_system_lcd.py @@ -18,9 +18,10 @@ import os import unittest from configparser import ConfigParser -from psutil import process_iter from vyos.configsession import ConfigSession +from vyos.util import process_named_running +config_file = '/run/LCDd/LCDd.conf' base_path = ['system', 'lcd'] class TestSystemLCD(unittest.TestCase): @@ -42,13 +43,13 @@ class TestSystemLCD(unittest.TestCase): # load up ini-styled LCDd.conf conf = ConfigParser() - conf.read('/run/LCDd/LCDd.conf') + conf.read(config_file) self.assertEqual(conf['CFontzPacket']['Model'], '533') self.assertEqual(conf['CFontzPacket']['Device'], '/dev/ttyS1') - # both processes running - self.assertTrue('LCDd' in (p.name() for p in process_iter())) + # Check for running process + self.assertTrue(process_named_running('LCDd')) if __name__ == '__main__': unittest.main() diff --git a/smoketest/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py index 856a28916..2a7c64870 100755 --- a/smoketest/scripts/cli/test_system_ntp.py +++ b/smoketest/scripts/cli/test_system_ntp.py @@ -18,11 +18,14 @@ import re import os import unittest -from psutil import process_iter -from vyos.configsession import ConfigSession, ConfigSessionError -from vyos.template import vyos_address_from_cidr, vyos_netmask_from_cidr +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.template import vyos_address_from_cidr +from vyos.template import vyos_netmask_from_cidr from vyos.util import read_file +from vyos.util import process_named_running +PROCESS_NAME = 'ntpd' NTP_CONF = '/etc/ntp.conf' base_path = ['system', 'ntp'] @@ -63,7 +66,7 @@ class TestSystemNTP(unittest.TestCase): self.assertTrue(test in tmp) # Check for running process - self.assertTrue("ntpd" in (p.name() for p in process_iter())) + self.assertTrue(process_named_running(PROCESS_NAME)) def test_ntp_clients(self): """ Test the allowed-networks statement """ @@ -102,7 +105,7 @@ class TestSystemNTP(unittest.TestCase): self.assertEqual(tmp, test) # Check for running process - self.assertTrue("ntpd" in (p.name() for p in process_iter())) + self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': unittest.main() diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index d2b82d686..2ba6aabf9 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -14,13 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import re import os import unittest -from psutil import process_iter -from vyos.configsession import ConfigSession, ConfigSessionError -from vyos.util import read_file +from vyos.configsession import ConfigSession +from vyos.util import process_named_running OCSERV_CONF = '/run/ocserv/ocserv.conf' base_path = ['vpn', 'openconnect'] @@ -52,7 +50,7 @@ class TestVpnOpenconnect(unittest.TestCase): self.session.commit() # Check for running process - self.assertTrue("ocserv-main" in (p.name() for p in process_iter())) + self.assertTrue(process_named_running('ocserv-main')) if __name__ == '__main__': unittest.main() diff --git a/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py new file mode 100755 index 000000000..043567c4f --- /dev/null +++ b/smoketest/scripts/system/test_kernel_options.py @@ -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/>. + +import re +import platform +import unittest + +from vyos.util import read_file + +kernel = platform.release() +config = read_file(f'/boot/config-{kernel}') + +class TestKernelModules(unittest.TestCase): + """ VyOS makes use of a lot of Kernel drivers, modules and features. The + required modules which are essential for VyOS should be tested that they are + available in the Kernel that is run. """ + + def test_bond_interface(self): + """ The bond/lacp interface must be enabled in the OS Kernel """ + for option in ['CONFIG_BONDING']: + tmp = re.findall(f'{option}=(y|m)', config) + self.assertTrue(tmp) + + def test_bridge_interface(self): + """ The bridge interface must be enabled in the OS Kernel """ + for option in ['CONFIG_BRIDGE', + 'CONFIG_BRIDGE_IGMP_SNOOPING', + 'CONFIG_BRIDGE_VLAN_FILTERING']: + tmp = re.findall(f'{option}=(y|m)', config) + self.assertTrue(tmp) + + def test_qemu_support(self): + """ The bond/lacp interface must be enabled in the OS Kernel """ + for option in ['CONFIG_VIRTIO_BLK', 'CONFIG_SCSI_VIRTIO', + 'CONFIG_VIRTIO_NET', 'CONFIG_VIRTIO_CONSOLE', + 'CONFIG_VIRTIO', 'CONFIG_VIRTIO_PCI', + 'CONFIG_VIRTIO_BALLOON', 'CONFIG_CRYPTO_DEV_VIRTIO', + 'CONFIG_X86_PLATFORM_DEVICES']: + tmp = re.findall(f'{option}=(y|m)', config) + self.assertTrue(tmp) + + def test_vmware_support(self): + """ The bond/lacp interface must be enabled in the OS Kernel """ + for option in ['CONFIG_VMXNET3']: + tmp = re.findall(f'{option}=(y|m)', config) + self.assertTrue(tmp) + +if __name__ == '__main__': + unittest.main() + diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 53bc37882..5101c1e79 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -17,14 +17,17 @@ import os from sys import exit -from copy import deepcopy from vyos.config import Config +from vyos.configdict import dict_merge from vyos.hostsd_client import Client as hostsd_client -from vyos import ConfigError -from vyos.util import call, chown +from vyos.util import call +from vyos.util import chown +from vyos.util import vyos_dict_search from vyos.template import render +from vyos.xml import defaults +from vyos import ConfigError from vyos import airbag airbag.enable() @@ -35,136 +38,84 @@ pdns_rec_hostsd_lua_conf_file = f'{pdns_rec_run_dir}/recursor.vyos-hostsd.conf.l 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) +def get_config(config=None): + if config: + conf = config + else: + conf = Config() 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) + dns = 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) + dns = dict_merge(default_values, dns) - if conf.exists(['ignore-hosts-file']): - dns['export_hosts_file'] = "no" + # some additions to the default dictionary + if 'system' in dns: + base_nameservers = ['system', 'name-server'] + if conf.exists(base_nameservers): + dns.update({'system_name_server': conf.return_values(base_nameservers)}) - 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']) + base_nameservers_dhcp = ['system', 'name-servers-dhcp'] + if conf.exists(base_nameservers_dhcp): + dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_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): +def verify(dns): # bail out early - looks like removal from running config - if dns is None: + if not dns: 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 - conf.set_level([]) - 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")) + if 'listen_address' not in dns: + raise ConfigError('DNS forwarding requires a listen-address') + + if 'allow_from' not in dns: + raise ConfigError('DNS forwarding requires an allow-from network') + + # we can not use vyos_dict_search() when testing for domain servers + # as a domain will contains dot's which is out dictionary delimiter. + if 'domain' in dns: + for domain in dns['domain']: + if 'server' not in dns['domain'][domain]: + raise ConfigError(f'No server configured for domain {domain}!') + + if 'system' in dns: + if not ('system_name_server' in dns or 'system_name_server_dhcp' in dns): + print("Warning: No 'system name-server' or 'system " \ + "name-servers-dhcp' configured") return None def generate(dns): # bail out early - looks like removal from running config - if dns is None: + if not dns: return None render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.tmpl', - dns, user=pdns_rec_user, group=pdns_rec_group) + dns, trim_blocks=True, 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) + dns, trim_blocks=True, 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'): + for file in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]: + with open(file, 'a'): pass - chown(f, user=pdns_rec_user, group=pdns_rec_group) + chown(file, user=pdns_rec_user, group=pdns_rec_group) return None def apply(dns): - if dns is None: + if not dns: # DNS forwarding is removed in the commit - call("systemctl stop pdns-recursor.service") + call('systemctl stop pdns-recursor.service') + if os.path.isfile(pdns_rec_config_file): os.unlink(pdns_rec_config_file) else: @@ -174,8 +125,8 @@ def apply(dns): # 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']}) + if 'name_server' in dns: + hc.add_name_servers({hostsd_tag: dns['name_server']}) # delete all nameserver tags hc.delete_name_server_tags_recursor(hc.get_name_server_tags_recursor()) @@ -184,32 +135,33 @@ def apply(dns): # our own tag (static) hc.add_name_server_tags_recursor([hostsd_tag]) - if dns['system']: + if 'system' in dns: 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 ]) + if 'system_name_server_dhcp' in dns: + for interface in dns['system_name_server_dhcp']: + hc.add_name_server_tags_recursor(['dhcp-' + interface, + 'dhcpv6-' + interface ]) # 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']) + if 'domain' in dns: + hc.add_forward_zones(dns['domain']) # call hostsd to generate forward-zones and its lua-config-file hc.apply() ### finally (re)start pdns-recursor - call("systemctl restart pdns-recursor.service") + call('systemctl restart pdns-recursor.service') if __name__ == '__main__': try: - conf = Config() - c = get_config(conf) - verify(conf, c) + c = get_config() + verify(c) generate(c) apply(c) except ConfigError as e: diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index a9679b47c..9763620ac 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -22,15 +22,18 @@ 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.configdict import is_member +from vyos.configdict import is_source_interface 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_mtu_ipv6 from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.ifconfig import BondIf from vyos.ifconfig import Section -from vyos.validate import is_member +from vyos.util import vyos_dict_search from vyos.validate import has_address_configured from vyos import ConfigError from vyos import airbag @@ -98,18 +101,21 @@ def get_config(config=None): # also present the interfaces to be removed from the bond as dictionary bond['member'].update({'interface_remove': tmp}) - if 'member' in bond and 'interface' in bond['member']: + if vyos_dict_search('member.interface', bond): for interface, interface_config in bond['member']['interface'].items(): - # Check if we are a member of another bond device + # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') - if tmp: - interface_config.update({'is_bridge_member' : tmp}) + if tmp: interface_config.update({'is_bridge_member' : tmp}) - # Check if we are a member of a bond device + # Check if member interface is already member of a bond tmp = is_member(conf, interface, 'bonding') if tmp and tmp != bond['ifname']: interface_config.update({'is_bond_member' : tmp}) + # Check if member interface is used as source-interface on another interface + tmp = is_source_interface(conf, interface) + if tmp: interface_config.update({'is_source_interface' : tmp}) + # bond members must not have an assigned address tmp = has_address_configured(conf, interface) if tmp: interface_config.update({'has_address' : ''}) @@ -136,6 +142,7 @@ def verify(bond): raise ConfigError('Option primary - mode dependency failed, not' 'supported in mode {mode}!'.format(**bond)) + verify_mtu_ipv6(bond) verify_address(bond) verify_dhcpv6(bond) verify_vrf(bond) @@ -144,10 +151,9 @@ def verify(bond): 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 vyos_dict_search('member.interface', bond): + for interface, interface_config in bond['member']['interface'].items(): + error_msg = f'Can not add interface "{interface}" to bond, ' if interface == 'lo': raise ConfigError('Loopback interface "lo" can not be added to a bond') @@ -163,6 +169,10 @@ def verify(bond): tmp = interface_config['is_bond_member'] raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') + if 'is_source_interface' in interface_config: + tmp = interface_config['is_source_interface'] + raise ConfigError(error_msg + f'it is the source-interface of "{tmp}"!') + if 'has_address' in interface_config: raise ConfigError(error_msg + 'it has an address assigned!') diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 47c8c05f9..485decb17 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -22,13 +22,16 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import node_changed +from vyos.configdict import is_member +from vyos.configdict import is_source_interface 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.validate import has_address_configured from vyos.xml import defaults from vyos.util import cmd +from vyos.util import vyos_dict_search from vyos import ConfigError from vyos import airbag @@ -54,8 +57,8 @@ def get_config(config=None): 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: + if vyos_dict_search('member.interface', bridge): + # XXX: T2665: 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']: @@ -69,20 +72,22 @@ def get_config(config=None): 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 + # Check if member interface is already member of another bridge 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 + # Check if member interface is already member of a bond tmp = is_member(conf, interface, 'bonding') - if tmp: - interface_config.update({'is_bond_member' : tmp}) + if tmp: interface_config.update({'is_bond_member' : tmp}) + + # Check if member interface is used as source-interface on another interface + tmp = is_source_interface(conf, interface) + if tmp: interface_config.update({'is_source_interface' : tmp}) # Bridge members must not have an assigned address tmp = has_address_configured(conf, interface) - if tmp: - interface_config.update({'has_address' : ''}) + if tmp: interface_config.update({'has_address' : ''}) return bridge @@ -93,11 +98,9 @@ def verify(bridge): 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 vyos_dict_search('member.interface', bridge): + for interface, interface_config in bridge['member']['interface'].items(): + error_msg = f'Can not add interface "{interface}" to bridge, ' if interface == 'lo': raise ConfigError('Loopback interface "lo" can not be added to a bridge') @@ -113,6 +116,10 @@ def verify(bridge): tmp = interface_config['is_bond_member'] raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') + if 'is_source_interface' in interface_config: + tmp = interface_config['is_source_interface'] + raise ConfigError(error_msg + f'it is the source-interface of "{tmp}"!') + if 'has_address' in interface_config: raise ConfigError(error_msg + 'it has an address assigned!') diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index a8df64cce..1f622c003 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -20,11 +20,13 @@ 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_dhcpv6 +from vyos.configverify import verify_interface_exists +from vyos.configverify import verify_mtu +from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_vrf from vyos.ifconfig import EthernetIf from vyos import ConfigError from vyos import airbag @@ -41,6 +43,7 @@ def get_config(config=None): conf = Config() base = ['interfaces', 'ethernet'] ethernet = get_interface_dict(conf, base) + return ethernet def verify(ethernet): @@ -57,6 +60,8 @@ def verify(ethernet): if ethernet.get('speed', None) != 'auto': raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too') + verify_mtu(ethernet) + verify_mtu_ipv6(ethernet) verify_dhcpv6(ethernet) verify_address(ethernet) verify_vrf(ethernet) diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index cc2cf025a..979a5612e 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -17,12 +17,12 @@ 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_mtu_ipv6 from vyos.configverify import verify_bridge_delete from vyos.ifconfig import GeneveIf from vyos import ConfigError @@ -48,6 +48,7 @@ def verify(geneve): verify_bridge_delete(geneve) return None + verify_mtu_ipv6(geneve) verify_address(geneve) if 'remote' not in geneve: @@ -62,7 +63,6 @@ def verify(geneve): def generate(geneve): return None - def apply(geneve): # Check if GENEVE interface already exists if geneve['ifname'] in interfaces(): @@ -72,10 +72,11 @@ def apply(geneve): 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()) + # This is a special type of interface which needs additional parameters + # when created using iproute2. Instead of passing a ton of arguments, + # use a dictionary provided by the interface class which holds all the + # options necessary. + conf = GeneveIf.get_config() # Assign GENEVE instance configuration parameters to config dict conf['vni'] = geneve['vni'] diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 144cee5fe..1118143e4 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -17,7 +17,6 @@ import os from sys import exit -from copy import deepcopy from netifaces import interfaces from vyos.config import Config @@ -25,6 +24,7 @@ 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_mtu_ipv6 from vyos.ifconfig import L2TPv3If from vyos.util import check_kmod from vyos.validate import is_addr_assigned @@ -81,6 +81,7 @@ def verify(l2tpv3): raise ConfigError('L2TPv3 local-ip address ' '"{local_ip}" is not configured!'.format(**l2tpv3)) + verify_mtu_ipv6(l2tpv3) verify_address(l2tpv3) return None @@ -88,10 +89,11 @@ 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()) + # This is a special type of interface which needs additional parameters + # when created using iproute2. Instead of passing a ton of arguments, + # use a dictionary provided by the interface class which holds all the + # options necessary. + conf = L2TPv3If.get_config() # Check if L2TPv3 interface already exists if l2tpv3['ifname'] in interfaces(): diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 2866ccc0a..0a20a121b 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -16,17 +16,18 @@ 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.ifconfig import Interface 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_mtu_ipv6 from vyos.configverify import verify_source_interface from vyos import ConfigError from vyos import airbag @@ -47,6 +48,14 @@ def get_config(config=None): base = ['interfaces', 'macsec'] macsec = get_interface_dict(conf, base) + # MACsec is "special" the default MTU is 1460 - 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: + # base MTU for MACsec is 1468 bytes, but we leave room for 802.1ad and + # 802.1q VLAN tags, thus the limit is 1460 bytes. + macsec['mtu'] = '1460' + # Check if interface has been removed if 'deleted' in macsec: source_interface = conf.return_effective_value( @@ -63,6 +72,7 @@ def verify(macsec): verify_source_interface(macsec) verify_vrf(macsec) + verify_mtu_ipv6(macsec) verify_address(macsec) if not (('security' in macsec) and @@ -80,6 +90,15 @@ def verify(macsec): raise ConfigError('Missing mandatory MACsec security ' 'keys as encryption is enabled!') + if 'source_interface' in macsec: + # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad + # and 802.1q) - we need to check the underlaying MTU if our configured + # MTU is at least 40 bytes less then the MTU of our physical interface. + lower_mtu = Interface(macsec['source_interface']).get_mtu() + if lower_mtu < (int(macsec['mtu']) + 40): + raise ConfigError('MACsec overhead does not fit into underlaying device MTU,\n' \ + f'{underlay_mtu} bytes is too small!') + return None @@ -102,12 +121,11 @@ def apply(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()) + # This is a special type of interface which needs additional parameters + # when created using iproute2. Instead of passing a ton of arguments, + # use a dictionary provided by the interface class which holds all the + # options necessary. + conf = MACsecIf.get_config() conf['source_interface'] = macsec['source_interface'] conf['security_cipher'] = macsec['security']['cipher'] diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 958b305dd..518dbdc0e 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -26,10 +26,11 @@ from shutil import rmtree from vyos.config import Config from vyos.configdict import list_diff +from vyos.configdict import is_member 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.validate import is_addr_assigned, is_ipv4 from vyos import ConfigError from vyos import airbag @@ -256,7 +257,10 @@ def get_config(config=None): if conf.exists('encryption ncp-ciphers'): _ncp_ciphers = [] for enc in conf.return_values('encryption ncp-ciphers'): - if enc == 'des': + if enc == 'none': + _ncp_ciphers.append('none') + _ncp_ciphers.append('NONE') + elif enc == 'des': _ncp_ciphers.append('des-cbc') _ncp_ciphers.append('DES-CBC') elif enc == '3des': @@ -943,6 +947,9 @@ def verify(openvpn): else: print('Diffie-Hellman prime file is unspecified, assuming ECDH') + if openvpn['encryption'] == 'none': + print('Warning: "encryption none" was specified. NO encryption will be performed and tunnelled data WILL be transmitted in clear text over the network!') + # # Auth user/pass # diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 1b4b9e4ee..ee3b142c8 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -24,6 +24,7 @@ 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.configverify import verify_mtu_ipv6 from vyos.template import render from vyos.util import call from vyos import ConfigError @@ -57,6 +58,7 @@ def verify(pppoe): verify_source_interface(pppoe) verify_vrf(pppoe) + verify_mtu_ipv6(pppoe) if {'connect_on_demand', 'vrf'} <= set(pppoe): raise ConfigError('On-demand dialing and VRF can not be used at the same time') diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 59edca1cc..ddbef56ac 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -14,9 +14,6 @@ # 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 @@ -28,7 +25,6 @@ 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 @@ -47,19 +43,7 @@ def get_config(config=None): 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}) + if mode: peth.update({'mode_old' : mode}) return peth @@ -72,16 +56,6 @@ def verify(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 @@ -101,9 +75,11 @@ def apply(peth): 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()) + # This is a special type of interface which needs additional parameters + # when created using iproute2. Instead of passing a ton of arguments, + # use a dictionary provided by the interface class which holds all the + # options necessary. + conf = MACVLANIf.get_config() # Assign MACVLAN instance configuration parameters to config dict conf['source_interface'] = peth['source_interface'] diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 11d8d6edc..f1d885b15 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -22,10 +22,11 @@ from copy import deepcopy from netifaces import interfaces from vyos.config import Config +from vyos.configdict import is_member 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.validate import is_ipv4, is_ipv6 from vyos import ConfigError from vyos.dicts import FixedDict @@ -170,8 +171,8 @@ class ConfigurationState(object): """ >>> 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 + 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' @@ -263,7 +264,7 @@ class ConfigurationState(object): d = d[lpath[-1]] # XXX: it should have provided me the content and not the key self._conf.set_level(l) - return d + return d def to_api(self): """ diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index bea3aa25b..002f40aef 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -17,13 +17,13 @@ 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_mtu_ipv6 from vyos.configverify import verify_source_interface from vyos.ifconfig import VXLANIf, Interface from vyos import ConfigError @@ -73,11 +73,12 @@ def verify(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): + lower_mtu = Interface(vxlan['source_interface']).get_mtu() + if lower_mtu < (int(vxlan['mtu']) + 50): raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ f'MTU is to small ({underlay_mtu} bytes)') + verify_mtu_ipv6(vxlan) verify_address(vxlan) return None @@ -95,10 +96,11 @@ def apply(vxlan): 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()) + # This is a special type of interface which needs additional parameters + # when created using iproute2. Instead of passing a ton of arguments, + # use a dictionary provided by the interface class which holds all the + # options necessary. + conf = VXLANIf.get_config() # Assign VXLAN instance configuration parameters to config dict for tmp in ['vni', 'group', 'source_address', 'source_interface', 'remote', 'port']: diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index e7c22da1a..d5800264f 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -27,6 +27,7 @@ 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_mtu_ipv6 from vyos.ifconfig import WireGuardIf from vyos.util import check_kmod from vyos import ConfigError @@ -71,6 +72,7 @@ def verify(wireguard): verify_bridge_delete(wireguard) return None + verify_mtu_ipv6(wireguard) verify_address(wireguard) verify_vrf(wireguard) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index c6c843e7b..f8520aecf 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -18,7 +18,6 @@ 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 @@ -233,13 +232,15 @@ def apply(wifi): 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()) + # This is a special type of interface which needs additional parameters + # when created using iproute2. Instead of passing a ton of arguments, + # use a dictionary provided by the interface class which holds all the + # options necessary. + conf = WiFiIf.get_config() # Assign WiFi instance configuration parameters to config dict conf['phy'] = wifi['physical_device'] + conf['wds'] = 'on' if 'wds' in wifi else 'off' # Finally create the new interface w = WiFiIf(interface, **conf) diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index e9806ef47..117bf0274 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -22,7 +22,7 @@ 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.util import call, chmod_755 from vyos.validate import is_ipv4, is_addr_assigned from vyos.version import get_version_data from vyos import ConfigError, airbag diff --git a/src/migration-scripts/interfaces/12-to-13 b/src/migration-scripts/interfaces/12-to-13 new file mode 100755 index 000000000..f866ca9a6 --- /dev/null +++ b/src/migration-scripts/interfaces/12-to-13 @@ -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/>. + +# - T2903: Change vif-s ethertype from numeric number to literal +# - 0x88a8 -> 802.1ad +# - 0x8100 -> 802.1q +# - T2905: Change WWAN "ondemand" node to "connect-on-demand" to have identical +# CLI nodes for both types of dialer interfaces + +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) + + # + # T2903 + # + for type in config.list_nodes(['interfaces']): + for interface in config.list_nodes(['interfaces', type]): + if not config.exists(['interfaces', type, interface, 'vif-s']): + continue + + for vif_s in config.list_nodes(['interfaces', type, interface, 'vif-s']): + base_path = ['interfaces', type, interface, 'vif-s', vif_s] + if config.exists(base_path + ['ethertype']): + protocol = '802.1ad' + tmp = config.return_value(base_path + ['ethertype']) + if tmp == '0x8100': + protocol = '802.1q' + + config.set(base_path + ['protocol'], value=protocol) + config.delete(base_path + ['ethertype']) + + # + # T2905 + # + wwan_base = ['interfaces', 'wirelessmodem'] + if config.exists(wwan_base): + for interface in config.list_nodes(wwan_base): + if config.exists(wwan_base + [interface, 'ondemand']): + config.rename(wwan_base + [interface, 'ondemand'], 'connect-on-demand') + + 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/system/10-to-11 b/src/migration-scripts/system/10-to-11 index 1a0233c7d..3c49f0d95 100755 --- a/src/migration-scripts/system/10-to-11 +++ b/src/migration-scripts/system/10-to-11 @@ -1,9 +1,7 @@ #!/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. +# 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 @@ -18,54 +16,21 @@ 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 +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: - # - # 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']) + 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: - with open(file_name, 'w') as f: - f.write(config.to_string()) + 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/11-to-12 b/src/migration-scripts/system/11-to-12 index 0c92a0746..1a0233c7d 100755 --- a/src/migration-scripts/system/11-to-12 +++ b/src/migration-scripts/system/11-to-12 @@ -1,47 +1,71 @@ #!/usr/bin/env python3 -# converts 'set system syslog host <address>:<port>' -# to 'set system syslog host <address> port <port>' +# 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 -import re - from vyos.configtree import ConfigTree if (len(sys.argv) < 1): - print("Must specify file name!") - sys.exit(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_file = f.read() config = ConfigTree(config_file) -cbase = ['system', 'syslog', 'host'] - -if not config.exists(cbase): +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) -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]) + # delete top level tag node + if config.exists(cfg_base + ['radius-server']): + config.delete(cfg_base + ['radius-server']) try: - open(file_name,'w').write(config.to_string()) + with open(file_name, 'w') as f: + f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) sys.exit(1) diff --git a/src/migration-scripts/system/12-to-13 b/src/migration-scripts/system/12-to-13 index 5b068f4fc..36311a19d 100755 --- a/src/migration-scripts/system/12-to-13 +++ b/src/migration-scripts/system/12-to-13 @@ -1,70 +1,47 @@ #!/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! +# converts 'set system syslog host <address>:<port>' +# to 'set system syslog host <address> port <port>' -import re import sys +import re from vyos.configtree import ConfigTree -from vyos.util import cmd - if (len(sys.argv) < 1): - print("Must specify file name!") - sys.exit(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_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) +cbase = ['system', 'syslog', 'host'] - # 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) +if not config.exists(cbase): + 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) +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/13-to-14 b/src/migration-scripts/system/13-to-14 index c055dad1f..5b068f4fc 100755 --- a/src/migration-scripts/system/13-to-14 +++ b/src/migration-scripts/system/13-to-14 @@ -1,15 +1,19 @@ #!/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! # -# 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. +# Migrate all configured timezones to real IANA assigned timezones! -import os +import re import sys -ipv6_blacklist_file = '/etc/modprobe.d/vyatta_blacklist_ipv6.conf' - from vyos.configtree import ConfigTree +from vyos.util import cmd + if (len(sys.argv) < 1): print("Must specify file name!") @@ -21,16 +25,42 @@ 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): +tz_base = ['system', 'time-zone'] +if not config.exists(tz_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) + 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: diff --git a/src/migration-scripts/system/14-to-15 b/src/migration-scripts/system/14-to-15 index 2491e3d0d..c055dad1f 100755 --- a/src/migration-scripts/system/14-to-15 +++ b/src/migration-scripts/system/14-to-15 @@ -1,10 +1,14 @@ #!/usr/bin/env python3 # -# Make 'system options reboot-on-panic' valueless +# 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): @@ -17,17 +21,16 @@ with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) -base = ['system', 'options'] -if not config.exists(base): +ip_base = ['system', 'ipv6'] +if not config.exists(ip_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']) + # 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: diff --git a/src/migration-scripts/system/15-to-16 b/src/migration-scripts/system/15-to-16 index e70893d55..2491e3d0d 100755 --- a/src/migration-scripts/system/15-to-16 +++ b/src/migration-scripts/system/15-to-16 @@ -1,24 +1,6 @@ #!/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? +# Make 'system options reboot-on-panic' valueless import os import sys @@ -35,17 +17,17 @@ with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) -base = ['system', 'login', 'user'] +base = ['system', 'options'] 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']) + 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: diff --git a/src/migration-scripts/system/16-to-17 b/src/migration-scripts/system/16-to-17 index 8f762c0e2..e70893d55 100755 --- a/src/migration-scripts/system/16-to-17 +++ b/src/migration-scripts/system/16-to-17 @@ -14,8 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# remove "system console netconsole" -# remove "system console device <device> modem" +# * 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 @@ -32,41 +35,17 @@ with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) -base = ['system', 'console'] +base = ['system', 'login', 'user'] 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']) + for user in config.list_nodes(base): + if config.exists(base + [user, 'group']): + config.delete(base + [user, 'group']) - 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) + if config.exists(base + [user, 'level']): + config.delete(base + [user, 'level']) try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/system/17-to-18 b/src/migration-scripts/system/17-to-18 index dd2abce00..8f762c0e2 100755 --- a/src/migration-scripts/system/17-to-18 +++ b/src/migration-scripts/system/17-to-18 @@ -13,93 +13,64 @@ # # 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 +# remove "system console netconsole" +# remove "system console device <device> modem" + +import os +import sys -from sys import argv, exit -from vyos.ifconfig import Interface from vyos.configtree import ConfigTree -if (len(argv) < 1): +if (len(sys.argv) < 1): print("Must specify file name!") - exit(1) + sys.exit(1) -file_name = argv[1] +file_name = sys.argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) - -base = ['system'] +base = ['system', 'console'] if not config.exists(base): # Nothing to do - exit(0) - -if config.exists(base + ['disable-dhcp-nameservers']): - config.delete(base + ['disable-dhcp-nameservers']) + sys.exit(0) 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) + # 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/18-to-19 b/src/migration-scripts/system/18-to-19 new file mode 100755 index 000000000..dd2abce00 --- /dev/null +++ b/src/migration-scripts/system/18-to-19 @@ -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/9-to-10 b/src/migration-scripts/system/9-to-10 deleted file mode 100755 index 3c49f0d95..000000000 --- a/src/migration-scripts/system/9-to-10 +++ /dev/null @@ -1,36 +0,0 @@ -#!/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/services/vyos-hostsd b/src/services/vyos-hostsd index 0079f7e5c..59dbeda17 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -107,16 +107,17 @@ # ### 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. +## If recursion_desired is true, '+' will be prepended to the zone line. +## If addnta is true, a NTA (Negative Trust Anchor) will be added via +## lua-config-file. # # { 'type': 'forward_zones', # 'op': 'add', # 'data': { # '<str zone>': { -# 'nslist': ['<str nameserver>', ...], -# 'addNTA': <bool>, -# 'recursion-desired': <bool> +# 'server': ['<str nameserver>', ...], +# 'addnta': <bool>, +# 'recursion_desired': <bool> # } # ... # } @@ -305,12 +306,12 @@ tag_regex_schema = op_type_schema.extend({ forward_zone_add_schema = op_type_schema.extend({ 'data': { str: { - 'nslist': [str], - 'addNTA': bool, - 'recursion-desired': bool + 'server': [str], + 'addnta': Any({}, None), + 'recursion_desired': Any({}, None), } } - }, required=True) + }, required=False) hosts_add_schema = op_type_schema.extend({ 'data': { @@ -586,7 +587,7 @@ if __name__ == '__main__': 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) diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client index 48ebc83f7..d4d38315a 100755 --- a/src/utils/vyos-hostsd-client +++ b/src/utils/vyos-hostsd-client @@ -99,9 +99,9 @@ try: 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 + 'server': args.nameservers, + 'addnta': args.addnta, + 'recursion_desired': args.recursion_desired } }) elif args.delete_forward_zones: |