summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/pr-conflicts.yml18
-rw-r--r--data/templates/accel-ppp/config_shaper_radius.j23
-rw-r--r--data/templates/accel-ppp/ipoe.config.tmpl40
-rw-r--r--data/templates/conntrackd/conntrackd.conf.tmpl4
-rw-r--r--data/templates/dhcp-server/dhcpd.conf.tmpl4
-rw-r--r--data/templates/dhcp-server/dhcpdv6.conf.tmpl4
-rw-r--r--data/templates/dns-forwarding/recursor.conf.tmpl5
-rw-r--r--data/templates/dynamic-dns/ddclient.conf.tmpl2
-rw-r--r--data/templates/frr/isisd.frr.tmpl8
-rw-r--r--data/templates/https/nginx.default.tmpl2
-rw-r--r--data/templates/macsec/wpa_supplicant.conf.tmpl12
-rw-r--r--data/templates/monitoring/override.conf.tmpl2
-rw-r--r--data/templates/monitoring/telegraf.tmpl26
-rw-r--r--data/templates/netflow/uacctd.conf.tmpl24
-rw-r--r--data/templates/ntp/ntpd.conf.tmpl13
-rw-r--r--data/templates/ocserv/ocserv_config.tmpl6
-rw-r--r--data/templates/openvpn/server.conf.tmpl9
-rw-r--r--data/templates/pppoe/ip-down.script.tmpl4
-rw-r--r--data/templates/pppoe/ipv6-up.script.tmpl8
-rw-r--r--data/templates/snmp/etc.snmpd.conf.tmpl2
-rw-r--r--data/templates/syslog/rsyslog.conf.tmpl12
-rw-r--r--data/templates/vyos-hostsd/hosts.tmpl5
-rw-r--r--debian/control2
-rw-r--r--debian/vyos-1x.install1
-rw-r--r--debian/vyos-1x.postinst2
-rw-r--r--interface-definitions/dhcp-server.xml.in12
-rw-r--r--interface-definitions/dns-domain-name.xml.in2
-rw-r--r--interface-definitions/dns-dynamic.xml.in6
-rw-r--r--interface-definitions/dns-forwarding.xml.in12
-rw-r--r--interface-definitions/firewall-options.xml.in24
-rw-r--r--interface-definitions/include/accel-ppp/client-ip-pool-subnet-single.xml.i15
-rw-r--r--interface-definitions/include/accel-ppp/radius-additions-rate-limit.xml.i14
-rw-r--r--interface-definitions/include/generic-interface-multi.xml.i18
-rw-r--r--interface-definitions/include/interface/dhcp-options.xml.i6
-rw-r--r--interface-definitions/include/interface/dhcpv6-options.xml.i4
-rw-r--r--interface-definitions/include/isis/password.xml.i20
-rw-r--r--interface-definitions/include/isis/protocol-common-config.xml.i54
-rw-r--r--interface-definitions/include/url.xml.i15
-rw-r--r--interface-definitions/include/username.xml.i11
-rw-r--r--interface-definitions/interfaces-bonding.xml.in17
-rw-r--r--interface-definitions/interfaces-macsec.xml.in9
-rw-r--r--interface-definitions/interfaces-wireless.xml.in3
-rw-r--r--interface-definitions/interfaces-wwan.xml.in2
-rw-r--r--interface-definitions/ntp.xml.in1
-rw-r--r--interface-definitions/protocols-mpls.xml.in2
-rw-r--r--interface-definitions/protocols-rpki.xml.in6
-rw-r--r--interface-definitions/service_ipoe-server.xml.in25
-rw-r--r--interface-definitions/service_monitoring_telegraf.xml.in93
-rw-r--r--interface-definitions/service_webproxy.xml.in4
-rw-r--r--interface-definitions/snmp.xml.in24
-rw-r--r--interface-definitions/system-proxy.xml.in2
-rw-r--r--interface-definitions/vpn_openconnect.xml.in13
-rw-r--r--op-mode-definitions/clear-dhcp-server-lease.xml.in20
-rw-r--r--op-mode-definitions/restart-frr.xml.in6
-rw-r--r--op-mode-definitions/webproxy.xml.in4
-rw-r--r--python/vyos/base.py44
-rw-r--r--python/vyos/config.py57
-rw-r--r--python/vyos/configdict.py34
-rw-r--r--python/vyos/configdiff.py84
-rw-r--r--python/vyos/configsession.py6
-rw-r--r--python/vyos/configtree.py96
-rw-r--r--python/vyos/configverify.py45
-rw-r--r--python/vyos/ifconfig/bond.py87
-rw-r--r--python/vyos/ifconfig/bridge.py48
-rw-r--r--python/vyos/ifconfig/ethernet.py25
-rw-r--r--python/vyos/ifconfig/interface.py54
-rw-r--r--python/vyos/ifconfig/loopback.py13
-rw-r--r--python/vyos/ifconfig/wireguard.py85
-rw-r--r--python/vyos/template.py10
-rw-r--r--python/vyos/util.py42
-rw-r--r--python/vyos/validate.py19
-rw-r--r--smoketest/configs/qos-basic205
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bonding.py89
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bridge.py174
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_macsec.py45
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py38
-rwxr-xr-xsmoketest/scripts/cli/test_load_balancning_wan.py189
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_isis.py69
-rwxr-xr-xsmoketest/scripts/cli/test_service_dns_forwarding.py47
-rwxr-xr-xsmoketest/scripts/cli/test_service_salt.py94
-rwxr-xr-xsmoketest/scripts/cli/test_service_snmp.py47
-rwxr-xr-xsmoketest/scripts/cli/test_service_ssh.py73
-rwxr-xr-xsmoketest/scripts/cli/test_system_flow-accounting.py38
-rwxr-xr-xsmoketest/scripts/cli/test_system_ipv6.py31
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py8
-rwxr-xr-xsmoketest/scripts/cli/test_system_ntp.py17
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_openconnect.py72
-rwxr-xr-xsmoketest/scripts/cli/test_vrf.py141
-rwxr-xr-xsrc/conf_mode/dhcp_server.py4
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py4
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py31
-rwxr-xr-xsrc/conf_mode/firewall_options.py14
-rwxr-xr-xsrc/conf_mode/host_name.py2
-rwxr-xr-xsrc/conf_mode/https.py4
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py65
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py20
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py6
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py24
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py2
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py103
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py11
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py5
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py2
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py3
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py23
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py2
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py93
-rwxr-xr-xsrc/conf_mode/ntp.py20
-rwxr-xr-xsrc/conf_mode/salt-minion.py2
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py37
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py22
-rwxr-xr-xsrc/conf_mode/service_router-advert.py4
-rwxr-xr-xsrc/conf_mode/snmp.py11
-rwxr-xr-xsrc/conf_mode/system-ipv6.py4
-rwxr-xr-xsrc/conf_mode/system-login.py21
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py6
-rwxr-xr-xsrc/conf_mode/vrf.py14
-rw-r--r--src/etc/cron.d/check-wwan1
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper2
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf4
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup4
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook6
-rwxr-xr-xsrc/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers14
-rwxr-xr-xsrc/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers23
-rw-r--r--src/etc/systemd/system/wpa_supplicant-wired@.service.d/override.conf11
-rwxr-xr-xsrc/helpers/strip-private.py10
-rwxr-xr-xsrc/op_mode/clear_dhcp_lease.py73
-rwxr-xr-xsrc/op_mode/generate_public_key_command.py11
-rwxr-xr-xsrc/op_mode/restart_frr.py2
-rwxr-xr-xsrc/op_mode/show_ipsec_connections.py284
-rwxr-xr-xsrc/op_mode/vpn_ipsec.py52
-rwxr-xr-xsrc/services/vyos-hostsd5
-rwxr-xr-xsrc/services/vyos-http-api-server37
-rwxr-xr-xsrc/system/keepalived-fifo.py12
-rw-r--r--src/systemd/wpa_supplicant-macsec@.service8
-rw-r--r--src/tests/test_util.py16
-rwxr-xr-xsrc/utils/vyos-hostsd-client3
137 files changed, 3245 insertions, 680 deletions
diff --git a/.github/workflows/pr-conflicts.yml b/.github/workflows/pr-conflicts.yml
new file mode 100644
index 000000000..72ff3969b
--- /dev/null
+++ b/.github/workflows/pr-conflicts.yml
@@ -0,0 +1,18 @@
+name: "PR Conflicts checker"
+on:
+ pull_request_target:
+ types: [synchronize]
+
+jobs:
+ Conflict_Check:
+ name: 'Check PR status: conflicts and resolution'
+ runs-on: ubuntu-18.04
+ steps:
+ - name: check if PRs are dirty
+ uses: eps1lon/actions-label-merge-conflict@releases/2.x
+ with:
+ dirtyLabel: "state: conflict"
+ removeOnDirtyLabel: "state: conflict resolved"
+ repoToken: "${{ secrets.GITHUB_TOKEN }}"
+ commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request."
+ commentOnClean: "Conflicts have been resolved. A maintainer will review the pull request shortly."
diff --git a/data/templates/accel-ppp/config_shaper_radius.j2 b/data/templates/accel-ppp/config_shaper_radius.j2
index 8de5f5df3..c409c3718 100644
--- a/data/templates/accel-ppp/config_shaper_radius.j2
+++ b/data/templates/accel-ppp/config_shaper_radius.j2
@@ -6,5 +6,8 @@ attr={{ authentication.radius.rate_limit.attribute }}
{% if authentication.radius.rate_limit.vendor is defined and authentication.radius.rate_limit.vendor is not none %}
vendor={{ authentication.radius.rate_limit.vendor }}
{% endif %}
+{% if authentication.radius.rate_limit.multiplier is defined and authentication.radius.rate_limit.multiplier is not none %}
+rate-multiplier={{ authentication.radius.rate_limit.multiplier }}
+{% endif %}
{% endif %}
{% endif %}
diff --git a/data/templates/accel-ppp/ipoe.config.tmpl b/data/templates/accel-ppp/ipoe.config.tmpl
index 1cf2ab0be..476c922b8 100644
--- a/data/templates/accel-ppp/ipoe.config.tmpl
+++ b/data/templates/accel-ppp/ipoe.config.tmpl
@@ -24,11 +24,24 @@ level=5
[ipoe]
verbose=1
{% for interface in interfaces %}
-{% if interface.vlan_mon %}
-interface=re:{{ interface.name }}\.\d+,{% else %}interface={{ interface.name }},{% endif %}shared={{ interface.shared }},mode={{ interface.mode }},ifcfg={{ interface.ifcfg }},range={{ interface.range }},start={{ interface.sess_start }},ipv6=1
+{% set ifname = interface.name %}
+{% if interface.vlan_mon %}
+{% set ifname = 're:' ~ interface.name ~ '\.\d+' %}
+{% endif %}
+interface={{ ifname }},shared={{ interface.shared }},mode={{ interface.mode }},ifcfg={{ interface.ifcfg }}{{ ',range=' ~ interface.range if interface.range is defined and interface.range is not none }},start={{ interface.sess_start }},ipv6=1
{% endfor %}
{% if auth_mode == 'noauth' %}
noauth=1
+{% if client_named_ip_pool %}
+{% for pool in client_named_ip_pool %}
+{% if pool.subnet is defined %}
+ip-pool={{ pool.name }}
+{% endif %}
+{% if pool.gateway_address is defined %}
+gw-ip-address={{ pool.gateway_address }}/{{ pool.subnet.split('/')[1] }}
+{% endif %}
+{% endfor%}
+{% endif %}
{% elif auth_mode == 'local' %}
username=ifname
password=csid
@@ -61,6 +74,18 @@ verbose=1
[ipv6-dhcp]
verbose=1
+{% if client_named_ip_pool %}
+[ip-pool]
+{% for pool in client_named_ip_pool %}
+{% if pool.subnet is defined %}
+{{ pool.subnet }},name={{ pool.name }}
+{% endif %}
+{% if pool.gateway_address is defined %}
+gw-ip-address={{ pool.gateway_address }}/{{ pool.subnet.split('/')[1] }}
+{% endif %}
+{% endfor%}
+{% endif %}
+
{% if client_ipv6_pool %}
[ipv6-pool]
{% for p in client_ipv6_pool %}
@@ -102,13 +127,18 @@ bind={{ radius_source_address }}
dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }}
{% endif %}
-{% if radius_shaper_attr %}
+{% if radius_shaper_enable %}
[shaper]
verbose=1
+{% if radius_shaper_attr %}
attr={{ radius_shaper_attr }}
-{% if radius_shaper_vendor %}
+{% endif %}
+{% if radius_shaper_multiplier %}
+rate-multiplier={{ radius_shaper_multiplier }}
+{% endif %}
+{% if radius_shaper_vendor %}
vendor={{ radius_shaper_vendor }}
-{% endif %}
+{% endif %}
{% endif %}
{% endif %}
diff --git a/data/templates/conntrackd/conntrackd.conf.tmpl b/data/templates/conntrackd/conntrackd.conf.tmpl
index ff6f785d5..7ec8a727c 100644
--- a/data/templates/conntrackd/conntrackd.conf.tmpl
+++ b/data/templates/conntrackd/conntrackd.conf.tmpl
@@ -10,7 +10,9 @@ Sync {
{% if iface_config.peer is defined and iface_config.peer is not none %}
UDP {
{% if listen_address is defined and listen_address is not none %}
- IPv4_address {{ listen_address }}
+{% for address in listen_address %}
+ IPv4_address {{ address }}
+{% endfor %}
{% endif %}
IPv4_Destination_Address {{ iface_config.peer }}
Port {{ iface_config.port if iface_config.port is defined else '3780' }}
diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl
index dbd864b5e..0f0c622d4 100644
--- a/data/templates/dhcp-server/dhcpd.conf.tmpl
+++ b/data/templates/dhcp-server/dhcpd.conf.tmpl
@@ -62,7 +62,7 @@ subnet {{ address | network_from_ipv4 }} netmask {{ address | netmask_from_ipv4
# Shared network configration(s)
{% if shared_network_name is defined and shared_network_name is not none %}
{% for network, network_config in shared_network_name.items() if network_config.disable is not defined %}
-shared-network {{ network | replace('_','-') }} {
+shared-network {{ network }} {
{% if network_config.authoritative is defined %}
authoritative;
{% endif %}
@@ -212,7 +212,7 @@ shared-network {{ network | replace('_','-') }} {
{% endfor %}
{% endif %}
on commit {
- set shared-networkname = "{{ network | replace('_','-') }}";
+ set shared-networkname = "{{ network }}";
{% if hostfile_update is defined %}
set ClientIp = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6));
diff --git a/data/templates/dhcp-server/dhcpdv6.conf.tmpl b/data/templates/dhcp-server/dhcpdv6.conf.tmpl
index 45d629928..d5f277463 100644
--- a/data/templates/dhcp-server/dhcpdv6.conf.tmpl
+++ b/data/templates/dhcp-server/dhcpdv6.conf.tmpl
@@ -15,7 +15,7 @@ option dhcp6.name-servers {{ global_parameters.name_server | join(', ') }};
# Shared network configration(s)
{% if shared_network_name is defined and shared_network_name is not none %}
{% for network, network_config in shared_network_name.items() if network_config.disable is not defined %}
-shared-network {{ network | replace('_','-') }} {
+shared-network {{ network }} {
{% if network_config.common_options is defined and network_config.common_options is not none %}
{% if network_config.common_options.info_refresh_time is defined and network_config.common_options.info_refresh_time is not none %}
option dhcp6.info-refresh-time {{ network_config.common_options.info_refresh_time }};
@@ -117,7 +117,7 @@ shared-network {{ network | replace('_','-') }} {
{% endfor %}
{% endif %}
on commit {
- set shared-networkname = "{{ network | replace('_','-') }}";
+ set shared-networkname = "{{ network }}";
}
}
{% endfor %}
diff --git a/data/templates/dns-forwarding/recursor.conf.tmpl b/data/templates/dns-forwarding/recursor.conf.tmpl
index be0778993..294b228d2 100644
--- a/data/templates/dns-forwarding/recursor.conf.tmpl
+++ b/data/templates/dns-forwarding/recursor.conf.tmpl
@@ -28,6 +28,11 @@ local-address={{ listen_address | join(',') }}
# dnssec
dnssec={{ dnssec }}
+{% if dns64_prefix is defined %}
+# dns64-prefix
+dns64-prefix={{ dns64_prefix }}
+{% endif %}
+
{# dns: T3277: #}
{% if no_serve_rfc1918 is defined %}
# serve-rfc1918
diff --git a/data/templates/dynamic-dns/ddclient.conf.tmpl b/data/templates/dynamic-dns/ddclient.conf.tmpl
index 9d379de00..517e4bad4 100644
--- a/data/templates/dynamic-dns/ddclient.conf.tmpl
+++ b/data/templates/dynamic-dns/ddclient.conf.tmpl
@@ -9,7 +9,7 @@ ssl=yes
{% set web_skip = ", web-skip='" + interface[iface].use_web.skip + "'" if interface[iface].use_web.skip is defined else '' %}
use=web, web='{{ interface[iface].use_web.url }}'{{ web_skip }}
{% else %}
-use=if, if={{ iface }}
+{{ 'usev6=if' if interface[iface].ipv6_enable is defined else 'use=if' }}, if={{ iface }}
{% endif %}
{% if interface[iface].rfc2136 is defined and interface[iface].rfc2136 is not none %}
diff --git a/data/templates/frr/isisd.frr.tmpl b/data/templates/frr/isisd.frr.tmpl
index 6cfa076d0..324419456 100644
--- a/data/templates/frr/isisd.frr.tmpl
+++ b/data/templates/frr/isisd.frr.tmpl
@@ -99,8 +99,8 @@ router isis VyOS {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
{% endfor %}
{% endif %}
{% endif %}
-{% if spf_delay_ietf is defined and spf_delay_ietf.init_delay is defined and spf_delay_ietf.init_delay is not none %}
- spf-delay-ietf init-delay {{ spf_delay_ietf.init_delay }}
+{% if spf_delay_ietf is defined and spf_delay_ietf.init_delay is defined and spf_delay_ietf.short_delay is defined and spf_delay_ietf.long_delay is defined and spf_delay_ietf.holddown is defined and spf_delay_ietf.time_to_learn is defined %}
+ spf-delay-ietf init-delay {{ spf_delay_ietf.init_delay }} short-delay {{ spf_delay_ietf.short_delay }} long-delay {{ spf_delay_ietf.long_delay }} holddown {{ spf_delay_ietf.holddown }} time-to-learn {{ spf_delay_ietf.time_to_learn }}
{% endif %}
{% if area_password is defined and area_password is not none %}
{% if area_password.md5 is defined and area_password.md5 is not none %}
@@ -181,7 +181,9 @@ interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
{% if iface_config.passive is defined %}
isis passive
{% endif %}
-{% if iface_config.password is defined and iface_config.password.plaintext_password is defined and iface_config.password.plaintext_password is not none %}
+{% if iface_config.password is defined and iface_config.password.md5 is defined and iface_config.password.md5 is not none %}
+ isis password md5 {{ iface_config.password.md5 }}
+{% elif iface_config.password is defined and iface_config.password.plaintext_password is defined and iface_config.password.plaintext_password is not none %}
isis password clear {{ iface_config.password.plaintext_password }}
{% endif %}
{% if iface_config.priority is defined and iface_config.priority is not none %}
diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl
index 968ba806c..04e0d558a 100644
--- a/data/templates/https/nginx.default.tmpl
+++ b/data/templates/https/nginx.default.tmpl
@@ -41,7 +41,7 @@ server {
ssl_protocols TLSv1.2 TLSv1.3;
# proxy settings for HTTP API, if enabled; 503, if not
- location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc|graphql) {
+ location ~ /(retrieve|configure|config-file|image|generate|show|reset|docs|openapi.json|redoc|graphql) {
{% if server.api %}
{% if server.api.socket %}
proxy_pass http://unix:/run/api.sock;
diff --git a/data/templates/macsec/wpa_supplicant.conf.tmpl b/data/templates/macsec/wpa_supplicant.conf.tmpl
index 5b353def8..65747ea6f 100644
--- a/data/templates/macsec/wpa_supplicant.conf.tmpl
+++ b/data/templates/macsec/wpa_supplicant.conf.tmpl
@@ -47,6 +47,12 @@ network={
# 1: Integrity only
macsec_integ_only={{ '0' if security is defined and security.encrypt is defined else '1' }}
+ # macsec_csindex: IEEE 802.1X/MACsec cipher suite
+ # 0 = GCM-AES-128
+ # 1 = GCM-AES-256
+{# security.cipher is a mandatory key #}
+ macsec_csindex={{ '1' if security.cipher is defined and security.cipher == 'gcm-aes-256' else '0' }}
+
{% if security is defined %}
{% if security.encrypt is defined %}
# mka_cak, mka_ckn, and mka_priority: IEEE 802.1X/MACsec pre-shared key mode
@@ -63,7 +69,7 @@ network={
# mka_priority (Priority of MKA Actor) is in 0..255 range with 255 being
# default priority
mka_priority={{ security.mka.priority }}
-{% endif %}
+{% endif %}
{% if security.replay_window is defined %}
# macsec_replay_protect: IEEE 802.1X/MACsec replay protection
@@ -85,5 +91,9 @@ network={
macsec_replay_window={{ security.replay_window }}
{% endif %}
{% endif %}
+
+ # macsec_port: IEEE 802.1X/MACsec port - Port component of the SCI
+ # Range: 1-65534 (default: 1)
+ macsec_port=1
}
diff --git a/data/templates/monitoring/override.conf.tmpl b/data/templates/monitoring/override.conf.tmpl
index f8f150791..00dbc63a0 100644
--- a/data/templates/monitoring/override.conf.tmpl
+++ b/data/templates/monitoring/override.conf.tmpl
@@ -2,6 +2,8 @@
After=vyos-router.service
ConditionPathExists=/run/telegraf/vyos-telegraf.conf
[Service]
+{% if influxdb_configured is defined %}
Environment=INFLUX_TOKEN={{ authentication.token }}
+{% endif %}
CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_ADMIN
AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN
diff --git a/data/templates/monitoring/telegraf.tmpl b/data/templates/monitoring/telegraf.tmpl
index d3145a500..c5cbddc8c 100644
--- a/data/templates/monitoring/telegraf.tmpl
+++ b/data/templates/monitoring/telegraf.tmpl
@@ -1,12 +1,12 @@
# Generated by /usr/libexec/vyos/conf_mode/service_monitoring_telegraf.py
[agent]
- interval = "10s"
+ interval = "15s"
round_interval = true
metric_batch_size = 1000
metric_buffer_limit = 10000
- collection_jitter = "0s"
- flush_interval = "10s"
+ collection_jitter = "5s"
+ flush_interval = "15s"
flush_jitter = "0s"
precision = ""
debug = false
@@ -14,12 +14,30 @@
logfile = ""
hostname = ""
omit_hostname = false
+{% if influxdb_configured is defined %}
[[outputs.influxdb_v2]]
urls = ["{{ url }}:{{ port }}"]
insecure_skip_verify = true
token = "$INFLUX_TOKEN"
organization = "{{ authentication.organization }}"
bucket = "{{ bucket }}"
+{% endif %}
+{% if prometheus_client is defined %}
+ [[outputs.prometheus_client]]
+ ## Address to listen on
+ listen = "{{ prometheus_client.listen_address | bracketize_ipv6 if prometheus_client.listen_address is defined else '' }}:{{ prometheus_client.port }}"
+ metric_version = {{ prometheus_client.metric_version }}
+{% if prometheus_client.authentication is defined %}
+{% if prometheus_client.authentication.username is defined and prometheus_client.authentication.username is not none and prometheus_client.authentication.password is defined and prometheus_client.authentication.password is not none %}
+ ## Use HTTP Basic Authentication
+ basic_username = "{{ prometheus_client.authentication.username }}"
+ basic_password = "{{ prometheus_client.authentication.password }}"
+{% endif %}
+{% endif %}
+{% if prometheus_client.allow_from is defined and prometheus_client.allow_from is not none %}
+ ip_range = {{ prometheus_client.allow_from }}
+{% endif %}
+{% endif %}
[[inputs.cpu]]
percpu = true
totalcpu = true
@@ -50,6 +68,7 @@
server = "unixgram:///run/telegraf/telegraf_syslog.sock"
best_effort = true
syslog_standard = "RFC3164"
+{% if influxdb_configured is defined %}
[[inputs.exec]]
commands = [
"{{ custom_scripts_dir }}/show_firewall_input_filter.py",
@@ -58,3 +77,4 @@
]
timeout = "10s"
data_format = "influx"
+{% endif %}
diff --git a/data/templates/netflow/uacctd.conf.tmpl b/data/templates/netflow/uacctd.conf.tmpl
index 11fc76769..1ee65a28e 100644
--- a/data/templates/netflow/uacctd.conf.tmpl
+++ b/data/templates/netflow/uacctd.conf.tmpl
@@ -21,7 +21,7 @@ imt_mem_pools_number: 169
{% endif %}
plugins: {% if templatecfg['netflow']['servers'] != none %}
{% for server in templatecfg['netflow']['servers'] %}
-{% if loop.last %}nfprobe[nf_{{ server['address'] }}]{% else %}nfprobe[nf_{{ server['address'] }}],{% endif %}
+{% if loop.last %}nfprobe[nf_{{ server['address'] | dot_colon_to_dash }}]{% else %}nfprobe[nf_{{ server['address'] | dot_colon_to_dash }}],{% endif %}
{% endfor %}
{% set plugins_presented = true %}
{% endif %}
@@ -41,35 +41,35 @@ plugins: {% if templatecfg['netflow']['servers'] != none %}
{% if templatecfg['netflow']['servers'] != none %}
{% for server in templatecfg['netflow']['servers'] %}
-nfprobe_receiver[nf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }}
-nfprobe_version[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['version'] }}
+nfprobe_receiver[nf_{{ server['address'] | dot_colon_to_dash }}]: {{ server['address'] | bracketize_ipv6 }}:{{ server['port'] }}
+nfprobe_version[nf_{{ server['address'] | dot_colon_to_dash }}]: {{ templatecfg['netflow']['version'] }}
{% if templatecfg['netflow']['engine-id'] != none %}
-nfprobe_engine[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['engine-id'] }}
+nfprobe_engine[nf_{{ server['address'] | dot_colon_to_dash }}]: {{ templatecfg['netflow']['engine-id'] }}
{% endif %}
{% if templatecfg['netflow']['max-flows'] != none %}
-nfprobe_maxflows[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['max-flows'] }}
+nfprobe_maxflows[nf_{{ server['address'] | dot_colon_to_dash }}]: {{ templatecfg['netflow']['max-flows'] }}
{% endif %}
{% if templatecfg['netflow']['sampling-rate'] != none %}
-sampling_rate[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['sampling-rate'] }}
+sampling_rate[nf_{{ server['address'] | dot_colon_to_dash }}]: {{ templatecfg['netflow']['sampling-rate'] }}
{% endif %}
{% if templatecfg['netflow']['source-ip'] != none %}
-nfprobe_source_ip[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['source-ip'] }}
+nfprobe_source_ip[nf_{{ server['address'] | dot_colon_to_dash }}]: {{ templatecfg['netflow']['source-ip'] }}
{% endif %}
{% if templatecfg['netflow']['timeout_string'] != '' %}
-nfprobe_timeouts[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['timeout_string'] }}
+nfprobe_timeouts[nf_{{ server['address'] | dot_colon_to_dash }}]: {{ templatecfg['netflow']['timeout_string'] }}
{% endif %}
{% endfor %}
{% endif %}
{% if templatecfg['sflow']['servers'] != none %}
{% for server in templatecfg['sflow']['servers'] %}
-sfprobe_receiver[sf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }}
-sfprobe_agentip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['agent-address'] }}
+sfprobe_receiver[sf_{{ server['address'] | dot_colon_to_dash }}]: {{ server['address'] | bracketize_ipv6 }}:{{ server['port'] }}
+sfprobe_agentip[sf_{{ server['address'] | dot_colon_to_dash }}]: {{ templatecfg['sflow']['agent-address'] }}
{% if templatecfg['sflow']['sampling-rate'] != none %}
-sampling_rate[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['sampling-rate'] }}
+sampling_rate[sf_{{ server['address'] | dot_colon_to_dash }}]: {{ templatecfg['sflow']['sampling-rate'] }}
{% endif %}
{% if templatecfg['sflow']['source-address'] != none %}
-sfprobe_source_ip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['source-address'] }}
+sfprobe_source_ip[sf_{{ server['address'] | dot_colon_to_dash }}]: {{ templatecfg['sflow']['source-address'] }}
{% endif %}
{% endfor %}
{% endif %}
diff --git a/data/templates/ntp/ntpd.conf.tmpl b/data/templates/ntp/ntpd.conf.tmpl
index e7afcc16b..30bfd575d 100644
--- a/data/templates/ntp/ntpd.conf.tmpl
+++ b/data/templates/ntp/ntpd.conf.tmpl
@@ -33,10 +33,17 @@ restrict {{ address|address_from_cidr }} mask {{ address|netmask_from_cidr }} no
{% endfor %}
{% endif %}
-{% if listen_address %}
+{% if listen_address is defined or interface is defined %}
# NTP should listen on configured addresses only
interface ignore wildcard
-{% for address in listen_address %}
+{% if listen_address is defined %}
+{% for address in listen_address %}
interface listen {{ address }}
-{% endfor %}
+{% endfor %}
+{% endif %}
+{% if interface is defined %}
+{% for ifname in interface %}
+interface listen {{ ifname }}
+{% endfor %}
+{% endif %}
{% endif %}
diff --git a/data/templates/ocserv/ocserv_config.tmpl b/data/templates/ocserv/ocserv_config.tmpl
index 328af0c0d..8a394f0ac 100644
--- a/data/templates/ocserv/ocserv_config.tmpl
+++ b/data/templates/ocserv/ocserv_config.tmpl
@@ -70,6 +70,11 @@ ipv6-network = {{ network_settings.client_ipv6_pool.prefix }}
ipv6-subnet-prefix = {{ network_settings.client_ipv6_pool.mask }}
{% endif %}
{% endif %}
+{% if network_settings.split_dns is defined %}
+{% for tmp in network_settings.split_dns %}
+split-dns = {{ tmp }}
+{% endfor %}
+{% endif %}
{% endif %}
{% if network_settings.push_route is string %}
@@ -79,4 +84,3 @@ route = {{ network_settings.push_route }}
route = {{ route }}
{% endfor %}
{% endif %}
-
diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl
index 75aae2981..627068b3e 100644
--- a/data/templates/openvpn/server.conf.tmpl
+++ b/data/templates/openvpn/server.conf.tmpl
@@ -82,15 +82,18 @@ push "route-ipv6 {{ route }}"
{% endif %}
{% endfor %}
{% endif %}
+
+{% if server.client_ip_pool is not defined %}
{# OpenVPN assigns the first IP address to its local interface so the pool used #}
{# in net30 topology - where each client receives a /30 must start from the second subnet #}
{% if server.topology is defined and server.topology == 'net30' %}
-ifconfig-pool {{ subnet | inc_ip('4') }} {{ subnet | last_host_address | dec_ip('1') }} {{ subnet | netmask_from_cidr if device_type == 'tap' else '' }}
+ifconfig-pool {{ subnet | inc_ip('4') }} {{ subnet | last_host_address | dec_ip('1') }} {% if device_type == 'tap' %} {{ subnet | netmask_from_cidr }} {% endif %}
{% else %}
{# OpenVPN assigns the first IP address to its local interface so the pool must #}
{# start from the second address and end on the last address #}
-ifconfig-pool {{ subnet | first_host_address | inc_ip('1') }} {{ subnet | last_host_address | dec_ip('1') }} {{ subnet | netmask_from_cidr if device_type == 'tun' else '' }}
+ifconfig-pool {{ subnet | first_host_address | inc_ip('1') }} {{ subnet | last_host_address | dec_ip('1') }} {% if device_type == 'tap' %} {{ subnet | netmask_from_cidr }} {% endif %}
{% endif %}
+{% endif %}
{% elif subnet | is_ipv6 %}
server-ipv6 {{ subnet }}
{% endif %}
@@ -98,7 +101,7 @@ server-ipv6 {{ subnet }}
{% endif %}
{% if server.client_ip_pool is defined and server.client_ip_pool is not none and server.client_ip_pool.disable is not defined %}
-ifconfig-pool {{ server.client_ip_pool.start }} {{ server.client_ip_pool.stop }}{{ server.client_ip_pool.subnet_mask if server.client_ip_pool.subnet_mask is defined and server.client_ip_pool.subnet_mask is not none }}
+ifconfig-pool {{ server.client_ip_pool.start }} {{ server.client_ip_pool.stop }} {{ server.client_ip_pool.subnet_mask if server.client_ip_pool.subnet_mask is defined and server.client_ip_pool.subnet_mask is not none and device_type == 'tap' }}
{% endif %}
{% if server.max_connections is defined and server.max_connections is not none %}
max-clients {{ server.max_connections }}
diff --git a/data/templates/pppoe/ip-down.script.tmpl b/data/templates/pppoe/ip-down.script.tmpl
index bac4155d6..c1d1132b3 100644
--- a/data/templates/pppoe/ip-down.script.tmpl
+++ b/data/templates/pppoe/ip-down.script.tmpl
@@ -33,6 +33,6 @@ vtysh -c "conf t" ${VRF_NAME} -c "no ipv6 route ::/0 {{ ifname }} ${VRF_NAME}"
{% endif %}
{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %}
-# Stop wide dhcpv6 client
-systemctl stop dhcp6c@{{ ifname }}.service
+# Stop wide dhcpv6 client without blocking (by default the ip-down script can only run up to 5 seconds)
+systemctl stop --no-block dhcp6c@{{ ifname }}.service
{% endif %}
diff --git a/data/templates/pppoe/ipv6-up.script.tmpl b/data/templates/pppoe/ipv6-up.script.tmpl
index 7e1bc33b4..238f8c28e 100644
--- a/data/templates/pppoe/ipv6-up.script.tmpl
+++ b/data/templates/pppoe/ipv6-up.script.tmpl
@@ -36,6 +36,14 @@ echo 1 > /proc/sys/net/ipv6/conf/{{ ifname }}/forwarding
#
echo 2 > /proc/sys/net/ipv6/conf/{{ ifname }}/accept_ra
+{% if default_route == 'none' %}
+# Prevent learning of default router from router advertisements
+echo 0 > /proc/sys/net/ipv6/conf/{{ ifname }}/accept_ra_defrtr
+{% else %}
+# Enable learning of default router from router advertisements
+echo 1 > /proc/sys/net/ipv6/conf/{{ ifname }}/accept_ra_defrtr
+{% endif %}
+
# Autoconfigure addresses using Prefix Information in Router Advertisements.
echo 1 > /proc/sys/net/ipv6/conf/{{ ifname }}/autoconf
{% endif %}
diff --git a/data/templates/snmp/etc.snmpd.conf.tmpl b/data/templates/snmp/etc.snmpd.conf.tmpl
index db2114fa1..30806ce8a 100644
--- a/data/templates/snmp/etc.snmpd.conf.tmpl
+++ b/data/templates/snmp/etc.snmpd.conf.tmpl
@@ -39,7 +39,7 @@ SysDescr {{ description }}
{% endif %}
# Listen
-agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161{% if ipv6_enabled %},udp6:161{% endif %}{% endif %}
+agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},{{protocol}}:161{% if ipv6_enabled %},{{protocol}}6:161{% endif %}{% endif %}
# SNMP communities
{% for c in communities %}
diff --git a/data/templates/syslog/rsyslog.conf.tmpl b/data/templates/syslog/rsyslog.conf.tmpl
index e25ef48d4..2fb621760 100644
--- a/data/templates/syslog/rsyslog.conf.tmpl
+++ b/data/templates/syslog/rsyslog.conf.tmpl
@@ -25,12 +25,18 @@ $outchannel {{ file }},{{ file_options['log-file'] }},{{ file_options['max-size'
{% if host_options.proto == 'tcp' %}
{% if host_options.port is defined %}
{% if host_options.oct_count is defined %}
-{{ host_options.selectors }} @@(o){{ host }}:{{ host_options.port }};RSYSLOG_SyslogProtocol23Format
+{{ host_options.selectors }} @@(o){{ host | bracketize_ipv6 }}:{{ host_options.port }};RSYSLOG_SyslogProtocol23Format
{% else %}
-{{ host_options.selectors }} @@{{ host }}:{{ host_options.port }}
+{{ host_options.selectors }} @@{{ host | bracketize_ipv6 }}:{{ host_options.port }}
{% endif %}
{% else %}
-{{ host_options.selectors }} @@{{ host }}
+{{ host_options.selectors }} @@{{ host | bracketize_ipv6 }}
+{% endif %}
+{% elif host_options.proto == 'udp' %}
+{% if host_options.port is defined %}
+{{ host_options.selectors }} @{{ host | bracketize_ipv6 }}:{{ host_options.port }}{{ ';RSYSLOG_SyslogProtocol23Format' if host_options.oct_count is sameas true }}
+{% else %}
+{{ host_options.selectors }} @{{ host | bracketize_ipv6 }}
{% endif %}
{% else %}
{% if host_options['port'] %}
diff --git a/data/templates/vyos-hostsd/hosts.tmpl b/data/templates/vyos-hostsd/hosts.tmpl
index 8b73c6e51..03662d562 100644
--- a/data/templates/vyos-hostsd/hosts.tmpl
+++ b/data/templates/vyos-hostsd/hosts.tmpl
@@ -17,8 +17,9 @@ ff02::2 ip6-allrouters
{% for tag, taghosts in hosts.items() %}
# {{ tag }}
{% for host, hostprops in taghosts.items() if hostprops.address is defined %}
-{{ "%-15s" | format(hostprops.address) }} {{ host }} {{ hostprops.aliases|join(' ') if hostprops.aliases is defined }}
+{% for addr in hostprops.address %}
+{{ "%-15s" | format(addr) }} {{ host }} {{ hostprops.aliases|join(' ') if hostprops.aliases is defined }}
+{% endfor %}
{% endfor %}
{% endfor %}
{% endif %}
-
diff --git a/debian/control b/debian/control
index 7706c6523..5100f326a 100644
--- a/debian/control
+++ b/debian/control
@@ -51,7 +51,7 @@ Depends:
frr,
frr-pythontools,
grc,
- hostapd (>= 0.6.8),
+ hostapd,
hvinfo,
igmpproxy,
ipaddrcheck,
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
index 38a032a63..96c3f2008 100644
--- a/debian/vyos-1x.install
+++ b/debian/vyos-1x.install
@@ -1,4 +1,3 @@
-etc/cron.d
etc/dhcp
etc/netplug
etc/ppp
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index d54f6c5af..6ff466c27 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -36,7 +36,7 @@ chsh -s /bin/sh proxy
# Remove unwanted daemon files from /etc
# conntackd
-DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd"
+DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd /etc/ppp/ip-up.d/0000usepeerdns /etc/ppp/ip-down.d/0000usepeerdns"
for file in $DELETE; do
if [ -f ${file} ]; then
rm -f ${file}
diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in
index d1ed579e9..8c10ccf99 100644
--- a/interface-definitions/dhcp-server.xml.in
+++ b/interface-definitions/dhcp-server.xml.in
@@ -369,6 +369,18 @@
<leafNode name="tftp-server-name">
<properties>
<help>TFTP server name</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>TFTP server IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hostname</format>
+ <description>TFTP server FQDN</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="fqdn"/>
+ </constraint>
</properties>
</leafNode>
<leafNode name="time-offset">
diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in
index 2b1644609..005a55ab3 100644
--- a/interface-definitions/dns-domain-name.xml.in
+++ b/interface-definitions/dns-domain-name.xml.in
@@ -102,11 +102,11 @@
<constraint>
<validator name="ip-address"/>
</constraint>
+ <multi/>
</properties>
</leafNode>
</children>
</tagNode>
-
</children>
</node>
</children>
diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in
index 250642691..64826516e 100644
--- a/interface-definitions/dns-dynamic.xml.in
+++ b/interface-definitions/dns-dynamic.xml.in
@@ -274,6 +274,12 @@
</leafNode>
</children>
</node>
+ <leafNode name="ipv6-enable">
+ <properties>
+ <help>Allow explicit IPv6 addresses for Dynamic DNS for this interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in
index 5b0c87597..5a824973a 100644
--- a/interface-definitions/dns-forwarding.xml.in
+++ b/interface-definitions/dns-forwarding.xml.in
@@ -36,6 +36,18 @@
<multi/>
</properties>
</leafNode>
+ <leafNode name="dns64-prefix">
+ <properties>
+ <help>Help to communicate between IPv6-only client and IPv4-only server</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and /96 only prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
<leafNode name="dnssec">
<properties>
<help>DNSSEC mode (default: process-no-validate)</help>
diff --git a/interface-definitions/firewall-options.xml.in b/interface-definitions/firewall-options.xml.in
index 8d9225a9a..1bcee2011 100644
--- a/interface-definitions/firewall-options.xml.in
+++ b/interface-definitions/firewall-options.xml.in
@@ -20,24 +20,40 @@
<leafNode name="adjust-mss">
<properties>
<help>Adjust MSS for IPv4 transit packets</help>
+ <completionHelp>
+ <list>clamp-mss-to-pmtu</list>
+ </completionHelp>
<valueHelp>
- <format>500-1460</format>
+ <format>clamp-mss-to-pmtu</format>
+ <description>Automatically sets the MSS to the proper value</description>
+ </valueHelp>
+ <valueHelp>
+ <format>536-65535</format>
<description>TCP Maximum segment size in bytes</description>
</valueHelp>
<constraint>
- <validator name="numeric" argument="--range 500-1460"/>
+ <validator name="numeric" argument="--range 536-65535"/>
+ <regex>(clamp-mss-to-pmtu)</regex>
</constraint>
</properties>
</leafNode>
<leafNode name="adjust-mss6">
<properties>
<help>Adjust MSS for IPv6 transit packets</help>
+ <completionHelp>
+ <list>clamp-mss-to-pmtu</list>
+ </completionHelp>
+ <valueHelp>
+ <format>clamp-mss-to-pmtu</format>
+ <description>Automatically sets the MSS to the proper value</description>
+ </valueHelp>
<valueHelp>
- <format>1280-1492</format>
+ <format>1220-65535</format>
<description>TCP Maximum segment size in bytes</description>
</valueHelp>
<constraint>
- <validator name="numeric" argument="--range 1280-1492"/>
+ <validator name="numeric" argument="--range 1220-65535"/>
+ <regex>(clamp-mss-to-pmtu)</regex>
</constraint>
</properties>
</leafNode>
diff --git a/interface-definitions/include/accel-ppp/client-ip-pool-subnet-single.xml.i b/interface-definitions/include/accel-ppp/client-ip-pool-subnet-single.xml.i
new file mode 100644
index 000000000..e5918b765
--- /dev/null
+++ b/interface-definitions/include/accel-ppp/client-ip-pool-subnet-single.xml.i
@@ -0,0 +1,15 @@
+<!-- include start from accel-ppp/client-ip-pool-subnet-single.xml.i -->
+<leafNode name="subnet">
+ <properties>
+ <help>Client IP subnet (CIDR notation)</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/accel-ppp/radius-additions-rate-limit.xml.i b/interface-definitions/include/accel-ppp/radius-additions-rate-limit.xml.i
index be49fce5a..f44920c3f 100644
--- a/interface-definitions/include/accel-ppp/radius-additions-rate-limit.xml.i
+++ b/interface-definitions/include/accel-ppp/radius-additions-rate-limit.xml.i
@@ -21,6 +21,20 @@
<valueless />
</properties>
</leafNode>
+ <leafNode name="multiplier">
+ <properties>
+ <help>Shaper multiplier</help>
+ <valueHelp>
+ <format>&lt;0.001-1000&gt;</format>
+ <description>Shaper multiplier</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0.001-1000 --float"/>
+ </constraint>
+ <constraintErrorMessage>Multiplier needs to be between 0.001 and 1000</constraintErrorMessage>
+ </properties>
+ <defaultValue>1</defaultValue>
+ </leafNode>
</children>
</node>
<!-- include end -->
diff --git a/interface-definitions/include/generic-interface-multi.xml.i b/interface-definitions/include/generic-interface-multi.xml.i
new file mode 100644
index 000000000..65aae28ae
--- /dev/null
+++ b/interface-definitions/include/generic-interface-multi.xml.i
@@ -0,0 +1,18 @@
+<!-- include start from generic-interface-multi.xml.i -->
+<leafNode name="interface">
+ <properties>
+ <help>Interface to use</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <constraint>
+ <validator name="interface-name"/>
+ </constraint>
+ <multi/>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/dhcp-options.xml.i b/interface-definitions/include/interface/dhcp-options.xml.i
index 39a3dbb2e..5b30ca08b 100644
--- a/interface-definitions/include/interface/dhcp-options.xml.i
+++ b/interface-definitions/include/interface/dhcp-options.xml.i
@@ -14,6 +14,12 @@
<help>Override system host-name sent to DHCP server</help>
</properties>
</leafNode>
+ <leafNode name="mtu">
+ <properties>
+ <help>Use MTU value from DHCP server - ignore interface setting</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<leafNode name="vendor-class-id">
<properties>
<help>Identify the vendor client type to the DHCP server</help>
diff --git a/interface-definitions/include/interface/dhcpv6-options.xml.i b/interface-definitions/include/interface/dhcpv6-options.xml.i
index a0cac34f1..e3be6713d 100644
--- a/interface-definitions/include/interface/dhcpv6-options.xml.i
+++ b/interface-definitions/include/interface/dhcpv6-options.xml.i
@@ -71,11 +71,11 @@
<properties>
<help>Interface site-Level aggregator (SLA)</help>
<valueHelp>
- <format>u32:0-128</format>
+ <format>u32:0-65535</format>
<description>Decimal integer which fits in the length of SLA IDs</description>
</valueHelp>
<constraint>
- <validator name="numeric" argument="--range 0-128"/>
+ <validator name="numeric" argument="--range 0-65535"/>
</constraint>
</properties>
</leafNode>
diff --git a/interface-definitions/include/isis/password.xml.i b/interface-definitions/include/isis/password.xml.i
new file mode 100644
index 000000000..27c3b0fa0
--- /dev/null
+++ b/interface-definitions/include/isis/password.xml.i
@@ -0,0 +1,20 @@
+<!-- include start from isis/password.xml.i -->
+<leafNode name="plaintext-password">
+ <properties>
+ <help>Plain-text authentication type</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Circuit password</description>
+ </valueHelp>
+ </properties>
+</leafNode>
+<leafNode name="md5">
+ <properties>
+ <help>MD5 authentication type</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Level-wide password</description>
+ </valueHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i
index 84e2f7bb2..190fa6149 100644
--- a/interface-definitions/include/isis/protocol-common-config.xml.i
+++ b/interface-definitions/include/isis/protocol-common-config.xml.i
@@ -4,24 +4,7 @@
<help>Configure the authentication password for an area</help>
</properties>
<children>
- <leafNode name="plaintext-password">
- <properties>
- <help>Plain-text authentication type</help>
- <valueHelp>
- <format>txt</format>
- <description>Level-wide password</description>
- </valueHelp>
- </properties>
- </leafNode>
- <leafNode name="md5">
- <properties>
- <help>MD5 authentication type</help>
- <valueHelp>
- <format>txt</format>
- <description>Level-wide password</description>
- </valueHelp>
- </properties>
- </leafNode>
+ #include <include/isis/password.xml.i>
</children>
</node>
<node name="default-information">
@@ -59,24 +42,7 @@
<help>Set the authentication password for a routing domain</help>
</properties>
<children>
- <leafNode name="plaintext-password">
- <properties>
- <help>Plain-text authentication type</help>
- <valueHelp>
- <format>txt</format>
- <description>Level-wide password</description>
- </valueHelp>
- </properties>
- </leafNode>
- <leafNode name="md5">
- <properties>
- <help>MD5 authentication type</help>
- <valueHelp>
- <format>txt</format>
- <description>Level-wide password</description>
- </valueHelp>
- </properties>
- </leafNode>
+ #include <include/isis/password.xml.i>
</children>
</node>
<leafNode name="dynamic-hostname">
@@ -104,7 +70,7 @@
<description>Act as an area router</description>
</valueHelp>
<constraint>
- <regex>^(level-1|level-1-2|level-2)$</regex>
+ <regex>(level-1|level-1-2|level-2)</regex>
</constraint>
</properties>
</leafNode>
@@ -182,7 +148,7 @@
<description>Use new style of TLVs to carry wider metric</description>
</valueHelp>
<constraint>
- <regex>^(narrow|transition|wide)$</regex>
+ <regex>(narrow|transition|wide)</regex>
</constraint>
</properties>
</leafNode>
@@ -668,7 +634,7 @@
<description>Level-2 only adjacencies are formed</description>
</valueHelp>
<constraint>
- <regex>^(level-1|level-1-2|level-2-only)$</regex>
+ <regex>(level-1|level-1-2|level-2-only)</regex>
</constraint>
</properties>
</leafNode>
@@ -722,15 +688,7 @@
<help>Configure the authentication password for a circuit</help>
</properties>
<children>
- <leafNode name="plaintext-password">
- <properties>
- <help>Plain-text authentication type</help>
- <valueHelp>
- <format>txt</format>
- <description>Circuit password</description>
- </valueHelp>
- </properties>
- </leafNode>
+ #include <include/isis/password.xml.i>
</children>
</node>
<leafNode name="priority">
diff --git a/interface-definitions/include/url.xml.i b/interface-definitions/include/url.xml.i
new file mode 100644
index 000000000..caa6f67bd
--- /dev/null
+++ b/interface-definitions/include/url.xml.i
@@ -0,0 +1,15 @@
+<!-- include start from url.xml.i -->
+<leafNode name="url">
+ <properties>
+ <help>Remote URL</help>
+ <valueHelp>
+ <format>url</format>
+ <description>Remote URL</description>
+ </valueHelp>
+ <constraint>
+ <regex>^https?:\/\/?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\:[0-9]+)*(\/.*)?</regex>
+ </constraint>
+ <constraintErrorMessage>Incorrect URL format</constraintErrorMessage>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/username.xml.i b/interface-definitions/include/username.xml.i
new file mode 100644
index 000000000..2263ec08d
--- /dev/null
+++ b/interface-definitions/include/username.xml.i
@@ -0,0 +1,11 @@
+<!-- include start from username.xml.i -->
+<leafNode name="username">
+ <properties>
+ <help>Authentication username</help>
+ <constraint>
+ <regex>^[-_a-zA-Z0-9.]{1,100}</regex>
+ </constraint>
+ <constraintErrorMessage>Illegal characters or more than 100 characters</constraintErrorMessage>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in
index 5a4f08bef..307dd7558 100644
--- a/interface-definitions/interfaces-bonding.xml.in
+++ b/interface-definitions/interfaces-bonding.xml.in
@@ -92,6 +92,23 @@
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
#include <include/interface/mac.xml.i>
+ <leafNode name="mii-mon-interval">
+ <properties>
+ <help>Specifies the MII link monitoring frequency in milliseconds</help>
+ <valueHelp>
+ <format>u32:0</format>
+ <description>Disable MII link monitoring</description>
+ </valueHelp>
+ <valueHelp>
+ <format>u32:50-1000</format>
+ <description>MII link monitoring frequency in milliseconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-0 --range 50-1000"/>
+ </constraint>
+ </properties>
+ <defaultValue>100</defaultValue>
+ </leafNode>
<leafNode name="min-links">
<properties>
<help>Minimum number of member interfaces required up before enabling bond</help>
diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in
index 96dcf3ca0..fa54aedc1 100644
--- a/interface-definitions/interfaces-macsec.xml.in
+++ b/interface-definitions/interfaces-macsec.xml.in
@@ -60,11 +60,12 @@
<properties>
<help>Secure Connectivity Association Key</help>
<valueHelp>
- <format>key</format>
- <description>16-byte (128-bit) hex-string (32 hex-digits)</description>
+ <format>txt</format>
+ <description>16-byte (128-bit) hex-string (32 hex-digits) for gcm-aes-128 or 32-byte (256-bit) hex-string (64 hex-digits) for gcm-aes-256</description>
</valueHelp>
<constraint>
- <regex>^[A-Fa-f0-9]{32}$</regex>
+ <regex>[A-Fa-f0-9]{32}</regex>
+ <regex>[A-Fa-f0-9]{64}</regex>
</constraint>
</properties>
</leafNode>
@@ -72,7 +73,7 @@
<properties>
<help>Secure Connectivity Association Key Name</help>
<valueHelp>
- <format>key</format>
+ <format>txt</format>
<description>32-byte (256-bit) hex-string (64 hex-digits)</description>
</valueHelp>
<constraint>
diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in
index cc3fe2a6a..77afa3b15 100644
--- a/interface-definitions/interfaces-wireless.xml.in
+++ b/interface-definitions/interfaces-wireless.xml.in
@@ -6,6 +6,9 @@
<properties>
<help>Wireless (WiFi/WLAN) Network Interface</help>
<priority>318</priority>
+ <completionHelp>
+ <script>cd /sys/class/net; if compgen -G "wlan*" > /dev/null; then ls -d wlan*; fi</script>
+ </completionHelp>
<constraint>
<regex>^wlan[0-9]+$</regex>
</constraint>
diff --git a/interface-definitions/interfaces-wwan.xml.in b/interface-definitions/interfaces-wwan.xml.in
index 19f152a06..e3feb7a8b 100644
--- a/interface-definitions/interfaces-wwan.xml.in
+++ b/interface-definitions/interfaces-wwan.xml.in
@@ -7,7 +7,7 @@
<help>Wireless Modem (WWAN) Interface</help>
<priority>350</priority>
<completionHelp>
- <script>cd /sys/class/net; ls -d wwan*</script>
+ <script>cd /sys/class/net; if compgen -G "wwan*" > /dev/null; then ls -d wwan*; fi</script>
</completionHelp>
<constraint>
<regex>^wwan[0-9]+$</regex>
diff --git a/interface-definitions/ntp.xml.in b/interface-definitions/ntp.xml.in
index a518a9def..85636a50f 100644
--- a/interface-definitions/ntp.xml.in
+++ b/interface-definitions/ntp.xml.in
@@ -81,6 +81,7 @@
</leafNode>
</children>
</node>
+ #include <include/generic-interface-multi.xml.i>
#include <include/listen-address.xml.i>
#include <include/interface/vrf.xml.i>
</children>
diff --git a/interface-definitions/protocols-mpls.xml.in b/interface-definitions/protocols-mpls.xml.in
index de1aa436d..a9de3aa14 100644
--- a/interface-definitions/protocols-mpls.xml.in
+++ b/interface-definitions/protocols-mpls.xml.in
@@ -6,7 +6,7 @@
<node name="mpls" owner="${vyos_conf_scripts_dir}/protocols_mpls.py">
<properties>
<help>Multiprotocol Label Switching (MPLS)</help>
- <priority>299</priority>
+ <priority>400</priority>
</properties>
<children>
<node name="ldp">
diff --git a/interface-definitions/protocols-rpki.xml.in b/interface-definitions/protocols-rpki.xml.in
index 94fab54a5..707eb4bf1 100644
--- a/interface-definitions/protocols-rpki.xml.in
+++ b/interface-definitions/protocols-rpki.xml.in
@@ -12,15 +12,15 @@
<help>RPKI cache server address</help>
<valueHelp>
<format>ipv4</format>
- <description>IP address of NTP server</description>
+ <description>IP address of RPKI server</description>
</valueHelp>
<valueHelp>
<format>ipv6</format>
- <description>IPv6 address of NTP server</description>
+ <description>IPv6 address of RPKI server</description>
</valueHelp>
<valueHelp>
<format>hostname</format>
- <description>Fully qualified domain name of NTP server</description>
+ <description>Fully qualified domain name of RPKI server</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in
index b19acab56..b9279793c 100644
--- a/interface-definitions/service_ipoe-server.xml.in
+++ b/interface-definitions/service_ipoe-server.xml.in
@@ -112,6 +112,26 @@
</children>
</tagNode>
#include <include/name-server-ipv4-ipv6.xml.i>
+ <node name="client-ip-pool">
+ <properties>
+ <help>Client IP pools and gateway setting</help>
+ </properties>
+ <children>
+ <tagNode name="name">
+ <properties>
+ <help>Pool name</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>Client IP pool is limited to alphanumerical characters and can contain hyphen and underscores</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/accel-ppp/gateway-address.xml.i>
+ #include <include/accel-ppp/client-ip-pool-subnet-single.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
#include <include/accel-ppp/client-ipv6-pool.xml.i>
<node name="authentication">
<properties>
@@ -197,6 +217,11 @@
</tagNode>
</children>
</tagNode>
+ <node name="radius">
+ <children>
+ #include <include/accel-ppp/radius-additions-rate-limit.xml.i>
+ </children>
+ </node>
#include <include/radius-server-ipv4.xml.i>
#include <include/accel-ppp/radius-additions.xml.i>
</children>
diff --git a/interface-definitions/service_monitoring_telegraf.xml.in b/interface-definitions/service_monitoring_telegraf.xml.in
index 81ba67430..8a6b31d8c 100644
--- a/interface-definitions/service_monitoring_telegraf.xml.in
+++ b/interface-definitions/service_monitoring_telegraf.xml.in
@@ -85,19 +85,86 @@
</properties>
<defaultValue>all</defaultValue>
</leafNode>
- <leafNode name="url">
- <properties>
- <help>Remote URL [REQUIRED]</help>
- <valueHelp>
- <format>url</format>
- <description>Remote URL to InfluxDB v2</description>
- </valueHelp>
- <constraint>
- <regex>^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}?(\/.*)?$</regex>
- </constraint>
- <constraintErrorMessage>Incorrect URL format.</constraintErrorMessage>
- </properties>
- </leafNode>
+ <node name="prometheus-client">
+ <properties>
+ <help>Output plugin Prometheus client</help>
+ </properties>
+ <children>
+ <node name="authentication">
+ <properties>
+ <help>HTTP basic authentication parameters</help>
+ </properties>
+ <children>
+ #include <include/username.xml.i>
+ <leafNode name="password">
+ <properties>
+ <help>Authentication password</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Authentication password</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="allow-from">
+ <properties>
+ <help>Networks allowed to query this server</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IP address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ip-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="listen-address">
+ <properties>
+ <help>Local IP addresses to listen on</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_local_ips.sh --both</script>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to listen for incoming connections</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address to listen for incoming connections</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ <validator name="ipv6-link-local"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="metric-version">
+ <properties>
+ <help>Metric version control mapping from Telegraf to Prometheus format</help>
+ <valueHelp>
+ <format>u32:1-2</format>
+ <description>Metric version (default: 2)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-2"/>
+ </constraint>
+ </properties>
+ <defaultValue>2</defaultValue>
+ </leafNode>
+ #include <include/port-number.xml.i>
+ <leafNode name="port">
+ <defaultValue>9273</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ #include <include/url.xml.i>
#include <include/port-number.xml.i>
<leafNode name="port">
<defaultValue>8086</defaultValue>
diff --git a/interface-definitions/service_webproxy.xml.in b/interface-definitions/service_webproxy.xml.in
index 3acac7f9d..bd5396291 100644
--- a/interface-definitions/service_webproxy.xml.in
+++ b/interface-definitions/service_webproxy.xml.in
@@ -488,9 +488,9 @@
<description>Name of source group</description>
</valueHelp>
<constraint>
- <regex>^[^0-9]</regex>
+ <regex>^(?!-|_|\d)[a-zA-Z_][a-zA-Z0-9][\w\-\.]*</regex>
</constraint>
- <constraintErrorMessage>URL-filter source-group cannot start with a number!</constraintErrorMessage>
+ <constraintErrorMessage>URL-filter source-group must start with a letter and can contain dashes or underscores!</constraintErrorMessage>
</properties>
<children>
<leafNode name="address">
diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in
index 3cb736bf7..1067aa8a8 100644
--- a/interface-definitions/snmp.xml.in
+++ b/interface-definitions/snmp.xml.in
@@ -13,9 +13,9 @@
<properties>
<help>Community name</help>
<constraint>
- <regex>^[a-zA-Z0-9\-_]{1,100}$</regex>
+ <regex>^[a-zA-Z0-9\-_!@*#]{1,100}$</regex>
</constraint>
- <constraintErrorMessage>Community string is limited to alphanumerical characters only with a total lenght of 100</constraintErrorMessage>
+ <constraintErrorMessage>Community string is limited to alphanumerical characters, !, @, * and # with a total lenght of 100</constraintErrorMessage>
</properties>
<children>
<leafNode name="authorization">
@@ -129,6 +129,26 @@
<constraintErrorMessage>Location is limited to 255 characters or less</constraintErrorMessage>
</properties>
</leafNode>
+ <leafNode name="protocol">
+ <properties>
+ <help>Listen protocol for SNMP</help>
+ <completionHelp>
+ <list>udp tcp</list>
+ </completionHelp>
+ <valueHelp>
+ <format>udp</format>
+ <description>Listen protocol UDP (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp</format>
+ <description>Listen protocol TCP</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(udp|tcp)$</regex>
+ </constraint>
+ </properties>
+ <defaultValue>udp</defaultValue>
+ </leafNode>
<leafNode name="smux-peer">
<properties>
<help>Register a subtree for SMUX-based processing</help>
diff --git a/interface-definitions/system-proxy.xml.in b/interface-definitions/system-proxy.xml.in
index 791f41f2f..abd29ea22 100644
--- a/interface-definitions/system-proxy.xml.in
+++ b/interface-definitions/system-proxy.xml.in
@@ -11,7 +11,7 @@
<properties>
<help>Proxy URL</help>
<constraint>
- <regex>http:\/\/[a-z0-9\.]+$</regex>
+ <regex>http(s)?:\/\/[a-z0-9-\.]+$</regex>
</constraint>
</properties>
</leafNode>
diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn_openconnect.xml.in
index f35b1ebbd..888f32b99 100644
--- a/interface-definitions/vpn_openconnect.xml.in
+++ b/interface-definitions/vpn_openconnect.xml.in
@@ -191,6 +191,19 @@
</children>
</node>
#include <include/name-server-ipv4-ipv6.xml.i>
+ <leafNode name="split-dns">
+ <properties>
+ <help>Domains over which the provided DNS should be used</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Client prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="fqdn"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
</children>
</node>
</children>
diff --git a/op-mode-definitions/clear-dhcp-server-lease.xml.in b/op-mode-definitions/clear-dhcp-server-lease.xml.in
new file mode 100644
index 000000000..b1241588c
--- /dev/null
+++ b/op-mode-definitions/clear-dhcp-server-lease.xml.in
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="clear">
+ <children>
+ <node name="dhcp-server">
+ <properties>
+ <help>clear DHCP server lease</help>
+ </properties>
+ <children>
+ <tagNode name="lease">
+ <properties>
+ <help>DHCP server lease</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/clear_dhcp_lease.py --ip $4</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/restart-frr.xml.in b/op-mode-definitions/restart-frr.xml.in
index 475bd1ee8..4e2be1bf2 100644
--- a/op-mode-definitions/restart-frr.xml.in
+++ b/op-mode-definitions/restart-frr.xml.in
@@ -26,6 +26,12 @@
</properties>
<command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon isisd</command>
</leafNode>
+ <leafNode name="ldp">
+ <properties>
+ <help>Restart the Label Distribution Protocol (LDP) daemon</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ldpd</command>
+ </leafNode>
<leafNode name="ospf">
<properties>
<help>Restart Open Shortest Path First (OSPF) routing daemon</help>
diff --git a/op-mode-definitions/webproxy.xml.in b/op-mode-definitions/webproxy.xml.in
index f8ec8fb0a..5ae1577d8 100644
--- a/op-mode-definitions/webproxy.xml.in
+++ b/op-mode-definitions/webproxy.xml.in
@@ -12,7 +12,7 @@
<properties>
<help>Monitor the last lines of the squid access log</help>
</properties>
- <command>if [ -f /var/log/squid3/access.log ]; then sudo tail --follow=name /var/log/squid3/access.log; else echo "WebProxy cache-log does not exist"; fi</command>
+ <command>if [ -f /var/log/squid/access.log ]; then sudo tail --follow=name /var/log/squid/access.log; else echo "WebProxy access-log does not exist"; fi</command>
</node>
<node name="background">
<properties>
@@ -37,7 +37,7 @@
<properties>
<help>Monitor the last lines of the squid cache log</help>
</properties>
- <command>if [ -f /var/log/squid3/cache.log ]; then sudo tail --follow=name /var/log/squid3/cache.log; else echo "WebProxy cache-log does not exist"; fi</command>
+ <command>if [ -f /var/log/squid/cache.log ]; then sudo tail --follow=name /var/log/squid/cache.log; else echo "WebProxy cache-log does not exist"; fi</command>
</node>
</children>
</node>
diff --git a/python/vyos/base.py b/python/vyos/base.py
index c78045548..9b93cb2f2 100644
--- a/python/vyos/base.py
+++ b/python/vyos/base.py
@@ -1,4 +1,4 @@
-# Copyright 2018-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2018-2022 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
@@ -15,6 +15,48 @@
from textwrap import fill
+
+class BaseWarning:
+ def __init__(self, header, message, **kwargs):
+ self.message = message
+ self.kwargs = kwargs
+ if 'width' not in kwargs:
+ self.width = 72
+ if 'initial_indent' in kwargs:
+ del self.kwargs['initial_indent']
+ if 'subsequent_indent' in kwargs:
+ del self.kwargs['subsequent_indent']
+ self.textinitindent = header
+ self.standardindent = ''
+
+ def print(self):
+ messages = self.message.split('\n')
+ isfirstmessage = True
+ initial_indent = self.textinitindent
+ print('')
+ for mes in messages:
+ mes = fill(mes, initial_indent=initial_indent,
+ subsequent_indent=self.standardindent, **self.kwargs)
+ if isfirstmessage:
+ isfirstmessage = False
+ initial_indent = self.standardindent
+ print(f'{mes}')
+ print('')
+
+
+class Warning():
+ def __init__(self, message, **kwargs):
+ self.BaseWarn = BaseWarning('WARNING: ', message, **kwargs)
+ self.BaseWarn.print()
+
+
+class DeprecationWarning():
+ def __init__(self, message, **kwargs):
+ # Reformat the message and trim it to 72 characters in length
+ self.BaseWarn = BaseWarning('DEPRECATION WARNING: ', message, **kwargs)
+ self.BaseWarn.print()
+
+
class ConfigError(Exception):
def __init__(self, message):
# Reformat the message and trim it to 72 characters in length
diff --git a/python/vyos/config.py b/python/vyos/config.py
index a5c1ad122..287fd2ed1 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -142,31 +142,41 @@ class Config(object):
def exists(self, path):
"""
- Checks if a node with given path exists in the running or proposed config
+ Checks if a node or value with given path exists in the proposed config.
+
+ Args:
+ path (str): Configuration tree path
Returns:
- True if node exists, False otherwise
+ True if node or value exists in the proposed config, False otherwise
Note:
- This function cannot be used outside a configuration sessions.
+ This function should not be used outside of configuration sessions.
In operational mode scripts, use ``exists_effective``.
"""
- if not self._session_config:
+ if self._session_config is None:
return False
+
+ # Assume the path is a node path first
if self._session_config.exists(self._make_path(path)):
return True
else:
+ # If that check fails, it may mean the path has a value at the end.
# libvyosconfig exists() works only for _nodes_, not _values_
- # libvyattacfg one also worked for values, so we emulate that case here
+ # libvyattacfg also worked for values, so we emulate that case here
if isinstance(path, str):
path = re.split(r'\s+', path)
path_without_value = path[:-1]
- path_str = " ".join(path_without_value)
try:
- value = self._session_config.return_value(self._make_path(path_str))
- return (value == path[-1])
+ # return_values() is safe to use with single-value nodes,
+ # it simply returns a single-item list in that case.
+ values = self._session_config.return_values(self._make_path(path_without_value))
+
+ # If we got this far, the node does exist and has values,
+ # so we need to check if it has the value in question among its values.
+ return (path[-1] in values)
except vyos.configtree.ConfigTreeError:
- # node doesn't exist at all
+ # Even the parent node doesn't exist at all
return False
def session_changed(self):
@@ -380,7 +390,7 @@ class Config(object):
def exists_effective(self, path):
"""
- Check if a node exists in the running (effective) config
+ Checks if a node or value exists in the running (effective) config.
Args:
path (str): Configuration tree path
@@ -392,10 +402,31 @@ class Config(object):
This function is safe to use in operational mode. In configuration mode,
it ignores uncommited changes.
"""
- if self._running_config:
- return(self._running_config.exists(self._make_path(path)))
+ if self._running_config is None:
+ return False
+
+ # Assume the path is a node path first
+ if self._running_config.exists(self._make_path(path)):
+ return True
+ else:
+ # If that check fails, it may mean the path has a value at the end.
+ # libvyosconfig exists() works only for _nodes_, not _values_
+ # libvyattacfg also worked for values, so we emulate that case here
+ if isinstance(path, str):
+ path = re.split(r'\s+', path)
+ path_without_value = path[:-1]
+ try:
+ # return_values() is safe to use with single-value nodes,
+ # it simply returns a single-item list in that case.
+ values = self._running_config.return_values(self._make_path(path_without_value))
+
+ # If we got this far, the node does exist and has values,
+ # so we need to check if it has the value in question among its values.
+ return (path[-1] in values)
+ except vyos.configtree.ConfigTreeError:
+ # Even the parent node doesn't exist at all
+ return False
- return False
def return_effective_value(self, path, default=None):
"""
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 1f245f3d2..785207c7f 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -104,6 +104,12 @@ def list_diff(first, second):
second = set(second)
return [item for item in first if item not in second]
+def is_node_changed(conf, path):
+ from vyos.configdiff import get_config_diff
+ D = get_config_diff(conf, key_mangling=('-', '_'))
+ D.set_level(conf.get_level())
+ return D.is_node_changed(path)
+
def leaf_node_changed(conf, path):
"""
Check if a leaf node was altered. If it has been altered - values has been
@@ -197,11 +203,12 @@ def is_member(conf, interface, intftype=None):
intftype is optional, if not passed it will search all known types
(currently bridge and bonding)
- Returns:
- None -> Interface is not a member
- interface name -> Interface is a member of this interface
- False -> interface type cannot have members
+ Returns: dict
+ empty -> Interface is not a member
+ key -> Interface is a member of this interface
"""
+ from vyos.ifconfig import Section
+
ret_val = {}
intftypes = ['bonding', 'bridge']
@@ -222,7 +229,8 @@ def is_member(conf, interface, intftype=None):
member = base + [intf, 'member', 'interface', interface]
if conf.exists(member):
tmp = conf.get_config_dict(member, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
ret_val.update({intf : tmp})
old_level = conf.set_level(old_level)
@@ -301,12 +309,18 @@ def is_source_interface(conf, interface, intftype=None):
"""
ret_val = None
intftypes = ['macsec', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan']
- if intftype not in intftypes + [None]:
+ if not intftype:
+ intftype = intftypes
+
+ if isinstance(intftype, str):
+ intftype = [intftype]
+ elif not isinstance(intftype, list):
+ raise ValueError(f'Interface type "{type(intftype)}" must be either str or list!')
+
+ if not all(x in intftypes for x in intftype):
raise ValueError(f'unknown interface type "{intftype}" or it can not '
'have a source-interface')
- intftype = intftypes if intftype == None else [intftype]
-
# set config level to root
old_level = conf.get_level()
conf.set_level([])
@@ -314,8 +328,8 @@ def is_source_interface(conf, interface, intftype=None):
for it in intftype:
base = ['interfaces', it]
for intf in conf.list_nodes(base):
- lower_intf = base + [intf, 'source-interface']
- if conf.exists(lower_intf) and interface in conf.return_values(lower_intf):
+ src_intf = base + [intf, 'source-interface']
+ if conf.exists(src_intf) and interface in conf.return_values(src_intf):
ret_val = intf
break
diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py
index 0e41fbe27..81932e6d0 100644
--- a/python/vyos/configdiff.py
+++ b/python/vyos/configdiff.py
@@ -16,6 +16,7 @@
from enum import IntFlag, auto
from vyos.config import Config
+from vyos.configtree import DiffTree
from vyos.configdict import dict_merge
from vyos.util import get_sub_dict, mangle_dict_keys
from vyos.xml import defaults
@@ -36,6 +37,8 @@ class Diff(IntFlag):
ADD = auto()
STABLE = auto()
+ALL = Diff.MERGE | Diff.DELETE | Diff.ADD | Diff.STABLE
+
requires_effective = [enum_to_key(Diff.DELETE)]
target_defaults = [enum_to_key(Diff.MERGE)]
@@ -73,19 +76,24 @@ def get_config_diff(config, key_mangling=None):
isinstance(key_mangling[1], str)):
raise ValueError("key_mangling must be a tuple of two strings")
- return ConfigDiff(config, key_mangling)
+ diff_t = DiffTree(config._running_config, config._session_config)
+
+ return ConfigDiff(config, key_mangling, diff_tree=diff_t)
class ConfigDiff(object):
"""
The class of config changes as represented by comparison between the
session config dict and the effective config dict.
"""
- def __init__(self, config, key_mangling=None):
+ def __init__(self, config, key_mangling=None, diff_tree=None):
self._level = config.get_level()
self._session_config_dict = config.get_cached_root_dict(effective=False)
self._effective_config_dict = config.get_cached_root_dict(effective=True)
self._key_mangling = key_mangling
+ self._diff_tree = diff_tree
+ self._diff_dict = diff_tree.dict if diff_tree else {}
+
# mirrored from Config; allow path arguments relative to level
def _make_path(self, path):
if isinstance(path, str):
@@ -134,7 +142,17 @@ class ConfigDiff(object):
self._key_mangling[1])
return config_dict
- def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False):
+ def is_node_changed(self, path=[]):
+ if self._diff_tree is None:
+ raise NotImplementedError("diff_tree class not available")
+
+ if (self._diff_tree.add.exists(self._make_path(path)) or
+ self._diff_tree.sub.exists(self._make_path(path))):
+ return True
+ return False
+
+ def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False,
+ recursive=False):
"""
Args:
path (str|list): config path
@@ -144,6 +162,8 @@ class ConfigDiff(object):
value
no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default
values to ret['merge']
+ recursive: if true, use config_tree diff algorithm provided by
+ diff_tree class
Returns: dict of lists, representing differences between session
and effective config, under path
@@ -154,6 +174,34 @@ class ConfigDiff(object):
"""
session_dict = get_sub_dict(self._session_config_dict,
self._make_path(path), get_first_key=True)
+
+ if recursive:
+ if self._diff_tree is None:
+ raise NotImplementedError("diff_tree class not available")
+ else:
+ add = get_sub_dict(self._diff_tree.dict, ['add'], get_first_key=True)
+ sub = get_sub_dict(self._diff_tree.dict, ['sub'], get_first_key=True)
+ inter = get_sub_dict(self._diff_tree.dict, ['inter'], get_first_key=True)
+ ret = {}
+ ret[enum_to_key(Diff.MERGE)] = session_dict
+ ret[enum_to_key(Diff.DELETE)] = get_sub_dict(sub, self._make_path(path),
+ get_first_key=True)
+ ret[enum_to_key(Diff.ADD)] = get_sub_dict(add, self._make_path(path),
+ get_first_key=True)
+ ret[enum_to_key(Diff.STABLE)] = get_sub_dict(inter, self._make_path(path),
+ get_first_key=True)
+ for e in Diff:
+ k = enum_to_key(e)
+ if not (e & expand_nodes):
+ ret[k] = list(ret[k])
+ else:
+ if self._key_mangling:
+ ret[k] = self._mangle_dict_keys(ret[k])
+ if k in target_defaults and not no_defaults:
+ default_values = defaults(self._make_path(path))
+ ret[k] = dict_merge(default_values, ret[k])
+ return ret
+
effective_dict = get_sub_dict(self._effective_config_dict,
self._make_path(path), get_first_key=True)
@@ -179,7 +227,8 @@ class ConfigDiff(object):
return ret
- def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False):
+ def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False,
+ recursive=False):
"""
Args:
path (str|list): config path
@@ -189,6 +238,8 @@ class ConfigDiff(object):
value
no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default
values to ret['merge']
+ recursive: if true, use config_tree diff algorithm provided by
+ diff_tree class
Returns: dict of lists, representing differences between session
and effective config, at path
@@ -198,6 +249,31 @@ class ConfigDiff(object):
dict['stable'] = config values in both session and effective
"""
session_dict = get_sub_dict(self._session_config_dict, self._make_path(path))
+
+ if recursive:
+ if self._diff_tree is None:
+ raise NotImplementedError("diff_tree class not available")
+ else:
+ add = get_sub_dict(self._diff_tree.dict, ['add'], get_first_key=True)
+ sub = get_sub_dict(self._diff_tree.dict, ['sub'], get_first_key=True)
+ inter = get_sub_dict(self._diff_tree.dict, ['inter'], get_first_key=True)
+ ret = {}
+ ret[enum_to_key(Diff.MERGE)] = session_dict
+ ret[enum_to_key(Diff.DELETE)] = get_sub_dict(sub, self._make_path(path))
+ ret[enum_to_key(Diff.ADD)] = get_sub_dict(add, self._make_path(path))
+ ret[enum_to_key(Diff.STABLE)] = get_sub_dict(inter, self._make_path(path))
+ for e in Diff:
+ k = enum_to_key(e)
+ if not (e & expand_nodes):
+ ret[k] = list(ret[k])
+ else:
+ if self._key_mangling:
+ ret[k] = self._mangle_dict_keys(ret[k])
+ if k in target_defaults and not no_defaults:
+ default_values = defaults(self._make_path(path))
+ ret[k] = dict_merge(default_values, ret[k])
+ return ret
+
effective_dict = get_sub_dict(self._effective_config_dict, self._make_path(path))
ret = _key_sets_from_dicts(session_dict, effective_dict)
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index 670e6c7fc..d2645e5e1 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -1,5 +1,5 @@
# configsession -- the write API for the VyOS running config
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 VyOS maintainers and contributors
#
# This library is free software; you can redistribute it and/or modify it under the terms of
# the GNU Lesser General Public License as published by the Free Software Foundation;
@@ -33,6 +33,7 @@ INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url']
REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del']
GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate']
SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show']
+RESET = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'reset']
# Default "commit via" string
APP = "vyos-http-api"
@@ -201,3 +202,6 @@ class ConfigSession(object):
out = self.__run_command(SHOW + path)
return out
+ def reset(self, path):
+ out = self.__run_command(RESET + path)
+ return out
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index d8ffaca99..e9cdb69e4 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -17,6 +17,7 @@ import json
from ctypes import cdll, c_char_p, c_void_p, c_int
+LIBPATH = '/usr/lib/libvyosconfig.so.0'
def escape_backslash(string: str) -> str:
"""Escape single backslashes in string that are not in escape sequence"""
@@ -42,7 +43,9 @@ class ConfigTreeError(Exception):
class ConfigTree(object):
- def __init__(self, config_string, libpath='/usr/lib/libvyosconfig.so.0'):
+ def __init__(self, config_string=None, address=None, libpath=LIBPATH):
+ if config_string is None and address is None:
+ raise TypeError("ConfigTree() requires one of 'config_string' or 'address'")
self.__config = None
self.__lib = cdll.LoadLibrary(libpath)
@@ -60,7 +63,7 @@ class ConfigTree(object):
self.__to_string.restype = c_char_p
self.__to_commands = self.__lib.to_commands
- self.__to_commands.argtypes = [c_void_p]
+ self.__to_commands.argtypes = [c_void_p, c_char_p]
self.__to_commands.restype = c_char_p
self.__to_json = self.__lib.to_json
@@ -123,18 +126,26 @@ class ConfigTree(object):
self.__set_tag.argtypes = [c_void_p, c_char_p]
self.__set_tag.restype = c_int
+ self.__get_subtree = self.__lib.get_subtree
+ self.__get_subtree.argtypes = [c_void_p, c_char_p]
+ self.__get_subtree.restype = c_void_p
+
self.__destroy = self.__lib.destroy
self.__destroy.argtypes = [c_void_p]
- config_section, version_section = extract_version(config_string)
- config_section = escape_backslash(config_section)
- config = self.__from_string(config_section.encode())
- if config is None:
- msg = self.__get_error().decode()
- raise ValueError("Failed to parse config: {0}".format(msg))
+ if address is None:
+ config_section, version_section = extract_version(config_string)
+ config_section = escape_backslash(config_section)
+ config = self.__from_string(config_section.encode())
+ if config is None:
+ msg = self.__get_error().decode()
+ raise ValueError("Failed to parse config: {0}".format(msg))
+ else:
+ self.__config = config
+ self.__version = version_section
else:
- self.__config = config
- self.__version = version_section
+ self.__config = address
+ self.__version = ''
def __del__(self):
if self.__config is not None:
@@ -143,13 +154,16 @@ class ConfigTree(object):
def __str__(self):
return self.to_string()
+ def _get_config(self):
+ return self.__config
+
def to_string(self):
config_string = self.__to_string(self.__config).decode()
config_string = "{0}\n{1}".format(config_string, self.__version)
return config_string
- def to_commands(self):
- return self.__to_commands(self.__config).decode()
+ def to_commands(self, op="set"):
+ return self.__to_commands(self.__config, op.encode()).decode()
def to_json(self):
return self.__to_json(self.__config).decode()
@@ -281,3 +295,61 @@ class ConfigTree(object):
else:
raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ def get_subtree(self, path, with_node=False):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res = self.__get_subtree(self.__config, path_str, with_node)
+ subt = ConfigTree(address=res)
+ return subt
+
+class DiffTree:
+ def __init__(self, left, right, path=[], libpath=LIBPATH):
+ if left is None:
+ left = ConfigTree(config_string='\n')
+ if right is None:
+ right = ConfigTree(config_string='\n')
+ if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
+ raise TypeError("Arguments must be instances of ConfigTree")
+ if path:
+ if not left.exists(path):
+ raise ConfigTreeError(f"Path {path} doesn't exist in lhs tree")
+ if not right.exists(path):
+ raise ConfigTreeError(f"Path {path} doesn't exist in rhs tree")
+
+ self.left = left
+ self.right = right
+
+ self.__lib = cdll.LoadLibrary(libpath)
+
+ self.__diff_tree = self.__lib.diff_tree
+ self.__diff_tree.argtypes = [c_char_p, c_void_p, c_void_p]
+ self.__diff_tree.restype = c_void_p
+
+ self.__trim_tree = self.__lib.trim_tree
+ self.__trim_tree.argtypes = [c_void_p, c_void_p]
+ self.__trim_tree.restype = c_void_p
+
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res = self.__diff_tree(path_str, left._get_config(), right._get_config())
+
+ # full diff config_tree and python dict representation
+ self.full = ConfigTree(address=res)
+ self.dict = json.loads(self.full.to_json())
+
+ # config_tree sub-trees
+ self.add = self.full.get_subtree(['add'])
+ self.sub = self.full.get_subtree(['sub'])
+ self.inter = self.full.get_subtree(['inter'])
+
+ # trim sub(-tract) tree to get delete tree for commands
+ ref = self.right.get_subtree(path, with_node=True) if path else self.right
+ res = self.__trim_tree(self.sub._get_config(), ref._get_config())
+ self.delete = ConfigTree(address=res)
+
+ def to_commands(self):
+ add = self.add.to_commands()
+ delete = self.delete.to_commands(op="delete")
+ return delete + "\n" + add
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 6566e8863..a35ea0b74 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -84,6 +84,18 @@ def verify_mtu_ipv6(config):
tmp = dict_search('ipv6.address.eui64', config)
if tmp != None: raise ConfigError(error_msg)
+def verify_bond_bridge_member(config):
+ """
+ Checks if interface has a VRF configured and is also part of a bond or
+ bridge, which is not allowed!
+ """
+ if 'vrf' in config:
+ ifname = config['ifname']
+ if 'is_bond_member' in config:
+ raise ConfigError(f'Can not add interface "{ifname}" to bond, it has a VRF assigned!')
+ if 'is_bridge_member' in config:
+ raise ConfigError(f'Can not add interface "{ifname}" to bridge, it has a VRF assigned!')
+
def verify_tunnel(config):
"""
This helper is used to verify the common part of the tunnel
@@ -184,10 +196,10 @@ def verify_address(config):
of a bridge or bond.
"""
if {'is_bridge_member', 'address'} <= set(config):
- raise ConfigError(
- 'Cannot assign address to interface "{ifname}" as it is a '
- 'member of bridge "{is_bridge_member}"!'.format(**config))
-
+ interface = config['ifname']
+ bridge_name = next(iter(config['is_bridge_member']))
+ raise ConfigError(f'Cannot assign address to interface "{interface}" '
+ f'as it is a member of bridge "{bridge_name}"!')
def verify_bridge_delete(config):
"""
@@ -197,9 +209,9 @@ def verify_bridge_delete(config):
"""
if 'is_bridge_member' in config:
interface = config['ifname']
- for bridge in config['is_bridge_member']:
- raise ConfigError(f'Interface "{interface}" cannot be deleted as it '
- f'is a member of bridge "{bridge}"!')
+ bridge_name = next(iter(config['is_bridge_member']))
+ raise ConfigError(f'Interface "{interface}" cannot be deleted as it '
+ f'is a member of bridge "{bridge_name}"!')
def verify_interface_exists(ifname):
"""
@@ -225,15 +237,22 @@ def verify_source_interface(config):
raise ConfigError('Specified source-interface {source_interface} does '
'not exist'.format(**config))
+ src_ifname = config['source_interface']
if 'source_interface_is_bridge_member' in config:
- raise ConfigError('Invalid source-interface {source_interface}. Interface '
- 'is already a member of bridge '
- '{source_interface_is_bridge_member}'.format(**config))
+ bridge_name = next(iter(config['source_interface_is_bridge_member']))
+ raise ConfigError(f'Invalid source-interface "{src_ifname}". Interface '
+ f'is already a member of bridge "{bridge_name}"!')
if 'source_interface_is_bond_member' in config:
- raise ConfigError('Invalid source-interface {source_interface}. Interface '
- 'is already a member of bond '
- '{source_interface_is_bond_member}'.format(**config))
+ bond_name = next(iter(config['source_interface_is_bond_member']))
+ raise ConfigError(f'Invalid source-interface "{src_ifname}". Interface '
+ f'is already a member of bond "{bond_name}"!')
+
+ if 'is_source_interface' in config:
+ tmp = config['is_source_interface']
+ src_ifname = config['source_interface']
+ raise ConfigError(f'Can not use source-interface "{src_ifname}", it already ' \
+ f'belongs to interface "{tmp}"!')
def verify_dhcpv6(config):
"""
diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py
index 27d0182e9..f831551d8 100644
--- a/python/vyos/ifconfig/bond.py
+++ b/python/vyos/ifconfig/bond.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2022 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
@@ -176,6 +176,21 @@ class BondIf(Interface):
"""
self.set_interface('bond_lacp_rate', slow_fast)
+ def set_miimon_interval(self, interval):
+ """
+ Specifies the MII link monitoring frequency in milliseconds. This
+ determines how often the link state of each slave is inspected for link
+ failures. A value of zero disables MII link monitoring. A value of 100
+ is a good starting point.
+
+ The default value is 0.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_miimon_interval('100')
+ """
+ return self.set_interface('bond_miimon', interval)
+
def set_arp_interval(self, interval):
"""
Specifies the ARP link monitoring frequency in milliseconds.
@@ -199,16 +214,7 @@ class BondIf(Interface):
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').set_arp_interval('100')
"""
- if int(interval) == 0:
- """
- Specifies the MII link monitoring frequency in milliseconds.
- This determines how often the link state of each slave is
- inspected for link failures. A value of zero disables MII
- link monitoring. A value of 100 is a good starting point.
- """
- return self.set_interface('bond_miimon', interval)
- else:
- return self.set_interface('bond_arp_interval', interval)
+ return self.set_interface('bond_arp_interval', interval)
def get_arp_ip_target(self):
"""
@@ -365,26 +371,9 @@ class BondIf(Interface):
if 'shutdown_required' in config:
self.set_admin_state('down')
- # 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 = dict_search('arp_monitor.target', config)
- if isinstance(value, str):
- value = [value]
- if value:
- for addr in value:
- self.set_arp_ip_target('+' + addr)
+ # Specifies the MII link monitoring frequency in milliseconds
+ value = config.get('mii_mon_interval')
+ self.set_miimon_interval(value)
# Bonding transmit hash policy
value = config.get('hash_policy')
@@ -400,10 +389,12 @@ class BondIf(Interface):
# Remove ALL bond member interfaces
for interface in self.get_slaves():
self.del_port(interface)
- # Removing an interface from a bond will always place the underlaying
- # physical interface in admin-down state! If physical interface is
- # not disabled, re-enable it.
- if not dict_search(f'member.interface_remove.{interface}.disable', config):
+
+ # Restore correct interface status based on config
+ if dict_search(f'member.interface.{interface}.disable', config) is not None or \
+ dict_search(f'member.interface_remove.{interface}.disable', config) is not None:
+ Interface(interface).set_admin_state('down')
+ else:
Interface(interface).set_admin_state('up')
# Bonding policy/mode - default value, always present
@@ -414,6 +405,32 @@ class BondIf(Interface):
if mode == '802.3ad':
self.set_lacp_rate(config.get('lacp_rate'))
+ if mode not in ['802.3ad', 'balance-tlb', 'balance-alb']:
+ tmp = dict_search('arp_monitor.interval', config)
+ value = tmp if (tmp != None) else '0'
+ self.set_arp_interval(value)
+
+ # 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 = dict_search('arp_monitor.target', config)
+ if isinstance(value, str):
+ value = [value]
+ if value:
+ for addr in value:
+ self.set_arp_ip_target('+' + addr)
+
# Add (enslave) interfaces to bond
value = dict_search('member.interface', config)
for interface in (value or []):
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
index ffd9c590f..79192b480 100644
--- a/python/vyos/ifconfig/bridge.py
+++ b/python/vyos/ifconfig/bridge.py
@@ -183,6 +183,11 @@ class BridgeIf(Interface):
"""
self.set_interface('vlan_filter', state)
+ # VLAN of bridge parent interface is always 1
+ # VLAN 1 is the default VLAN for all unlabeled packets
+ cmd = f'bridge vlan add dev {self.ifname} vid 1 pvid untagged self'
+ self._cmd(cmd)
+
def set_multicast_querier(self, enable):
"""
Sets whether the bridge actively runs a multicast querier or not. When a
@@ -269,31 +274,23 @@ class BridgeIf(Interface):
self.del_port(member)
# enable/disable Vlan Filter
- vlan_filter = '1' if 'enable_vlan' in config else '0'
- self.set_vlan_filter(vlan_filter)
-
- ifname = config['ifname']
- if int(vlan_filter):
- add_vlan = []
- cur_vlan_ids = get_vlan_ids(ifname)
-
- tmp = dict_search('vif', config)
- if tmp:
- for vif, vif_config in tmp.items():
- add_vlan.append(vif)
-
- # Remove redundant VLANs from the system
- for vlan in list_diff(cur_vlan_ids, add_vlan):
- cmd = f'bridge vlan del dev {ifname} vid {vlan} self'
+ tmp = '1' if 'enable_vlan' in config else '0'
+ self.set_vlan_filter(tmp)
+
+ # add VLAN interfaces to local 'parent' bridge to allow forwarding
+ if 'enable_vlan' in config:
+ for vlan in config.get('vif_remove', {}):
+ # Remove old VLANs from the bridge
+ cmd = f'bridge vlan del dev {self.ifname} vid {vlan} self'
self._cmd(cmd)
- for vlan in add_vlan:
- cmd = f'bridge vlan add dev {ifname} vid {vlan} self'
+ for vlan in config.get('vif', {}):
+ cmd = f'bridge vlan add dev {self.ifname} vid {vlan} self'
self._cmd(cmd)
- # VLAN of bridge parent interface is always 1
- # VLAN 1 is the default VLAN for all unlabeled packets
- cmd = f'bridge vlan add dev {ifname} vid 1 pvid untagged self'
+ # VLAN of bridge parent interface is always 1. VLAN 1 is the default
+ # VLAN for all unlabeled packets
+ cmd = f'bridge vlan add dev {self.ifname} vid 1 pvid untagged self'
self._cmd(cmd)
tmp = dict_search('member.interface', config)
@@ -325,15 +322,13 @@ class BridgeIf(Interface):
# set bridge port path cost
if 'cost' in interface_config:
- value = interface_config.get('cost')
- lower.set_path_cost(value)
+ lower.set_path_cost(interface_config['cost'])
# set bridge port path priority
if 'priority' in interface_config:
- value = interface_config.get('priority')
- lower.set_path_priority(value)
+ lower.set_path_priority(interface_config['priority'])
- if int(vlan_filter):
+ if 'enable_vlan' in config:
add_vlan = []
native_vlan_id = None
allowed_vlan_ids= []
@@ -363,6 +358,7 @@ class BridgeIf(Interface):
for vlan in allowed_vlan_ids:
cmd = f'bridge vlan add dev {interface} vid {vlan} master'
self._cmd(cmd)
+
# Setting native VLAN to system
if native_vlan_id:
cmd = f'bridge vlan add dev {interface} vid {native_vlan_id} pvid untagged master'
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 4ae350634..c9fa6ea8b 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2022 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
@@ -16,6 +16,8 @@
import os
import re
+from glob import glob
+
from vyos.ethtool import Ethtool
from vyos.ifconfig.interface import Interface
from vyos.util import run
@@ -70,13 +72,6 @@ class EthernetIf(Interface):
},
}}
- _sysfs_set = {**Interface._sysfs_set, **{
- 'rps': {
- 'convert': lambda cpus: cpus if cpus else '0',
- 'location': '/sys/class/net/{ifname}/queues/rx-0/rps_cpus',
- },
- }}
-
def __init__(self, ifname, **kargs):
super().__init__(ifname, **kargs)
self.ethtool = Ethtool(ifname)
@@ -230,7 +225,7 @@ class EthernetIf(Interface):
enabled, fixed = self.ethtool.get_large_receive_offload()
if enabled != state:
if not fixed:
- return self.set_interface('gro', 'on' if state else 'off')
+ return self.set_interface('lro', 'on' if state else 'off')
else:
print('Adapter does not support changing large-receive-offload settings!')
return False
@@ -240,6 +235,7 @@ class EthernetIf(Interface):
raise ValueError('Value out of range')
rps_cpus = '0'
+ queues = len(glob(f'/sys/class/net/{self.ifname}/queues/rx-*'))
if state:
# Enable RPS on all available CPUs except CPU0 which we will not
# utilize so the system has one spare core when it's under high
@@ -249,8 +245,11 @@ class EthernetIf(Interface):
# Linux will clip that internally!
rps_cpus = 'ffffffff,ffffffff,ffffffff,fffffffe'
+ for i in range(0, queues):
+ self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_cpus', rps_cpus)
+
# send bitmask representation as hex string without leading '0x'
- return self.set_interface('rps', rps_cpus)
+ return True
def set_sg(self, state):
"""
@@ -267,7 +266,7 @@ class EthernetIf(Interface):
enabled, fixed = self.ethtool.get_scatter_gather()
if enabled != state:
if not fixed:
- return self.set_interface('gro', 'on' if state else 'off')
+ return self.set_interface('sg', 'on' if state else 'off')
else:
print('Adapter does not support changing scatter-gather settings!')
return False
@@ -287,7 +286,7 @@ class EthernetIf(Interface):
enabled, fixed = self.ethtool.get_tcp_segmentation_offload()
if enabled != state:
if not fixed:
- return self.set_interface('gro', 'on' if state else 'off')
+ return self.set_interface('tso', 'on' if state else 'off')
else:
print('Adapter does not support changing tcp-segmentation-offload settings!')
return False
@@ -353,5 +352,5 @@ class EthernetIf(Interface):
for rx_tx, size in config['ring_buffer'].items():
self.set_ring_buffer(rx_tx, size)
- # call base class first
+ # call base class last
super().update(config)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index a2fa96d82..7dbedea45 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -38,7 +38,7 @@ from vyos.util import dict_search
from vyos.util import read_file
from vyos.util import get_interface_config
from vyos.util import is_systemd_service_active
-from vyos.util import sysctl_read
+from vyos.util import is_ipv6_enabled
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.validate import is_intf_addr_assigned
@@ -990,6 +990,10 @@ class Interface(Control):
"Can't configure both static IPv4 and DHCP address "
"on the same interface"))
+ # Failsave - do not add IPv6 address if IPv6 is disabled
+ if is_ipv6(addr) and not is_ipv6_enabled():
+ return False
+
# add to interface
if addr == 'dhcp':
self.set_dhcp(True)
@@ -1296,23 +1300,32 @@ class Interface(Control):
else:
self.del_addr(addr)
- for addr in new_addr:
- self.add_addr(addr)
-
# start DHCPv6 client when only PD was configured
if dhcpv6pd:
self.set_dhcpv6(True)
- # There are some items in the configuration which can only be applied
- # if this instance is not bound to a bridge. This should be checked
- # by the caller but better save then sorry!
- if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config):
- # Bind interface to given VRF or unbind it if vrf node is not set.
- # unbinding will call 'ip link set dev eth0 nomaster' which will
- # also drop the interface out of a bridge or bond - thus this is
- # checked before
+ # XXX: Bind interface to given VRF or unbind it if vrf is not set. Unbinding
+ # will call 'ip link set dev eth0 nomaster' which will also drop the
+ # interface out of any bridge or bond - thus this is checked before.
+ if 'is_bond_member' in config:
+ bond_if = next(iter(config['is_bond_member']))
+ tmp = get_interface_config(config['ifname'])
+ if 'master' in tmp and tmp['master'] != bond_if:
+ self.set_vrf('')
+
+ elif 'is_bridge_member' in config:
+ bridge_if = next(iter(config['is_bridge_member']))
+ tmp = get_interface_config(config['ifname'])
+ if 'master' in tmp and tmp['master'] != bridge_if:
+ self.set_vrf('')
+
+ else:
self.set_vrf(config.get('vrf', ''))
+ # Add this section after vrf T4331
+ for addr in new_addr:
+ self.add_addr(addr)
+
# Configure ARP cache timeout in milliseconds - has default value
tmp = dict_search('ip.arp_cache_timeout', config)
value = tmp if (tmp != None) else '30'
@@ -1358,8 +1371,15 @@ class Interface(Control):
value = tmp if (tmp != None) else '0'
self.set_ipv4_source_validation(value)
+ # MTU - Maximum Transfer Unit has a default value. It must ALWAYS be set
+ # before mangling any IPv6 option. If MTU is less then 1280 IPv6 will be
+ # automatically disabled by the kernel. Also MTU must be increased before
+ # configuring any IPv6 address on the interface.
+ if 'mtu' in config and dict_search('dhcp_options.mtu', config) == None:
+ self.set_mtu(config.get('mtu'))
+
# Only change IPv6 parameters if IPv6 was not explicitly disabled
- if sysctl_read('net.ipv6.conf.all.disable_ipv6') == '0':
+ if is_ipv6_enabled():
# IPv6 forwarding
tmp = dict_search('ipv6.disable_forwarding', config)
value = '0' if (tmp != None) else '1'
@@ -1382,10 +1402,6 @@ class Interface(Control):
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
for addr in (dict_search('ipv6.address.eui64_old', config) or []):
self.del_ipv6_eui64_address(addr)
@@ -1404,8 +1420,8 @@ class Interface(Control):
# re-add ourselves to any bridge we might have fallen out of
if 'is_bridge_member' in config:
- bridge_dict = config.get('is_bridge_member')
- self.add_to_bridge(bridge_dict)
+ tmp = config.get('is_bridge_member')
+ self.add_to_bridge(tmp)
# configure port mirror
self.set_mirror()
diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py
index de554ef44..30c890fdf 100644
--- a/python/vyos/ifconfig/loopback.py
+++ b/python/vyos/ifconfig/loopback.py
@@ -13,9 +13,8 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-import vyos.util
-
from vyos.ifconfig.interface import Interface
+from vyos.util import is_ipv6_enabled
@Interface.register
class LoopbackIf(Interface):
@@ -34,8 +33,6 @@ class LoopbackIf(Interface):
}
}
- name = 'loopback'
-
def remove(self):
"""
Loopback interface can not be deleted from operating system. We can
@@ -62,11 +59,11 @@ class LoopbackIf(Interface):
on any interface. """
addr = config.get('address', [])
- # We must ensure that the loopback addresses are never deleted from the system
- addr += ['127.0.0.1/8']
- if (vyos.util.sysctl_read('net.ipv6.conf.all.disable_ipv6') == '0'):
- addr += ['::1/128']
+ # We must ensure that the loopback addresses are never deleted from the system
+ addr.append('127.0.0.1/8')
+ if is_ipv6_enabled():
+ addr.append('::1/128')
# Update IP address entry in our dictionary
config.update({'address' : addr})
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index de1b56ce5..200beb123 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -171,12 +171,8 @@ class WireGuardIf(Interface):
# remove no longer associated peers first
if 'peer_remove' in config:
- for tmp in config['peer_remove']:
- peer = config['peer_remove'][tmp]
- peer['ifname'] = config['ifname']
-
- cmd = 'wg set {ifname} peer {pubkey} remove'
- self._cmd(cmd.format(**peer))
+ for peer, public_key in config['peer_remove'].items():
+ self._cmd(f'wg set {self.ifname} peer {public_key} remove')
# Wireguard base command is identical for every peer
base_cmd = 'wg set {ifname} private-key {private_key}'
@@ -187,44 +183,49 @@ class WireGuardIf(Interface):
base_cmd = base_cmd.format(**config)
- for tmp in config['peer']:
- peer = config['peer'][tmp]
-
- # start of with a fresh 'wg' command
- cmd = base_cmd + ' peer {pubkey}'
-
- # If no PSK is given remove it by using /dev/null - passing keys via
- # the shell (usually bash) is considered insecure, thus we use a file
- no_psk_file = '/dev/null'
- psk_file = no_psk_file
- if 'preshared_key' in peer:
- psk_file = '/tmp/tmp.wireguard.psk'
- with open(psk_file, 'w') as f:
- f.write(peer['preshared_key'])
- cmd += f' preshared-key {psk_file}'
-
- # Persistent keepalive is optional
- if 'persistent_keepalive'in peer:
- cmd += ' persistent-keepalive {persistent_keepalive}'
-
- # Multiple allowed-ip ranges can be defined - ensure we are always
- # dealing with a list
- if isinstance(peer['allowed_ips'], str):
- peer['allowed_ips'] = [peer['allowed_ips']]
- cmd += ' allowed-ips ' + ','.join(peer['allowed_ips'])
-
- # Endpoint configuration is optional
- if {'address', 'port'} <= set(peer):
- if is_ipv6(peer['address']):
- cmd += ' endpoint [{address}]:{port}'
- else:
- cmd += ' endpoint {address}:{port}'
+ if 'peer' in config:
+ for peer, peer_config in config['peer'].items():
+ # T4702: No need to configure this peer when it was explicitly
+ # marked as disabled - also active sessions are terminated as
+ # the public key was already removed when entering this method!
+ if 'disable' in peer_config:
+ continue
+
+ # start of with a fresh 'wg' command
+ cmd = base_cmd + ' peer {pubkey}'
+
+ # If no PSK is given remove it by using /dev/null - passing keys via
+ # the shell (usually bash) is considered insecure, thus we use a file
+ no_psk_file = '/dev/null'
+ psk_file = no_psk_file
+ if 'preshared_key' in peer_config:
+ psk_file = '/tmp/tmp.wireguard.psk'
+ with open(psk_file, 'w') as f:
+ f.write(peer_config['preshared_key'])
+ cmd += f' preshared-key {psk_file}'
+
+ # Persistent keepalive is optional
+ if 'persistent_keepalive' in peer_config:
+ cmd += ' persistent-keepalive {persistent_keepalive}'
+
+ # Multiple allowed-ip ranges can be defined - ensure we are always
+ # dealing with a list
+ if isinstance(peer_config['allowed_ips'], str):
+ peer_config['allowed_ips'] = [peer_config['allowed_ips']]
+ cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips'])
+
+ # Endpoint configuration is optional
+ if {'address', 'port'} <= set(peer_config):
+ if is_ipv6(peer_config['address']):
+ cmd += ' endpoint [{address}]:{port}'
+ else:
+ cmd += ' endpoint {address}:{port}'
- self._cmd(cmd.format(**peer))
+ self._cmd(cmd.format(**peer_config))
- # PSK key file is not required to be stored persistently as its backed by CLI
- if psk_file != no_psk_file and os.path.exists(psk_file):
- os.remove(psk_file)
+ # PSK key file is not required to be stored persistently as its backed by CLI
+ if psk_file != no_psk_file and os.path.exists(psk_file):
+ os.remove(psk_file)
# call base class
super().update(config)
diff --git a/python/vyos/template.py b/python/vyos/template.py
index f9e754357..88271125c 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -151,6 +151,16 @@ def bracketize_ipv6(address):
return f'[{address}]'
return address
+@register_filter('dot_colon_to_dash')
+def dot_colon_to_dash(text):
+ """ Replace dot and colon to dash for string
+ Example:
+ 192.0.2.1 => 192-0-2-1, 2001:db8::1 => 2001-db8--1
+ """
+ text = text.replace(":", "-")
+ text = text.replace(".", "-")
+ return text
+
@register_filter('netmask_from_cidr')
def netmask_from_cidr(prefix):
""" Take CIDR prefix and convert the prefix length to a "subnet mask".
diff --git a/python/vyos/util.py b/python/vyos/util.py
index b5d81fba5..67ec3ecc6 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2020-2022 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
@@ -623,6 +623,11 @@ def is_admin() -> bool:
(_, _, _, admin_group_members) = getgrnam('sudo')
return current_user in admin_group_members
+def is_list_equal(first: list, second: list) -> bool:
+ """ Check if 2 lists are equal and list not empty """
+ if len(first) != len(second) or len(first) == 0:
+ return False
+ return sorted(first) == sorted(second)
def mac2eui64(mac, prefix=None):
"""
@@ -694,6 +699,32 @@ def dict_search(path, dict_object):
c = c.get(p, {})
return c.get(parts[-1], None)
+def convert_data(data):
+ """Convert multiple types of data to types usable in CLI
+
+ Args:
+ data (str | bytes | list | OrderedDict): input data
+
+ Returns:
+ str | list | dict: converted data
+ """
+ from collections import OrderedDict
+
+ if isinstance(data, str):
+ return data
+ if isinstance(data, bytes):
+ return data.decode()
+ if isinstance(data, list):
+ list_tmp = []
+ for item in data:
+ list_tmp.append(convert_data(item))
+ return list_tmp
+ if isinstance(data, OrderedDict):
+ dict_tmp = {}
+ for key, value in data.items():
+ dict_tmp[key] = convert_data(value)
+ return dict_tmp
+
def get_bridge_fdb(interface):
""" Returns the forwarding database entries for a given interface """
if not os.path.exists(f'/sys/class/net/{interface}'):
@@ -797,6 +828,11 @@ def is_wwan_connected(interface):
if not interface.startswith('wwan'):
raise ValueError(f'Specified interface "{interface}" is not a WWAN interface')
+ # ModemManager is required for connection(s) - if service is not running,
+ # there won't be any connection at all!
+ if not is_systemd_service_active('ModemManager.service'):
+ return False
+
modem = interface.lstrip('wwan')
tmp = cmd(f'mmcli --modem {modem} --output-json')
@@ -827,3 +863,7 @@ def sysctl_write(name, value):
call(f'sysctl -wq {name}={value}')
return True
return False
+
+def is_ipv6_enabled() -> bool:
+ """ Check if IPv6 support on the system is enabled or not """
+ return (sysctl_read('net.ipv6.conf.all.disable_ipv6') == '0')
diff --git a/python/vyos/validate.py b/python/vyos/validate.py
index 23e88b5ac..9aa23d3dc 100644
--- a/python/vyos/validate.py
+++ b/python/vyos/validate.py
@@ -260,3 +260,22 @@ def has_address_configured(conf, intf):
conf.set_level(old_level)
return ret
+
+def has_vrf_configured(conf, intf):
+ """
+ Checks if interface has a VRF configured.
+
+ Returns True if interface has VRF configured, False if it doesn't.
+ """
+ from vyos.ifconfig import Section
+ ret = False
+
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ tmp = ['interfaces', Section.get_config_path(intf), 'vrf']
+ if conf.exists(tmp):
+ ret = True
+
+ conf.set_level(old_level)
+ return ret
diff --git a/smoketest/configs/qos-basic b/smoketest/configs/qos-basic
new file mode 100644
index 000000000..f94a5650d
--- /dev/null
+++ b/smoketest/configs/qos-basic
@@ -0,0 +1,205 @@
+interfaces {
+ ethernet eth0 {
+ address 10.1.1.100/24
+ traffic-policy {
+ out FS
+ }
+ }
+ ethernet eth1 {
+ address 10.2.1.1/24
+ traffic-policy {
+ out M2
+ }
+ }
+ ethernet eth2 {
+ address 10.9.9.1/24
+ traffic-policy {
+ out MY-HTB
+ }
+ }
+ loopback lo {
+ }
+}
+protocols {
+ static {
+ route 0.0.0.0/0 {
+ next-hop 10.9.9.2 {
+ }
+ next-hop 10.1.1.1 {
+ }
+ }
+ }
+}
+system {
+ config-management {
+ commit-revisions 10
+ }
+ conntrack {
+ modules {
+ ftp
+ h323
+ nfs
+ pptp
+ sip
+ sqlnet
+ tftp
+ }
+ }
+ console {
+ device ttyS0 {
+ speed 115200
+ }
+ }
+ host-name vyos
+ login {
+ user vyos {
+ authentication {
+ encrypted-password $6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0
+ plaintext-password ""
+ }
+ }
+ }
+ ntp {
+ server time1.vyos.net {
+ }
+ server time2.vyos.net {
+ }
+ server time3.vyos.net {
+ }
+ }
+ syslog {
+ global {
+ facility all {
+ level info
+ }
+ facility protocols {
+ level debug
+ }
+ }
+ }
+}
+traffic-policy {
+ shaper M2 {
+ bandwidth auto
+ class 10 {
+ bandwidth 100%
+ burst 15k
+ match ADDRESS10 {
+ ip {
+ dscp CS4
+ }
+ }
+ queue-type fair-queue
+ set-dscp CS5
+ }
+ default {
+ bandwidth 10mbit
+ burst 15k
+ queue-type fair-queue
+ }
+ }
+ shaper MY-HTB {
+ bandwidth 10mbit
+ class 30 {
+ bandwidth 10%
+ burst 15k
+ ceiling 50%
+ match ADDRESS30 {
+ ip {
+ source {
+ address 10.1.1.0/24
+ }
+ }
+ }
+ priority 5
+ queue-type fair-queue
+ }
+ class 40 {
+ bandwidth 90%
+ burst 15k
+ ceiling 100%
+ match ADDRESS40 {
+ ip {
+ dscp CS4
+ source {
+ address 10.2.1.0/24
+ }
+ }
+ }
+ priority 5
+ queue-type fair-queue
+ }
+ class 50 {
+ bandwidth 100%
+ burst 15k
+ match ADDRESS50 {
+ ip {
+ dscp CS5
+ }
+ }
+ queue-type fair-queue
+ set-dscp CS7
+ }
+ default {
+ bandwidth 10%
+ burst 15k
+ ceiling 100%
+ priority 7
+ queue-type fair-queue
+ set-dscp CS1
+ }
+ }
+ shaper FS {
+ bandwidth auto
+ class 10 {
+ bandwidth 100%
+ burst 15k
+ match ADDRESS10 {
+ ip {
+ source {
+ address 172.17.1.2/32
+ }
+ }
+ }
+ queue-type fair-queue
+ set-dscp CS4
+ }
+ class 20 {
+ bandwidth 100%
+ burst 15k
+ match ADDRESS20 {
+ ip {
+ source {
+ address 172.17.1.3/32
+ }
+ }
+ }
+ queue-type fair-queue
+ set-dscp CS5
+ }
+ class 30 {
+ bandwidth 100%
+ burst 15k
+ match ADDRESS30 {
+ ip {
+ source {
+ address 172.17.1.4/32
+ }
+ }
+ }
+ queue-type fair-queue
+ set-dscp CS6
+ }
+ default {
+ bandwidth 10%
+ burst 15k
+ ceiling 100%
+ priority 7
+ queue-type fair-queue
+ }
+ }
+}
+// Warning: Do not remove the following line.
+// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1"
+// Release version: 1.3.1
+
diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py
index 4f2fe979a..ab75cdcc9 100755
--- a/smoketest/scripts/cli/test_interfaces_bonding.py
+++ b/smoketest/scripts/cli/test_interfaces_bonding.py
@@ -49,7 +49,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
if not '.' in tmp:
cls._members.append(tmp)
- cls._options['bond0'] = []
+ cls._options = {'bond0' : []}
for member in cls._members:
cls._options['bond0'].append(f'member interface {member}')
cls._interfaces = list(cls._options)
@@ -136,7 +136,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
def test_bonding_hash_policy(self):
# Define available bonding hash policies
- hash_policies = ['layer2', 'layer2+3', 'layer2+3', 'encap2+3', 'encap3+4']
+ hash_policies = ['layer2', 'layer2+3', 'encap2+3', 'encap3+4']
for hash_policy in hash_policies:
for interface in self._interfaces:
for option in self._options.get(interface, []):
@@ -151,6 +151,29 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
defined_policy = read_file(f'/sys/class/net/{interface}/bonding/xmit_hash_policy').split()
self.assertEqual(defined_policy[0], hash_policy)
+ def test_bonding_mii_monitoring_interval(self):
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_commit()
+
+ # verify default
+ for interface in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{interface}/bonding/miimon').split()
+ self.assertIn('100', tmp)
+
+ mii_mon = '250'
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'mii-mon-interval', mii_mon])
+
+ self.cli_commit()
+
+ # verify new CLI value
+ for interface in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{interface}/bonding/miimon').split()
+ self.assertIn(mii_mon, tmp)
+
def test_bonding_multi_use_member(self):
# Define available bonding hash policies
for interface in ['bond10', 'bond20']:
@@ -165,5 +188,67 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_commit()
+ def test_bonding_source_interface(self):
+ # Re-use member interface that is already a source-interface
+ bond = 'bond99'
+ pppoe = 'pppoe98756'
+ member = next(iter(self._members))
+
+ self.cli_set(self._base_path + [bond, 'member', 'interface', member])
+ self.cli_set(['interfaces', 'pppoe', pppoe, 'source-interface', member])
+
+ # check validate() - can not add interface to bond, it is the source-interface of ...
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(['interfaces', 'pppoe', pppoe])
+ self.cli_commit()
+
+ # verify config
+ slaves = read_file(f'/sys/class/net/{bond}/bonding/slaves').split()
+ self.assertIn(member, slaves)
+
+ def test_bonding_source_bridge_interface(self):
+ # Re-use member interface that is already a source-interface
+ bond = 'bond1097'
+ bridge = 'br6327'
+ member = next(iter(self._members))
+
+ self.cli_set(self._base_path + [bond, 'member', 'interface', member])
+ self.cli_set(['interfaces', 'bridge', bridge, 'member', 'interface', member])
+
+ # check validate() - can not add interface to bond, it is a member of bridge ...
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(['interfaces', 'bridge', bridge])
+ self.cli_commit()
+
+ # verify config
+ slaves = read_file(f'/sys/class/net/{bond}/bonding/slaves').split()
+ self.assertIn(member, slaves)
+
+ def test_bonding_uniq_member_description(self):
+ ethernet_path = ['interfaces', 'ethernet']
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_commit()
+
+ # Add any changes on bonding members
+ # For example add description on separate ethX interfaces
+ for interface in self._interfaces:
+ for member in self._members:
+ self.cli_set(ethernet_path + [member, 'description', member + '_interface'])
+
+ self.cli_commit()
+
+ # verify config
+ for interface in self._interfaces:
+ slaves = read_file(f'/sys/class/net/{interface}/bonding/slaves').split()
+ for member in self._members:
+ self.assertIn(member, slaves)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py
index 09441326a..884fa5c6b 100755
--- a/smoketest/scripts/cli/test_interfaces_bridge.py
+++ b/smoketest/scripts/cli/test_interfaces_bridge.py
@@ -19,6 +19,7 @@ import json
import unittest
from base_interfaces_test import BasicInterfaceTest
+from copy import deepcopy
from glob import glob
from netifaces import interfaces
@@ -144,87 +145,123 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase):
super().test_vif_8021q_mtu_limits()
def test_bridge_vlan_filter(self):
- vif_vlan = 2
+ vifs = ['10', '20', '30', '40']
+ native_vlan = '20'
+
# Add member interface to bridge and set VLAN filter
for interface in self._interfaces:
base = self._base_path + [interface]
self.cli_set(base + ['enable-vlan'])
self.cli_set(base + ['address', '192.0.2.1/24'])
- self.cli_set(base + ['vif', str(vif_vlan), 'address', '192.0.3.1/24'])
- self.cli_set(base + ['vif', str(vif_vlan), 'mtu', self._mtu])
- vlan_id = 101
- allowed_vlan = 2
- allowed_vlan_range = '4-9'
- # assign members to bridge interface
+ for vif in vifs:
+ self.cli_set(base + ['vif', vif, 'address', f'192.0.{vif}.1/24'])
+ self.cli_set(base + ['vif', vif, 'mtu', self._mtu])
+
for member in self._members:
base_member = base + ['member', 'interface', member]
- self.cli_set(base_member + ['allowed-vlan', str(allowed_vlan)])
- self.cli_set(base_member + ['allowed-vlan', allowed_vlan_range])
- self.cli_set(base_member + ['native-vlan', str(vlan_id)])
- vlan_id += 1
+ self.cli_set(base_member + ['native-vlan', native_vlan])
+ for vif in vifs:
+ self.cli_set(base_member + ['allowed-vlan', vif])
# commit config
self.cli_commit()
- # Detect the vlan filter function
+ def _verify_members(interface, members) -> None:
+ # check member interfaces are added on the bridge
+ bridge_members = []
+ for tmp in glob(f'/sys/class/net/{interface}/lower_*'):
+ bridge_members.append(os.path.basename(tmp).replace('lower_', ''))
+
+ self.assertListEqual(sorted(members), sorted(bridge_members))
+
+ def _check_vlan_filter(interface, vifs) -> None:
+ configured_vlan_ids = []
+
+ bridge_json = cmd(f'bridge -j vlan show dev {interface}')
+ bridge_json = json.loads(bridge_json)
+ self.assertIsNotNone(bridge_json)
+
+ for tmp in bridge_json:
+ self.assertIn('vlans', tmp)
+
+ for vlan in tmp['vlans']:
+ self.assertIn('vlan', vlan)
+ configured_vlan_ids.append(str(vlan['vlan']))
+
+ # Verify native VLAN ID has 'PVID' flag set on individual member ports
+ if not interface.startswith('br') and str(vlan['vlan']) == native_vlan:
+ self.assertIn('flags', vlan)
+ self.assertIn('PVID', vlan['flags'])
+
+ self.assertListEqual(sorted(configured_vlan_ids), sorted(vifs))
+
+ # Verify correct setting of VLAN filter function
for interface in self._interfaces:
tmp = read_file(f'/sys/class/net/{interface}/bridge/vlan_filtering')
self.assertEqual(tmp, '1')
- # Execute the program to obtain status information
- json_data = cmd('bridge -j vlan show', shell=True)
- vlan_filter_status = None
- vlan_filter_status = json.loads(json_data)
-
- if vlan_filter_status is not None:
- for interface_status in vlan_filter_status:
- ifname = interface_status['ifname']
- for interface in self._members:
- vlan_success = 0;
- if interface == ifname:
- vlans_status = interface_status['vlans']
- for vlan_status in vlans_status:
- vlan_id = vlan_status['vlan']
- flag_num = 0
- if 'flags' in vlan_status:
- flags = vlan_status['flags']
- for flag in flags:
- flag_num = flag_num +1
- if vlan_id == 2:
- if flag_num == 0:
- vlan_success = vlan_success + 1
- else:
- for id in range(4,10):
- if vlan_id == id:
- if flag_num == 0:
- vlan_success = vlan_success + 1
- if vlan_id >= 101:
- if flag_num == 2:
- vlan_success = vlan_success + 1
- if vlan_success >= 7:
- self.assertTrue(True)
- else:
- self.assertTrue(False)
+ # Obtain status information and verify proper VLAN filter setup.
+ # First check if all members are present, second check if all VLANs
+ # are assigned on the parend bridge interface, third verify all the
+ # VLANs are properly setup on the downstream "member" ports
+ for interface in self._interfaces:
+ # check member interfaces are added on the bridge
+ _verify_members(interface, self._members)
- else:
- self.assertTrue(False)
+ # Check if all VLAN ids are properly set up. Bridge interface always
+ # has native VLAN 1
+ tmp = deepcopy(vifs)
+ tmp.append('1')
+ _check_vlan_filter(interface, tmp)
- # check member interfaces are added on the bridge
+ for member in self._members:
+ _check_vlan_filter(member, vifs)
+
+ # change member interface description to trigger config update,
+ # VLANs must still exist (T4565)
for interface in self._interfaces:
- bridge_members = []
- for tmp in glob(f'/sys/class/net/{interface}/lower_*'):
- bridge_members.append(os.path.basename(tmp).replace('lower_', ''))
+ for member in self._members:
+ self.cli_set(['interfaces', Section.section(member), member, 'description', f'foo {member}'])
+
+ # commit config
+ self.cli_commit()
+
+ # Obtain status information and verify proper VLAN filter setup.
+ # First check if all members are present, second check if all VLANs
+ # are assigned on the parend bridge interface, third verify all the
+ # VLANs are properly setup on the downstream "member" ports
+ for interface in self._interfaces:
+ # check member interfaces are added on the bridge
+ _verify_members(interface, self._members)
+
+ # Check if all VLAN ids are properly set up. Bridge interface always
+ # has native VLAN 1
+ tmp = deepcopy(vifs)
+ tmp.append('1')
+ _check_vlan_filter(interface, tmp)
for member in self._members:
- self.assertIn(member, bridge_members)
+ _check_vlan_filter(member, vifs)
# delete all members
for interface in self._interfaces:
self.cli_delete(self._base_path + [interface, 'member'])
+ # commit config
+ self.cli_commit()
- def test_bridge_vlan_members(self):
+ # verify member interfaces are no longer assigned on the bridge
+ for interface in self._interfaces:
+ bridge_members = []
+ for tmp in glob(f'/sys/class/net/{interface}/lower_*'):
+ bridge_members.append(os.path.basename(tmp).replace('lower_', ''))
+
+ self.assertNotEqual(len(self._members), len(bridge_members))
+ for member in self._members:
+ self.assertNotIn(member, bridge_members)
+
+ def test_bridge_vif_members(self):
# T2945: ensure that VIFs are not dropped from bridge
vifs = ['300', '400']
for interface in self._interfaces:
@@ -249,5 +286,34 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_delete(['interfaces', 'ethernet', member, 'vif', vif])
self.cli_delete(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif}'])
+ def test_bridge_vif_s_vif_c_members(self):
+ # T2945: ensure that VIFs are not dropped from bridge
+ vifs = ['300', '400']
+ vifc = ['301', '401']
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif_s in vifs:
+ for vif_c in vifc:
+ self.cli_set(['interfaces', 'ethernet', member, 'vif-s', vif_s, 'vif-c', vif_c])
+ self.cli_set(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif_s}.{vif_c}'])
+
+ self.cli_commit()
+
+ # Verify config
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif_s in vifs:
+ for vif_c in vifc:
+ # member interface must be assigned to the bridge
+ self.assertTrue(os.path.exists(f'/sys/class/net/{interface}/lower_{member}.{vif_s}.{vif_c}'))
+
+ # delete all members
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif_s in vifs:
+ self.cli_delete(['interfaces', 'ethernet', member, 'vif-s', vif_s])
+ for vif_c in vifc:
+ self.cli_delete(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif_s}.{vif_c}'])
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py
index 5b10bfa44..64bfa0dc9 100755
--- a/smoketest/scripts/cli/test_interfaces_macsec.py
+++ b/smoketest/scripts/cli/test_interfaces_macsec.py
@@ -28,6 +28,8 @@ from vyos.util import read_file
from vyos.util import get_interface_config
from vyos.util import process_named_running
+PROCESS_NAME = 'wpa_supplicant'
+
def get_config_value(interface, key):
tmp = read_file(f'/run/wpa_supplicant/{interface}.conf')
tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp)
@@ -55,6 +57,10 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase):
# call base-classes classmethod
super(cls, cls).setUpClass()
+ def tearDown(self):
+ super().tearDown()
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
def test_macsec_encryption(self):
# MACsec can be operating in authentication and encryption mode - both
# using different mandatory settings, lets test encryption as the basic
@@ -96,28 +102,29 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_commit()
tmp = get_config_value(src_interface, 'macsec_integ_only')
- self.assertTrue("0" in tmp)
+ self.assertIn("0", tmp)
tmp = get_config_value(src_interface, 'mka_cak')
- self.assertTrue(mak_cak in tmp)
+ self.assertIn(mak_cak, tmp)
tmp = get_config_value(src_interface, 'mka_ckn')
- self.assertTrue(mak_ckn in tmp)
+ self.assertIn(mak_ckn, tmp)
# check that the default priority of 255 is programmed
tmp = get_config_value(src_interface, 'mka_priority')
- self.assertTrue("255" in tmp)
+ self.assertIn("255", tmp)
tmp = get_config_value(src_interface, 'macsec_replay_window')
- self.assertTrue(replay_window in tmp)
+ self.assertIn(replay_window, tmp)
tmp = read_file(f'/sys/class/net/{interface}/mtu')
self.assertEqual(tmp, '1460')
- # Check for running process
- self.assertTrue(process_named_running('wpa_supplicant'))
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
def test_macsec_gcm_aes_128(self):
+ src_interface = 'eth0'
interface = 'macsec1'
cipher = 'gcm-aes-128'
self.cli_set(self._base_path + [interface])
@@ -125,7 +132,7 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase):
# check validate() - source interface is mandatory
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(self._base_path + [interface, 'source-interface', 'eth0'])
+ self.cli_set(self._base_path + [interface, 'source-interface', src_interface])
# check validate() - cipher is mandatory
with self.assertRaises(ConfigSessionError):
@@ -138,7 +145,15 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase):
self.assertIn(interface, interfaces())
self.assertEqual(cipher, get_cipher(interface))
+ # check that we use the new macsec_csindex option (T4537)
+ tmp = get_config_value(src_interface, 'macsec_csindex')
+ self.assertIn("0", tmp)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
def test_macsec_gcm_aes_256(self):
+ src_interface = 'eth0'
interface = 'macsec4'
cipher = 'gcm-aes-256'
self.cli_set(self._base_path + [interface])
@@ -146,7 +161,7 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase):
# check validate() - source interface is mandatory
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(self._base_path + [interface, 'source-interface', 'eth0'])
+ self.cli_set(self._base_path + [interface, 'source-interface', src_interface])
# check validate() - cipher is mandatory
with self.assertRaises(ConfigSessionError):
@@ -158,6 +173,13 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase):
self.assertIn(interface, interfaces())
self.assertEqual(cipher, get_cipher(interface))
+ # check that we use the new macsec_csindex option (T4537)
+ tmp = get_config_value(src_interface, 'macsec_csindex')
+ self.assertIn("1", tmp)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
def test_macsec_source_interface(self):
# Ensure source-interface can bot be part of any other bond or bridge
@@ -186,6 +208,9 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_commit()
self.assertIn(interface, interfaces())
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ unittest.main(verbosity=2, failfast=True)
diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py
index 24df0af4d..55218ecac 100755
--- a/smoketest/scripts/cli/test_interfaces_openvpn.py
+++ b/smoketest/scripts/cli/test_interfaces_openvpn.py
@@ -607,6 +607,44 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
interface = f'vtun{ii}'
self.assertNotIn(interface, interfaces())
+ def test_openvpn_options(self):
+ # Ensure OpenVPN process restart on openvpn-option CLI node change
+
+ interface = 'vtun5001'
+ path = base_path + [interface]
+
+ self.cli_set(path + ['mode', 'site-to-site'])
+ self.cli_set(path + ['local-address', '10.0.0.2'])
+ self.cli_set(path + ['remote-address', '192.168.0.3'])
+ self.cli_set(path + ['shared-secret-key-file', s2s_key])
+
+ self.cli_commit()
+
+ # Now verify the OpenVPN "raw" option passing. Once an openvpn-option is
+ # added, modified or deleted from the CLI, OpenVPN daemon must be restarted
+ cur_pid = process_named_running('openvpn')
+ self.cli_set(path + ['openvpn-option', '--persist-tun'])
+ self.cli_commit()
+
+ # PID must be different as OpenVPN Must be restarted
+ new_pid = process_named_running('openvpn')
+ self.assertNotEqual(cur_pid, new_pid)
+ cur_pid = new_pid
+
+ self.cli_set(path + ['openvpn-option', '--persist-key'])
+ self.cli_commit()
+
+ # PID must be different as OpenVPN Must be restarted
+ new_pid = process_named_running('openvpn')
+ self.assertNotEqual(cur_pid, new_pid)
+ cur_pid = new_pid
+
+ self.cli_delete(path + ['openvpn-option'])
+ self.cli_commit()
+
+ # PID must be different as OpenVPN Must be restarted
+ new_pid = process_named_running('openvpn')
+ self.assertNotEqual(cur_pid, new_pid)
if __name__ == '__main__':
# Our SSL certificates need a subject ...
diff --git a/smoketest/scripts/cli/test_load_balancning_wan.py b/smoketest/scripts/cli/test_load_balancning_wan.py
new file mode 100755
index 000000000..edc6deb04
--- /dev/null
+++ b/smoketest/scripts/cli/test_load_balancning_wan.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+import time
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.util import call
+from vyos.util import cmd
+
+
+base_path = ['load-balancing']
+
+
+def create_netns(name):
+ return call(f'sudo ip netns add {name}')
+
+def create_veth_pair(local='veth0', peer='ceth0'):
+ return call(f'sudo ip link add {local} type veth peer name {peer}')
+
+def move_interface_to_netns(iface, netns_name):
+ return call(f'sudo ip link set {iface} netns {netns_name}')
+
+def rename_interface(iface, new_name):
+ return call(f'sudo ip link set {iface} name {new_name}')
+
+def cmd_in_netns(netns, cmd):
+ return call(f'sudo ip netns exec {netns} {cmd}')
+
+def delete_netns(name):
+ return call(f'sudo ip netns del {name}')
+
+
+class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestLoadBalancingWan, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_table_routes(self):
+
+ ns1 = 'ns201'
+ ns2 = 'ns202'
+ iface1 = 'eth201'
+ iface2 = 'eth202'
+ container_iface1 = 'ceth0'
+ container_iface2 = 'ceth1'
+
+ # Create network namespeces
+ create_netns(ns1)
+ create_netns(ns2)
+ create_veth_pair(iface1, container_iface1)
+ create_veth_pair(iface2, container_iface2)
+ move_interface_to_netns(container_iface1, ns1)
+ move_interface_to_netns(container_iface2, ns2)
+ call(f'sudo ip a add 203.0.113.10/24 dev {iface1}')
+ call(f'sudo ip a add 192.0.2.10/24 dev {iface2}')
+ call(f'sudo ip link set dev {iface1} up')
+ call(f'sudo ip link set dev {iface2} up')
+ cmd_in_netns(ns1, f'ip link set {container_iface1} name eth0')
+ cmd_in_netns(ns2, f'ip link set {container_iface2} name eth0')
+ cmd_in_netns(ns1, 'ip a add 203.0.113.1/24 dev eth0')
+ cmd_in_netns(ns2, 'ip a add 192.0.2.1/24 dev eth0')
+ cmd_in_netns(ns1, 'ip link set dev eth0 up')
+ cmd_in_netns(ns2, 'ip link set dev eth0 up')
+
+ # Set load-balancing configuration
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'failure-count', '2'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'nexthop', '192.0.2.1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'success-count', '1'])
+
+ # commit changes
+ self.cli_commit()
+
+ time.sleep(5)
+ # Check default routes in tables 201, 202
+ # Expected values
+ original = 'default via 203.0.113.1 dev eth201'
+ tmp = cmd('sudo ip route show table 201')
+ self.assertEqual(tmp, original)
+
+ original = 'default via 192.0.2.1 dev eth202'
+ tmp = cmd('sudo ip route show table 202')
+ self.assertEqual(tmp, original)
+
+ # Delete veth interfaces and netns
+ for iface in [iface1, iface2]:
+ call(f'sudo ip link del dev {iface}')
+
+ delete_netns(ns1)
+ delete_netns(ns2)
+
+ def test_check_chains(self):
+
+ ns1 = 'nsA'
+ ns2 = 'nsB'
+ iface1 = 'veth1'
+ iface2 = 'veth2'
+ container_iface1 = 'ceth0'
+ container_iface2 = 'ceth1'
+ mangle_isp1 = """table ip mangle {
+ chain ISP_veth1 {
+ counter ct mark set 0xc9
+ counter meta mark set 0xc9
+ counter accept
+ }
+}"""
+ mangle_isp2 = """table ip mangle {
+ chain ISP_veth2 {
+ counter ct mark set 0xca
+ counter meta mark set 0xca
+ counter accept
+ }
+}"""
+
+ # Create network namespeces
+ create_netns(ns1)
+ create_netns(ns2)
+ create_veth_pair(iface1, container_iface1)
+ create_veth_pair(iface2, container_iface2)
+ move_interface_to_netns(container_iface1, ns1)
+ move_interface_to_netns(container_iface2, ns2)
+ call(f'sudo ip a add 203.0.113.10/24 dev {iface1}')
+ call(f'sudo ip a add 192.0.2.10/24 dev {iface2}')
+ call(f'sudo ip link set dev {iface1} up')
+ call(f'sudo ip link set dev {iface2} up')
+ cmd_in_netns(ns1, f'ip link set {container_iface1} name eth0')
+ cmd_in_netns(ns2, f'ip link set {container_iface2} name eth0')
+ cmd_in_netns(ns1, 'ip a add 203.0.113.1/24 dev eth0')
+ cmd_in_netns(ns2, 'ip a add 192.0.2.1/24 dev eth0')
+ cmd_in_netns(ns1, 'ip link set dev eth0 up')
+ cmd_in_netns(ns2, 'ip link set dev eth0 up')
+
+ # Set load-balancing configuration
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'failure-count', '2'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'nexthop', '192.0.2.1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'success-count', '1'])
+
+ # commit changes
+ self.cli_commit()
+
+ time.sleep(5)
+ # Check chains
+ #call('sudo nft list ruleset')
+ tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface1}')
+ self.assertEqual(tmp, mangle_isp1)
+
+ tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface2}')
+ self.assertEqual(tmp, mangle_isp2)
+
+ # Delete veth interfaces and netns
+ for iface in [iface1, iface2]:
+ call(f'sudo ip link del dev {iface}')
+
+ delete_netns(ns1)
+ delete_netns(ns2)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py
index 8abdd6d37..167cd05f8 100755
--- a/smoketest/scripts/cli/test_protocols_isis.py
+++ b/smoketest/scripts/cli/test_protocols_isis.py
@@ -71,13 +71,13 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}')
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
self.assertIn(f' net {net}', tmp)
self.assertIn(f' log-adjacency-changes', tmp)
self.assertIn(f' redistribute ipv4 connected level-2 route-map {route_map}', tmp)
for interface in self._interfaces:
- tmp = self.getFRRconfig(f'interface {interface}')
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
self.assertIn(f' ip router isis {domain}', tmp)
self.assertIn(f' ipv6 router isis {domain}', tmp)
@@ -93,22 +93,26 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.isis_base_config()
self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map])
self.cli_set(base_path + ['route-map', route_map])
+ self.cli_set(base_path + ['level', 'level-2'])
# commit changes
self.cli_commit()
# Verify FRR configuration
zebra_route_map = f'ip protocol isis route-map {route_map}'
- frrconfig = self.getFRRconfig(zebra_route_map)
+ frrconfig = self.getFRRconfig(zebra_route_map, daemon='zebra')
self.assertIn(zebra_route_map, frrconfig)
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(' is-type level-2-only', tmp)
+
# Remove the route-map again
self.cli_delete(base_path + ['route-map'])
# commit changes
self.cli_commit()
# Verify FRR configuration
- frrconfig = self.getFRRconfig(zebra_route_map)
+ frrconfig = self.getFRRconfig(zebra_route_map, daemon='zebra')
self.assertNotIn(zebra_route_map, frrconfig)
self.cli_delete(['policy', 'route-map', route_map])
@@ -128,7 +132,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}')
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
self.assertIn(f' net {net}', tmp)
for afi in ['ipv4', 'ipv6']:
@@ -140,6 +144,8 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
password = 'foo'
self.isis_base_config()
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface, 'password', 'plaintext-password', f'{password}-{interface}'])
self.cli_set(base_path + ['area-password', 'plaintext-password', password])
self.cli_set(base_path + ['area-password', 'md5', password])
@@ -160,10 +166,61 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}')
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
self.assertIn(f' net {net}', tmp)
self.assertIn(f' domain-password clear {password}', tmp)
self.assertIn(f' area-password clear {password}', tmp)
+ for interface in self._interfaces:
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ self.assertIn(f' isis password clear {password}-{interface}', tmp)
+
+ def test_isis_06_spf_delay(self):
+ network = 'point-to-point'
+ holddown = '10'
+ init_delay = '50'
+ long_delay = '200'
+ short_delay = '100'
+ time_to_learn = '75'
+
+ self.cli_set(base_path + ['net', net])
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface, 'network', network])
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'holddown', holddown])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'init-delay', init_delay])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'long-delay', long_delay])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'short-delay', short_delay])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['spf-delay-ietf', 'time-to-learn', time_to_learn])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify all changes
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' spf-delay-ietf init-delay {init_delay} short-delay {short_delay} long-delay {long_delay} holddown {holddown} time-to-learn {time_to_learn}', tmp)
+
+ for interface in self._interfaces:
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ self.assertIn(f' ip router isis {domain}', tmp)
+ self.assertIn(f' ipv6 router isis {domain}', tmp)
+ self.assertIn(f' isis network {network}', tmp)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py
index 44e27828d..bffb1947f 100755
--- a/smoketest/scripts/cli/test_service_dns_forwarding.py
+++ b/smoketest/scripts/cli/test_service_dns_forwarding.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 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
@@ -26,7 +26,7 @@ from vyos.util import process_named_running
CONFIG_FILE = '/run/powerdns/recursor.conf'
FORWARD_FILE = '/run/powerdns/recursor.forward-zones.conf'
HOSTSD_FILE = '/run/powerdns/recursor.vyos-hostsd.conf.lua'
-PROCESS_NAME= 'pdns-r/worker'
+PROCESS_NAME= 'pdns_recursor'
base_path = ['service', 'dns', 'forwarding']
@@ -39,7 +39,18 @@ def get_config_value(key, file=CONFIG_FILE):
return tmp[0]
class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServicePowerDNS, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
# Delete DNS forwarding configuration
self.cli_delete(base_path)
self.cli_commit()
@@ -93,9 +104,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase):
tmp = get_config_value('export-etc-hosts')
self.assertEqual(tmp, 'no')
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
def test_dnssec(self):
# DNSSEC option testing
@@ -114,9 +122,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase):
tmp = get_config_value('dnssec')
self.assertEqual(tmp, option)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
def test_external_nameserver(self):
# Externe Domain Name Servers (DNS) addresses
@@ -140,9 +145,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase):
tmp = get_config_value('export-etc-hosts')
self.assertEqual(tmp, 'yes')
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
def test_domain_forwarding(self):
for network in allow_from:
self.cli_set(base_path + ['allow-from', network])
@@ -179,9 +181,26 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase):
if domain == domains[1]:
self.assertIn(f'addNTA("{domain}", "static")', hosts_conf)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ def test_dns64(self):
+ dns_prefix = '64:ff9b::/96'
+
+ for network in allow_from:
+ self.cli_set(base_path + ['allow-from', network])
+ for address in listen_adress:
+ self.cli_set(base_path + ['listen-address', address])
+
+ # Check dns64-prefix - must be prefix /96
+ self.cli_set(base_path + ['dns64-prefix', '2001:db8:aabb::/64'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['dns64-prefix', dns_prefix])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify dns64-prefix configuration
+ tmp = get_config_value('dns64-prefix')
+ self.assertEqual(tmp, dns_prefix)
if __name__ == '__main__':
unittest.main(verbosity=2)
-
diff --git a/smoketest/scripts/cli/test_service_salt.py b/smoketest/scripts/cli/test_service_salt.py
new file mode 100755
index 000000000..5b328677e
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_salt.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from socket import gethostname
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.util import process_named_running
+from vyos.util import read_file
+from vyos.util import cmd
+
+PROCESS_NAME = 'salt-minion'
+SALT_CONF = '/etc/salt/minion'
+base_path = ['service', 'salt-minion']
+
+class TestServiceSALT(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceSALT, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # delete testing SALT config
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # For an unknown reason on QEMU systems (e.g. where smoketests are executed
+ # from the CI) salt-minion process is not killed by systemd. Apparently
+ # no issue on VMWare.
+ if cmd('systemd-detect-virt') != 'kvm':
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_default(self):
+ servers = ['192.0.2.1', '192.0.2.2']
+
+ for server in servers:
+ self.cli_set(base_path + ['master', server])
+
+ self.cli_commit()
+
+ # commiconf = read_file() Check configured port
+ conf = read_file(SALT_CONF)
+ self.assertIn(f'- {server}', conf)
+
+ # defaults
+ hostname = gethostname()
+ self.assertIn(f'hash_type: sha256', conf)
+ self.assertIn(f'id: {hostname}', conf)
+ self.assertIn(f'mine_interval: 60', conf)
+
+ def test_options(self):
+ server = '192.0.2.3'
+ hash = 'sha1'
+ id = 'foo'
+ interval = '120'
+
+ self.cli_set(base_path + ['master', server])
+ self.cli_set(base_path + ['hash', hash])
+ self.cli_set(base_path + ['id', id])
+ self.cli_set(base_path + ['interval', interval])
+
+ self.cli_commit()
+
+ # commiconf = read_file() Check configured port
+ conf = read_file(SALT_CONF)
+ self.assertIn(f'- {server}', conf)
+
+ # defaults
+ self.assertIn(f'hash_type: {hash}', conf)
+ self.assertIn(f'id: {id}', conf)
+ self.assertIn(f'mine_interval: {interval}', conf)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py
index 00754e668..864097771 100755
--- a/smoketest/scripts/cli/test_service_snmp.py
+++ b/smoketest/scripts/cli/test_service_snmp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 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
@@ -94,6 +94,51 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):
self.assertTrue(process_named_running(PROCESS_NAME))
self.cli_delete(['interfaces', 'dummy', dummy_if])
+ def test_snmp_tcp(self):
+ dummy_if = 'dum123'
+ dummy_addr = '100.64.0.1/32'
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', dummy_addr])
+
+ # Check if SNMP can be configured and service runs
+ clients = ['192.0.2.1', '2001:db8::1']
+ networks = ['192.0.2.128/25', '2001:db8:babe::/48']
+ listen = ['127.0.0.1', '::1', address_from_cidr(dummy_addr)]
+ port = '2325'
+
+ for auth in ['ro', 'rw']:
+ community = 'VyOS' + auth
+ self.cli_set(base_path + ['community', community, 'authorization', auth])
+ for client in clients:
+ self.cli_set(base_path + ['community', community, 'client', client])
+ for network in networks:
+ self.cli_set(base_path + ['community', community, 'network', network])
+
+ for addr in listen:
+ self.cli_set(base_path + ['listen-address', addr, 'port', port])
+
+ self.cli_set(base_path + ['contact', 'maintainers@vyos.io'])
+ self.cli_set(base_path + ['location', 'qemu'])
+ self.cli_set(base_path + ['protocol', 'tcp'])
+
+ self.cli_commit()
+
+ # verify listen address, it will be returned as
+ # ['unix:/run/snmpd.socket,tcp:127.0.0.1:161,tcp6:[::1]:161']
+ # thus we need to transfor this into a proper list
+ config = get_config_value('agentaddress')
+ expected = 'unix:/run/snmpd.socket'
+ self.assertIn(expected, config)
+
+ for addr in listen:
+ if is_ipv4(addr):
+ expected = f'tcp:{addr}:{port}'
+ else:
+ expected = f'tcp6:[{addr}]:{port}'
+ self.assertIn(expected, config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.cli_delete(['interfaces', 'dummy', dummy_if])
def test_snmpv3_sha(self):
# Check if SNMPv3 can be configured with SHA authentication
diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py
index 6f58ce3d3..49a167e04 100755
--- a/smoketest/scripts/cli/test_service_ssh.py
+++ b/smoketest/scripts/cli/test_service_ssh.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,14 +14,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
+import paramiko
import re
import os
import unittest
+from pwd import getpwall
+
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
+from vyos.util import is_systemd_service_running
from vyos.util import process_named_running
from vyos.util import read_file
@@ -42,10 +47,18 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
# delete testing SSH config
self.cli_delete(base_path)
self.cli_commit()
+ # Established SSH connections remains running after service is stopped.
+ # We can not use process_named_running here - we rather need to check
+ # that the systemd service is no longer running
+ self.assertFalse(is_systemd_service_running(PROCESS_NAME))
+
def test_ssh_default(self):
# Check if SSH service runs with default settings - used for checking
# behavior of <defaultValue> in XML definition
@@ -58,9 +71,6 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
port = get_config_value('Port')[0]
self.assertEqual('22', port)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
def test_ssh_single_listen_address(self):
# Check if SSH service can be configured and runs
self.cli_set(base_path + ['port', '1234'])
@@ -97,9 +107,6 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
keepalive = get_config_value('ClientAliveInterval')[0]
self.assertTrue("100" in keepalive)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
def test_ssh_multiple_listen_addresses(self):
# Check if SSH service can be configured and runs with multiple
# listen ports and listen-addresses
@@ -124,9 +131,6 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
for address in addresses:
self.assertIn(address, tmp)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
def test_ssh_vrf(self):
# Check if SSH service can be bound to given VRF
port = '22'
@@ -146,9 +150,6 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
tmp = get_config_value('Port')
self.assertIn(port, tmp)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
# Check for process in VRF
tmp = cmd(f'ip vrf pids {vrf}')
self.assertIn(PROCESS_NAME, tmp)
@@ -156,5 +157,51 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
# delete VRF
self.cli_delete(['vrf', 'name', vrf])
+ def test_ssh_login(self):
+ # Perform SSH login and command execution with a predefined user. The
+ # result (output of uname -a) must match the output if the command is
+ # run natively.
+ #
+ # We also try to login as an invalid user - this is not allowed to work.
+
+ def ssh_send_cmd(command, username, password, host='localhost'):
+ """ SSH command execution helper """
+ # Try to login via SSH
+ ssh_client = paramiko.SSHClient()
+ ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh_client.connect(hostname='localhost', username=username, password=password)
+ _, stdout, stderr = ssh_client.exec_command(command)
+ output = stdout.read().decode().strip()
+ error = stderr.read().decode().strip()
+ ssh_client.close()
+ return output, error
+
+ test_user = 'ssh_test'
+ test_pass = 'v2i57DZs8idUwMN3VC92'
+ test_command = 'uname -a'
+
+ self.cli_set(base_path)
+ self.cli_set(['system', 'login', 'user', test_user, 'authentication', 'plaintext-password', test_pass])
+
+ # commit changes
+ self.cli_commit()
+
+ # Login with proper credentials
+ output, error = ssh_send_cmd(test_command, test_user, test_pass)
+ # verify login
+ self.assertFalse(error)
+ self.assertEqual(output, cmd(test_command))
+
+ # Login with invalid credentials
+ with self.assertRaises(paramiko.ssh_exception.AuthenticationException):
+ output, error = ssh_send_cmd(test_command, 'invalid_user', 'invalid_password')
+
+ self.cli_delete(['system', 'login', 'user', test_user])
+ self.cli_commit()
+
+ # After deletion the test user is not allowed to remain in /etc/passwd
+ usernames = [x[0] for x in getpwall()]
+ self.assertNotIn(test_user, usernames)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py
index 57866a198..92a7973fa 100755
--- a/smoketest/scripts/cli/test_system_flow-accounting.py
+++ b/smoketest/scripts/cli/test_system_flow-accounting.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 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
@@ -64,7 +64,8 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
# verify configuration
tmp = cmd('sudo iptables-save -t raw')
for interface in Section.interfaces('ethernet'):
- self.assertIn(f'-A VYATTA_CT_PREROUTING_HOOK -i {interface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size 128 --nflog-threshold 100', tmp)
+ self.assertIn(f'-A VYATTA_CT_PREROUTING_HOOK -i {interface} -m comment --comment FLOW_ACCOUNTING_RULE -j '
+ f'NFLOG --nflog-group 2 --nflog-size 128 --nflog-threshold 100', tmp)
uacctd = read_file(uacctd_conf)
# circular queue size - buffer_size
@@ -130,14 +131,15 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
self.assertNotIn(f'plugins: memory', uacctd)
for server, server_config in sflow_server.items():
+ tmp_srv = server.replace('.', '-')
if 'port' in server_config:
- self.assertIn(f'sfprobe_receiver[sf_{server}]: {server}', uacctd)
+ self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {server}', uacctd)
else:
- self.assertIn(f'sfprobe_receiver[sf_{server}]: {server}:6343', uacctd)
+ self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {server}:6343', uacctd)
- self.assertIn(f'sfprobe_agentip[sf_{server}]: {agent_address}', uacctd)
- self.assertIn(f'sampling_rate[sf_{server}]: {sampling_rate}', uacctd)
- self.assertIn(f'sfprobe_source_ip[sf_{server}]: {source_address}', uacctd)
+ self.assertIn(f'sfprobe_agentip[sf_{tmp_srv}]: {agent_address}', uacctd)
+ self.assertIn(f'sampling_rate[sf_{tmp_srv}]: {sampling_rate}', uacctd)
+ self.assertIn(f'sfprobe_source_ip[sf_{tmp_srv}]: {source_address}', uacctd)
self.cli_delete(['interfaces', 'dummy', dummy_if])
@@ -203,23 +205,27 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
tmp = 'plugins: '
for server, server_config in netflow_server.items():
- tmp += f'nfprobe[nf_{server}],'
+ tmp_srv = server.replace('.', '-')
+ tmp += f'nfprobe[nf_{tmp_srv}],'
tmp += 'memory'
self.assertIn(f'{tmp}', uacctd)
for server, server_config in netflow_server.items():
- self.assertIn(f'nfprobe_engine[nf_{server}]: {engine_id}', uacctd)
- self.assertIn(f'nfprobe_maxflows[nf_{server}]: {max_flows}', uacctd)
- self.assertIn(f'sampling_rate[nf_{server}]: {sampling_rate}', uacctd)
- self.assertIn(f'nfprobe_source_ip[nf_{server}]: {source_address}', uacctd)
- self.assertIn(f'nfprobe_version[nf_{server}]: {version}', uacctd)
+ tmp_srv = server.replace('.', '-')
+ self.assertIn(f'nfprobe_engine[nf_{tmp_srv}]: {engine_id}', uacctd)
+ self.assertIn(f'nfprobe_maxflows[nf_{tmp_srv}]: {max_flows}', uacctd)
+ self.assertIn(f'sampling_rate[nf_{tmp_srv}]: {sampling_rate}', uacctd)
+ self.assertIn(f'nfprobe_source_ip[nf_{tmp_srv}]: {source_address}', uacctd)
+ self.assertIn(f'nfprobe_version[nf_{tmp_srv}]: {version}', uacctd)
if 'port' in server_config:
- self.assertIn(f'nfprobe_receiver[nf_{server}]: {server}', uacctd)
+ self.assertIn(f'nfprobe_receiver[nf_{tmp_srv}]: {server}', uacctd)
else:
- self.assertIn(f'nfprobe_receiver[nf_{server}]: {server}:2055', uacctd)
+ self.assertIn(f'nfprobe_receiver[nf_{tmp_srv}]: {server}:2055', uacctd)
- self.assertIn(f'nfprobe_timeouts[nf_{server}]: expint={tmo_expiry}:general={tmo_flow}:icmp={tmo_icmp}:maxlife={tmo_max}:tcp.fin={tmo_tcp_fin}:tcp={tmo_tcp_generic}:tcp.rst={tmo_tcp_rst}:udp={tmo_udp}', uacctd)
+ self.assertIn(f'nfprobe_timeouts[nf_{tmp_srv}]: expint={tmo_expiry}:general={tmo_flow}:'
+ f'icmp={tmo_icmp}:maxlife={tmo_max}:tcp.fin={tmo_tcp_fin}:tcp={tmo_tcp_generic}:'
+ f'tcp.rst={tmo_tcp_rst}:udp={tmo_udp}', uacctd)
self.cli_delete(['interfaces', 'dummy', dummy_if])
diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py
index 3112d2e46..837d1dc12 100755
--- a/smoketest/scripts/cli/test_system_ipv6.py
+++ b/smoketest/scripts/cli/test_system_ipv6.py
@@ -17,7 +17,12 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.template import is_ipv4
from vyos.util import read_file
+from vyos.util import is_ipv6_enabled
+from vyos.util import get_interface_config
+from vyos.validate import is_intf_addr_assigned
base_path = ['system', 'ipv6']
@@ -42,6 +47,14 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
self.assertEqual(read_file(file_forwarding), '0')
def test_system_ipv6_disable(self):
+ # Verify previous "enable" state
+ self.assertEqual(read_file(file_disable), '0')
+ self.assertTrue(is_ipv6_enabled())
+
+ loopbacks = ['127.0.0.1', '::1']
+ for addr in loopbacks:
+ self.assertTrue(is_intf_addr_assigned('lo', addr))
+
# Do not assign any IPv6 address on interfaces, this requires a reboot
# which can not be tested, but we can read the config file :)
self.cli_set(base_path + ['disable'])
@@ -49,6 +62,24 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
# Verify configuration file
self.assertEqual(read_file(file_disable), '1')
+ self.assertFalse(is_ipv6_enabled())
+
+ for addr in loopbacks:
+ if is_ipv4(addr):
+ self.assertTrue(is_intf_addr_assigned('lo', addr))
+ else:
+ self.assertFalse(is_intf_addr_assigned('lo', addr))
+
+ # T4330: Verify MTU can be changed with IPv6 disabled
+ mtu = '1600'
+ eth_if = 'eth0'
+ self.cli_set(['interfaces', 'ethernet', eth_if, 'mtu', mtu])
+ self.cli_commit()
+
+ tmp = get_interface_config(eth_if)
+ self.assertEqual(tmp['mtu'], int(mtu))
+
+ self.cli_delete(['interfaces', 'ethernet', eth_if, 'mtu'])
def test_system_ipv6_strict_dad(self):
# This defaults to 1
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index 69a06eeac..bc76de0ad 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 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
@@ -23,6 +23,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM
from distutils.version import LooseVersion
from platform import release as kernel_version
from subprocess import Popen, PIPE
+from pwd import getpwall
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
@@ -52,6 +53,11 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
+ # After deletion, a user is not allowed to remain in /etc/passwd
+ usernames = [x[0] for x in getpwall()]
+ for user in users:
+ self.assertNotIn(user, usernames)
+
def test_add_linux_system_user(self):
# We are not allowed to re-use a username already taken by the Linux
# base system
diff --git a/smoketest/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py
index c8cf04b7d..0ee96157c 100755
--- a/smoketest/scripts/cli/test_system_ntp.py
+++ b/smoketest/scripts/cli/test_system_ntp.py
@@ -108,5 +108,22 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase):
for listen in listen_address:
self.assertIn(f'interface listen {listen}', config)
+ def test_03_ntp_interface(self):
+ interfaces = ['eth0', 'eth1']
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface])
+
+ servers = ['time1.vyos.net', 'time2.vyos.net']
+ for server in servers:
+ self.cli_set(base_path + ['server', server])
+
+ self.cli_commit()
+
+ # Check generated client address configuration
+ config = read_file(NTP_CONF)
+ self.assertIn('interface ignore wildcard', config)
+ for interface in interfaces:
+ self.assertIn(f'interface listen {interface}', config)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py
index ccac0820d..6db49abab 100755
--- a/smoketest/scripts/cli/test_vpn_openconnect.py
+++ b/smoketest/scripts/cli/test_vpn_openconnect.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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
@@ -19,36 +19,82 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.util import process_named_running
from vyos.util import cmd
+from vyos.util import read_file
from os import path, mkdir
-OCSERV_CONF = '/run/ocserv/ocserv.conf'
base_path = ['vpn', 'openconnect']
cert_dir = '/config/auth/'
ca_cert = f'{cert_dir}ca.crt'
ssl_cert = f'{cert_dir}server.crt'
ssl_key = f'{cert_dir}server.key'
-class TestVpnOpenconnect(VyOSUnitTestSHIM.TestCase):
+PROCESS_NAME = 'ocserv-main'
+config_file = '/run/ocserv/ocserv.conf'
+auth_file = '/run/ocserv/ocpasswd'
+otp_file = '/run/ocserv/users.oath'
+
+class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestVPNOpenConnect, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ cls.cli_set(cls, base_path + ["ssl", "ca-cert-file", ca_cert])
+ cls.cli_set(cls, base_path + ["ssl", "cert-file", ssl_cert])
+ cls.cli_set(cls, base_path + ["ssl", "key-file", ssl_key])
+
def tearDown(self):
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
# Delete vpn openconnect configuration
self.cli_delete(base_path)
self.cli_commit()
- def test_vpn(self):
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_ocserv(self):
user = 'vyos_user'
password = 'vyos_pass'
- self.cli_delete(base_path)
- self.cli_set(base_path + ["authentication", "local-users", "username", user, "password", password])
- self.cli_set(base_path + ["authentication", "mode", "local"])
- self.cli_set(base_path + ["network-settings", "client-ip-settings", "subnet", "192.0.2.0/24"])
- self.cli_set(base_path + ["ssl", "ca-cert-file", ca_cert])
- self.cli_set(base_path + ["ssl", "cert-file", ssl_cert])
- self.cli_set(base_path + ["ssl", "key-file", ssl_key])
+
+ v4_subnet = '192.0.2.0/24'
+ v6_prefix = '2001:db8:1000::/64'
+ v6_len = '126'
+ name_server = ['1.2.3.4', '1.2.3.5', '2001:db8::1']
+ split_dns = ['vyos.net', 'vyos.io']
+
+ self.cli_set(base_path + ['authentication', 'local-users', 'username', user, 'password', password])
+ self.cli_set(base_path + ['authentication', 'mode', "local"])
+ self.cli_set(base_path + ["network-settings", "client-ip-settings", "subnet", v4_subnet])
+ self.cli_set(base_path + ['network-settings', 'client-ip-settings', 'subnet', v4_subnet])
+ self.cli_set(base_path + ['network-settings', 'client-ipv6-pool', 'prefix', v6_prefix])
+ self.cli_set(base_path + ['network-settings', 'client-ipv6-pool', 'mask', v6_len])
+
+ for ns in name_server:
+ self.cli_set(base_path + ['network-settings', 'name-server', ns])
+ for domain in split_dns:
+ self.cli_set(base_path + ['network-settings', 'split-dns', domain])
self.cli_commit()
- # Check for running process
- self.assertTrue(process_named_running('ocserv-main'))
+ # Verify configuration
+ daemon_config = read_file(config_file)
+
+ # authentication mode local password-otp
+ self.assertIn(f'auth = "plain[/run/ocserv/ocpasswd]"', daemon_config)
+ self.assertIn(f'ipv4-network = {v4_subnet}', daemon_config)
+ self.assertIn(f'ipv6-network = {v6_prefix}', daemon_config)
+ self.assertIn(f'ipv6-subnet-prefix = {v6_len}', daemon_config)
+
+ for ns in name_server:
+ self.assertIn(f'dns = {ns}', daemon_config)
+ for domain in split_dns:
+ self.assertIn(f'split-dns = {domain}', daemon_config)
+
+ auth_config = read_file(auth_file)
+ self.assertIn(f'{user}:*:$', auth_config)
if __name__ == '__main__':
if not path.exists(cert_dir):
diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py
index 0f006ca3c..6614aeb06 100755
--- a/smoketest/scripts/cli/test_vrf.py
+++ b/smoketest/scripts/cli/test_vrf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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
@@ -27,8 +27,10 @@ from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.template import is_ipv6
+from vyos.template import is_ipv4
from vyos.util import cmd
from vyos.util import read_file
+from vyos.util import get_interface_config
from vyos.validate import is_intf_addr_assigned
base_path = ['vrf']
@@ -61,6 +63,8 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
def tearDown(self):
# delete all VRFs
self.cli_delete(base_path)
+ self.cli_delete(['interfaces', 'dummy'])
+ self.cli_delete(['protocols', 'vrf'])
self.cli_commit()
for vrf in vrfs:
self.assertNotIn(vrf, interfaces())
@@ -108,9 +112,14 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
# ...
regex = f'{table}\s+{vrf}\s+#\s+{description}'
self.assertTrue(re.findall(regex, iproute2_config))
+
+ tmp = get_interface_config(vrf)
+ self.assertEqual(int(table), tmp['linkinfo']['info_data']['table'])
+
+ # Increment table ID for the next run
table = str(int(table) + 1)
- def test_vrf_loopback_ips(self):
+ def test_vrf_loopbacks_ips(self):
table = '2000'
for vrf in vrfs:
base = base_path + ['name', vrf]
@@ -121,10 +130,48 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify VRF configuration
+ loopbacks = ['127.0.0.1', '::1']
for vrf in vrfs:
- self.assertTrue(vrf in interfaces())
- self.assertTrue(is_intf_addr_assigned(vrf, '127.0.0.1'))
- self.assertTrue(is_intf_addr_assigned(vrf, '::1'))
+ # Ensure VRF was created
+ self.assertIn(vrf, interfaces())
+ # Test for proper loopback IP assignment
+ for addr in loopbacks:
+ self.assertTrue(is_intf_addr_assigned(vrf, addr))
+
+ def test_vrf_loopbacks_no_ipv6(self):
+ table = '2002'
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', str(table)])
+ table = str(int(table) + 1)
+
+ # Globally disable IPv6 - this will remove all IPv6 interface addresses
+ self.cli_set(['system', 'ipv6', 'disable'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ table = '2002'
+ loopbacks = ['127.0.0.1', '::1']
+ for vrf in vrfs:
+ # Ensure VRF was created
+ self.assertIn(vrf, interfaces())
+
+ # Verify VRF table ID
+ tmp = get_interface_config(vrf)
+ self.assertEqual(int(table), tmp['linkinfo']['info_data']['table'])
+
+ # Test for proper loopback IP assignment
+ for addr in loopbacks:
+ if is_ipv4(addr):
+ self.assertTrue(is_intf_addr_assigned(vrf, addr))
+ else:
+ self.assertFalse(is_intf_addr_assigned(vrf, addr))
+
+ table = str(int(table) + 1)
+
+ self.cli_delete(['system', 'ipv6'])
def test_vrf_bind_all(self):
table = '2000'
@@ -176,11 +223,11 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
# commit changes
self.cli_commit()
- # Verify & cleanup
+ # Verify VRF assignmant
for interface in self._interfaces:
- # os.readlink resolves to: '../../../../../virtual/net/foovrf'
- tmp = os.readlink(f'/sys/class/net/{interface}/master').split('/')[-1]
- self.assertEqual(tmp, vrf)
+ tmp = get_interface_config(interface)
+ self.assertEqual(vrf, tmp['master'])
+
# cleanup
section = Section.section(interface)
self.cli_delete(['interfaces', section, interface, 'vrf'])
@@ -204,14 +251,14 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
},
}
+ # required interface for leaking to default table
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '192.0.2.1/24'])
+
table = '2000'
for vrf in vrfs:
base = base_path + ['name', vrf]
self.cli_set(base + ['table', str(table)])
- # required interface for leaking to default table
- self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '192.0.2.1/24'])
-
# we also need an interface in "UP" state to install routes
self.cli_set(['interfaces', 'dummy', f'dum{table}', 'vrf', vrf])
self.cli_set(['interfaces', 'dummy', f'dum{table}', 'address', '192.0.2.1/24'])
@@ -233,28 +280,64 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify routes
- table = '2000'
for vrf in vrfs:
- for route, route_config in routes.items():
- if is_ipv6(route):
- tmp = get_vrf_ipv6_routes(vrf)
- else:
- tmp = get_vrf_ipv4_routes(vrf)
+ self.assertIn(vrf, interfaces())
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ for prefix, prefix_config in routes.items():
+ tmp = 'ip'
+ if is_ipv6(prefix):
+ tmp += 'v6'
- found = False
- for result in tmp:
- if 'dst' in result and result['dst'] == route:
- if 'gateway' in result and result['gateway'] == route_config['next_hop']:
- found = True
+ tmp += f' route {prefix} {prefix_config["next_hop"]}'
+ if 'distance' in prefix_config:
+ tmp += ' ' + prefix_config['distance']
+ if 'next_hop_vrf' in prefix_config:
+ tmp += ' nexthop-vrf ' + prefix_config['next_hop_vrf']
- self.assertTrue(found)
+ self.assertIn(tmp, frrconfig)
- # Cleanup
- self.cli_delete(['protocols', 'vrf', vrf])
- self.cli_delete(['interfaces', 'dummy', f'dum{table}'])
- self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', '192.0.2.1/24'])
+ self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address'])
- table = str(int(table) + 1)
+
+ def test_vrf_link_local_ip_addresses(self):
+ # Testcase for issue T4331
+ table = '100'
+ vrf = 'orange'
+ interface = 'dum9998'
+ addresses = ['192.0.2.1/26', '2001:db8:9998::1/64', 'fe80::1/64']
+
+ for address in addresses:
+ self.cli_set(['interfaces', 'dummy', interface, 'address', address])
+
+ # Create dummy interfaces
+ self.cli_commit()
+
+ # ... and verify IP addresses got assigned
+ for address in addresses:
+ self.assertTrue(is_intf_addr_assigned(interface, address))
+
+ # Move interface to VRF
+ self.cli_set(base_path + ['name', vrf, 'table', table])
+ self.cli_set(['interfaces', 'dummy', interface, 'vrf', vrf])
+
+ # Apply VRF config
+ self.cli_commit()
+ # Ensure VRF got created
+ self.assertIn(vrf, interfaces())
+ # ... and IP addresses are still assigned
+ for address in addresses:
+ self.assertTrue(is_intf_addr_assigned(interface, address))
+ # Verify VRF table ID
+ tmp = get_interface_config(vrf)
+ self.assertEqual(int(table), tmp['linkinfo']['info_data']['table'])
+
+ # Verify interface is assigned to VRF
+ tmp = get_interface_config(interface)
+ self.assertEqual(vrf, tmp['master'])
+
+ # Delete Interface
+ self.cli_delete(['interfaces', 'dummy', interface])
+ self.cli_commit()
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index a8cef5ebf..d27f8d995 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 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
@@ -109,7 +109,7 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
# T2665: defaults include lease time per TAG node which need to be added to
# individual subnet definitions
default_values = defaults(base + ['shared-network-name', 'subnet'])
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index e6a2e4486..be1e6db1e 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 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
@@ -41,7 +41,7 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
return dhcpv6
def verify(dhcpv6):
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 06366362a..3f3a2e232 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -16,6 +16,7 @@
import os
+from netifaces import interfaces
from sys import exit
from vyos.config import Config
@@ -62,10 +63,6 @@ def get_config(config=None):
if conf.exists(base_nameservers):
dns.update({'system_name_server': conf.return_values(base_nameservers)})
- base_nameservers_dhcp = ['system', 'name-servers-dhcp']
- if conf.exists(base_nameservers_dhcp):
- dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)})
-
return dns
def verify(dns):
@@ -86,10 +83,15 @@ def verify(dns):
if 'server' not in dns['domain'][domain]:
raise ConfigError(f'No server configured for domain {domain}!')
+ if 'dns64_prefix' in dns:
+ dns_prefix = dns['dns64_prefix'].split('/')[1]
+ # RFC 6147 requires prefix /96
+ if int(dns_prefix) != 96:
+ raise ConfigError('DNS 6to4 prefix must be of length /96')
+
if 'system' in dns:
- if not ('system_name_server' in dns or 'system_name_server_dhcp' in dns):
- print("Warning: No 'system name-server' or 'system " \
- "name-servers-dhcp' configured")
+ if not 'system_name_server' in dns:
+ print('Warning: No "system name-server" configured')
return None
@@ -142,10 +144,15 @@ def apply(dns):
hc.delete_name_server_tags_recursor(['system'])
# add dhcp nameserver tags for configured interfaces
- if 'system_name_server_dhcp' in dns:
- for interface in dns['system_name_server_dhcp']:
- hc.add_name_server_tags_recursor(['dhcp-' + interface,
- 'dhcpv6-' + interface ])
+ if 'system_name_server' in dns:
+ for interface in dns['system_name_server']:
+ # system_name_server key contains both IP addresses and interface
+ # names (DHCP) to use DNS servers. We need to check if the
+ # value is an interface name - only if this is the case, add the
+ # interface based DNS forwarder.
+ if interface in interfaces():
+ hc.add_name_server_tags_recursor(['dhcp-' + interface,
+ 'dhcpv6-' + interface ])
# hostsd will generate the forward-zones file
# the list and keys() are required as get returns a dict, not list
diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py
index 67bf5d0e2..b7f4aa82c 100755
--- a/src/conf_mode/firewall_options.py
+++ b/src/conf_mode/firewall_options.py
@@ -115,9 +115,12 @@ def apply(tcp):
continue
# adjust TCP MSS per interface
- if mss:
+ if mss == 'clamp-mss-to-pmtu':
call('iptables --table mangle --append {} --out-interface {} --protocol tcp '
- '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
+ '--tcp-flags SYN,RST SYN --jump TCPMSS --clamp-mss-to-pmtu >&/dev/null'.format(target, intf))
+ elif mss:
+ call('iptables --table mangle --append {} --out-interface {} --protocol tcp '
+ '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
# Setup new ip6tables rules
if tcp['new_chain6']:
@@ -133,9 +136,12 @@ def apply(tcp):
continue
# adjust TCP MSS per interface
- if mss:
+ if mss == 'clamp-mss-to-pmtu':
+ call('ip6tables --table mangle --append {} --out-interface {} --protocol tcp '
+ '--tcp-flags SYN,RST SYN --jump TCPMSS --clamp-mss-to-pmtu >&/dev/null'.format(target, intf))
+ elif mss:
call('ip6tables --table mangle --append {} --out-interface {} --protocol tcp '
- '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
+ '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
return None
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index a7135911d..87bad0dc6 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -79,7 +79,7 @@ def get_config(config=None):
# system static-host-mapping
for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']):
hosts['static_host_mapping'][hn] = {}
- hosts['static_host_mapping'][hn]['address'] = conf.return_value(['system', 'static-host-mapping', 'host-name', hn, 'inet'])
+ hosts['static_host_mapping'][hn]['address'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'inet'])
hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias'])
return hosts
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index 96c50b4b5..1e58bb1e4 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -142,6 +142,10 @@ def get_config(config=None):
'api_set': api_set,
'certbot': certbot}
+ vrf_path = ['service', 'https', 'vrf']
+ if conf.exists(vrf_path):
+ https['vrf'] = conf.return_value(vrf_path)
+
return https
def verify(https):
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index d5be21949..94deb1abb 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 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
@@ -35,6 +35,7 @@ from vyos.ifconfig import BondIf
from vyos.ifconfig import Section
from vyos.util import dict_search
from vyos.validate import has_address_configured
+from vyos.validate import has_vrf_configured
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -71,57 +72,83 @@ def get_config(config=None):
# 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
+ # later on for each member
if 'member' in bond and 'interface' in bond['member']:
- # convert list if member interfaces to a dictionary
- bond['member']['interface'] = dict.fromkeys(
- bond['member']['interface'], {})
+ # convert list of member interfaces to a dictionary
+ bond['member']['interface'] = {k: {} for k in bond['member']['interface']}
if 'mode' in bond:
bond['mode'] = get_bond_mode(bond['mode'])
tmp = leaf_node_changed(conf, ['mode'])
- if tmp: bond.update({'shutdown_required': {}})
+ if tmp: bond['shutdown_required'] = {}
tmp = leaf_node_changed(conf, ['lacp-rate'])
- if tmp: bond.update({'shutdown_required': {}})
+ if tmp: bond['shutdown_required'] = {}
# determine which members have been removed
interfaces_removed = leaf_node_changed(conf, ['member', 'interface'])
+
+ # Reset config level to interfaces
+ old_level = conf.get_level()
+ conf.set_level(['interfaces'])
+
if interfaces_removed:
- bond.update({'shutdown_required': {}})
+ bond['shutdown_required'] = {}
if 'member' not in bond:
- bond.update({'member': {}})
+ bond['member'] = {}
tmp = {}
for interface in interfaces_removed:
section = Section.section(interface) # this will be 'ethernet' for 'eth0'
- if conf.exists(['insterfaces', section, interface, 'disable']):
- tmp.update({interface : {'disable': ''}})
+ if conf.exists([section, interface, 'disable']):
+ tmp[interface] = {'disable': ''}
else:
- tmp.update({interface : {}})
+ tmp[interface] = {}
# also present the interfaces to be removed from the bond as dictionary
- bond['member'].update({'interface_remove': tmp})
+ bond['member']['interface_remove'] = tmp
+
+ # Restore existing config level
+ conf.set_level(old_level)
if dict_search('member.interface', bond):
for interface, interface_config in bond['member']['interface'].items():
+ # Check if member interface is a new member
+ if not conf.exists_effective(['member', 'interface', interface]):
+ bond['shutdown_required'] = {}
+
+ # Check if member interface is disabled
+ conf.set_level(['interfaces'])
+
+ section = Section.section(interface) # this will be 'ethernet' for 'eth0'
+ if conf.exists([section, interface, 'disable']):
+ interface_config['disable'] = ''
+
+ conf.set_level(old_level)
+
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
- if tmp: interface_config.update({'is_bridge_member' : tmp})
+ if tmp: interface_config['is_bridge_member'] = tmp
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
- if tmp and bond['ifname'] not in tmp:
- interface_config.update({'is_bond_member' : tmp})
+ for tmp in is_member(conf, interface, 'bonding'):
+ if bond['ifname'] == tmp:
+ continue
+ interface_config['is_bond_member'] = tmp
# Check if member interface is used as source-interface on another interface
tmp = is_source_interface(conf, interface)
- if tmp: interface_config.update({'is_source_interface' : tmp})
+ if tmp: interface_config['is_source_interface'] = tmp
# bond members must not have an assigned address
tmp = has_address_configured(conf, interface)
- if tmp: interface_config.update({'has_address' : ''})
+ if tmp: interface_config['has_address'] = {}
+
+ # bond members must not have a VRF attached
+ tmp = has_vrf_configured(conf, interface)
+ if tmp: interface_config['has_vrf'] = {}
return bond
@@ -179,6 +206,8 @@ def verify(bond):
if 'has_address' in interface_config:
raise ConfigError(error_msg + 'it has an address assigned!')
+ if 'has_vrf' in interface_config:
+ raise ConfigError(error_msg + 'it has a VRF assigned!')
if 'primary' in bond:
if bond['primary'] not in bond['member']['interface']:
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index f4dba9d4a..f548a0e75 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -30,6 +30,7 @@ from vyos.configverify import verify_dhcpv6
from vyos.configverify import verify_vrf
from vyos.ifconfig import BridgeIf
from vyos.validate import has_address_configured
+from vyos.validate import has_vrf_configured
from vyos.xml import defaults
from vyos.util import cmd
@@ -59,7 +60,7 @@ def get_config(config=None):
else:
bridge.update({'member': {'interface_remove': tmp }})
- if dict_search('member.interface', bridge):
+ if dict_search('member.interface', bridge) != None:
# XXX: T2665: we need a copy of the dict keys for iteration, else we will get:
# RuntimeError: dictionary changed size during iteration
for interface in list(bridge['member']['interface']):
@@ -92,11 +93,23 @@ def get_config(config=None):
tmp = has_address_configured(conf, interface)
if tmp: bridge['member']['interface'][interface].update({'has_address' : ''})
+ # Bridge members must not have a VRF attached
+ tmp = has_vrf_configured(conf, interface)
+ if tmp: bridge['member']['interface'][interface].update({'has_vrf' : ''})
+
# VLAN-aware bridge members must not have VLAN interface configuration
tmp = has_vlan_subinterface_configured(conf,interface)
if 'enable_vlan' in bridge and tmp:
bridge['member']['interface'][interface].update({'has_vlan' : ''})
+ # delete empty dictionary keys - no need to run code paths if nothing is there to do
+ if 'member' in bridge:
+ if 'interface' in bridge['member'] and len(bridge['member']['interface']) == 0:
+ del bridge['member']['interface']
+
+ if len(bridge['member']) == 0:
+ del bridge['member']
+
return bridge
def verify(bridge):
@@ -130,9 +143,12 @@ def verify(bridge):
if 'has_address' in interface_config:
raise ConfigError(error_msg + 'it has an address assigned!')
+ if 'has_vrf' in interface_config:
+ raise ConfigError(error_msg + 'it has a VRF assigned!')
+
if 'enable_vlan' in bridge:
if 'has_vlan' in interface_config:
- raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!')
+ raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!')
if 'wlan' in interface:
raise ConfigError(error_msg + 'VLAN aware cannot be set!')
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index de851262b..75690dd81 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -30,6 +30,7 @@ from vyos.configverify import verify_mtu
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_bond_bridge_member
from vyos.ethtool import Ethtool
from vyos.ifconfig import EthernetIf
from vyos.template import render
@@ -67,6 +68,7 @@ def verify(ethernet):
verify_dhcpv6(ethernet)
verify_address(ethernet)
verify_vrf(ethernet)
+ verify_bond_bridge_member(ethernet)
verify_eapol(ethernet)
verify_mirror(ethernet)
@@ -151,9 +153,9 @@ def apply(ethernet):
else:
e.update(ethernet)
if 'eapol' in ethernet:
- eapol_action='restart'
+ eapol_action='reload-or-restart'
- call(f'systemctl {eapol_action} wpa_supplicant-macsec@{ifname}')
+ call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}')
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index 979a5612e..a5213fbce 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-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 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,9 +21,11 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import GeneveIf
from vyos import ConfigError
@@ -41,6 +43,14 @@ def get_config(config=None):
conf = Config()
base = ['interfaces', 'geneve']
geneve = get_interface_dict(conf, base)
+
+ # GENEVE interfaces are picky and require recreation if certain parameters
+ # change. But a GENEVE interface should - of course - not be re-created if
+ # it's description or IP address is adjusted. Feels somehow logic doesn't it?
+ for cli_option in ['remote', 'vni']:
+ if leaf_node_changed(conf, cli_option):
+ geneve.update({'rebuild_required': {}})
+
return geneve
def verify(geneve):
@@ -50,6 +60,7 @@ def verify(geneve):
verify_mtu_ipv6(geneve)
verify_address(geneve)
+ verify_bond_bridge_member(geneve)
if 'remote' not in geneve:
raise ConfigError('Remote side must be configured')
@@ -65,11 +76,12 @@ def generate(geneve):
def apply(geneve):
# Check if GENEVE interface already exists
- if geneve['ifname'] in interfaces():
- g = GeneveIf(geneve['ifname'])
- # GENEVE is super picky and the tunnel always needs to be recreated,
- # thus we can simply always delete it first.
- g.remove()
+ if 'rebuild_required' in geneve or 'delete' in geneve:
+ if geneve['ifname'] in interfaces():
+ g = GeneveIf(geneve['ifname'])
+ # GENEVE is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ g.remove()
if 'deleted' not in geneve:
# This is a special type of interface which needs additional parameters
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 9b6ddd5aa..c1ed8ec59 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -25,6 +25,7 @@ from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import L2TPv3If
from vyos.util import check_kmod
from vyos.validate import is_addr_assigned
@@ -76,6 +77,7 @@ def verify(l2tpv3):
verify_mtu_ipv6(l2tpv3)
verify_address(l2tpv3)
+ verify_bond_bridge_member(l2tpv3)
return None
def generate(l2tpv3):
diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
index bfebed7e4..8a969d90f 100755
--- a/src/conf_mode/interfaces-macsec.py
+++ b/src/conf_mode/interfaces-macsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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,15 +21,20 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
-from vyos.ifconfig import MACsecIf
-from vyos.ifconfig import Interface
-from vyos.template import render
-from vyos.util import call
+from vyos.configdict import is_node_changed
+from vyos.configdict import is_source_interface
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import MACsecIf
+from vyos.ifconfig import Interface
+from vyos.template import render
+from vyos.util import call
+from vyos.util import dict_search
+from vyos.util import is_systemd_service_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -54,6 +59,17 @@ def get_config(config=None):
source_interface = conf.return_effective_value(['source-interface'])
macsec.update({'source_interface': source_interface})
+ ifname = macsec['ifname']
+ if is_node_changed(conf, base + [ifname, 'security']):
+ macsec.update({'shutdown_required': {}})
+
+ if is_node_changed(conf, base + [ifname, 'source_interface']):
+ macsec.update({'shutdown_required': {}})
+
+ if 'source_interface' in macsec:
+ tmp = is_source_interface(conf, macsec['source_interface'], ['macsec', 'pseudo-ethernet'])
+ if tmp and tmp != ifname: macsec.update({'is_source_interface' : tmp})
+
return macsec
@@ -66,21 +82,24 @@ def verify(macsec):
verify_vrf(macsec)
verify_mtu_ipv6(macsec)
verify_address(macsec)
+ verify_bond_bridge_member(macsec)
+
+ if dict_search('security.cipher', macsec) == None:
+ raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))
+
+ if dict_search('security.encrypt', macsec) != None:
+ if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None:
+ raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!')
- if not (('security' in macsec) and
- ('cipher' in macsec['security'])):
- raise ConfigError(
- 'Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))
+ cak_len = len(dict_search('security.mka.cak', macsec))
- if (('security' in macsec) and
- ('encrypt' in macsec['security'])):
- tmp = macsec.get('security')
+ if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != 32:
+ # gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit
+ raise ConfigError('gcm-aes-128 requires a 128bit long key!')
- 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!')
+ elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != 64:
+ # gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit
+ raise ConfigError('gcm-aes-128 requires a 256bit long key!')
if 'source_interface' in macsec:
# MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad
@@ -101,35 +120,37 @@ def generate(macsec):
def apply(macsec):
- # Remove macsec interface
- if 'deleted' in macsec:
- call('systemctl stop wpa_supplicant-macsec@{source_interface}'
- .format(**macsec))
+ systemd_service = 'wpa_supplicant-macsec@{source_interface}'.format(**macsec)
+
+ # Remove macsec interface on deletion or mandatory parameter change
+ if 'deleted' in macsec or 'shutdown_required' in macsec:
+ call(f'systemctl stop {systemd_service}')
if macsec['ifname'] in interfaces():
tmp = MACsecIf(macsec['ifname'])
tmp.remove()
- # delete configuration on interface removal
- if os.path.isfile(wpa_suppl_conf.format(**macsec)):
- os.unlink(wpa_suppl_conf.format(**macsec))
-
- else:
- # This is a special type of interface which needs additional parameters
- # when created using iproute2. Instead of passing a ton of arguments,
- # use a dictionary provided by the interface class which holds all the
- # options necessary.
- conf = MACsecIf.get_config()
- conf['source_interface'] = macsec['source_interface']
- conf['security_cipher'] = macsec['security']['cipher']
-
- # It is safe to "re-create" the interface always, there is a sanity
- # check that the interface will only be create if its non existent
- i = MACsecIf(macsec['ifname'], **conf)
- i.update(macsec)
-
- call('systemctl restart wpa_supplicant-macsec@{source_interface}'
- .format(**macsec))
+ if 'deleted' in macsec:
+ # delete configuration on interface removal
+ if os.path.isfile(wpa_suppl_conf.format(**macsec)):
+ os.unlink(wpa_suppl_conf.format(**macsec))
+ return None
+
+ # This is a special type of interface which needs additional parameters
+ # when created using iproute2. Instead of passing a ton of arguments,
+ # use a dictionary provided by the interface class which holds all the
+ # options necessary.
+ conf = MACsecIf.get_config()
+ conf['source_interface'] = macsec['source_interface']
+ conf['security_cipher'] = macsec['security']['cipher']
+
+ # It is safe to "re-create" the interface always, there is a sanity
+ # check that the interface will only be create if its non existent
+ i = MACsecIf(macsec['ifname'], **conf)
+ i.update(macsec)
+
+ if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec:
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 38ed127ff..fe5898282 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -29,10 +29,11 @@ from shutil import rmtree
from vyos.config import Config
from vyos.configdict import get_interface_dict
-from vyos.configdict import leaf_node_changed
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_diffie_hellman_length
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import VTunIf
from vyos.template import render
from vyos.template import is_ipv4
@@ -42,6 +43,7 @@ from vyos.util import chown
from vyos.util import chmod_600
from vyos.util import cmd
from vyos.util import dict_search
+from vyos.util import is_list_equal
from vyos.util import makedir
from vyos.validate import is_addr_assigned
@@ -83,8 +85,8 @@ def get_config(config=None):
openvpn = get_interface_dict(conf, base)
if 'deleted' not in openvpn:
- tmp = leaf_node_changed(conf, ['openvpn-option'])
- if tmp: openvpn['restart_required'] = ''
+ if is_node_changed(conf, ['openvpn-option']):
+ openvpn.update({'restart_required': {}})
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
return openvpn
@@ -158,7 +160,7 @@ def verify(openvpn):
elif v6remAddr and not v6loAddr:
raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"')
- if (v4loAddr == v4remAddr) or (v6remAddr == v4remAddr):
+ if is_list_equal(v4loAddr, v4remAddr) or is_list_equal(v6loAddr, v6remAddr):
raise ConfigError('"local-address" and "remote-address" cannot be the same')
if dict_search('local_host', openvpn) in dict_search('local_address', openvpn):
@@ -425,6 +427,7 @@ def verify(openvpn):
raise ConfigError('Username for authentication is missing')
verify_vrf(openvpn)
+ verify_bond_bridge_member(openvpn)
return None
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index 4bd75f770..48e62a345 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -19,6 +19,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import leaf_node_changed
+from vyos.configdict import is_source_interface
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
@@ -49,6 +50,10 @@ def get_config(config=None):
if 'source_interface' in peth:
peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'],
peth['source_interface'])
+ # test if source-interface is maybe already used by another interface
+ tmp = is_source_interface(conf, peth['source_interface'], ['macsec'])
+ if tmp and tmp != peth['ifname']: peth.update({'is_source_interface' : tmp})
+
return peth
def verify(peth):
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index c4023586a..3bfc7d665 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -28,6 +28,7 @@ from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vrf
from vyos.configverify import verify_tunnel
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.ifconfig import TunnelIf
@@ -137,6 +138,7 @@ def verify(tunnel):
verify_mtu_ipv6(tunnel)
verify_address(tunnel)
verify_vrf(tunnel)
+ verify_bond_bridge_member(tunnel)
if 'source_interface' in tunnel:
verify_interface_exists(tunnel['source_interface'])
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 5ee4603af..bdf47eba0 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -26,6 +26,7 @@ from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import Interface
from vyos.ifconfig import VXLANIf
from vyos.template import is_ipv6
@@ -113,6 +114,8 @@ def verify(vxlan):
verify_mtu_ipv6(vxlan)
verify_address(vxlan)
+ verify_bond_bridge_member(vxlan)
+
return None
def generate(vxlan):
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 024ab8f59..b28aa9568 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -17,17 +17,16 @@
import os
from sys import exit
-from copy import deepcopy
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import get_interface_dict
-from vyos.configdict import node_changed
-from vyos.configdict import leaf_node_changed
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import WireGuardIf
from vyos.util import check_kmod
from vyos import ConfigError
@@ -51,13 +50,16 @@ def get_config(config=None):
# Determine which Wireguard peer has been removed.
# Peers can only be removed with their public key!
- dict = {}
- tmp = node_changed(conf, ['peer'], key_mangling=('-', '_'))
- for peer in (tmp or []):
- pubkey = leaf_node_changed(conf, ['peer', peer, 'pubkey'])
- if pubkey:
- dict = dict_merge({'peer_remove' : {peer : {'pubkey' : pubkey[0]}}}, dict)
- wireguard.update(dict)
+ if 'peer' in wireguard:
+ ifname = wireguard['ifname']
+ peer_remove = {}
+ for peer, peer_config in wireguard['peer'].items():
+ # T4702: If anything on a peer changes we remove the peer first and re-add it
+ if is_node_changed(conf, ['peer', peer]):
+ if 'pubkey' in peer_config:
+ peer_remove = dict_merge({'peer_remove' : {peer : peer_config['pubkey']}}, peer_remove)
+ if peer_remove:
+ wireguard.update(peer_remove)
return wireguard
@@ -69,6 +71,7 @@ def verify(wireguard):
verify_mtu_ipv6(wireguard)
verify_address(wireguard)
verify_vrf(wireguard)
+ verify_bond_bridge_member(wireguard)
if not os.path.exists(wireguard['private_key']):
raise ConfigError('Wireguard private-key not found! Execute: ' \
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index b25fcd4e0..7e967ed79 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -29,6 +29,7 @@ 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.configverify import verify_bond_bridge_member
from vyos.ifconfig import WiFiIf
from vyos.template import render
from vyos.util import call
@@ -188,6 +189,7 @@ def verify(wifi):
verify_address(wifi)
verify_vrf(wifi)
+ verify_bond_bridge_member(wifi)
# use common function to verify VLAN configuration
verify_vlan_config(wifi)
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index a4b033374..179d1efb4 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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,6 +21,7 @@ from time import sleep
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_authentication
from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_vrf
@@ -36,7 +37,7 @@ from vyos import airbag
airbag.enable()
service_name = 'ModemManager.service'
-cron_script = '/etc/cron.d/wwan'
+cron_script = '/etc/cron.d/vyos-wwan'
def get_config(config=None):
"""
@@ -50,6 +51,30 @@ def get_config(config=None):
base = ['interfaces', 'wwan']
wwan = get_interface_dict(conf, base)
+ # We should only terminate the WWAN session if critical parameters change.
+ # All parameters that can be changed on-the-fly (like interface description)
+ # should not lead to a reconnect!
+ tmp = is_node_changed(conf, ['address'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = is_node_changed(conf, ['apn'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = is_node_changed(conf, ['disable'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = is_node_changed(conf, ['vrf'])
+ if tmp: wwan.update({'vrf_old': {}})
+
+ tmp = is_node_changed(conf, ['authentication', 'user'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = is_node_changed(conf, ['authentication', 'password'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = is_node_changed(conf, ['ipv6', 'address', 'autoconf'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
# We need to know the amount of other WWAN interfaces as ModemManager needs
# to be started or stopped.
conf.set_level(base)
@@ -57,8 +82,8 @@ def get_config(config=None):
get_first_key=True,
no_tag_node_value_mangle=True)
- # This if-clause is just to be sure - it will always evaluate to true
ifname = wwan['ifname']
+ # This if-clause is just to be sure - it will always evaluate to true
if ifname in wwan['other_interfaces']:
del wwan['other_interfaces'][ifname]
if len(wwan['other_interfaces']) == 0:
@@ -82,13 +107,25 @@ def verify(wwan):
def generate(wwan):
if 'deleted' in wwan:
+ # We are the last WWAN interface - there are no other ones remaining
+ # thus the cronjob needs to go away, too
+ if 'other_interfaces' not in wwan:
+ if os.path.exists(cron_script):
+ os.unlink(cron_script)
return None
+ # Install cron triggered helper script to re-dial WWAN interfaces on
+ # disconnect - e.g. happens during RF signal loss. The script watches every
+ # WWAN interface - so there is only one instance.
if not os.path.exists(cron_script):
write_file(cron_script, '*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py')
+
return None
def apply(wwan):
+ # ModemManager is required to dial WWAN connections - one instance is
+ # required to serve all modems. Activate ModemManager on first invocation
+ # of any WWAN interface.
if not is_systemd_service_active(service_name):
cmd(f'systemctl start {service_name}')
@@ -101,17 +138,19 @@ def apply(wwan):
break
sleep(0.250)
- # we only need the modem number. wwan0 -> 0, wwan1 -> 1
- modem = wwan['ifname'].lstrip('wwan')
- base_cmd = f'mmcli --modem {modem}'
- # Number of bearers is limited - always disconnect first
- cmd(f'{base_cmd} --simple-disconnect')
+ if 'shutdown_required' in wwan:
+ # we only need the modem number. wwan0 -> 0, wwan1 -> 1
+ modem = wwan['ifname'].lstrip('wwan')
+ base_cmd = f'mmcli --modem {modem}'
+ # Number of bearers is limited - always disconnect first
+ cmd(f'{base_cmd} --simple-disconnect')
w = WWANIf(wwan['ifname'])
if 'deleted' in wwan or 'disable' in wwan:
w.remove()
- # There are no other WWAN interfaces - stop the daemon
+ # We are the last WWAN interface - there are no other WWAN interfaces
+ # remaining, thus we can stop ModemManager and free resources.
if 'other_interfaces' not in wwan:
cmd(f'systemctl stop {service_name}')
# Clean CRON helper script which is used for to re-connect when
@@ -121,27 +160,25 @@ def apply(wwan):
return None
- ip_type = 'ipv4'
- slaac = dict_search('ipv6.address.autoconf', wwan) != None
- if 'address' in wwan:
- if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac):
- ip_type = 'ipv4v6'
- elif 'dhcpv6' in wwan['address'] or slaac:
- ip_type = 'ipv6'
- elif 'dhcp' in wwan['address']:
- ip_type = 'ipv4'
-
- options = f'ip-type={ip_type},apn=' + wwan['apn']
- if 'authentication' in wwan:
- options += ',user={user},password={password}'.format(**wwan['authentication'])
-
- command = f'{base_cmd} --simple-connect="{options}"'
- call(command, stdout=DEVNULL)
- w.update(wwan)
+ if 'shutdown_required' in wwan:
+ ip_type = 'ipv4'
+ slaac = dict_search('ipv6.address.autoconf', wwan) != None
+ if 'address' in wwan:
+ if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac):
+ ip_type = 'ipv4v6'
+ elif 'dhcpv6' in wwan['address'] or slaac:
+ ip_type = 'ipv6'
+ elif 'dhcp' in wwan['address']:
+ ip_type = 'ipv4'
- if 'other_interfaces' not in wwan and 'deleted' in wwan:
- cmd(f'systemctl start {service_name}')
+ options = f'ip-type={ip_type},apn=' + wwan['apn']
+ if 'authentication' in wwan:
+ options += ',user={user},password={password}'.format(**wwan['authentication'])
+ command = f'{base_cmd} --simple-connect="{options}"'
+ call(command, stdout=DEVNULL)
+
+ w.update(wwan)
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index 52070aabc..181c9e322 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 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
@@ -18,9 +18,11 @@ import os
from vyos.config import Config
from vyos.configverify import verify_vrf
-from vyos import ConfigError
+from vyos.configverify import verify_interface_exists
from vyos.util import call
+from vyos.util import get_interface_config
from vyos.template import render
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -49,6 +51,20 @@ def verify(ntp):
raise ConfigError('NTP server not configured')
verify_vrf(ntp)
+
+ if 'interface' in ntp:
+ # If ntpd should listen on a given interface, ensure it exists
+ for interface in ntp['interface']:
+ verify_interface_exists(interface)
+
+ # If we run in a VRF, our interface must belong to this VRF, too
+ if 'vrf' in ntp:
+ tmp = get_interface_config(interface)
+ vrf_name = ntp['vrf']
+ if 'master' not in tmp or tmp['master'] != vrf_name:
+ raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\
+ f'does not belong to this VRF!')
+
return None
def generate(ntp):
diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py
index 841bf6a39..5a8528741 100755
--- a/src/conf_mode/salt-minion.py
+++ b/src/conf_mode/salt-minion.py
@@ -39,7 +39,7 @@ default_config_data = {
'user': 'minion',
'group': 'vyattacfg',
'salt_id': gethostname(),
- 'mine_interval': '60',
+ 'interval': '60',
'verify_master_pubkey_sign': 'false',
'master_key': ''
}
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index f676fdbbe..42cfd0f12 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 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
@@ -41,6 +41,7 @@ default_config_data = {
'interfaces': [],
'dnsv4': [],
'dnsv6': [],
+ 'client_named_ip_pool': [],
'client_ipv6_pool': [],
'client_ipv6_delegate_prefix': [],
'radius_server': [],
@@ -52,6 +53,8 @@ default_config_data = {
'radius_nas_ip': '',
'radius_source_address': '',
'radius_shaper_attr': '',
+ 'radius_shaper_enable': False,
+ 'radius_shaper_multiplier': '',
'radius_shaper_vendor': '',
'radius_dynamic_author': '',
'thread_cnt': get_half_cpus()
@@ -195,6 +198,18 @@ def get_config(config=None):
if conf.exists(['nas-ip-address']):
ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+ if conf.exists(['rate-limit', 'attribute']):
+ ipoe['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute'])
+
+ if conf.exists(['rate-limit', 'enable']):
+ ipoe['radius_shaper_enable'] = True
+
+ if conf.exists(['rate-limit', 'multiplier']):
+ ipoe['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier'])
+
+ if conf.exists(['rate-limit', 'vendor']):
+ ipoe['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor'])
+
if conf.exists(['source-address']):
ipoe['radius_source_address'] = conf.return_value(['source-address'])
@@ -219,6 +234,22 @@ def get_config(config=None):
conf.set_level(base_path)
+ # Named client-ip-pool
+ if conf.exists(['client-ip-pool', 'name']):
+ for name in conf.list_nodes(['client-ip-pool', 'name']):
+ tmp = {
+ 'name': name,
+ 'gateway_address': '',
+ 'subnet': ''
+ }
+
+ if conf.exists(['client-ip-pool', 'name', name, 'gateway-address']):
+ tmp['gateway_address'] += conf.return_value(['client-ip-pool', 'name', name, 'gateway-address'])
+ if conf.exists(['client-ip-pool', 'name', name, 'subnet']):
+ tmp['subnet'] += conf.return_value(['client-ip-pool', 'name', name, 'subnet'])
+
+ ipoe['client_named_ip_pool'].append(tmp)
+
if conf.exists(['client-ipv6-pool', 'prefix']):
for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']):
tmp = {
@@ -254,10 +285,6 @@ def verify(ipoe):
if not ipoe['interfaces']:
raise ConfigError('No IPoE interface configured')
- for interface in ipoe['interfaces']:
- if not interface['range']:
- raise ConfigError(f'No IPoE client subnet defined on interface "{ interface }"')
-
if len(ipoe['dnsv4']) > 2:
raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
index 8a972b9fe..a71565df4 100755
--- a/src/conf_mode/service_monitoring_telegraf.py
+++ b/src/conf_mode/service_monitoring_telegraf.py
@@ -99,6 +99,15 @@ def get_config(config=None):
monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False)
monitoring['nft_chains'] = get_nft_filter_chains()
+ if 'authentication' in monitoring or \
+ 'url' in monitoring:
+ monitoring['influxdb_configured'] = True
+
+ # Ignore default XML values if config doesn't exists
+ # Delete key from dict
+ if not conf.exists(base + ['prometheus-client']):
+ del monitoring['prometheus_client']
+
return monitoring
def verify(monitoring):
@@ -106,13 +115,14 @@ def verify(monitoring):
if not monitoring:
return None
- if 'authentication' not in monitoring or \
- 'organization' not in monitoring['authentication'] or \
- 'token' not in monitoring['authentication']:
- raise ConfigError(f'Authentication "organization and token" are mandatory!')
+ if 'influxdb_configured' in monitoring:
+ if 'authentication' not in monitoring or \
+ 'organization' not in monitoring['authentication'] or \
+ 'token' not in monitoring['authentication']:
+ raise ConfigError(f'Authentication "organization and token" are mandatory!')
- if 'url' not in monitoring:
- raise ConfigError(f'Monitoring "url" is mandatory!')
+ if 'url' not in monitoring:
+ raise ConfigError(f'Monitoring "url" is mandatory!')
return None
diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py
index 9afcdd63e..367567bf6 100755
--- a/src/conf_mode/service_router-advert.py
+++ b/src/conf_mode/service_router-advert.py
@@ -92,8 +92,8 @@ def verify(rtradv):
if preferred_lifetime == 'infinity':
preferred_lifetime = 4294967295
- if not (int(valid_lifetime) > int(preferred_lifetime)):
- raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime')
+ if not (int(valid_lifetime) >= int(preferred_lifetime)):
+ raise ConfigError('Prefix valid-lifetime must be greater then or equal to preferred-lifetime')
return None
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 6d22d06f1..25ff9d0dd 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 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
@@ -56,6 +56,7 @@ default_config_data = {
'communities': [],
'smux_peers': [],
'location' : '',
+ 'protocol' : 'udp',
'description' : '',
'contact' : '',
'trap_source': '',
@@ -154,6 +155,9 @@ def get_config():
if conf.exists('location'):
snmp['location'] = conf.return_value('location')
+ if conf.exists('protocol'):
+ snmp['protocol'] = conf.return_value('protocol')
+
if conf.exists('smux-peer'):
snmp['smux_peers'] = conf.return_values('smux-peer')
@@ -404,14 +408,15 @@ def verify(snmp):
for listen in snmp['listen_address']:
addr = listen[0]
port = listen[1]
+ protocol = snmp['protocol']
tmp = None
if is_ipv4(addr):
# example: udp:127.0.0.1:161
- tmp = f'udp:{addr}:{port}'
+ tmp = f'{protocol}:{addr}:{port}'
elif snmp['ipv6_enabled']:
# example: udp6:[::1]:161
- tmp = f'udp6:[{addr}]:{port}'
+ tmp = f'{protocol}6:[{addr}]:{port}'
# We only wan't to configure addresses that exist on the system.
# Hint the user if they don't exist
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index 7fb2dd1cf..e6bcc12ad 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -17,6 +17,7 @@
import os
from sys import exit
+from vyos.base import DeprecationWarning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import leaf_node_changed
@@ -49,6 +50,9 @@ def get_config(config=None):
return opt
def verify(opt):
+ if 'disable' in opt:
+ DeprecationWarning('VyOS 1.4 (sagitta) will remove the CLI command to '\
+ 'disable IPv6 address family in the Linux Kernel!')
pass
def generate(opt):
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 8aa43dd32..aba10689d 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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
@@ -23,6 +23,7 @@ from pwd import getpwall
from pwd import getpwnam
from spwd import getspnam
from sys import exit
+from time import sleep
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -31,6 +32,7 @@ from vyos.template import render
from vyos.template import is_ipv4
from vyos.util import cmd
from vyos.util import call
+from vyos.util import run
from vyos.util import DEVNULL
from vyos.util import dict_search
from vyos.xml import defaults
@@ -256,13 +258,22 @@ def apply(login):
if 'rm_users' in login:
for user in login['rm_users']:
try:
+ # Disable user to prevent re-login
+ call(f'usermod -s /sbin/nologin {user}')
+
# Logout user if he is still logged in
if user in list(set([tmp[0] for tmp in users()])):
print(f'{user} is logged in, forcing logout!')
- call(f'pkill -HUP -u {user}')
-
- # Remove user account but leave home directory to be safe
- call(f'userdel -r {user}', stderr=DEVNULL)
+ # re-run command until user is logged out
+ while run(f'pkill -HUP -u {user}'):
+ sleep(0.250)
+
+ # Remove user account but leave home directory in place. Re-run
+ # command until user is removed - userdel might return 8 as
+ # SSH sessions are not all yet properly cleaned away, thus we
+ # simply re-run the command until the account wen't away
+ while run(f'userdel --remove {user}', stderr=DEVNULL):
+ sleep(0.250)
except Exception as e:
raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py
index 00b96884b..f24d5b618 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 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
@@ -61,8 +61,8 @@ def verify(ocserv):
if "authentication" in ocserv:
if "mode" in ocserv["authentication"]:
if "local" in ocserv["authentication"]["mode"]:
- if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]:
- raise ConfigError('openconnect mode local required at leat one user')
+ if 'local_users' not in ocserv["authentication"] or 'username' not in ocserv["authentication"]["local_users"]:
+ raise ConfigError('openconnect mode local requires at leat one user')
else:
for user in ocserv["authentication"]["local_users"]["username"]:
if not "password" in ocserv["authentication"]["local_users"]["username"][user]:
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index dd1739087..def4cc70d 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -27,6 +27,7 @@ from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search
from vyos.util import sysctl_write
+from vyos.util import is_ipv6_enabled
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -107,8 +108,14 @@ def verify(vrf):
f'static routes installed!')
if 'name' in vrf:
+ reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", "get", "inet", "mtu", "link", "type",
+ "vrf"]
table_ids = []
for name, config in vrf['name'].items():
+ # Reserved VRF names
+ if name in reserved_names:
+ raise ConfigError(f'VRF name "{name}" is reserved and connot be used!')
+
# table id is mandatory
if 'table' not in config:
raise ConfigError(f'VRF "{name}" table id is mandatory!')
@@ -194,10 +201,11 @@ def apply(vrf):
# set VRF description for e.g. SNMP monitoring
vrf_if = Interface(name)
- # We also should add proper loopback IP addresses to the newly
- # created VRFs for services bound to the loopback address (SNMP, NTP)
+ # We also should add proper loopback IP addresses to the newly added
+ # VRF for services bound to the loopback address (SNMP, NTP)
vrf_if.add_addr('127.0.0.1/8')
- vrf_if.add_addr('::1/128')
+ if is_ipv6_enabled():
+ vrf_if.add_addr('::1/128')
# add VRF description if available
vrf_if.set_alias(config.get('description', ''))
diff --git a/src/etc/cron.d/check-wwan b/src/etc/cron.d/check-wwan
deleted file mode 100644
index 28190776f..000000000
--- a/src/etc/cron.d/check-wwan
+++ /dev/null
@@ -1 +0,0 @@
-*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
index fc035766b..2bd2d1d22 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
@@ -23,7 +23,7 @@ function iptovtysh () {
local VTYSH_GATEWAY=""
local VTYSH_DEV=""
local VTYSH_TAG="210"
- local VTYSH_DISTANCE=""
+ local VTYSH_DISTANCE=$IF_METRIC
# convert default route to 0.0.0.0/0
if [ "$4" == "default" ] ; then
VTYSH_NETADDR="0.0.0.0/0"
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
index b1902b585..518abeaec 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
@@ -33,8 +33,8 @@ if /usr/bin/systemctl -q is-active vyos-hostsd; then
if [ -n "$new_dhcp6_name_servers" ]; then
logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
$hostsd_client --delete-name-servers --tag "dhcpv6-$interface"
- logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
- $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface"
+ logmsg info "Adding nameservers \"$new_dhcp6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --add-name-servers $new_dhcp6_name_servers --tag "dhcpv6-$interface"
hostsd_changes=y
fi
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
index eac860cd8..378f74741 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -8,7 +8,7 @@ hostsd_changes=
/usr/bin/systemctl -q is-active vyos-hostsd
hostsd_status=$?
-if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
+if [[ $reason =~ ^(EXPIRE|FAIL|RELEASE|STOP)$ ]]; then
if [[ $hostsd_status -eq 0 ]]; then
# delete search domains and nameservers via vyos-hostsd
logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client"
@@ -96,7 +96,7 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
fi
fi
-if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then
+if [[ $reason =~ ^(EXPIRE6|RELEASE6|STOP6)$ ]]; then
if [[ $hostsd_status -eq 0 ]]; then
# delete search domains and nameservers via vyos-hostsd
logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook
index eeb8b0782..49bb18372 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook
@@ -8,12 +8,12 @@
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
-#
+#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
-#
+#
# This code was originally developed by Vyatta, Inc.
# Portions created by Vyatta are Copyright (C) 2006, 2007, 2008 Vyatta, Inc.
# All Rights Reserved.
@@ -23,7 +23,7 @@
RUN="yes"
proto=""
-if [[ $reason =~ (REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6) ]]; then
+if [[ $reason =~ ^(REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6)$ ]]; then
proto="v6"
fi
diff --git a/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers
new file mode 100755
index 000000000..5157469f4
--- /dev/null
+++ b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+interface=$6
+if [ -z "$interface" ]; then
+ exit
+fi
+
+if ! /usr/bin/systemctl -q is-active vyos-hostsd; then
+ exit # vyos-hostsd is not running
+fi
+
+hostsd_client="/usr/bin/vyos-hostsd-client"
+$hostsd_client --delete-name-servers --tag "dhcp-$interface"
+$hostsd_client --apply
diff --git a/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers
new file mode 100755
index 000000000..4affaeb5c
--- /dev/null
+++ b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+interface=$6
+if [ -z "$interface" ]; then
+ exit
+fi
+
+if ! /usr/bin/systemctl -q is-active vyos-hostsd; then
+ exit # vyos-hostsd is not running
+fi
+
+hostsd_client="/usr/bin/vyos-hostsd-client"
+
+$hostsd_client --delete-name-servers --tag "dhcp-$interface"
+
+if [ "$USEPEERDNS" ] && [ -n "$DNS1" ]; then
+$hostsd_client --add-name-servers "$DNS1" --tag "dhcp-$interface"
+fi
+if [ "$USEPEERDNS" ] && [ -n "$DNS2" ]; then
+$hostsd_client --add-name-servers "$DNS2" --tag "dhcp-$interface"
+fi
+
+$hostsd_client --apply
diff --git a/src/etc/systemd/system/wpa_supplicant-wired@.service.d/override.conf b/src/etc/systemd/system/wpa_supplicant-wired@.service.d/override.conf
new file mode 100644
index 000000000..030b89a2b
--- /dev/null
+++ b/src/etc/systemd/system/wpa_supplicant-wired@.service.d/override.conf
@@ -0,0 +1,11 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=
+WorkingDirectory=/run/wpa_supplicant
+PIDFile=/run/wpa_supplicant/%I.pid
+ExecStart=
+ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dwired -P/run/wpa_supplicant/%I.pid -i%I
+ExecReload=/bin/kill -HUP $MAINPID
diff --git a/src/helpers/strip-private.py b/src/helpers/strip-private.py
index e4e1fe11d..eb584edaf 100755
--- a/src/helpers/strip-private.py
+++ b/src/helpers/strip-private.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2021-2022 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
@@ -111,6 +111,10 @@ if __name__ == "__main__":
(True, re.compile(r'public-keys \S+'), 'public-keys xxxx@xxx.xxx'),
(True, re.compile(r'type \'ssh-(rsa|dss)\''), 'type ssh-xxx'),
(True, re.compile(r' key \S+'), ' key xxxxxx'),
+ # Strip bucket
+ (True, re.compile(r' bucket \S+'), ' bucket xxxxxx'),
+ # Strip tokens
+ (True, re.compile(r' token \S+'), ' token xxxxxx'),
# Strip OpenVPN secrets
(True, re.compile(r'(shared-secret-key-file|ca-cert-file|cert-file|dh-file|key-file|client) (\S+)'), r'\1 xxxxxx'),
# Strip IPSEC secrets
@@ -123,8 +127,8 @@ if __name__ == "__main__":
# Strip MAC addresses
(args.mac, re.compile(r'([0-9a-fA-F]{2}\:){5}([0-9a-fA-F]{2}((\:{0,1})){3})'), r'xx:xx:xx:xx:xx:\2'),
- # Strip host-name, domain-name, and domain-search
- (args.hostname, re.compile(r'(host-name|domain-name|domain-search) \S+'), r'\1 xxxxxx'),
+ # Strip host-name, domain-name, domain-search and url
+ (args.hostname, re.compile(r'(host-name|domain-name|domain-search|url) \S+'), r'\1 xxxxxx'),
# Strip user-names
(args.username, re.compile(r'(user|username|user-id) \S+'), r'\1 xxxxxx'),
diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py
new file mode 100755
index 000000000..6ac3d4c1c
--- /dev/null
+++ b/src/op_mode/clear_dhcp_lease.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+
+import argparse
+import re
+
+from isc_dhcp_leases import Lease
+from isc_dhcp_leases import IscDhcpLeases
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import ask_yes_no
+from vyos.util import call
+
+
+config = ConfigTreeQuery()
+base = ['service', 'dhcp-server']
+lease_file = '/config/dhcpd.leases'
+
+
+def del_lease_ip(address):
+ """
+ Read lease_file and write data to this file
+ without specific section "lease ip"
+ Delete section "lease x.x.x.x { x;x;x; }"
+ """
+ with open(lease_file, encoding='utf-8') as f:
+ data = f.read().rstrip()
+ lease_config_ip = '{(?P<config>[\s\S]+?)\n}'
+ pattern = rf"lease {address} {lease_config_ip}"
+ # Delete lease for ip block
+ data = re.sub(pattern, '', data)
+
+ # Write new data to original lease_file
+ with open(lease_file, 'w', encoding='utf-8') as f:
+ f.write(data)
+
+def is_ip_in_leases(address):
+ """
+ Return True if address found in the lease file
+ """
+ leases = IscDhcpLeases(lease_file)
+ lease_ips = []
+ for lease in leases.get():
+ lease_ips.append(lease.ip)
+ if address not in lease_ips:
+ print(f'Address "{address}" not found in "{lease_file}"')
+ return False
+ return True
+
+
+if not config.exists(base):
+ print('DHCP-server not configured!')
+ exit(0)
+
+if config.exists(base + ['failover']):
+ print('Lease cannot be reset in failover mode!')
+ exit(0)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--ip', help='IPv4 address', action='store', required=True)
+
+ args = parser.parse_args()
+ address = args.ip
+
+ if not is_ip_in_leases(address):
+ exit(1)
+
+ if not ask_yes_no(f'This will restart DHCP server.\nContinue?'):
+ exit(1)
+ else:
+ del_lease_ip(address)
+ call('systemctl restart isc-dhcp-server.service')
diff --git a/src/op_mode/generate_public_key_command.py b/src/op_mode/generate_public_key_command.py
index 7a7b6c923..f071ae350 100755
--- a/src/op_mode/generate_public_key_command.py
+++ b/src/op_mode/generate_public_key_command.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -29,8 +29,12 @@ def get_key(path):
key_string = vyos.remote.get_remote_config(path)
return key_string.split()
-username = sys.argv[1]
-algorithm, key, identifier = get_key(sys.argv[2])
+try:
+ username = sys.argv[1]
+ algorithm, key, identifier = get_key(sys.argv[2])
+except Exception as e:
+ print("Failed to retrieve the public key: {}".format(e))
+ sys.exit(1)
print('# To add this key as an embedded key, run the following commands:')
print('configure')
@@ -39,3 +43,4 @@ print(f'set system login user {username} authentication public-keys {identifier}
print('commit')
print('save')
print('exit')
+
diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py
index 109c8dd7b..e5014452f 100755
--- a/src/op_mode/restart_frr.py
+++ b/src/op_mode/restart_frr.py
@@ -138,7 +138,7 @@ def _reload_config(daemon):
# define program arguments
cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons')
cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons')
-cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons')
+cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ldpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons')
# parse arguments
cmd_args = cmd_args_parser.parse_args()
diff --git a/src/op_mode/show_ipsec_connections.py b/src/op_mode/show_ipsec_connections.py
new file mode 100755
index 000000000..4ca8f8e51
--- /dev/null
+++ b/src/op_mode/show_ipsec_connections.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+
+from vyos.util import convert_data
+
+
+def _get_vici_sas():
+ from vici import Session as vici_session
+
+ try:
+ session = vici_session()
+ except PermissionError:
+ print("You do not have a permission to connect to the IPsec daemon")
+ sys.exit(1)
+ except ConnectionRefusedError:
+ print("IPsec is not runing")
+ sys.exit(1)
+ except Exception as e:
+ print("An error occured: {0}".format(e))
+ sys.exit(1)
+ sas = list(session.list_sas())
+ return convert_data(sas)
+
+
+def _get_vici_connections():
+ from vici import Session as vici_session
+
+ try:
+ session = vici_session()
+ except PermissionError:
+ print("You do not have a permission to connect to the IPsec daemon")
+ sys.exit(1)
+ except ConnectionRefusedError:
+ print("IPsec is not runing")
+ sys.exit(1)
+ except Exception as e:
+ print("An error occured: {0}".format(e))
+ sys.exit(1)
+ connections = list(session.list_conns())
+ return convert_data(connections)
+
+
+def _get_parent_sa_proposal(connection_name: str, data: list) -> dict:
+ """Get parent SA proposals by connection name
+ if connections not in the 'down' state
+ Args:
+ connection_name (str): Connection name
+ data (list): List of current SAs from vici
+ Returns:
+ str: Parent SA connection proposal
+ AES_CBC/256/HMAC_SHA2_256_128/MODP_1024
+ """
+ if not data:
+ return {}
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return {}
+ if 'encr-alg' in sa[connection_name]:
+ encr_alg = sa.get(connection_name, '').get('encr-alg')
+ cipher = encr_alg.split('_')[0]
+ mode = encr_alg.split('_')[1]
+ encr_keysize = sa.get(connection_name, '').get('encr-keysize')
+ integ_alg = sa.get(connection_name, '').get('integ-alg')
+ # prf_alg = sa.get(connection_name, '').get('prf-alg')
+ dh_group = sa.get(connection_name, '').get('dh-group')
+ proposal = {
+ 'cipher': cipher,
+ 'mode': mode,
+ 'key_size': encr_keysize,
+ 'hash': integ_alg,
+ 'dh': dh_group
+ }
+ return proposal
+ return {}
+
+
+def _get_parent_sa_state(connection_name: str, data: list) -> str:
+ """Get parent SA state by connection name
+ Args:
+ connection_name (str): Connection name
+ data (list): List of current SAs from vici
+ Returns:
+ Parent SA connection state
+ """
+ if not data:
+ return 'down'
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return 'down'
+ if sa[connection_name]['state'].lower() == 'established':
+ return 'up'
+ else:
+ return 'down'
+
+
+def _get_child_sa_state(connection_name: str, tunnel_name: str,
+ data: list) -> str:
+ """Get child SA state by connection and tunnel name
+ Args:
+ connection_name (str): Connection name
+ tunnel_name (str): Tunnel name
+ data (list): List of current SAs from vici
+ Returns:
+ str: `up` if child SA state is 'installed' otherwise `down`
+ """
+ if not data:
+ return 'down'
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return 'down'
+ child_sas = sa[connection_name]['child-sas']
+ # Get all child SA states
+ # there can be multiple SAs per tunnel
+ child_sa_states = [
+ v['state'] for k, v in child_sas.items() if v['name'] == tunnel_name
+ ]
+ return 'up' if 'INSTALLED' in child_sa_states else 'down'
+
+
+def _get_child_sa_info(connection_name: str, tunnel_name: str,
+ data: list) -> dict:
+ """Get child SA installed info by connection and tunnel name
+ Args:
+ connection_name (str): Connection name
+ tunnel_name (str): Tunnel name
+ data (list): List of current SAs from vici
+ Returns:
+ dict: Info of the child SA in the dictionary format
+ """
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return {}
+ child_sas = sa[connection_name]['child-sas']
+ # Get all child SA data
+ # Skip temp SA name (first key), get only SA values as dict
+ # {'OFFICE-B-tunnel-0-46': {'name': 'OFFICE-B-tunnel-0'}...}
+ # i.e get all data after 'OFFICE-B-tunnel-0-46'
+ child_sa_info = [
+ v for k, v in child_sas.items() if 'name' in v and
+ v['name'] == tunnel_name and v['state'] == 'INSTALLED'
+ ]
+ return child_sa_info[-1] if child_sa_info else {}
+
+
+def _get_child_sa_proposal(child_sa_data: dict) -> dict:
+ if child_sa_data and 'encr-alg' in child_sa_data:
+ encr_alg = child_sa_data.get('encr-alg')
+ cipher = encr_alg.split('_')[0]
+ mode = encr_alg.split('_')[1]
+ key_size = child_sa_data.get('encr-keysize')
+ integ_alg = child_sa_data.get('integ-alg')
+ dh_group = child_sa_data.get('dh-group')
+ proposal = {
+ 'cipher': cipher,
+ 'mode': mode,
+ 'key_size': key_size,
+ 'hash': integ_alg,
+ 'dh': dh_group
+ }
+ return proposal
+ return {}
+
+
+def _get_raw_data_connections(list_connections: list, list_sas: list) -> list:
+ """Get configured VPN IKE connections and IPsec states
+ Args:
+ list_connections (list): List of configured connections from vici
+ list_sas (list): List of current SAs from vici
+ Returns:
+ list: List and status of IKE/IPsec connections/tunnels
+ """
+ base_dict = []
+ for connections in list_connections:
+ base_list = {}
+ for connection, conn_conf in connections.items():
+ base_list['ike_connection_name'] = connection
+ base_list['ike_connection_state'] = _get_parent_sa_state(
+ connection, list_sas)
+ base_list['ike_remote_address'] = conn_conf['remote_addrs']
+ base_list['ike_proposal'] = _get_parent_sa_proposal(
+ connection, list_sas)
+ base_list['local_id'] = conn_conf.get('local-1', '').get('id')
+ base_list['remote_id'] = conn_conf.get('remote-1', '').get('id')
+ base_list['version'] = conn_conf.get('version', 'IKE')
+ base_list['children'] = []
+ children = conn_conf['children']
+ for tunnel, tun_options in children.items():
+ state = _get_child_sa_state(connection, tunnel, list_sas)
+ local_ts = tun_options.get('local-ts')
+ remote_ts = tun_options.get('remote-ts')
+ dpd_action = tun_options.get('dpd_action')
+ close_action = tun_options.get('close_action')
+ sa_info = _get_child_sa_info(connection, tunnel, list_sas)
+ esp_proposal = _get_child_sa_proposal(sa_info)
+ base_list['children'].append({
+ 'name': tunnel,
+ 'state': state,
+ 'local_ts': local_ts,
+ 'remote_ts': remote_ts,
+ 'dpd_action': dpd_action,
+ 'close_action': close_action,
+ 'esp_proposal': esp_proposal
+ })
+ base_dict.append(base_list)
+ return base_dict
+
+
+def _get_formatted_output_conections(data):
+ from tabulate import tabulate
+ data_entries = ''
+ connections = []
+ for entry in data:
+ tunnels = []
+ ike_name = entry['ike_connection_name']
+ ike_state = entry['ike_connection_state']
+ conn_type = entry.get('version', 'IKE')
+ remote_addrs = ','.join(entry['ike_remote_address'])
+ local_ts, remote_ts = '-', '-'
+ local_id = entry['local_id']
+ remote_id = entry['remote_id']
+ proposal = '-'
+ if entry.get('ike_proposal'):
+ proposal = (f'{entry["ike_proposal"]["cipher"]}_'
+ f'{entry["ike_proposal"]["mode"]}/'
+ f'{entry["ike_proposal"]["key_size"]}/'
+ f'{entry["ike_proposal"]["hash"]}/'
+ f'{entry["ike_proposal"]["dh"]}')
+ connections.append([
+ ike_name, ike_state, conn_type, remote_addrs, local_ts, remote_ts,
+ local_id, remote_id, proposal
+ ])
+ for tun in entry['children']:
+ tun_name = tun.get('name')
+ tun_state = tun.get('state')
+ conn_type = 'IPsec'
+ local_ts = '\n'.join(tun.get('local_ts'))
+ remote_ts = '\n'.join(tun.get('remote_ts'))
+ proposal = '-'
+ if tun.get('esp_proposal'):
+ proposal = (f'{tun["esp_proposal"]["cipher"]}_'
+ f'{tun["esp_proposal"]["mode"]}/'
+ f'{tun["esp_proposal"]["key_size"]}/'
+ f'{tun["esp_proposal"]["hash"]}/'
+ f'{tun["esp_proposal"]["dh"]}')
+ connections.append([
+ tun_name, tun_state, conn_type, remote_addrs, local_ts,
+ remote_ts, local_id, remote_id, proposal
+ ])
+ connection_headers = [
+ 'Connection', 'State', 'Type', 'Remote address', 'Local TS',
+ 'Remote TS', 'Local id', 'Remote id', 'Proposal'
+ ]
+ output = tabulate(connections, connection_headers, numalign='left')
+ return output
+
+
+def main():
+ list_conns = _get_vici_connections()
+ list_sas = _get_vici_sas()
+ connections = _get_raw_data_connections(list_conns, list_sas)
+ return _get_formatted_output_conections(connections)
+
+
+if __name__ == '__main__':
+ print(main())
diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py
new file mode 100755
index 000000000..0c9e83112
--- /dev/null
+++ b/src/op_mode/vpn_ipsec.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+
+from vyos.util import call
+
+
+def debug_peer(peer, tunnel):
+ if not peer or peer == "all":
+ debug_commands = [
+ "sudo ipsec statusall",
+ "sudo swanctl -L",
+ "sudo swanctl -l",
+ "sudo swanctl -P",
+ "sudo ip x sa show",
+ "sudo ip x policy show",
+ "sudo ip tunnel show",
+ "sudo ip address",
+ "sudo ip rule show",
+ "sudo ip route | head -100",
+ "sudo ip route show table 220"
+ ]
+ for debug_cmd in debug_commands:
+ print(f'\n### {debug_cmd} ###')
+ call(debug_cmd)
+ return
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--action', help='Control action', required=True)
+ parser.add_argument('--name', help='Name for peer reset', required=False)
+ parser.add_argument('--tunnel', help='Specific tunnel of peer', required=False)
+
+ args = parser.parse_args()
+
+ if args.action == "vpn-debug":
+ debug_peer(args.name, args.tunnel)
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
index 4c4bb036e..3cbd31331 100755
--- a/src/services/vyos-hostsd
+++ b/src/services/vyos-hostsd
@@ -317,7 +317,7 @@ hosts_add_schema = op_type_schema.extend({
'data': {
str: {
str: {
- 'address': str,
+ 'address': [str],
'aliases': [str]
}
}
@@ -378,8 +378,7 @@ def validate_schema(data):
def pdns_rec_control(command):
- # pdns-r process name is NOT equal to the name shown in ps
- if not process_named_running('pdns-r/worker'):
+ if not process_named_running('pdns_recursor'):
logger.info(f'pdns_recursor not running, not sending "{command}"')
return
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 1000d8b72..ed8cf6a44 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -1,6 +1,6 @@
#!/usr/share/vyos-http-api-tools/bin/python3
#
-# Copyright (C) 2019-2021 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 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
@@ -201,6 +201,19 @@ class ShowModel(ApiModel):
}
}
+class ResetModel(ApiModel):
+ op: StrictStr
+ path: List[StrictStr]
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "key": "id_key",
+ "op": "reset",
+ "path": ["op", "mode", "path"],
+ }
+ }
+
class Success(BaseModel):
success: bool
data: Union[str, bool, Dict]
@@ -372,7 +385,7 @@ class MultipartRoute(APIRoute):
return error(400, "Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(request.offending_command)))
if request.ERR_PATH_NOT_LIST_OF_STR:
return error(400, "Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(request.offending_command)))
- if endpoint in ('/retrieve','/generate','/show'):
+ if endpoint in ('/retrieve','/generate','/show','reset'):
if request.ERR_NO_OP or request.ERR_NO_PATH:
return error(400, "Missing required field. \"op\" and \"path\" fields are required")
if endpoint in ('/config-file', '/image'):
@@ -607,6 +620,26 @@ def show_op(data: ShowModel):
return success(res)
+@app.post('/reset')
+def reset_op(data: ResetModel):
+ session = app.state.vyos_session
+
+ op = data.op
+ path = data.path
+
+ try:
+ if op == 'reset':
+ res = session.reset(path)
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except ConfigSessionError as e:
+ return error(400, str(e))
+ except Exception as e:
+ logger.critical(traceback.format_exc())
+ return error(500, "An internal error occured. Check the logs for details.")
+
+ return success(res)
+
###
# GraphQL integration
###
diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
index a8df232ae..a0fccd1d0 100755
--- a/src/system/keepalived-fifo.py
+++ b/src/system/keepalived-fifo.py
@@ -30,6 +30,7 @@ from vyos.ifconfig.vrrp import VRRP
from vyos.configquery import ConfigTreeQuery
from vyos.util import cmd
from vyos.util import dict_search
+from vyos.util import commit_in_progress
# configure logging
logger = logging.getLogger(__name__)
@@ -63,6 +64,17 @@ class KeepalivedFifo:
# load configuration
def _config_load(self):
+ # For VRRP configuration to be read, the commit must be finished
+ count = 1
+ while commit_in_progress():
+ if ( count <= 40 ):
+ logger.debug(f'commit in progress try: {count}')
+ else:
+ logger.error(f'commit still in progress after {count} continuing anyway')
+ break
+ count += 1
+ time.sleep(0.5)
+
try:
base = ['high-availability', 'vrrp']
conf = ConfigTreeQuery()
diff --git a/src/systemd/wpa_supplicant-macsec@.service b/src/systemd/wpa_supplicant-macsec@.service
index 7e0bee8e1..d5739583e 100644
--- a/src/systemd/wpa_supplicant-macsec@.service
+++ b/src/systemd/wpa_supplicant-macsec@.service
@@ -1,17 +1,17 @@
[Unit]
-Description=WPA supplicant daemon (macsec-specific version)
+Description=WPA supplicant daemon (MACsec-specific version)
Requires=sys-subsystem-net-devices-%i.device
ConditionPathExists=/run/wpa_supplicant/%I.conf
After=vyos-router.service
RequiresMountsFor=/run
-# NetworkManager users will probably want the dbus version instead.
-
[Service]
Type=simple
WorkingDirectory=/run/wpa_supplicant
PIDFile=/run/wpa_supplicant/%I.pid
-ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -i%I
+ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -P/run/wpa_supplicant/%I.pid -i%I
+Restart=always
+RestartSec=2
[Install]
WantedBy=multi-user.target
diff --git a/src/tests/test_util.py b/src/tests/test_util.py
index 22bc085c5..91890262c 100644
--- a/src/tests/test_util.py
+++ b/src/tests/test_util.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from unittest import TestCase
-from vyos.util import mangle_dict_keys
+from vyos.util import *
class TestVyOSUtil(TestCase):
def test_key_mangline(self):
@@ -24,3 +24,15 @@ class TestVyOSUtil(TestCase):
new_data = mangle_dict_keys(data, '-', '_')
self.assertEqual(new_data, expected_data)
+ def test_sysctl_read(self):
+ self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1')
+
+ def test_ipv6_enabled(self):
+ tmp = sysctl_read('net.ipv6.conf.all.disable_ipv6')
+ # We need to test for both variants as this depends on how the
+ # Docker container is started (with or without IPv6 support) - so we
+ # will simply check both cases to not make the users life miserable.
+ if tmp == '0':
+ self.assertTrue(is_ipv6_enabled())
+ else:
+ self.assertFalse(is_ipv6_enabled())
diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client
index d4d38315a..a0515951a 100755
--- a/src/utils/vyos-hostsd-client
+++ b/src/utils/vyos-hostsd-client
@@ -129,7 +129,8 @@ try:
params = h.split(",")
if len(params) < 2:
raise ValueError("Malformed host entry")
- entry['address'] = params[1]
+ # Address needs to be a list because of changes made in T2683
+ entry['address'] = [params[1]]
entry['aliases'] = params[2:]
data[params[0]] = entry
client.add_hosts({args.tag: data})