diff options
34 files changed, 1408 insertions, 2847 deletions
| diff --git a/data/templates/wifi/cfg80211.conf.tmpl b/data/templates/wifi/cfg80211.conf.tmpl index b21bacc1e..91df57aab 100644 --- a/data/templates/wifi/cfg80211.conf.tmpl +++ b/data/templates/wifi/cfg80211.conf.tmpl @@ -1,3 +1 @@ -{%- if regdom -%} -options cfg80211 ieee80211_regdom={{ regdom }} -{% endif %} +{{ 'options cfg80211 ieee80211_regdom=' + regdom if regdom is defined }} diff --git a/data/templates/wifi/crda.tmpl b/data/templates/wifi/crda.tmpl index 750ad86ee..6cd125e37 100644 --- a/data/templates/wifi/crda.tmpl +++ b/data/templates/wifi/crda.tmpl @@ -1,3 +1 @@ -{%- if regdom -%} -REGDOMAIN={{ regdom }} -{% endif %} +{{ 'REGDOMAIN=' + regdom if regdom is defined }} diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl index d6068e4db..765668c57 100644 --- a/data/templates/wifi/hostapd.conf.tmpl +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -9,7 +9,7 @@ device_name={{ description | truncate(32, True) }}  # management frames with the Host AP driver); wlan0 with many nl80211 drivers  # Note: This attribute can be overridden by the values supplied with the '-i'  # command line parameter. -interface={{ intf }} +interface={{ ifname }}  # Driver interface type (hostap/wired/none/nl80211/bsd);  # default: hostap). nl80211 is used with all Linux mac80211 drivers. @@ -28,8 +28,7 @@ logger_syslog_level=0  logger_stdout=-1  logger_stdout_level=0 -{%- if country_code %} - +{% if country_code %}  # Country code (ISO/IEC 3166-1). Used to set regulatory domain.  # Set as needed to indicate country in which device is operating.  # This can limit available channels and transmit power. @@ -42,14 +41,12 @@ country_code={{ country_code }}  ieee80211d=1  {% endif %} -{%- if ssid %} - +{% if ssid %}  # SSID to be used in IEEE 802.11 management frames  ssid={{ ssid }}  {% endif %} -{%- if channel %} - +{% if channel %}  # Channel number (IEEE 802.11)  # (default: 0, i.e., not set)  # Please note that some drivers do not use this value from hostapd and the @@ -61,8 +58,7 @@ ssid={{ ssid }}  channel={{ channel }}  {% endif %} -{%- if mode %} - +{% if mode %}  # Operation mode (a = IEEE 802.11a (5 GHz), b = IEEE 802.11b (2.4 GHz),  # g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used  # with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this @@ -71,29 +67,30 @@ channel={{ channel }}  # special value "any" can be used to indicate that any support band can be used.  # This special case is currently supported only with drivers with which  # offloaded ACS is used. -{% if 'n' in mode -%} +{%   if 'n' in mode %}  hw_mode=g -{% elif 'ac' in mode -%} +{%   elif 'ac' in mode %}  hw_mode=a  ieee80211h=1  ieee80211ac=1 -{% else -%} +{%   else %}  hw_mode={{ mode }} -{% endif %} +{%   endif %}  {% endif %}  # ieee80211w: Whether management frame protection (MFP) is enabled  # 0 = disabled (default)  # 1 = optional  # 2 = required -{% if 'disabled' in mgmt_frame_protection -%} +{% if 'disabled' in mgmt_frame_protection %}  ieee80211w=0 -{% elif 'optional' in mgmt_frame_protection -%} +{% elif 'optional' in mgmt_frame_protection %}  ieee80211w=1 -{% elif 'required' in mgmt_frame_protection -%} +{% elif 'required' in mgmt_frame_protection %}  ieee80211w=2  {% endif %} +{% if capabilities is defined and capabilities.ht is defined %}  # ht_capab: HT capabilities (list of flags)  # LDPC coding capability: [LDPC] = supported  # Supported channel width set: [HT40-] = both 20 MHz and 40 MHz with secondary @@ -127,79 +124,50 @@ ieee80211w=2  # DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set)  # 40 MHz intolerant [40-INTOLERANT] (not advertised if not set)  # L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set) -{% if cap_ht %} -ht_capab= -{%- endif -%} - -{%- if cap_ht_40mhz_incapable -%} -[40-INTOLERANT] -{%- endif -%} - -{%- if cap_ht_delayed_block_ack -%} -[DELAYED-BA] -{%- endif -%} - -{%- if cap_ht_dsss_cck_40 -%} -[DSSS_CCK-40] -{%- endif -%} - -{%- if cap_ht_greenfield -%} -[GF] -{%- endif -%} - -{%- if cap_ht_ldpc -%} -[LDPC] -{%- endif -%} - -{%- if cap_ht_lsig_protection -%} -[LSIG-TXOP-PROT] -{%- endif -%} - -{%- if cap_ht_max_amsdu -%} -[MAX-AMSDU-{{ cap_ht_max_amsdu }}] -{%- endif -%} - -{%- if cap_ht_smps -%} -[SMPS-{{ cap_ht_smps | upper }}] -{%- endif -%} - -{%- if cap_ht_chan_set_width -%} -{%- for csw in cap_ht_chan_set_width -%} -[{{ csw | upper }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_ht_short_gi -%} -{%- for gi in cap_ht_short_gi -%} -[SHORT-GI-{{ gi }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_ht_stbc_tx -%} -[TX-STBC] -{%- endif -%} -{%- if cap_ht_stbc_rx -%} -[RX-STBC{{ cap_ht_stbc_rx }}] -{%- endif %} +{%   set output = '' %} +{%   set output = output + '[40-INTOLERANT]'  if capabilities.ht.fourtymhz_incapable is defined else '' %} +{%   set output = output + '[DELAYED-BA]'     if capabilities.ht.delayed_block_ack   is defined else '' %} +{%   set output = output + '[DSSS_CCK-40]'    if capabilities.ht.dsss_cck_40         is defined else '' %} +{%   set output = output + '[GF]'             if capabilities.ht.greenfield          is defined else '' %} +{%   set output = output + '[LDPC]'           if capabilities.ht.ldpc                is defined else '' %} +{%   set output = output + '[LSIG-TXOP-PROT]' if capabilities.ht.lsig_protection     is defined else '' %} +{%   set output = output + '[TX-STBC]'        if capabilities.ht.stbc.tx             is defined else '' %} +{%   set output = output + '[RX-STBC-'   + capabilities.ht.stbc.rx | upper + ']' if capabilities.ht.stbc.tx   is defined else '' %} +{%   set output = output + '[MAX-AMSDU-' + capabilities.ht.max_amsdu       + ']' if capabilities.ht.max_amsdu is defined else '' %} +{%   set output = output + '[SMPS-'      + capabilities.ht.smps | upper    + ']' if capabilities.ht.smps      is defined else '' %} + +{% if capabilities.ht.channel_set_width is defined %} +{%   for csw in capabilities.ht.channel_set_width %} +{%     set output = output + '[' + csw | upper + ']'  %} +{%   endfor %} +{% endif %} -# Required for full HT and VHT functionality -wme_enabled=1 +{% if capabilities.ht.short_gi is defined %} +{%   for short_gi in capabilities.ht.short_gi %} +{%     set output = output + '[SHORT-GI-' + short_gi | upper + ']'  %} +{%   endfor %} +{% endif %} -{% if cap_ht_powersave -%} +ht_capab={{ output }} + +{% if capabilities.ht.auto_powersave is defined %}  # WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD]  # Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver)  uapsd_advertisement_enabled=1 -{%- endif %} +{% endif %} + +{% endif %} + +# Required for full HT and VHT functionality +wme_enabled=1 -{% if cap_req_ht -%} + +{% if capabilities is defined and capabilities.require_ht is defined %}  # Require stations to support HT PHY (reject association if they do not)  require_ht=1  {% endif %} -{%- if cap_vht_chan_set_width -%} -vht_oper_chwidth={{ cap_vht_chan_set_width }} -{%- endif %} - +{% if capabilities is defined and capabilities.vht is defined %}  # vht_capab: VHT capabilities (list of flags)  #  # vht_max_mpdu_len: [MAX-MPDU-7991] [MAX-MPDU-11454] @@ -316,133 +284,95 @@ vht_oper_chwidth={{ cap_vht_chan_set_width }}  # Tx Antenna Pattern Consistency: [TX-ANTENNA-PATTERN]  # Indicates the possibility of Tx antenna pattern change  # 0 = Tx antenna pattern might change during the lifetime of an association -# 1 = Tx antenna pattern does not change during the lifetime of an association -{% if cap_vht %} -vht_capab= -{%- endif -%} - -{%- if cap_vht_max_mpdu -%} -[MAX-MPDU-{{ cap_vht_max_mpdu }}] -{%- endif -%} - -{%- if cap_vht_max_mpdu_exp -%} -[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}] -{%- endif -%} - -{%- if cap_vht_chan_set_width -%} -{%- if '2' in cap_vht_chan_set_width -%} -[VHT160] -{%- elif '3' in cap_vht_chan_set_width -%} -[VHT160-80PLUS80] -{%- endif -%} -{%- endif -%} - -{%- if cap_vht_stbc_tx -%} -[TX-STBC-2BY1] -{%- endif -%} - -{%- if cap_vht_stbc_rx -%} -[RX-STBC-{{ cap_vht_stbc_rx }}] -{%- endif -%} - -{%- if cap_vht_link_adaptation -%} -{%- if 'unsolicited' in cap_vht_link_adaptation -%} -[VHT-LINK-ADAPT2] -{%- elif 'both' in cap_vht_link_adaptation -%} -[VHT-LINK-ADAPT3] -{%- endif -%} -{%- endif -%} - -{%- if cap_vht_short_gi -%} -{%- for gi in cap_vht_short_gi -%} -[SHORT-GI-{{ gi }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_vht_ldpc -%} -[RXLDPC] -{%- endif -%} - -{%- if cap_vht_tx_powersave -%} -[VHT-TXOP-PS] -{%- endif -%} - -{%- if cap_vht_vht_cf -%} -[HTC-VHT] -{%- endif -%} - -{%- if cap_vht_beamform -%} -{%- for beamform in cap_vht_beamform -%} -{%- if 'single-user-beamformer' in beamform -%} -[SU-BEAMFORMER] -{%- elif 'single-user-beamformee' in beamform -%} -[SU-BEAMFORMEE] -{%- elif 'multi-user-beamformer' in beamform -%} -[MU-BEAMFORMER] -{%- elif 'multi-user-beamformee' in beamform -%} -[MU-BEAMFORMEE] -{%- endif -%} -{%- endfor -%} -{%- endif -%} - -{%- if cap_vht_antenna_fixed -%} -[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN] -{%- endif -%} - -{%- if cap_vht_antenna_cnt -%} -{%- if cap_vht_antenna_cnt|int > 1 -%} -{%- if cap_vht_beamform -%} -{%- for beamform in cap_vht_beamform -%} -{%- if 'single-user-beamformer' in beamform -%} -{%- if cap_vht_antenna_cnt|int < 6 -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt|int -1 }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt|int -1}}] -{%- endif -%} -{%- else -%} -{%- if cap_vht_antenna_cnt|int < 5 -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] -{%- endif -%} -{%- endif -%} -{%- endfor -%} -{%- else -%} -{%- if cap_vht_antenna_cnt|int < 5 -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] -{%- endif -%} -{%- endif -%} -{%- endif -%} -{%- endif %} +# 1 = Tx antenna pattern does not change during the lifetime of an + +{%   if capabilities.vht.center_channel_freq.freq_1 is defined %} +# center freq = 5 GHz + (5 * index) +# So index 42 gives center freq 5.210 GHz +# which is channel 42 in 5G band +vht_oper_centr_freq_seg0_idx={{ capabilities.vht.center_channel_freq.freq_1 }} +{%   endif %} + +{%   if capabilities.vht.center_channel_freq.freq_2 is defined %} +# center freq = 5 GHz + (5 * index) +# So index 159 gives center freq 5.795 GHz +# which is channel 159 in 5G band +vht_oper_centr_freq_seg1_idx={{ capabilities.vht.center_channel_freq.freq_2 }} +{%   endif %} + +{%   if capabilities.vht.channel_set_width is defined %} +vht_oper_chwidth={{ capabilities.vht.channel_set_width }} +{%   endif %} + +{%   set output = '' %} +{%   set output = output + '[TX-STBC-2BY1]'       if capabilities.vht.stbc.tx               is defined else '' %} +{%   set output = output + '[RXLDPC]'             if capabilities.vht.ldpc                  is defined else '' %} +{%   set output = output + '[VHT-TXOP-PS]'        if capabilities.vht.tx_powersave          is defined else '' %} +{%   set output = output + '[HTC-VHT]'            if capabilities.vht.vht_cf                is defined else '' %} +{%   set output = output + '[RX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %} +{%   set output = output + '[TX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %} + +{%   set output = output + '[RX-STBC-'            + capabilities.vht.stbc.rx      + ']' if capabilities.vht.stbc.rx      is defined else '' %} +{%   set output = output + '[MAX-MPDU-'           + capabilities.vht.max_mpdu     + ']' if capabilities.vht.max_mpdu     is defined else '' %} +{%   set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %} +{%   set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %} + +{%   set output = output + '[VHT160]'          if capabilities.vht.max_mpdu_exp    is defined and capabilities.vht.max_mpdu_exp == '2'              else '' %} +{%   set output = output + '[VHT160-80PLUS80]' if capabilities.vht.max_mpdu_exp    is defined and capabilities.vht.max_mpdu_exp == '3'              else '' %} +{%   set output = output + '[VHT-LINK-ADAPT2]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'unsolicited' else '' %} +{%   set output = output + '[VHT-LINK-ADAPT3]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'both'        else '' %} + +{%   if capabilities.vht.short_gi is defined %} +{%     for short_gi in capabilities.vht.short_gi %} +{%       set output = output + '[SHORT-GI-' + short_gi | upper + ']'  %} +{%     endfor %} +{%   endif %} + +{%   if capabilities.vht.beamform %} +{%     for beamform in capabilities.vht.beamform %} +{%       set output = output + '[SU-BEAMFORMER]' if beamform == 'single-user-beamformer' else '' %} +{%       set output = output + '[SU-BEAMFORMEE]' if beamform == 'single-user-beamformee' else '' %} +{%       set output = output + '[MU-BEAMFORMER]' if beamform == 'multi-user-beamformer'  else '' %} +{%       set output = output + '[MU-BEAMFORMEE]' if beamform == 'multi-user-beamformee'  else '' %} +{%     endfor %} +{%   endif %} + +{%   if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1  %} +{%     if capabilities.vht.beamform %} +{%       if beamform == 'single-user-beamformer' %} +{%         if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 6  %} +{%           set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count|int -1 + ']' %} +{%           set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count|int -1 + ']' %} +{%         endif %} +{%       endif %} +{%       if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 5  %} +{%         set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count + ']' %} +{%         set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count+ ']' %} +{%       endif %} +{%     endif %} +{%   endif %} + +vht_capab={{ output }} +{% endif %}  # ieee80211n: Whether IEEE 802.11n (HT) is enabled  # 0 = disabled (default)  # 1 = enabled  # Note: You will also need to enable WMM for full HT functionality.  # Note: hw_mode=g (2.4 GHz) and hw_mode=a (5 GHz) is used to specify the band. -{% if cap_req_vht -%} +{% if capabilities is defined and capabilities.require_vht is defined %}  ieee80211n=0  # Require stations to support VHT PHY (reject association if they do not)  require_vht=1 -{% else -%} -{% if 'n' in mode or 'ac' in mode -%} +{% else %} +{%   if 'n' in mode or 'ac' in mode %}  ieee80211n=1 -{% else -%} +{%   else %}  ieee80211n=0 -{%- endif %} +{%   endif %}  {% endif %} -{% if cap_vht_center_freq_1 -%} -# center freq = 5 GHz + (5 * index) -# So index 42 gives center freq 5.210 GHz -# which is channel 42 in 5G band -vht_oper_centr_freq_seg0_idx={{ cap_vht_center_freq_1 }} -{% endif %} - -{% if cap_vht_center_freq_2 -%} -# center freq = 5 GHz + (5 * index) -# So index 159 gives center freq 5.795 GHz -# which is channel 159 in 5G band -vht_oper_centr_freq_seg1_idx={{ cap_vht_center_freq_2 }} -{% endif %} - -{% if disable_broadcast_ssid -%} +{% if disable_broadcast_ssid is defined %}  # Send empty SSID in beacons and ignore probe request frames that do not  # specify full SSID, i.e., require stations to know SSID.  # default: disabled (0) @@ -463,7 +393,7 @@ ignore_broadcast_ssid=1  # 2 = use external RADIUS server (accept/deny lists are searched first)  macaddr_acl=0 -{% if max_stations -%} +{% if max_stations is defined %}  # Maximum number of stations allowed in station table. New stations will be  # rejected after the station table is full. IEEE 802.11 has a limit of 2007  # different association IDs, so this number should not be larger than that. @@ -471,13 +401,13 @@ macaddr_acl=0  max_num_sta={{ max_stations }}  {% endif %} -{% if isolate_stations -%} +{% if isolate_stations is defined %}  # Client isolation can be used to prevent low-level bridging of frames between  # associated stations in the BSS. By default, this bridging is allowed.  ap_isolate=1  {% endif %} -{% if reduce_transmit_power -%} +{% if reduce_transmit_power is defined %}  # Add Power Constraint element to Beacon and Probe Response frames  # This config option adds Power Constraint element when applicable and Country  # element is added. Power Constraint element is required by Transmit Power @@ -486,14 +416,15 @@ ap_isolate=1  local_pwr_constraint={{ reduce_transmit_power }}  {% endif %} -{% if expunge_failing_stations -%} +{% if expunge_failing_stations is defined %}  # Disassociate stations based on excessive transmission failures or other  # indications of connection loss. This depends on the driver capabilities and  # may not be available with all drivers.  disassoc_low_ack=1  {% endif %} -{% if sec_wep -%} + +{% if security is defined and security.wep is defined %}  # IEEE 802.11 specifies two authentication algorithms. hostapd can be  # configured to allow both of these or only one. Open system authentication  # should be used with IEEE 802.1X. @@ -522,13 +453,14 @@ wep_default_key=0  # digits, depending on whether 40-bit (64-bit), 104-bit (128-bit), or  # 128-bit (152-bit) WEP is used.  # Only the default key must be supplied; the others are optional. -{% if sec_wep_key -%} -{% for key in sec_wep_key -%} -wep_key{{ loop.index -1 }}={{ key}} -{% endfor %} -{%- endif %} +{%   if security.wep.key is defined %} +{%     for key in sec_wep_key %} +wep_key{{ loop.index -1 }}={{ security.wep.key }} +{%     endfor %} +{%   endif %} -{% elif sec_wpa -%} + +{% elif security is defined and security.wpa is defined %}  ##### WPA/IEEE 802.11i configuration ##########################################  # Enable WPA. Setting this variable configures the AP to require WPA (either @@ -542,15 +474,17 @@ wep_key{{ loop.index -1 }}={{ key}}  # and/or WPA2 (full IEEE 802.11i/RSN):  # bit0 = WPA  # bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled) -{% if 'both' in sec_wpa_mode -%} +{%   if security.wpa.mode is defined %} +{%     if security.wpa.mode == 'both' %}  wpa=3 -{%- elif 'wpa2' in sec_wpa_mode -%} +{%     elif security.wpa.mode == 'wpa2' %}  wpa=2 -{%- elif 'wpa' in sec_wpa_mode -%} +{%     elif security.wpa.mode == 'wpa' %}  wpa=1 -{%- endif %} +{%     endif %} +{%   endif %} -{% if sec_wpa_cipher -%} +{%   if security.wpa.cipher is defined %}  # Set of accepted cipher suites (encryption algorithms) for pairwise keys  # (unicast packets). This is a space separated list of algorithms:  # CCMP = AES in Counter mode with CBC-MAC (CCMP-128) @@ -563,26 +497,39 @@ wpa=1  # allowed as the pairwise cipher, group cipher will also be CCMP. Otherwise,  # TKIP will be used as the group cipher. The optional group_cipher parameter can  # be used to override this automatic selection. -{% if 'wpa2' in sec_wpa_mode -%} + +{%     if security.wpa.mode is defined and security.wpa.mode == 'wpa2' %}  # Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value) -rsn_pairwise={{ sec_wpa_cipher | join(" ") }} -{% else -%} +{%       if security.wpa.cipher is string %} +rsn_pairwise={{ security.wpa.cipher }} +{%       else %} +rsn_pairwise={{ security.wpa.cipher | join(" ") }} +{%       endif %} +{%     else %}  # Pairwise cipher for WPA (v1) (default: TKIP) -wpa_pairwise={{ sec_wpa_cipher | join(" ") }} -{%- endif -%} -{% endif %} - -{% if sec_wpa_group_cipher -%} +{%       if security.wpa.cipher is string %} +wpa_pairwise={{ security.wpa.cipher }} +{%       else %} +wpa_pairwise={{ security.wpa.cipher | join(" ") }} +{%       endif %} +{%     endif %} +{%   endif %} + +{%   if security.wpa.group_cipher is defined %}  # Optional override for automatic group cipher selection  # This can be used to select a specific group cipher regardless of which  # pairwise ciphers were enabled for WPA and RSN. It should be noted that  # overriding the group cipher with an unexpected value can result in  # interoperability issues and in general, this parameter is mainly used for  # testing purposes. -group_cipher={{ sec_wpa_group_cipher | join(" ") }} -{% endif %} - -{% if sec_wpa_passphrase -%} +{%     if security.wpa.group_cipher is string %} +group_cipher={{ security.wpa.group_cipher }} +{%     else %} +group_cipher={{ security.wpa.group_cipher | join(" ") }} +{%     endif %} +{%   endif %} + +{%   if security.wpa.passphrase is defined %}  # IEEE 802.11 specifies two authentication algorithms. hostapd can be  # configured to allow both of these or only one. Open system authentication  # should be used with IEEE 802.1X. @@ -595,7 +542,7 @@ auth_algs=1  # secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase  # (8..63 characters) that will be converted to PSK. This conversion uses SSID  # so the PSK changes when ASCII passphrase is used and the SSID is changed. -wpa_passphrase={{ sec_wpa_passphrase }} +wpa_passphrase={{ security.wpa.passphrase }}  # Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The  # entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be @@ -604,7 +551,7 @@ wpa_passphrase={{ sec_wpa_passphrase }}  # WPA-PSK-SHA256 = WPA2-Personal using SHA256  wpa_key_mgmt=WPA-PSK -{% elif sec_wpa_radius -%} +{%   elif security.wpa.radius is defined %}  ##### IEEE 802.1X-2004 related configuration ##################################  # Require IEEE 802.1X authorization  ieee8021x=1 @@ -616,40 +563,37 @@ ieee8021x=1  # WPA-EAP-SHA256 = WPA2-Enterprise using SHA256  wpa_key_mgmt=WPA-EAP -{% if sec_wpa_radius_source -%} +{%     if security.wpa.radius.server is defined %}  # RADIUS client forced local IP address for the access point  # Normally the local IP address is determined automatically based on configured  # IP addresses, but this field can be used to force a specific address to be  # used, e.g., when the device has multiple IP addresses. -radius_client_addr={{ sec_wpa_radius_source }} - -# The own IP address of the access point (used as NAS-IP-Address) -own_ip_addr={{ sec_wpa_radius_source }} -{% else %}  # The own IP address of the access point (used as NAS-IP-Address) +{%       if security.wpa.radius.source_address is defined %} +radius_client_addr={{ security.wpa.radius.source_address }} +own_ip_addr={{ security.wpa.radius.source_address }} +{%       else %}  own_ip_addr=127.0.0.1 -{% endif %} +{%       endif %} -{% for radius in sec_wpa_radius -%} -{%- if not radius.disabled -%} +{%       for radius in security.wpa.radius.server if not radius.disabled %}  # RADIUS authentication server  auth_server_addr={{ radius.server }}  auth_server_port={{ radius.port }}  auth_server_shared_secret={{ radius.key }} -{% if radius.acc_port -%} + +{%         if radius.acc_port %}  # RADIUS accounting server  acct_server_addr={{ radius.server }}  acct_server_port={{ radius.acc_port }}  acct_server_shared_secret={{ radius.key }} -{% endif %} -{% endif %} -{% endfor %} - -{% endif %} - -{% else %} +{%         endif %} +{%       endfor %} +{%     else %}  # Open system  auth_algs=1 +{%     endif %} +{%   endif %}  {% endif %}  # TX queue parameters (EDCF / bursting) diff --git a/data/templates/wifi/wpa_supplicant.conf.tmpl b/data/templates/wifi/wpa_supplicant.conf.tmpl index 2784883f1..9ddad35fd 100644 --- a/data/templates/wifi/wpa_supplicant.conf.tmpl +++ b/data/templates/wifi/wpa_supplicant.conf.tmpl @@ -1,8 +1,8 @@  # WPA supplicant config  network={      ssid="{{ ssid }}" -{%- if sec_wpa_passphrase %} -    psk="{{ sec_wpa_passphrase }}" +{% if security is defined and security.wpa is defined and security.wpa.passphrase is defined %} +    psk="{{ security.wpa.passphrase }}"  {% else %}      key_mgmt=NONE  {% endif %} diff --git a/interface-definitions/include/interface-arp-cache-timeout.xml.i b/interface-definitions/include/interface-arp-cache-timeout.xml.i index 81d35f593..e65321158 100644 --- a/interface-definitions/include/interface-arp-cache-timeout.xml.i +++ b/interface-definitions/include/interface-arp-cache-timeout.xml.i @@ -10,4 +10,5 @@      </constraint>      <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage>    </properties> +  <defaultValue>30</defaultValue>  </leafNode> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index ddd52979b..7d658f6a0 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -78,6 +78,7 @@                </constraint>                <constraintErrorMessage>hash-policy must be layer2 layer2+3 or layer3+4</constraintErrorMessage>              </properties> +            <defaultValue>layer2</defaultValue>            </leafNode>            <node name="ip">              <children> @@ -137,6 +138,7 @@                </constraint>                <constraintErrorMessage>mode must be 802.3ad, active-backup, broadcast, round-robin, transmit-load-balance, adaptive-load-balance, or xor</constraintErrorMessage>              </properties> +            <defaultValue>802.3ad</defaultValue>            </leafNode>            <node name="member">              <properties> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index 6b610e623..92356d696 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -32,6 +32,7 @@                  <validator name="numeric" argument="--range 0-0 --range 10-1000000"/>                </constraint>              </properties> +            <defaultValue>300</defaultValue>            </leafNode>            #include <include/interface-description.xml.i>            #include <include/dhcp-options.xml.i> @@ -51,6 +52,7 @@                </constraint>                <constraintErrorMessage>Forwarding delay must be between 0 and 200 seconds</constraintErrorMessage>              </properties> +            <defaultValue>14</defaultValue>            </leafNode>            <leafNode name="hello-time">              <properties> @@ -64,6 +66,7 @@                </constraint>                <constraintErrorMessage>Bridge Hello interval must be between 1 and 10 seconds</constraintErrorMessage>              </properties> +            <defaultValue>2</defaultValue>            </leafNode>            <node name="igmp">              <properties> @@ -107,6 +110,7 @@                </constraint>                <constraintErrorMessage>Bridge max aging value must be between 1 and 40 seconds</constraintErrorMessage>              </properties> +            <defaultValue>20</defaultValue>            </leafNode>            <node name="member">              <properties> @@ -133,6 +137,7 @@                        </constraint>                        <constraintErrorMessage>Path cost value must be between 1 and 65535</constraintErrorMessage>                      </properties> +                    <defaultValue>100</defaultValue>                    </leafNode>                    <leafNode name="priority">                      <properties> @@ -146,6 +151,7 @@                        </constraint>                        <constraintErrorMessage>Port priority value must be between 0 and 63</constraintErrorMessage>                      </properties> +                    <defaultValue>32</defaultValue>                    </leafNode>                  </children>                </tagNode> @@ -163,6 +169,7 @@                </constraint>                <constraintErrorMessage>Bridge priority must be between 0 and 65535 (multiples of 4096)</constraintErrorMessage>              </properties> +            <defaultValue>32768</defaultValue>            </leafNode>            <leafNode name="stp">              <properties> diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in index 1e32a15f8..e8f3f09f1 100644 --- a/interface-definitions/interfaces-ethernet.xml.in +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -56,6 +56,7 @@                </constraint>                <constraintErrorMessage>duplex must be auto, half or full</constraintErrorMessage>              </properties> +            <defaultValue>auto</defaultValue>            </leafNode>            #include <include/interface-hw-id.xml.i>            <node name="ip"> @@ -265,6 +266,7 @@                </constraint>                <constraintErrorMessage>Speed must be auto, 10, 100, 1000, 2500, 5000, 10000, 25000, 40000, 50000 or 100000</constraintErrorMessage>              </properties> +            <defaultValue>auto</defaultValue>            </leafNode>            #include <include/vif-s.xml.i>            #include <include/vif.xml.i> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index d5f9ca661..0ef45e2c2 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -70,6 +70,7 @@                </constraint>                <constraintErrorMessage>mode must be private, vepa, bridge or passthru</constraintErrorMessage>              </properties> +            <defaultValue>private</defaultValue>            </leafNode>            #include <include/vif-s.xml.i>            #include <include/vif.xml.i> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index 06c7734f5..6f0ec9e71 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -320,7 +320,7 @@                      <properties>                        <help>VHT link adaptation capabilities</help>                        <completionHelp> -			<list>unsolicited both</list> +                        <list>unsolicited both</list>                        </completionHelp>                        <valueHelp>                          <format>unsolicited</format> @@ -451,6 +451,7 @@            <leafNode name="disable-broadcast-ssid">              <properties>                <help>Disable broadcast of SSID from access-point</help> +              <valueless/>              </properties>            </leafNode>            #include <include/interface-disable-link-detect.xml.i> @@ -551,9 +552,10 @@                  <description>802.11ac - 1300 Mbits/sec</description>                </valueHelp>                <constraint> -                <regex>(a|b|g|n|ac)</regex> +                <regex>^(a|b|g|n|ac)$</regex>                </constraint>              </properties> +            <defaultValue>g</defaultValue>            </leafNode>            <leafNode name="physical-device">              <properties> @@ -637,7 +639,7 @@                          <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description>                        </valueHelp>                        <constraint> -                        <regex>(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)</regex> +                        <regex>^(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)$</regex>                        </constraint>                        <constraintErrorMessage>Invalid cipher selection</constraintErrorMessage>                        <multi/> @@ -670,7 +672,7 @@                          <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description>                        </valueHelp>                        <constraint> -                        <regex>(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)</regex> +                        <regex>^(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)$</regex>                        </constraint>                        <constraintErrorMessage>Invalid group cipher selection</constraintErrorMessage>                        <multi/> @@ -695,7 +697,7 @@                          <description>Allow both WPA and WPA2</description>                        </valueHelp>                        <constraint> -                        <regex>(wpa|wpa2|both)</regex> +                        <regex>^(wpa|wpa2|both)$</regex>                        </constraint>                        <constraintErrorMessage>Unknown WPA mode</constraintErrorMessage>                      </properties> @@ -762,10 +764,11 @@                  <description>Passively monitor all packets on the frequency/channel</description>                </valueHelp>                <constraint> -                <regex>(access-point|station|monitor)</regex> +                <regex>^(access-point|station|monitor)$</regex>                </constraint>                <constraintErrorMessage>Type must be access-point, station or monitor</constraintErrorMessage>              </properties> +            <defaultValue>monitor</defaultValue>            </leafNode>            #include <include/vif.xml.i>            #include <include/vif-s.xml.i> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 0dc7578d8..126d6195a 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -15,15 +15,15 @@  """  A library for retrieving value dicts from VyOS configs in a declarative fashion. -  """ +import os +import jmespath  from enum import Enum  from copy import deepcopy  from vyos import ConfigError  from vyos.validate import is_member -from vyos.util import ifname_from_config  def retrieve_config(path_hash, base_path, config):      """ @@ -102,397 +102,173 @@ def dict_merge(source, destination):      return tmp  def list_diff(first, second): -    """ -    Diff two dictionaries and return only unique items -    """ +    """ Diff two dictionaries and return only unique items """      second = set(second)      return [item for item in first if item not in second] - -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)) - -dhcpv6_pd_default_data = { -    'dhcpv6_prm_only': False, -    'dhcpv6_temporary': False, -    'dhcpv6_pd_length': '', -    'dhcpv6_pd_interfaces': [] -} - -interface_default_data = { -    **dhcpv6_pd_default_data, -    'address': [], -    'address_remove': [], -    'description': '', -    'dhcp_client_id': '', -    'dhcp_hostname': '', -    'dhcp_vendor_class_id': '', -    'disable': False, -    'disable_link_detect': 1, -    'ip_disable_arp_filter': 1, -    'ip_enable_arp_accept': 0, -    'ip_enable_arp_announce': 0, -    'ip_enable_arp_ignore': 0, -    'ip_proxy_arp': 0, -    'ipv6_accept_ra': 1, -    'ipv6_autoconf': 0, -    'ipv6_eui64_prefix': [], -    'ipv6_eui64_prefix_remove': [], -    'ipv6_forwarding': 1, -    'ipv6_dup_addr_detect': 1, -    'is_bridge_member': False, -    'mac': '', -    'mtu': 1500, -    'vrf': '' -} - -vlan_default = { -    **interface_default_data, -    'egress_qos': '', -    'egress_qos_changed': False, -    'ingress_qos': '', -    'ingress_qos_changed': False, -    'vif_c': {}, -    'vif_c_remove': [] -} - -# see: https://docs.python.org/3/library/enum.html#functional-api -disable = Enum('disable','none was now both') - -def disable_state(conf, check=[3,5,7]): +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.keys(): +            for key in ['ip', 'mtu']: +                if key in dict[vif].keys(): +                    del dict[vif][key] + +            # cleanup VIF-S defaults +            if 'vif_c' in dict[vif].keys(): +                for key in ['ip', 'mtu']: +                    if key in dict[vif]['vif_c'].keys(): +                        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] + +    return dict + +def leaf_node_changed(conf, path):      """ -    return if and how a particual section of the configuration is has disable'd -    using "disable" including if it was disabled by one of its parent. - -    check: a list of the level we should check, here 7,5 and 3 -          interfaces ethernet eth1 vif-s 1 vif-c 2 disable -          interfaces ethernet eth1 vif 1 disable -          interfaces ethernet eth1 disable - -    it returns an enum (none, was, now, both) +    Check if a leaf node was altered. If it has been altered - values has been +    changed, or it was added/removed, we will return the old value. If nothing +    has been changed, None is returned      """ - -    # save where we are in the config -    current_level = conf.get_level() - -    # logic to figure out if the interface (or one of it parent is disabled) -    eff_disable = False -    act_disable = False - -    levels = check[:] -    working_level = current_level[:] - -    while levels: -        position = len(working_level) -        if not position: -            break -        if position not in levels: -            working_level = working_level[:-1] -            continue - -        levels.remove(position) -        conf.set_level(working_level) -        working_level = working_level[:-1] - -        eff_disable = eff_disable or conf.exists_effective('disable') -        act_disable = act_disable or conf.exists('disable') - -    conf.set_level(current_level) - -    # how the disabling changed -    if eff_disable and act_disable: -        return disable.both -    if eff_disable and not eff_disable: -        return disable.was -    if not eff_disable and act_disable: -        return disable.now -    return disable.none - - -def intf_to_dict(conf, default): -    from vyos.ifconfig import Interface - +    from vyos.configdiff import get_config_diff +    D = get_config_diff(conf, key_mangling=('-', '_')) +    D.set_level(conf.get_level()) +    (new, old) = D.get_value_diff(path) +    if new != old: +        if isinstance(old, str): +            return old +        elif isinstance(old, list): +            if isinstance(new, str): +                new = [new] +            elif isinstance(new, type(None)): +                new = [] +            return list_diff(old, new) + +    return None + +def node_changed(conf, path):      """ -    Common used function which will extract VLAN related information from config -    and represent the result as Python dictionary. - -    Function call's itself recursively if a vif-s/vif-c pair is detected. +    Check if a leaf node was altered. If it has been altered - values has been +    changed, or it was added/removed, we will return the old value. If nothing +    has been changed, None is returned      """ +    from vyos.configdiff import get_config_diff, Diff +    D = get_config_diff(conf, key_mangling=('-', '_')) +    D.set_level(conf.get_level()) +    # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 +    keys = D.get_child_nodes_diff(path, expand_nodes=Diff.DELETE)['delete'].keys() +    return list(keys) + +def get_removed_vlans(conf, dict): +    """ +    Common function to parse a dictionary retrieved via get_config_dict() and +    determine any added/removed VLAN interfaces - be it 802.1q or Q-in-Q. +    """ +    from vyos.configdiff import get_config_diff, Diff -    intf = deepcopy(default) -    intf['intf'] = ifname_from_config(conf) - -    current_vif_list = conf.list_nodes(['vif']) -    previous_vif_list = conf.list_effective_nodes(['vif']) - -    # set the vif to be deleted -    for vif in previous_vif_list: -        if vif not in current_vif_list: -            intf['vif_remove'].append(vif) - -    # retrieve interface description -    if conf.exists(['description']): -        intf['description'] = conf.return_value(['description']) - -    # get DHCP client identifier -    if conf.exists(['dhcp-options', 'client-id']): -        intf['dhcp_client_id'] = conf.return_value(['dhcp-options', 'client-id']) - -    # DHCP client host name (overrides the system host name) -    if conf.exists(['dhcp-options', 'host-name']): -        intf['dhcp_hostname'] = conf.return_value(['dhcp-options', 'host-name']) - -    # DHCP client vendor identifier -    if conf.exists(['dhcp-options', 'vendor-class-id']): -        intf['dhcp_vendor_class_id'] = conf.return_value( -            ['dhcp-options', 'vendor-class-id']) - -    # DHCPv6 only acquire config parameters, no address -    if conf.exists(['dhcpv6-options', 'parameters-only']): -        intf['dhcpv6_prm_only'] = True - -    # DHCPv6 prefix delegation (RFC3633) -    current_level = conf.get_level() -    if conf.exists(['dhcpv6-options', 'prefix-delegation']): -        dhcpv6_pd_path = current_level + ['dhcpv6-options', 'prefix-delegation'] -        conf.set_level(dhcpv6_pd_path) - -        # retriebe DHCPv6-PD prefix helper length as some ISPs only hand out a -        # /64 by default (https://phabricator.vyos.net/T2506) -        if conf.exists(['length']): -            intf['dhcpv6_pd_length'] = conf.return_value(['length']) +    # Check vif, vif-s/vif-c VLAN interfaces for removal +    D = get_config_diff(conf, key_mangling=('-', '_')) +    D.set_level(conf.get_level()) +    # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 +    keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys() +    if keys: +        dict.update({'vif_remove': [*keys]}) -        for interface in conf.list_nodes(['interface']): -            conf.set_level(dhcpv6_pd_path + ['interface', interface]) -            pd = { -                'ifname': interface, -                'sla_id': '', -                'sla_len': '', -                'if_id': '' -            } +    # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 +    keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys() +    if keys: +        dict.update({'vif_s_remove': [*keys]}) -            if conf.exists(['sla-id']): -                pd['sla_id'] = conf.return_value(['sla-id']) +    for vif in dict.get('vif_s', {}).keys(): +        keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys() +        if keys: +            dict.update({'vif_s': { vif : {'vif_c_remove': [*keys]}}}) -            if conf.exists(['sla-len']): -                pd['sla_len'] = conf.return_value(['sla-len']) +    return dict -            if conf.exists(['address']): -                pd['if_id'] = conf.return_value(['address']) - -            intf['dhcpv6_pd_interfaces'].append(pd) - -    # re-set config level -    conf.set_level(current_level) - -    # DHCPv6 temporary IPv6 address -    if conf.exists(['dhcpv6-options', 'temporary']): -        intf['dhcpv6_temporary'] = True - -    # ignore link state changes -    if conf.exists(['disable-link-detect']): -        intf['disable_link_detect'] = 2 - -    # ARP filter configuration -    if conf.exists(['ip', 'disable-arp-filter']): -        intf['ip_disable_arp_filter'] = 0 - -    # ARP enable accept -    if conf.exists(['ip', 'enable-arp-accept']): -        intf['ip_enable_arp_accept'] = 1 - -    # ARP enable announce -    if conf.exists(['ip', 'enable-arp-announce']): -        intf['ip_enable_arp_announce'] = 1 - -    # ARP enable ignore -    if conf.exists(['ip', 'enable-arp-ignore']): -        intf['ip_enable_arp_ignore'] = 1 - -    # Enable Proxy ARP -    if conf.exists(['ip', 'enable-proxy-arp']): -        intf['ip_proxy_arp'] = 1 - -    # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) -    if conf.exists(['ipv6', 'address', 'autoconf']): -        intf['ipv6_autoconf'] = 1 - -    # Disable IPv6 forwarding on this interface -    if conf.exists(['ipv6', 'disable-forwarding']): -        intf['ipv6_forwarding'] = 0 - -    # check if interface is member of a bridge -    intf['is_bridge_member'] = is_member(conf, intf['intf'], 'bridge') - -    # IPv6 Duplicate Address Detection (DAD) tries -    if conf.exists(['ipv6', 'dup-addr-detect-transmits']): -        intf['ipv6_dup_addr_detect'] = int( -            conf.return_value(['ipv6', 'dup-addr-detect-transmits'])) - -    # Media Access Control (MAC) address -    if conf.exists(['mac']): -        intf['mac'] = conf.return_value(['mac']) - -    # Maximum Transmission Unit (MTU) -    if conf.exists(['mtu']): -        intf['mtu'] = int(conf.return_value(['mtu'])) - -    # retrieve VRF instance -    if conf.exists(['vrf']): -        intf['vrf'] = conf.return_value(['vrf']) - -    #  egress QoS -    if conf.exists(['egress-qos']): -        intf['egress_qos'] = conf.return_value(['egress-qos']) - -    # egress changes QoS require VLAN interface recreation -    if conf.return_effective_value(['egress-qos']): -        if intf['egress_qos'] != conf.return_effective_value(['egress-qos']): -            intf['egress_qos_changed'] = True - -    # ingress QoS -    if conf.exists(['ingress-qos']): -        intf['ingress_qos'] = conf.return_value(['ingress-qos']) - -    # ingress changes QoS require VLAN interface recreation -    if conf.return_effective_value(['ingress-qos']): -        if intf['ingress_qos'] != conf.return_effective_value(['ingress-qos']): -            intf['ingress_qos_changed'] = True - -    # Get the interface addresses -    intf['address'] = conf.return_values(['address']) - -    # addresses to remove - difference between effective and working config -    intf['address_remove'] = list_diff( -            conf.return_effective_values(['address']), intf['address']) - -    # Get prefixes for IPv6 addressing based on MAC address (EUI-64) -    intf['ipv6_eui64_prefix'] = conf.return_values(['ipv6', 'address', 'eui64']) - -    # EUI64 to remove - difference between effective and working config -    intf['ipv6_eui64_prefix_remove'] = list_diff( -            conf.return_effective_values(['ipv6', 'address', 'eui64']), -            intf['ipv6_eui64_prefix']) - -    # Determine if the interface should be disabled -    disabled = disable_state(conf) -    if disabled == disable.both: -        # was and is still disabled -        intf['disable'] = True -    elif disabled == disable.now: -        # it is now disable but was not before -        intf['disable'] = True -    elif disabled == disable.was: -        # it was disable but not anymore -        intf['disable'] = False -    else: -        # normal change -        intf['disable'] = False - -    # Remove the default link-local address if no-default-link-local is set, -    # if member of a bridge or if disabled (it may not have a MAC if it's down) -    if ( conf.exists(['ipv6', 'address', 'no-default-link-local']) -            or intf.get('is_bridge_member') or intf['disable'] ): -        intf['ipv6_eui64_prefix_remove'].append('fe80::/64') -    else: -        # add the link-local by default to make IPv6 work -        intf['ipv6_eui64_prefix'].append('fe80::/64') - -    # If MAC has changed, remove and re-add all IPv6 EUI64 addresses -    try: -        interface = Interface(intf['intf'], create=False) -        if intf['mac'] and intf['mac'] != interface.get_mac(): -            intf['ipv6_eui64_prefix_remove'] += intf['ipv6_eui64_prefix'] -    except Exception: -        # If the interface does not exist, it could not have changed -        pass - -    # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, -    # accept_ra must be 2 -    if intf['ipv6_autoconf'] or 'dhcpv6' in intf['address']: -        intf['ipv6_accept_ra'] = 2 - -    return intf, disable - - - -def add_to_dict(conf, disabled, ifdict, section, key): +def get_interface_dict(config, base, ifname=''):      """ -    parse a section of vif/vif-s/vif-c and add them to the dict -    follow the convention to: -    * use the "key" for what to add -    * use the "key" what what to remove - -    conf:     is the Config() already at the level we need to parse -    disabled: is a disable enum so we know how to handle to data -    intf:     if the interface dictionary -    section:  is the section name to parse (vif/vif-s/vif-c) -    key:      is the dict key to use (vif/vifs/vifc) +    Common utility function to retrieve and mandgle the interfaces available +    in CLI configuration. All interfaces have a common base ground where the +    value retrival is identical - so it can and should be reused + +    Will return a dictionary with the necessary interface configuration      """ +    from vyos.xml import defaults + +    if not ifname: +        # determine tagNode instance +        if 'VYOS_TAGNODE_VALUE' not in os.environ: +            raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') +        ifname = os.environ['VYOS_TAGNODE_VALUE'] + +    # retrieve interface default values +    default_values = defaults(base) + +    # setup config level which is extracted in get_removed_vlans() +    config.set_level(base + [ifname]) +    dict = config.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + +    # Check if interface has been removed +    if dict == {}: +        dict.update({'deleted' : ''}) + +    # Add interface instance name into dictionary +    dict.update({'ifname': ifname}) + +    # We have gathered the dict representation of the CLI, but there are +    # default options which we need to update into the dictionary +    # retrived. +    dict = dict_merge(default_values, dict) + +    # Check if we are a member of a bridge device +    bridge = is_member(config, ifname, 'bridge') +    if bridge: +        dict.update({'is_bridge_member' : bridge}) + +    # Check if we are a member of a bond device +    bond = is_member(config, ifname, 'bonding') +    if bond: +        dict.update({'is_bond_member' : bond}) + +    mac = leaf_node_changed(config, ['mac']) +    if mac: +        dict.update({'mac_old' : mac}) + +    eui64 = leaf_node_changed(config, ['ipv6', 'address', 'eui64']) +    if eui64: +        # XXX: T2636 workaround: convert string to a list with one element +        if isinstance(eui64, str): +            eui64 = [eui64] +        tmp = jmespath.search('ipv6.address', dict) +        if not tmp: +            dict.update({'ipv6': {'address': {'eui64_old': eui64}}}) +        else: +            dict['ipv6']['address'].update({'eui64_old': eui64}) + +    # remove wrongly inserted values +    dict = T2665_default_dict_cleanup(dict) + +    # The values are identical for vif, vif-s and vif-c as the 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(): +        vif_config = dict_merge(default_vif_values, vif_config) +    for vif_s, vif_s_config in dict.get('vif_s', {}).items(): +        vif_s_config = dict_merge(default_vif_values, vif_s_config) +        for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items(): +            vif_c_config = dict_merge(default_vif_values, vif_c_config) + +    # Check vif, vif-s/vif-c VLAN interfaces for removal +    dict = get_removed_vlans(config, dict) + +    return dict -    if not conf.exists(section): -        return ifdict - -    effect = conf.list_effective_nodes(section) -    active = conf.list_nodes(section) - -    # the section to parse for vlan -    sections = [] - -    # determine which interfaces to add or remove based on disable state -    if disabled == disable.both: -        # was and is still disabled -        ifdict[f'{key}_remove'] = [] -    elif disabled == disable.now: -        # it is now disable but was not before -        ifdict[f'{key}_remove'] = effect -    elif disabled == disable.was: -        # it was disable but not anymore -        ifdict[f'{key}_remove'] = [] -        sections = active -    else: -        # normal change -        # get interfaces (currently effective) - to determine which -        # interface is no longer present and needs to be removed -        ifdict[f'{key}_remove'] = list_diff(effect, active) -        sections = active - -    current_level = conf.get_level() - -    # add each section, the key must already exists -    for s in sections: -        # set config level to vif interface -        conf.set_level(current_level + [section, s]) -        # add the vlan config as a key (vlan id) - value (config) pair -        ifdict[key][s] = vlan_to_dict(conf) - -    # re-set configuration level to leave things as found -    conf.set_level(current_level) - -    return ifdict - - -def vlan_to_dict(conf, default=vlan_default): -    vlan, disabled = intf_to_dict(conf, default) - -    # if this is a not within vif-s node, we are done -    if conf.get_level()[-2] != 'vif-s': -        return vlan - -    # ethertype is mandatory on vif-s nodes and only exists here! -    # ethertype uses a default of 0x88A8 -    tmp = '0x88A8' -    if conf.exists('ethertype'): -        tmp = conf.return_value('ethertype') -    vlan['ethertype'] = get_ethertype(tmp) - -    # check if there is a Q-in-Q vlan customer interface -    # and call this function recursively -    add_to_dict(conf, disable, vlan, 'vif-c', 'vif_c') - -    return vlan diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 32129a048..8e06d16f2 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -41,14 +41,14 @@ def verify_vrf(config):  def verify_address(config):      """ -    Common helper function used by interface implementations to -    perform recurring validation of IP address assignmenr -    when interface also is part of a bridge. +    Common helper function used by interface implementations to perform +    recurring validation of IP address assignment when interface is part +    of a bridge or bond.      """      if {'is_bridge_member', 'address'} <= set(config):          raise ConfigError( -            f'Cannot assign address to interface "{ifname}" as it is a ' -            f'member of bridge "{is_bridge_member}"!'.format(**config)) +            'Cannot assign address to interface "{ifname}" as it is a ' +            'member of bridge "{is_bridge_member}"!'.format(**config))  def verify_bridge_delete(config): @@ -62,6 +62,15 @@ def verify_bridge_delete(config):              'Interface "{ifname}" cannot be deleted as it is a '              'member of bridge "{is_bridge_member}"!'.format(**config)) +def verify_interface_exists(config): +    """ +    Common helper function used by interface implementations to perform +    recurring validation if an interface actually exists. +    """ +    from netifaces import interfaces +    if not config['ifname'] in interfaces(): +        raise ConfigError(f'Interface "{ifname}" does not exist!' +                          .format(**config))  def verify_source_interface(config):      """ @@ -70,9 +79,43 @@ def verify_source_interface(config):      required by e.g. peth/MACvlan, MACsec ...      """      from netifaces import interfaces -    if not 'source_interface' in config.keys(): +    if 'source_interface' not in config:          raise ConfigError('Physical source-interface required for '                            'interface "{ifname}"'.format(**config)) -    if not config['source_interface'] in interfaces(): -        raise ConfigError(f'Source interface {source_interface} does not ' -                          f'exist'.format(**config)) +    if config['source_interface'] not in interfaces(): +        raise ConfigError('Source interface {source_interface} does not ' +                          'exist'.format(**config)) + +def verify_dhcpv6(config): +    """ +    Common helper function used by interface implementations to perform +    recurring validation of DHCPv6 options which are mutually exclusive. +    """ +    if {'parameters_only', 'temporary'} <= set(config.get('dhcpv6_options', {})): +        raise ConfigError('DHCPv6 temporary and parameters-only options ' +                          'are mutually exclusive!') + +def verify_vlan_config(config): +    """ +    Common helper function used by interface implementations to perform +    recurring validation of interface VLANs +    """ +    # 802.1q VLANs +    for vlan in config.get('vif', {}).keys(): +        vlan = config['vif'][vlan] +        verify_dhcpv6(vlan) +        verify_address(vlan) +        verify_vrf(vlan) + +    # 802.1ad (Q-in-Q) VLANs +    for vlan in config.get('vif_s', {}).keys(): +        vlan = config['vif_s'][vlan] +        verify_dhcpv6(vlan) +        verify_address(vlan) +        verify_vrf(vlan) + +        for vlan in config.get('vif_s', {}).get('vif_c', {}).keys(): +            vlan = config['vif_c'][vlan] +            verify_dhcpv6(vlan) +            verify_address(vlan) +            verify_vrf(vlan) diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 47dd4ff34..5a48ac632 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -14,14 +14,15 @@  # License along with this library.  If not, see <http://www.gnu.org/licenses/>.  import os +import jmespath  from vyos.ifconfig.interface import Interface  from vyos.ifconfig.vlan import VLAN +from vyos.util import cmd  from vyos.validate import assert_list  from vyos.validate import assert_positive -  @Interface.register  @VLAN.enable  class BondIf(Interface): @@ -179,7 +180,13 @@ class BondIf(Interface):          >>> BondIf('bond0').get_arp_ip_target()          '192.0.2.1'          """ -        return self.get_interface('bond_arp_ip_target') +        # As this function might also be called from update() of a VLAN interface +        # we must check if the bond_arp_ip_target retrieval worked or not - as this +        # can not be set for a bond vif interface +        try: +            return self.get_interface('bond_arp_ip_target') +        except FileNotFoundError: +            return ''      def set_arp_ip_target(self, target):          """ @@ -209,11 +216,31 @@ class BondIf(Interface):          >>> BondIf('bond0').add_port('eth0')          >>> BondIf('bond0').add_port('eth1')          """ -        # An interface can only be added to a bond if it is in 'down' state. If -        # interface is in 'up' state, the following Kernel error will  be thrown: -        # bond0: eth1 is up - this may be due to an out of date ifenslave. -        Interface(interface).set_admin_state('down') -        return self.set_interface('bond_add_port', f'+{interface}') + +        # From drivers/net/bonding/bond_main.c: +        # ... +        # bond_set_slave_link_state(new_slave, +        #      BOND_LINK_UP, +        #      BOND_SLAVE_NOTIFY_NOW); +        # ... +        # +        # The kernel will ALWAYS place new bond members in "up" state regardless +        # what the CLI will tell us! + +        # Physical interface must be in admin down state before they can be +        # enslaved. If this is not the case an error will be shown: +        # bond0: eth0 is up - this may be due to an out of date ifenslave +        slave = Interface(interface) +        slave_state = slave.get_admin_state() +        if slave_state == 'up': +            slave.set_admin_state('down') + +        ret = self.set_interface('bond_add_port', f'+{interface}') +        # The kernel will ALWAYS place new bond members in "up" state regardless +        # what the LI is configured for - thus we place the interface in its +        # desired state +        slave.set_admin_state(slave_state) +        return ret      def del_port(self, interface):          """ @@ -277,3 +304,80 @@ class BondIf(Interface):          >>> BondIf('bond0').set_mode('802.3ad')          """          return self.set_interface('bond_mode', mode) + +    def update(self, config): +        """ General helper function which works on a dictionary retrived by +        get_config_dict(). It's main intention is to consolidate the scattered +        interface setup code and provide a single point of entry when workin +        on any interface. """ + +        # use ref-counting function to place an interface into admin down state. +        # set_admin_state_up() must be called the same amount of times else the +        # interface won't come up. This can/should be used to prevent link flapping +        # when changing interface parameters require the interface to be down. +        # We will disable it once before reconfiguration and enable it afterwards. +        if 'shutdown_required' in config: +            self.set_admin_state('down') + +        # call base class first +        super().update(config) + +        # ARP monitor targets need to be synchronized between sysfs and CLI. +        # Unfortunately an address can't be send twice to sysfs as this will +        # result in the following exception:  OSError: [Errno 22] Invalid argument. +        # +        # We remove ALL addresses prior to adding new ones, this will remove +        # addresses manually added by the user too - but as we are limited to 16 adresses +        # from the kernel side this looks valid to me. We won't run into an error +        # when a user added manual adresses which would result in having more +        # then 16 adresses in total. +        arp_tgt_addr = list(map(str, self.get_arp_ip_target().split())) +        for addr in arp_tgt_addr: +            self.set_arp_ip_target('-' + addr) + +        # Add configured ARP target addresses +        value = jmespath.search('arp_monitor.target', config) +        if isinstance(value, str): +            value = [value] +        if value: +            for addr in value: +                self.set_arp_ip_target('+' + addr) + +        # Bonding transmit hash policy +        value = config.get('hash_policy') +        if value: self.set_hash_policy(value) + +        # Some interface options can only be changed if the interface is +        # administratively down +        if self.get_admin_state() == 'down': +            # Delete bond member port(s) +            for interface in self.get_slaves(): +                self.del_port(interface) + +            # Bonding policy/mode +            value = config.get('mode') +            if value: self.set_mode(value) + +            # Add (enslave) interfaces to bond +            value = jmespath.search('member.interface', config) +            if value: +                for interface in value: +                    # if we've come here we already verified the interface does +                    # not have an addresses configured so just flush any +                    # remaining ones +                    cmd(f'ip addr flush dev "{interface}"') +                    self.add_port(interface) + +        # Primary device interface - must be set after 'mode' +        value = config.get('primary') +        if value: self.set_primary(value) + +        # Enable/Disable of an interface must always be done at the end of the +        # derived class to make use of the ref-counting set_admin_state() +        # function. We will only enable the interface if 'up' was called as +        # often as 'down'. This is required by some interface implementations +        # as certain parameters can only be changed when the interface is +        # in admin-down state. This ensures the link does not flap during +        # reconfiguration. +        state = 'down' if 'disable' in config else 'up' +        self.set_admin_state(state) diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 44b92c1db..da4e1a289 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -13,12 +13,13 @@  # 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 jmespath  from vyos.ifconfig.interface import Interface - +from vyos.ifconfig.stp import STP  from vyos.validate import assert_boolean  from vyos.validate import assert_positive - +from vyos.util import cmd  @Interface.register  class BridgeIf(Interface): @@ -187,3 +188,76 @@ class BridgeIf(Interface):          >>> BridgeIf('br0').del_port('eth1')          """          return self.set_interface('del_port', interface) + +    def update(self, config): +        """ General helper function which works on a dictionary retrived by +        get_config_dict(). It's main intention is to consolidate the scattered +        interface setup code and provide a single point of entry when workin +        on any interface. """ + +        # call base class first +        super().update(config) + +        # Set ageing time +        value = config.get('aging') +        self.set_ageing_time(value) + +        # set bridge forward delay +        value = config.get('forwarding_delay') +        self.set_forward_delay(value) + +        # set hello time +        value = config.get('hello_time') +        self.set_hello_time(value) + +        # set max message age +        value = config.get('max_age') +        self.set_max_age(value) + +        # set bridge priority +        value = config.get('priority') +        self.set_priority(value) + +        # enable/disable spanning tree +        value = '1' if 'stp' in config else '0' +        self.set_stp(value) + +        # enable or disable IGMP querier +        tmp = jmespath.search('igmp.querier', config) +        value = '1' if (tmp != None) else '0' +        self.set_multicast_querier(value) + +        # remove interface from bridge +        tmp = jmespath.search('member.interface_remove', config) +        if tmp: +            for member in tmp: +                self.del_port(member) + +        STPBridgeIf = STP.enable(BridgeIf) +        tmp = jmespath.search('member.interface', config) +        if tmp: +            for interface, interface_config in tmp.items(): +                # if we've come here we already verified the interface doesn't +                # have addresses configured so just flush any remaining ones +                cmd(f'ip addr flush dev "{interface}"') +                # enslave interface port to bridge +                self.add_port(interface) + +                tmp = STPBridgeIf(interface) +                # set bridge port path cost +                value = interface_config.get('cost') +                tmp.set_path_cost(value) + +                # set bridge port path priority +                value = interface_config.get('priority') +                tmp.set_path_priority(value) + +        # Enable/Disable of an interface must always be done at the end of the +        # derived class to make use of the ref-counting set_admin_state() +        # function. We will only enable the interface if 'up' was called as +        # often as 'down'. This is required by some interface implementations +        # as certain parameters can only be changed when the interface is +        # in admin-down state. This ensures the link does not flap during +        # reconfiguration. +        state = 'down' if 'disable' in config else 'up' +        self.set_admin_state(state) diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py index 404c490c7..43614cd1c 100644 --- a/python/vyos/ifconfig/dummy.py +++ b/python/vyos/ifconfig/dummy.py @@ -35,3 +35,22 @@ class DummyIf(Interface):              'prefixes': ['dum', ],          },      } + +    def update(self, config): +        """ General helper function which works on a dictionary retrived by +        get_config_dict(). It's main intention is to consolidate the scattered +        interface setup code and provide a single point of entry when workin +        on any interface. """ + +        # call base class first +        super().update(config) + +        # Enable/Disable of an interface must always be done at the end of the +        # derived class to make use of the ref-counting set_admin_state() +        # function. We will only enable the interface if 'up' was called as +        # often as 'down'. This is required by some interface implementations +        # as certain parameters can only be changed when the interface is +        # in admin-down state. This ensures the link does not flap during +        # reconfiguration. +        state = 'down' if 'disable' in config else 'up' +        self.set_admin_state(state) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 5b18926c9..b2f701e00 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -15,13 +15,13 @@  import os  import re +import jmespath  from vyos.ifconfig.interface import Interface  from vyos.ifconfig.vlan import VLAN  from vyos.validate import assert_list  from vyos.util import run -  @Interface.register  @VLAN.enable  class EthernetIf(Interface): @@ -252,3 +252,58 @@ class EthernetIf(Interface):          >>> i.set_udp_offload('on')          """          return self.set_interface('ufo', state) + + +    def update(self, config): +        """ General helper function which works on a dictionary retrived by +        get_config_dict(). It's main intention is to consolidate the scattered +        interface setup code and provide a single point of entry when workin +        on any interface. """ + +        # call base class first +        super().update(config) + +        # disable ethernet flow control (pause frames) +        value = 'off' if 'disable_flow_control' in config.keys() else 'on' +        self.set_flow_control(value) + +        # GRO (generic receive offload) +        tmp = jmespath.search('offload_options.generic_receive', config) +        value = tmp if (tmp != None) else 'off' +        self.set_gro(value) + +        # GSO (generic segmentation offload) +        tmp = jmespath.search('offload_options.generic_segmentation', config) +        value = tmp if (tmp != None) else 'off' +        self.set_gso(value) + +        # scatter-gather option +        tmp = jmespath.search('offload_options.scatter_gather', config) +        value = tmp if (tmp != None) else 'off' +        self.set_sg(value) + +        # TSO (TCP segmentation offloading) +        tmp = jmespath.search('offload_options.udp_fragmentation', config) +        value = tmp if (tmp != None) else 'off' +        self.set_tso(value) + +        # UDP fragmentation offloading +        tmp = jmespath.search('offload_options.udp_fragmentation', config) +        value = tmp if (tmp != None) else 'off' +        self.set_ufo(value) + +        # Set physical interface speed and duplex +        if {'speed', 'duplex'} <= set(config): +            speed = config.get('speed') +            duplex = config.get('duplex') +            self.set_speed_duplex(speed, duplex) + +        # Enable/Disable of an interface must always be done at the end of the +        # derived class to make use of the ref-counting set_admin_state() +        # function. We will only enable the interface if 'up' was called as +        # often as 'down'. This is required by some interface implementations +        # as certain parameters can only be changed when the interface is +        # in admin-down state. This ensures the link does not flap during +        # reconfiguration. +        state = 'down' if 'disable' in config else 'up' +        self.set_admin_state(state) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 8d7b247fc..5496499e5 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -16,7 +16,10 @@  import os  import re  import json +import jmespath +  from copy import deepcopy +from glob import glob  from ipaddress import IPv4Network  from ipaddress import IPv6Address @@ -45,6 +48,13 @@ 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 @@ -72,8 +82,12 @@ class Interface(Control):      _command_get = {          'admin_state': {              'shellcmd': 'ip -json link show dev {ifname}', -            'format': lambda j: 'up' if 'UP' in json.loads(j)[0]['flags'] else 'down', -        } +            'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down', +        }, +        'vlan_protocol': { +            'shellcmd': 'ip -json -details link show dev {ifname}', +            'format': lambda j: jmespath.search('[*].linkinfo.info_data.protocol | [0]', json.loads(j)), +        },      }      _command_set = { @@ -197,6 +211,7 @@ class Interface(Control):          # make sure the ifname is the first argument and not from the dict          self.config['ifname'] = ifname +        self._admin_state_down_cnt = 0          # we must have updated config before initialising the Interface          super().__init__(**kargs) @@ -322,11 +337,11 @@ class Interface(Control):              self.set_admin_state('down')          self.set_interface('mac', mac) -         +          # Turn an interface to the 'up' state if it was changed to 'down' by this fucntion          if prev_state == 'up':              self.set_admin_state('up') -     +      def set_vrf(self, vrf=''):          """          Add/Remove interface from given VRF instance. @@ -543,6 +558,17 @@ class Interface(Control):          """          self.set_interface('alias', ifalias) +    def get_vlan_protocol(self): +        """ +        Retrieve VLAN protocol in use, this can be 802.1Q, 802.1ad or None + +        Example: +        >>> from vyos.ifconfig import Interface +        >>> Interface('eth0.10').get_vlan_protocol() +        '802.1Q' +        """ +        return self.get_interface('vlan_protocol') +      def get_admin_state(self):          """          Get interface administrative state. Function will return 'up' or 'down' @@ -564,7 +590,24 @@ class Interface(Control):          >>> Interface('eth0').get_admin_state()          'down'          """ -        return self.set_interface('admin_state', state) +        # A VLAN interface can only be placed in admin up state when +        # the lower interface is up, too +        if self.get_vlan_protocol(): +            lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0] +            with open(lower_interface, 'r') as f: +                flags = f.read() +            # If parent is not up - bail out as we can not bring up the VLAN. +            # Flags are defined in kernel source include/uapi/linux/if.h +            if not int(flags, 16) & 1: +                return None + +        if state == 'up': +            self._admin_state_down_cnt -= 1 +            if self._admin_state_down_cnt < 1: +                return self.set_interface('admin_state', state) +        else: +            self._admin_state_down_cnt += 1 +            return self.set_interface('admin_state', state)      def set_proxy_arp(self, enable):          """ @@ -773,14 +816,17 @@ class Interface(Control):          on any interface. """          # Update interface description -        self.set_alias(config.get('description', None)) +        self.set_alias(config.get('description', '')) + +        # Ignore link state changes +        value = '2' if 'disable_link_detect' in config else '1' +        self.set_link_detect(value)          # Configure assigned interface IP addresses. No longer          # configured addresses will be removed first          new_addr = config.get('address', []) -        # XXX workaround for T2636, convert IP address string to a list -        # with one element +        # XXX: T2636 workaround: convert string to a list with one element          if isinstance(new_addr, str):              new_addr = [new_addr] @@ -796,10 +842,156 @@ class Interface(Control):          # There are some items in the configuration which can only be applied          # if this instance is not bound to a bridge. This should be checked          # by the caller but better save then sorry! -        if not config.get('is_bridge_member', False): -            # Bind interface instance into VRF +        if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config): +            # Bind interface to given VRF or unbind it if vrf node is not set. +            # unbinding will call 'ip link set dev eth0 nomaster' which will +            # also drop the interface out of a bridge or bond - thus this is +            # checked before              self.set_vrf(config.get('vrf', '')) -        # Interface administrative state -        state = 'down' if 'disable' in config.keys() else 'up' -        self.set_admin_state(state) +        # DHCP options +        if 'dhcp_options' in config: +            dhcp_options = config.get('dhcp_options') +            if 'client_id' in dhcp_options: +                self.dhcp.v4.options['client_id'] = dhcp_options.get('client_id') + +            if 'host_name' in dhcp_options: +                self.dhcp.v4.options['hostname'] = dhcp_options.get('host_name') + +            if 'vendor_class_id' in dhcp_options: +                self.dhcp.v4.options['vendor_class_id'] = dhcp_options.get('vendor_class_id') + +        # DHCPv6 options +        if 'dhcpv6_options' in config: +            dhcpv6_options = config.get('dhcpv6_options') +            if 'parameters_only' in dhcpv6_options: +                self.dhcp.v6.options['dhcpv6_prm_only'] = True + +            if 'temporary' in dhcpv6_options: +                self.dhcp.v6.options['dhcpv6_temporary'] = True + +            if 'prefix_delegation' in dhcpv6_options: +                prefix_delegation = dhcpv6_options.get('prefix_delegation') +                if 'length' in prefix_delegation: +                    self.dhcp.v6.options['dhcpv6_pd_length'] = prefix_delegation.get('length') + +                if 'interface' in prefix_delegation: +                    self.dhcp.v6.options['dhcpv6_pd_interfaces'] = prefix_delegation.get('interface') + +        # Configure ARP cache timeout in milliseconds - has default value +        tmp = jmespath.search('ip.arp_cache_timeout', config) +        value = tmp if (tmp != None) else '30' +        self.set_arp_cache_tmo(value) + +        # Configure ARP filter configuration +        tmp = jmespath.search('ip.disable_arp_filter', config) +        value = '0' if (tmp != None) else '1' +        self.set_arp_filter(value) + +        # Configure ARP accept +        tmp = jmespath.search('ip.enable_arp_accept', config) +        value = '1' if (tmp != None) else '0' +        self.set_arp_accept(value) + +        # Configure ARP announce +        tmp = jmespath.search('ip.enable_arp_announce', config) +        value = '1' if (tmp != None) else '0' +        self.set_arp_announce(value) + +        # Configure ARP ignore +        tmp = jmespath.search('ip.enable_arp_ignore', config) +        value = '1' if (tmp != None) else '0' +        self.set_arp_ignore(value) + +        # Enable proxy-arp on this interface +        tmp = jmespath.search('ip.enable_proxy_arp', config) +        value = '1' if (tmp != None) else '0' +        self.set_proxy_arp(value) + +        # Enable private VLAN proxy ARP on this interface +        tmp = jmespath.search('ip.proxy_arp_pvlan', config) +        value = '1' if (tmp != None) else '0' +        self.set_proxy_arp_pvlan(value) + +        # IPv6 forwarding +        tmp = jmespath.search('ipv6.disable_forwarding', config) +        value = '0' if (tmp != None) else '1' +        self.set_ipv6_forwarding(value) + +        # IPv6 router advertisements +        tmp = jmespath.search('ipv6.address.autoconf', config) +        value = '2' if (tmp != None) else '1' +        if 'dhcpv6' in new_addr: +            value = '2' +        self.set_ipv6_accept_ra(value) + +        # IPv6 address autoconfiguration +        tmp = jmespath.search('ipv6.address.autoconf', config) +        value = '1' if (tmp != None) else '0' +        self.set_ipv6_autoconf(value) + +        # IPv6 Duplicate Address Detection (DAD) tries +        tmp = jmespath.search('ipv6.dup_addr_detect_transmits', config) +        value = tmp if (tmp != None) else '1' +        self.set_ipv6_dad_messages(value) + +        # MTU - Maximum Transfer Unit +        if 'mtu' in config: +            self.set_mtu(config.get('mtu')) + +        # Delete old IPv6 EUI64 addresses before changing MAC +        tmp = jmespath.search('ipv6.address.eui64_old', config) +        if tmp: +            for addr in tmp: +                self.del_ipv6_eui64_address(addr) + +        # Change interface MAC address - re-set to real hardware address (hw-id) +        # if custom mac is removed. Skip if bond member. +        if 'is_bond_member' not in config: +            mac = config.get('hw_id') +            if 'mac' in config: +                mac = config.get('mac') +            if mac: +                self.set_mac(mac) + +        # Add IPv6 EUI-based addresses +        tmp = jmespath.search('ipv6.address.eui64', config) +        if tmp: +            # XXX: T2636 workaround: convert string to a list with one element +            if isinstance(tmp, str): +                tmp = [tmp] +            for addr in tmp: +                self.add_ipv6_eui64_address(addr) + +        # re-add ourselves to any bridge we might have fallen out of +        if 'is_bridge_member' in config: +            bridge = config.get('is_bridge_member') +            self.add_to_bridge(bridge) + +        # remove no longer required 802.1ad (Q-in-Q VLANs) +        for vif_s_id in config.get('vif_s_remove', {}): +            self.del_vlan(vif_s_id) + +        # create/update 802.1ad (Q-in-Q VLANs) +        for vif_s_id, vif_s in config.get('vif_s', {}).items(): +            tmp=get_ethertype(vif_s.get('ethertype', '0x88A8')) +            s_vlan = self.add_vlan(vif_s_id, ethertype=tmp) +            s_vlan.update(vif_s) + +            # remove no longer required client VLAN (vif-c) +            for vif_c_id in vif_s.get('vif_c_remove', {}): +                s_vlan.del_vlan(vif_c_id) + +            # create/update client VLAN (vif-c) interface +            for vif_c_id, vif_c in vif_s.get('vif_c', {}).items(): +                c_vlan = s_vlan.add_vlan(vif_c_id) +                c_vlan.update(vif_c) + +        # remove no longer required 802.1q VLAN interfaces +        for vif_id in config.get('vif_remove', {}): +            self.del_vlan(vif_id) + +        # create/update 802.1q VLAN interfaces +        for vif_id, vif in config.get('vif', {}).items(): +            vlan = self.add_vlan(vif_id) +            vlan.update(vif) diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py index 7ebd13b54..2b4ebfdcc 100644 --- a/python/vyos/ifconfig/loopback.py +++ b/python/vyos/ifconfig/loopback.py @@ -75,5 +75,15 @@ class LoopbackIf(Interface):          # Update IP address entry in our dictionary          config.update({'address' : addr}) -        # now call the regular function from within our base class +        # call base class          super().update(config) + +        # Enable/Disable of an interface must always be done at the end of the +        # derived class to make use of the ref-counting set_admin_state() +        # function. We will only enable the interface if 'up' was called as +        # often as 'down'. This is required by some interface implementations +        # as certain parameters can only be changed when the interface is +        # in admin-down state. This ensures the link does not flap during +        # reconfiguration. +        state = 'down' if 'disable' in config else 'up' +        self.set_admin_state(state) diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py index ea8c9807e..6f570d162 100644 --- a/python/vyos/ifconfig/macsec.py +++ b/python/vyos/ifconfig/macsec.py @@ -71,3 +71,22 @@ class MACsecIf(Interface):              'source_interface': '',          }          return config + +    def update(self, config): +        """ General helper function which works on a dictionary retrived by +        get_config_dict(). It's main intention is to consolidate the scattered +        interface setup code and provide a single point of entry when workin +        on any interface. """ + +        # call base class first +        super().update(config) + +        # Enable/Disable of an interface must always be done at the end of the +        # derived class to make use of the ref-counting set_admin_state() +        # function. We will only enable the interface if 'up' was called as +        # often as 'down'. This is required by some interface implementations +        # as certain parameters can only be changed when the interface is +        # in admin-down state. This ensures the link does not flap during +        # reconfiguration. +        state = 'down' if 'disable' in config else 'up' +        self.set_admin_state(state) diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index b5481f4a7..b068ce873 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.py @@ -68,3 +68,22 @@ class MACVLANIf(Interface):          >> dict = MACVLANIf().get_config()          """          return deepcopy(cls.default) + +    def update(self, config): +        """ General helper function which works on a dictionary retrived by +        get_config_dict(). It's main intention is to consolidate the scattered +        interface setup code and provide a single point of entry when workin +        on any interface. """ + +        # call base class first +        super().update(config) + +        # Enable/Disable of an interface must always be done at the end of the +        # derived class to make use of the ref-counting set_admin_state() +        # function. We will only enable the interface if 'up' was called as +        # often as 'down'. This is required by some interface implementations +        # as certain parameters can only be changed when the interface is +        # in admin-down state. This ensures the link does not flap during +        # reconfiguration. +        state = 'down' if 'disable' in config else 'up' +        self.set_admin_state(state) diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py deleted file mode 100644 index 442cb0db8..000000000 --- a/python/vyos/ifconfig_vlan.py +++ /dev/null @@ -1,245 +0,0 @@ -# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library.  If not, see <http://www.gnu.org/licenses/>. - -from netifaces import interfaces -from vyos import ConfigError - -def apply_all_vlans(intf, intfconfig): -    """ -    Function applies all VLANs to the passed interface. - -    intf: object of Interface class -    intfconfig: dict with interface configuration -    """ -    # remove no longer required service VLAN interfaces (vif-s) -    for vif_s in intfconfig['vif_s_remove']: -        intf.del_vlan(vif_s) - -    # create service VLAN interfaces (vif-s) -    for vif_s_id, vif_s in intfconfig['vif_s'].items(): -        s_vlan = intf.add_vlan(vif_s_id, ethertype=vif_s['ethertype']) -        apply_vlan_config(s_vlan, vif_s) - -        # remove no longer required client VLAN interfaces (vif-c) -        # on lower service VLAN interface -        for vif_c in vif_s['vif_c_remove']: -            s_vlan.del_vlan(vif_c) - -        # create client VLAN interfaces (vif-c) -        # on lower service VLAN interface -        for vif_c_id, vif_c in vif_s['vif_c'].items(): -            c_vlan = s_vlan.add_vlan(vif_c_id) -            apply_vlan_config(c_vlan, vif_c) - -    # remove no longer required VLAN interfaces (vif) -    for vif in intfconfig['vif_remove']: -        intf.del_vlan(vif) - -    # create VLAN interfaces (vif) -    for vif_id, vif in intfconfig['vif'].items(): -        # QoS priority mapping can only be set during interface creation -        # so we delete the interface first if required. -        if vif['egress_qos_changed'] or vif['ingress_qos_changed']: -            try: -                # on system bootup the above condition is true but the interface -                # does not exists, which throws an exception, but that's legal -                intf.del_vlan(vif_id) -            except: -                pass - -        vlan = intf.add_vlan(vif_id, ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos']) -        apply_vlan_config(vlan, vif) - - -def apply_vlan_config(vlan, config): -    """ -    Generic function to apply a VLAN configuration from a dictionary -    to a VLAN interface -    """ - -    if not vlan.definition['vlan']: -        raise TypeError() - -    if config['dhcp_client_id']: -        vlan.dhcp.v4.options['client_id'] = config['dhcp_client_id'] - -    if config['dhcp_hostname']: -        vlan.dhcp.v4.options['hostname'] = config['dhcp_hostname'] - -    if config['dhcp_vendor_class_id']: -        vlan.dhcp.v4.options['vendor_class_id'] = config['dhcp_vendor_class_id'] - -    if config['dhcpv6_prm_only']: -        vlan.dhcp.v6.options['dhcpv6_prm_only'] = True - -    if config['dhcpv6_temporary']: -        vlan.dhcp.v6.options['dhcpv6_temporary'] = True - -    if config['dhcpv6_pd_length']: -        vlan.dhcp.v6.options['dhcpv6_pd_length'] = config['dhcpv6_pd_length'] - -    if config['dhcpv6_pd_interfaces']: -        vlan.dhcp.v6.options['dhcpv6_pd_interfaces'] = config['dhcpv6_pd_interfaces'] - -    # update interface description used e.g. within SNMP -    vlan.set_alias(config['description']) -    # ignore link state changes -    vlan.set_link_detect(config['disable_link_detect']) -    # configure ARP filter configuration -    vlan.set_arp_filter(config['ip_disable_arp_filter']) -    # configure ARP accept -    vlan.set_arp_accept(config['ip_enable_arp_accept']) -    # configure ARP announce -    vlan.set_arp_announce(config['ip_enable_arp_announce']) -    # configure ARP ignore -    vlan.set_arp_ignore(config['ip_enable_arp_ignore']) -    # configure Proxy ARP -    vlan.set_proxy_arp(config['ip_proxy_arp']) -    # IPv6 accept RA -    vlan.set_ipv6_accept_ra(config['ipv6_accept_ra']) -    # IPv6 address autoconfiguration -    vlan.set_ipv6_autoconf(config['ipv6_autoconf']) -    # IPv6 forwarding -    vlan.set_ipv6_forwarding(config['ipv6_forwarding']) -    # IPv6 Duplicate Address Detection (DAD) tries -    vlan.set_ipv6_dad_messages(config['ipv6_dup_addr_detect']) -    # Maximum Transmission Unit (MTU) -    vlan.set_mtu(config['mtu']) - -    # assign/remove VRF (ONLY when not a member of a bridge, -    # otherwise 'nomaster' removes it from it) -    if not config['is_bridge_member']: -        vlan.set_vrf(config['vrf']) - -    # Delete old IPv6 EUI64 addresses before changing MAC -    for addr in config['ipv6_eui64_prefix_remove']: -        vlan.del_ipv6_eui64_address(addr) - -    # Change VLAN interface MAC address -    if config['mac']: -        vlan.set_mac(config['mac']) - -    # Add IPv6 EUI-based addresses -    for addr in config['ipv6_eui64_prefix']: -        vlan.add_ipv6_eui64_address(addr) - -    # enable/disable VLAN interface -    if config['disable']: -        vlan.set_admin_state('down') -    else: -        vlan.set_admin_state('up') - -    # Configure interface address(es) -    # - not longer required addresses get removed first -    # - newly addresses will be added second -    for addr in config['address_remove']: -        vlan.del_addr(addr) -    for addr in config['address']: -        vlan.add_addr(addr) - -    # re-add ourselves to any bridge we might have fallen out of -    if config['is_bridge_member']: -        vlan.add_to_bridge(config['is_bridge_member']) - -def verify_vlan_config(config): -    """ -    Generic function to verify VLAN config consistency. Instead of re- -    implementing this function in multiple places use single source \o/ -    """ - -    # config['vif'] is a dict with ids as keys and config dicts as values -    for vif in config['vif'].values(): -        # DHCPv6 parameters-only and temporary address are mutually exclusive -        if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: -            raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - -        if ( vif['is_bridge_member'] -                and ( vif['address'] -                    or vif['ipv6_eui64_prefix'] -                    or vif['ipv6_autoconf'] ) ): -            raise ConfigError(( -                    f'Cannot assign address to vif interface {vif["intf"]} ' -                    f'which is a member of bridge {vif["is_bridge_member"]}')) - -        if vif['vrf']: -            if vif['vrf'] not in interfaces(): -                raise ConfigError(f'VRF "{vif["vrf"]}" does not exist') - -            if vif['is_bridge_member']: -                raise ConfigError(( -                    f'vif {vif["intf"]} cannot be member of VRF {vif["vrf"]} ' -                    f'and bridge {vif["is_bridge_member"]} at the same time!')) - -    # e.g. wireless interface has no vif_s support -    # thus we bail out eraly. -    if 'vif_s' not in config.keys(): -        return - -    # config['vif_s'] is a dict with ids as keys and config dicts as values -    for vif_s_id, vif_s in config['vif_s'].items(): -        for vif_id, vif in config['vif'].items(): -            if vif_id == vif_s_id: -                raise ConfigError(( -                    f'Cannot use identical ID on vif "{vif["intf"]}" ' -                    f'and vif-s "{vif_s["intf"]}"')) - -        # DHCPv6 parameters-only and temporary address are mutually exclusive -        if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: -            raise ConfigError(( -                'DHCPv6 temporary and parameters-only options are mutually ' -                'exclusive!')) - -        if ( vif_s['is_bridge_member'] -                and ( vif_s['address'] -                    or vif_s['ipv6_eui64_prefix'] -                    or vif_s['ipv6_autoconf'] ) ): -            raise ConfigError(( -                    f'Cannot assign address to vif-s interface {vif_s["intf"]} ' -                    f'which is a member of bridge {vif_s["is_bridge_member"]}')) - -        if vif_s['vrf']: -            if vif_s['vrf'] not in interfaces(): -                raise ConfigError(f'VRF "{vif_s["vrf"]}" does not exist') - -            if vif_s['is_bridge_member']: -                raise ConfigError(( -                    f'vif-s {vif_s["intf"]} cannot be member of VRF {vif_s["vrf"]} ' -                    f'and bridge {vif_s["is_bridge_member"]} at the same time!')) - -        # vif_c is a dict with ids as keys and config dicts as values -        for vif_c in vif_s['vif_c'].values(): -            # DHCPv6 parameters-only and temporary address are mutually exclusive -            if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: -                raise ConfigError(( -                    'DHCPv6 temporary and parameters-only options are ' -                    'mutually exclusive!')) - -            if ( vif_c['is_bridge_member'] -                    and ( vif_c['address'] -                        or vif_c['ipv6_eui64_prefix'] -                        or vif_c['ipv6_autoconf'] ) ): -                raise ConfigError(( -                    f'Cannot assign address to vif-c interface {vif_c["intf"]} ' -                    f'which is a member of bridge {vif_c["is_bridge_member"]}')) - -            if vif_c['vrf']: -                if vif_c['vrf'] not in interfaces(): -                    raise ConfigError(f'VRF "{vif_c["vrf"]}" does not exist') - -                if vif_c['is_bridge_member']: -                    raise ConfigError(( -                    f'vif-c {vif_c["intf"]} cannot be member of VRF {vif_c["vrf"]} ' -                    f'and bridge {vif_c["is_bridge_member"]} at the same time!')) - diff --git a/python/vyos/util.py b/python/vyos/util.py index 7234be6cb..7078762df 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -242,7 +242,7 @@ def chown(path, user, group):      if not os.path.exists(path):          return False -	 +      uid = getpwnam(user).pw_uid      gid = getgrnam(group).gr_gid      os.chown(path, uid, gid) diff --git a/python/vyos/validate.py b/python/vyos/validate.py index a0620e4dd..ceeb6888a 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -279,7 +279,6 @@ def is_member(conf, interface, intftype=None):      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 ' @@ -292,9 +291,9 @@ def is_member(conf, interface, intftype=None):      conf.set_level([])      for it in intftype: -        base = 'interfaces ' + it +        base = ['interfaces', it]          for intf in conf.list_nodes(base): -            memberintf = [base, intf, 'member', 'interface'] +            memberintf = base + [intf, 'member', 'interface']              if xml.is_tag(memberintf):                  if interface in conf.list_nodes(memberintf):                      ret_val = intf diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index a16c4e105..3b238f1ea 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -16,41 +16,25 @@  import os -from copy import deepcopy  from sys import exit  from netifaces import interfaces -from vyos.ifconfig import BondIf -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data  from vyos.config import Config -from vyos.util import call, cmd -from vyos.validate import is_member, has_address_configured +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_vrf +from vyos.ifconfig import BondIf +from vyos.validate import is_member +from vyos.validate import has_address_configured  from vyos import ConfigError -  from vyos import airbag  airbag.enable() -default_config_data = { -    **interface_default_data, -    'arp_mon_intvl': 0, -    'arp_mon_tgt': [], -    'deleted': False, -    'hash_policy': 'layer2', -    'intf': '', -    'ip_arp_cache_tmo': 30, -    'ip_proxy_arp_pvlan': 0, -    'mode': '802.3ad', -    'member': [], -    'shutdown_required': False, -    'primary': '', -    'vif_s': {}, -    'vif_s_remove': [], -    'vif': {}, -    'vif_remove': [], -} - -  def get_bond_mode(mode):      if mode == 'round-robin':          return 'balance-rr' @@ -67,339 +51,138 @@ def get_bond_mode(mode):      elif mode == 'adaptive-load-balance':          return 'balance-alb'      else: -        raise ConfigError('invalid bond mode "{}"'.format(mode)) +        raise ConfigError(f'invalid bond mode "{mode}"')  def get_config(): -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    ifname = os.environ['VYOS_TAGNODE_VALUE'] +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() +    base = ['interfaces', 'bonding'] +    bond = get_interface_dict(conf, base) + +    # To make our own life easier transfor the list of member interfaces +    # into a dictionary - we will use this to add additional information +    # later on for wach member +    if 'member' in bond and 'interface' in bond['member']: +        # first convert it to a list if only one member is given +        if isinstance(bond['member']['interface'], str): +            bond['member']['interface'] = [bond['member']['interface']] + +        tmp={} +        for interface in bond['member']['interface']: +            tmp.update({interface: {}}) + +        bond['member']['interface'] = tmp + +    if 'mode' in bond: +        bond['mode'] = get_bond_mode(bond['mode']) + +    tmp = leaf_node_changed(conf, ['mode']) +    if tmp: +        bond.update({'shutdown_required': ''}) + +    # determine which members have been removed +    tmp = leaf_node_changed(conf, ['member', 'interface']) +    if tmp: +        bond.update({'shutdown_required': ''}) +        if 'member' in bond: +            bond['member'].update({'interface_remove': tmp }) +        else: +            bond.update({'member': {'interface_remove': tmp }}) + +    if 'member' in bond and 'interface' in bond['member']: +        for interface, interface_config in bond['member']['interface'].items(): +            # Check if we are a member of another bond device +            tmp = is_member(conf, interface, 'bridge') +            if tmp: +                interface_config.update({'is_bridge_member' : tmp}) + +            # Check if we are a member of a bond device +            tmp = is_member(conf, interface, 'bonding') +            if tmp and tmp != bond['ifname']: +                interface_config.update({'is_bond_member' : tmp}) -    # initialize kernel module if not loaded -    if not os.path.isfile('/sys/class/net/bonding_masters'): -        import syslog -        syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") -        if call('modprobe bonding max_bonds=0 miimon=250') != 0: -            syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") -            raise ConfigError("failed loading bonding kernel module") - -    # check if bond has been removed -    cfg_base = 'interfaces bonding ' + ifname -    if not conf.exists(cfg_base): -        bond = deepcopy(default_config_data) -        bond['intf'] = ifname -        bond['deleted'] = True -        return bond - -    # set new configuration level -    conf.set_level(cfg_base) - -    bond, disabled = intf_to_dict(conf, default_config_data) - -    # ARP link monitoring frequency in milliseconds -    if conf.exists('arp-monitor interval'): -        bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) - -    # IP address to use for ARP monitoring -    if conf.exists('arp-monitor target'): -        bond['arp_mon_tgt'] = conf.return_values('arp-monitor target') - -    # Bonding transmit hash policy -    if conf.exists('hash-policy'): -        bond['hash_policy'] = conf.return_value('hash-policy') - -    # ARP cache entry timeout in seconds -    if conf.exists('ip arp-cache-timeout'): -        bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - -    # Enable private VLAN proxy ARP on this interface -    if conf.exists('ip proxy-arp-pvlan'): -        bond['ip_proxy_arp_pvlan'] = 1 - -    # Bonding mode -    if conf.exists('mode'): -        act_mode = conf.return_value('mode') -        eff_mode = conf.return_effective_value('mode') -        if not (act_mode == eff_mode): -            bond['shutdown_required'] = True - -        bond['mode'] = get_bond_mode(act_mode) - -    # determine bond member interfaces (currently configured) -    bond['member'] = conf.return_values('member interface') - -    # We can not call conf.return_effective_values() as it would not work -    # on reboots. Reboots/First boot will return that running config and -    # saved config is the same, thus on a reboot the bond members will -    # not be added all (https://phabricator.vyos.net/T2030) -    live_members = BondIf(bond['intf']).get_slaves() -    if not (bond['member'] == live_members): -        bond['shutdown_required'] = True - -    # Primary device interface -    if conf.exists('primary'): -        bond['primary'] = conf.return_value('primary') - -    add_to_dict(conf, disabled, bond, 'vif', 'vif') -    add_to_dict(conf, disabled, bond, 'vif-s', 'vif_s') +            # bond members must not have an assigned address +            tmp = has_address_configured(conf, interface) +            if tmp: +                interface_config.update({'has_address' : ''})      return bond  def verify(bond): -    if bond['deleted']: -        if bond['is_bridge_member']: -            raise ConfigError(( -                f'Cannot delete interface "{bond["intf"]}" as it is a ' -                f'member of bridge "{bond["is_bridge_member"]}"!')) - +    if 'deleted' in bond: +        verify_bridge_delete(bond)          return None -    if len(bond['arp_mon_tgt']) > 16: -        raise ConfigError('The maximum number of arp-monitor targets is 16') +    if 'arp_monitor' in bond: +        if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16: +            raise ConfigError('The maximum number of arp-monitor targets is 16') + +        if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0: +            if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: +                raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ +                                  'transmit-load-balance or adaptive-load-balance') -    if bond['primary']: +    if 'primary' in bond:          if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: -            raise ConfigError(( -                'Mode dependency failed, primary not supported in mode ' -                f'"{bond["mode"]}"!')) - -    if ( bond['is_bridge_member'] -            and ( bond['address'] -                or bond['ipv6_eui64_prefix'] -                or bond['ipv6_autoconf'] ) ): -        raise ConfigError(( -            f'Cannot assign address to interface "{bond["intf"]}" ' -            f'as it is a member of bridge "{bond["is_bridge_member"]}"!')) - -    if bond['vrf']: -        if bond['vrf'] not in interfaces(): -            raise ConfigError(f'VRF "{bond["vrf"]}" does not exist') - -        if bond['is_bridge_member']: -            raise ConfigError(( -                f'Interface "{bond["intf"]}" cannot be member of VRF ' -                f'"{bond["vrf"]}" and bridge {bond["is_bridge_member"]} ' -                f'at the same time!')) +            raise ConfigError('Option primary - mode dependency failed, not' +                              'supported in mode {mode}!'.format(**bond)) + +    verify_address(bond) +    verify_dhcpv6(bond) +    verify_vrf(bond)      # use common function to verify VLAN configuration      verify_vlan_config(bond) -    conf = Config() -    for intf in bond['member']: -        # check if member interface is "real" -        if intf not in interfaces(): -            raise ConfigError(f'Interface {intf} does not exist!') - -        # a bonding member interface is only allowed to be assigned to one bond! -        all_bonds = conf.list_nodes('interfaces bonding') -        # We do not need to check our own bond -        all_bonds.remove(bond['intf']) -        for tmp in all_bonds: -            if conf.exists('interfaces bonding {tmp} member interface {intf}'): -                raise ConfigError(( -                    f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' -                    f'it is already a member of bond "{tmp}"!')) - -        # can not add interfaces with an assigned address to a bond -        if has_address_configured(conf, intf): -            raise ConfigError(( -                f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' -                f'it has an address assigned!')) - -        # bond members are not allowed to be bridge members -        tmp = is_member(conf, intf, 'bridge') -        if tmp: -            raise ConfigError(( -                    f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' -                    f'it is already a member of bridge "{tmp}"!')) - -        # bond members are not allowed to be vrrp members -        for tmp in conf.list_nodes('high-availability vrrp group'): -            if conf.exists('high-availability vrrp group {tmp} interface {intf}'): -                raise ConfigError(( -                    f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' -                    f'it is already a member of VRRP group "{tmp}"!')) - -        # bond members are not allowed to be underlaying psuedo-ethernet devices -        for tmp in conf.list_nodes('interfaces pseudo-ethernet'): -            if conf.exists('interfaces pseudo-ethernet {tmp} link {intf}'): -                raise ConfigError(( -                    f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' -                    f'it is already the link of pseudo-ethernet "{tmp}"!')) - -        # bond members are not allowed to be underlaying vxlan devices -        for tmp in conf.list_nodes('interfaces vxlan'): -            if conf.exists('interfaces vxlan {tmp} link {intf}'): -                raise ConfigError(( -                    f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' -                    f'it is already the link of VXLAN "{tmp}"!')) - -    if bond['primary']: -        if bond['primary'] not in bond['member']: -            raise ConfigError(f'Bond "{bond["intf"]}" primary interface must be a member') +    bond_name = bond['ifname'] +    if 'member' in bond: +        member = bond.get('member') +        for interface, interface_config in member.get('interface', {}).items(): +            error_msg = f'Can not add interface "{interface}" to bond "{bond_name}", ' + +            if interface == 'lo': +                raise ConfigError('Loopback interface "lo" can not be added to a bond') + +            if interface not in interfaces(): +                raise ConfigError(error_msg + 'it does not exist!') + +            if 'is_bridge_member' in interface_config: +                tmp = interface_config['is_bridge_member'] +                raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') + +            if 'is_bond_member' in interface_config: +                tmp = interface_config['is_bond_member'] +                raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') + +            if 'has_address' in interface_config: +                raise ConfigError(error_msg + 'it has an address assigned!') + + +    if 'primary' in bond: +        if bond['primary'] not in bond['member']['interface']: +            raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface')          if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:              raise ConfigError('primary interface only works for mode active-backup, ' \                                'transmit-load-balance or adaptive-load-balance') -    if bond['arp_mon_intvl'] > 0: -        if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: -            raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ -                              'transmit-load-balance or adaptive-load-balance') -      return None  def generate(bond):      return None  def apply(bond): -    b = BondIf(bond['intf']) +    b = BondIf(bond['ifname']) -    if bond['deleted']: +    if 'deleted' in bond:          # delete interface          b.remove()      else: -        # ARP link monitoring frequency, reset miimon when arp-montior is inactive -        # this is done inside BondIf automatically -        b.set_arp_interval(bond['arp_mon_intvl']) - -        # ARP monitor targets need to be synchronized between sysfs and CLI. -        # Unfortunately an address can't be send twice to sysfs as this will -        # result in the following exception:  OSError: [Errno 22] Invalid argument. -        # -        # We remove ALL adresses prior adding new ones, this will remove addresses -        # added manually by the user too - but as we are limited to 16 adresses -        # from the kernel side this looks valid to me. We won't run into an error -        # when a user added manual adresses which would result in having more -        # then 16 adresses in total. -        arp_tgt_addr = list(map(str, b.get_arp_ip_target().split())) -        for addr in arp_tgt_addr: -            b.set_arp_ip_target('-' + addr) - -        # Add configured ARP target addresses -        for addr in bond['arp_mon_tgt']: -            b.set_arp_ip_target('+' + addr) - -        # update interface description used e.g. within SNMP -        b.set_alias(bond['description']) - -        if bond['dhcp_client_id']: -            b.dhcp.v4.options['client_id'] = bond['dhcp_client_id'] - -        if bond['dhcp_hostname']: -            b.dhcp.v4.options['hostname'] = bond['dhcp_hostname'] - -        if bond['dhcp_vendor_class_id']: -            b.dhcp.v4.options['vendor_class_id'] = bond['dhcp_vendor_class_id'] - -        if bond['dhcpv6_prm_only']: -            b.dhcp.v6.options['dhcpv6_prm_only'] = True - -        if bond['dhcpv6_temporary']: -            b.dhcp.v6.options['dhcpv6_temporary'] = True - -        if bond['dhcpv6_pd_length']: -            b.dhcp.v6.options['dhcpv6_pd_length'] = bond['dhcpv6_pd_length'] - -        if bond['dhcpv6_pd_interfaces']: -            b.dhcp.v6.options['dhcpv6_pd_interfaces'] = bond['dhcpv6_pd_interfaces'] - -        # ignore link state changes -        b.set_link_detect(bond['disable_link_detect']) -        # Bonding transmit hash policy -        b.set_hash_policy(bond['hash_policy']) -        # configure ARP cache timeout in milliseconds -        b.set_arp_cache_tmo(bond['ip_arp_cache_tmo']) -        # configure ARP filter configuration -        b.set_arp_filter(bond['ip_disable_arp_filter']) -        # configure ARP accept -        b.set_arp_accept(bond['ip_enable_arp_accept']) -        # configure ARP announce -        b.set_arp_announce(bond['ip_enable_arp_announce']) -        # configure ARP ignore -        b.set_arp_ignore(bond['ip_enable_arp_ignore']) -        # Enable proxy-arp on this interface -        b.set_proxy_arp(bond['ip_proxy_arp']) -        # Enable private VLAN proxy ARP on this interface -        b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan']) -        # IPv6 accept RA -        b.set_ipv6_accept_ra(bond['ipv6_accept_ra']) -        # IPv6 address autoconfiguration -        b.set_ipv6_autoconf(bond['ipv6_autoconf']) -        # IPv6 forwarding -        b.set_ipv6_forwarding(bond['ipv6_forwarding']) -        # IPv6 Duplicate Address Detection (DAD) tries -        b.set_ipv6_dad_messages(bond['ipv6_dup_addr_detect']) - -        # Delete old IPv6 EUI64 addresses before changing MAC -        for addr in bond['ipv6_eui64_prefix_remove']: -            b.del_ipv6_eui64_address(addr) - -        # Change interface MAC address -        if bond['mac']: -            b.set_mac(bond['mac']) - -        # Add IPv6 EUI-based addresses -        for addr in bond['ipv6_eui64_prefix']: -            b.add_ipv6_eui64_address(addr) - -        # Maximum Transmission Unit (MTU) -        b.set_mtu(bond['mtu']) - -        # Primary device interface -        if bond['primary']: -            b.set_primary(bond['primary']) - -        # Some parameters can not be changed when the bond is up. -        if bond['shutdown_required']: -            # Disable bond prior changing of certain properties -            b.set_admin_state('down') - -            # The bonding mode can not be changed when there are interfaces enslaved -            # to this bond, thus we will free all interfaces from the bond first! -            for intf in b.get_slaves(): -                b.del_port(intf) - -            # Bonding policy/mode -            b.set_mode(bond['mode']) - -            # Add (enslave) interfaces to bond -            for intf in bond['member']: -                # if we've come here we already verified the interface doesn't -                # have addresses configured so just flush any remaining ones -                cmd(f'ip addr flush dev "{intf}"') -                b.add_port(intf) - -        # As the bond interface is always disabled first when changing -        # parameters we will only re-enable the interface if it is not -        # administratively disabled -        if not bond['disable']: -            b.set_admin_state('up') -        else: -            b.set_admin_state('down') - -        # Configure interface address(es) -        # - not longer required addresses get removed first -        # - newly addresses will be added second -        for addr in bond['address_remove']: -            b.del_addr(addr) -        for addr in bond['address']: -            b.add_addr(addr) - -        # assign/remove VRF (ONLY when not a member of a bridge, -        # otherwise 'nomaster' removes it from it) -        if not bond['is_bridge_member']: -            b.set_vrf(bond['vrf']) - -        # re-add ourselves to any bridge we might have fallen out of -        if bond['is_bridge_member']: -            b.add_to_bridge(bond['is_bridge_member']) - -        # apply all vlans to interface -        apply_all_vlans(b, bond) +        b.update(bond)      return None diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 1e4fa5816..ee8e85e73 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -16,251 +16,102 @@  import os -from copy import deepcopy  from sys import exit  from netifaces import interfaces -from vyos.ifconfig import BridgeIf, Section -from vyos.ifconfig.stp import STP -from vyos.configdict import list_diff, interface_default_data -from vyos.validate import is_member, has_address_configured  from vyos.config import Config -from vyos.util import cmd, get_bridge_member_config +from vyos.configdict import get_interface_dict +from vyos.configdict import node_changed +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_vrf +from vyos.ifconfig import BridgeIf +from vyos.validate import is_member, has_address_configured +from vyos.xml import defaults + +from vyos.util import cmd  from vyos import ConfigError  from vyos import airbag  airbag.enable() -default_config_data = { -    **interface_default_data, -    'aging': 300, -    'arp_cache_tmo': 30, -    'deleted': False, -    'forwarding_delay': 14, -    'hello_time': 2, -    'igmp_querier': 0, -    'intf': '', -    'max_age': 20, -    'member': [], -    'member_remove': [], -    'priority': 32768, -    'stp': 0 -} -  def get_config(): -    bridge = deepcopy(default_config_data) +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() - -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - -    # Check if bridge has been removed -    if not conf.exists('interfaces bridge ' + bridge['intf']): -        bridge['deleted'] = True -        return bridge - -    # set new configuration level -    conf.set_level('interfaces bridge ' + bridge['intf']) - -    # retrieve configured interface addresses -    if conf.exists('address'): -        bridge['address'] = conf.return_values('address') - -    # Determine interface addresses (currently effective) - to determine which -    # address is no longer valid and needs to be removed -    eff_addr = conf.return_effective_values('address') -    bridge['address_remove'] = list_diff(eff_addr, bridge['address']) - -    # retrieve aging - how long addresses are retained -    if conf.exists('aging'): -        bridge['aging'] = int(conf.return_value('aging')) - -    # retrieve interface description -    if conf.exists('description'): -        bridge['description'] = conf.return_value('description') - -    # get DHCP client identifier -    if conf.exists('dhcp-options client-id'): -        bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - -    # DHCP client host name (overrides the system host name) -    if conf.exists('dhcp-options host-name'): -        bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - -    # DHCP client vendor identifier -    if conf.exists('dhcp-options vendor-class-id'): -        bridge['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') - -    # DHCPv6 only acquire config parameters, no address -    if conf.exists('dhcpv6-options parameters-only'): -        bridge['dhcpv6_prm_only'] = True - -    # DHCPv6 temporary IPv6 address -    if conf.exists('dhcpv6-options temporary'): -        bridge['dhcpv6_temporary'] = True - -    # Disable this bridge interface -    if conf.exists('disable'): -        bridge['disable'] = True - -    # Ignore link state changes -    if conf.exists('disable-link-detect'): -        bridge['disable_link_detect'] = 2 - -    # Forwarding delay -    if conf.exists('forwarding-delay'): -        bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay')) - -    # Hello packet advertisment interval -    if conf.exists('hello-time'): -        bridge['hello_time'] = int(conf.return_value('hello-time')) - -    # Enable Internet Group Management Protocol (IGMP) querier -    if conf.exists('igmp querier'): -        bridge['igmp_querier'] = 1 - -    # ARP cache entry timeout in seconds -    if conf.exists('ip arp-cache-timeout'): -        bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - -    # ARP filter configuration -    if conf.exists('ip disable-arp-filter'): -        bridge['ip_disable_arp_filter'] = 0 - -    # ARP enable accept -    if conf.exists('ip enable-arp-accept'): -        bridge['ip_enable_arp_accept'] = 1 - -    # ARP enable announce -    if conf.exists('ip enable-arp-announce'): -        bridge['ip_enable_arp_announce'] = 1 - -    # ARP enable ignore -    if conf.exists('ip enable-arp-ignore'): -        bridge['ip_enable_arp_ignore'] = 1 - -    # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) -    if conf.exists('ipv6 address autoconf'): -        bridge['ipv6_autoconf'] = 1 - -    # Get prefixes for IPv6 addressing based on MAC address (EUI-64) -    if conf.exists('ipv6 address eui64'): -        bridge['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') - -    # Determine currently effective EUI64 addresses - to determine which -    # address is no longer valid and needs to be removed -    eff_addr = conf.return_effective_values('ipv6 address eui64') -    bridge['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, bridge['ipv6_eui64_prefix']) - -    # Remove the default link-local address if set. -    if conf.exists('ipv6 address no-default-link-local'): -        bridge['ipv6_eui64_prefix_remove'].append('fe80::/64') -    else: -        # add the link-local by default to make IPv6 work -        bridge['ipv6_eui64_prefix'].append('fe80::/64') - -    # Disable IPv6 forwarding on this interface -    if conf.exists('ipv6 disable-forwarding'): -        bridge['ipv6_forwarding'] = 0 - -    # IPv6 Duplicate Address Detection (DAD) tries -    if conf.exists('ipv6 dup-addr-detect-transmits'): -        bridge['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) - -    # Media Access Control (MAC) address -    if conf.exists('mac'): -        bridge['mac'] = conf.return_value('mac') - -    # Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses -    # before re-adding them -    if ( bridge['mac'] and bridge['intf'] in Section.interfaces(section='bridge') -             and bridge['mac'] != BridgeIf(bridge['intf'], create=False).get_mac() ): -        bridge['ipv6_eui64_prefix_remove'] += bridge['ipv6_eui64_prefix'] - -    # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, -    # accept_ra must be 2 -    if bridge['ipv6_autoconf'] or 'dhcpv6' in bridge['address']: -        bridge['ipv6_accept_ra'] = 2 - -    # Interval at which neighbor bridges are removed -    if conf.exists('max-age'): -        bridge['max_age'] = int(conf.return_value('max-age')) - -    # Determine bridge member interface (currently configured) -    for intf in conf.list_nodes('member interface'): -        # defaults are stored in util.py (they can't be here as all interface -        # scripts use the function) -        memberconf = get_bridge_member_config(conf, bridge['intf'], intf) -        if memberconf: -            memberconf['name'] = intf -            bridge['member'].append(memberconf) - -    # Determine bridge member interface (currently effective) - to determine which -    # interfaces is no longer assigend to the bridge and thus can be removed -    eff_intf = conf.list_effective_nodes('member interface') -    act_intf = conf.list_nodes('member interface') -    bridge['member_remove'] = list_diff(eff_intf, act_intf) - -    # Priority for this bridge -    if conf.exists('priority'): -        bridge['priority'] = int(conf.return_value('priority')) - -    # Enable spanning tree protocol -    if conf.exists('stp'): -        bridge['stp'] = 1 - -    # retrieve VRF instance -    if conf.exists('vrf'): -        bridge['vrf'] = conf.return_value('vrf') +    base = ['interfaces', 'bridge'] +    bridge = get_interface_dict(conf, base) + +    # determine which members have been removed +    tmp = node_changed(conf, ['member', 'interface']) +    if tmp: +        if 'member' in bridge: +            bridge['member'].update({'interface_remove': tmp }) +        else: +            bridge.update({'member': {'interface_remove': tmp }}) + +    if 'member' in bridge and 'interface' in bridge['member']: +        # XXX TT2665 we need a copy of the dict keys for iteration, else we will get: +        # RuntimeError: dictionary changed size during iteration +        for interface in list(bridge['member']['interface']): +            for key in ['cost', 'priority']: +                if interface == key: +                    del bridge['member']['interface'][key] +                    continue + +        # the default dictionary is not properly paged into the dict (see T2665) +        # thus we will ammend it ourself +        default_member_values = defaults(base + ['member', 'interface']) +        for interface, interface_config in bridge['member']['interface'].items(): +            interface_config.update(default_member_values) + +            # Check if we are a member of another bridge device +            tmp = is_member(conf, interface, 'bridge') +            if tmp and tmp != bridge['ifname']: +                interface_config.update({'is_bridge_member' : tmp}) + +            # Check if we are a member of a bond device +            tmp = is_member(conf, interface, 'bonding') +            if tmp: +                interface_config.update({'is_bond_member' : tmp}) + +            # Bridge members must not have an assigned address +            tmp = has_address_configured(conf, interface) +            if tmp: +                interface_config.update({'has_address' : ''})      return bridge  def verify(bridge): -    if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']: -        raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') +    if 'deleted' in bridge: +        return None -    vrf_name = bridge['vrf'] -    if vrf_name and vrf_name not in interfaces(): -        raise ConfigError(f'VRF "{vrf_name}" does not exist') +    verify_dhcpv6(bridge) +    verify_vrf(bridge) -    conf = Config() -    for intf in bridge['member']: -        # the interface must exist prior adding it to a bridge -        if intf['name'] not in interfaces(): -            raise ConfigError(( -                f'Cannot add nonexistent interface "{intf["name"]}" ' -                f'to bridge "{bridge["intf"]}"')) +    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 intf['name'] == 'lo': -            raise ConfigError('Loopback interface "lo" can not be added to a bridge') +            if interface == 'lo': +                raise ConfigError('Loopback interface "lo" can not be added to a bridge') -        # bridge members aren't allowed to be members of another bridge -        for br in conf.list_nodes('interfaces bridge'): -            # it makes no sense to verify ourself in this case -            if br == bridge['intf']: -                continue +            if interface not in interfaces(): +                raise ConfigError(error_msg + 'it does not exist!') -            tmp = conf.list_nodes(f'interfaces bridge {br} member interface') -            if intf['name'] in tmp: -                raise ConfigError(( -                    f'Cannot add interface "{intf["name"]}" to bridge ' -                    f'"{bridge["intf"]}", it is already a member of bridge "{br}"!')) +            if 'is_bridge_member' in interface_config: +                tmp = interface_config['is_bridge_member'] +                raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') -        # bridge members are not allowed to be bond members -        tmp = is_member(conf, intf['name'], 'bonding') -        if tmp: -            raise ConfigError(( -                f'Cannot add interface "{intf["name"]}" to bridge ' -                f'"{bridge["intf"]}", it is already a member of bond "{tmp}"!')) +            if 'is_bond_member' in interface_config: +                tmp = interface_config['is_bond_member'] +                raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') -        # bridge members must not have an assigned address -        if has_address_configured(conf, intf['name']): -            raise ConfigError(( -                f'Cannot add interface "{intf["name"]}" to bridge ' -                f'"{bridge["intf"]}", it has an address assigned!')) +            if 'has_address' in interface_config: +                raise ConfigError(error_msg + 'it has an address assigned!')      return None @@ -268,120 +119,12 @@ def generate(bridge):      return None  def apply(bridge): -    br = BridgeIf(bridge['intf']) - -    if bridge['deleted']: +    br = BridgeIf(bridge['ifname']) +    if 'deleted' in bridge:          # delete interface          br.remove()      else: -        # enable interface -        br.set_admin_state('up') -        # set ageing time -        br.set_ageing_time(bridge['aging']) -        # set bridge forward delay -        br.set_forward_delay(bridge['forwarding_delay']) -        # set hello time -        br.set_hello_time(bridge['hello_time']) -        # configure ARP filter configuration -        br.set_arp_filter(bridge['ip_disable_arp_filter']) -        # configure ARP accept -        br.set_arp_accept(bridge['ip_enable_arp_accept']) -        # configure ARP announce -        br.set_arp_announce(bridge['ip_enable_arp_announce']) -        # configure ARP ignore -        br.set_arp_ignore(bridge['ip_enable_arp_ignore']) -        # IPv6 accept RA -        br.set_ipv6_accept_ra(bridge['ipv6_accept_ra']) -        # IPv6 address autoconfiguration -        br.set_ipv6_autoconf(bridge['ipv6_autoconf']) -        # IPv6 forwarding -        br.set_ipv6_forwarding(bridge['ipv6_forwarding']) -        # IPv6 Duplicate Address Detection (DAD) tries -        br.set_ipv6_dad_messages(bridge['ipv6_dup_addr_detect']) -        # set max message age -        br.set_max_age(bridge['max_age']) -        # set bridge priority -        br.set_priority(bridge['priority']) -        # turn stp on/off -        br.set_stp(bridge['stp']) -        # enable or disable IGMP querier -        br.set_multicast_querier(bridge['igmp_querier']) -        # update interface description used e.g. within SNMP -        br.set_alias(bridge['description']) - -        if bridge['dhcp_client_id']: -            br.dhcp.v4.options['client_id'] = bridge['dhcp_client_id'] - -        if bridge['dhcp_hostname']: -            br.dhcp.v4.options['hostname'] = bridge['dhcp_hostname'] - -        if bridge['dhcp_vendor_class_id']: -            br.dhcp.v4.options['vendor_class_id'] = bridge['dhcp_vendor_class_id'] - -        if bridge['dhcpv6_prm_only']: -            br.dhcp.v6.options['dhcpv6_prm_only'] = True - -        if bridge['dhcpv6_temporary']: -            br.dhcp.v6.options['dhcpv6_temporary'] = True - -        if bridge['dhcpv6_pd_length']: -            br.dhcp.v6.options['dhcpv6_pd_length'] = br['dhcpv6_pd_length'] - -        if bridge['dhcpv6_pd_interfaces']: -            br.dhcp.v6.options['dhcpv6_pd_interfaces'] = br['dhcpv6_pd_interfaces'] - -        # assign/remove VRF -        br.set_vrf(bridge['vrf']) - -        # Delete old IPv6 EUI64 addresses before changing MAC -        # (adding members to a fresh bridge changes its MAC too) -        for addr in bridge['ipv6_eui64_prefix_remove']: -            br.del_ipv6_eui64_address(addr) - -        # remove interface from bridge -        for intf in bridge['member_remove']: -            br.del_port(intf) - -        # add interfaces to bridge -        for member in bridge['member']: -            # if we've come here we already verified the interface doesn't -            # have addresses configured so just flush any remaining ones -            cmd(f'ip addr flush dev "{member["name"]}"') -            br.add_port(member['name']) - -        # Change interface MAC address -        if bridge['mac']: -            br.set_mac(bridge['mac']) - -        # Add IPv6 EUI-based addresses (must be done after adding the -        # 1st bridge member or setting its MAC) -        for addr in bridge['ipv6_eui64_prefix']: -            br.add_ipv6_eui64_address(addr) - -        # up/down interface -        if bridge['disable']: -            br.set_admin_state('down') - -        # Configure interface address(es) -        # - not longer required addresses get removed first -        # - newly addresses will be added second -        for addr in bridge['address_remove']: -            br.del_addr(addr) -        for addr in bridge['address']: -            br.add_addr(addr) - -        STPBridgeIf = STP.enable(BridgeIf) -        # configure additional bridge member options -        for member in bridge['member']: -            i = STPBridgeIf(member['name']) -            # configure ARP cache timeout -            i.set_arp_cache_tmo(member['arp_cache_tmo']) -            # ignore link state changes -            i.set_link_detect(member['disable_link_detect']) -            # set bridge port path cost -            i.set_path_cost(member['cost']) -            # set bridge port path priority -            i.set_path_priority(member['priority']) +        br.update(bridge)      return None diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index 2d62420a6..8df86c8ea 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -19,41 +19,23 @@ import os  from sys import exit  from vyos.config import Config +from vyos.configdict import get_interface_dict  from vyos.configverify import verify_vrf  from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete  from vyos.ifconfig import DummyIf -from vyos.validate import is_member  from vyos import ConfigError  from vyos import airbag  airbag.enable()  def get_config(): -    """ Retrive CLI config as dictionary. Dictionary can never be empty, -        as at least the interface name will be added or a deleted flag """ +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() - -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    ifname = os.environ['VYOS_TAGNODE_VALUE'] -    base = ['interfaces', 'dummy', ifname] - -    dummy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) -    # Check if interface has been removed -    if dummy == {}: -        dummy.update({'deleted' : ''}) - -    # store interface instance name in dictionary -    dummy.update({'ifname': ifname}) - -    # check if we are a member of any bridge -    bridge = is_member(conf, ifname, 'bridge') -    if bridge: -        tmp = {'is_bridge_member' : bridge} -        dummy.update(tmp) - +    base = ['interfaces', 'dummy'] +    dummy = get_interface_dict(conf, base)      return dummy  def verify(dummy): diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 8b895c4d2..10758e35a 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -17,295 +17,65 @@  import os  from sys import exit -from copy import deepcopy -from netifaces import interfaces -from vyos.ifconfig import EthernetIf -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data -from vyos.validate import is_member  from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_interface_exists +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_address +from vyos.configverify import verify_vrf +from vyos.configverify import verify_vlan_config +from vyos.ifconfig import EthernetIf  from vyos import ConfigError -  from vyos import airbag  airbag.enable() -default_config_data = { -    **interface_default_data, -    'deleted': False, -    'duplex': 'auto', -    'flow_control': 'on', -    'hw_id': '', -    'ip_arp_cache_tmo': 30, -    'ip_proxy_arp_pvlan': 0, -    'is_bond_member': False, -    'intf': '', -    'offload_gro': 'off', -    'offload_gso': 'off', -    'offload_sg': 'off', -    'offload_tso': 'off', -    'offload_ufo': 'off', -    'speed': 'auto', -    'vif_s': {}, -    'vif_s_remove': [], -    'vif': {}, -    'vif_remove': [], -    'vrf': '' -} - -  def get_config(): -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    ifname = os.environ['VYOS_TAGNODE_VALUE'] +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() +    base = ['interfaces', 'ethernet'] +    ethernet = get_interface_dict(conf, base) +    return ethernet -    # check if ethernet interface has been removed -    cfg_base = ['interfaces', 'ethernet', ifname] -    if not conf.exists(cfg_base): -        eth = deepcopy(default_config_data) -        eth['intf'] = ifname -        eth['deleted'] = True -        # we can not bail out early as ethernet interface can not be removed -        # Kernel will complain with: RTNETLINK answers: Operation not supported. -        # Thus we need to remove individual settings -        return eth - -    # set new configuration level -    conf.set_level(cfg_base) - -    eth, disabled = intf_to_dict(conf, default_config_data) - -    # disable ethernet flow control (pause frames) -    if conf.exists('disable-flow-control'): -        eth['flow_control'] = 'off' - -    # retrieve real hardware address -    if conf.exists('hw-id'): -        eth['hw_id'] = conf.return_value('hw-id') - -    # interface duplex -    if conf.exists('duplex'): -        eth['duplex'] = conf.return_value('duplex') - -    # ARP cache entry timeout in seconds -    if conf.exists('ip arp-cache-timeout'): -        eth['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - -    # Enable private VLAN proxy ARP on this interface -    if conf.exists('ip proxy-arp-pvlan'): -        eth['ip_proxy_arp_pvlan'] = 1 - -    # check if we are a member of any bond -    eth['is_bond_member'] = is_member(conf, eth['intf'], 'bonding') - -    # GRO (generic receive offload) -    if conf.exists('offload-options generic-receive'): -        eth['offload_gro'] = conf.return_value('offload-options generic-receive') - -    # GSO (generic segmentation offload) -    if conf.exists('offload-options generic-segmentation'): -        eth['offload_gso'] = conf.return_value('offload-options generic-segmentation') - -    # scatter-gather option -    if conf.exists('offload-options scatter-gather'): -        eth['offload_sg'] = conf.return_value('offload-options scatter-gather') - -    # TSO (TCP segmentation offloading) -    if conf.exists('offload-options tcp-segmentation'): -        eth['offload_tso'] = conf.return_value('offload-options tcp-segmentation') - -    # UDP fragmentation offloading -    if conf.exists('offload-options udp-fragmentation'): -        eth['offload_ufo'] = conf.return_value('offload-options udp-fragmentation') - -    # interface speed -    if conf.exists('speed'): -        eth['speed'] = conf.return_value('speed') - -    # remove default IPv6 link-local address if member of a bond -    if eth['is_bond_member'] and 'fe80::/64' in eth['ipv6_eui64_prefix']: -        eth['ipv6_eui64_prefix'].remove('fe80::/64') -        eth['ipv6_eui64_prefix_remove'].append('fe80::/64') - -    add_to_dict(conf, disabled, eth, 'vif', 'vif') -    add_to_dict(conf, disabled, eth, 'vif-s', 'vif_s') - -    return eth - - -def verify(eth): -    if eth['deleted']: +def verify(ethernet): +    if 'deleted' in ethernet:          return None -    if eth['intf'] not in interfaces(): -        raise ConfigError(f"Interface ethernet {eth['intf']} does not exist") +    verify_interface_exists(ethernet) -    if eth['speed'] == 'auto': -        if eth['duplex'] != 'auto': +    if ethernet.get('speed', None) == 'auto': +        if ethernet.get('duplex', None) != 'auto':              raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too') -    if eth['duplex'] == 'auto': -        if eth['speed'] != 'auto': +    if ethernet.get('duplex', None) == 'auto': +        if ethernet.get('speed', None) != 'auto':              raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too') -    if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']: -        raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - -    memberof = eth['is_bridge_member'] if eth['is_bridge_member'] else eth['is_bond_member'] - -    if ( memberof -            and ( eth['address'] -                or eth['ipv6_eui64_prefix'] -                or eth['ipv6_autoconf'] ) ): -        raise ConfigError(( -            f'Cannot assign address to interface "{eth["intf"]}" ' -            f'as it is a member of "{memberof}"!')) +    verify_dhcpv6(ethernet) +    verify_address(ethernet) +    verify_vrf(ethernet) -    if eth['vrf']: -        if eth['vrf'] not in interfaces(): -            raise ConfigError(f'VRF "{eth["vrf"]}" does not exist') - -        if memberof: -            raise ConfigError(( -                    f'Interface "{eth["intf"]}" cannot be member of VRF "{eth["vrf"]}" ' -                    f'and "{memberof}" at the same time!')) - -    if eth['mac'] and eth['is_bond_member']: -        print('WARNING: "mac {0}" command will be ignored because {1} is a part of {2}'\ -            .format(eth['mac'], eth['intf'], eth['is_bond_member'])) +    if {'is_bond_member', 'mac'} <= set(ethernet): +        print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" ' +              f'is a member of bond "{is_bond_member}"'.format(**ethernet))      # use common function to verify VLAN configuration -    verify_vlan_config(eth) +    verify_vlan_config(ethernet)      return None -def generate(eth): +def generate(ethernet):      return None -def apply(eth): -    e = EthernetIf(eth['intf']) -    if eth['deleted']: -        # apply all vlans to interface (they need removing too) -        apply_all_vlans(e, eth) - +def apply(ethernet): +    e = EthernetIf(ethernet['ifname']) +    if 'deleted' in ethernet:          # delete interface          e.remove()      else: -        # update interface description used e.g. within SNMP -        e.set_alias(eth['description']) - -        if eth['dhcp_client_id']: -            e.dhcp.v4.options['client_id'] = eth['dhcp_client_id'] - -        if eth['dhcp_hostname']: -            e.dhcp.v4.options['hostname'] = eth['dhcp_hostname'] - -        if eth['dhcp_vendor_class_id']: -            e.dhcp.v4.options['vendor_class_id'] = eth['dhcp_vendor_class_id'] - -        if eth['dhcpv6_prm_only']: -            e.dhcp.v6.options['dhcpv6_prm_only'] = True - -        if eth['dhcpv6_temporary']: -            e.dhcp.v6.options['dhcpv6_temporary'] = True - -        if eth['dhcpv6_pd_length']: -            e.dhcp.v6.options['dhcpv6_pd_length'] = eth['dhcpv6_pd_length'] - -        if eth['dhcpv6_pd_interfaces']: -            e.dhcp.v6.options['dhcpv6_pd_interfaces'] = eth['dhcpv6_pd_interfaces'] - -        # ignore link state changes -        e.set_link_detect(eth['disable_link_detect']) -        # disable ethernet flow control (pause frames) -        e.set_flow_control(eth['flow_control']) -        # configure ARP cache timeout in milliseconds -        e.set_arp_cache_tmo(eth['ip_arp_cache_tmo']) -        # configure ARP filter configuration -        e.set_arp_filter(eth['ip_disable_arp_filter']) -        # configure ARP accept -        e.set_arp_accept(eth['ip_enable_arp_accept']) -        # configure ARP announce -        e.set_arp_announce(eth['ip_enable_arp_announce']) -        # configure ARP ignore -        e.set_arp_ignore(eth['ip_enable_arp_ignore']) -        # Enable proxy-arp on this interface -        e.set_proxy_arp(eth['ip_proxy_arp']) -        # Enable private VLAN proxy ARP on this interface -        e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan']) -        # IPv6 accept RA -        e.set_ipv6_accept_ra(eth['ipv6_accept_ra']) -        # IPv6 address autoconfiguration -        e.set_ipv6_autoconf(eth['ipv6_autoconf']) -        # IPv6 forwarding -        e.set_ipv6_forwarding(eth['ipv6_forwarding']) -        # IPv6 Duplicate Address Detection (DAD) tries -        e.set_ipv6_dad_messages(eth['ipv6_dup_addr_detect']) - -        # Delete old IPv6 EUI64 addresses before changing MAC -        for addr in eth['ipv6_eui64_prefix_remove']: -            e.del_ipv6_eui64_address(addr) - -        # Change interface MAC address - re-set to real hardware address (hw-id) -        # if custom mac is removed. Skip if bond member. -        if not eth['is_bond_member']: -            if eth['mac']: -                e.set_mac(eth['mac']) -            elif eth['hw_id']: -                e.set_mac(eth['hw_id']) - -        # Add IPv6 EUI-based addresses -        for addr in eth['ipv6_eui64_prefix']: -            e.add_ipv6_eui64_address(addr) - -        # Maximum Transmission Unit (MTU) -        e.set_mtu(eth['mtu']) - -        # GRO (generic receive offload) -        e.set_gro(eth['offload_gro']) - -        # GSO (generic segmentation offload) -        e.set_gso(eth['offload_gso']) - -        # scatter-gather option -        e.set_sg(eth['offload_sg']) - -        # TSO (TCP segmentation offloading) -        e.set_tso(eth['offload_tso']) - -        # UDP fragmentation offloading -        e.set_ufo(eth['offload_ufo']) - -        # Set physical interface speed and duplex -        e.set_speed_duplex(eth['speed'], eth['duplex']) - -        # Enable/Disable interface -        if eth['disable']: -            e.set_admin_state('down') -        else: -            e.set_admin_state('up') - -        # Configure interface address(es) -        # - not longer required addresses get removed first -        # - newly addresses will be added second -        for addr in eth['address_remove']: -            e.del_addr(addr) -        for addr in eth['address']: -            e.add_addr(addr) - -        # assign/remove VRF (ONLY when not a member of a bridge or bond, -        # otherwise 'nomaster' removes it from it) -        if not ( eth['is_bridge_member'] or eth['is_bond_member'] ): -            e.set_vrf(eth['vrf']) - -        # re-add ourselves to any bridge we might have fallen out of -        if eth['is_bridge_member']: -            e.add_to_bridge(eth['is_bridge_member']) - -        # apply all vlans to interface -        apply_all_vlans(e, eth) +        e.update(ethernet)  if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index 31f6eb6b5..1104bd3c0 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -21,102 +21,37 @@ from copy import deepcopy  from netifaces import interfaces  from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete  from vyos.ifconfig import GeneveIf -from vyos.validate import is_member  from vyos import ConfigError  from vyos import airbag  airbag.enable() -default_config_data = { -    'address': [], -    'deleted': False, -    'description': '', -    'disable': False, -    'intf': '', -    'ip_arp_cache_tmo': 30, -    'ip_proxy_arp': 0, -    'is_bridge_member': False, -    'mtu': 1500, -    'remote': '', -    'vni': '' -} -  def get_config(): -    geneve = deepcopy(default_config_data) +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() - -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - -    # check if interface is member if a bridge -    geneve['is_bridge_member'] = is_member(conf, geneve['intf'], 'bridge') - -    # Check if interface has been removed -    if not conf.exists('interfaces geneve ' + geneve['intf']): -        geneve['deleted'] = True -        return geneve - -    # set new configuration level -    conf.set_level('interfaces geneve ' + geneve['intf']) - -    # retrieve configured interface addresses -    if conf.exists('address'): -        geneve['address'] = conf.return_values('address') - -    # retrieve interface description -    if conf.exists('description'): -        geneve['description'] = conf.return_value('description') - -    # Disable this interface -    if conf.exists('disable'): -        geneve['disable'] = True - -    # ARP cache entry timeout in seconds -    if conf.exists('ip arp-cache-timeout'): -        geneve['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - -    # Enable proxy-arp on this interface -    if conf.exists('ip enable-proxy-arp'): -        geneve['ip_proxy_arp'] = 1 - -    # Maximum Transmission Unit (MTU) -    if conf.exists('mtu'): -        geneve['mtu'] = int(conf.return_value('mtu')) - -    # Remote address of GENEVE tunnel -    if conf.exists('remote'): -        geneve['remote'] = conf.return_value('remote') - -    # Virtual Network Identifier -    if conf.exists('vni'): -        geneve['vni'] = conf.return_value('vni') - +    base = ['interfaces', 'geneve'] +    geneve = get_interface_dict(conf, base)      return geneve -  def verify(geneve): -    if geneve['deleted']: -        if geneve['is_bridge_member']: -            raise ConfigError(( -                f'Cannot delete interface "{geneve["intf"]}" as it is a ' -                f'member of bridge "{geneve["is_bridge_member"]}"!')) - +    if 'deleted' in geneve: +        verify_bridge_delete(geneve)          return None -    if geneve['is_bridge_member'] and geneve['address']: -        raise ConfigError(( -            f'Cannot assign address to interface "{geneve["intf"]}" ' -            f'as it is a member of bridge "{geneve["is_bridge_member"]}"!')) +    verify_address(geneve) -    if not geneve['remote']: -        raise ConfigError('GENEVE remote must be configured') +    if 'remote' not in geneve: +        raise ConfigError('Remote side must be configured') -    if not geneve['vni']: -        raise ConfigError('GENEVE VNI must be configured') +    if 'vni' not in geneve: +        raise ConfigError('VNI must be configured')      return None @@ -127,13 +62,13 @@ def generate(geneve):  def apply(geneve):      # Check if GENEVE interface already exists -    if geneve['intf'] in interfaces(): -        g = GeneveIf(geneve['intf']) +    if geneve['ifname'] in interfaces(): +        g = GeneveIf(geneve['ifname'])          # GENEVE is super picky and the tunnel always needs to be recreated,          # thus we can simply always delete it first.          g.remove() -    if not geneve['deleted']: +    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 @@ -144,32 +79,8 @@ def apply(geneve):          conf['remote'] = geneve['remote']          # Finally create the new interface -        g = GeneveIf(geneve['intf'], **conf) -        # update interface description used e.g. by SNMP -        g.set_alias(geneve['description']) -        # Maximum Transfer Unit (MTU) -        g.set_mtu(geneve['mtu']) - -        # configure ARP cache timeout in milliseconds -        g.set_arp_cache_tmo(geneve['ip_arp_cache_tmo']) -        # Enable proxy-arp on this interface -        g.set_proxy_arp(geneve['ip_proxy_arp']) - -        # Configure interface address(es) - no need to implicitly delete the -        # old addresses as they have already been removed by deleting the -        # interface above -        for addr in geneve['address']: -            g.add_addr(addr) - -        # As the GENEVE interface is always disabled first when changing -        # parameters we will only re-enable the interface if it is not -        # administratively disabled -        if not geneve['disable']: -            g.set_admin_state('up') - -        # re-add ourselves to any bridge we might have fallen out of -        if geneve['is_bridge_member']: -            g.add_to_bridge(geneve['is_bridge_member']) +        g = GeneveIf(geneve['ifname'], **conf) +        g.update(geneve)      return None diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py index 2368f88a9..0398cd591 100755 --- a/src/conf_mode/interfaces-loopback.py +++ b/src/conf_mode/interfaces-loopback.py @@ -18,31 +18,21 @@ import os  from sys import exit -from vyos.ifconfig import LoopbackIf  from vyos.config import Config -from vyos import ConfigError, airbag +from vyos.configdict import get_interface_dict +from vyos.ifconfig import LoopbackIf +from vyos import ConfigError +from vyos import airbag  airbag.enable()  def get_config(): -    """ Retrive CLI config as dictionary. Dictionary can never be empty, -        as at least the interface name will be added or a deleted flag """ +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() - -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    ifname = os.environ['VYOS_TAGNODE_VALUE'] -    base = ['interfaces', 'loopback', ifname] - -    loopback = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) -    # Check if interface has been removed -    if loopback == {}: -        loopback.update({'deleted' : ''}) - -    # store interface instance name in dictionary -    loopback.update({'ifname': ifname}) - +    base = ['interfaces', 'loopback'] +    loopback = get_interface_dict(conf, base)      return loopback  def verify(loopback): diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 56273f71a..ca15212d4 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -20,16 +20,14 @@ from copy import deepcopy  from sys import exit  from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict  from vyos.ifconfig import MACsecIf  from vyos.template import render  from vyos.util import call -from vyos.validate import is_member  from vyos.configverify import verify_vrf  from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete  from vyos.configverify import verify_source_interface -from vyos.xml import defaults  from vyos import ConfigError  from vyos import airbag  airbag.enable() @@ -38,50 +36,25 @@ airbag.enable()  wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf'  def get_config(): -    """ Retrive CLI config as dictionary. Dictionary can never be empty, -    as at least the interface name will be added or a deleted flag """ +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() - -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    # retrieve interface default values      base = ['interfaces', 'macsec'] -    default_values = defaults(base) +    macsec = get_interface_dict(conf, base) -    ifname = os.environ['VYOS_TAGNODE_VALUE'] -    base = base + [ifname] - -    macsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)      # Check if interface has been removed -    if macsec == {}: -        tmp = { -            'deleted' : '', -            'source_interface' : conf.return_effective_value( +    if 'deleted' in macsec: +        source_interface = conf.return_effective_value(                  base + ['source-interface']) -        } -        macsec.update(tmp) - -    # We have gathered the dict representation of the CLI, but there are -    # default options which we need to update into the dictionary -    # retrived. -    macsec = dict_merge(default_values, macsec) - -    # Add interface instance name into dictionary -    macsec.update({'ifname': ifname}) - -    # Check if we are a member of any bridge -    bridge = is_member(conf, ifname, 'bridge') -    if bridge: -        tmp = {'is_bridge_member' : bridge} -        macsec.update(tmp) +        macsec.update({'source_interface': source_interface})      return macsec  def verify(macsec): -    if 'deleted' in macsec.keys(): +    if 'deleted' in macsec:          verify_bridge_delete(macsec)          return None @@ -89,18 +62,18 @@ def verify(macsec):      verify_vrf(macsec)      verify_address(macsec) -    if not (('security' in macsec.keys()) and -            ('cipher' in macsec['security'].keys())): +    if not (('security' in macsec) and +            ('cipher' in macsec['security'])):          raise ConfigError(              'Cipher suite must be set for MACsec "{ifname}"'.format(**macsec)) -    if (('security' in macsec.keys()) and -        ('encrypt' in macsec['security'].keys())): +    if (('security' in macsec) and +        ('encrypt' in macsec['security'])):          tmp = macsec.get('security') -        if not (('mka' in tmp.keys()) and -                ('cak' in tmp['mka'].keys()) and -                ('ckn' in tmp['mka'].keys())): +        if not (('mka' in tmp) and +                ('cak' in tmp['mka']) and +                ('ckn' in tmp['mka'])):              raise ConfigError('Missing mandatory MACsec security '                                'keys as encryption is enabled!') diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 3ee57e83c..b9a88a949 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -22,51 +22,34 @@ from copy import deepcopy  from netifaces import interfaces  from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict  from vyos.configverify import verify_source_interface  from vyos.configverify import verify_vrf  from vyos.template import render  from vyos.util import call -from vyos.xml import defaults  from vyos import ConfigError  from vyos import airbag  airbag.enable()  def get_config(): -    """ Retrive CLI config as dictionary. Dictionary can never be empty, -    as at least the interface name will be added or a deleted flag """ +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() - -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    # retrieve interface default values      base = ['interfaces', 'pppoe'] -    default_values = defaults(base) -    # PPPoE is "special" the default MTU is 1492 - update accordingly -    default_values['mtu'] = '1492' - -    ifname = os.environ['VYOS_TAGNODE_VALUE'] -    base = base + [ifname] +    pppoe = get_interface_dict(conf, base) -    pppoe = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) -    # Check if interface has been removed -    if pppoe == {}: -        pppoe.update({'deleted' : ''}) - -    # We have gathered the dict representation of the CLI, but there are -    # default options which we need to update into the dictionary -    # retrived. -    pppoe = dict_merge(default_values, pppoe) - -    # Add interface instance name into dictionary -    pppoe.update({'ifname': ifname}) +    # PPPoE is "special" the default MTU is 1492 - update accordingly +    # as the config_level is already st in get_interface_dict() - we can use [] +    tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) +    if 'mtu' not in tmp: +        pppoe['mtu'] = '1492'      return pppoe  def verify(pppoe): -    if 'deleted' in pppoe.keys(): +    if 'deleted' in pppoe:          # bail out early          return None @@ -92,7 +75,7 @@ def generate(pppoe):      config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up,                      script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c] -    if 'deleted' in pppoe.keys(): +    if 'deleted' in pppoe:          # stop DHCPv6-PD client          call(f'systemctl stop dhcp6c@{ifname}.service')          # Hang-up PPPoE connection @@ -130,11 +113,11 @@ def generate(pppoe):      return None  def apply(pppoe): -    if 'deleted' in pppoe.keys(): +    if 'deleted' in pppoe:          # bail out early          return None -    if 'disable' not in pppoe.keys(): +    if 'disable' not in pppoe:          # Dial PPPoE connection          call('systemctl restart ppp@{ifname}.service'.format(**pppoe)) diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index fb8237bee..4afea2b3a 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -18,115 +18,44 @@ import os  from copy import deepcopy  from sys import exit -from netifaces import interfaces  from vyos.config import Config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data -from vyos.ifconfig import MACVLANIf, Section -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.ifconfig import MACVLANIf  from vyos import ConfigError  from vyos import airbag  airbag.enable() -default_config_data = { -    **interface_default_data, -    'deleted': False, -    'intf': '', -    'ip_arp_cache_tmo': 30, -    'ip_proxy_arp_pvlan': 0, -    'source_interface': '', -    'recreating_required': False, -    'mode': 'private', -    'vif_s': {}, -    'vif_s_remove': [], -    'vif': {}, -    'vif_remove': [], -    'vrf': '' -} -  def get_config(): -    peth = deepcopy(default_config_data) +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() +    base = ['interfaces', 'pseudo-ethernet'] +    peth = get_interface_dict(conf, base) -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    peth['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - -    # Check if interface has been removed -    cfg_base = ['interfaces', 'pseudo-ethernet', peth['intf']] -    if not conf.exists(cfg_base): -        peth['deleted'] = True -        return peth - -    # set new configuration level -    conf.set_level(cfg_base) - -    peth, disabled = intf_to_dict(conf, default_config_data) - -    # ARP cache entry timeout in seconds -    if conf.exists(['ip', 'arp-cache-timeout']): -        peth['ip_arp_cache_tmo'] = int(conf.return_value(['ip', 'arp-cache-timeout'])) - -    # Enable private VLAN proxy ARP on this interface -    if conf.exists(['ip', 'proxy-arp-pvlan']): -        peth['ip_proxy_arp_pvlan'] = 1 - -    # Physical interface -    if conf.exists(['source-interface']): -        peth['source_interface'] = conf.return_value(['source-interface']) -        tmp = conf.return_effective_value(['source-interface']) -        if tmp != peth['source_interface']: -            peth['recreating_required'] = True - -    # MACvlan mode -    if conf.exists(['mode']): -        peth['mode'] = conf.return_value(['mode']) -        tmp = conf.return_effective_value(['mode']) -        if tmp != peth['mode']: -            peth['recreating_required'] = True - -    add_to_dict(conf, disabled, peth, 'vif', 'vif') -    add_to_dict(conf, disabled, peth, 'vif-s', 'vif_s') +    mode = leaf_node_changed(conf, ['mode']) +    if mode: +        peth.update({'mode_old' : mode})      return peth  def verify(peth): -    if peth['deleted']: -        if peth['is_bridge_member']: -            raise ConfigError(( -                f'Cannot delete interface "{peth["intf"]}" as it is a ' -                f'member of bridge "{peth["is_bridge_member"]}"!')) - +    if 'deleted' in peth: +        verify_bridge_delete(peth)          return None -    if not peth['source_interface']: -        raise ConfigError(( -            f'Link device must be set for pseudo-ethernet "{peth["intf"]}"')) - -    if not peth['source_interface'] in interfaces(): -        raise ConfigError(( -            f'Pseudo-ethernet "{peth["intf"]}" link device does not exist')) - -    if ( peth['is_bridge_member'] -            and ( peth['address'] -                or peth['ipv6_eui64_prefix'] -                or peth['ipv6_autoconf'] ) ): -        raise ConfigError(( -            f'Cannot assign address to interface "{peth["intf"]}" ' -            f'as it is a member of bridge "{peth["is_bridge_member"]}"!')) - -    if peth['vrf']: -        if peth['vrf'] not in interfaces(): -            raise ConfigError(f'VRF "{peth["vrf"]}" does not exist') - -        if peth['is_bridge_member']: -            raise ConfigError(( -                f'Interface "{peth["intf"]}" cannot be member of VRF ' -                f'"{peth["vrf"]}" and bridge {peth["is_bridge_member"]} ' -                f'at the same time!')) +    verify_source_interface(peth) +    verify_vrf(peth) +    verify_address(peth)      # use common function to verify VLAN configuration      verify_vlan_config(peth) @@ -136,17 +65,16 @@ def generate(peth):      return None  def apply(peth): -    if peth['deleted']: +    if 'deleted' in peth:          # delete interface -        MACVLANIf(peth['intf']).remove() +        MACVLANIf(peth['ifname']).remove()          return None      # Check if MACVLAN interface already exists. Parameters like the underlaying      # source-interface device or mode can not be changed on the fly and the interface      # needs to be recreated from the bottom. -    if peth['intf'] in interfaces(): -        if peth['recreating_required']: -            MACVLANIf(peth['intf']).remove() +    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 @@ -158,98 +86,8 @@ def apply(peth):      # It is safe to "re-create" the interface always, there is a sanity check      # that the interface will only be create if its non existent -    p = MACVLANIf(peth['intf'], **conf) - -    # update interface description used e.g. within SNMP -    p.set_alias(peth['description']) - -    if peth['dhcp_client_id']: -        p.dhcp.v4.options['client_id'] = peth['dhcp_client_id'] - -    if peth['dhcp_hostname']: -        p.dhcp.v4.options['hostname'] = peth['dhcp_hostname'] - -    if peth['dhcp_vendor_class_id']: -        p.dhcp.v4.options['vendor_class_id'] = peth['dhcp_vendor_class_id'] - -    if peth['dhcpv6_prm_only']: -        p.dhcp.v6.options['dhcpv6_prm_only'] = True - -    if peth['dhcpv6_temporary']: -        p.dhcp.v6.options['dhcpv6_temporary'] = True - -    if peth['dhcpv6_pd_length']: -        p.dhcp.v6.options['dhcpv6_pd_length'] = peth['dhcpv6_pd_length'] - -    if peth['dhcpv6_pd_interfaces']: -        p.dhcp.v6.options['dhcpv6_pd_interfaces'] = peth['dhcpv6_pd_interfaces'] - -    # ignore link state changes -    p.set_link_detect(peth['disable_link_detect']) -    # configure ARP cache timeout in milliseconds -    p.set_arp_cache_tmo(peth['ip_arp_cache_tmo']) -    # configure ARP filter configuration -    p.set_arp_filter(peth['ip_disable_arp_filter']) -    # configure ARP accept -    p.set_arp_accept(peth['ip_enable_arp_accept']) -    # configure ARP announce -    p.set_arp_announce(peth['ip_enable_arp_announce']) -    # configure ARP ignore -    p.set_arp_ignore(peth['ip_enable_arp_ignore']) -    # Enable proxy-arp on this interface -    p.set_proxy_arp(peth['ip_proxy_arp']) -    # Enable private VLAN proxy ARP on this interface -    p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan']) -    # IPv6 accept RA -    p.set_ipv6_accept_ra(peth['ipv6_accept_ra']) -    # IPv6 address autoconfiguration -    p.set_ipv6_autoconf(peth['ipv6_autoconf']) -    # IPv6 forwarding -    p.set_ipv6_forwarding(peth['ipv6_forwarding']) -    # IPv6 Duplicate Address Detection (DAD) tries -    p.set_ipv6_dad_messages(peth['ipv6_dup_addr_detect']) - -    # assign/remove VRF (ONLY when not a member of a bridge, -    # otherwise 'nomaster' removes it from it) -    if not peth['is_bridge_member']: -        p.set_vrf(peth['vrf']) - -    # Delete old IPv6 EUI64 addresses before changing MAC -    for addr in peth['ipv6_eui64_prefix_remove']: -        p.del_ipv6_eui64_address(addr) - -    # Change interface MAC address -    if peth['mac']: -        p.set_mac(peth['mac']) - -    # Add IPv6 EUI-based addresses -    for addr in peth['ipv6_eui64_prefix']: -        p.add_ipv6_eui64_address(addr) - -    # Change interface mode -    p.set_mode(peth['mode']) - -    # Enable/Disable interface -    if peth['disable']: -        p.set_admin_state('down') -    else: -        p.set_admin_state('up') - -    # Configure interface address(es) -    # - not longer required addresses get removed first -    # - newly addresses will be added second -    for addr in peth['address_remove']: -        p.del_addr(addr) -    for addr in peth['address']: -        p.add_addr(addr) - -    # re-add ourselves to any bridge we might have fallen out of -    if peth['is_bridge_member']: -        p.add_to_bridge(peth['is_bridge_member']) - -    # apply all vlans to interface -    apply_all_vlans(p, peth) - +    p = MACVLANIf(peth['ifname'], **conf) +    p.update(peth)      return None  if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 0162b642c..b6f247952 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -15,497 +15,163 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os +  from sys import exit  from re import findall -  from copy import deepcopy - -from netifaces import interfaces  from netaddr import EUI, mac_unix_expanded  from vyos.config import Config -from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data -from vyos.ifconfig import WiFiIf, Section -from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config +from vyos.configdict import get_interface_dict +from vyos.configdict import dict_merge +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_vrf +from vyos.ifconfig import WiFiIf  from vyos.template import render -from vyos.util import chown, call -from vyos.validate import is_member +from vyos.util import call  from vyos import ConfigError -  from vyos import airbag  airbag.enable() -default_config_data = { -    **interface_default_data, -    'cap_ht' : False, -    'cap_ht_40mhz_incapable' : False, -    'cap_ht_powersave' : False, -    'cap_ht_chan_set_width' : '', -    'cap_ht_delayed_block_ack' : False, -    'cap_ht_dsss_cck_40' : False, -    'cap_ht_greenfield' : False, -    'cap_ht_ldpc' : False, -    'cap_ht_lsig_protection' : False, -    'cap_ht_max_amsdu' : '', -    'cap_ht_short_gi' :  [], -    'cap_ht_smps' : '', -    'cap_ht_stbc_rx' : '', -    'cap_ht_stbc_tx' : False, -    'cap_req_ht' : False, -    'cap_req_vht' : False, -    'cap_vht' : False, -    'cap_vht_antenna_cnt' : '', -    'cap_vht_antenna_fixed' : False, -    'cap_vht_beamform' : '', -    'cap_vht_center_freq_1' : '', -    'cap_vht_center_freq_2' : '', -    'cap_vht_chan_set_width' : '', -    'cap_vht_ldpc' : False, -    'cap_vht_link_adaptation' : '', -    'cap_vht_max_mpdu_exp' : '', -    'cap_vht_max_mpdu' : '', -    'cap_vht_short_gi' : [], -    'cap_vht_stbc_rx' : '', -    'cap_vht_stbc_tx' : False, -    'cap_vht_tx_powersave' : False, -    'cap_vht_vht_cf' : False, -    'channel': '', -    'country_code': '', -    'deleted': False, -    'disable_broadcast_ssid' : False, -    'disable_link_detect' : 1, -    'expunge_failing_stations' : False, -    'hw_id' : '', -    'intf': '', -    'isolate_stations' : False, -    'max_stations' : '', -    'mgmt_frame_protection' : 'disabled', -    'mode' : 'g', -    'phy' : '', -    'reduce_transmit_power' : '', -    'sec_wep' : False, -    'sec_wep_key' : [], -    'sec_wpa' : False, -    'sec_wpa_cipher' : [], -    'sec_wpa_mode' : 'both', -    'sec_wpa_passphrase' : '', -    'sec_wpa_radius' : [], -    'ssid' : '', -    'op_mode' : 'monitor', -    'vif': {}, -    'vif_remove': [], -    'vif_s': {}, -    'vif_s_remove': [] -} -  # XXX: wpa_supplicant works on the source interface -wpa_suppl_conf = '/run/wpa_supplicant/{intf}.conf' -hostapd_conf = '/run/hostapd/{intf}.conf' +wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf' +hostapd_conf = '/run/hostapd/{ifname}.conf' + +def find_other_stations(conf, base, ifname): +    """ +    Only one wireless interface per phy can be in station mode - +    find all interfaces attached to a phy which run in station mode +    """ +    old_level = conf.get_level() +    conf.set_level(base) +    dict = {} +    for phy in os.listdir('/sys/class/ieee80211'): +        list = [] +        for interface in conf.list_nodes([]): +            if interface == ifname: +                continue +            # the following node is mandatory +            if conf.exists([interface, 'physical-device', phy]): +                tmp = conf.return_value([interface, 'type']) +                if tmp == 'station': +                    list.append(interface) +        if list: +            dict.update({phy: list}) +    conf.set_level(old_level) +    return dict  def get_config(): -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    ifname = os.environ['VYOS_TAGNODE_VALUE'] +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() - -    # check if wireless interface has been removed -    cfg_base = ['interfaces', 'wireless ', ifname] -    if not conf.exists(cfg_base): -        wifi = deepcopy(default_config_data) -        wifi['intf'] = ifname -        wifi['deleted'] = True -        # we need to know if we're a bridge member so we can refuse deletion -        wifi['is_bridge_member'] = is_member(conf, wifi['intf'], 'bridge') -        # we can not bail out early as wireless interface can not be removed -        # Kernel will complain with: RTNETLINK answers: Operation not supported. -        # Thus we need to remove individual settings -        return wifi - -    # set new configuration level -    conf.set_level(cfg_base) - -    # get common interface settings -    wifi, disabled = intf_to_dict(conf, default_config_data) - -    # 40MHz intolerance, use 20MHz only -    if conf.exists('capabilities ht 40mhz-incapable'): -        wifi['cap_ht'] = True -        wifi['cap_ht_40mhz_incapable'] = True - -    # WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] -    if conf.exists('capabilities ht auto-powersave'): -        wifi['cap_ht'] = True -        wifi['cap_ht_powersave'] = True - -    # Supported channel set width -    if conf.exists('capabilities ht channel-set-width'): -        wifi['cap_ht'] = True -        wifi['cap_ht_chan_set_width'] = conf.return_values('capabilities ht channel-set-width') - -    # HT-delayed Block Ack -    if conf.exists('capabilities ht delayed-block-ack'): -        wifi['cap_ht'] = True -        wifi['cap_ht_delayed_block_ack'] = True - -    # DSSS/CCK Mode in 40 MHz -    if conf.exists('capabilities ht dsss-cck-40'): -        wifi['cap_ht'] = True -        wifi['cap_ht_dsss_cck_40'] = True - -    # HT-greenfield capability -    if conf.exists('capabilities ht greenfield'): -        wifi['cap_ht'] = True -        wifi['cap_ht_greenfield'] = True - -    # LDPC coding capability -    if conf.exists('capabilities ht ldpc'): -        wifi['cap_ht'] = True -        wifi['cap_ht_ldpc'] = True - -    # L-SIG TXOP protection capability -    if conf.exists('capabilities ht lsig-protection'): -        wifi['cap_ht'] = True -        wifi['cap_ht_lsig_protection'] = True - -    # Set Maximum A-MSDU length -    if conf.exists('capabilities ht max-amsdu'): -        wifi['cap_ht'] = True -        wifi['cap_ht_max_amsdu'] = conf.return_value('capabilities ht max-amsdu') - -    # Short GI capabilities -    if conf.exists('capabilities ht short-gi'): -        wifi['cap_ht'] = True -        wifi['cap_ht_short_gi'] = conf.return_values('capabilities ht short-gi') - -    # Spatial Multiplexing Power Save (SMPS) settings -    if conf.exists('capabilities ht smps'): -        wifi['cap_ht'] = True -        wifi['cap_ht_smps'] = conf.return_value('capabilities ht smps') - -    # Support for receiving PPDU using STBC (Space Time Block Coding) -    if conf.exists('capabilities ht stbc rx'): -        wifi['cap_ht'] = True -        wifi['cap_ht_stbc_rx'] = conf.return_value('capabilities ht stbc rx') - -    # Support for sending PPDU using STBC (Space Time Block Coding) -    if conf.exists('capabilities ht stbc tx'): -        wifi['cap_ht'] = True -        wifi['cap_ht_stbc_tx'] = True - -    # Require stations to support HT PHY (reject association if they do not) -    if conf.exists('capabilities require-ht'): -        wifi['cap_req_ht'] = True - -    # Require stations to support VHT PHY (reject association if they do not) -    if conf.exists('capabilities require-vht'): -        wifi['cap_req_vht'] = True - -    # Number of antennas on this card -    if conf.exists('capabilities vht antenna-count'): -        wifi['cap_vht'] = True -        wifi['cap_vht_antenna_cnt'] = conf.return_value('capabilities vht antenna-count') - -    # set if antenna pattern does not change during the lifetime of an association -    if conf.exists('capabilities vht antenna-pattern-fixed'): -        wifi['cap_vht'] = True -        wifi['cap_vht_antenna_fixed'] = True - -    # Beamforming capabilities -    if conf.exists('capabilities vht beamform'): -        wifi['cap_vht'] = True -        wifi['cap_vht_beamform'] = conf.return_values('capabilities vht beamform') - -    # VHT operating channel center frequency - center freq 1 (for use with 80, 80+80 and 160 modes) -    if conf.exists('capabilities vht center-channel-freq freq-1'): -        wifi['cap_vht'] = True -        wifi['cap_vht_center_freq_1'] = conf.return_value('capabilities vht center-channel-freq freq-1') - -    # VHT operating channel center frequency - center freq 2 (for use with the 80+80 mode) -    if conf.exists('capabilities vht center-channel-freq freq-2'): -        wifi['cap_vht'] = True -        wifi['cap_vht_center_freq_2'] = conf.return_value('capabilities vht center-channel-freq freq-2') - -    # VHT operating Channel width -    if conf.exists('capabilities vht channel-set-width'): -        wifi['cap_vht'] = True -        wifi['cap_vht_chan_set_width'] = conf.return_value('capabilities vht channel-set-width') - -    # LDPC coding capability -    if conf.exists('capabilities vht ldpc'): -        wifi['cap_vht'] = True -        wifi['cap_vht_ldpc'] = True - -    # VHT link adaptation capabilities -    if conf.exists('capabilities vht link-adaptation'): -        wifi['cap_vht'] = True -        wifi['cap_vht_link_adaptation'] = conf.return_value('capabilities vht link-adaptation') - -    # Set the maximum length of A-MPDU pre-EOF padding that the station can receive -    if conf.exists('capabilities vht max-mpdu-exp'): -        wifi['cap_vht'] = True -        wifi['cap_vht_max_mpdu_exp'] = conf.return_value('capabilities vht max-mpdu-exp') - -    # Increase Maximum MPDU length -    if conf.exists('capabilities vht max-mpdu'): -        wifi['cap_vht'] = True -        wifi['cap_vht_max_mpdu'] = conf.return_value('capabilities vht max-mpdu') - -    # Increase Maximum MPDU length -    if conf.exists('capabilities vht short-gi'): -        wifi['cap_vht'] = True -        wifi['cap_vht_short_gi'] = conf.return_values('capabilities vht short-gi') - -    # Support for receiving PPDU using STBC (Space Time Block Coding) -    if conf.exists('capabilities vht stbc rx'): -        wifi['cap_vht'] = True -        wifi['cap_vht_stbc_rx'] = conf.return_value('capabilities vht stbc rx') - -    # Support for the transmission of at least 2x1 STBC (Space Time Block Coding) -    if conf.exists('capabilities vht stbc tx'): -        wifi['cap_vht'] = True -        wifi['cap_vht_stbc_tx'] = True - -    # Support for VHT TXOP Power Save Mode -    if conf.exists('capabilities vht tx-powersave'): -        wifi['cap_vht'] = True -        wifi['cap_vht_tx_powersave'] = True - -    # STA supports receiving a VHT variant HT Control field -    if conf.exists('capabilities vht vht-cf'): -        wifi['cap_vht'] = True -        wifi['cap_vht_vht_cf'] = True - -    # Wireless radio channel -    if conf.exists('channel'): -        wifi['channel'] = conf.return_value('channel') - -    # Disable broadcast of SSID from access-point -    if conf.exists('disable-broadcast-ssid'): -        wifi['disable_broadcast_ssid'] = True - -    # Disassociate stations based on excessive transmission failures -    if conf.exists('expunge-failing-stations'): -        wifi['expunge_failing_stations'] = True - -    # retrieve real hardware address -    if conf.exists('hw-id'): -        wifi['hw_id'] = conf.return_value('hw-id') - -    # Isolate stations on the AP so they cannot see each other -    if conf.exists('isolate-stations'): -        wifi['isolate_stations'] = True - -    # Wireless physical device -    if conf.exists('physical-device'): -        wifi['phy'] = conf.return_value('physical-device') - -    # Maximum number of wireless radio stations -    if conf.exists('max-stations'): -        wifi['max_stations'] = conf.return_value('max-stations') - -    # Management Frame Protection (MFP) according to IEEE 802.11w -    if conf.exists('mgmt-frame-protection'): -        wifi['mgmt_frame_protection'] = conf.return_value('mgmt-frame-protection') - -    # Wireless radio mode -    if conf.exists('mode'): -        wifi['mode'] = conf.return_value('mode') - -    # Transmission power reduction in dBm -    if conf.exists('reduce-transmit-power'): -        wifi['reduce_transmit_power'] = conf.return_value('reduce-transmit-power') - -    # WEP enabled? -    if conf.exists('security wep'): -        wifi['sec_wep'] = True - -    # WEP encryption key(s) -    if conf.exists('security wep key'): -        wifi['sec_wep_key'] = conf.return_values('security wep key') - -    # WPA enabled? -    if conf.exists('security wpa'): -        wifi['sec_wpa'] = True - -    # WPA Cipher suite -    if conf.exists('security wpa cipher'): -        wifi['sec_wpa_cipher'] = conf.return_values('security wpa cipher') - -    # WPA mode -    if conf.exists('security wpa mode'): -        wifi['sec_wpa_mode'] = conf.return_value('security wpa mode') - -    # WPA default ciphers depend on WPA mode -    if not wifi['sec_wpa_cipher']: -        if wifi['sec_wpa_mode'] == 'wpa': -            wifi['sec_wpa_cipher'].append('TKIP') -            wifi['sec_wpa_cipher'].append('CCMP') - -        elif wifi['sec_wpa_mode'] == 'wpa2': -            wifi['sec_wpa_cipher'].append('CCMP') - -        elif wifi['sec_wpa_mode'] == 'both': -            wifi['sec_wpa_cipher'].append('CCMP') -            wifi['sec_wpa_cipher'].append('TKIP') - -    # WPA Group Cipher suite -    if conf.exists('security wpa group-cipher'): -        wifi['sec_wpa_group_cipher'] = conf.return_values('security wpa group-cipher') - -    # WPA personal shared pass phrase -    if conf.exists('security wpa passphrase'): -        wifi['sec_wpa_passphrase'] = conf.return_value('security wpa passphrase') - -    # WPA RADIUS source address -    if conf.exists('security wpa radius source-address'): -        wifi['sec_wpa_radius_source'] = conf.return_value('security wpa radius source-address') - -    # WPA RADIUS server -    for server in conf.list_nodes('security wpa radius server'): -        # set new configuration level -        conf.set_level(cfg_base + ' security wpa radius server ' + server) -        radius = { -            'server' : server, -            'acc_port' : '', -            'disabled': False, -            'port' : 1812, -            'key' : '' -        } - -        # RADIUS server port -        if conf.exists('port'): -            radius['port'] = int(conf.return_value('port')) - -        # receive RADIUS accounting info -        if conf.exists('accounting'): -            radius['acc_port'] = radius['port'] + 1 - -        # Check if RADIUS server was temporary disabled -        if conf.exists(['disable']): -            radius['disabled'] = True - -        # RADIUS server shared-secret -        if conf.exists('key'): -            radius['key'] = conf.return_value('key') - -        # append RADIUS server to list of servers -        wifi['sec_wpa_radius'].append(radius) - -    # re-set configuration level to parse new nodes -    conf.set_level(cfg_base) - -    # Wireless access-point service set identifier (SSID) -    if conf.exists('ssid'): -        wifi['ssid'] = conf.return_value('ssid') - -    # Wireless device type for this interface -    if conf.exists('type'): -        tmp = conf.return_value('type') -        if tmp == 'access-point': -            tmp = 'ap' - -        wifi['op_mode'] = tmp +    base = ['interfaces', 'wireless'] +    wifi = get_interface_dict(conf, base) + +    if 'security' in wifi and 'wpa' in wifi['security']: +        wpa_cipher = wifi['security']['wpa'].get('cipher') +        wpa_mode = wifi['security']['wpa'].get('mode') +        if not wpa_cipher: +            tmp = None +            if wpa_mode == 'wpa': +                tmp = {'security': {'wpa': {'cipher' : ['TKIP', 'CCMP']}}} +            elif wpa_mode == 'wpa2': +                tmp = {'security': {'wpa': {'cipher' : ['CCMP']}}} +            elif wpa_mode == 'both': +                tmp = {'security': {'wpa': {'cipher' : ['CCMP', 'TKIP']}}} + +            if tmp: wifi = dict_merge(tmp, wifi)      # retrieve configured regulatory domain -    conf.set_level('system') -    if conf.exists('wifi-regulatory-domain'): -        wifi['country_code'] = conf.return_value('wifi-regulatory-domain') +    conf.set_level(['system']) +    if conf.exists(['wifi-regulatory-domain']): +        wifi['country_code'] = conf.return_value(['wifi-regulatory-domain']) -    return wifi +    # Only one wireless interface per phy can be in station mode +    tmp = find_other_stations(conf, base, wifi['ifname']) +    if tmp: wifi['station_interfaces'] = tmp +    return wifi  def verify(wifi): -    if wifi['deleted']: -        if wifi['is_bridge_member']: -            raise ConfigError(( -                f'Cannot delete interface "{wifi["intf"]}" as it is a ' -                f'member of bridge "{wifi["is_bridge_member"]}"!')) - +    if 'deleted' in wifi: +        verify_bridge_delete(wifi)          return None -    if wifi['op_mode'] != 'monitor' and not wifi['ssid']: -        raise ConfigError('SSID must be set for {}'.format(wifi['intf'])) +    if 'physical_device' not in wifi: +        raise ConfigError('You must specify a physical-device "phy"') -    if not wifi['phy']: -        raise ConfigError('You must specify physical-device') - -    if not wifi['mode']: +    if 'type' not in wifi:          raise ConfigError('You must specify a WiFi mode') -    if wifi['op_mode'] == 'ap': -        c = Config() -        if not c.exists('system wifi-regulatory-domain'): -            raise ConfigError('Wireless regulatory domain is mandatory,\n' \ -                              'use "set system wifi-regulatory-domain".') - -        if not wifi['channel']: -            raise ConfigError('Channel must be set for {}'.format(wifi['intf'])) - -    if len(wifi['sec_wep_key']) > 4: -        raise ConfigError('No more then 4 WEP keys configurable') +    if 'ssid' not in wifi and wifi['type'] != 'monitor': +        raise ConfigError('SSID must be configured') -    if wifi['cap_vht'] and not wifi['cap_ht']: -        raise ConfigError('Specify HT flags if you want to use VHT!') - -    if wifi['cap_vht_beamform'] and wifi['cap_vht_antenna_cnt'] == 1: -        raise ConfigError('Cannot use beam forming with just one antenna!') - -    if wifi['cap_vht_beamform'] == 'single-user-beamformer' and wifi['cap_vht_antenna_cnt'] < 3: -        # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705 -        raise ConfigError('Single-user beam former requires at least 3 antennas!') - -    if wifi['sec_wep'] and (len(wifi['sec_wep_key']) == 0): -        raise ConfigError('Missing WEP keys') - -    if wifi['sec_wpa'] and not (wifi['sec_wpa_passphrase'] or wifi['sec_wpa_radius']): -        raise ConfigError('Misssing WPA key or RADIUS server') - -    for radius in wifi['sec_wpa_radius']: -        if not radius['key']: -            raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server'])) - -    if ( wifi['is_bridge_member'] -            and ( wifi['address'] -                or wifi['ipv6_eui64_prefix'] -                or wifi['ipv6_autoconf'] ) ): -        raise ConfigError(( -            f'Cannot assign address to interface "{wifi["intf"]}" ' -            f'as it is a member of bridge "{wifi["is_bridge_member"]}"!')) - -    if wifi['vrf']: -        if wifi['vrf'] not in interfaces(): -            raise ConfigError(f'VRF "{wifi["vrf"]}" does not exist') - -        if wifi['is_bridge_member']: -            raise ConfigError(( -                f'Interface "{wifi["intf"]}" cannot be member of VRF ' -                f'"{wifi["vrf"]}" and bridge {wifi["is_bridge_member"]} ' -                f'at the same time!')) +    if wifi['type'] == 'access-point': +        if 'country_code' not in wifi: +            raise ConfigError('Wireless regulatory domain is mandatory,\n' \ +                              'use "set system wifi-regulatory-domain" for configuration.') + +        if 'channel' not in wifi: +            raise ConfigError('Wireless channel must be configured!') + +    if 'security' in wifi: +        if {'wep', 'wpa'} <= set(wifi.get('security', {})): +            raise ConfigError('Must either use WEP or WPA security!') + +        if 'wep' in wifi['security']: +            if 'key' in wifi['security']['wep'] and len(wifi['security']['wep']) > 4: +                raise ConfigError('No more then 4 WEP keys configurable') +            elif 'key' not in wifi['security']['wep']: +                raise ConfigError('Security WEP configured - missing WEP keys!') + +        elif 'wpa' in wifi['security']: +            wpa = wifi['security']['wpa'] +            if not any(i in ['passphrase', 'radius'] for i in wpa): +                raise ConfigError('Misssing WPA key or RADIUS server') + +            if 'radius' in wpa: +                if 'server' in wpa['radius']: +                    for server in wpa['radius']['server']: +                        if 'key' not in wpa['radius']['server'][server]: +                            raise ConfigError(f'Misssing RADIUS shared secret key for server: {server}') + +    if 'capabilities' in wifi: +        capabilities = wifi['capabilities'] +        if 'vht' in capabilities: +            if 'ht' not in capabilities: +                raise ConfigError('Specify HT flags if you want to use VHT!') + +            if {'beamform', 'antenna_count'} <= set(capabilities.get('vht', {})): +                if capabilities['vht']['antenna_count'] == '1': +                    raise ConfigError('Cannot use beam forming with just one antenna!') + +                if capabilities['vht']['beamform'] == 'single-user-beamformer': +                    if int(capabilities['vht']['antenna_count']) < 3: +                        # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705 +                        raise ConfigError('Single-user beam former requires at least 3 antennas!') + +    if 'station_interfaces' in wifi and wifi['type'] == 'station': +        phy = wifi['physical_device'] +        if phy in wifi['station_interfaces']: +            if len(wifi['station_interfaces'][phy]) > 0: +                raise ConfigError('Only one station per wireless physical interface possible!') + +    verify_address(wifi) +    verify_vrf(wifi)      # use common function to verify VLAN configuration      verify_vlan_config(wifi) -    conf = Config() -    # Only one wireless interface per phy can be in station mode -    base = ['interfaces', 'wireless'] -    for phy in os.listdir('/sys/class/ieee80211'): -        stations = [] -        for wlan in conf.list_nodes(base): -            # the following node is mandatory -            if conf.exists(base + [wlan, 'physical-device', phy]): -                tmp = conf.return_value(base + [wlan, 'type']) -                if tmp == 'station': -                    stations.append(wlan) - -        if len(stations) > 1: -            raise ConfigError('Only one station per wireless physical interface possible!') -      return None  def generate(wifi): -    interface = wifi['intf'] +    interface = wifi['ifname']      # always stop hostapd service first before reconfiguring it      call(f'systemctl stop hostapd@{interface}.service') @@ -513,7 +179,7 @@ def generate(wifi):      call(f'systemctl stop wpa_supplicant@{interface}.service')      # Delete config files if interface is removed -    if wifi['deleted']: +    if 'deleted' in wifi:          if os.path.isfile(hostapd_conf.format(**wifi)):              os.unlink(hostapd_conf.format(**wifi)) @@ -522,10 +188,10 @@ def generate(wifi):          return None -    if not wifi['mac']: +    if 'mac' not in wifi:          # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd          # generate locally administered MAC address from used phy interface -        with open('/sys/class/ieee80211/{}/addresses'.format(wifi['phy']), 'r') as f: +        with open('/sys/class/ieee80211/{physical_device}/addresses'.format(**wifi), 'r') as f:              # some PHYs tend to have multiple interfaces and thus supply multiple MAC              # addresses - we only need the first one for our calculation              tmp = f.readline().rstrip() @@ -545,20 +211,18 @@ def generate(wifi):              wifi['mac'] = str(mac)      # render appropriate new config files depending on access-point or station mode -    if wifi['op_mode'] == 'ap': -        render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi) +    if wifi['type'] == 'access-point': +        render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi, trim_blocks=True) -    elif wifi['op_mode'] == 'station': -        render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi) +    elif wifi['type'] == 'station': +        render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi, trim_blocks=True)      return None  def apply(wifi): -    interface = wifi['intf'] -    if wifi['deleted']: -        w = WiFiIf(interface) -        # delete interface -        w.remove() +    interface = wifi['ifname'] +    if 'deleted' in wifi: +        WiFiIf(interface).remove()      else:          # WiFi interface needs to be created on-block (e.g. mode or physical          # interface) instead of passing a ton of arguments, I just use a dict @@ -566,97 +230,21 @@ def apply(wifi):          conf = deepcopy(WiFiIf.get_config())          # Assign WiFi instance configuration parameters to config dict -        conf['phy'] = wifi['phy'] +        conf['phy'] = wifi['physical_device']          # Finally create the new interface          w = WiFiIf(interface, **conf) - -        # assign/remove VRF (ONLY when not a member of a bridge, -        # otherwise 'nomaster' removes it from it) -        if not wifi['is_bridge_member']: -            w.set_vrf(wifi['vrf']) - -        # update interface description used e.g. within SNMP -        w.set_alias(wifi['description']) - -        if wifi['dhcp_client_id']: -            w.dhcp.v4.options['client_id'] = wifi['dhcp_client_id'] - -        if wifi['dhcp_hostname']: -            w.dhcp.v4.options['hostname'] = wifi['dhcp_hostname'] - -        if wifi['dhcp_vendor_class_id']: -            w.dhcp.v4.options['vendor_class_id'] = wifi['dhcp_vendor_class_id'] - -        if wifi['dhcpv6_prm_only']: -            w.dhcp.v6.options['dhcpv6_prm_only'] = True - -        if wifi['dhcpv6_temporary']: -            w.dhcp.v6.options['dhcpv6_temporary'] = True - -        if wifi['dhcpv6_pd_length']: -            w.dhcp.v6.options['dhcpv6_pd_length'] = wifi['dhcpv6_pd_length'] - -        if wifi['dhcpv6_pd_interfaces']: -            w.dhcp.v6.options['dhcpv6_pd_interfaces'] = wifi['dhcpv6_pd_interfaces'] - -        # ignore link state changes -        w.set_link_detect(wifi['disable_link_detect']) - -        # Delete old IPv6 EUI64 addresses before changing MAC -        for addr in wifi['ipv6_eui64_prefix_remove']: -            w.del_ipv6_eui64_address(addr) - -        # Change interface MAC address - re-set to real hardware address (hw-id) -        # if custom mac is removed -        if wifi['mac']: -            w.set_mac(wifi['mac']) -        elif wifi['hw_id']: -            w.set_mac(wifi['hw_id']) - -        # Add IPv6 EUI-based addresses -        for addr in wifi['ipv6_eui64_prefix']: -            w.add_ipv6_eui64_address(addr) - -        # configure ARP filter configuration -        w.set_arp_filter(wifi['ip_disable_arp_filter']) -        # configure ARP accept -        w.set_arp_accept(wifi['ip_enable_arp_accept']) -        # configure ARP announce -        w.set_arp_announce(wifi['ip_enable_arp_announce']) -        # configure ARP ignore -        w.set_arp_ignore(wifi['ip_enable_arp_ignore']) -        # IPv6 accept RA -        w.set_ipv6_accept_ra(wifi['ipv6_accept_ra']) -        # IPv6 address autoconfiguration -        w.set_ipv6_autoconf(wifi['ipv6_autoconf']) -        # IPv6 forwarding -        w.set_ipv6_forwarding(wifi['ipv6_forwarding']) -        # IPv6 Duplicate Address Detection (DAD) tries -        w.set_ipv6_dad_messages(wifi['ipv6_dup_addr_detect']) - -        # Configure interface address(es) -        # - not longer required addresses get removed first -        # - newly addresses will be added second -        for addr in wifi['address_remove']: -            w.del_addr(addr) -        for addr in wifi['address']: -            w.add_addr(addr) - -        # apply all vlans to interface -        apply_all_vlans(w, wifi) +        w.update(wifi)          # Enable/Disable interface - interface is always placed in          # administrative down state in WiFiIf class -        if not wifi['disable']: -            w.set_admin_state('up') - +        if 'disable' not in wifi:              # Physical interface is now configured. Proceed by starting hostapd or              # wpa_supplicant daemon. When type is monitor we can just skip this. -            if wifi['op_mode'] == 'ap': +            if wifi['type'] == 'access-point':                  call(f'systemctl start hostapd@{interface}.service') -            elif wifi['op_mode'] == 'station': +            elif wifi['type'] == 'station':                  call(f'systemctl start wpa_supplicant@{interface}.service')      return None diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index 0964a8f4d..4081be3c9 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -20,11 +20,11 @@ from fnmatch import fnmatch  from sys import exit  from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict  from vyos.configverify import verify_vrf  from vyos.template import render  from vyos.util import call -from vyos.xml import defaults +from vyos.util import check_kmod  from vyos import ConfigError  from vyos import airbag  airbag.enable() @@ -42,44 +42,23 @@ def find_device_file(device):      return None  def get_config(): -    """ Retrive CLI config as dictionary. Dictionary can never be empty, -    as at least the interface name will be added or a deleted flag """ +    """ +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the +    interface name will be added or a deleted flag +    """      conf = Config() - -    # determine tagNode instance -    if 'VYOS_TAGNODE_VALUE' not in os.environ: -        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - -    # retrieve interface default values      base = ['interfaces', 'wirelessmodem'] -    default_values = defaults(base) - -    ifname = os.environ['VYOS_TAGNODE_VALUE'] -    base = base + [ifname] - -    wwan = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) -    # Check if interface has been removed -    if wwan == {}: -        wwan.update({'deleted' : ''}) - -    # We have gathered the dict representation of the CLI, but there are -    # default options which we need to update into the dictionary -    # retrived. -    wwan = dict_merge(default_values, wwan) - -    # Add interface instance name into dictionary -    wwan.update({'ifname': ifname}) - +    wwan = get_interface_dict(conf, base)      return wwan  def verify(wwan): -    if 'deleted' in wwan.keys(): +    if 'deleted' in wwan:          return None -    if not 'apn' in wwan.keys(): +    if not 'apn' in wwan:          raise ConfigError('No APN configured for "{ifname}"'.format(**wwan)) -    if not 'device' in wwan.keys(): +    if not 'device' in wwan:          raise ConfigError('Physical "device" must be configured')      # we can not use isfile() here as Linux device files are no regular files @@ -136,11 +115,11 @@ def generate(wwan):      return None  def apply(wwan): -    if 'deleted' in wwan.keys(): +    if 'deleted' in wwan:          # bail out early          return None -    if not 'disable' in wwan.keys(): +    if not 'disable' in wwan:          # "dial" WWAN connection          call('systemctl start ppp@{ifname}.service'.format(**wwan)) | 
