summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--data/templates/accel-ppp/l2tp.config.tmpl3
-rw-r--r--data/templates/accel-ppp/pppoe.config.tmpl18
-rw-r--r--data/templates/accel-ppp/sstp.config.tmpl6
-rw-r--r--data/templates/conserver/dropbear@.service.tmpl2
-rw-r--r--data/templates/dhcp-client/ipv4.tmpl10
-rw-r--r--data/templates/dynamic-dns/ddclient.conf.tmpl2
-rw-r--r--data/templates/frr/bfdd.frr.tmpl36
-rw-r--r--data/templates/frr/bgpd.frr.tmpl12
-rw-r--r--data/templates/frr/isisd.frr.tmpl91
-rw-r--r--data/templates/frr/ldpd.frr.tmpl99
-rw-r--r--data/templates/frr/ospf6d.frr.tmpl14
-rw-r--r--data/templates/frr/ospfd.frr.tmpl19
-rw-r--r--data/templates/frr/policy.frr.tmpl7
-rw-r--r--data/templates/frr/ripd.frr.tmpl6
-rw-r--r--data/templates/frr/ripngd.frr.tmpl7
-rw-r--r--data/templates/frr/rpki.frr.tmpl1
-rw-r--r--data/templates/frr/staticd.frr.tmpl9
-rw-r--r--data/templates/frr/vrf-vni.frr.tmpl10
-rw-r--r--data/templates/https/override.conf.tmpl15
-rw-r--r--data/templates/ipsec/swanctl/profile.tmpl2
-rw-r--r--data/templates/lcd/LCDd.conf.tmpl7
-rw-r--r--data/templates/mdns-repeater/avahi-daemon.tmpl18
-rw-r--r--data/templates/mdns-repeater/mdns-repeater.tmpl2
-rw-r--r--data/templates/netflow/uacctd.conf.tmpl3
-rw-r--r--data/templates/ntp/ntpd.conf.tmpl2
-rw-r--r--data/templates/openvpn/server.conf.tmpl19
-rw-r--r--data/templates/openvpn/service-override.conf.tmpl20
-rw-r--r--data/templates/snmp/etc.snmpd.conf.tmpl2
-rw-r--r--data/templates/tftp-server/default.tmpl5
-rw-r--r--data/templates/vrrp/keepalived.conf.tmpl3
-rw-r--r--data/templates/vyos-hostsd/hosts.tmpl5
-rw-r--r--debian/control4
-rw-r--r--debian/vyos-1x.install2
-rw-r--r--interface-definitions/bcast-relay.xml.in10
-rw-r--r--interface-definitions/containers.xml.in38
-rw-r--r--interface-definitions/dhcp-relay.xml.in10
-rw-r--r--interface-definitions/dhcp-server.xml.in8
-rw-r--r--interface-definitions/dns-domain-name.xml.in2
-rw-r--r--interface-definitions/dns-dynamic.xml.in6
-rw-r--r--interface-definitions/flow-accounting-conf.xml.in11
-rw-r--r--interface-definitions/https.xml.in2
-rw-r--r--interface-definitions/include/accel-ppp/auth-local-users.xml.i3
-rw-r--r--interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i1
-rw-r--r--interface-definitions/include/accel-ppp/ppp-options-ipv4.xml.i23
-rw-r--r--interface-definitions/include/accel-ppp/ppp-options-ipv6.xml.i1
-rw-r--r--interface-definitions/include/bfd-common.xml.i12
-rw-r--r--interface-definitions/include/bgp/afi-aggregate-address.xml.i1
-rw-r--r--interface-definitions/include/bgp/afi-l2vpn-common.xml.i6
-rw-r--r--interface-definitions/include/bgp/afi-route-target-vpn.xml.i8
-rw-r--r--interface-definitions/include/bgp/protocol-common-config.xml.i6
-rw-r--r--interface-definitions/include/bgp/route-distinguisher.xml.i2
-rw-r--r--interface-definitions/include/dhcp/ntp-server.xml.i26
-rw-r--r--interface-definitions/include/generic-disable-node.xml.i2
-rw-r--r--interface-definitions/include/generic-interface-broadcast.xml.i17
-rw-r--r--interface-definitions/include/generic-interface-multi-broadcast.xml.i18
-rw-r--r--interface-definitions/include/generic-interface-multi.xml.i18
-rw-r--r--interface-definitions/include/generic-interface.xml.i17
-rw-r--r--interface-definitions/include/interface/netns.xml.i14
-rw-r--r--interface-definitions/include/interface/vrf.xml.i2
-rw-r--r--interface-definitions/include/listen-address-vrf.xml.i25
-rw-r--r--interface-definitions/include/nat-translation-options.xml.i2
-rw-r--r--interface-definitions/include/ospf/protocol-common-config.xml.i27
-rw-r--r--interface-definitions/include/ospfv3/protocol-common-config.xml.i241
-rw-r--r--interface-definitions/interfaces-bonding.xml.in14
-rw-r--r--interface-definitions/interfaces-dummy.xml.in1
-rw-r--r--interface-definitions/interfaces-openvpn.xml.in86
-rw-r--r--interface-definitions/interfaces-tunnel.xml.in1
-rw-r--r--interface-definitions/netns.xml.in23
-rw-r--r--interface-definitions/policy.xml.in9
-rw-r--r--interface-definitions/protocols-bfd.xml.in10
-rw-r--r--interface-definitions/protocols-mpls.xml.in20
-rw-r--r--interface-definitions/protocols-ospfv3.xml.in236
-rw-r--r--interface-definitions/service_mdns-repeater.xml.in10
-rw-r--r--interface-definitions/service_pppoe-server.xml.in18
-rw-r--r--interface-definitions/service_webproxy.xml.in2
-rw-r--r--interface-definitions/snmp.xml.in20
-rw-r--r--interface-definitions/system-console.xml.in1
-rw-r--r--interface-definitions/system-lcd.xml.in8
-rw-r--r--interface-definitions/system-option.xml.in6
-rw-r--r--interface-definitions/tftp-server.xml.in2
-rw-r--r--interface-definitions/vpn_ipsec.xml.in39
-rw-r--r--interface-definitions/vpn_l2tp.xml.in8
-rw-r--r--interface-definitions/vpn_sstp.xml.in1
-rw-r--r--interface-definitions/vrf.xml.in11
-rw-r--r--interface-definitions/vrrp.xml.in9
-rw-r--r--op-mode-definitions/disks.xml.in2
-rw-r--r--op-mode-definitions/force-root-partition-auto-resize.xml.in13
-rw-r--r--op-mode-definitions/generate-ipsec-debug-archive.xml.in17
-rw-r--r--op-mode-definitions/generate-ipsec-profile.xml.in2
-rw-r--r--op-mode-definitions/include/bfd-common.xml.i54
-rw-r--r--op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i1
-rw-r--r--op-mode-definitions/include/bgp/show-ip-bgp-common.xml.i1
-rw-r--r--op-mode-definitions/openvpn.xml.in35
-rw-r--r--op-mode-definitions/show-bfd.xml.in8
-rw-r--r--op-mode-definitions/show-configuration.xml.in15
-rw-r--r--op-mode-definitions/show-interfaces-geneve.xml.in42
-rw-r--r--op-mode-definitions/show-ip-route.xml.in7
-rw-r--r--op-mode-definitions/show-log.xml.in99
-rw-r--r--op-mode-definitions/show-netns.xml.in13
-rw-r--r--op-mode-definitions/show-protocols.xml.in45
-rw-r--r--op-mode-definitions/show-system.xml.in4
-rw-r--r--python/vyos/configdict.py95
-rw-r--r--python/vyos/configquery.py46
-rw-r--r--python/vyos/configsource.py3
-rw-r--r--python/vyos/configverify.py7
-rw-r--r--python/vyos/defaults.py2
-rw-r--r--python/vyos/ethtool.py3
-rw-r--r--python/vyos/frr.py44
-rw-r--r--python/vyos/ifconfig/ethernet.py17
-rwxr-xr-xpython/vyos/ifconfig/interface.py61
-rw-r--r--python/vyos/ifconfig/section.py10
-rw-r--r--python/vyos/ifconfig/wwan.py17
-rw-r--r--python/vyos/range_regex.py142
-rw-r--r--python/vyos/remote.py581
-rw-r--r--python/vyos/template.py1
-rw-r--r--python/vyos/util.py121
-rw-r--r--smoketest/configs/bgp-dmvpn-hub174
-rw-r--r--smoketest/configs/bgp-dmvpn-spoke201
-rw-r--r--smoketest/configs/bgp-small-ipv4-unicast77
-rw-r--r--smoketest/configs/tunnel-broker4
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py10
-rw-r--r--smoketest/scripts/cli/test_container.py1
-rwxr-xr-xsmoketest/scripts/cli/test_ha_vrrp.py7
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_geneve.py1
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_netns.py83
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py1
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_pppoe.py1
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_tunnel.py20
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_vxlan.py1
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireguard.py1
-rwxr-xr-xsmoketest/scripts/cli/test_nat.py1
-rwxr-xr-xsmoketest/scripts/cli/test_nat66.py1
-rwxr-xr-xsmoketest/scripts/cli/test_pki.py1
-rwxr-xr-xsmoketest/scripts/cli/test_policy.py3
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bfd.py55
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py3
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_igmp-proxy.py1
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_isis.py9
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_mpls.py117
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ospf.py115
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ospfv3.py45
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rip.py1
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ripng.py23
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rpki.py3
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_static.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_bcast-relay.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcp-relay.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcp-server.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcpv6-relay.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcpv6-server.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_dns_dynamic.py45
-rwxr-xr-xsmoketest/scripts/cli/test_service_dns_forwarding.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_https.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_mdns-repeater.py3
-rwxr-xr-xsmoketest/scripts/cli/test_service_router-advert.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_snmp.py34
-rwxr-xr-xsmoketest/scripts/cli/test_service_ssh.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_tftp-server.py40
-rwxr-xr-xsmoketest/scripts/cli/test_service_webproxy.py1
-rwxr-xr-xsmoketest/scripts/cli/test_system_acceleration_qat.py1
-rwxr-xr-xsmoketest/scripts/cli/test_system_conntrack.py1
-rwxr-xr-xsmoketest/scripts/cli/test_system_flow-accounting.py80
-rwxr-xr-xsmoketest/scripts/cli/test_system_ip.py1
-rwxr-xr-xsmoketest/scripts/cli/test_system_ipv6.py1
-rwxr-xr-xsmoketest/scripts/cli/test_system_lcd.py1
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py1
-rwxr-xr-xsmoketest/scripts/cli/test_system_nameserver.py1
-rwxr-xr-xsmoketest/scripts/cli/test_system_ntp.py1
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py2
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_openconnect.py1
-rwxr-xr-xsmoketest/scripts/cli/test_vrf.py46
-rwxr-xr-xsrc/completion/list_openvpn_users.py48
-rwxr-xr-xsrc/conf_mode/containers.py39
-rwxr-xr-xsrc/conf_mode/dhcp_server.py12
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py2
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py4
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py5
-rwxr-xr-xsrc/conf_mode/host_name.py2
-rwxr-xr-xsrc/conf_mode/http-api.py2
-rwxr-xr-xsrc/conf_mode/https.py23
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py71
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py23
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py5
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py4
-rwxr-xr-xsrc/conf_mode/netns.py118
-rwxr-xr-xsrc/conf_mode/policy.py21
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py32
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py23
-rwxr-xr-xsrc/conf_mode/protocols_isis.py26
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py38
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py27
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py80
-rwxr-xr-xsrc/conf_mode/protocols_rip.py33
-rwxr-xr-xsrc/conf_mode/protocols_ripng.py27
-rwxr-xr-xsrc/conf_mode/protocols_rpki.py17
-rwxr-xr-xsrc/conf_mode/protocols_static.py22
-rwxr-xr-xsrc/conf_mode/service_mdns-repeater.py12
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py15
-rwxr-xr-xsrc/conf_mode/snmp.py24
-rwxr-xr-xsrc/conf_mode/system-login-banner.py15
-rwxr-xr-xsrc/conf_mode/system-option.py6
-rwxr-xr-xsrc/conf_mode/system_console.py70
-rwxr-xr-xsrc/conf_mode/tftp_server.py9
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py2
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py12
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py42
-rwxr-xr-xsrc/conf_mode/vrf.py14
-rwxr-xr-xsrc/conf_mode/vrf_vni.py31
-rwxr-xr-xsrc/conf_mode/vrrp.py8
-rw-r--r--src/etc/cron.d/check-wwan1
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient6
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup12
-rw-r--r--src/etc/logrotate.d/vyos-atop20
-rw-r--r--src/etc/systemd/system/atop.service.d/10-override.conf6
-rw-r--r--src/etc/systemd/system/avahi-daemon.service.d/override.conf8
-rw-r--r--src/etc/systemd/system/openvpn@.service.d/10-override.conf (renamed from src/etc/systemd/system/openvpn@.service.d/override.conf)1
-rw-r--r--src/etc/udev/rules.d/62-temporary-interface-rename.rules1
-rw-r--r--src/etc/udev/rules.d/65-vyos-net.rules5
-rw-r--r--src/etc/udev/rules.d/90-vyos-serial.rules4
-rwxr-xr-xsrc/helpers/strip-private.py1
-rwxr-xr-xsrc/helpers/vyos-boot-config-loader.py4
-rwxr-xr-xsrc/helpers/vyos-check-wwan.py35
-rwxr-xr-xsrc/helpers/vyos_net_name54
-rwxr-xr-xsrc/migration-scripts/bgp/1-to-277
-rwxr-xr-xsrc/migration-scripts/interfaces/21-to-22141
-rwxr-xr-xsrc/migration-scripts/interfaces/22-to-23143
-rwxr-xr-xsrc/migration-scripts/interfaces/23-to-24379
-rwxr-xr-xsrc/migration-scripts/interfaces/24-to-25369
-rwxr-xr-xsrc/migration-scripts/ospf/0-to-181
-rwxr-xr-xsrc/op_mode/connect_disconnect.py68
-rwxr-xr-xsrc/op_mode/force_root-partition-auto-resize.sh60
-rwxr-xr-xsrc/op_mode/format_disk.py76
-rwxr-xr-xsrc/op_mode/generate_ipsec_debug_archive.sh36
-rwxr-xr-xsrc/op_mode/lldp_op.py3
-rwxr-xr-xsrc/op_mode/pki.py73
-rwxr-xr-xsrc/op_mode/ppp-server-ctrl.py2
-rwxr-xr-xsrc/op_mode/show_configuration_json.py36
-rwxr-xr-xsrc/op_mode/show_interfaces.py4
-rwxr-xr-xsrc/op_mode/show_ipsec_sa.py3
-rwxr-xr-xsrc/op_mode/show_openvpn_mfa.py64
-rwxr-xr-xsrc/op_mode/show_ram.py64
-rwxr-xr-xsrc/op_mode/show_ram.sh33
-rwxr-xr-xsrc/op_mode/show_uptime.py50
-rwxr-xr-xsrc/op_mode/vpn_ipsec.py2
-rw-r--r--src/services/api/graphql/README.graphql87
-rw-r--r--src/services/api/graphql/bindings.py13
-rw-r--r--src/services/api/graphql/graphql/directives.py58
-rw-r--r--src/services/api/graphql/graphql/mutations.py53
-rw-r--r--src/services/api/graphql/graphql/schema/config_file.graphql27
-rw-r--r--src/services/api/graphql/graphql/schema/dhcp_server.graphql12
-rw-r--r--src/services/api/graphql/graphql/schema/firewall_group.graphql47
-rw-r--r--src/services/api/graphql/graphql/schema/image.graphql29
-rw-r--r--src/services/api/graphql/graphql/schema/interface_ethernet.graphql8
-rw-r--r--src/services/api/graphql/graphql/schema/schema.graphql19
-rw-r--r--src/services/api/graphql/graphql/schema/show.graphql14
-rw-r--r--src/services/api/graphql/graphql/schema/show_config.graphql21
-rw-r--r--src/services/api/graphql/recipes/dhcp_server.py13
-rw-r--r--src/services/api/graphql/recipes/interface_ethernet.py13
-rw-r--r--src/services/api/graphql/recipes/recipe.py49
-rw-r--r--src/services/api/graphql/recipes/remove_firewall_address_group_members.py21
-rw-r--r--src/services/api/graphql/recipes/session.py123
-rw-r--r--src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl (renamed from src/services/api/graphql/recipes/templates/dhcp_server.tmpl)2
-rw-r--r--src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl4
-rw-r--r--src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl (renamed from src/services/api/graphql/recipes/templates/interface_ethernet.tmpl)0
-rw-r--r--src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl3
-rw-r--r--src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl3
-rwxr-xr-xsrc/services/vyos-configd3
-rwxr-xr-xsrc/services/vyos-hostsd2
-rwxr-xr-xsrc/services/vyos-http-api-server44
-rwxr-xr-xsrc/system/keepalived-fifo.py45
-rw-r--r--src/systemd/dhcp6c@.service2
-rw-r--r--src/systemd/root-partition-auto-resize.service12
-rw-r--r--src/systemd/tftpd@.service2
-rw-r--r--src/systemd/vyos-http-api.service6
-rwxr-xr-xsrc/utils/vyos-hostsd-client3
-rwxr-xr-xsrc/validators/bgp-large-community-list2
-rwxr-xr-xsrc/validators/bgp-rd-rt (renamed from src/validators/bgp-route-target)30
-rwxr-xr-xsrc/validators/range56
-rwxr-xr-xsrc/validators/script4
280 files changed, 5616 insertions, 2397 deletions
diff --git a/README.md b/README.md
index a090cf0cb..fcaad5c89 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# vyos-1x: VyOS 1.2.0+ configuration scripts and data
+# vyos-1x: VyOS 1.2.0+ Configuration Scripts and Data
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=vyos%3Avyos-1x&metric=coverage)](https://sonarcloud.io/component_measures?id=vyos%3Avyos-1x&metric=coverage)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_shield)
@@ -69,7 +69,7 @@ pipenv shell
make test
```
-### Runtime (Smoketests)
+### Runtime (Smoke Tests)
Runtime tests are executed by the CI system on a running VyOS instance inside
QEMU. The testcases can be found inside the smoketest subdirectory which will
diff --git a/data/templates/accel-ppp/l2tp.config.tmpl b/data/templates/accel-ppp/l2tp.config.tmpl
index 44c96b935..9fcda76d4 100644
--- a/data/templates/accel-ppp/l2tp.config.tmpl
+++ b/data/templates/accel-ppp/l2tp.config.tmpl
@@ -57,6 +57,9 @@ bind={{ outside_addr }}
{% if lns_shared_secret %}
secret={{ lns_shared_secret }}
{% endif %}
+{% if lns_host_name %}
+host-name={{ lns_host_name }}
+{% endif %}
[client-ip-range]
0.0.0.0/0
diff --git a/data/templates/accel-ppp/pppoe.config.tmpl b/data/templates/accel-ppp/pppoe.config.tmpl
index 238e7ee15..0a8e0079b 100644
--- a/data/templates/accel-ppp/pppoe.config.tmpl
+++ b/data/templates/accel-ppp/pppoe.config.tmpl
@@ -108,19 +108,17 @@ ac-name={{ access_concentrator }}
{% if iface_config.vlan_id is not defined and iface_config.vlan_range is not defined %}
interface={{ iface }}
{% endif %}
-{% if iface_config.vlan_id is defined and iface_config.vlan_range is not defined %}
-{% for vlan in iface_config.vlan_id %}
-interface={{ iface }}.{{ vlan }}
-vlan-mon={{ iface }},{{ vlan }}
+{% if iface_config.vlan_range is defined %}
+{% for regex in iface_config.regex %}
+interface=re:^{{ iface | replace('.', '\\.') }}\.({{ regex }})$
{% endfor %}
-{% endif %}
-{% if iface_config.vlan_range is defined and iface_config.vlan_id is not defined %}
vlan-mon={{ iface }},{{ iface_config.vlan_range | join(',') }}
-interface=re:{{ iface | replace('.', '\\.') }}\.\d+
{% endif %}
-{% if iface_config.vlan_id is defined and iface_config.vlan_range is defined %}
-vlan-mon={{ iface }},{{ iface_config.vlan_id | join(',') }},{{ iface_config.vlan_range | join(',') }}
-interface=re:{{ iface | replace('.', '\\.') }}\.\d+
+{% if iface_config.vlan_id is defined %}
+{% for vlan in iface_config.vlan_id %}
+vlan-mon={{ iface }},{{ vlan }}
+interface=re:^{{ iface | replace('.', '\\.') }}\.{{ vlan }}$
+{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl
index fad91d118..8fd7d230d 100644
--- a/data/templates/accel-ppp/sstp.config.tmpl
+++ b/data/templates/accel-ppp/sstp.config.tmpl
@@ -50,9 +50,9 @@ verbose=1
check-ip=1
{# MTU #}
mtu={{ mtu }}
-{% if client_ipv6_pool is defined %}
-ipv6=allow
-{% endif %}
+ipv6={{ 'allow' if ppp_options.ipv6 == "deny" and client_ipv6_pool is defined else ppp_options.ipv6 }}
+ipv4={{ ppp_options.ipv4 }}
+
mppe={{ ppp_options.mppe }}
lcp-echo-interval={{ ppp_options.lcp_echo_interval }}
lcp-echo-timeout={{ ppp_options.lcp_echo_timeout }}
diff --git a/data/templates/conserver/dropbear@.service.tmpl b/data/templates/conserver/dropbear@.service.tmpl
index 4bb73f751..e355dab43 100644
--- a/data/templates/conserver/dropbear@.service.tmpl
+++ b/data/templates/conserver/dropbear@.service.tmpl
@@ -1,4 +1,4 @@
[Service]
ExecStart=
-ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I
+ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -b /etc/issue.net -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I
PIDFile=/run/conserver/dropbear.%I.pid
diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl
index c934b7cdb..b3e74c22b 100644
--- a/data/templates/dhcp-client/ipv4.tmpl
+++ b/data/templates/dhcp-client/ipv4.tmpl
@@ -2,12 +2,18 @@
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
timeout 60;
-retry 300;
+retry 60;
+initial-interval 2;
interface "{{ ifname }}" {
send host-name "{{ dhcp_options.host_name }}";
{% if dhcp_options.client_id is defined and dhcp_options.client_id is not none %}
- send dhcp-client-identifier "{{ dhcp_options.client_id }}";
+{% set client_id = dhcp_options.client_id %}
+{# Use HEX representation of client-id as it is send in MAC-address style using hex characters. If not HEX, use double quotes ASCII format #}
+{% if not dhcp_options.client_id.split(':') | length >= 5 %}
+{% set client_id = '"' + dhcp_options.client_id + '"' %}
+{% endif %}
+ send dhcp-client-identifier {{ client_id }};
{% endif %}
{% if dhcp_options.vendor_class_id is defined and dhcp_options.vendor_class_id is not none %}
send vendor-class-identifier "{{ dhcp_options.vendor_class_id }}";
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/bfdd.frr.tmpl b/data/templates/frr/bfdd.frr.tmpl
index 16f8be92c..439f79d67 100644
--- a/data/templates/frr/bfdd.frr.tmpl
+++ b/data/templates/frr/bfdd.frr.tmpl
@@ -1,4 +1,4 @@
-!
+{% if profile is defined or peer is defined %}
bfd
{% if profile is defined and profile is not none %}
{% for profile_name, profile_config in profile.items() %}
@@ -6,39 +6,53 @@ bfd
detect-multiplier {{ profile_config.interval.multiplier }}
receive-interval {{ profile_config.interval.receive }}
transmit-interval {{ profile_config.interval.transmit }}
-{% if profile_config.interval['echo-interval'] is defined and profile_config.interval['echo-interval'] is not none %}
- echo-interval {{ profile_config.interval['echo-interval'] }}
+{% if profile_config.interval.echo_interval is defined and profile_config.interval.echo_interval is not none %}
+ echo transmit-interval {{ profile_config.interval.echo_interval }}
+ echo receive-interval {{ profile_config.interval.echo_interval }}
{% endif %}
-{% if profile_config['echo-mode'] is defined %}
+{% if profile_config.echo_mode is defined %}
echo-mode
{% endif %}
+{% if profile_config.passive is defined %}
+ passive-mode
+{% endif %}
{% if profile_config.shutdown is defined %}
shutdown
{% else %}
no shutdown
{% endif %}
- exit
+ exit
+ !
{% endfor %}
{% endif %}
{% if peer is defined and peer is not none %}
{% for peer_name, peer_config in peer.items() %}
- peer {{ peer_name }}{{ ' multihop' if peer_config.multihop is defined }}{{ ' local-address ' + peer_config.source.address if peer_config.source is defined and peer_config.source.address is defined }}{{ ' interface ' + peer_config.source.interface if peer_config.source is defined and peer_config.source.interface is defined }}
+ peer {{ peer_name }}{{ ' multihop' if peer_config.multihop is defined }}{{ ' local-address ' + peer_config.source.address if peer_config.source is defined and peer_config.source.address is defined }}{{ ' interface ' + peer_config.source.interface if peer_config.source is defined and peer_config.source.interface is defined }} {{ ' vrf ' + peer_config.vrf if peer_config.vrf is defined and peer_config.vrf is not none }}
detect-multiplier {{ peer_config.interval.multiplier }}
receive-interval {{ peer_config.interval.receive }}
transmit-interval {{ peer_config.interval.transmit }}
-{% if peer_config.interval['echo-interval'] is defined and peer_config.interval['echo-interval'] is not none %}
- echo-interval {{ peer_config.interval['echo-interval'] }}
+{% if peer_config.interval.echo_interval is defined and peer_config.interval.echo_interval is not none %}
+ echo transmit-interval {{ peer_config.interval.echo_interval }}
+ echo receive-interval {{ peer_config.interval.echo_interval }}
{% endif %}
-{% if peer_config['echo-mode'] is defined %}
+{% if peer_config.echo_mode is defined %}
echo-mode
{% endif %}
+{% if peer_config.passive is defined %}
+ passive-mode
+{% endif %}
+{% if peer_config.profile is defined and peer_config.profile is not none %}
+ profile {{ peer_config.profile }}
+{% endif %}
{% if peer_config.shutdown is defined %}
shutdown
{% else %}
no shutdown
{% endif %}
- exit
+ exit
+ !
{% endfor %}
{% endif %}
- end
+exit
!
+{% endif %}
diff --git a/data/templates/frr/bgpd.frr.tmpl b/data/templates/frr/bgpd.frr.tmpl
index 27a2b98a5..fbdbafd6e 100644
--- a/data/templates/frr/bgpd.frr.tmpl
+++ b/data/templates/frr/bgpd.frr.tmpl
@@ -230,10 +230,8 @@ router bgp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is defined and vrf is not none
{% else %}
no bgp ebgp-requires-policy
{% endif %}
-{% if parameters is defined and parameters.default is defined and parameters.default.no_ipv4_unicast is defined %}
{# Option must be set before any neighbor - see https://phabricator.vyos.net/T3463 #}
no bgp default ipv4-unicast
-{% endif %}
{# Workaround for T2100 until we have decided about a migration script #}
no bgp network import-check
{% if address_family is defined and address_family is not none %}
@@ -266,8 +264,11 @@ router bgp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is defined and vrf is not none
{% endif %}
{% endif %}
{% if afi_config.aggregate_address is defined and afi_config.aggregate_address is not none %}
-{% for ip in afi_config.aggregate_address %}
- aggregate-address {{ ip }}{{ ' as-set' if afi_config.aggregate_address[ip].as_set is defined }}{{ ' summary-only' if afi_config.aggregate_address[ip].summary_only is defined }}
+{% for aggregate, aggregate_config in afi_config.aggregate_address.items() %}
+ aggregate-address {{ aggregate }}{{ ' as-set' if aggregate_config.as_set is defined }}{{ ' summary-only' if aggregate_config.summary_only is defined }}
+{% if aggregate_config.route_map is defined and aggregate_config.route_map is not none %}
+ aggregate-address {{ aggregate }} route-map {{ aggregate_config.route_map }}
+{% endif %}
{% endfor %}
{% endif %}
{% if afi_config.maximum_paths is defined and afi_config.maximum_paths.ebgp is defined and afi_config.maximum_paths.ebgp is not none %}
@@ -523,5 +524,4 @@ router bgp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is defined and vrf is not none
{% if timers is defined and timers.keepalive is defined and timers.holdtime is defined %}
timers bgp {{ timers.keepalive }} {{ timers.holdtime }}
{% endif %}
- end
-! \ No newline at end of file
+exit \ No newline at end of file
diff --git a/data/templates/frr/isisd.frr.tmpl b/data/templates/frr/isisd.frr.tmpl
index 51ac40060..fc0799e02 100644
--- a/data/templates/frr/isisd.frr.tmpl
+++ b/data/templates/frr/isisd.frr.tmpl
@@ -1,4 +1,50 @@
!
+{% if interface is defined and interface is not none %}
+{% for iface, iface_config in interface.items() %}
+interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
+ ip router isis VyOS
+ ipv6 router isis VyOS
+{% if iface_config.bfd is defined %}
+ isis bfd
+{% endif %}
+{% if iface_config.network is defined and iface_config.network.point_to_point is defined %}
+ isis network point-to-point
+{% endif %}
+{% if iface_config.circuit_type is defined %}
+ isis circuit-type {{ iface_config.circuit_type }}
+{% endif %}
+{% if iface_config.hello_interval is defined and iface_config.hello_interval is not none %}
+ isis hello-interval {{ iface_config.hello_interval }}
+{% endif %}
+{% if iface_config.hello_multiplier is defined and iface_config.hello_multiplier is not none %}
+ isis hello-multiplier {{ iface_config.hello_multiplier }}
+{% endif %}
+{% if iface_config.hello_padding is defined %}
+ isis hello padding
+{% endif %}
+{% if iface_config.metric is defined and iface_config.metric is not none %}
+ isis metric {{ iface_config.metric }}
+{% endif %}
+{% 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 %}
+ isis password clear {{ iface_config.password.plaintext_password }}
+{% endif %}
+{% if iface_config.priority is defined and iface_config.priority is not none %}
+ isis priority {{ iface_config.priority }}
+{% endif %}
+{% if iface_config.psnp_interval is defined and iface_config.psnp_interval is not none %}
+ isis psnp-interval {{ iface_config.psnp_interval }}
+{% endif %}
+{% if iface_config.no_three_way_handshake is defined %}
+ no isis three-way-handshake
+{% endif %}
+exit
+!
+{% endfor %}
+{% endif %}
+!
router isis VyOS {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
net {{ net }}
{% if dynamic_hostname is defined %}
@@ -151,48 +197,5 @@ router isis VyOS {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
is-type {{ level }}
{% endif %}
{% endif %}
-!
-{% if interface is defined and interface is not none %}
-{% for iface, iface_config in interface.items() %}
-interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
- ip router isis VyOS
- ipv6 router isis VyOS
-{% if iface_config.bfd is defined %}
- isis bfd
-{% endif %}
-{% if iface_config.network is defined and iface_config.network.point_to_point is defined %}
- isis network point-to-point
-{% endif %}
-{% if iface_config.circuit_type is defined %}
- isis circuit-type {{ iface_config.circuit_type }}
-{% endif %}
-{% if iface_config.hello_interval is defined and iface_config.hello_interval is not none %}
- isis hello-interval {{ iface_config.hello_interval }}
-{% endif %}
-{% if iface_config.hello_multiplier is defined and iface_config.hello_multiplier is not none %}
- isis hello-multiplier {{ iface_config.hello_multiplier }}
-{% endif %}
-{% if iface_config.hello_padding is defined %}
- isis hello padding
-{% endif %}
-{% if iface_config.metric is defined and iface_config.metric is not none %}
- isis metric {{ iface_config.metric }}
-{% endif %}
-{% 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 %}
- isis password clear {{ iface_config.password.plaintext_password }}
-{% endif %}
-{% if iface_config.priority is defined and iface_config.priority is not none %}
- isis priority {{ iface_config.priority }}
-{% endif %}
-{% if iface_config.psnp_interval is defined and iface_config.psnp_interval is not none %}
- isis psnp-interval {{ iface_config.psnp_interval }}
-{% endif %}
-{% if iface_config.no_three_way_handshake is defined %}
- no isis three-way-handshake
-{% endif %}
-{% endfor %}
-{% endif %}
+exit
! \ No newline at end of file
diff --git a/data/templates/frr/ldpd.frr.tmpl b/data/templates/frr/ldpd.frr.tmpl
index 0a5411552..537ea4025 100644
--- a/data/templates/frr/ldpd.frr.tmpl
+++ b/data/templates/frr/ldpd.frr.tmpl
@@ -2,69 +2,69 @@
{% if ldp is defined %}
mpls ldp
{% if ldp.router_id is defined %}
-router-id {{ ldp.router_id }}
+ router-id {{ ldp.router_id }}
{% endif %}
{% if ldp.parameters is defined %}
{% if ldp.parameters.cisco_interop_tlv is defined %}
-dual-stack cisco-interop
+ dual-stack cisco-interop
{% endif %}
{% if ldp.parameters.transport_prefer_ipv4 is defined%}
-dual-stack transport-connection prefer ipv4
+ dual-stack transport-connection prefer ipv4
{% endif %}
{% if ldp.parameters.ordered_control is defined%}
-ordered-control
+ ordered-control
{% endif %}
{% endif %}
{% if ldp.neighbor is defined %}
{% for neighbors in ldp.neighbor %}
{% if ldp.neighbor[neighbors].password is defined %}
-neighbor {{neighbors}} password {{ldp.neighbor[neighbors].password}}
+ neighbor {{ neighbors }} password {{ ldp.neighbor[neighbors].password }}
{% endif %}
{% if ldp.neighbor[neighbors].ttl_security is defined %}
{% if 'disable' in ldp.neighbor[neighbors].ttl_security %}
-neighbor {{neighbors}} ttl-security disable
+ neighbor {{ neighbors }} ttl-security disable
{% else %}
-neighbor {{neighbors}} ttl-security hops {{ldp.neighbor[neighbors].ttl_security}}
+ neighbor {{ neighbors }} ttl-security hops {{ ldp.neighbor[neighbors].ttl_security }}
{% endif %}
{% endif %}
{% if ldp.neighbor[neighbors].session_holdtime is defined %}
-neighbor {{neighbors}} session holdtime {{ldp.neighbor[neighbors].session_holdtime}}
+ neighbor {{ neighbors }} session holdtime {{ ldp.neighbor[neighbors].session_holdtime }}
{% endif %}
{% endfor %}
{% endif %}
-!
+ !
{% if ldp.discovery is defined %}
{% if ldp.discovery.transport_ipv4_address is defined %}
-address-family ipv4
+ address-family ipv4
{% if ldp.allocation is defined %}
{% if ldp.allocation.ipv4 is defined %}
{% if ldp.allocation.ipv4.access_list is defined %}
-label local allocate for {{ ldp.allocation.ipv4.access_list }}
+ label local allocate for {{ ldp.allocation.ipv4.access_list }}
{% endif %}
{% endif %}
{% else %}
-label local allocate host-routes
+ label local allocate host-routes
{% endif %}
{% if ldp.discovery.transport_ipv4_address is defined %}
-discovery transport-address {{ ldp.discovery.transport_ipv4_address }}
+ discovery transport-address {{ ldp.discovery.transport_ipv4_address }}
{% endif %}
{% if ldp.discovery.hello_ipv4_holdtime is defined %}
-discovery hello holdtime {{ ldp.discovery.hello_ipv4_holdtime }}
+ discovery hello holdtime {{ ldp.discovery.hello_ipv4_holdtime }}
{% endif %}
{% if ldp.discovery.hello_ipv4_interval is defined %}
-discovery hello interval {{ ldp.discovery.hello_ipv4_interval }}
+ discovery hello interval {{ ldp.discovery.hello_ipv4_interval }}
{% endif %}
{% if ldp.discovery.session_ipv4_holdtime is defined %}
-session holdtime {{ ldp.discovery.session_ipv4_holdtime }}
+ session holdtime {{ ldp.discovery.session_ipv4_holdtime }}
{% endif %}
{% if ldp.import is defined %}
{% if ldp.import.ipv4 is defined %}
{% if ldp.import.ipv4.import_filter is defined %}
{% if ldp.import.ipv4.import_filter.filter_access_list is defined %}
{% if ldp.import.ipv4.import_filter.neighbor_access_list is defined %}
-label remote accept for {{ ldp.import.ipv4.import_filter.filter_access_list }} from {{ ldp.import.ipv4.import_filter.neighbor_access_list }}
+ label remote accept for {{ ldp.import.ipv4.import_filter.filter_access_list }} from {{ ldp.import.ipv4.import_filter.neighbor_access_list }}
{% else %}
-label remote accept for {{ ldp.import.ipv4.import_filter.filter_access_list }}
+ label remote accept for {{ ldp.import.ipv4.import_filter.filter_access_list }}
{% endif %}
{% endif %}
{% endif %}
@@ -73,14 +73,14 @@ label remote accept for {{ ldp.import.ipv4.import_filter.filter_access_list }}
{% if ldp.export is defined %}
{% if ldp.export.ipv4 is defined %}
{% if ldp.export.ipv4.explicit_null is defined %}
-label local advertise explicit-null
+ label local advertise explicit-null
{% endif %}
{% if ldp.export.ipv4.export_filter is defined %}
{% if ldp.export.ipv4.export_filter.filter_access_list is defined %}
{% if ldp.export.ipv4.export_filter.neighbor_access_list is defined %}
-label local advertise for {{ ldp.export.ipv4.export_filter.filter_access_list }} to {{ ldp.export.ipv4.export_filter.neighbor_access_list }}
+ label local advertise for {{ ldp.export.ipv4.export_filter.filter_access_list }} to {{ ldp.export.ipv4.export_filter.neighbor_access_list }}
{% else %}
-label local advertise for {{ ldp.export.ipv4.export_filter.filter_access_list }}
+ label local advertise for {{ ldp.export.ipv4.export_filter.filter_access_list }}
{% endif %}
{% endif %}
{% endif %}
@@ -88,59 +88,59 @@ label local advertise for {{ ldp.export.ipv4.export_filter.filter_access_list }}
{% endif %}
{% if ldp.targeted_neighbor is defined %}
{% if ldp.targeted_neighbor.ipv4.enable is defined %}
-discovery targeted-hello accept
+ discovery targeted-hello accept
{% endif %}
{% if ldp.targeted_neighbor.ipv4.hello_holdtime is defined %}
-discovery targeted-hello holdtime {{ ldp.targeted_neighbor.ipv4.hello_holdtime }}
+ discovery targeted-hello holdtime {{ ldp.targeted_neighbor.ipv4.hello_holdtime }}
{% endif %}
{% if ldp.targeted_neighbor.ipv4.hello_interval is defined %}
-discovery targeted-hello interval {{ ldp.targeted_neighbor.ipv4.hello_interval }}
+ discovery targeted-hello interval {{ ldp.targeted_neighbor.ipv4.hello_interval }}
{% endif %}
{% for addresses in ldp.targeted_neighbor.ipv4.address %}
-neighbor {{addresses}} targeted
+ neighbor {{addresses}} targeted
{% endfor %}
{% endif %}
{% for interfaces in ldp.interface %}
-interface {{interfaces}}
+ interface {{interfaces}}
{% endfor %}
-exit-address-family
+ exit-address-family
{% else %}
-no address-family ipv4
+ no address-family ipv4
{% endif %}
{% endif %}
-!
+ !
{% if ldp.discovery is defined %}
{% if ldp.discovery.transport_ipv6_address is defined %}
-address-family ipv6
+ address-family ipv6
{% if ldp.allocation is defined %}
{% if ldp.allocation.ipv6 is defined %}
{% if ldp.allocation.ipv6.access_list6 is defined %}
-label local allocate for {{ ldp.allocation.ipv6.access_list6 }}
+ label local allocate for {{ ldp.allocation.ipv6.access_list6 }}
{% endif %}
{% endif %}
{% else %}
-label local allocate host-routes
+ label local allocate host-routes
{% endif %}
{% if ldp.discovery.transport_ipv6_address is defined %}
-discovery transport-address {{ ldp.discovery.transport_ipv6_address }}
+ discovery transport-address {{ ldp.discovery.transport_ipv6_address }}
{% endif %}
{% if ldp.discovery.hello_ipv6_holdtime is defined %}
-discovery hello holdtime {{ ldp.discovery.hello_ipv6_holdtime }}
+ discovery hello holdtime {{ ldp.discovery.hello_ipv6_holdtime }}
{% endif %}
{% if ldp.discovery.hello_ipv6_interval is defined %}
-discovery hello interval {{ ldp.discovery.hello_ipv6_interval }}
+ discovery hello interval {{ ldp.discovery.hello_ipv6_interval }}
{% endif %}
{% if ldp.discovery.session_ipv6_holdtime is defined %}
-session holdtime {{ ldp.discovery.session_ipv6_holdtime }}
+ session holdtime {{ ldp.discovery.session_ipv6_holdtime }}
{% endif %}
{% if ldp.import is defined %}
{% if ldp.import.ipv6 is defined %}
{% if ldp.import.ipv6.import_filter is defined %}
{% if ldp.import.ipv6.import_filter.filter_access_list6 is defined %}
{% if ldp.import.ipv6.import_filter.neighbor_access_list6 is defined %}
-label remote accept for {{ ldp.import.ipv6.import_filter.filter_access_list6 }} from {{ ldp.import.ipv6.import_filter.neighbor_access_list6 }}
+ label remote accept for {{ ldp.import.ipv6.import_filter.filter_access_list6 }} from {{ ldp.import.ipv6.import_filter.neighbor_access_list6 }}
{% else %}
-label remote accept for {{ ldp.import.ipv6.import_filter.filter_access_list6 }}
+ label remote accept for {{ ldp.import.ipv6.import_filter.filter_access_list6 }}
{% endif %}
{% endif %}
{% endif %}
@@ -149,14 +149,14 @@ label remote accept for {{ ldp.import.ipv6.import_filter.filter_access_list6 }}
{% if ldp.export is defined %}
{% if ldp.export.ipv6 is defined %}
{% if ldp.export.ipv6.explicit_null is defined %}
-label local advertise explicit-null
+ label local advertise explicit-null
{% endif %}
{% if ldp.export.ipv6.export_filter is defined %}
{% if ldp.export.ipv6.export_filter.filter_access_list6 is defined %}
{% if ldp.export.ipv6.export_filter.neighbor_access_list6 is defined %}
-label local advertise for {{ ldp.export.ipv6.export_filter.filter_access_list6 }} to {{ ldp.export.ipv6.export_filter.neighbor_access_list6 }}
+ label local advertise for {{ ldp.export.ipv6.export_filter.filter_access_list6 }} to {{ ldp.export.ipv6.export_filter.neighbor_access_list6 }}
{% else %}
-label local advertise for {{ ldp.export.ipv6.export_filter.filter_access_list6 }}
+ label local advertise for {{ ldp.export.ipv6.export_filter.filter_access_list6 }}
{% endif %}
{% endif %}
{% endif %}
@@ -164,24 +164,27 @@ label local advertise for {{ ldp.export.ipv6.export_filter.filter_access_list6 }
{% endif %}
{% if ldp.targeted_neighbor is defined %}
{% if ldp.targeted_neighbor.ipv6.enable is defined %}
-discovery targeted-hello accept
+ discovery targeted-hello accept
{% endif %}
{% if ldp.targeted_neighbor.ipv6.hello_holdtime is defined %}
-discovery targeted-hello holdtime {{ ldp.targeted_neighbor.ipv6.hello_holdtime }}
+ discovery targeted-hello holdtime {{ ldp.targeted_neighbor.ipv6.hello_holdtime }}
{% endif %}
{% if ldp.targeted_neighbor.ipv6.hello_interval is defined %}
-discovery targeted-hello interval {{ ldp.targeted_neighbor.ipv6.hello_interval }}
+ discovery targeted-hello interval {{ ldp.targeted_neighbor.ipv6.hello_interval }}
{% endif %}
{% for addresses in ldp.targeted_neighbor.ipv6.address %}
-neighbor {{addresses}} targeted
+ neighbor {{addresses}} targeted
{% endfor %}
{% endif %}
{% for interfaces in ldp.interface %}
-interface {{interfaces}}
+ interface {{interfaces}}
{% endfor %}
-exit-address-family
+ exit-address-family
{% else %}
-no address-family ipv6
+ no address-family ipv6
{% endif %}
+ !
{% endif %}
+exit
{% endif %}
+!
diff --git a/data/templates/frr/ospf6d.frr.tmpl b/data/templates/frr/ospf6d.frr.tmpl
index a8c53738f..10a6d9b4b 100644
--- a/data/templates/frr/ospf6d.frr.tmpl
+++ b/data/templates/frr/ospf6d.frr.tmpl
@@ -1,7 +1,10 @@
!
{% if interface is defined and interface is not none %}
{% for iface, iface_config in interface.items() %}
-interface {{ iface }}
+interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
+{% if iface_config.area is defined and iface_config.area is not none %}
+ ipv6 ospf6 area {{ iface_config.area }}
+{% endif %}
{% if iface_config.cost is defined and iface_config.cost is not none %}
ipv6 ospf6 cost {{ iface_config.cost }}
{% endif %}
@@ -38,18 +41,14 @@ interface {{ iface }}
{% if iface_config.passive is defined %}
ipv6 ospf6 passive
{% endif %}
+exit
!
{% endfor %}
{% endif %}
!
-router ospf6
+router ospf6 {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
{% if area is defined and area is not none %}
{% for area_id, area_config in area.items() %}
-{% if area_config.interface is defined and area_config.interface is not none %}
-{% for interface in area_config.interface %}
- interface {{ interface }} area {{ area_id }}
-{% endfor %}
-{% endif %}
{% if area_config.area_type is defined and area_config.area_type is not none %}
{% for type, type_config in area_config.area_type.items() %}
area {{ area_id }} {{ type }} {{ 'no-summary' if type_config.no_summary is defined }}
@@ -89,4 +88,5 @@ router ospf6
redistribute {{ protocol }} {{ 'route-map ' + options.route_map if options.route_map is defined }}
{% endfor %}
{% endif %}
+exit
!
diff --git a/data/templates/frr/ospfd.frr.tmpl b/data/templates/frr/ospfd.frr.tmpl
index 90a6bbd56..a7b770f07 100644
--- a/data/templates/frr/ospfd.frr.tmpl
+++ b/data/templates/frr/ospfd.frr.tmpl
@@ -49,6 +49,10 @@ interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
{% if iface_config.network is defined and iface_config.network is not none %}
ip ospf network {{ iface_config.network }}
{% endif %}
+{% if iface_config.passive is defined %}
+ {{ 'no ' if iface_config.passive.disable is defined }}ip ospf passive
+{% endif %}
+exit
!
{% endfor %}
{% endif %}
@@ -158,18 +162,8 @@ router ospf {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
ospf router-id {{ parameters.router_id }}
{% endif %}
{% endif %}
-{% if passive_interface is defined and passive_interface is not none %}
-{% for interface in passive_interface %}
- passive-interface {{ interface }}
-{% endfor %}
-{% endif %}
-{% if passive_interface_exclude is defined and passive_interface_exclude is not none %}
-{% for interface in passive_interface_exclude if passive_interface_exclude is defined %}
-{% if interface.startswith('vlink') %}
-{% set interface = interface.upper() %}
-{% endif %}
- no passive-interface {{ interface }}
-{% endfor %}
+{% if passive_interface is defined and passive_interface.default is defined %}
+ passive-interface default
{% endif %}
{% if redistribute is defined and redistribute is not none %}
{% for protocol, protocols_options in redistribute.items() %}
@@ -189,4 +183,5 @@ router ospf {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
{# Timer values have default values #}
timers throttle spf {{ timers.throttle.spf.delay }} {{ timers.throttle.spf.initial_holdtime }} {{ timers.throttle.spf.max_holdtime }}
{% endif %}
+exit
!
diff --git a/data/templates/frr/policy.frr.tmpl b/data/templates/frr/policy.frr.tmpl
index 51adc1902..d3d3957a5 100644
--- a/data/templates/frr/policy.frr.tmpl
+++ b/data/templates/frr/policy.frr.tmpl
@@ -1,4 +1,3 @@
-!
{% if access_list is defined and access_list is not none %}
{% for acl, acl_config in access_list.items() | natural_sort %}
{% if acl_config.description is defined and acl_config.description is not none %}
@@ -60,7 +59,7 @@ ipv6 access-list {{ acl }} seq {{ rule }} {{ rule_config.action }} {{ src }} {{
{% for acl, acl_config in as_path_list.items() | natural_sort %}
{% if acl_config.rule is defined and acl_config.rule is not none %}
{% for rule, rule_config in acl_config.rule.items() | natural_sort %}
-bgp as-path access-list {{ acl }} {{ rule_config.action }} {{ rule_config.regex }}
+bgp as-path access-list {{ acl }} seq {{ rule }} {{ rule_config.action }} {{ rule_config.regex }}
{% endfor %}
{% endif %}
{% endfor %}
@@ -314,9 +313,9 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }}
set weight {{ rule_config.set.weight }}
{% endif %}
{% endif %}
-{% endfor %}
+exit
!
+{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
-!
diff --git a/data/templates/frr/ripd.frr.tmpl b/data/templates/frr/ripd.frr.tmpl
index cabc236f0..c44bb6d27 100644
--- a/data/templates/frr/ripd.frr.tmpl
+++ b/data/templates/frr/ripd.frr.tmpl
@@ -1,4 +1,3 @@
-!
{# RIP key-chain definition #}
{% if interface is defined and interface is not none %}
{% for iface, iface_config in interface.items() %}
@@ -9,7 +8,9 @@ key chain {{ iface }}-rip
{% if key_options.password is defined and key_options.password is not none %}
key-string {{ key_options.password }}
{% endif %}
+ exit
{% endfor %}
+exit
{% endif %}
{% endfor %}
{% endif %}
@@ -31,6 +32,8 @@ interface {{ iface }}
{% if iface_config.split_horizon is defined and iface_config.split_horizon.poison_reverse is defined %}
ip rip split-horizon poisoned-reverse
{% endif %}
+exit
+!
{% endfor %}
{% endif %}
!
@@ -89,6 +92,7 @@ router rip
{% endif %}
{% endif %}
{% include 'frr/rip_ripng.frr.j2' %}
+exit
!
{% if route_map is defined and route_map is not none %}
ip protocol rip route-map {{ route_map }}
diff --git a/data/templates/frr/ripngd.frr.tmpl b/data/templates/frr/ripngd.frr.tmpl
index 25df15121..ca7b9b5fb 100644
--- a/data/templates/frr/ripngd.frr.tmpl
+++ b/data/templates/frr/ripngd.frr.tmpl
@@ -1,4 +1,3 @@
-!
{# Interface specific configuration #}
{% if interface is defined and interface is not none %}
{% for iface, iface_config in interface.items() %}
@@ -9,6 +8,7 @@ interface {{ iface }}
{% if iface_config.split_horizon is defined and iface_config.split_horizon.poison_reverse is defined %}
ipv6 rip split-horizon poisoned-reverse
{% endif %}
+exit
{% endfor %}
{% endif %}
!
@@ -57,4 +57,9 @@ router ripng
{% endif %}
{% endif %}
{% include 'frr/rip_ripng.frr.j2' %}
+exit
+!
+{% if route_map is defined and route_map is not none %}
+ipv6 protocol ripng route-map {{ route_map }}
+{% endif %}
!
diff --git a/data/templates/frr/rpki.frr.tmpl b/data/templates/frr/rpki.frr.tmpl
index fbdfa27c3..7f9823f6b 100644
--- a/data/templates/frr/rpki.frr.tmpl
+++ b/data/templates/frr/rpki.frr.tmpl
@@ -14,4 +14,5 @@ rpki
{% if polling_period is defined and polling_period is not none %}
rpki polling_period {{ polling_period }}
{% endif %}
+exit
!
diff --git a/data/templates/frr/staticd.frr.tmpl b/data/templates/frr/staticd.frr.tmpl
index db59a44c2..bfe959c1d 100644
--- a/data/templates/frr/staticd.frr.tmpl
+++ b/data/templates/frr/staticd.frr.tmpl
@@ -15,6 +15,15 @@ vrf {{ vrf }}
{{ static_routes(ip_prefix, prefix, prefix_config) }}
{%- endfor -%}
{% endif %}
+{# IPv4 default routes from DHCP interfaces #}
+{% if dhcp is defined and dhcp is not none %}
+{% for interface in dhcp %}
+{% set next_hop = interface | get_dhcp_router %}
+{% if next_hop is defined and next_hop is not none %}
+{{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 210
+{% endif %}
+{% endfor %}
+{% endif %}
{# IPv6 routing #}
{% if route6 is defined and route6 is not none %}
{% for prefix, prefix_config in route6.items() %}
diff --git a/data/templates/frr/vrf-vni.frr.tmpl b/data/templates/frr/vrf-vni.frr.tmpl
index 51d4ede1b..299c9719e 100644
--- a/data/templates/frr/vrf-vni.frr.tmpl
+++ b/data/templates/frr/vrf-vni.frr.tmpl
@@ -1,7 +1,9 @@
-{% if vrf is defined and vrf is not none %}
+{% if name is defined and name is not none %}
+{% for vrf, vrf_config in name.items() %}
vrf {{ vrf }}
-{% if vni is defined and vni is not none %}
- vni {{ vni }}
-{% endif %}
+{% if vrf_config.vni is defined and vrf_config.vni is not none %}
+ vni {{ vrf_config.vni }}
+{% endif %}
exit-vrf
+{% endfor %}
{% endif %}
diff --git a/data/templates/https/override.conf.tmpl b/data/templates/https/override.conf.tmpl
new file mode 100644
index 000000000..824b1ba3b
--- /dev/null
+++ b/data/templates/https/override.conf.tmpl
@@ -0,0 +1,15 @@
+{% set vrf_command = 'ip vrf exec ' + vrf + ' ' if vrf is defined else '' %}
+[Unit]
+StartLimitIntervalSec=0
+After=vyos-router.service
+
+[Service]
+ExecStartPre=
+ExecStartPre={{vrf_command}}/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
+ExecStart=
+ExecStart={{vrf_command}}/usr/sbin/nginx -g 'daemon on; master_process on;'
+ExecReload=
+ExecReload={{vrf_command}}/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload
+Restart=always
+RestartPreventExitStatus=
+RestartSec=10
diff --git a/data/templates/ipsec/swanctl/profile.tmpl b/data/templates/ipsec/swanctl/profile.tmpl
index 948dd8f87..a5cae31c0 100644
--- a/data/templates/ipsec/swanctl/profile.tmpl
+++ b/data/templates/ipsec/swanctl/profile.tmpl
@@ -7,7 +7,7 @@
dmvpn-{{ name }}-{{ interface }} {
proposals = {{ ike_group[profile_conf.ike_group] | get_esp_ike_cipher | join(',') }}
version = {{ ike.key_exchange[4:] if ike is defined and ike.key_exchange is defined else "0" }}
- life_time = {{ ike.lifetime }}s
+ rekey_time = {{ ike.lifetime }}s
keyingtries = 0
{% if profile_conf.authentication is defined and profile_conf.authentication.mode is defined and profile_conf.authentication.mode == 'pre-shared-secret' %}
local {
diff --git a/data/templates/lcd/LCDd.conf.tmpl b/data/templates/lcd/LCDd.conf.tmpl
index 6cf6a440f..2c7ad920f 100644
--- a/data/templates/lcd/LCDd.conf.tmpl
+++ b/data/templates/lcd/LCDd.conf.tmpl
@@ -53,6 +53,8 @@ DriverPath=/usr/lib/x86_64-linux-gnu/lcdproc/
Driver=CFontzPacket
{% elif model == 'sdec' %}
Driver=sdeclcd
+{% elif model == 'hd44780' %}
+Driver=hd44780
{% endif %}
{% endif %}
@@ -128,5 +130,10 @@ USB=yes
## SDEC driver for Lanner, Watchguard, Sophos sppliances ##
[sdeclcd]
# No options
+{% elif model == 'hd44780' %}
+[hd44780]
+ConnectionType=ezio
+Device={{ device }}
+Size=16x2
{% endif %}
{% endif %}
diff --git a/data/templates/mdns-repeater/avahi-daemon.tmpl b/data/templates/mdns-repeater/avahi-daemon.tmpl
new file mode 100644
index 000000000..65bb5a306
--- /dev/null
+++ b/data/templates/mdns-repeater/avahi-daemon.tmpl
@@ -0,0 +1,18 @@
+[server]
+use-ipv4=yes
+use-ipv6=yes
+allow-interfaces={{ interface | join(', ') }}
+disallow-other-stacks=no
+
+[wide-area]
+enable-wide-area=yes
+
+[publish]
+disable-publishing=yes
+disable-user-service-publishing=yes
+publish-addresses=no
+publish-hinfo=no
+publish-workstation=no
+
+[reflector]
+enable-reflector=yes
diff --git a/data/templates/mdns-repeater/mdns-repeater.tmpl b/data/templates/mdns-repeater/mdns-repeater.tmpl
deleted file mode 100644
index 80f4ab047..000000000
--- a/data/templates/mdns-repeater/mdns-repeater.tmpl
+++ /dev/null
@@ -1,2 +0,0 @@
-### Autogenerated by mdns_repeater.py ###
-DAEMON_ARGS="{{ interface | join(' ') }}"
diff --git a/data/templates/netflow/uacctd.conf.tmpl b/data/templates/netflow/uacctd.conf.tmpl
index 1c183bb20..11fc76769 100644
--- a/data/templates/netflow/uacctd.conf.tmpl
+++ b/data/templates/netflow/uacctd.conf.tmpl
@@ -68,5 +68,8 @@ sfprobe_agentip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['agent-addr
{% if templatecfg['sflow']['sampling-rate'] != none %}
sampling_rate[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['sampling-rate'] }}
{% endif %}
+{% if templatecfg['sflow']['source-address'] != none %}
+sfprobe_source_ip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['source-address'] }}
+{% endif %}
{% endfor %}
{% endif %}
diff --git a/data/templates/ntp/ntpd.conf.tmpl b/data/templates/ntp/ntpd.conf.tmpl
index 2b56b53c3..38e68f24f 100644
--- a/data/templates/ntp/ntpd.conf.tmpl
+++ b/data/templates/ntp/ntpd.conf.tmpl
@@ -6,6 +6,8 @@
driftfile /var/lib/ntp/ntp.drift
# By default, only allow ntpd to query time sources, ignore any incoming requests
restrict default noquery nopeer notrap nomodify
+# Allow pool associations
+restrict source nomodify notrap noquery
# Local users have unrestricted access, allowing reconfiguration via ntpdc
restrict 127.0.0.1
restrict -6 ::1
diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl
index bdf88b85f..7a0470d0e 100644
--- a/data/templates/openvpn/server.conf.tmpl
+++ b/data/templates/openvpn/server.conf.tmpl
@@ -126,6 +126,12 @@ push "dhcp-option DNS6 {{ nameserver }}"
{% if server.domain_name is defined and server.domain_name is not none %}
push "dhcp-option DOMAIN {{ server.domain_name }}"
{% endif %}
+{% if server.mfa is defined and server.mfa is not none %}
+{% if server.mfa.totp is defined and server.mfa.totp is not none %}
+{% set totp_config = server.mfa.totp %}
+plugin "{{ plugin_dir}}/openvpn-otp.so" "otp_secrets=/config/auth/openvpn/{{ ifname }}-otp-secrets {{ 'otp_slop=' ~ totp_config.slop }} {{ 'totp_t0=' ~ totp_config.drift }} {{ 'totp_step=' ~ totp_config.step }} {{ 'totp_digits=' ~ totp_config.digits }} password_is_cr={{ '1' if totp_config.challenge == 'enable' else '0' }}"
+{% endif %}
+{% endif %}
{% endif %}
{% else %}
#
@@ -218,16 +224,3 @@ auth {{ hash }}
auth-user-pass {{ auth_user_pass_file }}
auth-retry nointeract
{% endif %}
-
-{% if openvpn_option is defined and openvpn_option is not none %}
-#
-# Custom options added by user (not validated)
-#
-{% for option in openvpn_option %}
-{% for argument in option.split('--') %}
-{% if argument is defined and argument != '' %}
---{{ argument }}
-{% endif %}
-{% endfor %}
-{% endfor %}
-{% endif %}
diff --git a/data/templates/openvpn/service-override.conf.tmpl b/data/templates/openvpn/service-override.conf.tmpl
new file mode 100644
index 000000000..069bdbd08
--- /dev/null
+++ b/data/templates/openvpn/service-override.conf.tmpl
@@ -0,0 +1,20 @@
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid
+{%- if openvpn_option is defined and openvpn_option is not none %}
+{% for option in openvpn_option %}
+{# Remove the '--' prefix from variable if it is presented #}
+{% if option.startswith('--') %}
+{% set option = option.split('--', maxsplit=1)[1] %}
+{% endif %}
+{# Workaround to pass '--push' options properly. Previously openvpn accepted this option without values in double-quotes #}
+{# But now it stopped doing this, so we need to add them for compatibility #}
+{# HOWEVER! This is a raw option and we do not promise that this or any other trick will work for all the cases. #}
+{# Using 'openvpn-option' you take all responsibility for compatibility for yourself. #}
+{% if option.startswith('push') and not (option.startswith('push "') and option.endswith('"')) %}
+{% set option = 'push \"%s\"'|format(option.split('push ', maxsplit=1)[1]) %}
+{% endif %}
+ --{{ option }}
+{%- endfor %}
+{% 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/tftp-server/default.tmpl b/data/templates/tftp-server/default.tmpl
index 6b2d6a903..a7edf60ad 100644
--- a/data/templates/tftp-server/default.tmpl
+++ b/data/templates/tftp-server/default.tmpl
@@ -1,2 +1,7 @@
### Autogenerated by tftp_server.py ###
DAEMON_ARGS="--listen --user tftp --address {{ listen_address }} {{ "--create --umask 000" if allow_upload is defined }} --secure {{ directory }}"
+{% if vrf is defined %}
+VRF_ARGS="ip vrf exec {{ vrf }}"
+{% else %}
+VRF_ARGS=""
+{% endif %}
diff --git a/data/templates/vrrp/keepalived.conf.tmpl b/data/templates/vrrp/keepalived.conf.tmpl
index b4824a994..b93aa4bc9 100644
--- a/data/templates/vrrp/keepalived.conf.tmpl
+++ b/data/templates/vrrp/keepalived.conf.tmpl
@@ -5,9 +5,6 @@
global_defs {
dynamic_interfaces
script_user root
- # Don't run scripts configured to be run as root if any part of the path
- # is writable by a non-root user.
- enable_script_security
notify_fifo /run/keepalived/keepalived_notify_fifo
notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py
}
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 f3a26e73e..08c04b439 100644
--- a/debian/control
+++ b/debian/control
@@ -33,6 +33,7 @@ Architecture: amd64 arm64
Depends:
${python3:Depends},
accel-ppp,
+ avahi-daemon,
beep,
bmon,
bsdmainutils,
@@ -72,6 +73,7 @@ Depends:
iw,
keepalived (>=2.0.5),
lcdproc,
+ lcdproc-extra-drivers,
libatomic1,
libcharon-extra-plugins (>=5.9),
libcharon-extauth-plugins (>=5.9),
@@ -86,7 +88,6 @@ Depends:
lldpd,
lm-sensors,
lsscsi,
- mdns-repeater,
minisign,
modemmanager,
mtr-tiny,
@@ -105,6 +106,7 @@ Depends:
openvpn,
openvpn-auth-ldap,
openvpn-auth-radius,
+ openvpn-otp,
pciutils,
pdns-recursor,
pmacct (>= 1.6.0),
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
index d332e0d36..29d74390f 100644
--- a/debian/vyos-1x.install
+++ b/debian/vyos-1x.install
@@ -1,6 +1,8 @@
+etc/cron.d
etc/cron.hourly
etc/dhcp
etc/ipsec.d
+etc/logrotate.d
etc/netplug
etc/opennhrp
etc/ppp
diff --git a/interface-definitions/bcast-relay.xml.in b/interface-definitions/bcast-relay.xml.in
index 3f781f07f..aeaa5ab37 100644
--- a/interface-definitions/bcast-relay.xml.in
+++ b/interface-definitions/bcast-relay.xml.in
@@ -39,15 +39,7 @@
<help>Description</help>
</properties>
</leafNode>
- <leafNode name="interface">
- <properties>
- <help>Interface to repeat UDP broadcasts to [REQUIRED]</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <multi/>
- </properties>
- </leafNode>
+ #include <include/generic-interface-multi.xml.i>
#include <include/port-number.xml.i>
</children>
</tagNode>
diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in
index bf672307c..30c7110b8 100644
--- a/interface-definitions/containers.xml.in
+++ b/interface-definitions/containers.xml.in
@@ -21,6 +21,42 @@
<valueless/>
</properties>
</leafNode>
+ <leafNode name="cap-add">
+ <properties>
+ <help>Container capabilities/permissions</help>
+ <completionHelp>
+ <list>net-admin net-bind-service net-raw setpcap sys-admin sys-time</list>
+ </completionHelp>
+ <valueHelp>
+ <format>net-admin</format>
+ <description>Network operations (interface, firewall, routing tables)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>net-bind-service</format>
+ <description>Bind a socket to privileged ports (port numbers less than 1024)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>net-raw</format>
+ <description>Permission to create raw network sockets</description>
+ </valueHelp>
+ <valueHelp>
+ <format>setpcap</format>
+ <description>Capability sets (from bounded or inherited set)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sys-admin</format>
+ <description>Administation operations (quotactl, mount, sethostname, setdomainame)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sys-time</format>
+ <description>Permission to set system clock</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(net-admin|net-bind-service|net-raw|setpcap|sys-admin|sys-time)$</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
#include <include/generic-description.xml.i>
#include <include/generic-disable-node.xml.i>
<tagNode name="environment">
@@ -141,7 +177,7 @@
</tagNode>
<leafNode name="restart">
<properties>
- <help>Mount a volume into the container</help>
+ <help>Restart options for container</help>
<completionHelp>
<list>no on-failure always</list>
</completionHelp>
diff --git a/interface-definitions/dhcp-relay.xml.in b/interface-definitions/dhcp-relay.xml.in
index 0d485ef80..483e776a7 100644
--- a/interface-definitions/dhcp-relay.xml.in
+++ b/interface-definitions/dhcp-relay.xml.in
@@ -9,15 +9,7 @@
<priority>910</priority>
</properties>
<children>
- <leafNode name="interface">
- <properties>
- <help>DHCP relay interface [REQUIRED]</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py -b</script>
- </completionHelp>
- <multi/>
- </properties>
- </leafNode>
+ #include <include/generic-interface-multi-broadcast.xml.i>
<node name="relay-options">
<properties>
<help>Relay options</help>
diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in
index 47bdc4db1..d1ed579e9 100644
--- a/interface-definitions/dhcp-server.xml.in
+++ b/interface-definitions/dhcp-server.xml.in
@@ -254,9 +254,9 @@
<properties>
<help>DHCP lease range</help>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ <regex>^[-_a-zA-Z0-9.]+$</regex>
</constraint>
- <constraintErrorMessage>Invalid DHCP lease range name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ <constraintErrorMessage>Invalid range name, may only be alphanumeric, dot and hyphen</constraintErrorMessage>
</properties>
<children>
<leafNode name="start">
@@ -289,9 +289,9 @@
<properties>
<help>Name of static mapping</help>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ <regex>^[-_a-zA-Z0-9.]+$</regex>
</constraint>
- <constraintErrorMessage>Invalid static mapping name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ <constraintErrorMessage>Invalid static mapping name, may only be alphanumeric, dot and hyphen</constraintErrorMessage>
</properties>
<children>
#include <include/generic-disable-node.xml.i>
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/flow-accounting-conf.xml.in b/interface-definitions/flow-accounting-conf.xml.in
index b0f308afd..b98794792 100644
--- a/interface-definitions/flow-accounting-conf.xml.in
+++ b/interface-definitions/flow-accounting-conf.xml.in
@@ -136,15 +136,7 @@
</constraint>
</properties>
</leafNode>
- <leafNode name="interface">
- <properties>
- <help>Interface for flow-accounting [REQUIRED]</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <multi/>
- </properties>
- </leafNode>
+ #include <include/generic-interface-multi.xml.i>
<node name="netflow">
<properties>
<help>NetFlow settings</help>
@@ -428,6 +420,7 @@
</leafNode>
</children>
</tagNode>
+ #include <include/source-address-ipv4-ipv6.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in
index bb6f71744..d26cd5e7a 100644
--- a/interface-definitions/https.xml.in
+++ b/interface-definitions/https.xml.in
@@ -121,6 +121,7 @@
<help>TLS certificates</help>
</properties>
<children>
+ #include <include/pki/ca-certificate.xml.i>
#include <include/pki/certificate.xml.i>
<node name="certbot" owner="${vyos_conf_scripts_dir}/le_cert.py">
<properties>
@@ -142,6 +143,7 @@
</node>
</children>
</node>
+ #include <include/interface/vrf.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/include/accel-ppp/auth-local-users.xml.i b/interface-definitions/include/accel-ppp/auth-local-users.xml.i
index 308d6510d..1b40a9ea7 100644
--- a/interface-definitions/include/accel-ppp/auth-local-users.xml.i
+++ b/interface-definitions/include/accel-ppp/auth-local-users.xml.i
@@ -18,6 +18,9 @@
<leafNode name="static-ip">
<properties>
<help>Static client IP address</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
</properties>
<defaultValue>*</defaultValue>
</leafNode>
diff --git a/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i b/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i
index bd3dadf8d..a692f2335 100644
--- a/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i
+++ b/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i
@@ -27,6 +27,7 @@
<validator name="numeric" argument="--range 48-128"/>
</constraint>
</properties>
+ <defaultValue>64</defaultValue>
</leafNode>
</children>
</tagNode>
diff --git a/interface-definitions/include/accel-ppp/ppp-options-ipv4.xml.i b/interface-definitions/include/accel-ppp/ppp-options-ipv4.xml.i
new file mode 100644
index 000000000..3e065329d
--- /dev/null
+++ b/interface-definitions/include/accel-ppp/ppp-options-ipv4.xml.i
@@ -0,0 +1,23 @@
+<!-- include start from accel-ppp/ppp-options-ipv4.xml.i -->
+<leafNode name="ipv4">
+ <properties>
+ <help>IPv4 negotiation algorithm</help>
+ <constraint>
+ <regex>^(deny|allow)$</regex>
+ </constraint>
+ <constraintErrorMessage>invalid value</constraintErrorMessage>
+ <valueHelp>
+ <format>deny</format>
+ <description>Do not negotiate IPv4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>allow</format>
+ <description>Negotiate IPv4 only if client requests</description>
+ </valueHelp>
+ <completionHelp>
+ <list>deny allow</list>
+ </completionHelp>
+ </properties>
+ <defaultValue>allow</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/accel-ppp/ppp-options-ipv6.xml.i b/interface-definitions/include/accel-ppp/ppp-options-ipv6.xml.i
index cd40a1f96..b9fbac5c6 100644
--- a/interface-definitions/include/accel-ppp/ppp-options-ipv6.xml.i
+++ b/interface-definitions/include/accel-ppp/ppp-options-ipv6.xml.i
@@ -26,5 +26,6 @@
<list>deny allow prefer require</list>
</completionHelp>
</properties>
+ <defaultValue>deny</defaultValue>
</leafNode>
<!-- include end -->
diff --git a/interface-definitions/include/bfd-common.xml.i b/interface-definitions/include/bfd-common.xml.i
index 1d6ab5d55..8379784f7 100644
--- a/interface-definitions/include/bfd-common.xml.i
+++ b/interface-definitions/include/bfd-common.xml.i
@@ -15,7 +15,7 @@
<help>Minimum interval of receiving control packets</help>
<valueHelp>
<format>u32:10-60000</format>
- <description>Interval in milliseconds</description>
+ <description>Interval in milliseconds (default: 300)</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 10-60000"/>
@@ -28,7 +28,7 @@
<help>Minimum interval of transmitting control packets</help>
<valueHelp>
<format>u32:10-60000</format>
- <description>Interval in milliseconds</description>
+ <description>Interval in milliseconds (default: 300)</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 10-60000"/>
@@ -41,7 +41,7 @@
<help>Multiplier to determine packet loss</help>
<valueHelp>
<format>u32:2-255</format>
- <description>Remote transmission interval will be multiplied by this value</description>
+ <description>Remote transmission interval will be multiplied by this value (default: 3)</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 2-255"/>
@@ -63,6 +63,12 @@
</leafNode>
</children>
</node>
+<leafNode name="passive">
+ <properties>
+ <help>Do not attempt to start sessions</help>
+ <valueless/>
+ </properties>
+</leafNode>
<leafNode name="shutdown">
<properties>
<help>Disable this peer</help>
diff --git a/interface-definitions/include/bgp/afi-aggregate-address.xml.i b/interface-definitions/include/bgp/afi-aggregate-address.xml.i
index 646751c32..c1b7958da 100644
--- a/interface-definitions/include/bgp/afi-aggregate-address.xml.i
+++ b/interface-definitions/include/bgp/afi-aggregate-address.xml.i
@@ -5,6 +5,7 @@
<valueless/>
</properties>
</leafNode>
+#include <include/route-map.xml.i>
<leafNode name="summary-only">
<properties>
<help>Announce the aggregate summary network only</help>
diff --git a/interface-definitions/include/bgp/afi-l2vpn-common.xml.i b/interface-definitions/include/bgp/afi-l2vpn-common.xml.i
index 8deb189ab..d586635c8 100644
--- a/interface-definitions/include/bgp/afi-l2vpn-common.xml.i
+++ b/interface-definitions/include/bgp/afi-l2vpn-common.xml.i
@@ -25,7 +25,7 @@
<description>Route target (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description>
</valueHelp>
<constraint>
- <validator name="bgp-route-target" argument="--single"/>
+ <validator name="bgp-rd-rt" argument="--route-target"/>
</constraint>
</properties>
</leafNode>
@@ -37,7 +37,7 @@
<description>Route target (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description>
</valueHelp>
<constraint>
- <validator name="bgp-route-target" argument="--single"/>
+ <validator name="bgp-rd-rt" argument="--route-target"/>
</constraint>
</properties>
</leafNode>
@@ -49,7 +49,7 @@
<description>Route target (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description>
</valueHelp>
<constraint>
- <validator name="bgp-route-target" argument="--single"/>
+ <validator name="bgp-rd-rt" argument="--route-target"/>
</constraint>
</properties>
</leafNode>
diff --git a/interface-definitions/include/bgp/afi-route-target-vpn.xml.i b/interface-definitions/include/bgp/afi-route-target-vpn.xml.i
index 1dc184a02..5784f9eac 100644
--- a/interface-definitions/include/bgp/afi-route-target-vpn.xml.i
+++ b/interface-definitions/include/bgp/afi-route-target-vpn.xml.i
@@ -1,7 +1,7 @@
<!-- include start from bgp/route-target-both.xml.i -->
<node name="route-target">
<properties>
- <help>Specify route distinguisher</help>
+ <help>Specify route target list</help>
</properties>
<children>
<node name="vpn">
@@ -17,7 +17,7 @@
<description>Space separated route target list (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description>
</valueHelp>
<constraint>
- <validator name="bgp-route-target" argument="--multi"/>
+ <validator name="bgp-rd-rt" argument="--route-target-multi"/>
</constraint>
</properties>
</leafNode>
@@ -29,7 +29,7 @@
<description>Space separated route target list (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description>
</valueHelp>
<constraint>
- <validator name="bgp-route-target" argument="--multi"/>
+ <validator name="bgp-rd-rt" argument="--route-target-multi"/>
</constraint>
</properties>
</leafNode>
@@ -41,7 +41,7 @@
<description>Space separated route target list (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description>
</valueHelp>
<constraint>
- <validator name="bgp-route-target" argument="--multi"/>
+ <validator name="bgp-rd-rt" argument="--route-target-multi"/>
</constraint>
</properties>
</leafNode>
diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i
index 30033bc50..2dfae517e 100644
--- a/interface-definitions/include/bgp/protocol-common-config.xml.i
+++ b/interface-definitions/include/bgp/protocol-common-config.xml.i
@@ -1253,12 +1253,6 @@
</constraint>
</properties>
</leafNode>
- <leafNode name="no-ipv4-unicast">
- <properties>
- <help>Deactivate IPv4 unicast for a peer by default</help>
- <valueless/>
- </properties>
- </leafNode>
</children>
</node>
<leafNode name="deterministic-med">
diff --git a/interface-definitions/include/bgp/route-distinguisher.xml.i b/interface-definitions/include/bgp/route-distinguisher.xml.i
index 6d0aa3ef1..8bc5b452e 100644
--- a/interface-definitions/include/bgp/route-distinguisher.xml.i
+++ b/interface-definitions/include/bgp/route-distinguisher.xml.i
@@ -7,7 +7,7 @@
<description>Route Distinguisher, (x.x.x.x:yyy|xxxx:yyyy)</description>
</valueHelp>
<constraint>
- <regex>^((25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)(\.(25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)){3}|[0-9]{1,10}):[0-9]{1,5}$</regex>
+ <validator name="bgp-rd-rt" argument="--route-distinguisher"/>
</constraint>
</properties>
</leafNode>
diff --git a/interface-definitions/include/dhcp/ntp-server.xml.i b/interface-definitions/include/dhcp/ntp-server.xml.i
index 32d8207e5..4d7235aa1 100644
--- a/interface-definitions/include/dhcp/ntp-server.xml.i
+++ b/interface-definitions/include/dhcp/ntp-server.xml.i
@@ -1,15 +1,15 @@
<!-- include start from dhcp/ntp-server.xml.i -->
- <leafNode name="ntp-server">
- <properties>
- <help>IP address of NTP server</help>
- <valueHelp>
- <format>ipv4</format>
- <description>NTP server IPv4 address</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-address"/>
- </constraint>
- <multi/>
- </properties>
- </leafNode>
+<leafNode name="ntp-server">
+ <properties>
+ <help>IP address of NTP server</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>NTP server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+</leafNode>
<!-- include end -->
diff --git a/interface-definitions/include/generic-disable-node.xml.i b/interface-definitions/include/generic-disable-node.xml.i
index bb4fa5c4b..97a328ecc 100644
--- a/interface-definitions/include/generic-disable-node.xml.i
+++ b/interface-definitions/include/generic-disable-node.xml.i
@@ -1,7 +1,7 @@
<!-- include start from generic-disable-node.xml.i -->
<leafNode name="disable">
<properties>
- <help>Temporary disable</help>
+ <help>Disable instance</help>
<valueless/>
</properties>
</leafNode>
diff --git a/interface-definitions/include/generic-interface-broadcast.xml.i b/interface-definitions/include/generic-interface-broadcast.xml.i
new file mode 100644
index 000000000..6f76dde1a
--- /dev/null
+++ b/interface-definitions/include/generic-interface-broadcast.xml.i
@@ -0,0 +1,17 @@
+<!-- include start from generic-interface-broadcast.xml.i -->
+<leafNode name="interface">
+ <properties>
+ <help>Interface Name to use</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <constraint>
+ <validator name="interface-name"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/generic-interface-multi-broadcast.xml.i b/interface-definitions/include/generic-interface-multi-broadcast.xml.i
new file mode 100644
index 000000000..00638f3b7
--- /dev/null
+++ b/interface-definitions/include/generic-interface-multi-broadcast.xml.i
@@ -0,0 +1,18 @@
+<!-- include start from generic-interface-multi-broadcast.xml.i -->
+<leafNode name="interface">
+ <properties>
+ <help>Interface Name to use</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --broadcast</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/generic-interface-multi.xml.i b/interface-definitions/include/generic-interface-multi.xml.i
new file mode 100644
index 000000000..44e87775c
--- /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 Name 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/generic-interface.xml.i b/interface-definitions/include/generic-interface.xml.i
new file mode 100644
index 000000000..50af718a5
--- /dev/null
+++ b/interface-definitions/include/generic-interface.xml.i
@@ -0,0 +1,17 @@
+<!-- include start from generic-interface.xml.i -->
+<leafNode name="interface">
+ <properties>
+ <help>Interface Name 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>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/netns.xml.i b/interface-definitions/include/interface/netns.xml.i
new file mode 100644
index 000000000..39f9118fa
--- /dev/null
+++ b/interface-definitions/include/interface/netns.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from interface/netns.xml.i -->
+<leafNode name="netns">
+ <properties>
+ <help>Network namespace name</help>
+ <valueHelp>
+ <format>text</format>
+ <description>Network namespace name</description>
+ </valueHelp>
+ <completionHelp>
+ <path>netns name</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/vrf.xml.i b/interface-definitions/include/interface/vrf.xml.i
index 5ad978a27..8605f56e8 100644
--- a/interface-definitions/include/interface/vrf.xml.i
+++ b/interface-definitions/include/interface/vrf.xml.i
@@ -3,7 +3,7 @@
<properties>
<help>VRF instance name</help>
<valueHelp>
- <format>text</format>
+ <format>txt</format>
<description>VRF instance name</description>
</valueHelp>
<completionHelp>
diff --git a/interface-definitions/include/listen-address-vrf.xml.i b/interface-definitions/include/listen-address-vrf.xml.i
new file mode 100644
index 000000000..7ec9eace4
--- /dev/null
+++ b/interface-definitions/include/listen-address-vrf.xml.i
@@ -0,0 +1,25 @@
+<!-- include start from listen-address-vrf.xml.i -->
+<tagNode name="listen-address">
+ <properties>
+ <help>Local IP addresses for service to listen on</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_local_ips.sh --both</script>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IP address to listen for incoming connections</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address to listen for incoming connections</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/interface/vrf.xml.i>
+ </children>
+</tagNode>
+<!-- include end -->
diff --git a/interface-definitions/include/nat-translation-options.xml.i b/interface-definitions/include/nat-translation-options.xml.i
index defc8c0d5..df2f76397 100644
--- a/interface-definitions/include/nat-translation-options.xml.i
+++ b/interface-definitions/include/nat-translation-options.xml.i
@@ -16,7 +16,7 @@
</valueHelp>
<valueHelp>
<format>random</format>
- <description>Random source or destination address allocation for each connection (defaut)</description>
+ <description>Random source or destination address allocation for each connection (default)</description>
</valueHelp>
<constraint>
<regex>^(persistent|random)$</regex>
diff --git a/interface-definitions/include/ospf/protocol-common-config.xml.i b/interface-definitions/include/ospf/protocol-common-config.xml.i
index 982e519a9..ac165a157 100644
--- a/interface-definitions/include/ospf/protocol-common-config.xml.i
+++ b/interface-definitions/include/ospf/protocol-common-config.xml.i
@@ -436,6 +436,14 @@
<constraintErrorMessage>Must be broadcast, non-broadcast, point-to-multipoint or point-to-point</constraintErrorMessage>
</properties>
</leafNode>
+ <node name="passive">
+ <properties>
+ <help>Suppress routing updates on an interface</help>
+ </properties>
+ <children>
+ #include <include/generic-disable-node.xml.i>
+ </children>
+ </node>
</children>
</tagNode>
#include <include/ospf/log-adjacency-changes.xml.i>
@@ -597,26 +605,19 @@
#include <include/router-id.xml.i>
</children>
</node>
-#include <include/routing-passive-interface.xml.i>
-<leafNode name="passive-interface-exclude">
+<leafNode name="passive-interface">
<properties>
- <help>Interface to exclude when using 'passive-interface default'</help>
+ <help>Suppress routing updates on an interface</help>
<completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
+ <list>default</list>
</completionHelp>
<valueHelp>
- <format>txt</format>
- <description>Interface to exclude when suppressing routing updates</description>
- </valueHelp>
- <valueHelp>
- <format>vlinkN</format>
- <description>Virtual-link interface to exclude when suppressing routing updates</description>
+ <format>default</format>
+ <description>Default to suppress routing updates on all interfaces</description>
</valueHelp>
<constraint>
- <validator name="interface-name"/>
- <regex>^(vlink[0-9]+)$</regex>
+ <regex>^(default)$</regex>
</constraint>
- <multi/>
</properties>
</leafNode>
<node name="redistribute">
diff --git a/interface-definitions/include/ospfv3/protocol-common-config.xml.i b/interface-definitions/include/ospfv3/protocol-common-config.xml.i
new file mode 100644
index 000000000..a93939a34
--- /dev/null
+++ b/interface-definitions/include/ospfv3/protocol-common-config.xml.i
@@ -0,0 +1,241 @@
+<!-- include start from ospfv3/protocol-common-config.xml.i -->
+<tagNode name="area">
+ <properties>
+ <help>OSPFv3 Area</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Area ID as a decimal value</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Area ID in IP address forma</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <node name="area-type">
+ <properties>
+ <help>OSPFv3 Area type</help>
+ </properties>
+ <children>
+ <node name="stub">
+ <properties>
+ <help>Stub OSPFv3 area</help>
+ </properties>
+ <children>
+ <leafNode name="no-summary">
+ <properties>
+ <help>Do not inject inter-area routes into the stub</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="export-list">
+ <properties>
+ <help>Name of export-list</help>
+ <completionHelp>
+ <path>policy access-list6</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="import-list">
+ <properties>
+ <help>Name of import-list</help>
+ <completionHelp>
+ <path>policy access-list6</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <tagNode name="range">
+ <properties>
+ <help>Specify IPv6 prefix (border routers only)</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>Specify IPv6 prefix (border routers only)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="advertise">
+ <properties>
+ <help>Advertise this range</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="not-advertise">
+ <properties>
+ <help>Do not advertise this range</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+</tagNode>
+<node name="distance">
+ <properties>
+ <help>Administrative distance</help>
+ </properties>
+ <children>
+ #include <include/ospf/distance-global.xml.i>
+ <node name="ospfv3">
+ <properties>
+ <help>OSPFv3 administrative distance</help>
+ </properties>
+ <children>
+ #include <include/ospf/distance-per-protocol.xml.i>
+ </children>
+ </node>
+ </children>
+</node>
+<tagNode name="interface">
+ <properties>
+ <help>Enable routing on an IPv6 interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface used for routing information exchange</description>
+ </valueHelp>
+ <constraint>
+ <validator name="interface-name"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="area">
+ <properties>
+ <help>Enable OSPF on this interface</help>
+ <completionHelp>
+ <path>protocols ospfv3 area</path>
+ </completionHelp>
+ <valueHelp>
+ <format>u32</format>
+ <description>OSPF area ID as decimal notation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>OSPF area ID in IP address notation</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/ospf/intervals.xml.i>
+ #include <include/ospf/interface-common.xml.i>
+ <leafNode name="ifmtu">
+ <properties>
+ <help>Interface MTU</help>
+ <valueHelp>
+ <format>u32:1-65535</format>
+ <description>Interface MTU</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="instance-id">
+ <properties>
+ <help>Instance Id (default: 0)</help>
+ <valueHelp>
+ <format>u32:0-255</format>
+ <description>Instance Id</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ </properties>
+ <defaultValue>0</defaultValue>
+ </leafNode>
+ <leafNode name="network">
+ <properties>
+ <help>Network type</help>
+ <completionHelp>
+ <list>broadcast point-to-point</list>
+ </completionHelp>
+ <valueHelp>
+ <format>broadcast</format>
+ <description>Broadcast network type</description>
+ </valueHelp>
+ <valueHelp>
+ <format>point-to-point</format>
+ <description>Point-to-point network type</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(broadcast|point-to-point)$</regex>
+ </constraint>
+ <constraintErrorMessage>Must be broadcast or point-to-point</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ #include <include/isis/passive.xml.i>
+ </children>
+</tagNode>
+#include <include/ospf/log-adjacency-changes.xml.i>
+<node name="parameters">
+ <properties>
+ <help>OSPFv3 specific parameters</help>
+ </properties>
+ <children>
+ #include <include/router-id.xml.i>
+ </children>
+</node>
+<node name="redistribute">
+ <properties>
+ <help>Redistribute information from another routing protocol</help>
+ </properties>
+ <children>
+ <node name="bgp">
+ <properties>
+ <help>Redistribute BGP routes</help>
+ </properties>
+ <children>
+ #include <include/route-map.xml.i>
+ </children>
+ </node>
+ <node name="connected">
+ <properties>
+ <help>Redistribute connected routes</help>
+ </properties>
+ <children>
+ #include <include/route-map.xml.i>
+ </children>
+ </node>
+ <node name="kernel">
+ <properties>
+ <help>Redistribute kernel routes</help>
+ </properties>
+ <children>
+ #include <include/route-map.xml.i>
+ </children>
+ </node>
+ <node name="ripng">
+ <properties>
+ <help>Redistribute RIPNG routes</help>
+ </properties>
+ <children>
+ #include <include/route-map.xml.i>
+ </children>
+ </node>
+ <node name="static">
+ <properties>
+ <help>Redistribute static routes</help>
+ </properties>
+ <children>
+ #include <include/route-map.xml.i>
+ </children>
+ </node>
+ </children>
+</node>
+#include <include/route-map.xml.i>
+<!-- include end -->
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in
index 05e0d8461..17879cf1e 100644
--- a/interface-definitions/interfaces-bonding.xml.in
+++ b/interface-definitions/interfaces-bonding.xml.in
@@ -177,6 +177,13 @@
<completionHelp>
<script>${vyos_completion_dir}/list_interfaces.py --bondable</script>
</completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <constraint>
+ <validator name="interface-name"/>
+ </constraint>
<multi/>
</properties>
</leafNode>
@@ -189,6 +196,13 @@
<completionHelp>
<script>${vyos_completion_dir}/list_interfaces.py --bondable</script>
</completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <constraint>
+ <validator name="interface-name"/>
+ </constraint>
</properties>
</leafNode>
#include <include/interface/vif-s.xml.i>
diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in
index 2bc88c1a7..4d4c44160 100644
--- a/interface-definitions/interfaces-dummy.xml.in
+++ b/interface-definitions/interfaces-dummy.xml.in
@@ -27,6 +27,7 @@
#include <include/interface/source-validation.xml.i>
</children>
</node>
+ #include <include/interface/netns.xml.i>
#include <include/interface/vrf.xml.i>
</children>
</tagNode>
diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in
index d67549d87..6b4440688 100644
--- a/interface-definitions/interfaces-openvpn.xml.in
+++ b/interface-definitions/interfaces-openvpn.xml.in
@@ -633,6 +633,92 @@
</properties>
<defaultValue>net30</defaultValue>
</leafNode>
+ <node name="mfa">
+ <properties>
+ <help>multi-factor authentication</help>
+ </properties>
+ <children>
+ <node name="totp">
+ <properties>
+ <help>Time-based one-time passwords</help>
+ </properties>
+ <children>
+ <leafNode name="slop">
+ <properties>
+ <help>Maximum allowed clock slop in seconds (default: 180)</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>180</defaultValue>
+ </leafNode>
+ <leafNode name="drift">
+ <properties>
+ <help>Time drift in seconds (default: 0)</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>0</defaultValue>
+ </leafNode>
+ <leafNode name="step">
+ <properties>
+ <help>Step value for totp in seconds (default: 30)</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>30</defaultValue>
+ </leafNode>
+ <leafNode name="digits">
+ <properties>
+ <help>Number of digits to use for totp hash (default: 6)</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ <defaultValue>6</defaultValue>
+ </leafNode>
+ <leafNode name="challenge">
+ <properties>
+ <help>Expect password as result of a challenge response protocol (default: enabled)</help>
+ <completionHelp>
+ <list>disable enable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable challenge-response</description>
+ </valueHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable chalenge-response (default)</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(disable|enable)$</regex>
+ </constraint>
+ </properties>
+ <defaultValue>enable</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
</children>
</node>
<leafNode name="shared-secret-key">
diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in
index 7450ef2af..cca732f82 100644
--- a/interface-definitions/interfaces-tunnel.xml.in
+++ b/interface-definitions/interfaces-tunnel.xml.in
@@ -54,7 +54,6 @@
</constraint>
</properties>
</leafNode>
- #include <include/dhcp-interface.xml.i>
<leafNode name="encapsulation">
<properties>
<help>Encapsulation of this tunnel interface</help>
diff --git a/interface-definitions/netns.xml.in b/interface-definitions/netns.xml.in
new file mode 100644
index 000000000..80de805fb
--- /dev/null
+++ b/interface-definitions/netns.xml.in
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="netns" owner="${vyos_conf_scripts_dir}/netns.py">
+ <properties>
+ <help>Network namespace</help>
+ <priority>299</priority>
+ </properties>
+ <children>
+ <tagNode name="name">
+ <properties>
+ <help>Network namespace name</help>
+ <constraint>
+ <regex>^[a-zA-Z0-9-_]{1,100}</regex>
+ </constraint>
+ <constraintErrorMessage>Netns name must be alphanumeric and can contain hyphens and underscores.</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/interface/description.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in
index cf65daf00..225f9a6f9 100644
--- a/interface-definitions/policy.xml.in
+++ b/interface-definitions/policy.xml.in
@@ -597,14 +597,7 @@
</completionHelp>
</properties>
</leafNode>
- <leafNode name="interface">
- <properties>
- <help>First hop interface of a route to match</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- </properties>
- </leafNode>
+ #include <include/generic-interface.xml.i>
<node name="ip">
<properties>
<help>IP prefix parameters to match</help>
diff --git a/interface-definitions/protocols-bfd.xml.in b/interface-definitions/protocols-bfd.xml.in
index cc3c3bf12..d5a968001 100644
--- a/interface-definitions/protocols-bfd.xml.in
+++ b/interface-definitions/protocols-bfd.xml.in
@@ -43,14 +43,7 @@
<help>Bind listener to specified interface/address, mandatory for IPv6</help>
</properties>
<children>
- <leafNode name="interface">
- <properties>
- <help>Local interface to bind our peer listener to</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- </properties>
- </leafNode>
+ #include <include/generic-interface.xml.i>
<leafNode name="address">
<properties>
<help>Local address to bind our peer listener to</help>
@@ -80,6 +73,7 @@
<valueless/>
</properties>
</leafNode>
+ #include <include/interface/vrf.xml.i>
</children>
</tagNode>
<tagNode name="profile">
diff --git a/interface-definitions/protocols-mpls.xml.in b/interface-definitions/protocols-mpls.xml.in
index e7646e625..be8e30c18 100644
--- a/interface-definitions/protocols-mpls.xml.in
+++ b/interface-definitions/protocols-mpls.xml.in
@@ -524,15 +524,7 @@
</node>
</children>
</node>
- <leafNode name="interface">
- <properties>
- <help>Enable LDP and neighbor discovery on interface</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <multi/>
- </properties>
- </leafNode>
+ #include <include/generic-interface-multi.xml.i>
</children>
</node>
<node name="parameters">
@@ -560,15 +552,7 @@
</leafNode>
</children>
</node>
- <leafNode name="interface">
- <properties>
- <help>Enable MPLS packet processing on interface</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <multi/>
- </properties>
- </leafNode>
+ #include <include/generic-interface-multi.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/protocols-ospfv3.xml.in b/interface-definitions/protocols-ospfv3.xml.in
index 99cfec661..2b98ffa7b 100644
--- a/interface-definitions/protocols-ospfv3.xml.in
+++ b/interface-definitions/protocols-ospfv3.xml.in
@@ -8,241 +8,7 @@
<priority>620</priority>
</properties>
<children>
- <tagNode name="area">
- <properties>
- <help>OSPFv3 Area</help>
- <valueHelp>
- <format>u32</format>
- <description>Area ID as a decimal value</description>
- </valueHelp>
- <valueHelp>
- <format>ipv4</format>
- <description>Area ID in IP address forma</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-4294967295"/>
- <validator name="ip-address"/>
- </constraint>
- </properties>
- <children>
- <node name="area-type">
- <properties>
- <help>OSPFv3 Area type</help>
- </properties>
- <children>
- <node name="stub">
- <properties>
- <help>Stub OSPFv3 area</help>
- </properties>
- <children>
- <leafNode name="no-summary">
- <properties>
- <help>Do not inject inter-area routes into the stub</help>
- <valueless/>
- </properties>
- </leafNode>
- </children>
- </node>
- </children>
- </node>
- <leafNode name="export-list">
- <properties>
- <help>Name of export-list</help>
- <completionHelp>
- <path>policy access-list6</path>
- </completionHelp>
- </properties>
- </leafNode>
- <leafNode name="import-list">
- <properties>
- <help>Name of import-list</help>
- <completionHelp>
- <path>policy access-list6</path>
- </completionHelp>
- </properties>
- </leafNode>
- <leafNode name="interface">
- <properties>
- <help>Enable routing on an IPv6 interface</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <valueHelp>
- <format>txt</format>
- <description>Interface used for routing information exchange</description>
- </valueHelp>
- <constraint>
- <validator name="interface-name"/>
- </constraint>
- <multi/>
- </properties>
- </leafNode>
- <tagNode name="range">
- <properties>
- <help>Specify IPv6 prefix (border routers only)</help>
- <valueHelp>
- <format>ipv6net</format>
- <description>Specify IPv6 prefix (border routers only)</description>
- </valueHelp>
- <constraint>
- <validator name="ipv6-prefix"/>
- </constraint>
- </properties>
- <children>
- <leafNode name="advertise">
- <properties>
- <help>Advertise this range</help>
- <valueless/>
- </properties>
- </leafNode>
- <leafNode name="not-advertise">
- <properties>
- <help>Do not advertise this range</help>
- <valueless/>
- </properties>
- </leafNode>
- </children>
- </tagNode>
- </children>
- </tagNode>
- <node name="distance">
- <properties>
- <help>Administrative distance</help>
- </properties>
- <children>
- #include <include/ospf/distance-global.xml.i>
- <node name="ospfv3">
- <properties>
- <help>OSPFv3 administrative distance</help>
- </properties>
- <children>
- #include <include/ospf/distance-per-protocol.xml.i>
- </children>
- </node>
- </children>
- </node>
- <tagNode name="interface">
- <properties>
- <help>Enable routing on an IPv6 interface</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <valueHelp>
- <format>txt</format>
- <description>Interface used for routing information exchange</description>
- </valueHelp>
- <constraint>
- <validator name="interface-name"/>
- </constraint>
- </properties>
- <children>
- #include <include/ospf/intervals.xml.i>
- #include <include/ospf/interface-common.xml.i>
- <leafNode name="ifmtu">
- <properties>
- <help>Interface MTU</help>
- <valueHelp>
- <format>u32:1-65535</format>
- <description>Interface MTU</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-65535"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="instance-id">
- <properties>
- <help>Instance Id (default: 0)</help>
- <valueHelp>
- <format>u32:0-255</format>
- <description>Instance Id</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-255"/>
- </constraint>
- </properties>
- <defaultValue>0</defaultValue>
- </leafNode>
- <leafNode name="network">
- <properties>
- <help>Network type</help>
- <completionHelp>
- <list>broadcast point-to-point</list>
- </completionHelp>
- <valueHelp>
- <format>broadcast</format>
- <description>Broadcast network type</description>
- </valueHelp>
- <valueHelp>
- <format>point-to-point</format>
- <description>Point-to-point network type</description>
- </valueHelp>
- <constraint>
- <regex>^(broadcast|point-to-point)$</regex>
- </constraint>
- <constraintErrorMessage>Must be broadcast or point-to-point</constraintErrorMessage>
- </properties>
- </leafNode>
- #include <include/isis/passive.xml.i>
- </children>
- </tagNode>
- #include <include/ospf/log-adjacency-changes.xml.i>
- <node name="parameters">
- <properties>
- <help>OSPFv3 specific parameters</help>
- </properties>
- <children>
- #include <include/router-id.xml.i>
- </children>
- </node>
- <node name="redistribute">
- <properties>
- <help>Redistribute information from another routing protocol</help>
- </properties>
- <children>
- <node name="bgp">
- <properties>
- <help>Redistribute BGP routes</help>
- </properties>
- <children>
- #include <include/route-map.xml.i>
- </children>
- </node>
- <node name="connected">
- <properties>
- <help>Redistribute connected routes</help>
- </properties>
- <children>
- #include <include/route-map.xml.i>
- </children>
- </node>
- <node name="kernel">
- <properties>
- <help>Redistribute kernel routes</help>
- </properties>
- <children>
- #include <include/route-map.xml.i>
- </children>
- </node>
- <node name="ripng">
- <properties>
- <help>Redistribute RIPNG routes</help>
- </properties>
- <children>
- #include <include/route-map.xml.i>
- </children>
- </node>
- <node name="static">
- <properties>
- <help>Redistribute static routes</help>
- </properties>
- <children>
- #include <include/route-map.xml.i>
- </children>
- </node>
- </children>
- </node>
- #include <include/route-map.xml.i>
+ #include <include/ospfv3/protocol-common-config.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/service_mdns-repeater.xml.in b/interface-definitions/service_mdns-repeater.xml.in
index d02dac8a6..9a94f1488 100644
--- a/interface-definitions/service_mdns-repeater.xml.in
+++ b/interface-definitions/service_mdns-repeater.xml.in
@@ -14,15 +14,7 @@
</properties>
<children>
#include <include/generic-disable-node.xml.i>
- <leafNode name="interface">
- <properties>
- <help>Interface to repeat mDNS advertisements [REQUIRED]</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <multi/>
- </properties>
- </leafNode>
+ #include <include/generic-interface-multi.xml.i>
<leafNode name="vrrp-disable">
<properties>
<help>Disables mDNS repeater on VRRP interfaces not in MASTER state</help>
diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in
index 188aed6c4..97952d882 100644
--- a/interface-definitions/service_pppoe-server.xml.in
+++ b/interface-definitions/service_pppoe-server.xml.in
@@ -70,19 +70,27 @@
<children>
<leafNode name="vlan-id">
<properties>
- <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help>
+ <help>VLAN monitor for the automatic creation of single vlan</help>
+ <valueHelp>
+ <format>u32:1-4094</format>
+ <description>VLAN monitor for the automatic creation of single vlan</description>
+ </valueHelp>
<constraint>
- <validator name="numeric" argument="--range 1-4096"/>
+ <validator name="numeric" argument="--range 1-4094"/>
</constraint>
- <constraintErrorMessage>VLAN ID needs to be between 1 and 4096</constraintErrorMessage>
+ <constraintErrorMessage>VLAN ID needs to be between 1 and 4094</constraintErrorMessage>
<multi/>
</properties>
</leafNode>
<leafNode name="vlan-range">
<properties>
- <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help>
+ <help>VLAN monitor for the automatic creation of vlans range</help>
+ <valueHelp>
+ <format>start-end</format>
+ <description>VLAN monitor range for the automatic creation of vlans (e.g. 1-4094)</description>
+ </valueHelp>
<constraint>
- <regex>(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})-(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})</regex>
+ <validator name="range" argument="--min=1 --max=4094"/>
</constraint>
<multi/>
</properties>
diff --git a/interface-definitions/service_webproxy.xml.in b/interface-definitions/service_webproxy.xml.in
index d61a95690..03f504ac7 100644
--- a/interface-definitions/service_webproxy.xml.in
+++ b/interface-definitions/service_webproxy.xml.in
@@ -16,7 +16,7 @@
<description>Domain to use for urls that do not contain a '.'</description>
</valueHelp>
<constraint>
- <regex>^[\.][a-z0-9-][$]?</regex>
+ <regex>[.][A-Za-z0-9][-.A-Za-z0-9]*</regex>
</constraint>
<constraintErrorMessage>Must start append-domain with a '.'</constraintErrorMessage>
</properties>
diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in
index b0b7768d2..949536fe7 100644
--- a/interface-definitions/snmp.xml.in
+++ b/interface-definitions/snmp.xml.in
@@ -149,6 +149,26 @@
<constraintErrorMessage>Oid must be 'route-table'</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-console.xml.in b/interface-definitions/system-console.xml.in
index 88f7f82a9..2897e5e97 100644
--- a/interface-definitions/system-console.xml.in
+++ b/interface-definitions/system-console.xml.in
@@ -74,6 +74,7 @@
<regex>^(1200|2400|4800|9600|19200|38400|57600|115200)$</regex>
</constraint>
</properties>
+ <defaultValue>115200</defaultValue>
</leafNode>
</children>
</tagNode>
diff --git a/interface-definitions/system-lcd.xml.in b/interface-definitions/system-lcd.xml.in
index 36116ae1b..4c9d5c92e 100644
--- a/interface-definitions/system-lcd.xml.in
+++ b/interface-definitions/system-lcd.xml.in
@@ -12,7 +12,7 @@
<properties>
<help>Model of the display attached to this system [REQUIRED]</help>
<completionHelp>
- <list>cfa-533 cfa-631 cfa-633 cfa-635 sdec</list>
+ <list>cfa-533 cfa-631 cfa-633 cfa-635 hd44780 sdec</list>
</completionHelp>
<valueHelp>
<format>cfa-533</format>
@@ -31,11 +31,15 @@
<description>Crystalfontz CFA-635</description>
</valueHelp>
<valueHelp>
+ <format>hd44780</format>
+ <description>Hitachi HD44780, Caswell Appliances</description>
+ </valueHelp>
+ <valueHelp>
<format>sdec</format>
<description>Lanner, Watchguard, Nexcom NSA, Sophos UTM appliances</description>
</valueHelp>
<constraint>
- <regex>^(cfa-533|cfa-631|cfa-633|cfa-635|sdec)$</regex>
+ <regex>^(cfa-533|cfa-631|cfa-633|cfa-635|hd44780|sdec)$</regex>
</constraint>
</properties>
</leafNode>
diff --git a/interface-definitions/system-option.xml.in b/interface-definitions/system-option.xml.in
index f73c1ee08..75fa67271 100644
--- a/interface-definitions/system-option.xml.in
+++ b/interface-definitions/system-option.xml.in
@@ -117,6 +117,12 @@
<valueless/>
</properties>
</leafNode>
+ <leafNode name="root-partition-auto-resize">
+ <properties>
+ <help>Enable root partition auto-extention on system boot</help>
+ <valueless/>
+ </properties>
+ </leafNode>
</children>
</node>
</children>
diff --git a/interface-definitions/tftp-server.xml.in b/interface-definitions/tftp-server.xml.in
index 037c097ca..4963eab3c 100644
--- a/interface-definitions/tftp-server.xml.in
+++ b/interface-definitions/tftp-server.xml.in
@@ -24,7 +24,7 @@
<leafNode name="port">
<defaultValue>69</defaultValue>
</leafNode>
- #include <include/listen-address.xml.i>
+ #include <include/listen-address-vrf.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index 164ba6618..e82249d44 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -498,15 +498,7 @@
<help>Sets to include an additional secrets file for strongSwan. Use an absolute path to specify the included file.</help>
</properties>
</leafNode>
- <leafNode name="interface">
- <properties>
- <help>Onterface used for IPsec communication</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <multi/>
- </properties>
- </leafNode>
+ #include <include/generic-interface-multi.xml.i>
<node name="log">
<properties>
<help>IPsec logging</help>
@@ -516,15 +508,15 @@
<properties>
<help>strongSwan Logger Level</help>
<valueHelp>
- <format>u32:0</format>
+ <format>0</format>
<description>Very basic auditing logs e.g. SA up/SA down (default)</description>
</valueHelp>
<valueHelp>
- <format>u32:1</format>
+ <format>1</format>
<description>Generic control flow with errors, a good default to see whats going on</description>
</valueHelp>
<valueHelp>
- <format>u32:2</format>
+ <format>2</format>
<description>More detailed debugging control flow</description>
</valueHelp>
<constraint>
@@ -732,11 +724,11 @@
</completionHelp>
<valueHelp>
<format>pre-shared-secret</format>
- <description>pre-shared-secret_description</description>
+ <description>Authentication pre-shared-secret</description>
</valueHelp>
<valueHelp>
<format>x509</format>
- <description>x509_description</description>
+ <description>Authentication x509</description>
</valueHelp>
<constraint>
<regex>^(pre-shared-secret|x509)$</regex>
@@ -822,14 +814,7 @@
<help>DHCP pool options for remote-access</help>
</properties>
<children>
- <leafNode name="interface">
- <properties>
- <help>Interface with DHCP server to use</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- </properties>
- </leafNode>
+ #include <include/generic-interface.xml.i>
<leafNode name="server">
<properties>
<help>DHCP server address</help>
@@ -943,15 +928,15 @@
</completionHelp>
<valueHelp>
<format>pre-shared-secret</format>
- <description>pre-shared-secret_description</description>
+ <description>Use pre-shared secret key</description>
</valueHelp>
<valueHelp>
<format>rsa</format>
- <description>rsa_description</description>
+ <description>Use RSA key</description>
</valueHelp>
<valueHelp>
<format>x509</format>
- <description>x509_description</description>
+ <description>Use X.509 certificate</description>
</valueHelp>
<constraint>
<regex>^(pre-shared-secret|rsa|x509)$</regex>
@@ -984,11 +969,11 @@
</completionHelp>
<valueHelp>
<format>initiate</format>
- <description>initiate_description</description>
+ <description>Bring the connection up immediately</description>
</valueHelp>
<valueHelp>
<format>respond</format>
- <description>respond_description</description>
+ <description>Bring the connection up only if traffic is detected</description>
</valueHelp>
<constraint>
<regex>^(initiate|respond)$</regex>
diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in
index cbd5e38e7..6a88756a7 100644
--- a/interface-definitions/vpn_l2tp.xml.in
+++ b/interface-definitions/vpn_l2tp.xml.in
@@ -34,6 +34,14 @@
<help>Tunnel password used to authenticate the client (LAC)</help>
</properties>
</leafNode>
+ <leafNode name="host-name">
+ <properties>
+ <help>Sent to the client (LAC) in the Host-Name attribute</help>
+ <constraint>
+ <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]</regex>
+ </constraint>
+ </properties>
+ </leafNode>
</children>
</node>
<leafNode name="ccp-disable">
diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in
index 9901a0cdf..fe2fea9f8 100644
--- a/interface-definitions/vpn_sstp.xml.in
+++ b/interface-definitions/vpn_sstp.xml.in
@@ -43,6 +43,7 @@
</properties>
<children>
#include <include/accel-ppp/ppp-mppe.xml.i>
+ #include <include/accel-ppp/ppp-options-ipv4.xml.i>
#include <include/accel-ppp/ppp-options-ipv6.xml.i>
#include <include/accel-ppp/lcp-echo-interval-failure.xml.i>
#include <include/accel-ppp/lcp-echo-timeout.xml.i>
diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in
index a82c0b2a6..14c31fa8a 100644
--- a/interface-definitions/vrf.xml.in
+++ b/interface-definitions/vrf.xml.in
@@ -60,6 +60,15 @@
#include <include/ospf/protocol-common-config.xml.i>
</children>
</node>
+ <node name="ospfv3" owner="${vyos_conf_scripts_dir}/protocols_ospfv3.py $VAR(../../@)">
+ <properties>
+ <help>Open Shortest Path First (OSPF) for IPv6</help>
+ <priority>621</priority>
+ </properties>
+ <children>
+ #include <include/ospfv3/protocol-common-config.xml.i>
+ </children>
+ </node>
<node name="static" owner="${vyos_conf_scripts_dir}/protocols_static.py $VAR(../../@)">
<properties>
<help>Static route parameters</help>
@@ -85,7 +94,7 @@
<constraintErrorMessage>VRF routing table must be in range from 100 to 65535</constraintErrorMessage>
</properties>
</leafNode>
- <leafNode name="vni" owner="${vyos_conf_scripts_dir}/vrf_vni.py $VAR(../@)">
+ <leafNode name="vni" owner="${vyos_conf_scripts_dir}/vrf_vni.py">
<properties>
<help>Virtual Network Identifier</help>
<!-- priority must be after BGP -->
diff --git a/interface-definitions/vrrp.xml.in b/interface-definitions/vrrp.xml.in
index 44a9a1f54..53d79caac 100644
--- a/interface-definitions/vrrp.xml.in
+++ b/interface-definitions/vrrp.xml.in
@@ -16,14 +16,7 @@
<help>VRRP group</help>
</properties>
<children>
- <leafNode name="interface">
- <properties>
- <help>Network interface</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script>
- </completionHelp>
- </properties>
- </leafNode>
+ #include <include/generic-interface-broadcast.xml.i>
<leafNode name="advertise-interval">
<properties>
<help>Advertise interval</help>
diff --git a/op-mode-definitions/disks.xml.in b/op-mode-definitions/disks.xml.in
index 2102a2e8e..117ac5065 100644
--- a/op-mode-definitions/disks.xml.in
+++ b/op-mode-definitions/disks.xml.in
@@ -20,7 +20,7 @@
<script>${vyos_completion_dir}/list_disks.py --exclude ${COMP_WORDS[2]}</script>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/format_disk.py --target $3 --proto $5</command>
+ <command>sudo ${vyos_op_scripts_dir}/format_disk.py --target $3 --proto $5</command>
</tagNode>
</children>
</tagNode>
diff --git a/op-mode-definitions/force-root-partition-auto-resize.xml.in b/op-mode-definitions/force-root-partition-auto-resize.xml.in
new file mode 100644
index 000000000..f84c073b8
--- /dev/null
+++ b/op-mode-definitions/force-root-partition-auto-resize.xml.in
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="force">
+ <children>
+ <node name="root-partition-auto-resize">
+ <properties>
+ <help>Resize the VyOS partition</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/force_root-partition-auto-resize.sh</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/generate-ipsec-debug-archive.xml.in b/op-mode-definitions/generate-ipsec-debug-archive.xml.in
new file mode 100644
index 000000000..f268d5ae5
--- /dev/null
+++ b/op-mode-definitions/generate-ipsec-debug-archive.xml.in
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="ipsec">
+ <children>
+ <node name="debug-archive">
+ <properties>
+ <help>Generate IPSec debug-archive</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/generate_ipsec_debug_archive.sh</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/generate-ipsec-profile.xml.in b/op-mode-definitions/generate-ipsec-profile.xml.in
index 8d1051b94..b7203d7d1 100644
--- a/op-mode-definitions/generate-ipsec-profile.xml.in
+++ b/op-mode-definitions/generate-ipsec-profile.xml.in
@@ -4,7 +4,7 @@
<children>
<node name="ipsec">
<properties>
- <help>Generate IPsec related configurations</help>
+ <help>Generate IPsec related configurations and archives</help>
</properties>
<children>
<node name="profile">
diff --git a/op-mode-definitions/include/bfd-common.xml.i b/op-mode-definitions/include/bfd-common.xml.i
new file mode 100644
index 000000000..eebfbdad4
--- /dev/null
+++ b/op-mode-definitions/include/bfd-common.xml.i
@@ -0,0 +1,54 @@
+<!-- included start from bfd-common.xml.i -->
+<node name="bfd">
+ <properties>
+ <help>Show Bidirectional Forwarding Detection (BFD)</help>
+ </properties>
+ <children>
+ <node name="peer">
+ <properties>
+ <help>Show all Bidirectional Forwarding Detection (BFD) peer status</help>
+ </properties>
+ <command>vtysh -c "show bfd peers"</command>
+ <children>
+ <leafNode name="counters">
+ <properties>
+ <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help>
+ </properties>
+ <command>vtysh -c "show bfd peers counters"</command>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="peer">
+ <properties>
+ <help>Show Bidirectional Forwarding Detection (BFD) peer status</help>
+ <completionHelp>
+ <script>vtysh -c "show bfd peers" | awk '/[:blank:]*peer/ { printf "%s\n", $2 }'</script>
+ </completionHelp>
+ </properties>
+ <command>vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 'BEGIN { regex = sprintf("(peer %s.*)vrf", BFD_PEER) } { if (match($0, regex, bfd_peer_value)) peer=bfd_peer_value[1] } END { if (peer) system("vtysh -c \"show bfd " peer "\"") }'</command>
+ <children>
+ <leafNode name="counters">
+ <properties>
+ <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help>
+ </properties>
+ <command>vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 'BEGIN { regex = sprintf("(peer %s.*)vrf", BFD_PEER) } { if (match($0, regex, bfd_peer_value)) peer=bfd_peer_value[1] } END { if (peer) system("vtysh -c \"show bfd " peer " counters\"") }'</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="peers">
+ <properties>
+ <help>Show Bidirectional Forwarding Detection peers</help>
+ </properties>
+ <command>vtysh -c "show bfd peers"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show Bidirectional Forwarding Detection (BFD) peers brief</help>
+ </properties>
+ <command>vtysh -c "show bfd peers brief"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+</node>
+<!-- included end -->
diff --git a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i
index ba6edb256..f6737c8bd 100644
--- a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i
+++ b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i
@@ -19,5 +19,6 @@
#include <include/bgp/afi-common.xml.i>
#include <include/bgp/afi-ipv4-ipv6-common.xml.i>
</children>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
</node>
<!-- included end -->
diff --git a/op-mode-definitions/include/bgp/show-ip-bgp-common.xml.i b/op-mode-definitions/include/bgp/show-ip-bgp-common.xml.i
index e599bfb3f..36cc9a3fa 100644
--- a/op-mode-definitions/include/bgp/show-ip-bgp-common.xml.i
+++ b/op-mode-definitions/include/bgp/show-ip-bgp-common.xml.i
@@ -35,6 +35,7 @@
<properties>
<help>Show BGP IPv4 unicast information</help>
</properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
<children>
<leafNode name="cidr-only">
<properties>
diff --git a/op-mode-definitions/openvpn.xml.in b/op-mode-definitions/openvpn.xml.in
index 73cbbe501..301688271 100644
--- a/op-mode-definitions/openvpn.xml.in
+++ b/op-mode-definitions/openvpn.xml.in
@@ -55,6 +55,41 @@
</properties>
<command>${vyos_op_scripts_dir}/show_interfaces.py --intf=$4</command>
<children>
+ <tagNode name="user">
+ <properties>
+ <help>Show OpenVPN interface users</help>
+ <completionHelp>
+ <script>sudo ${vyos_completion_dir}/list_openvpn_users.py --interface ${COMP_WORDS[3]}</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <node name="mfa">
+ <properties>
+ <help>Show multi-factor authentication information</help>
+ </properties>
+ <children>
+ <leafNode name="secret">
+ <properties>
+ <help>Show multi-factor authentication secret</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_openvpn_mfa.py --user="$6" --intf="$4" --action=secret</command>
+ </leafNode>
+ <leafNode name="uri">
+ <properties>
+ <help>Show multi-factor authentication otpauth uri</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_openvpn_mfa.py --user="$6" --intf="$4" --action=uri</command>
+ </leafNode>
+ <leafNode name="qrcode">
+ <properties>
+ <help>Show multi-factor authentication QR code</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_openvpn_mfa.py --user="$6" --intf="$4" --action=qrcode</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
<leafNode name="brief">
<properties>
<help>Show summary of specified OpenVPN interface information</help>
diff --git a/op-mode-definitions/show-bfd.xml.in b/op-mode-definitions/show-bfd.xml.in
new file mode 100644
index 000000000..7339c92a2
--- /dev/null
+++ b/op-mode-definitions/show-bfd.xml.in
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ #include <include/bfd-common.xml.i>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-configuration.xml.in b/op-mode-definitions/show-configuration.xml.in
index 318942ab0..5a2fdedfa 100644
--- a/op-mode-definitions/show-configuration.xml.in
+++ b/op-mode-definitions/show-configuration.xml.in
@@ -30,6 +30,21 @@
<!-- no admin check -->
<command>${vyos_op_scripts_dir}/show_configuration_files.sh</command>
</node>
+ <node name="json">
+ <properties>
+ <help>Show running configuration in JSON format</help>
+ </properties>
+ <!-- no admin check -->
+ <command>${vyos_op_scripts_dir}/show_configuration_json.py</command>
+ <children>
+ <node name="pretty">
+ <properties>
+ <help>Show running configuration in readable JSON format</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_configuration_json.py --pretty</command>
+ </node>
+ </children>
+ </node>
</children>
</node>
</children>
diff --git a/op-mode-definitions/show-interfaces-geneve.xml.in b/op-mode-definitions/show-interfaces-geneve.xml.in
new file mode 100644
index 000000000..a47933315
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-geneve.xml.in
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="geneve">
+ <properties>
+ <help>Show specified GENEVE interface information</help>
+ <completionHelp>
+ <path>interfaces geneve</path>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of the specified GENEVE interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="geneve">
+ <properties>
+ <help>Show GENEVE interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=geneve --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed GENEVE interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=geneve --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ip-route.xml.in b/op-mode-definitions/show-ip-route.xml.in
index 0a24bc45a..fdbb6859d 100644
--- a/op-mode-definitions/show-ip-route.xml.in
+++ b/op-mode-definitions/show-ip-route.xml.in
@@ -125,16 +125,11 @@
</properties>
<command>vtysh -c "show ip route tag $5"</command>
</tagNode>
- <node name="vrf">
- <properties>
- <help>Show IP routes in VRF</help>
- </properties>
- </node>
<tagNode name="vrf">
<properties>
<help>Show IP routes in VRF</help>
<completionHelp>
- <list>&lt;vrf&gt;</list>
+ <list>all</list>
<path>vrf name</path>
</completionHelp>
</properties>
diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in
index 92c1cf016..4c0a7913b 100644
--- a/op-mode-definitions/show-log.xml.in
+++ b/op-mode-definitions/show-log.xml.in
@@ -6,7 +6,7 @@
<properties>
<help>Show contents of current master log file</help>
</properties>
- <command>/bin/journalctl</command>
+ <command>journalctl --no-hostname --boot</command>
<children>
<leafNode name="all">
<properties>
@@ -18,7 +18,7 @@
<properties>
<help>Show listing of authorization attempts</help>
</properties>
- <command>/bin/journalctl --quiet SYSLOG_FACILITY=10 SYSLOG_FACILITY=4</command>
+ <command>journalctl --no-hostname --boot --quiet SYSLOG_FACILITY=10 SYSLOG_FACILITY=4</command>
</leafNode>
<leafNode name="cluster">
<properties>
@@ -30,14 +30,68 @@
<properties>
<help>Show log for Conntrack-sync</help>
</properties>
- <command>cat $(printf "%s\n" /var/log/messages* | sort -nr ) | grep -e conntrackd</command>
+ <command>journalctl --no-hostname --boot --unit conntrackd.service</command>
</leafNode>
- <leafNode name="dhcp">
+ <node name="dhcp">
<properties>
<help>Show log for Dynamic Host Control Protocol (DHCP)</help>
</properties>
- <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep dhcpd</command>
- </leafNode>
+ <children>
+ <node name="server">
+ <properties>
+ <help>Show log for DHCP server</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit isc-dhcp-server.service</command>
+ </node>
+ <node name="client">
+ <properties>
+ <help>Show DHCP client logs</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit "dhclient@*.service"</command>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Show DHCP client log on specific interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script>
+ </completionHelp>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit "dhclient@$6.service"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="dhcpv6">
+ <properties>
+ <help>Show log for Dynamic Host Control Protocol IPv6 (DHCPv6)</help>
+ </properties>
+ <children>
+ <node name="server">
+ <properties>
+ <help>Show log for DHCPv6 server</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit isc-dhcp-server6.service</command>
+ </node>
+ <node name="client">
+ <properties>
+ <help>Show DHCPv6 client logs</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit "dhcp6c@*.service"</command>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Show DHCPv6 client log on specific interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit "dhcp6c@$6.service"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
<node name="firewall">
<properties>
<help>Show log for Firewall</help>
@@ -89,7 +143,7 @@
<properties>
<help>Show log for HTTPs</help>
</properties>
- <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e nginx</command>
+ <command>journalctl --no-hostname --boot --unit nginx.service</command>
</leafNode>
<tagNode name="image">
<properties>
@@ -119,7 +173,7 @@
<list>&lt;NUMBER&gt;</list>
</completionHelp>
</properties>
- <command>tail -n "$6" /lib/live/mount/persistence/boot/$4/rw/var/log/messages | ${VYATTA_PAGER:-cat}</command>
+ <command>tail -n "$6" /lib/live/mount/persistence/boot/$4/rw/var/log/messages | ${VYATTA_PAGER:-cat}</command>
</tagNode>
</children>
</tagNode>
@@ -133,7 +187,7 @@
<properties>
<help>Show log for LLDP</help>
</properties>
- <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e lldpd</command>
+ <command>journalctl --no-hostname --boot --unit lldpd.service</command>
</leafNode>
<leafNode name="nat">
<properties>
@@ -141,17 +195,28 @@
</properties>
<command>egrep -i "kernel:.*\[NAT-[A-Z]{3,}-[0-9]+(-MASQ)?\]" $(find /var/log -maxdepth 1 -type f -name messages\* | sort -t. -k2nr)</command>
</leafNode>
- <leafNode name="openvpn">
+ <node name="openvpn">
<properties>
<help>Show log for OpenVPN</help>
</properties>
- <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e openvpn</command>
- </leafNode>
+ <command>journalctl --no-hostname --boot --unit openvpn@*.service</command>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Show OpenVPN log on specific interface</help>
+ <completionHelp>
+ <path>interfaces openvpn</path>
+ </completionHelp>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit openvpn@$5.service</command>
+ </tagNode>
+ </children>
+ </node>
<leafNode name="snmp">
<properties>
<help>Show log for Simple Network Monitoring Protocol (SNMP)</help>
</properties>
- <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e snmpd</command>
+ <command>journalctl --no-hostname --boot --unit snmpd.service</command>
</leafNode>
<tagNode name="tail">
<properties>
@@ -195,13 +260,13 @@
<properties>
<help>Show log for PPTP</help>
</properties>
- <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e accel-pptp -e ppp</command>
+ <command>journalctl --no-hostname --boot --unit accel-ppp@pptp.service</command>
</leafNode>
<leafNode name="sstp">
<properties>
<help>Show log for SSTP</help>
</properties>
- <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e accel-sstp -e ppp</command>
+ <command>journalctl --no-hostname --boot --unit accel-ppp@sstp.service</command>
</leafNode>
</children>
</node>
@@ -209,13 +274,13 @@
<properties>
<help>Show log for Virtual Router Redundancy Protocol (VRRP)</help>
</properties>
- <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e Keepalived_vrrp</command>
+ <command>journalctl --no-hostname --boot --unit keepalived.service</command>
</leafNode>
<leafNode name="webproxy">
<properties>
<help>Show log for Webproxy</help>
</properties>
- <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "squid"</command>
+ <command>journalctl --no-hostname --boot --unit squid.service</command>
</leafNode>
</children>
</node>
diff --git a/op-mode-definitions/show-netns.xml.in b/op-mode-definitions/show-netns.xml.in
new file mode 100644
index 000000000..8d5072d4e
--- /dev/null
+++ b/op-mode-definitions/show-netns.xml.in
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="netns">
+ <properties>
+ <help>Show network namespace information</help>
+ </properties>
+ <command>ip netns ls</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-protocols.xml.in b/op-mode-definitions/show-protocols.xml.in
index d595e2c3c..48bfa10dc 100644
--- a/op-mode-definitions/show-protocols.xml.in
+++ b/op-mode-definitions/show-protocols.xml.in
@@ -7,50 +7,7 @@
<help>Show protocol specific information</help>
</properties>
<children>
- <node name="bfd">
- <properties>
- <help>Show Bidirectional Forwarding Detection (BFD)</help>
- </properties>
- <children>
- <node name="peer">
- <properties>
- <help>Show all Bidirectional Forwarding Detection (BFD) peer status</help>
- </properties>
- <command>vtysh -c "show bfd peers"</command>
- <children>
- <leafNode name="counters">
- <properties>
- <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help>
- </properties>
- <command>vtysh -c "show bfd peers counters"</command>
- </leafNode>
- </children>
- </node>
- <tagNode name="peer">
- <properties>
- <help>Show Bidirectional Forwarding Detection (BFD) peer status</help>
- <completionHelp>
- <script>vtysh -c "show bfd peers" | awk '/[:blank:]*peer/ { printf "%s\n", $2 }'</script>
- </completionHelp>
- </properties>
- <command>vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 'BEGIN { regex = sprintf("(peer %s.*)vrf", BFD_PEER) } { if (match($0, regex, bfd_peer_value)) peer=bfd_peer_value[1] } END { if (peer) system("vtysh -c \"show bfd " peer "\"") }'</command>
- <children>
- <leafNode name="counters">
- <properties>
- <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help>
- </properties>
- <command>vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 'BEGIN { regex = sprintf("(peer %s.*)vrf", BFD_PEER) } { if (match($0, regex, bfd_peer_value)) peer=bfd_peer_value[1] } END { if (peer) system("vtysh -c \"show bfd " peer " counters\"") }'</command>
- </leafNode>
- </children>
- </tagNode>
- <leafNode name="peers">
- <properties>
- <help>Show Bidirectional Forwarding Detection (BFD) peers brief</help>
- </properties>
- <command>vtysh -c "show bfd peers brief"</command>
- </leafNode>
- </children>
- </node>
+ #include <include/bfd-common.xml.i>
<node name="static">
<properties>
<help>Show static protocol parameters</help>
diff --git a/op-mode-definitions/show-system.xml.in b/op-mode-definitions/show-system.xml.in
index 18a28868d..0f852164e 100644
--- a/op-mode-definitions/show-system.xml.in
+++ b/op-mode-definitions/show-system.xml.in
@@ -104,7 +104,7 @@
<properties>
<help>Show system memory usage</help>
</properties>
- <command>${vyos_op_scripts_dir}/show_ram.sh</command>
+ <command>${vyos_op_scripts_dir}/show_ram.py</command>
<children>
<leafNode name="cache">
<properties>
@@ -142,7 +142,7 @@
<properties>
<help>Show summary of system processes</help>
</properties>
- <command>uptime</command>
+ <command>${vyos_op_scripts_dir}/show_uptime.py</command>
</leafNode>
<leafNode name="tree">
<properties>
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 5c6836e97..425a2e416 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -155,18 +155,15 @@ def get_removed_vlans(conf, dict):
D.set_level(conf.get_level())
# get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys()
- if keys:
- dict.update({'vif_remove': [*keys]})
+ if keys: dict['vif_remove'] = [*keys]
# get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys()
- if keys:
- dict.update({'vif_s_remove': [*keys]})
+ if keys: dict['vif_s_remove'] = [*keys]
for vif in dict.get('vif_s', {}).keys():
keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys()
- if keys:
- dict.update({'vif_s': { vif : {'vif_c_remove': [*keys]}}})
+ if keys: dict['vif_s'][vif]['vif_c_remove'] = [*keys]
return dict
@@ -319,6 +316,40 @@ def is_source_interface(conf, interface, intftype=None):
old_level = conf.set_level(old_level)
return ret_val
+def get_dhcp_interfaces(conf, vrf=None):
+ """ Common helper functions to retrieve all interfaces from current CLI
+ sessions that have DHCP configured. """
+ dhcp_interfaces = []
+ dict = conf.get_config_dict(['interfaces'], get_first_key=True)
+ if not dict:
+ return dhcp_interfaces
+
+ def check_dhcp(config, ifname):
+ out = []
+ if 'address' in config and 'dhcp' in config['address']:
+ if 'vrf' in config:
+ if vrf is config['vrf']: out.append(ifname)
+ else: out.append(ifname)
+ return out
+
+ for section, interface in dict.items():
+ for ifname, ifconfig in interface.items():
+ tmp = check_dhcp(ifconfig, ifname)
+ dhcp_interfaces.extend(tmp)
+ # check per VLAN interfaces
+ for vif, vif_config in ifconfig.get('vif', {}).items():
+ tmp = check_dhcp(vif_config, f'{ifname}.{vif}')
+ dhcp_interfaces.extend(tmp)
+ # check QinQ VLAN interfaces
+ for vif_s, vif_s_config in ifconfig.get('vif-s', {}).items():
+ tmp = check_dhcp(vif_s_config, f'{ifname}.{vif_s}')
+ dhcp_interfaces.extend(tmp)
+ for vif_c, vif_c_config in vif_s_config.get('vif-c', {}).items():
+ tmp = check_dhcp(vif_c_config, f'{ifname}.{vif_s}.{vif_c}')
+ dhcp_interfaces.extend(tmp)
+
+ return dhcp_interfaces
+
def get_interface_dict(config, base, ifname=''):
"""
Common utility function to retrieve and mangle the interfaces configuration
@@ -368,9 +399,11 @@ def get_interface_dict(config, base, ifname=''):
del default_values['dhcpv6_options']
# We have gathered the dict representation of the CLI, but there are
- # default options which we need to update into the dictionary
- # retrived.
- dict = dict_merge(default_values, dict)
+ # default options which we need to update into the dictionary retrived.
+ # But we should only add them when interface is not deleted - as this might
+ # confuse parsers
+ if 'deleted' not in dict:
+ dict = dict_merge(default_values, dict)
# XXX: T2665: blend in proper DHCPv6-PD default values
dict = T2665_set_dhcpv6pd_defaults(dict)
@@ -423,9 +456,12 @@ def get_interface_dict(config, base, ifname=''):
if not 'dhcpv6_options' in vif_config:
del default_vif_values['dhcpv6_options']
- dict['vif'][vif] = dict_merge(default_vif_values, vif_config)
- # XXX: T2665: blend in proper DHCPv6-PD default values
- dict['vif'][vif] = T2665_set_dhcpv6pd_defaults(dict['vif'][vif])
+ # Only add defaults if interface is not about to be deleted - this is
+ # to keep a cleaner config dict.
+ if 'deleted' not in dict:
+ dict['vif'][vif] = dict_merge(default_vif_values, vif_config)
+ # XXX: T2665: blend in proper DHCPv6-PD default values
+ dict['vif'][vif] = T2665_set_dhcpv6pd_defaults(dict['vif'][vif])
# Check if we are a member of a bridge device
bridge = is_member(config, f'{ifname}.{vif}', 'bridge')
@@ -441,10 +477,12 @@ def get_interface_dict(config, base, ifname=''):
if not 'dhcpv6_options' in vif_s_config:
del default_vif_s_values['dhcpv6_options']
- dict['vif_s'][vif_s] = dict_merge(default_vif_s_values, vif_s_config)
- # XXX: T2665: blend in proper DHCPv6-PD default values
- dict['vif_s'][vif_s] = T2665_set_dhcpv6pd_defaults(
- dict['vif_s'][vif_s])
+ # Only add defaults if interface is not about to be deleted - this is
+ # to keep a cleaner config dict.
+ if 'deleted' not in dict:
+ dict['vif_s'][vif_s] = dict_merge(default_vif_s_values, vif_s_config)
+ # XXX: T2665: blend in proper DHCPv6-PD default values
+ dict['vif_s'][vif_s] = T2665_set_dhcpv6pd_defaults(dict['vif_s'][vif_s])
# Check if we are a member of a bridge device
bridge = is_member(config, f'{ifname}.{vif_s}', 'bridge')
@@ -458,11 +496,14 @@ def get_interface_dict(config, base, ifname=''):
if not 'dhcpv6_options' in vif_c_config:
del default_vif_c_values['dhcpv6_options']
- dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_merge(
+ # Only add defaults if interface is not about to be deleted - this is
+ # to keep a cleaner config dict.
+ if 'deleted' not in dict:
+ dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_merge(
default_vif_c_values, vif_c_config)
- # XXX: T2665: blend in proper DHCPv6-PD default values
- dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_set_dhcpv6pd_defaults(
- dict['vif_s'][vif_s]['vif_c'][vif_c])
+ # XXX: T2665: blend in proper DHCPv6-PD default values
+ dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_set_dhcpv6pd_defaults(
+ dict['vif_s'][vif_s]['vif_c'][vif_c])
# Check if we are a member of a bridge device
bridge = is_member(config, f'{ifname}.{vif_s}.{vif_c}', 'bridge')
@@ -522,6 +563,11 @@ def get_accel_dict(config, base, chap_secrets):
if dict_search('authentication.local_users.username', default_values):
del default_values['authentication']['local_users']['username']
+ # T2665: defaults include IPv6 client-pool mask per TAG node which need to be
+ # added to individual local users instead - so we can simply delete them
+ if dict_search('client_ipv6_pool.prefix.mask', default_values):
+ del default_values['client_ipv6_pool']['prefix']['mask']
+
dict = dict_merge(default_values, dict)
# set CPUs cores to process requests
@@ -565,4 +611,13 @@ def get_accel_dict(config, base, chap_secrets):
dict['authentication']['local_users']['username'][username] = dict_merge(
default_values, dict['authentication']['local_users']['username'][username])
+ # Add individual IPv6 client-pool default mask if required
+ if dict_search('client_ipv6_pool.prefix', dict):
+ # T2665
+ default_values = defaults(base + ['client-ipv6-pool', 'prefix'])
+
+ for prefix in dict_search('client_ipv6_pool.prefix', dict):
+ dict['client_ipv6_pool']['prefix'][prefix] = dict_merge(
+ default_values, dict['client_ipv6_pool']['prefix'][prefix])
+
return dict
diff --git a/python/vyos/configquery.py b/python/vyos/configquery.py
index 1cdcbcf39..5b097b312 100644
--- a/python/vyos/configquery.py
+++ b/python/vyos/configquery.py
@@ -18,16 +18,15 @@ A small library that allows querying existence or value(s) of config
settings from op mode, and execution of arbitrary op mode commands.
'''
-import re
-import json
-from copy import deepcopy
+import os
from subprocess import STDOUT
-import vyos.util
-import vyos.xml
+from vyos.util import popen, boot_configuration_complete
from vyos.config import Config
-from vyos.configtree import ConfigTree
-from vyos.configsource import ConfigSourceSession
+from vyos.configsource import ConfigSourceSession, ConfigSourceString
+from vyos.defaults import directories
+
+config_file = os.path.join(directories['config'], 'config.boot')
class ConfigQueryError(Exception):
pass
@@ -58,21 +57,21 @@ class CliShellApiConfigQuery(GenericConfigQuery):
def exists(self, path: list):
cmd = ' '.join(path)
- (_, err) = vyos.util.popen(f'cli-shell-api existsActive {cmd}')
+ (_, err) = popen(f'cli-shell-api existsActive {cmd}')
if err:
return False
return True
def value(self, path: list):
cmd = ' '.join(path)
- (out, err) = vyos.util.popen(f'cli-shell-api returnActiveValue {cmd}')
+ (out, err) = popen(f'cli-shell-api returnActiveValue {cmd}')
if err:
raise ConfigQueryError('No value for given path')
return out
def values(self, path: list):
cmd = ' '.join(path)
- (out, err) = vyos.util.popen(f'cli-shell-api returnActiveValues {cmd}')
+ (out, err) = popen(f'cli-shell-api returnActiveValues {cmd}')
if err:
raise ConfigQueryError('No values for given path')
return out
@@ -81,25 +80,36 @@ class ConfigTreeQuery(GenericConfigQuery):
def __init__(self):
super().__init__()
- config_source = ConfigSourceSession()
- self.configtree = Config(config_source=config_source)
+ if boot_configuration_complete():
+ config_source = ConfigSourceSession()
+ self.config = Config(config_source=config_source)
+ else:
+ try:
+ with open(config_file) as f:
+ config_string = f.read()
+ except OSError as err:
+ raise ConfigQueryError('No config file available') from err
+
+ config_source = ConfigSourceString(running_config_text=config_string,
+ session_config_text=config_string)
+ self.config = Config(config_source=config_source)
def exists(self, path: list):
- return self.configtree.exists(path)
+ return self.config.exists(path)
def value(self, path: list):
- return self.configtree.return_value(path)
+ return self.config.return_value(path)
def values(self, path: list):
- return self.configtree.return_values(path)
+ return self.config.return_values(path)
def list_nodes(self, path: list):
- return self.configtree.list_nodes(path)
+ return self.config.list_nodes(path)
def get_config_dict(self, path=[], effective=False, key_mangling=None,
get_first_key=False, no_multi_convert=False,
no_tag_node_value_mangle=False):
- return self.configtree.get_config_dict(path, effective=effective,
+ return self.config.get_config_dict(path, effective=effective,
key_mangling=key_mangling, get_first_key=get_first_key,
no_multi_convert=no_multi_convert,
no_tag_node_value_mangle=no_tag_node_value_mangle)
@@ -110,7 +120,7 @@ class VbashOpRun(GenericOpRun):
def run(self, path: list, **kwargs):
cmd = ' '.join(path)
- (out, err) = vyos.util.popen(f'. /opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run; _vyatta_op_run {cmd}', stderr=STDOUT, **kwargs)
+ (out, err) = popen(f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}', stderr=STDOUT, **kwargs)
if err:
raise ConfigQueryError(out)
return out
diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py
index b0981d25e..a0f6a46b5 100644
--- a/python/vyos/configsource.py
+++ b/python/vyos/configsource.py
@@ -19,6 +19,7 @@ import re
import subprocess
from vyos.configtree import ConfigTree
+from vyos.util import boot_configuration_complete
class VyOSError(Exception):
"""
@@ -117,7 +118,7 @@ class ConfigSourceSession(ConfigSource):
# Running config can be obtained either from op or conf mode, it always succeeds
# once the config system is initialized during boot;
# before initialization, set to empty string
- if os.path.isfile('/tmp/vyos-config-status'):
+ if boot_configuration_complete():
try:
running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig'])
except VyOSError:
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 8aca76568..365a28feb 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -110,15 +110,12 @@ def verify_tunnel(config):
raise ConfigError('Must configure the tunnel encapsulation for '\
'{ifname}!'.format(**config))
- if 'source_address' not in config and 'dhcp_interface' not in config:
- raise ConfigError('source-address is mandatory for tunnel')
+ if 'source_address' not in config and 'source_interface' not in config:
+ raise ConfigError('source-address or source-interface required for tunnel!')
if 'remote' not in config and config['encapsulation'] != 'gre':
raise ConfigError('remote ip address is mandatory for tunnel')
- if {'source_address', 'dhcp_interface'} <= set(config):
- raise ConfigError('Can not use both source-address and dhcp-interface')
-
if config['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre', 'ip6gretap', 'ip6erspan']:
error_ipv6 = 'Encapsulation mode requires IPv6'
if 'source_address' in config and not is_ipv6(config['source_address']):
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 00b14a985..f355c4919 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -29,6 +29,8 @@ directories = {
"vyos_udev_dir": "/run/udev/vyos"
}
+config_status = '/tmp/vyos-config-status'
+
cfg_group = 'vyattacfg'
cfg_vintage = 'vyos'
diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
index eb5b0a456..e45b0f041 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -56,9 +56,6 @@ class Ethtool:
link = os.readlink(sysfs_file)
self._driver_name = os.path.basename(link)
- if not self._driver_name:
- raise ValueError(f'Could not determine driver for interface {ifname}!')
-
# Build a dictinary of supported link-speed and dupley settings.
out, err = popen(f'ethtool {ifname}')
reading = False
diff --git a/python/vyos/frr.py b/python/vyos/frr.py
index df6849472..a8f115d9a 100644
--- a/python/vyos/frr.py
+++ b/python/vyos/frr.py
@@ -84,12 +84,14 @@ if DEBUG:
LOG.addHandler(ch2)
_frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd',
- 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd']
+ 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd',
+ 'bfdd']
path_vtysh = '/usr/bin/vtysh'
path_frr_reload = '/usr/lib/frr/frr-reload.py'
path_config = '/run/frr'
+default_add_before = r'(ip prefix-list .*|route-map .*|line vty|end)'
class FrrError(Exception):
pass
@@ -214,13 +216,8 @@ def reload_configuration(config, daemon=None):
def save_configuration():
- """Save FRR configuration to /run/frr/config/frr.conf
- It save configuration on each commit. T3217
- """
-
- cmd(f'{path_vtysh} -n -w')
-
- return
+ """ T3217: Save FRR configuration to /run/frr/config/frr.conf """
+ return cmd(f'{path_vtysh} -n -w')
def execute(command):
@@ -448,16 +445,37 @@ class FRRConfig:
mark_configuration('\n'.join(self.config))
def commit_configuration(self, daemon=None):
- '''Commit the current configuration to FRR
- daemon: str with name of the FRR daemon to commit to or
- None to use the consolidated config
+ '''
+ Commit the current configuration to FRR daemon: str with name of the
+ FRR daemon to commit to or None to use the consolidated config.
+
+ Configuration is automatically saved after apply
'''
LOG.debug('commit_configuration: Commiting configuration')
for i, e in enumerate(self.config):
LOG.debug(f'commit_configuration: new_config {i:3} {e}')
- reload_configuration('\n'.join(self.config), daemon=daemon)
- def modify_section(self, start_pattern, replacement=[], stop_pattern=r'\S+', remove_stop_mark=False, count=0):
+ # https://github.com/FRRouting/frr/issues/10132
+ # https://github.com/FRRouting/frr/issues/10133
+ count = 0
+ count_max = 5
+ while count < count_max:
+ count += 1
+ try:
+ reload_configuration('\n'.join(self.config), daemon=daemon)
+ break
+ except:
+ # we just need to re-try the commit of the configuration
+ # for the listed FRR issues above
+ pass
+ if count >= count_max:
+ raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded')
+
+ # Save configuration to /run/frr/config/frr.conf
+ save_configuration()
+
+
+ def modify_section(self, start_pattern, replacement='!', stop_pattern=r'\S+', remove_stop_mark=False, count=0):
if isinstance(replacement, str):
replacement = replacement.split('\n')
elif not isinstance(replacement, list):
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 2e59a7afc..9d54dc78e 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -80,6 +80,23 @@ class EthernetIf(Interface):
super().__init__(ifname, **kargs)
self.ethtool = Ethtool(ifname)
+ def remove(self):
+ """
+ Remove interface from config. Removing the interface deconfigures all
+ assigned IP addresses.
+ Example:
+ >>> from vyos.ifconfig import WWANIf
+ >>> i = EthernetIf('eth0')
+ >>> i.remove()
+ """
+
+ if self.exists(self.ifname):
+ # interface is placed in A/D state when removed from config! It
+ # will remain visible for the operating system.
+ self.set_admin_state('down')
+
+ super().remove()
+
def set_flow_control(self, enable):
"""
Changes the pause parameters of the specified Ethernet device.
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index e6dbd861b..bcb692697 100755
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -37,6 +37,7 @@ from vyos.util import mac2eui64
from vyos.util import dict_search
from vyos.util import read_file
from vyos.util import get_interface_config
+from vyos.util import get_interface_namespace
from vyos.util import is_systemd_service_active
from vyos.template import is_ipv4
from vyos.template import is_ipv6
@@ -135,6 +136,9 @@ class Interface(Control):
'validate': assert_mtu,
'shellcmd': 'ip link set dev {ifname} mtu {value}',
},
+ 'netns': {
+ 'shellcmd': 'ip link set dev {ifname} netns {value}',
+ },
'vrf': {
'convert': lambda v: f'master {v}' if v else 'nomaster',
'shellcmd': 'ip link set dev {ifname} {value}',
@@ -461,15 +465,19 @@ class Interface(Control):
# Get processor ID number
cpu_id = self._cmd('sudo dmidecode -t 4 | grep ID | head -n1 | sed "s/.*ID://;s/ //g"')
- # Get system eth0 base MAC address - every system has eth0
- eth0_mac = Interface('eth0').get_mac()
+
+ # XXX: T3894 - it seems not all systems have eth0 - get a list of all
+ # available Ethernet interfaces on the system (without VLAN subinterfaces)
+ # and then take the first one.
+ all_eth_ifs = [x for x in Section.interfaces('ethernet') if '.' not in x]
+ first_mac = Interface(all_eth_ifs[0]).get_mac()
sha = sha256()
# Calculate SHA256 sum based on the CPU ID number, eth0 mac address and
# this interface identifier - this is as predictable as an interface
# MAC address and thus can be used in the same way
sha.update(cpu_id.encode())
- sha.update(eth0_mac.encode())
+ sha.update(first_mac.encode())
sha.update(self.ifname.encode())
# take the most significant 48 bits from the SHA256 string
tmp = sha.hexdigest()[:12]
@@ -508,6 +516,35 @@ class Interface(Control):
if prev_state == 'up':
self.set_admin_state('up')
+ def del_netns(self, netns):
+ """
+ Remove interface from given NETNS.
+ """
+
+ # If NETNS does not exist then there is nothing to delete
+ if not os.path.exists(f'/run/netns/{netns}'):
+ return None
+
+ # As a PoC we only allow 'dummy' interfaces
+ if 'dum' not in self.ifname:
+ return None
+
+ # Check if interface realy exists in namespace
+ if get_interface_namespace(self.ifname) != None:
+ self._cmd(f'ip netns exec {get_interface_namespace(self.ifname)} ip link del dev {self.ifname}')
+ return
+
+ def set_netns(self, netns):
+ """
+ Add interface from given NETNS.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('dum0').set_netns('foo')
+ """
+
+ self.set_interface('netns', netns)
+
def set_vrf(self, vrf):
"""
Add/Remove interface from given VRF instance.
@@ -1052,14 +1089,6 @@ class Interface(Control):
addr_is_v4 = is_ipv4(addr)
- # we can't have both DHCP and static IPv4 addresses assigned
- for a in self._addr:
- if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or
- ( a == 'dhcp' and addr != 'dhcpv6' and addr_is_v4 ) ):
- raise ConfigError((
- "Can't configure both static IPv4 and DHCP address "
- "on the same interface"))
-
# add to interface
if addr == 'dhcp':
self.set_dhcp(True)
@@ -1357,6 +1386,16 @@ class Interface(Control):
if mac:
self.set_mac(mac)
+ # If interface is connected to NETNS we don't have to check all other
+ # settings like MTU/IPv6/sysctl values, etc.
+ # Since the interface is pushed onto a separate logical stack
+ # Configure NETNS
+ if dict_search('netns', config) != None:
+ self.set_netns(config.get('netns', ''))
+ return
+ else:
+ self.del_netns(config.get('netns', ''))
+
# Update interface description
self.set_alias(config.get('description', ''))
diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py
index 0e4447b9e..91f667b65 100644
--- a/python/vyos/ifconfig/section.py
+++ b/python/vyos/ifconfig/section.py
@@ -52,12 +52,12 @@ class Section:
name: name of the interface
vlan: if vlan is True, do not stop at the vlan number
"""
- name = name.rstrip('0123456789')
- name = name.rstrip('.')
- if vlan:
- name = name.rstrip('0123456789.')
if vrrp:
- name = name.rstrip('0123456789v')
+ name = re.sub(r'\d(\d|v|\.)*$', '', name)
+ elif vlan:
+ name = re.sub(r'\d(\d|\.)*$', '', name)
+ else:
+ name = re.sub(r'\d+$', '', name)
return name
@classmethod
diff --git a/python/vyos/ifconfig/wwan.py b/python/vyos/ifconfig/wwan.py
index f18959a60..845c9bef9 100644
--- a/python/vyos/ifconfig/wwan.py
+++ b/python/vyos/ifconfig/wwan.py
@@ -26,3 +26,20 @@ class WWANIf(Interface):
'eternal': 'wwan[0-9]+$',
},
}
+
+ def remove(self):
+ """
+ Remove interface from config. Removing the interface deconfigures all
+ assigned IP addresses.
+ Example:
+ >>> from vyos.ifconfig import WWANIf
+ >>> i = WWANIf('wwan0')
+ >>> i.remove()
+ """
+
+ if self.exists(self.ifname):
+ # interface is placed in A/D state when removed from config! It
+ # will remain visible for the operating system.
+ self.set_admin_state('down')
+
+ super().remove()
diff --git a/python/vyos/range_regex.py b/python/vyos/range_regex.py
new file mode 100644
index 000000000..a8190d140
--- /dev/null
+++ b/python/vyos/range_regex.py
@@ -0,0 +1,142 @@
+'''Copyright (c) 2013, Dmitry Voronin
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+'''
+import math
+
+# coding=utf8
+
+# Split range to ranges that has its unique pattern.
+# Example for 12-345:
+#
+# 12- 19: 1[2-9]
+# 20- 99: [2-9]\d
+# 100-299: [1-2]\d{2}
+# 300-339: 3[0-3]\d
+# 340-345: 34[0-5]
+
+def range_to_regex(inpt_range):
+ if isinstance(inpt_range, str):
+ range_list = inpt_range.split('-')
+ # Check input arguments
+ if len(range_list) == 2:
+ # The first element in range must be higher then the second
+ if int(range_list[0]) < int(range_list[1]):
+ return regex_for_range(int(range_list[0]), int(range_list[1]))
+
+ return None
+
+def bounded_regex_for_range(min_, max_):
+ return r'\b({})\b'.format(regex_for_range(min_, max_))
+
+def regex_for_range(min_, max_):
+ """
+ > regex_for_range(12, 345)
+ '1[2-9]|[2-9]\d|[1-2]\d{2}|3[0-3]\d|34[0-5]'
+ """
+ positive_subpatterns = []
+ negative_subpatterns = []
+
+ if min_ < 0:
+ min__ = 1
+ if max_ < 0:
+ min__ = abs(max_)
+ max__ = abs(min_)
+
+ negative_subpatterns = split_to_patterns(min__, max__)
+ min_ = 0
+
+ if max_ >= 0:
+ positive_subpatterns = split_to_patterns(min_, max_)
+
+ negative_only_subpatterns = ['-' + val for val in negative_subpatterns if val not in positive_subpatterns]
+ positive_only_subpatterns = [val for val in positive_subpatterns if val not in negative_subpatterns]
+ intersected_subpatterns = ['-?' + val for val in negative_subpatterns if val in positive_subpatterns]
+
+ subpatterns = negative_only_subpatterns + intersected_subpatterns + positive_only_subpatterns
+ return '|'.join(subpatterns)
+
+
+def split_to_patterns(min_, max_):
+ subpatterns = []
+
+ start = min_
+ for stop in split_to_ranges(min_, max_):
+ subpatterns.append(range_to_pattern(start, stop))
+ start = stop + 1
+
+ return subpatterns
+
+
+def split_to_ranges(min_, max_):
+ stops = {max_}
+
+ nines_count = 1
+ stop = fill_by_nines(min_, nines_count)
+ while min_ <= stop < max_:
+ stops.add(stop)
+
+ nines_count += 1
+ stop = fill_by_nines(min_, nines_count)
+
+ zeros_count = 1
+ stop = fill_by_zeros(max_ + 1, zeros_count) - 1
+ while min_ < stop <= max_:
+ stops.add(stop)
+
+ zeros_count += 1
+ stop = fill_by_zeros(max_ + 1, zeros_count) - 1
+
+ stops = list(stops)
+ stops.sort()
+
+ return stops
+
+
+def fill_by_nines(integer, nines_count):
+ return int(str(integer)[:-nines_count] + '9' * nines_count)
+
+
+def fill_by_zeros(integer, zeros_count):
+ return integer - integer % 10 ** zeros_count
+
+
+def range_to_pattern(start, stop):
+ pattern = ''
+ any_digit_count = 0
+
+ for start_digit, stop_digit in zip(str(start), str(stop)):
+ if start_digit == stop_digit:
+ pattern += start_digit
+ elif start_digit != '0' or stop_digit != '9':
+ pattern += '[{}-{}]'.format(start_digit, stop_digit)
+ else:
+ any_digit_count += 1
+
+ if any_digit_count:
+ pattern += r'\d'
+
+ if any_digit_count > 1:
+ pattern += '{{{}}}'.format(any_digit_count)
+
+ return pattern \ No newline at end of file
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
index e972050b7..732ef76b7 100644
--- a/python/vyos/remote.py
+++ b/python/vyos/remote.py
@@ -13,38 +13,40 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-from ftplib import FTP
import os
import shutil
import socket
+import ssl
import stat
import sys
import tempfile
import urllib.parse
-import urllib.request as urlreq
-from vyos.template import get_ip
-from vyos.template import ip_from_cidr
-from vyos.template import is_interface
-from vyos.template import is_ipv6
-from vyos.util import cmd
+from ftplib import FTP
+from ftplib import FTP_TLS
+
+from paramiko import SSHClient
+from paramiko import MissingHostKeyPolicy
+
+from requests import Session
+from requests.adapters import HTTPAdapter
+from requests.packages.urllib3 import PoolManager
+
from vyos.util import ask_yes_no
-from vyos.util import print_error
-from vyos.util import make_progressbar
+from vyos.util import begin
+from vyos.util import cmd
from vyos.util import make_incremental_progressbar
+from vyos.util import make_progressbar
+from vyos.util import print_error
from vyos.version import get_version
-from paramiko import SSHClient
-from paramiko import SSHException
-from paramiko import MissingHostKeyPolicy
-# This is a hardcoded path and no environment variable can change it.
-KNOWN_HOSTS_FILE = os.path.expanduser('~/.ssh/known_hosts')
+
CHUNK_SIZE = 8192
class InteractivePolicy(MissingHostKeyPolicy):
"""
- Policy for interactively querying the user on whether to proceed with
- SSH connections to unknown hosts.
+ Paramiko policy for interactively querying the user on whether to proceed
+ with SSH connections to unknown hosts.
"""
def missing_host_key(self, client, hostname, key):
print_error(f"Host '{hostname}' not found in known hosts.")
@@ -57,339 +59,279 @@ class InteractivePolicy(MissingHostKeyPolicy):
else:
raise SSHException(f"Cannot connect to unknown host '{hostname}'.")
-
-## Helper routines
-def get_authentication_variables(default_username=None, default_password=None):
+class SourceAdapter(HTTPAdapter):
"""
- Return the environment variables `$REMOTE_USERNAME` and `$REMOTE_PASSWORD` and
- return the defaults provided if environment variables are empty or nonexistent.
+ urllib3 transport adapter for setting source addresses per session.
"""
- username, password = os.getenv('REMOTE_USERNAME'), os.getenv('REMOTE_PASSWORD')
- # Fall back to defaults if the username variable doesn't exist or is an empty string.
- # Note that this is different from `os.getenv('REMOTE_USERNAME', default=default_username)`,
- # as we want the username and the password to have the same behaviour.
- if not username:
- return default_username, default_password
- else:
- return username, password
-
-def get_source_address(source):
+ def __init__(self, source_pair, *args, **kwargs):
+ # A source pair is a tuple of a source host string and source port respectively.
+ # Supply '' and 0 respectively for default values.
+ self._source_pair = source_pair
+ super(SourceAdapter, self).__init__(*args, **kwargs)
+
+ def init_poolmanager(self, connections, maxsize, block=False):
+ self.poolmanager = PoolManager(
+ num_pools=connections, maxsize=maxsize,
+ block=block, source_address=self._source_pair)
+
+class WrappedFile:
+ def __init__(self, obj, size=None, chunk_size=CHUNK_SIZE):
+ self._obj = obj
+ self._progress = size and make_incremental_progressbar(chunk_size / size)
+ def read(self, size=-1):
+ if self._progress:
+ next(self._progress)
+ self._obj.read(size)
+ def write(self, size=-1):
+ if self._progress:
+ next(self._progress)
+ self._obj.write(size)
+ def __getattr__(self, attr):
+ return getattr(self._obj, attr)
+
+
+def check_storage(path, size):
"""
- Take a string vaguely indicating an origin source (interface, hostname or IP address),
- return a tuple in the format `(source_pair, address_family)` where
- `source_pair` is `(source_address, source_port)`.
+ Check whether `path` has enough storage space for a transfer of `size` bytes.
"""
- # TODO: Properly distinguish between IPv4 and IPv6.
- port = 0
- if is_interface(source):
- source = ip_from_cidr(get_ip(source)[0])
- if is_ipv6(source):
- return (source, port), socket.AF_INET6
+ path = os.path.abspath(os.path.expanduser(path))
+ directory = path if os.path.isdir(path) else (os.path.dirname(os.path.expanduser(path)) or os.getcwd())
+ # `size` can be None or 0 to indicate unknown size.
+ if not size:
+ print_error('Warning: Cannot determine size of remote file.')
+ print_error('Bravely continuing regardless.')
+ return
+
+ if size < 1024 * 1024:
+ print_error(f'The file is {size / 1024.0:.3f} KiB.')
else:
- return (socket.gethostbyname(source), port), socket.AF_INET
-
-def get_port_from_url(url):
- """
- Return the port number from the given `url` named tuple, fall back to
- the default if there isn't one.
- """
- defaults = {"http": 80, "https": 443, "ftp": 21, "tftp": 69,\
- "ssh": 22, "scp": 22, "sftp": 22}
- if url.port:
- return url.port
- else:
- return defaults[url.scheme]
-
-
-## FTP routines
-def upload_ftp(local_path, hostname, remote_path,\
- username='anonymous', password='', port=21,\
- source_pair=None, progressbar=False):
- size = os.path.getsize(local_path)
- with FTP(source_address=source_pair) as conn:
- conn.connect(hostname, port)
- conn.login(username, password)
- with open(local_path, 'rb') as file:
- if progressbar and size:
+ print_error(f'The file is {size / (1024.0 * 1024.0):.3f} MiB.')
+
+ # Will throw `FileNotFoundError' if `directory' is absent.
+ if size > shutil.disk_usage(directory).free:
+ raise OSError(f'Not enough disk space available in "{directory}".')
+
+
+class FtpC:
+ def __init__(self, url, progressbar=False, check_space=False, source_host='', source_port=0):
+ self.secure = url.scheme == 'ftps'
+ self.hostname = url.hostname
+ self.path = url.path
+ self.username = url.username or os.getenv('REMOTE_USERNAME', 'anonymous')
+ self.password = url.password or os.getenv('REMOTE_PASSWORD', '')
+ self.port = url.port or 21
+ self.source = (source_host, source_port)
+ self.progressbar = progressbar
+ self.check_space = check_space
+
+ def _establish(self):
+ if self.secure:
+ return FTP_TLS(source_address=self.source, context=ssl.create_default_context())
+ else:
+ return FTP(source_address=self.source)
+
+ def download(self, location: str):
+ # Open the file upfront before establishing connection.
+ with open(location, 'wb') as f, self._establish() as conn:
+ conn.connect(self.hostname, self.port)
+ conn.login(self.username, self.password)
+ # Set secure connection over TLS.
+ if self.secure:
+ conn.prot_p()
+ # Almost all FTP servers support the `SIZE' command.
+ if self.check_space:
+ check_storage(path, conn.size(self.path))
+ # No progressbar if we can't determine the size or if the file is too small.
+ if self.progressbar and size and size > CHUNK_SIZE:
progress = make_incremental_progressbar(CHUNK_SIZE / size)
next(progress)
- callback = lambda block: next(progress)
+ callback = lambda block: begin(f.write(block), next(progress))
else:
- callback = None
- conn.storbinary(f'STOR {remote_path}', file, CHUNK_SIZE, callback)
-
-def download_ftp(local_path, hostname, remote_path,\
- username='anonymous', password='', port=21,\
- source_pair=None, progressbar=False):
- with FTP(source_address=source_pair) as conn:
- conn.connect(hostname, port)
- conn.login(username, password)
- size = conn.size(remote_path)
- with open(local_path, 'wb') as file:
- # No progressbar if we can't determine the size.
- if progressbar and size:
+ callback = f.write
+ conn.retrbinary('RETR ' + self.path, callback, CHUNK_SIZE)
+
+ def upload(self, location: str):
+ size = os.path.getsize(location)
+ with open(location, 'rb') as f, self._establish() as conn:
+ conn.connect(self.hostname, self.port)
+ conn.login(self.username, self.password)
+ if self.secure:
+ conn.prot_p()
+ if self.progressbar and size and size > CHUNK_SIZE:
progress = make_incremental_progressbar(CHUNK_SIZE / size)
next(progress)
- callback = lambda block: (file.write(block), next(progress))
+ callback = lambda block: next(progress)
else:
- callback = file.write
- conn.retrbinary(f'RETR {remote_path}', callback, CHUNK_SIZE)
-
-def get_ftp_file_size(hostname, remote_path,\
- username='anonymous', password='', port=21,\
- source_pair=None):
- with FTP(source_address=source) as conn:
- conn.connect(hostname, port)
- conn.login(username, password)
- size = conn.size(remote_path)
- if size:
- return size
- else:
- # SIZE is an extension to the FTP specification, although it's extremely common.
- raise ValueError('Failed to receive file size from FTP server. \
- Perhaps the server does not implement the SIZE command?')
-
-
-## SFTP/SCP routines
-def transfer_sftp(mode, local_path, hostname, remote_path,\
- username=None, password=None, port=22,\
- source_tuple=None, progressbar=False):
- sock = None
- if source_tuple:
- (source_address, source_port), address_family = source_tuple
- sock = socket.socket(address_family, socket.SOCK_STREAM)
- sock.bind((source_address, source_port))
- sock.connect((hostname, port))
- callback = make_progressbar() if progressbar else None
- with SSHClient() as ssh:
+ callback = None
+ conn.storbinary('STOR ' + self.path, f, CHUNK_SIZE, callback)
+
+class SshC:
+ known_hosts = os.path.expanduser('~/.ssh/known_hosts')
+ def __init__(self, url, progressbar=False, check_space=False, source_host='', source_port=0):
+ self.hostname = url.hostname
+ self.path = url.path
+ self.username = url.username or os.getenv('REMOTE_USERNAME')
+ self.password = url.password or os.getenv('REMOTE_PASSWORD')
+ self.port = url.port or 22
+ self.source = (source_host, source_port)
+ self.progressbar = progressbar
+ self.check_space = check_space
+
+ def _establish(self):
+ ssh = SSHClient()
ssh.load_system_host_keys()
- if os.path.exists(KNOWN_HOSTS_FILE):
- ssh.load_host_keys(KNOWN_HOSTS_FILE)
+ # Try to load from a user-local known hosts file if one exists.
+ if os.path.exists(self.known_hosts):
+ ssh.load_host_keys(self.known_hosts)
ssh.set_missing_host_key_policy(InteractivePolicy())
- ssh.connect(hostname, port, username, password, sock=sock)
- with ssh.open_sftp() as sftp:
- if mode == 'upload':
+ # `socket.create_connection()` automatically picks a NIC and an IPv4/IPv6 address family
+ # for us on dual-stack systems.
+ sock = socket.create_connection((self.hostname, self.port), socket.getdefaulttimeout(), self.source)
+ ssh.connect(self.hostname, self.port, self.username, self.password, sock=sock)
+ return ssh
+
+ def download(self, location: str):
+ callback = make_progressbar() if self.progressbar else None
+ with self._establish() as ssh, ssh.open_sftp() as sftp:
+ if self.check_space:
+ check_storage(location, sftp.stat(self.path).st_size)
+ sftp.get(self.path, location, callback=callback)
+
+ def upload(self, location: str):
+ callback = make_progressbar() if self.progressbar else None
+ with self._establish() as ssh, ssh.open_sftp() as sftp:
+ try:
+ # If the remote path is a directory, use the original filename.
+ if stat.S_ISDIR(sftp.stat(self.path).st_mode):
+ path = os.path.join(self.path, os.path.basename(location))
+ # A file exists at this destination. We're simply going to clobber it.
+ else:
+ path = self.path
+ # This path doesn't point at any existing file. We can freely use this filename.
+ except IOError:
+ path = self.path
+ finally:
+ sftp.put(location, path, callback=callback)
+
+
+class HttpC:
+ def __init__(self, url, progressbar=False, check_space=False, source_host='', source_port=0):
+ self.urlstring = urllib.parse.urlunsplit(url)
+ self.progressbar = progressbar
+ self.check_space = check_space
+ self.source_pair = (source_host, source_port)
+ self.username = url.username or os.getenv('REMOTE_USERNAME')
+ self.password = url.password or os.getenv('REMOTE_PASSWORD')
+
+ def _establish(self):
+ session = Session()
+ session.mount(self.urlstring, SourceAdapter(self.source_pair))
+ session.headers.update({'User-Agent': 'VyOS/' + get_version()})
+ if self.username:
+ session.auth = self.username, self.password
+ return session
+
+ def download(self, location: str):
+ with self._establish() as s:
+ # We ask for uncompressed downloads so that we don't have to deal with decoding.
+ # Not only would it potentially mess up with the progress bar but
+ # `shutil.copyfileobj(request.raw, file)` does not handle automatic decoding.
+ s.headers.update({'Accept-Encoding': 'identity'})
+ with s.head(self.urlstring, allow_redirects=True) as r:
+ # Abort early if the destination is inaccessible.
+ r.raise_for_status()
+ # If the request got redirected, keep the last URL we ended up with.
+ if r.history:
+ final_urlstring = r.history[-1].url
+ print_error('Redirecting to ' + final_urlstring)
+ else:
+ final_urlstring = self.urlstring
+ # Check for the prospective file size.
try:
- # If the remote path is a directory, use the original filename.
- if stat.S_ISDIR(sftp.stat(remote_path).st_mode):
- path = os.path.join(remote_path, os.path.basename(local_path))
- # A file exists at this destination. We're simply going to clobber it.
- else:
- path = remote_path
- # This path doesn't point at any existing file. We can freely use this filename.
- except IOError:
- path = remote_path
- finally:
- sftp.put(local_path, path, callback=callback)
- elif mode == 'download':
- sftp.get(remote_path, local_path, callback=callback)
- elif mode == 'size':
- return sftp.stat(remote_path).st_size
-
-def upload_sftp(*args, **kwargs):
- transfer_sftp('upload', *args, **kwargs)
-
-def download_sftp(*args, **kwargs):
- transfer_sftp('download', *args, **kwargs)
-
-def get_sftp_file_size(*args, **kwargs):
- return transfer_sftp('size', None, *args, **kwargs)
-
-
-## TFTP routines
-def upload_tftp(local_path, hostname, remote_path, port=69, source=None, progressbar=False):
- source_option = f'--interface {source}' if source else ''
- progress_flag = '--progress-bar' if progressbar else '-s'
- with open(local_path, 'rb') as file:
- cmd(f'curl {source_option} {progress_flag} -T - tftp://{hostname}:{port}/{remote_path}',\
- stderr=None, input=file.read()).encode()
-
-def download_tftp(local_path, hostname, remote_path, port=69, source=None, progressbar=False):
- source_option = f'--interface {source}' if source else ''
- # Not really applicable but we pass it for the sake of uniformity.
- progress_flag = '--progress-bar' if progressbar else '-s'
- with open(local_path, 'wb') as file:
- file.write(cmd(f'curl {source_option} {progress_flag} tftp://{hostname}:{port}/{remote_path}',\
- stderr=None).encode())
-
-# get_tftp_file_size() is unimplemented because there is no way to obtain a file's size through TFTP,
-# as TFTP does not specify a SIZE command.
-
-
-## HTTP(S) routines
-def install_request_opener(urlstring, username, password):
- """
- Take `username` and `password` strings and install the appropriate
- password manager to `urllib.request.urlopen()` for the given `urlstring`.
- """
- manager = urlreq.HTTPPasswordMgrWithDefaultRealm()
- manager.add_password(None, urlstring, username, password)
- urlreq.install_opener(urlreq.build_opener(urlreq.HTTPBasicAuthHandler(manager)))
-
-# upload_http() is unimplemented.
-
-def download_http(local_path, urlstring, username=None, password=None, progressbar=False):
- """
- Download the file from from `urlstring` to `local_path`.
- Optionally takes `username` and `password` for authentication.
- """
- request = urlreq.Request(urlstring, headers={'User-Agent': 'VyOS/' + get_version()})
- if username:
- install_request_opener(urlstring, username, password)
- with open(local_path, 'wb') as file, urlreq.urlopen(request) as response:
- size = response.getheader('Content-Length')
- if progressbar and size:
- progress = make_incremental_progressbar(CHUNK_SIZE / int(size))
- next(progress)
- for chunk in iter(lambda: response.read(CHUNK_SIZE), b''):
- file.write(chunk)
- next(progress)
- next(progress)
- # If we can't determine the size or if a progress bar wasn't requested,
- # we can let `shutil` take care of the copying.
- else:
- shutil.copyfileobj(response, file)
-
-def get_http_file_size(urlstring, username=None, password=None):
- """
- Return the size of the file from `urlstring` in terms of number of bytes.
- Optionally takes `username` and `password` for authentication.
- """
- request = urlreq.Request(urlstring, headers={'User-Agent': 'VyOS/' + get_version()})
- if username:
- install_request_opener(urlstring, username, password)
- with urlreq.urlopen(request) as response:
- size = response.getheader('Content-Length')
- if size:
- return int(size)
- # The server didn't send 'Content-Length' in the response headers.
- else:
- raise ValueError('Failed to receive file size from HTTP server.')
-
-
-## Dynamic dispatchers
-def download(local_path, urlstring, source=None, progressbar=False):
+ size = int(r.headers['Content-Length'])
+ # In case the server does not supply the header.
+ except KeyError:
+ size = None
+ if self.check_space:
+ check_storage(location, size)
+ with s.get(final_urlstring, stream=True) as r, open(location, 'wb') as f:
+ if self.progressbar and size:
+ progress = make_incremental_progressbar(CHUNK_SIZE / size)
+ next(progress)
+ for chunk in iter(lambda: begin(next(progress), r.raw.read(CHUNK_SIZE)), b''):
+ f.write(chunk)
+ else:
+ # We'll try to stream the download directly with `copyfileobj()` so that large
+ # files (like entire VyOS images) don't occupy much memory.
+ shutil.copyfileobj(r.raw, f)
+
+ def upload(self, location: str):
+ size = os.path.getsize(location) if self.progressbar else None
+ # Keep in mind that `data` can be a file-like or iterable object.
+ with self._establish() as s, file(location, 'rb') as f:
+ s.post(self.urlstring, data=WrappedFile(f, size), allow_redirects=True)
+
+
+class TftpC:
+ # We simply allow `curl` to take over because
+ # 1. TFTP is rather simple.
+ # 2. Since there's no concept authentication, we don't need to deal with keys/passwords.
+ # 3. It would be a waste to import, audit and maintain a third-party library for TFTP.
+ # 4. I'd rather not implement the entire protocol here, no matter how simple it is.
+ def __init__(self, url, progressbar=False, check_space=False, source_host=None, source_port=0):
+ source_option = f'--interface {source_host} --local-port {source_port}' if source_host else ''
+ progress_flag = '--progress-bar' if progressbar else '-s'
+ self.command = f'curl {source_option} {progress_flag}'
+ self.urlstring = urllib.parse.urlunsplit(url)
+
+ def download(self, location: str):
+ with open(location, 'wb') as f:
+ f.write(cmd(f'{self.command} "{self.urlstring}"').encode())
+
+ def upload(self, location: str):
+ with open(location, 'rb') as f:
+ cmd(f'{self.command} -T - "{self.urlstring}"', input=f.read())
+
+
+def urlc(urlstring, *args, **kwargs):
"""
- Dispatch the appropriate download function for the given `urlstring` and save to `local_path`.
- Optionally takes a `source` address or interface (not valid for HTTP(S)).
- Supports HTTP, HTTPS, FTP, SFTP, SCP (through SFTP) and TFTP.
- Reads `$REMOTE_USERNAME` and `$REMOTE_PASSWORD` environment variables.
+ Dynamically dispatch the appropriate protocol class.
"""
- url = urllib.parse.urlparse(urlstring)
- username, password = get_authentication_variables(url.username, url.password)
- port = get_port_from_url(url)
-
- if url.scheme == 'http' or url.scheme == 'https':
- if source:
- print_error('Warning: Custom source address not supported for HTTP connections.')
- download_http(local_path, urlstring, username, password, progressbar)
- elif url.scheme == 'ftp':
- source = get_source_address(source)[0] if source else None
- username = username if username else 'anonymous'
- download_ftp(local_path, url.hostname, url.path, username, password, port, source, progressbar)
- elif url.scheme == 'sftp' or url.scheme == 'scp':
- source = get_source_address(source) if source else None
- download_sftp(local_path, url.hostname, url.path, username, password, port, source, progressbar)
- elif url.scheme == 'tftp':
- download_tftp(local_path, url.hostname, url.path, port, source, progressbar)
- else:
- raise ValueError(f'Unsupported URL scheme: {url.scheme}')
+ url_classes = {'http': HttpC, 'https': HttpC, 'ftp': FtpC, 'ftps': FtpC, \
+ 'sftp': SshC, 'ssh': SshC, 'scp': SshC, 'tftp': TftpC}
+ url = urllib.parse.urlsplit(urlstring)
+ try:
+ return url_classes[url.scheme](url, *args, **kwargs)
+ except KeyError:
+ raise ValueError(f'Unsupported URL scheme: "{url.scheme}"')
-def upload(local_path, urlstring, source=None, progressbar=False):
- """
- Dispatch the appropriate upload function for the given URL and upload from local path.
- Optionally takes a `source` address.
- Supports FTP, SFTP, SCP (through SFTP) and TFTP.
- Reads `$REMOTE_USERNAME` and `$REMOTE_PASSWORD` environment variables.
- """
- url = urllib.parse.urlparse(urlstring)
- username, password = get_authentication_variables(url.username, url.password)
- port = get_port_from_url(url)
-
- if url.scheme == 'ftp':
- username = username if username else 'anonymous'
- source = get_source_address(source)[0] if source else None
- upload_ftp(local_path, url.hostname, url.path, username, password, port, source, progressbar)
- elif url.scheme == 'sftp' or url.scheme == 'scp':
- source = get_source_address(source) if source else None
- upload_sftp(local_path, url.hostname, url.path, username, password, port, source, progressbar)
- elif url.scheme == 'tftp':
- upload_tftp(local_path, url.hostname, url.path, port, source, progressbar)
- else:
- raise ValueError(f'Unsupported URL scheme: {url.scheme}')
+def download(local_path, urlstring, *args, **kwargs):
+ urlc(urlstring, *args, **kwargs).download(local_path)
-def get_remote_file_size(urlstring, source=None):
- """
- Dispatch the appropriate function to return the size of the remote file from `urlstring`
- in terms of number of bytes.
- Optionally takes a `source` address (not valid for HTTP(S)).
- Supports HTTP, HTTPS, FTP and SFTP (through SFTP).
- Reads `$REMOTE_USERNAME` and `$REMOTE_PASSWORD` environment variables.
- """
- url = urllib.parse.urlparse(urlstring)
- username, password = get_authentication_variables(url.username, url.password)
- port = get_port_from_url(url)
-
- if url.scheme == 'http' or url.scheme == 'https':
- if source:
- print_error('Warning: Custom source address not supported for HTTP connections.')
- return get_http_file_size(urlstring, username, password)
- elif url.scheme == 'ftp':
- source = get_source_address(source)[0] if source else None
- username = username if username else 'anonymous'
- return get_ftp_file_size(url.hostname, url.path, username, password, port, source)
- elif url.scheme == 'sftp' or url.scheme == 'scp':
- source = get_source_address(source) if source else None
- return get_sftp_file_size(url.hostname, url.path, username, password, port, source)
- else:
- raise ValueError(f'Unsupported URL scheme: {url.scheme}')
+def upload(local_path, urlstring, *args, **kwargs):
+ urlc(urlstring, *args, **kwargs).upload(local_path)
-def get_remote_config(urlstring, source=None):
+def get_remote_config(urlstring, source_host='', source_port=0):
"""
- Download remote (config) file from `urlstring` and return the contents as a string.
- Args:
- remote file URI:
- tftp://<host>[:<port>]/<file>
- http[s]://<host>[:<port>]/<file>
- [scp|sftp|ftp]://[<user>[:<passwd>]@]<host>[:port]/<file>
- source address (optional):
- <interface>
- <IP address>
+ Quietly download a file and return it as a string.
"""
temp = tempfile.NamedTemporaryFile(delete=False).name
try:
- download(temp, urlstring, source)
- with open(temp, 'r') as file:
- return file.read()
+ download(temp, urlstring, False, False, source_host, source_port)
+ with open(temp, 'r') as f:
+ return f.read()
finally:
os.remove(temp)
-def friendly_download(local_path, urlstring, source=None):
+def friendly_download(local_path, urlstring, source_host='', source_port=0):
"""
- Download from `urlstring` to `local_path` in an informative way.
- Checks the storage space before attempting download.
- Intended to be called from interactive, user-facing scripts.
+ Download with a progress bar, reassuring messages and free space checks.
"""
- destination_directory = os.path.dirname(local_path)
try:
- free_space = shutil.disk_usage(destination_directory).free
- try:
- file_size = get_remote_file_size(urlstring, source)
- if file_size < 1024 * 1024:
- print_error(f'The file is {file_size / 1024.0:.3f} KiB.')
- else:
- print_error(f'The file is {file_size / (1024.0 * 1024.0):.3f} MiB.')
- if file_size > free_space:
- raise OSError(f'Not enough disk space available in "{destination_directory}".')
- except ValueError:
- # Can't do a storage check in this case, so we bravely continue.
- file_size = 0
- print_error('Could not determine the file size in advance.')
- else:
- print_error('Downloading...')
- download(local_path, urlstring, source, progressbar=file_size > 1024 * 1024)
+ print_error('Downloading...')
+ download(local_path, urlstring, True, True, source_host, source_port)
except KeyboardInterrupt:
- print_error('Download aborted by user.')
+ print_error('\nDownload aborted by user.')
sys.exit(1)
except:
import traceback
@@ -401,3 +343,4 @@ def friendly_download(local_path, urlstring, source=None):
sys.exit(1)
else:
print_error('Download complete.')
+ sys.exit(0)
diff --git a/python/vyos/template.py b/python/vyos/template.py
index d13915766..b32cafe74 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -349,7 +349,6 @@ def get_dhcp_router(interface):
Returns False of no router is found, returns the IP address as string if
a router is found.
"""
- interface = interface.replace('.', '_')
lease_file = f'/var/lib/dhcp/dhclient_{interface}.leases'
if not os.path.exists(lease_file):
return None
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 849b27d3b..954c6670d 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -489,6 +489,40 @@ def seconds_to_human(s, separator=""):
return result
+def bytes_to_human(bytes, initial_exponent=0):
+ """ Converts a value in bytes to a human-readable size string like 640 KB
+
+ The initial_exponent parameter is the exponent of 2,
+ e.g. 10 (1024) for kilobytes, 20 (1024 * 1024) for megabytes.
+ """
+
+ from math import log2
+
+ bytes = bytes * (2**initial_exponent)
+
+ # log2 is a float, while range checking requires an int
+ exponent = int(log2(bytes))
+
+ if exponent < 10:
+ value = bytes
+ suffix = "B"
+ elif exponent in range(10, 20):
+ value = bytes / 1024
+ suffix = "KB"
+ elif exponent in range(20, 30):
+ value = bytes / 1024**2
+ suffix = "MB"
+ elif exponent in range(30, 40):
+ value = bytes / 1024**3
+ suffix = "GB"
+ else:
+ value = bytes / 1024**4
+ suffix = "TB"
+ # Add a new case when the first machine with petabyte RAM
+ # hits the market.
+
+ size_string = "{0:.2f} {1}".format(value, suffix)
+ return size_string
def get_cfg_group_id():
from grp import getgrnam
@@ -630,7 +664,6 @@ def ask_yes_no(question, default=False) -> bool:
except EOFError:
stdout.write("\nPlease respond with yes/y or no/n\n")
-
def is_admin() -> bool:
"""Look if current user is in sudo group"""
from getpass import getuser
@@ -761,6 +794,24 @@ def get_interface_address(interface):
tmp = loads(cmd(f'ip -d -j addr show {interface}'))[0]
return tmp
+def get_interface_namespace(iface):
+ """
+ Returns wich netns the interface belongs to
+ """
+ from json import loads
+ # Check if netns exist
+ tmp = loads(cmd(f'ip --json netns ls'))
+ if len(tmp) == 0:
+ return None
+
+ for ns in tmp:
+ namespace = f'{ns["name"]}'
+ # Search interface in each netns
+ data = loads(cmd(f'ip netns exec {namespace} ip -j link show'))
+ for compare in data:
+ if iface == compare["ifname"]:
+ return namespace
+
def get_all_vrfs():
""" Return a dictionary of all system wide known VRF instances """
from json import loads
@@ -823,6 +874,20 @@ def make_incremental_progressbar(increment: float):
while True:
yield
+def begin(*args):
+ """
+ Evaluate arguments in order and return the result of the *last* argument.
+ For combining multiple expressions in one statement. Useful for lambdas.
+ """
+ return args[-1]
+
+def begin0(*args):
+ """
+ Evaluate arguments in order and return the result of the *first* argument.
+ For combining multiple expressions in one statement. Useful for lambdas.
+ """
+ return args[0]
+
def is_systemd_service_active(service):
""" Test is a specified systemd service is activated.
Returns True if service is active, false otherwise.
@@ -869,3 +934,57 @@ def check_port_availability(ipaddress, port, protocol):
return True
except:
return False
+
+def install_into_config(conf, config_paths, override_prompt=True):
+ # Allows op-mode scripts to install values if called from an active config session
+ # config_paths: dict of config paths
+ # override_prompt: if True, user will be prompted before existing nodes are overwritten
+
+ if not config_paths:
+ return None
+
+ from vyos.config import Config
+
+ if not Config().in_session():
+ print('You are not in configure mode, commands to install manually from configure mode:')
+ for path in config_paths:
+ print(f'set {path}')
+ return None
+
+ count = 0
+
+ for path in config_paths:
+ if override_prompt and conf.exists(path) and not conf.is_multi(path):
+ if not ask_yes_no(f'Config node "{node}" already exists. Do you want to overwrite it?'):
+ continue
+
+ cmd(f'/opt/vyatta/sbin/my_set {path}')
+ count += 1
+
+ if count > 0:
+ print(f'{count} value(s) installed. Use "compare" to see the pending changes, and "commit" to apply.')
+
+def is_wwan_connected(interface):
+ """ Determine if a given WWAN interface, e.g. wwan0 is connected to the
+ carrier network or not """
+ import json
+
+ if not interface.startswith('wwan'):
+ raise ValueError(f'Specified interface "{interface}" is not a WWAN interface')
+
+ modem = interface.lstrip('wwan')
+
+ tmp = cmd(f'mmcli --modem {modem} --output-json')
+ tmp = json.loads(tmp)
+
+ # return True/False if interface is in connected state
+ return dict_search('modem.generic.state', tmp) == 'connected'
+
+def boot_configuration_complete() -> bool:
+ """ Check if the boot config loader has completed
+ """
+ from vyos.defaults import config_status
+
+ if os.path.isfile(config_status):
+ return True
+ return False
diff --git a/smoketest/configs/bgp-dmvpn-hub b/smoketest/configs/bgp-dmvpn-hub
new file mode 100644
index 000000000..fc5aadd8f
--- /dev/null
+++ b/smoketest/configs/bgp-dmvpn-hub
@@ -0,0 +1,174 @@
+interfaces {
+ ethernet eth0 {
+ address 100.64.10.1/31
+ }
+ ethernet eth1 {
+ }
+ loopback lo {
+ }
+ tunnel tun0 {
+ address 192.168.254.62/26
+ encapsulation gre
+ multicast enable
+ parameters {
+ ip {
+ key 1
+ }
+ }
+ source-address 100.64.10.1
+ }
+}
+protocols {
+ bgp 65000 {
+ address-family {
+ ipv4-unicast {
+ network 172.20.0.0/16 {
+ }
+ }
+ }
+ neighbor 192.168.254.1 {
+ peer-group DMVPN
+ remote-as 65001
+ }
+ neighbor 192.168.254.2 {
+ peer-group DMVPN
+ remote-as 65002
+ }
+ neighbor 192.168.254.3 {
+ peer-group DMVPN
+ remote-as 65003
+ }
+ parameters {
+ default {
+ no-ipv4-unicast
+ }
+ log-neighbor-changes
+ }
+ peer-group DMVPN {
+ address-family {
+ ipv4-unicast {
+ }
+ }
+ }
+ timers {
+ holdtime 30
+ keepalive 10
+ }
+ }
+ nhrp {
+ tunnel tun0 {
+ cisco-authentication secret
+ holding-time 300
+ multicast dynamic
+ redirect
+ shortcut
+ }
+ }
+ static {
+ route 0.0.0.0/0 {
+ next-hop 100.64.10.0 {
+ }
+ }
+ route 172.20.0.0/16 {
+ blackhole {
+ distance 200
+ }
+ }
+ }
+}
+system {
+ config-management {
+ commit-revisions 100
+ }
+ conntrack {
+ modules {
+ ftp
+ h323
+ nfs
+ pptp
+ sip
+ sqlnet
+ tftp
+ }
+ }
+ console {
+ device ttyS0 {
+ speed 115200
+ }
+ }
+ host-name cpe-4
+ login {
+ user vyos {
+ authentication {
+ encrypted-password $6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0
+ plaintext-password ""
+ }
+ }
+ }
+ name-server 1.1.1.1
+ name-server 8.8.8.8
+ name-server 9.9.9.9
+ ntp {
+ server time1.vyos.net {
+ }
+ server time2.vyos.net {
+ }
+ server time3.vyos.net {
+ }
+ }
+ syslog {
+ global {
+ facility all {
+ level info
+ }
+ facility protocols {
+ level debug
+ }
+ }
+ }
+}
+vpn {
+ ipsec {
+ esp-group ESP-DMVPN {
+ compression disable
+ lifetime 1800
+ mode transport
+ pfs dh-group2
+ proposal 1 {
+ encryption aes256
+ hash sha1
+ }
+ }
+ ike-group IKE-DMVPN {
+ close-action none
+ ikev2-reauth no
+ key-exchange ikev1
+ lifetime 3600
+ proposal 1 {
+ dh-group 2
+ encryption aes256
+ hash sha1
+ }
+ }
+ ipsec-interfaces {
+ interface eth0
+ }
+ profile NHRPVPN {
+ authentication {
+ mode pre-shared-secret
+ pre-shared-secret VyOS-topsecret
+ }
+ bind {
+ tunnel tun0
+ }
+ esp-group ESP-DMVPN
+ ike-group IKE-DMVPN
+ }
+ }
+}
+
+
+// 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.0-epa3
+
diff --git a/smoketest/configs/bgp-dmvpn-spoke b/smoketest/configs/bgp-dmvpn-spoke
new file mode 100644
index 000000000..3d7503a9b
--- /dev/null
+++ b/smoketest/configs/bgp-dmvpn-spoke
@@ -0,0 +1,201 @@
+interfaces {
+ ethernet eth0 {
+ vif 7 {
+ description PPPoE-UPLINK
+ }
+ }
+ ethernet eth1 {
+ address 172.17.1.1/24
+ }
+ loopback lo {
+ }
+ pppoe pppoe1 {
+ authentication {
+ password cpe-1
+ user cpe-1
+ }
+ no-peer-dns
+ source-interface eth0.7
+ }
+ tunnel tun0 {
+ address 192.168.254.1/26
+ encapsulation gre
+ multicast enable
+ parameters {
+ ip {
+ key 1
+ }
+ }
+ source-address 0.0.0.0
+ }
+}
+nat {
+ source {
+ rule 10 {
+ log enable
+ outbound-interface pppoe1
+ source {
+ address 172.17.0.0/16
+ }
+ translation {
+ address masquerade
+ }
+ }
+ }
+}
+protocols {
+ bgp 65001 {
+ address-family {
+ ipv4-unicast {
+ network 172.17.0.0/16 {
+ }
+ }
+ }
+ neighbor 192.168.254.62 {
+ address-family {
+ ipv4-unicast {
+ }
+ }
+ remote-as 65000
+ }
+ parameters {
+ default {
+ no-ipv4-unicast
+ }
+ log-neighbor-changes
+ }
+ timers {
+ holdtime 30
+ keepalive 10
+ }
+ }
+ nhrp {
+ tunnel tun0 {
+ cisco-authentication secret
+ holding-time 300
+ map 192.168.254.62/26 {
+ nbma-address 100.64.10.1
+ register
+ }
+ multicast nhs
+ redirect
+ shortcut
+ }
+ }
+ static {
+ route 172.17.0.0/16 {
+ blackhole {
+ distance 200
+ }
+ }
+ }
+}
+service {
+ dhcp-server {
+ shared-network-name LAN-3 {
+ subnet 172.17.1.0/24 {
+ default-router 172.17.1.1
+ name-server 172.17.1.1
+ range 0 {
+ start 172.17.1.100
+ stop 172.17.1.200
+ }
+ }
+ }
+ }
+}
+system {
+ config-management {
+ commit-revisions 100
+ }
+ conntrack {
+ modules {
+ ftp
+ h323
+ nfs
+ pptp
+ sip
+ sqlnet
+ tftp
+ }
+ }
+ console {
+ device ttyS0 {
+ speed 115200
+ }
+ }
+ host-name cpe-1
+ login {
+ user vyos {
+ authentication {
+ encrypted-password $6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0
+ plaintext-password ""
+ }
+ }
+ }
+ name-server 1.1.1.1
+ name-server 8.8.8.8
+ name-server 9.9.9.9
+ ntp {
+ server time1.vyos.net {
+ }
+ server time2.vyos.net {
+ }
+ server time3.vyos.net {
+ }
+ }
+ syslog {
+ global {
+ facility all {
+ level info
+ }
+ facility protocols {
+ level debug
+ }
+ }
+ }
+}
+vpn {
+ ipsec {
+ esp-group ESP-DMVPN {
+ compression disable
+ lifetime 1800
+ mode transport
+ pfs dh-group2
+ proposal 1 {
+ encryption aes256
+ hash sha1
+ }
+ }
+ ike-group IKE-DMVPN {
+ close-action none
+ ikev2-reauth no
+ key-exchange ikev1
+ lifetime 3600
+ proposal 1 {
+ dh-group 2
+ encryption aes256
+ hash sha1
+ }
+ }
+ ipsec-interfaces {
+ interface pppoe1
+ }
+ profile NHRPVPN {
+ authentication {
+ mode pre-shared-secret
+ pre-shared-secret VyOS-topsecret
+ }
+ bind {
+ tunnel tun0
+ }
+ esp-group ESP-DMVPN
+ ike-group IKE-DMVPN
+ }
+ }
+}
+
+
+// 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.0-epa3
diff --git a/smoketest/configs/bgp-small-ipv4-unicast b/smoketest/configs/bgp-small-ipv4-unicast
new file mode 100644
index 000000000..83f1effd2
--- /dev/null
+++ b/smoketest/configs/bgp-small-ipv4-unicast
@@ -0,0 +1,77 @@
+interfaces {
+ ethernet eth0 {
+ address 192.0.2.1/24
+ address 2001:db8::1/64
+ }
+ loopback lo {
+ }
+}
+protocols {
+ bgp 65001 {
+ address-family {
+ ipv4-unicast {
+ network 10.0.150.0/23 {
+ }
+ }
+ ipv6-unicast {
+ network 2001:db8:200::/40 {
+ }
+ }
+ }
+ neighbor 192.0.2.10 {
+ remote-as 65010
+ }
+ neighbor 192.0.2.11 {
+ remote-as 65011
+ }
+ neighbor 2001:db8::10 {
+ remote-as 65010
+ }
+ neighbor 2001:db8::11 {
+ remote-as 65011
+ }
+ parameters {
+ log-neighbor-changes
+ }
+ }
+}
+service {
+ ssh {
+ disable-host-validation
+ port 22
+ }
+}
+system {
+ config-management {
+ commit-revisions 200
+ }
+ console {
+ device ttyS0 {
+ speed 115200
+ }
+ }
+ domain-name vyos.net
+ host-name vyos
+ login {
+ user vyos {
+ authentication {
+ encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0
+ plaintext-password ""
+ }
+ }
+ }
+ syslog {
+ global {
+ facility all {
+ level notice
+ }
+ facility protocols {
+ level debug
+ }
+ }
+ }
+}
+
+/* Warning: Do not remove the following line. */
+/* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pptp@1:qos@1:quagga@6:snmp@1:ssh@1:system@9:vrrp@2:wanloadbalance@3:webgui@1:webproxy@1:webproxy@2:zone-policy@1" === */
+/* Release version: 1.2.5 */
diff --git a/smoketest/configs/tunnel-broker b/smoketest/configs/tunnel-broker
index d4a5c2dfc..9a1e79719 100644
--- a/smoketest/configs/tunnel-broker
+++ b/smoketest/configs/tunnel-broker
@@ -56,13 +56,13 @@ interfaces {
tunnel tun100 {
address 172.16.0.1/30
encapsulation gre-bridge
- local-ip 192.0.2.0
+ local-ip 192.0.2.1
remote-ip 192.0.2.100
}
tunnel tun200 {
address 172.16.0.5/30
encapsulation gre
- local-ip 192.0.2.1
+ dhcp-interface eth0
remote-ip 192.0.2.101
}
tunnel tun300 {
diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index 90c534796..340ec4edd 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -493,6 +493,16 @@ class BasicInterfaceTest:
tmp = get_interface_config(vif)
self.assertEqual(tmp['mtu'], int(self._mtu))
+ # T3972: remove vif-c interfaces from vif-s
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for vif_s in self._qinq_range:
+ base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c']
+ self.cli_delete(base)
+
+ self.cli_commit()
+
+
def test_vif_s_protocol_change(self):
# XXX: This testcase is not allowed to run as first testcase, reason
# is the Wireless test will first load the wifi kernel hwsim module
diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py
index 09ca89721..cc0cdaec0 100644
--- a/smoketest/scripts/cli/test_container.py
+++ b/smoketest/scripts/cli/test_container.py
@@ -19,7 +19,6 @@ import json
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
from vyos.util import process_named_running
diff --git a/smoketest/scripts/cli/test_ha_vrrp.py b/smoketest/scripts/cli/test_ha_vrrp.py
index 8c5bb86d8..23a9f7796 100755
--- a/smoketest/scripts/cli/test_ha_vrrp.py
+++ b/smoketest/scripts/cli/test_ha_vrrp.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.ifconfig.vrrp import VRRP
from vyos.util import cmd
@@ -45,7 +44,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase):
for group in groups:
vlan_id = group.lstrip('VLAN')
- self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id])
+ self.cli_delete(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id])
self.cli_delete(base_path)
self.cli_commit()
@@ -109,7 +108,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase):
# Authentication
self.cli_set(group_base + ['authentication', 'type', 'plaintext-password'])
- self.cli_set(group_base + ['authentication', 'password', f'vyos-{group}'])
+ self.cli_set(group_base + ['authentication', 'password', f'{group}'])
# commit changes
self.cli_commit()
@@ -130,7 +129,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' {vip}', config)
# Authentication
- self.assertIn(f'auth_pass "vyos-{group}"', config)
+ self.assertIn(f'auth_pass "{group}"', config)
self.assertIn(f'auth_type PASS', config)
def test_03_sync_group(self):
diff --git a/smoketest/scripts/cli/test_interfaces_geneve.py b/smoketest/scripts/cli/test_interfaces_geneve.py
index 129ee71e5..6233ade6e 100755
--- a/smoketest/scripts/cli/test_interfaces_geneve.py
+++ b/smoketest/scripts/cli/test_interfaces_geneve.py
@@ -16,7 +16,6 @@
import unittest
-from vyos.configsession import ConfigSession
from vyos.ifconfig import Interface
from vyos.util import get_interface_config
diff --git a/smoketest/scripts/cli/test_interfaces_netns.py b/smoketest/scripts/cli/test_interfaces_netns.py
new file mode 100755
index 000000000..9975a6b09
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_netns.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import os
+import json
+import unittest
+
+from netifaces import interfaces
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSession
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Interface
+from vyos.ifconfig import Section
+from vyos.util import cmd
+
+base_path = ['netns']
+namespaces = ['mgmt', 'front', 'back', 'ams-ix']
+
+class NETNSTest(VyOSUnitTestSHIM.TestCase):
+
+ def setUp(self):
+ self._interfaces = ['dum10', 'dum12', 'dum50']
+
+ def test_create_netns(self):
+ for netns in namespaces:
+ base = base_path + ['name', netns]
+ self.cli_set(base)
+
+ # commit changes
+ self.cli_commit()
+
+ netns_list = cmd('ip netns ls')
+
+ # Verify NETNS configuration
+ for netns in namespaces:
+ self.assertTrue(netns in netns_list)
+
+
+ def test_netns_assign_interface(self):
+ netns = 'foo'
+ self.cli_set(['netns', 'name', netns])
+
+ # Set
+ for iface in self._interfaces:
+ self.cli_set(['interfaces', 'dummy', iface, 'netns', netns])
+
+ # commit changes
+ self.cli_commit()
+
+ netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show')
+
+ for iface in self._interfaces:
+ self.assertTrue(iface in netns_iface_list)
+
+ # Delete
+ for iface in self._interfaces:
+ self.cli_delete(['interfaces', 'dummy', iface, 'netns', netns])
+
+ # commit changes
+ self.cli_commit()
+
+ netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show')
+
+ for iface in self._interfaces:
+ self.assertNotIn(iface, netns_iface_list)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py
index 7ce1b9872..f8a6ae986 100755
--- a/smoketest/scripts/cli/test_interfaces_openvpn.py
+++ b/smoketest/scripts/cli/test_interfaces_openvpn.py
@@ -23,7 +23,6 @@ from netifaces import interfaces
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
from vyos.util import process_named_running
diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py
index 67edce2a0..4f1e1ee99 100755
--- a/smoketest/scripts/cli/test_interfaces_pppoe.py
+++ b/smoketest/scripts/cli/test_interfaces_pppoe.py
@@ -20,7 +20,6 @@ import unittest
from psutil import process_iter
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
config_file = '/etc/ppp/peers/{}'
diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py
index 841527d21..fc2e254d6 100755
--- a/smoketest/scripts/cli/test_interfaces_tunnel.py
+++ b/smoketest/scripts/cli/test_interfaces_tunnel.py
@@ -156,26 +156,6 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_delete(self._base_path + [interface])
self.cli_commit()
- def test_tunnel_verify_local_dhcp(self):
- # We can not use source-address and dhcp-interface at the same time
-
- interface = f'tun1020'
- local_if_addr = f'10.0.0.1/24'
-
- self.cli_set(self._base_path + [interface, 'address', local_if_addr])
- self.cli_set(self._base_path + [interface, 'encapsulation', 'gre'])
- self.cli_set(self._base_path + [interface, 'source-address', self.local_v4])
- self.cli_set(self._base_path + [interface, 'remote', remote_ip4])
- self.cli_set(self._base_path + [interface, 'dhcp-interface', 'eth0'])
-
- # source-address and dhcp-interface can not be used at the same time
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
- self.cli_delete(self._base_path + [interface, 'dhcp-interface'])
-
- # Check if commit is ok
- self.cli_commit()
-
def test_tunnel_parameters_gre(self):
interface = f'tun1030'
gre_key = '10'
diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py
index 7b420cd51..f63c850d8 100755
--- a/smoketest/scripts/cli/test_interfaces_vxlan.py
+++ b/smoketest/scripts/cli/test_interfaces_vxlan.py
@@ -16,7 +16,6 @@
import unittest
-from vyos.configsession import ConfigSession
from vyos.ifconfig import Interface
from vyos.util import get_interface_config
diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py
index 3707eaac3..aaf27a2c4 100755
--- a/smoketest/scripts/cli/test_interfaces_wireguard.py
+++ b/smoketest/scripts/cli/test_interfaces_wireguard.py
@@ -18,7 +18,6 @@ import os
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
base_path = ['interfaces', 'wireguard']
diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py
index 0706f234e..75c628244 100755
--- a/smoketest/scripts/cli/test_nat.py
+++ b/smoketest/scripts/cli/test_nat.py
@@ -20,7 +20,6 @@ import json
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
from vyos.util import dict_search
diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py
index 7721105e0..8afe0da26 100755
--- a/smoketest/scripts/cli/test_nat66.py
+++ b/smoketest/scripts/cli/test_nat66.py
@@ -21,7 +21,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
from vyos.util import dict_search
diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py
index deaf23b05..45a4bd61e 100755
--- a/smoketest/scripts/cli/test_pki.py
+++ b/smoketest/scripts/cli/test_pki.py
@@ -17,7 +17,6 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
base_path = ['pki']
diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py
index c2288a86a..5844e1ec1 100755
--- a/smoketest/scripts/cli/test_policy.py
+++ b/smoketest/scripts/cli/test_policy.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
@@ -308,7 +307,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
continue
for rule, rule_config in as_path_config['rule'].items():
- tmp = f'bgp as-path access-list {as_path}'
+ tmp = f'bgp as-path access-list {as_path} seq {rule}'
if rule_config['action'] == 'permit':
tmp += ' permit'
else:
diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py
index a57f8d5f2..d234750b4 100755
--- a/smoketest/scripts/cli/test_protocols_bfd.py
+++ b/smoketest/scripts/cli/test_protocols_bfd.py
@@ -17,7 +17,6 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import process_named_running
@@ -25,6 +24,7 @@ PROCESS_NAME = 'bfdd'
base_path = ['protocols', 'bfd']
dum_if = 'dum1001'
+vrf_name = 'red'
peers = {
'192.0.2.10' : {
'intv_rx' : '500',
@@ -38,12 +38,14 @@ peers = {
'intv_mult' : '100',
'intv_rx' : '222',
'intv_tx' : '333',
+ 'passive' : '',
'shutdown' : '',
+ 'profile' : 'foo',
'source_intf': dum_if,
},
'2001:db8::a' : {
'source_addr': '2001:db8::1',
- 'source_intf': dum_if,
+ 'vrf' : vrf_name,
},
'2001:db8::b' : {
'source_addr': '2001:db8::1',
@@ -63,6 +65,7 @@ profiles = {
'bar' : {
'intv_mult' : '102',
'intv_rx' : '444',
+ 'passive' : '',
},
}
@@ -74,6 +77,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
self.assertTrue(process_named_running(PROCESS_NAME))
def test_bfd_peer(self):
+ self.cli_set(['vrf', 'name', vrf_name, 'table', '1000'])
+
for peer, peer_config in peers.items():
if 'echo_mode' in peer_config:
self.cli_set(base_path + ['peer', peer, 'echo-mode'])
@@ -87,12 +92,16 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['peer', peer, 'interval', 'transmit', peer_config["intv_tx"]])
if 'multihop' in peer_config:
self.cli_set(base_path + ['peer', peer, 'multihop'])
+ if 'passive' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'passive'])
if 'shutdown' in peer_config:
self.cli_set(base_path + ['peer', peer, 'shutdown'])
if 'source_addr' in peer_config:
self.cli_set(base_path + ['peer', peer, 'source', 'address', peer_config["source_addr"]])
if 'source_intf' in peer_config:
self.cli_set(base_path + ['peer', peer, 'source', 'interface', peer_config["source_intf"]])
+ if 'vrf' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'vrf', peer_config["vrf"]])
# commit changes
self.cli_commit()
@@ -107,6 +116,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
tmp += f' local-address {peer_config["source_addr"]}'
if 'source_intf' in peer_config:
tmp += f' interface {peer_config["source_intf"]}'
+ if 'vrf' in peer_config:
+ tmp += f' vrf {peer_config["vrf"]}'
self.assertIn(tmp, frrconfig)
peerconfig = self.getFRRconfig(f' peer {peer}', end='')
@@ -114,18 +125,23 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
if 'echo_mode' in peer_config:
self.assertIn(f'echo-mode', peerconfig)
if 'intv_echo' in peer_config:
- self.assertIn(f'echo-interval {peer_config["intv_echo"]}', peerconfig)
+ self.assertIn(f'echo receive-interval {peer_config["intv_echo"]}', peerconfig)
+ self.assertIn(f'echo transmit-interval {peer_config["intv_echo"]}', peerconfig)
if 'intv_mult' in peer_config:
self.assertIn(f'detect-multiplier {peer_config["intv_mult"]}', peerconfig)
if 'intv_rx' in peer_config:
self.assertIn(f'receive-interval {peer_config["intv_rx"]}', peerconfig)
if 'intv_tx' in peer_config:
self.assertIn(f'transmit-interval {peer_config["intv_tx"]}', peerconfig)
+ if 'passive' in peer_config:
+ self.assertIn(f'passive-mode', peerconfig)
if 'shutdown' in peer_config:
self.assertIn(f'shutdown', peerconfig)
else:
self.assertNotIn(f'shutdown', peerconfig)
+ self.cli_delete(['vrf', 'name', vrf_name])
+
def test_bfd_profile(self):
peer = '192.0.2.10'
@@ -140,10 +156,21 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['profile', profile, 'interval', 'receive', profile_config["intv_rx"]])
if 'intv_tx' in profile_config:
self.cli_set(base_path + ['profile', profile, 'interval', 'transmit', profile_config["intv_tx"]])
+ if 'passive' in profile_config:
+ self.cli_set(base_path + ['profile', profile, 'passive'])
if 'shutdown' in profile_config:
self.cli_set(base_path + ['profile', profile, 'shutdown'])
- self.cli_set(base_path + ['peer', peer, 'profile', list(profiles)[0]])
+ for peer, peer_config in peers.items():
+ if 'profile' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'profile', peer_config["profile"] + 'wrong'])
+
+ # BFD profile does not exist!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for peer, peer_config in peers.items():
+ if 'profile' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'profile', peer_config["profile"]])
# commit changes
self.cli_commit()
@@ -152,19 +179,27 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
for profile, profile_config in profiles.items():
config = self.getFRRconfig(f' profile {profile}', endsection='^ !')
if 'echo_mode' in profile_config:
- self.assertIn(f'echo-mode', config)
+ self.assertIn(f' echo-mode', config)
if 'intv_echo' in profile_config:
- self.assertIn(f'echo-interval {profile_config["intv_echo"]}', config)
+ self.assertIn(f' echo receive-interval {profile_config["intv_echo"]}', config)
+ self.assertIn(f' echo transmit-interval {profile_config["intv_echo"]}', config)
if 'intv_mult' in profile_config:
- self.assertIn(f'detect-multiplier {profile_config["intv_mult"]}', config)
+ self.assertIn(f' detect-multiplier {profile_config["intv_mult"]}', config)
if 'intv_rx' in profile_config:
- self.assertIn(f'receive-interval {profile_config["intv_rx"]}', config)
+ self.assertIn(f' receive-interval {profile_config["intv_rx"]}', config)
if 'intv_tx' in profile_config:
- self.assertIn(f'transmit-interval {profile_config["intv_tx"]}', config)
+ self.assertIn(f' transmit-interval {profile_config["intv_tx"]}', config)
+ if 'passive' in profile_config:
+ self.assertIn(f' passive-mode', config)
if 'shutdown' in profile_config:
- self.assertIn(f'shutdown', config)
+ self.assertIn(f' shutdown', config)
else:
self.assertNotIn(f'shutdown', config)
+ for peer, peer_config in peers.items():
+ peerconfig = self.getFRRconfig(f' peer {peer}', end='')
+ if 'profile' in peer_config:
+ self.assertIn(f' profile {peer_config["profile"]}', peerconfig)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py
index 29b5aa9d1..16284ed01 100755
--- a/smoketest/scripts/cli/test_protocols_bgp.py
+++ b/smoketest/scripts/cli/test_protocols_bgp.py
@@ -221,8 +221,6 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
# Default local preference (higher = more preferred, default value is 100)
self.cli_set(base_path + ['parameters', 'default', 'local-pref', local_pref])
- # Deactivate IPv4 unicast for a peer by default
- self.cli_set(base_path + ['parameters', 'default', 'no-ipv4-unicast'])
self.cli_set(base_path + ['parameters', 'graceful-restart', 'stalepath-time', stalepath_time])
self.cli_set(base_path + ['parameters', 'graceful-shutdown'])
self.cli_set(base_path + ['parameters', 'ebgp-requires-policy'])
@@ -246,7 +244,6 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' bgp router-id {router_id}', frrconfig)
self.assertIn(f' bgp log-neighbor-changes', frrconfig)
self.assertIn(f' bgp default local-preference {local_pref}', frrconfig)
- self.assertIn(f' no bgp default ipv4-unicast', frrconfig)
self.assertIn(f' bgp graceful-restart stalepath-time {stalepath_time}', frrconfig)
self.assertIn(f' bgp graceful-shutdown', frrconfig)
self.assertIn(f' bgp bestpath as-path multipath-relax', frrconfig)
diff --git a/smoketest/scripts/cli/test_protocols_igmp-proxy.py b/smoketest/scripts/cli/test_protocols_igmp-proxy.py
index 1eaf21722..079b5bee5 100755
--- a/smoketest/scripts/cli/test_protocols_igmp-proxy.py
+++ b/smoketest/scripts/cli/test_protocols_igmp-proxy.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import read_file
from vyos.util import process_named_running
diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py
index 8170f2b56..e42040025 100755
--- a/smoketest/scripts/cli/test_protocols_isis.py
+++ b/smoketest/scripts/cli/test_protocols_isis.py
@@ -17,7 +17,6 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Section
from vyos.util import process_named_running
@@ -200,8 +199,6 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
def test_isis_06_spf_delay(self):
- self.isis_base_config()
-
network = 'point-to-point'
holddown = '10'
init_delay = '50'
@@ -209,6 +206,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
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])
@@ -227,11 +225,6 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
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):
diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py
new file mode 100755
index 000000000..13d38d01b
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_mpls.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is 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 base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.util import process_named_running
+
+PROCESS_NAME = 'ldpd'
+base_path = ['protocols', 'mpls', 'ldp']
+
+peers = {
+ '192.0.2.10' : {
+ 'intv_rx' : '500',
+ 'intv_tx' : '600',
+ 'multihop' : '',
+ 'source_addr': '192.0.2.254',
+ },
+ '192.0.2.20' : {
+ 'echo_mode' : '',
+ 'intv_echo' : '100',
+ 'intv_mult' : '100',
+ 'intv_rx' : '222',
+ 'intv_tx' : '333',
+ 'passive' : '',
+ 'shutdown' : '',
+ },
+ '2001:db8::a' : {
+ 'source_addr': '2001:db8::1',
+ },
+ '2001:db8::b' : {
+ 'source_addr': '2001:db8::1',
+ 'multihop' : '',
+ },
+}
+
+profiles = {
+ 'foo' : {
+ 'echo_mode' : '',
+ 'intv_echo' : '100',
+ 'intv_mult' : '101',
+ 'intv_rx' : '222',
+ 'intv_tx' : '333',
+ 'shutdown' : '',
+ },
+ 'bar' : {
+ 'intv_mult' : '102',
+ 'intv_rx' : '444',
+ 'passive' : '',
+ },
+}
+
+class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(cls, 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()
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_mpls_basic(self):
+ self.debug = True
+ router_id = '1.2.3.4'
+ transport_ipv4_addr = '5.6.7.8'
+ interfaces = Section.interfaces('ethernet')
+
+ self.cli_set(base_path + ['router-id', router_id])
+
+ # At least one LDP interface must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface])
+
+ # LDP transport address missing
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['discovery', 'transport-ipv4-address', transport_ipv4_addr])
+
+ # Commit changes
+ self.cli_commit()
+
+ # Validate configuration
+ frrconfig = self.getFRRconfig('mpls ldp', daemon=PROCESS_NAME)
+ self.assertIn(f'mpls ldp', frrconfig)
+ self.assertIn(f' router-id {router_id}', frrconfig)
+
+ # Validate AFI IPv4
+ afiv4_config = self.getFRRconfig(' address-family ipv4', daemon=PROCESS_NAME)
+ self.assertIn(f' discovery transport-address {transport_ipv4_addr}', afiv4_config)
+ for interface in interfaces:
+ self.assertIn(f' interface {interface}', afiv4_config)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, failfast=True)
diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py
index 0529eefbd..04853c5fe 100755
--- a/smoketest/scripts/cli/test_protocols_ospf.py
+++ b/smoketest/scripts/cli/test_protocols_ospf.py
@@ -189,31 +189,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
for neighbor in neighbors:
self.assertIn(f' neighbor {neighbor} priority {priority} poll-interval {poll_interval}', frrconfig) # default
-
- def test_ospf_07_passive_interface(self):
- self.cli_set(base_path + ['passive-interface', 'default'])
- interfaces = Section.interfaces('ethernet')
- for interface in interfaces:
- self.cli_set(base_path + ['passive-interface-exclude', interface])
-
- # commit changes
- self.cli_commit()
-
- # Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf')
- try:
- self.assertIn(f'router ospf', frrconfig)
- self.assertIn(f' passive-interface default', frrconfig) # default
- for interface in interfaces:
- self.assertIn(f' no passive-interface {interface}', frrconfig) # default
- except:
- log.debug(frrconfig)
- log.debug(cmd('sudo dmesg'))
- log.debug(cmd('sudo cat /var/log/messages'))
- log.debug(cmd('vtysh -c "show run"'))
- self.fail('Now we can hopefully see why OSPF fails!')
-
- def test_ospf_08_redistribute(self):
+ def test_ospf_07_redistribute(self):
metric = '15'
metric_type = '1'
redistribute = ['bgp', 'connected', 'isis', 'kernel', 'rip', 'static']
@@ -238,7 +214,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
log.debug(cmd('vtysh -c "show run"'))
self.fail('Now we can hopefully see why OSPF fails!')
- def test_ospf_09_virtual_link(self):
+ def test_ospf_08_virtual_link(self):
networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
area = '10'
shortcut = 'enable'
@@ -268,7 +244,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' network {network} area {area}', frrconfig)
- def test_ospf_10_interface_configuration(self):
+ def test_ospf_09_interface_configuration(self):
interfaces = Section.interfaces('ethernet')
password = 'vyos1234'
bandwidth = '10000'
@@ -276,14 +252,17 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
network = 'point-to-point'
priority = '200'
+ self.cli_set(base_path + ['passive-interface', 'default'])
for interface in interfaces:
- self.cli_set(base_path + ['interface', interface, 'authentication', 'plaintext-password', password])
- self.cli_set(base_path + ['interface', interface, 'bandwidth', bandwidth])
- self.cli_set(base_path + ['interface', interface, 'bfd'])
- self.cli_set(base_path + ['interface', interface, 'cost', cost])
- self.cli_set(base_path + ['interface', interface, 'mtu-ignore'])
- self.cli_set(base_path + ['interface', interface, 'network', network])
- self.cli_set(base_path + ['interface', interface, 'priority', priority])
+ base_interface = base_path + ['interface', interface]
+ self.cli_set(base_interface + ['authentication', 'plaintext-password', password])
+ self.cli_set(base_interface + ['bandwidth', bandwidth])
+ self.cli_set(base_interface + ['bfd'])
+ self.cli_set(base_interface + ['cost', cost])
+ self.cli_set(base_interface + ['mtu-ignore'])
+ self.cli_set(base_interface + ['network', network])
+ self.cli_set(base_interface + ['priority', priority])
+ self.cli_set(base_interface + ['passive', 'disable'])
# commit changes
self.cli_commit()
@@ -297,41 +276,10 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' ip ospf mtu-ignore', config)
self.assertIn(f' ip ospf network {network}', config)
self.assertIn(f' ip ospf priority {priority}', config)
+ self.assertIn(f' no ip ospf passive', config)
self.assertIn(f' bandwidth {bandwidth}', config)
-
- def test_ospf_11_vrfs(self):
- # It is safe to assume that when the basic VRF test works, all
- # other OSPF related features work, as we entirely inherit the CLI
- # templates and Jinja2 FRR template.
- table = '1000'
- vrf = 'blue'
- vrf_base = ['vrf', 'name', vrf]
- vrf_iface = 'eth1'
- self.cli_set(vrf_base + ['table', table])
- self.cli_set(vrf_base + ['protocols', 'ospf', 'interface', vrf_iface])
- self.cli_set(['interfaces', 'ethernet', vrf_iface, 'vrf', vrf])
-
- # Also set a default VRF OSPF config
- self.cli_set(base_path)
- self.cli_commit()
-
- # Verify FRR ospfd configuration
- frrconfig = self.getFRRconfig('router ospf')
- self.assertIn(f'router ospf', frrconfig)
- self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig)
- self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
-
- frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}')
- self.assertIn(f'router ospf vrf {vrf}', frrconfig)
- self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig)
- self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
-
- self.cli_delete(['vrf', 'name', vrf])
- self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf'])
-
-
- def test_ospf_12_zebra_route_map(self):
+ def test_ospf_10_zebra_route_map(self):
# Implemented because of T3328
self.cli_set(base_path + ['route-map', route_map])
# commit changes
@@ -351,7 +299,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
frrconfig = self.getFRRconfig(zebra_route_map)
self.assertNotIn(zebra_route_map, frrconfig)
- def test_ospf_13_interface_area(self):
+ def test_ospf_11_interface_area(self):
area = '0'
interfaces = Section.interfaces('ethernet')
@@ -375,6 +323,37 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'interface {interface}', config)
self.assertIn(f' ip ospf area {area}', config)
+ def test_ospf_12_vrfs(self):
+ # It is safe to assume that when the basic VRF test works, all
+ # other OSPF related features work, as we entirely inherit the CLI
+ # templates and Jinja2 FRR template.
+ table = '1000'
+ vrf = 'blue'
+ vrf_base = ['vrf', 'name', vrf]
+ vrf_iface = 'eth1'
+ self.cli_set(vrf_base + ['table', table])
+ self.cli_set(vrf_base + ['protocols', 'ospf', 'interface', vrf_iface])
+ self.cli_set(['interfaces', 'ethernet', vrf_iface, 'vrf', vrf])
+
+ # Also set a default VRF OSPF config
+ self.cli_set(base_path)
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf')
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig)
+ self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
+
+ frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}')
+ self.assertIn(f'router ospf vrf {vrf}', frrconfig)
+ self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig)
+ self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
+
+ # cleanup
+ self.cli_delete(['vrf', 'name', vrf])
+ self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf'])
+
if __name__ == '__main__':
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_ospfv3.py b/smoketest/scripts/cli/test_protocols_ospfv3.py
index 0b4b01993..f0557f640 100755
--- a/smoketest/scripts/cli/test_protocols_ospfv3.py
+++ b/smoketest/scripts/cli/test_protocols_ospfv3.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.ifconfig import Section
from vyos.util import process_named_running
@@ -50,7 +49,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
interfaces = Section.interfaces('ethernet')
for interface in interfaces:
- self.cli_set(base_path + ['area', default_area, 'interface', interface])
+ self.cli_set(base_path + ['interface', interface, 'area', default_area])
# commit changes
self.cli_commit()
@@ -64,7 +63,8 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' area {default_area} export-list {acl_name}', frrconfig)
for interface in interfaces:
- self.assertIn(f' interface {interface} area {default_area}', frrconfig)
+ if_config = self.getFRRconfig(f'interface {interface}')
+ self.assertIn(f'ipv6 ospf6 area {default_area}', if_config)
self.cli_delete(['policy', 'access-list6', acl_name])
@@ -167,5 +167,44 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' area {area_stub} stub', frrconfig)
self.assertIn(f' area {area_stub_nosum} stub no-summary', frrconfig)
+
+ def test_ospfv3_06_vrfs(self):
+ # It is safe to assume that when the basic VRF test works, all
+ # other OSPF related features work, as we entirely inherit the CLI
+ # templates and Jinja2 FRR template.
+ table = '1000'
+ vrf = 'blue'
+ vrf_base = ['vrf', 'name', vrf]
+ vrf_iface = 'eth1'
+ router_id = '1.2.3.4'
+ router_id_vrf = '1.2.3.5'
+
+ self.cli_set(vrf_base + ['table', table])
+ self.cli_set(vrf_base + ['protocols', 'ospfv3', 'interface', vrf_iface, 'bfd'])
+ self.cli_set(vrf_base + ['protocols', 'ospfv3', 'parameters', 'router-id', router_id_vrf])
+
+ self.cli_set(['interfaces', 'ethernet', vrf_iface, 'vrf', vrf])
+
+ # Also set a default VRF OSPF config
+ self.cli_set(base_path + ['parameters', 'router-id', router_id])
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6')
+ self.assertIn(f'router ospf6', frrconfig)
+ self.assertIn(f' ospf6 router-id {router_id}', frrconfig)
+
+ frrconfig = self.getFRRconfig(f'interface {vrf_iface} vrf {vrf}')
+ self.assertIn(f'interface {vrf_iface} vrf {vrf}', frrconfig)
+ self.assertIn(f' ipv6 ospf6 bfd', frrconfig)
+
+ frrconfig = self.getFRRconfig(f'router ospf6 vrf {vrf}')
+ self.assertIn(f'router ospf6 vrf {vrf}', frrconfig)
+ self.assertIn(f' ospf6 router-id {router_id_vrf}', frrconfig)
+
+ # cleanup
+ self.cli_delete(['vrf', 'name', vrf])
+ self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf'])
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py
index 423cd811a..80d4e79f9 100755
--- a/smoketest/scripts/cli/test_protocols_rip.py
+++ b/smoketest/scripts/cli/test_protocols_rip.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.ifconfig import Section
from vyos.util import process_named_running
diff --git a/smoketest/scripts/cli/test_protocols_ripng.py b/smoketest/scripts/cli/test_protocols_ripng.py
index add92b73d..53336a533 100755
--- a/smoketest/scripts/cli/test_protocols_ripng.py
+++ b/smoketest/scripts/cli/test_protocols_ripng.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.ifconfig import Section
from vyos.util import process_named_running
@@ -55,7 +54,7 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase):
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
- def test_ripng(self):
+ def test_ripng_01_parameters(self):
metric = '8'
interfaces = Section.interfaces('ethernet')
aggregates = ['2001:db8:1000::/48', '2001:db8:2000::/48', '2001:db8:3000::/48']
@@ -122,5 +121,25 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase):
proto = 'ospf6'
self.assertIn(f' redistribute {proto} metric {metric} route-map {route_map}', frrconfig)
+ def test_ripng_02_zebra_route_map(self):
+ # Implemented because of T3328
+ self.cli_set(base_path + ['route-map', route_map])
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR configuration
+ zebra_route_map = f'ipv6 protocol ripng route-map {route_map}'
+ frrconfig = self.getFRRconfig(zebra_route_map)
+ self.assertIn(zebra_route_map, frrconfig)
+
+ # 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)
+ self.assertNotIn(zebra_route_map, frrconfig)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py
index 6d334a9f8..e5e45565b 100755
--- a/smoketest/scripts/cli/test_protocols_rpki.py
+++ b/smoketest/scripts/cli/test_protocols_rpki.py
@@ -19,7 +19,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
from vyos.util import process_named_running
@@ -37,8 +36,6 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Nothing RPKI specific should be left over in the config
- #
- # Disabled until T3266 is resolved
# frrconfig = self.getFRRconfig('rpki')
# self.assertNotIn('rpki', frrconfig)
diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py
index 0d3228cc7..4c4eb5a7c 100755
--- a/smoketest/scripts/cli/test_protocols_static.py
+++ b/smoketest/scripts/cli/test_protocols_static.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.template import is_ipv6
from vyos.util import get_interface_config
diff --git a/smoketest/scripts/cli/test_service_bcast-relay.py b/smoketest/scripts/cli/test_service_bcast-relay.py
index 58b730ab4..87901869e 100755
--- a/smoketest/scripts/cli/test_service_bcast-relay.py
+++ b/smoketest/scripts/cli/test_service_bcast-relay.py
@@ -19,7 +19,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from psutil import process_iter
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
base_path = ['service', 'broadcast-relay']
diff --git a/smoketest/scripts/cli/test_service_dhcp-relay.py b/smoketest/scripts/cli/test_service_dhcp-relay.py
index db2edba54..bbfd9e032 100755
--- a/smoketest/scripts/cli/test_service_dhcp-relay.py
+++ b/smoketest/scripts/cli/test_service_dhcp-relay.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Section
from vyos.util import process_named_running
diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
index 301f8fa31..14666db15 100755
--- a/smoketest/scripts/cli/test_service_dhcp-server.py
+++ b/smoketest/scripts/cli/test_service_dhcp-server.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import process_named_running
from vyos.util import read_file
diff --git a/smoketest/scripts/cli/test_service_dhcpv6-relay.py b/smoketest/scripts/cli/test_service_dhcpv6-relay.py
index 5a9dd1aa6..fc206435b 100755
--- a/smoketest/scripts/cli/test_service_dhcpv6-relay.py
+++ b/smoketest/scripts/cli/test_service_dhcpv6-relay.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Section
from vyos.template import address_from_cidr
diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py
index 3f9564e59..7177f1505 100755
--- a/smoketest/scripts/cli/test_service_dhcpv6-server.py
+++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.template import inc_ip
from vyos.util import process_named_running
diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py
index d8a87ffd4..90d10d40b 100755
--- a/smoketest/scripts/cli/test_service_dns_dynamic.py
+++ b/smoketest/scripts/cli/test_service_dns_dynamic.py
@@ -20,14 +20,17 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
from vyos.util import process_named_running
+from vyos.util import read_file
PROCESS_NAME = 'ddclient'
DDCLIENT_CONF = '/run/ddclient/ddclient.conf'
+
base_path = ['service', 'dns', 'dynamic']
+hostname = 'test.ddns.vyos.io'
+interface = 'eth0'
def get_config_value(key):
tmp = cmd(f'sudo cat {DDCLIENT_CONF}')
@@ -36,14 +39,13 @@ def get_config_value(key):
return tmp
class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
-
def tearDown(self):
# Delete DDNS configuration
self.cli_delete(base_path)
self.cli_commit()
def test_dyndns_service(self):
- ddns = ['interface', 'eth0', 'service']
+ ddns = ['interface', interface, 'service']
services = ['cloudflare', 'afraid', 'dyndns', 'zoneedit']
for service in services:
@@ -51,7 +53,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
password = 'vyos_pass'
zone = 'vyos.io'
self.cli_delete(base_path)
- self.cli_set(base_path + ddns + [service, 'host-name', 'test.ddns.vyos.io'])
+ self.cli_set(base_path + ddns + [service, 'host-name', hostname])
self.cli_set(base_path + ddns + [service, 'login', user])
self.cli_set(base_path + ddns + [service, 'password', password])
self.cli_set(base_path + ddns + [service, 'zone', zone])
@@ -94,7 +96,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
def test_dyndns_rfc2136(self):
# Check if DDNS service can be configured and runs
- ddns = ['interface', 'eth0', 'rfc2136', 'vyos']
+ ddns = ['interface', interface, 'rfc2136', 'vyos']
ddns_key_file = '/config/auth/my.key'
self.cli_set(base_path + ddns + ['key', ddns_key_file])
@@ -122,5 +124,38 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
+ def test_dyndns_ipv6(self):
+ ddns = ['interface', interface, 'service', 'dynv6']
+ proto = 'dyndns2'
+ user = 'none'
+ password = 'paSS_4ord'
+ srv = 'ddns.vyos.io'
+
+ self.cli_set(base_path + ['interface', interface, 'ipv6-enable'])
+ self.cli_set(base_path + ddns + ['host-name', hostname])
+ self.cli_set(base_path + ddns + ['login', user])
+ self.cli_set(base_path + ddns + ['password', password])
+ self.cli_set(base_path + ddns + ['protocol', proto])
+ self.cli_set(base_path + ddns + ['server', srv])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ protocol = get_config_value('protocol')
+ login = get_config_value('login')
+ pwd = get_config_value('password')
+ server = get_config_value('server')
+ usev6 = get_config_value('usev6')
+
+ # Check some generating config parameters
+ self.assertEqual(protocol, proto)
+ self.assertEqual(login, user)
+ self.assertEqual(pwd, f"'{password}'")
+ self.assertEqual(server, srv)
+ self.assertEqual(usev6, f"if, if={interface}")
+
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 45ca618cb..5929f8cba 100755
--- a/smoketest/scripts/cli/test_service_dns_forwarding.py
+++ b/smoketest/scripts/cli/test_service_dns_forwarding.py
@@ -19,7 +19,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import read_file
from vyos.util import process_named_running
diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py
index 3af63636a..8e69efd9c 100755
--- a/smoketest/scripts/cli/test_service_https.py
+++ b/smoketest/scripts/cli/test_service_https.py
@@ -17,7 +17,6 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.util import run
base_path = ['service', 'https']
diff --git a/smoketest/scripts/cli/test_service_mdns-repeater.py b/smoketest/scripts/cli/test_service_mdns-repeater.py
index b1092c3e5..f99a98da1 100755
--- a/smoketest/scripts/cli/test_service_mdns-repeater.py
+++ b/smoketest/scripts/cli/test_service_mdns-repeater.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.util import process_named_running
base_path = ['service', 'mdns', 'repeater']
@@ -42,7 +41,7 @@ class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Check for running process
- self.assertTrue(process_named_running('mdns-repeater'))
+ self.assertTrue(process_named_running('avahi-daemon'))
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py
index 26b4626c2..4875fb5d1 100755
--- a/smoketest/scripts/cli/test_service_router-advert.py
+++ b/smoketest/scripts/cli/test_service_router-advert.py
@@ -19,7 +19,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.util import read_file
from vyos.util import process_named_running
diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py
index 008271102..058835c72 100755
--- a/smoketest/scripts/cli/test_service_snmp.py
+++ b/smoketest/scripts/cli/test_service_snmp.py
@@ -19,9 +19,9 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.template import is_ipv4
+from vyos.template import address_from_cidr
from vyos.util import read_file
from vyos.util import process_named_running
@@ -36,16 +36,29 @@ def get_config_value(key):
return tmp[0]
class TestSNMPService(VyOSUnitTestSHIM.TestCase):
- def setUp(self):
+ @classmethod
+ def setUpClass(cls):
+ super(cls, 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):
+ # delete testing SNMP config
self.cli_delete(base_path)
+ self.cli_commit()
def test_snmp_basic(self):
+ dummy_if = 'dum7312'
+ 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']
+ listen = ['127.0.0.1', '::1', address_from_cidr(dummy_addr)]
+ port = '5000'
for auth in ['ro', 'rw']:
community = 'VyOS' + auth
@@ -56,7 +69,7 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['community', community, 'network', network])
for addr in listen:
- self.cli_set(base_path + ['listen-address', addr])
+ 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'])
@@ -68,16 +81,18 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):
# 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 += ',udp:{}:161'.format(addr)
+ expected = f'udp:{addr}:{port}'
else:
- expected += ',udp6:[{}]:161'.format(addr)
-
- self.assertTrue(expected in config)
+ expected = f'udp6:[{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):
@@ -86,7 +101,7 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['v3', 'engineid', '000000000000000000000002'])
self.cli_set(base_path + ['v3', 'group', 'default', 'mode', 'ro'])
- # check validate() - a view must be created before this can be comitted
+ # check validate() - a view must be created before this can be committed
with self.assertRaises(ConfigSessionError):
self.cli_commit()
@@ -152,4 +167,3 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):
if __name__ == '__main__':
unittest.main(verbosity=2)
-
diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py
index ded4d8301..a54c03919 100755
--- a/smoketest/scripts/cli/test_service_ssh.py
+++ b/smoketest/scripts/cli/test_service_ssh.py
@@ -20,7 +20,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
from vyos.util import process_named_running
diff --git a/smoketest/scripts/cli/test_service_tftp-server.py b/smoketest/scripts/cli/test_service_tftp-server.py
index aed4c6beb..b57c33f26 100755
--- a/smoketest/scripts/cli/test_service_tftp-server.py
+++ b/smoketest/scripts/cli/test_service_tftp-server.py
@@ -19,8 +19,8 @@ import unittest
from psutil import process_iter
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
+from vyos.util import cmd
from vyos.util import read_file
from vyos.util import process_named_running
from vyos.template import is_ipv6
@@ -30,6 +30,7 @@ base_path = ['service', 'tftp-server']
dummy_if_path = ['interfaces', 'dummy', 'dum69']
address_ipv4 = '192.0.2.1'
address_ipv6 = '2001:db8::1'
+vrf = 'mgmt'
class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase):
def setUp(self):
@@ -98,5 +99,42 @@ class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase):
count += 1
self.assertEqual(count, len(address))
+ def test_03_tftpd_vrf(self):
+ directory = '/tmp'
+ port = '69' # default port
+
+ self.cli_set(base_path + ['allow-upload'])
+ self.cli_set(base_path + ['directory', directory])
+ self.cli_set(base_path + ['listen-address', address_ipv4, 'vrf', vrf])
+
+ # VRF does yet not exist - an error must be thrown
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(['vrf', 'name', vrf, 'table', '1338'])
+ self.cli_set(dummy_if_path + ['vrf', vrf])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file('/etc/default/tftpd0')
+ # verify listen IP address
+ self.assertIn(f'{address_ipv4}:{port} -4', config)
+ # verify directory
+ self.assertIn(directory, config)
+ # verify upload
+ self.assertIn('--create --umask 000', config)
+
+ # 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)
+
+ # delete VRF
+ self.cli_delete(dummy_if_path + ['vrf'])
+ self.cli_delete(['vrf', 'name', vrf])
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py
index 6780a93f9..8a1a03ce7 100755
--- a/smoketest/scripts/cli/test_service_webproxy.py
+++ b/smoketest/scripts/cli/test_service_webproxy.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
from vyos.util import process_named_running
diff --git a/smoketest/scripts/cli/test_system_acceleration_qat.py b/smoketest/scripts/cli/test_system_acceleration_qat.py
index 9584888d6..9e60bb211 100755
--- a/smoketest/scripts/cli/test_system_acceleration_qat.py
+++ b/smoketest/scripts/cli/test_system_acceleration_qat.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
base_path = ['system', 'acceleration', 'qat']
diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py
index a2380981b..b2934cf04 100755
--- a/smoketest/scripts/cli/test_system_conntrack.py
+++ b/smoketest/scripts/cli/test_system_conntrack.py
@@ -19,7 +19,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.util import cmd
from vyos.util import read_file
diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py
new file mode 100755
index 000000000..a2b5b1481
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_flow-accounting.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is 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 base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.util import cmd
+from vyos.util import process_named_running
+from vyos.util import read_file
+
+PROCESS_NAME = 'uacctd'
+base_path = ['system', 'flow-accounting']
+
+uacctd_conf = '/etc/pmacct/uacctd.conf'
+
+class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(cls, 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()
+
+ # after service removal process must no longer run
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_basic(self):
+ buffer_size = '5' # MiB
+ self.cli_set(base_path + ['buffer-size', buffer_size])
+
+ # You need to configure at least one interface for flow-accounting
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in Section.interfaces('ethernet'):
+ self.cli_set(base_path + ['interface', interface])
+
+ # commit changes
+ self.cli_commit()
+
+ # 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)
+
+ uacctd = read_file(uacctd_conf)
+ # circular queue size - buffer_size
+ tmp = int(buffer_size) *1024 *1024
+ self.assertIn(f'plugin_pipe_size: {tmp}', uacctd)
+ # transfer buffer size - recommended value from pmacct developers 1/1000 of pipe size
+ tmp = int(buffer_size) *1024 *1024
+ # do an integer division
+ tmp //= 1000
+ self.assertIn(f'plugin_buffer_size: {tmp}', uacctd)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_ip.py b/smoketest/scripts/cli/test_system_ip.py
index e98a4e234..83df9d99e 100755
--- a/smoketest/scripts/cli/test_system_ip.py
+++ b/smoketest/scripts/cli/test_system_ip.py
@@ -17,7 +17,6 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.util import read_file
base_path = ['system', 'ip']
diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py
index c9c9e833d..1325d4b39 100755
--- a/smoketest/scripts/cli/test_system_ipv6.py
+++ b/smoketest/scripts/cli/test_system_ipv6.py
@@ -17,7 +17,6 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.util import read_file
base_path = ['system', 'ipv6']
diff --git a/smoketest/scripts/cli/test_system_lcd.py b/smoketest/scripts/cli/test_system_lcd.py
index 7a39e2986..831fba979 100755
--- a/smoketest/scripts/cli/test_system_lcd.py
+++ b/smoketest/scripts/cli/test_system_lcd.py
@@ -19,7 +19,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from configparser import ConfigParser
-from vyos.configsession import ConfigSession
from vyos.util import process_named_running
config_file = '/run/LCDd/LCDd.conf'
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index 0addd630e..69a06eeac 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -24,7 +24,6 @@ from distutils.version import LooseVersion
from platform import release as kernel_version
from subprocess import Popen, PIPE
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
from vyos.util import read_file
diff --git a/smoketest/scripts/cli/test_system_nameserver.py b/smoketest/scripts/cli/test_system_nameserver.py
index 50dc466c2..58c84988e 100755
--- a/smoketest/scripts/cli/test_system_nameserver.py
+++ b/smoketest/scripts/cli/test_system_nameserver.py
@@ -19,7 +19,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.util import read_file
diff --git a/smoketest/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py
index 2b86ebd7c..e8cc64463 100755
--- a/smoketest/scripts/cli/test_system_ntp.py
+++ b/smoketest/scripts/cli/test_system_ntp.py
@@ -19,7 +19,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.template import address_from_cidr
from vyos.template import netmask_from_cidr
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 93569c4ec..c710aec6e 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -307,7 +307,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
swanctl_lines = [
f'proposals = aes128-sha1-modp1024,aes256-sha1-modp1024',
f'version = 1',
- f'life_time = {ike_lifetime}s',
+ f'rekey_time = {ike_lifetime}s',
f'rekey_time = {esp_lifetime}s',
f'esp_proposals = aes128-sha1-modp1024,aes256-sha1-modp1024,3des-md5-modp1024',
f'local_ts = dynamic[gre]',
diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py
index cad3b1182..b0e859b5c 100755
--- a/smoketest/scripts/cli/test_vpn_openconnect.py
+++ b/smoketest/scripts/cli/test_vpn_openconnect.py
@@ -18,7 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.util import process_named_running
OCSERV_CONF = '/run/ocserv/ocserv.conf'
diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py
index f36d16344..5ffa9c086 100755
--- a/smoketest/scripts/cli/test_vrf.py
+++ b/smoketest/scripts/cli/test_vrf.py
@@ -22,7 +22,6 @@ import unittest
from netifaces import interfaces
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
@@ -58,7 +57,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
for vrf in vrfs:
self.assertNotIn(vrf, interfaces())
- def test_vrf_table_id(self):
+ def test_vrf_vni_and_table_id(self):
table = '1000'
for vrf in vrfs:
base = base_path + ['name', vrf]
@@ -70,6 +69,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
self.cli_set(base + ['table', table])
+ self.cli_set(base + ['vni', table])
if vrf == 'green':
self.cli_set(base + ['disable'])
@@ -101,6 +101,11 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
# ...
regex = f'{table}\s+{vrf}\s+#\s+{description}'
self.assertTrue(re.findall(regex, iproute2_config))
+
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ self.assertIn(f' vni {table}', frrconfig)
+
+ # Increment table ID for the next run
table = str(int(table) + 1)
def test_vrf_loopback_ips(self):
@@ -178,5 +183,42 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
section = Section.section(interface)
self.cli_delete(['interfaces', section, interface, 'vrf'])
+ def test_vrf_static_route(self):
+ table = '100'
+ for vrf in vrfs:
+ next_hop = f'192.0.{table}.1'
+ prefix = f'10.0.{table}.0/24'
+ base = base_path + ['name', vrf]
+
+ self.cli_set(base + ['vni', table])
+
+ # check validate() - a table ID is mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base + ['table', table])
+ self.cli_set(base + ['protocols', 'static', 'route', prefix, 'next-hop', next_hop])
+
+ table = str(int(table) + 1)
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ table = '100'
+ for vrf in vrfs:
+ next_hop = f'192.0.{table}.1'
+ prefix = f'10.0.{table}.0/24'
+
+ self.assertTrue(vrf in interfaces())
+ vrf_if = Interface(vrf)
+
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ self.assertIn(f' vni {table}', frrconfig)
+ self.assertIn(f' ip route {prefix} {next_hop}', frrconfig)
+
+ # Increment table ID for the next run
+ table = str(int(table) + 1)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/completion/list_openvpn_users.py b/src/completion/list_openvpn_users.py
new file mode 100755
index 000000000..c472dbeab
--- /dev/null
+++ b/src/completion/list_openvpn_users.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import argparse
+
+from vyos.config import Config
+from vyos.util import dict_search
+
+def get_user_from_interface(interface):
+ config = Config()
+ base = ['interfaces', 'openvpn', interface]
+ openvpn = config.get_config_dict(base, effective=True, key_mangling=('-', '_'))
+ users = []
+
+ try:
+ for user in (dict_search('server.client', openvpn[interface]) or []):
+ users.append(user.split(',')[0])
+ except:
+ pass
+
+ return users
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-i", "--interface", type=str, help="List users per interface")
+ args = parser.parse_args()
+
+ users = []
+
+ users = get_user_from_interface(args.interface)
+
+ print(" ".join(users))
+
diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py
index 1e0197a13..2e14e0b25 100755
--- a/src/conf_mode/containers.py
+++ b/src/conf_mode/containers.py
@@ -30,8 +30,6 @@ from vyos.util import cmd
from vyos.util import run
from vyos.util import read_file
from vyos.util import write_file
-from vyos.util import is_systemd_service_active
-from vyos.util import is_systemd_service_running
from vyos.template import inc_ip
from vyos.template import is_ipv4
from vyos.template import is_ipv6
@@ -102,7 +100,7 @@ def verify(container):
# Check if the specified container network exists
network_name = list(container_config['network'])[0]
if network_name not in container['network']:
- raise ConfigError('Container network "{network_name}" does not exist!')
+ raise ConfigError(f'Container network "{network_name}" does not exist!')
if 'address' in container_config['network'][network_name]:
if 'network' not in container_config:
@@ -160,7 +158,7 @@ def verify(container):
v6_prefix = 0
# If ipv4-prefix not defined for user-defined network
if 'prefix' not in network_config:
- raise ConfigError(f'prefix for network "{net}" must be defined!')
+ raise ConfigError(f'prefix for network "{network}" must be defined!')
for prefix in network_config['prefix']:
if is_ipv4(prefix): v4_prefix += 1
@@ -237,17 +235,6 @@ def apply(container):
if os.path.exists(tmp):
os.unlink(tmp)
- service_name = 'podman.service'
- if 'network' in container or 'name' in container:
- # Start podman if it's required and not yet running
- if not is_systemd_service_active(service_name):
- _cmd(f'systemctl start {service_name}')
- # Wait for podman to be running
- while not is_systemd_service_running(service_name):
- sleep(0.250)
- else:
- _cmd(f'systemctl stop {service_name}')
-
# Add container
if 'name' in container:
for name, container_config in container['name'].items():
@@ -271,6 +258,14 @@ def apply(container):
tmp = run(f'podman image exists {image}')
if tmp != 0: print(os.system(f'podman pull {image}'))
+ # Add capability options. Should be in uppercase
+ cap_add = ''
+ if 'cap_add' in container_config:
+ for c in container_config['cap_add']:
+ c = c.upper()
+ c = c.replace('-', '_')
+ cap_add += f' --cap-add={c}'
+
# Check/set environment options "-e foo=bar"
env_opt = ''
if 'environment' in container_config:
@@ -299,7 +294,7 @@ def apply(container):
dvol = vol_config['destination']
volume += f' -v {svol}:{dvol}'
- container_base_cmd = f'podman run --detach --interactive --tty --replace ' \
+ container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \
f'--memory {memory}m --memory-swap 0 --restart {restart} ' \
f'--name {name} {port} {volume} {env_opt}'
if 'allow_host_networks' in container_config:
@@ -310,7 +305,17 @@ def apply(container):
if 'address' in container_config['network'][network]:
address = container_config['network'][network]['address']
ipparam = f'--ip {address}'
- _cmd(f'{container_base_cmd} --net {network} {ipparam} {image}')
+
+ counter = 0
+ while True:
+ if counter >= 10:
+ break
+ try:
+ _cmd(f'{container_base_cmd} --net {network} {ipparam} {image}')
+ break
+ except:
+ counter = counter +1
+ sleep(0.5)
return None
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index 28f2a4ca5..a8cef5ebf 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -151,9 +151,15 @@ def verify(dhcp):
listen_ok = False
subnets = []
failover_ok = False
+ shared_networks = len(dhcp['shared_network_name'])
+ disabled_shared_networks = 0
+
# A shared-network requires a subnet definition
for network, network_config in dhcp['shared_network_name'].items():
+ if 'disable' in network_config:
+ disabled_shared_networks += 1
+
if 'subnet' not in network_config:
raise ConfigError(f'No subnets defined for {network}. At least one\n' \
'lease subnet must be configured.')
@@ -226,7 +232,7 @@ def verify(dhcp):
# There must be one subnet connected to a listen interface.
# This only counts if the network itself is not disabled!
if 'disable' not in network_config:
- if is_subnet_connected(subnet, primary=True):
+ if is_subnet_connected(subnet, primary=False):
listen_ok = True
# Subnets must be non overlapping
@@ -243,6 +249,10 @@ def verify(dhcp):
if net.overlaps(net2):
raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
+ # Prevent 'disable' for shared-network if only one network is configured
+ if (shared_networks - disabled_shared_networks) < 1:
+ raise ConfigError(f'At least one shared network must be active!')
+
if 'failover' in dhcp:
if not failover_ok:
raise ConfigError('DHCP failover must be enabled for at least one subnet!')
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index 175300bb0..e6a2e4486 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -128,7 +128,7 @@ def verify(dhcpv6):
# Subnets must be unique
if subnet in subnets:
- raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network']))
+ raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!')
subnets.append(subnet)
# DHCPv6 requires at least one configured address range or one static mapping
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index c979feca7..a31e5ed75 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -131,9 +131,7 @@ def generate(dyndns):
if not dyndns:
return None
- render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns,
- permission=0o600)
-
+ render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns)
return None
def apply(dyndns):
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 9cae29481..daad00067 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -169,7 +169,8 @@ def get_config():
'configured': vc.exists('system flow-accounting sflow'),
'agent-address': vc.return_value('system flow-accounting sflow agent-address'),
'sampling-rate': vc.return_value('system flow-accounting sflow sampling-rate'),
- 'servers': None
+ 'servers': None,
+ 'source-address': vc.return_value('system flow-accounting sflow source-address')
},
'netflow': {
'configured': vc.exists('system flow-accounting netflow'),
@@ -306,7 +307,7 @@ def verify(config):
source_ip_presented = True
break
if not source_ip_presented:
- raise ConfigError("Your \"netflow source-ip\" does not exist in the system")
+ print("Warning: your \"netflow source-ip\" does not exist in the system")
# check if engine-id compatible with selected protocol version
if config['netflow']['engine-id']:
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/http-api.py b/src/conf_mode/http-api.py
index 7e4b117c8..4bfcbeb47 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -100,7 +100,7 @@ def apply(http_api):
call('systemctl stop vyos-http-api.service')
# Let uvicorn settle before restarting Nginx
- time.sleep(2)
+ time.sleep(1)
cmd(f'{vyos_conf_scripts_dir}/https.py', raising=ConfigError)
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index be4380462..cd5073aa2 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -23,16 +23,19 @@ import vyos.defaults
import vyos.certbot_util
from vyos.config import Config
+from vyos.configverify import verify_vrf
from vyos import ConfigError
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
+from vyos.util import write_file
from vyos import airbag
airbag.enable()
config_file = '/etc/nginx/sites-available/default'
+systemd_override = r'/etc/systemd/system/nginx.service.d/override.conf'
cert_dir = '/etc/ssl/certs'
key_dir = '/etc/ssl/private'
certbot_dir = vyos.defaults.directories['certbot']
@@ -102,6 +105,8 @@ def verify(https):
if not domains_found:
raise ConfigError("At least one 'virtual-host <id> server-name' "
"matching the 'certbot domain-name' is required.")
+
+ verify_vrf(https)
return None
def generate(https):
@@ -139,15 +144,17 @@ def generate(https):
cert_path = os.path.join(cert_dir, f'{cert_name}.pem')
key_path = os.path.join(key_dir, f'{cert_name}.pem')
- with open(cert_path, 'w') as f:
- f.write(wrap_certificate(pki_cert['certificate']))
+ server_cert = str(wrap_certificate(pki_cert['certificate']))
+ if 'ca-certificate' in cert_dict:
+ ca_cert = cert_dict['ca-certificate']
+ server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate']))
- with open(key_path, 'w') as f:
- f.write(wrap_private_key(pki_cert['private']['key']))
+ write_file(cert_path, server_cert)
+ write_file(key_path, wrap_private_key(pki_cert['private']['key']))
vyos_cert_data = {
- "crt": cert_path,
- "key": key_path
+ 'crt': cert_path,
+ 'key': key_path
}
for block in server_block_list:
@@ -205,10 +212,12 @@ def generate(https):
}
render(config_file, 'https/nginx.default.tmpl', data)
-
+ render(systemd_override, 'https/override.conf.tmpl', https)
return None
def apply(https):
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
if https is not None:
call('systemctl restart nginx.service')
else:
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index ce62a8b82..3b8fae710 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -16,6 +16,7 @@
import os
import re
+import tempfile
from cryptography.hazmat.primitives.asymmetric import ec
from glob import glob
@@ -26,6 +27,7 @@ from ipaddress import IPv6Address
from ipaddress import IPv6Network
from ipaddress import summarize_address_range
from netifaces import interfaces
+from secrets import SystemRandom
from shutil import rmtree
from vyos.config import Config
@@ -48,6 +50,7 @@ from vyos.util import chown
from vyos.util import dict_search
from vyos.util import dict_search_args
from vyos.util import makedir
+from vyos.util import read_file
from vyos.util import write_file
from vyos.validate import is_addr_assigned
@@ -60,6 +63,10 @@ group = 'openvpn'
cfg_dir = '/run/openvpn'
cfg_file = '/run/openvpn/{ifname}.conf'
+otp_path = '/config/auth/openvpn'
+otp_file = '/config/auth/openvpn/{ifname}-otp-secrets'
+secret_chars = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
+service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf'
def get_config(config=None):
"""
@@ -80,7 +87,20 @@ def get_config(config=None):
if 'deleted' not in openvpn:
openvpn['pki'] = tmp_pki
+ # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'
+ # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.
+ tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True)
+
+ # We have to cleanup the config dict, as default values could enable features
+ # which are not explicitly enabled on the CLI. Example: server mfa totp
+ # originate comes with defaults, which will enable the
+ # totp plugin, even when not set via CLI so we
+ # need to check this first and drop those keys
+ if dict_search('server.mfa.totp', tmp) == None:
+ del openvpn['server']['mfa']
+
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
+
return openvpn
def is_ec_private_key(pki, cert_name):
@@ -169,6 +189,10 @@ def verify_pki(openvpn):
def verify(openvpn):
if 'deleted' in openvpn:
+ # remove totp secrets file if totp is not configured
+ if os.path.isfile(otp_file.format(**openvpn)):
+ os.remove(otp_file.format(**openvpn))
+
verify_bridge_delete(openvpn)
return None
@@ -309,10 +333,10 @@ def verify(openvpn):
if 'is_bridge_member' not in openvpn:
raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
-
- for client in (dict_search('client', openvpn) or []):
- if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1:
- raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
+ if hasattr(dict_search('server.client', openvpn), '__iter__'):
+ for client_k, client_v in dict_search('server.client', openvpn).items():
+ if (client_v.get('ip') and len(client_v['ip']) > 1) or (client_v.get('ipv6_ip') and len(client_v['ipv6_ip']) > 1):
+ raise ConfigError(f'Server client "{client_k}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
if dict_search('server.client_ip_pool', openvpn):
if not (dict_search('server.client_ip_pool.start', openvpn) and dict_search('server.client_ip_pool.stop', openvpn)):
@@ -360,6 +384,29 @@ def verify(openvpn):
if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet:
print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.')
+ # add mfa users to the file the mfa plugin uses
+ if dict_search('server.mfa.totp', openvpn):
+ user_data = ''
+ if not os.path.isfile(otp_file.format(**openvpn)):
+ write_file(otp_file.format(**openvpn), user_data,
+ user=user, group=group, mode=0o644)
+
+ ovpn_users = read_file(otp_file.format(**openvpn))
+ for client in (dict_search('server.client', openvpn) or []):
+ exists = None
+ for ovpn_user in ovpn_users.split('\n'):
+ if re.search('^' + client + ' ', ovpn_user):
+ user_data += f'{ovpn_user}\n'
+ exists = 'true'
+
+ if not exists:
+ random = SystemRandom()
+ totp_secret = ''.join(random.choice(secret_chars) for _ in range(16))
+ user_data += f'{client} otp totp:sha1:base32:{totp_secret}::xxx *\n'
+
+ write_file(otp_file.format(**openvpn), user_data,
+ user=user, group=group, mode=0o644)
+
else:
# checks for both client and site-to-site go here
if dict_search('server.reject_unconfigured_clients', openvpn):
@@ -525,6 +572,7 @@ def generate_pki_files(openvpn):
def generate(openvpn):
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
+ plugin_dir = '/usr/lib/openvpn'
# create base config directory on demand
makedir(directory, user, group)
# enforce proper permissions on /run/openvpn
@@ -536,6 +584,11 @@ def generate(openvpn):
if os.path.isdir(ccd_dir):
rmtree(ccd_dir, ignore_errors=True)
+ # Remove systemd directories with overrides
+ service_dir = os.path.dirname(service_file.format(**openvpn))
+ if os.path.isdir(service_dir):
+ rmtree(service_dir, ignore_errors=True)
+
if 'deleted' in openvpn or 'disable' in openvpn:
return None
@@ -571,14 +624,20 @@ def generate(openvpn):
render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn,
formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
+ # Render 20-override.conf for OpenVPN service
+ render(service_file.format(**openvpn), 'openvpn/service-override.conf.tmpl', openvpn,
+ formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
+ # Reload systemd services config to apply an override
+ call(f'systemctl daemon-reload')
+
return None
def apply(openvpn):
interface = openvpn['ifname']
- call(f'systemctl stop openvpn@{interface}.service')
# Do some cleanup when OpenVPN is disabled/deleted
if 'deleted' in openvpn or 'disable' in openvpn:
+ call(f'systemctl stop openvpn@{interface}.service')
for cleanup_file in glob(f'/run/openvpn/{interface}.*'):
if os.path.isfile(cleanup_file):
os.unlink(cleanup_file)
@@ -590,7 +649,7 @@ def apply(openvpn):
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- call(f'systemctl start openvpn@{interface}.service')
+ call(f'systemctl reload-or-restart openvpn@{interface}.service')
o = VTunIf(**openvpn)
o.update(openvpn)
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index ef385d2e7..30f57ec0c 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -98,7 +98,7 @@ def verify(tunnel):
# If tunnel source address any and key not set
if tunnel['encapsulation'] in ['gre'] and \
- tunnel['source_address'] == '0.0.0.0' and \
+ dict_search('source_address', tunnel) == '0.0.0.0' and \
dict_search('parameters.ip.key', tunnel) == None:
raise ConfigError('Tunnel parameters ip key must be set!')
@@ -107,19 +107,22 @@ def verify(tunnel):
# Check pairs tunnel source-address/encapsulation/key with exists tunnels.
# Prevent the same key for 2 tunnels with same source-address/encap. T2920
for tunnel_if in Section.interfaces('tunnel'):
+ # It makes no sense to run the test for re-used GRE keys on our
+ # own interface we are currently working on
+ if tunnel['ifname'] == tunnel_if:
+ continue
tunnel_cfg = get_interface_config(tunnel_if)
- exist_encap = tunnel_cfg['linkinfo']['info_kind']
- exist_source_address = tunnel_cfg['address']
- exist_key = tunnel_cfg['linkinfo']['info_data']['ikey']
- new_source_address = tunnel['source_address']
+ # no match on encapsulation - bail out
+ if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']:
+ continue
+ new_source_address = dict_search('source_address', tunnel)
# Convert tunnel key to ip key, format "ip -j link show"
# 1 => 0.0.0.1, 999 => 0.0.3.231
- orig_new_key = int(tunnel['parameters']['ip']['key'])
- new_key = IPv4Address(orig_new_key)
+ orig_new_key = dict_search('parameters.ip.key', tunnel)
+ new_key = IPv4Address(int(orig_new_key))
new_key = str(new_key)
- if tunnel['encapsulation'] == exist_encap and \
- new_source_address == exist_source_address and \
- new_key == exist_key:
+ if dict_search('address', tunnel_cfg) == new_source_address and \
+ dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key:
raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \
f'is already used for tunnel "{tunnel_if}"!')
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index 7b3de6e8a..af35b5f03 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -82,11 +82,12 @@ def get_config(config=None):
tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
if not (dict_search('security.wpa.passphrase', tmp) or
dict_search('security.wpa.radius', tmp)):
- del wifi['security']['wpa']
+ if 'deleted' not in wifi:
+ del wifi['security']['wpa']
# defaults include RADIUS server specifics per TAG node which need to be
# added to individual RADIUS servers instead - so we can simply delete them
- if dict_search('security.wpa.radius.server.port', wifi):
+ if dict_search('security.wpa.radius.server.port', wifi) != None:
del wifi['security']['wpa']['radius']['server']['port']
if not len(wifi['security']['wpa']['radius']['server']):
del wifi['security']['wpa']['radius']
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index faa5eb628..f013e5411 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -25,7 +25,9 @@ from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_vrf
from vyos.ifconfig import WWANIf
from vyos.util import cmd
+from vyos.util import call
from vyos.util import dict_search
+from vyos.util import DEVNULL
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -88,7 +90,7 @@ def apply(wwan):
options += ',user={user},password={password}'.format(**wwan['authentication'])
command = f'{base_cmd} --simple-connect="{options}"'
- cmd(command)
+ call(command, stdout=DEVNULL)
w.update(wwan)
return None
diff --git a/src/conf_mode/netns.py b/src/conf_mode/netns.py
new file mode 100755
index 000000000..0924eb616
--- /dev/null
+++ b/src/conf_mode/netns.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+from tempfile import NamedTemporaryFile
+
+from vyos.config import Config
+from vyos.configdict import node_changed
+from vyos.ifconfig import Interface
+from vyos.util import call
+from vyos.util import dict_search
+from vyos.util import get_interface_config
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+
+def netns_interfaces(c, match):
+ """
+ get NETNS bound interfaces
+ """
+ matched = []
+ old_level = c.get_level()
+ c.set_level(['interfaces'])
+ section = c.get_config_dict([], get_first_key=True)
+ for type in section:
+ interfaces = section[type]
+ for name in interfaces:
+ interface = interfaces[name]
+ if 'netns' in interface:
+ v = interface.get('netns', '')
+ if v == match:
+ matched.append(name)
+
+ c.set_level(old_level)
+ return matched
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['netns']
+ netns = conf.get_config_dict(base, get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ # determine which NETNS has been removed
+ for name in node_changed(conf, base + ['name']):
+ if 'netns_remove' not in netns:
+ netns.update({'netns_remove' : {}})
+
+ netns['netns_remove'][name] = {}
+ # get NETNS bound interfaces
+ interfaces = netns_interfaces(conf, name)
+ if interfaces: netns['netns_remove'][name]['interface'] = interfaces
+
+ return netns
+
+def verify(netns):
+ # ensure NETNS is not assigned to any interface
+ if 'netns_remove' in netns:
+ for name, config in netns['netns_remove'].items():
+ if 'interface' in config:
+ raise ConfigError(f'Can not remove NETNS "{name}", it still has '\
+ f'member interfaces!')
+
+ if 'name' in netns:
+ for name, config in netns['name'].items():
+ print(name)
+
+ return None
+
+
+def generate(netns):
+ if not netns:
+ return None
+
+ return None
+
+
+def apply(netns):
+
+ for tmp in (dict_search('netns_remove', netns) or []):
+ if os.path.isfile(f'/run/netns/{tmp}'):
+ call(f'ip netns del {tmp}')
+
+ if 'name' in netns:
+ for name, config in netns['name'].items():
+ if not os.path.isfile(f'/run/netns/{name}'):
+ call(f'ip netns add {name}')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py
index 1a03d520b..e251396c7 100755
--- a/src/conf_mode/policy.py
+++ b/src/conf_mode/policy.py
@@ -171,9 +171,7 @@ def verify(policy):
def generate(policy):
if not policy:
- policy['new_frr_config'] = ''
return None
-
policy['new_frr_config'] = render_to_string('frr/policy.frr.tmpl', policy)
return None
@@ -190,8 +188,9 @@ def apply(policy):
frr_cfg.modify_section(r'^bgp community-list .*')
frr_cfg.modify_section(r'^bgp extcommunity-list .*')
frr_cfg.modify_section(r'^bgp large-community-list .*')
- frr_cfg.modify_section(r'^route-map .*')
- frr_cfg.add_before('^line vty', policy['new_frr_config'])
+ frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', remove_stop_mark=True)
+ if 'new_frr_config' in policy:
+ frr_cfg.add_before(frr.default_add_before, policy['new_frr_config'])
frr_cfg.commit_configuration(bgp_daemon)
# The route-map used for the FIB (zebra) is part of the zebra daemon
@@ -200,19 +199,11 @@ def apply(policy):
frr_cfg.modify_section(r'^ipv6 access-list .*')
frr_cfg.modify_section(r'^ip prefix-list .*')
frr_cfg.modify_section(r'^ipv6 prefix-list .*')
- frr_cfg.modify_section(r'^route-map .*')
- frr_cfg.add_before('^line vty', policy['new_frr_config'])
+ frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', remove_stop_mark=True)
+ if 'new_frr_config' in policy:
+ frr_cfg.add_before(frr.default_add_before, policy['new_frr_config'])
frr_cfg.commit_configuration(zebra_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if policy['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(zebra_daemon)
-
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index 539fd7b8e..8593da170 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -16,10 +16,9 @@
import os
-from sys import exit
-
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configverify import verify_vrf
from vyos.template import is_ipv6
from vyos.template import render_to_string
from vyos.validate import is_ipv6_link_local
@@ -35,8 +34,8 @@ def get_config(config=None):
else:
conf = Config()
base = ['protocols', 'bfd']
- bfd = conf.get_config_dict(base, get_first_key=True)
-
+ bfd = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
# Bail out early if configuration tree does not exist
if not conf.exists(base):
return bfd
@@ -79,28 +78,37 @@ def verify(bfd):
# multihop and echo-mode cannot be used together
if 'echo_mode' in peer_config:
- raise ConfigError('Multihop and echo-mode cannot be used together')
+ raise ConfigError('BFD multihop and echo-mode cannot be used together')
# multihop doesn't accept interface names
if 'source' in peer_config and 'interface' in peer_config['source']:
- raise ConfigError('Multihop and source interface cannot be used together')
+ raise ConfigError('BFD multihop and source interface cannot be used together')
+
+ if 'profile' in peer_config:
+ profile_name = peer_config['profile']
+ if 'profile' not in bfd or profile_name not in bfd['profile']:
+ raise ConfigError(f'BFD profile "{profile_name}" does not exist!')
+
+ if 'vrf' in peer_config:
+ verify_vrf(peer_config)
return None
def generate(bfd):
if not bfd:
- bfd['new_frr_config'] = ''
return None
-
bfd['new_frr_config'] = render_to_string('frr/bfdd.frr.tmpl', bfd)
def apply(bfd):
+ bfd_daemon = 'bfdd'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration()
- frr_cfg.modify_section('^bfd', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bfd['new_frr_config'])
- frr_cfg.commit_configuration()
+ frr_cfg.load_configuration(bfd_daemon)
+ frr_cfg.modify_section('^bfd', stop_pattern='^exit', remove_stop_mark=True)
+ if 'new_frr_config' in bfd:
+ frr_cfg.add_before(frr.default_add_before, bfd['new_frr_config'])
+ frr_cfg.commit_configuration(bfd_daemon)
return None
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 68284e0f9..03fb17ba7 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -255,21 +255,11 @@ def verify(bgp):
tmp = dict_search(f'route_map.vpn.{export_import}', afi_config)
if tmp: verify_route_map(tmp, bgp)
- if afi in ['l2vpn_evpn'] and 'vrf' not in bgp:
- # Some L2VPN EVPN AFI options are only supported under VRF
- if 'vni' in afi_config:
- for vni, vni_config in afi_config['vni'].items():
- if 'rd' in vni_config:
- raise ConfigError('VNI route-distinguisher is only supported under EVPN VRF')
- if 'route_target' in vni_config:
- raise ConfigError('VNI route-target is only supported under EVPN VRF')
return None
def generate(bgp):
if not bgp or 'deleted' in bgp:
- bgp['frr_bgpd_config'] = ''
- bgp['frr_zebra_config'] = ''
return None
bgp['protocol'] = 'bgp' # required for frr/vrf.route-map.frr.tmpl
@@ -287,8 +277,9 @@ def apply(bgp):
# The route-map used for the FIB (zebra) is part of the zebra daemon
frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'(\s+)?ip protocol bgp route-map [-a-zA-Z0-9.]+$', '', '(\s|!)')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['frr_zebra_config'])
+ frr_cfg.modify_section(r'(\s+)?ip protocol bgp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in bgp:
+ frr_cfg.add_before(frr.default_add_before, bgp['frr_zebra_config'])
frr_cfg.commit_configuration(zebra_daemon)
# Generate empty helper string which can be ammended to FRR commands, it
@@ -298,13 +289,11 @@ def apply(bgp):
vrf = ' vrf ' + bgp['vrf']
frr_cfg.load_configuration(bgp_daemon)
- frr_cfg.modify_section(f'^router bgp \d+{vrf}$', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['frr_bgpd_config'])
+ frr_cfg.modify_section(f'^router bgp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+ if 'frr_bgpd_config' in bgp:
+ frr_cfg.add_before(frr.default_add_before, bgp['frr_bgpd_config'])
frr_cfg.commit_configuration(bgp_daemon)
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index 4505e2496..9b4b215de 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -56,10 +56,10 @@ def get_config(config=None):
# instead of the VRF instance.
if vrf: isis['vrf'] = vrf
- # As we no re-use this Python handler for both VRF and non VRF instances for
- # IS-IS we need to find out if any interfaces changed so properly adjust
- # the FRR configuration and not by acctident change interfaces from a
- # different VRF.
+ # FRR has VRF support for different routing daemons. As interfaces belong
+ # to VRFs - or the global VRF, we need to check for changed interfaces so
+ # that they will be properly rendered for the FRR config. Also this eases
+ # removal of interfaces from the running configuration.
interfaces_removed = node_changed(conf, base + ['interface'])
if interfaces_removed:
isis['interface_removed'] = list(interfaces_removed)
@@ -196,8 +196,6 @@ def verify(isis):
def generate(isis):
if not isis or 'deleted' in isis:
- isis['frr_isisd_config'] = ''
- isis['frr_zebra_config'] = ''
return None
isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.tmpl
@@ -214,8 +212,9 @@ def apply(isis):
# The route-map used for the FIB (zebra) is part of the zebra daemon
frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+$', '', '(\s|!)')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_zebra_config'])
+ frr_cfg.modify_section('(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in isis:
+ frr_cfg.add_before(frr.default_add_before, isis['frr_zebra_config'])
frr_cfg.commit_configuration(zebra_daemon)
# Generate empty helper string which can be ammended to FRR commands, it
@@ -225,19 +224,18 @@ def apply(isis):
vrf = ' vrf ' + isis['vrf']
frr_cfg.load_configuration(isis_daemon)
- frr_cfg.modify_section(f'^router isis VyOS{vrf}$', '')
+ frr_cfg.modify_section(f'^router isis VyOS{vrf}', stop_pattern='^exit', remove_stop_mark=True)
for key in ['interface', 'interface_removed']:
if key not in isis:
continue
for interface in isis[key]:
- frr_cfg.modify_section(f'^interface {interface}{vrf}$', '')
+ frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True)
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_isisd_config'])
- frr_cfg.commit_configuration(isis_daemon)
+ if 'frr_isisd_config' in isis:
+ frr_cfg.add_before(frr.default_add_before, isis['frr_isisd_config'])
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
+ frr_cfg.commit_configuration(isis_daemon)
return None
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
index 3b27608da..0b0c7d07b 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -66,36 +66,24 @@ def verify(mpls):
def generate(mpls):
# If there's no MPLS config generated, create dictionary key with no value.
- if not mpls:
- mpls['new_frr_config'] = ''
+ if not mpls or 'deleted' in mpls:
return None
- mpls['new_frr_config'] = render_to_string('frr/ldpd.frr.tmpl', mpls)
+ mpls['frr_ldpd_config'] = render_to_string('frr/ldpd.frr.tmpl', mpls)
return None
def apply(mpls):
- # Define dictionary that will load FRR config
- frr_cfg = {}
+ ldpd_damon = 'ldpd'
+
# Save original configuration prior to starting any commit actions
- frr_cfg['original_config'] = frr.get_configuration(daemon='ldpd')
- frr_cfg['modified_config'] = frr.replace_section(frr_cfg['original_config'], mpls['new_frr_config'], from_re='mpls.*')
-
- # If FRR config is blank, rerun the blank commit three times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if mpls['new_frr_config'] == '':
- for x in range(3):
- frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd')
- elif not 'ldp' in mpls:
- for x in range(3):
- frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd')
- else:
- # FRR mark configuration will test for syntax errors and throws an
- # exception if any syntax errors is detected
- frr.mark_configuration(frr_cfg['modified_config'])
+ frr_cfg = frr.FRRConfig()
+
+ frr_cfg.load_configuration(ldpd_damon)
+ frr_cfg.modify_section(f'^mpls ldp', stop_pattern='^exit', remove_stop_mark=True)
- # Commit resulting configuration to FRR, this will throw CommitError
- # on failure
- frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd')
+ if 'frr_ldpd_config' in mpls:
+ frr_cfg.add_before(frr.default_add_before, mpls['frr_ldpd_config'])
+ frr_cfg.commit_configuration(ldpd_damon)
# Set number of entries in the platform label tables
labels = '0'
@@ -122,7 +110,7 @@ def apply(mpls):
system_interfaces = []
# Populate system interfaces list with local MPLS capable interfaces
for interface in glob('/proc/sys/net/mpls/conf/*'):
- system_interfaces.append(os.path.basename(interface))
+ system_interfaces.append(os.path.basename(interface))
# This is where the comparison is done on if an interface needs to be enabled/disabled.
for system_interface in system_interfaces:
interface_state = read_file(f'/proc/sys/net/mpls/conf/{system_interface}/input')
@@ -138,7 +126,7 @@ def apply(mpls):
system_interfaces = []
# If MPLS interfaces are not configured, set MPLS processing disabled
for interface in glob('/proc/sys/net/mpls/conf/*'):
- system_interfaces.append(os.path.basename(interface))
+ system_interfaces.append(os.path.basename(interface))
for system_interface in system_interfaces:
system_interface = system_interface.replace('.', '/')
call(f'sysctl -wq net.mpls.conf.{system_interface}.input=0')
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 6ccda2e5a..4895cde6f 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -56,10 +56,10 @@ def get_config(config=None):
# instead of the VRF instance.
if vrf: ospf['vrf'] = vrf
- # As we no re-use this Python handler for both VRF and non VRF instances for
- # OSPF we need to find out if any interfaces changed so properly adjust
- # the FRR configuration and not by acctident change interfaces from a
- # different VRF.
+ # FRR has VRF support for different routing daemons. As interfaces belong
+ # to VRFs - or the global VRF, we need to check for changed interfaces so
+ # that they will be properly rendered for the FRR config. Also this eases
+ # removal of interfaces from the running configuration.
interfaces_removed = node_changed(conf, base + ['interface'])
if interfaces_removed:
ospf['interface_removed'] = list(interfaces_removed)
@@ -177,11 +177,11 @@ def verify(ospf):
raise ConfigError('Can not use OSPF interface area and area ' \
'network configuration at the same time!')
- if 'vrf' in ospf:
# If interface specific options are set, we must ensure that the
# interface is bound to our requesting VRF. Due to the VyOS
# priorities the interface is bound to the VRF after creation of
# the VRF itself, and before any routing protocol is configured.
+ if 'vrf' in ospf:
vrf = ospf['vrf']
tmp = get_interface_config(interface)
if 'master' not in tmp or tmp['master'] != vrf:
@@ -191,8 +191,6 @@ def verify(ospf):
def generate(ospf):
if not ospf or 'deleted' in ospf:
- ospf['frr_ospfd_config'] = ''
- ospf['frr_zebra_config'] = ''
return None
ospf['protocol'] = 'ospf' # required for frr/vrf.route-map.frr.tmpl
@@ -209,8 +207,9 @@ def apply(ospf):
# The route-map used for the FIB (zebra) is part of the zebra daemon
frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'(\s+)?ip protocol ospf route-map [-a-zA-Z0-9.]+$', '', '(\s|!)')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['frr_zebra_config'])
+ frr_cfg.modify_section('(\s+)?ip protocol ospf route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in ospf:
+ frr_cfg.add_before(frr.default_add_before, ospf['frr_zebra_config'])
frr_cfg.commit_configuration(zebra_daemon)
# Generate empty helper string which can be ammended to FRR commands, it
@@ -220,20 +219,18 @@ def apply(ospf):
vrf = ' vrf ' + ospf['vrf']
frr_cfg.load_configuration(ospf_daemon)
- frr_cfg.modify_section(f'^router ospf{vrf}$', '')
+ frr_cfg.modify_section(f'^router ospf{vrf}', stop_pattern='^exit', remove_stop_mark=True)
for key in ['interface', 'interface_removed']:
if key not in ospf:
continue
for interface in ospf[key]:
- frr_cfg.modify_section(f'^interface {interface}{vrf}$', '')
+ frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True)
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['frr_ospfd_config'])
+ if 'frr_ospfd_config' in ospf:
+ frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config'])
frr_cfg.commit_configuration(ospf_daemon)
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
index 536ffa690..d0460b830 100755
--- a/src/conf_mode/protocols_ospfv3.py
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -17,30 +17,53 @@
import os
from sys import exit
+from sys import argv
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configdict import node_changed
from vyos.configverify import verify_common_route_maps
from vyos.template import render_to_string
from vyos.ifconfig import Interface
+from vyos.util import get_interface_config
from vyos.xml import defaults
from vyos import ConfigError
from vyos import frr
from vyos import airbag
airbag.enable()
-frr_daemon = 'ospf6d'
-
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['protocols', 'ospfv3']
+
+ vrf = None
+ if len(argv) > 1:
+ vrf = argv[1]
+
+ base_path = ['protocols', 'ospfv3']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospfv3'] or base_path
ospfv3 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ # Assign the name of our VRF context. This MUST be done before the return
+ # statement below, else on deletion we will delete the default instance
+ # instead of the VRF instance.
+ if vrf: ospfv3['vrf'] = vrf
+
+ # FRR has VRF support for different routing daemons. As interfaces belong
+ # to VRFs - or the global VRF, we need to check for changed interfaces so
+ # that they will be properly rendered for the FRR config. Also this eases
+ # removal of interfaces from the running configuration.
+ interfaces_removed = node_changed(conf, base + ['interface'])
+ if interfaces_removed:
+ ospfv3['interface_removed'] = list(interfaces_removed)
+
# Bail out early if configuration tree does not exist
if not conf.exists(base):
+ ospfv3.update({'deleted' : ''})
return ospfv3
# We also need some additional information from the config, prefix-lists
@@ -61,33 +84,56 @@ def verify(ospfv3):
verify_common_route_maps(ospfv3)
if 'interface' in ospfv3:
- for ifname, if_config in ospfv3['interface'].items():
- if 'ifmtu' in if_config:
- mtu = Interface(ifname).get_mtu()
- if int(if_config['ifmtu']) > int(mtu):
+ for interface, interface_config in ospfv3['interface'].items():
+ if 'ifmtu' in interface_config:
+ mtu = Interface(interface).get_mtu()
+ if int(interface_config['ifmtu']) > int(mtu):
raise ConfigError(f'OSPFv3 ifmtu can not exceed physical MTU of "{mtu}"')
+ # If interface specific options are set, we must ensure that the
+ # interface is bound to our requesting VRF. Due to the VyOS
+ # priorities the interface is bound to the VRF after creation of
+ # the VRF itself, and before any routing protocol is configured.
+ if 'vrf' in ospfv3:
+ vrf = ospfv3['vrf']
+ tmp = get_interface_config(interface)
+ if 'master' not in tmp or tmp['master'] != vrf:
+ raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!')
+
return None
def generate(ospfv3):
- if not ospfv3:
- ospfv3['new_frr_config'] = ''
+ if not ospfv3 or 'deleted' in ospfv3:
return None
ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.tmpl', ospfv3)
return None
def apply(ospfv3):
+ ospf6_daemon = 'ospf6d'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(frr_daemon)
- frr_cfg.modify_section(r'^interface \S+', '')
- frr_cfg.modify_section('^router ospf6$', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospfv3['new_frr_config'])
- frr_cfg.commit_configuration(frr_daemon)
-
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
+
+ # Generate empty helper string which can be ammended to FRR commands, it
+ # will be either empty (default VRF) or contain the "vrf <name" statement
+ vrf = ''
+ if 'vrf' in ospfv3:
+ vrf = ' vrf ' + ospfv3['vrf']
+
+ frr_cfg.load_configuration(ospf6_daemon)
+ frr_cfg.modify_section(f'^router ospf6{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+
+ for key in ['interface', 'interface_removed']:
+ if key not in ospfv3:
+ continue
+ for interface in ospfv3[key]:
+ frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+
+ if 'new_frr_config' in ospfv3:
+ frr_cfg.add_before(frr.default_add_before, ospfv3['new_frr_config'])
+
+ frr_cfg.commit_configuration(ospf6_daemon)
return None
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index 6b78f6f2d..300f56489 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -20,6 +20,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configdict import node_changed
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_access_list
from vyos.configverify import verify_prefix_list
@@ -39,8 +40,17 @@ def get_config(config=None):
base = ['protocols', 'rip']
rip = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ # FRR has VRF support for different routing daemons. As interfaces belong
+ # to VRFs - or the global VRF, we need to check for changed interfaces so
+ # that they will be properly rendered for the FRR config. Also this eases
+ # removal of interfaces from the running configuration.
+ interfaces_removed = node_changed(conf, base + ['interface'])
+ if interfaces_removed:
+ rip['interface_removed'] = list(interfaces_removed)
+
# Bail out early if configuration tree does not exist
if not conf.exists(base):
+ rip.update({'deleted' : ''})
return rip
# We have gathered the dict representation of the CLI, but there are default
@@ -89,12 +99,10 @@ def verify(rip):
f'with "split-horizon disable" for "{interface}"!')
def generate(rip):
- if not rip:
- rip['new_frr_config'] = ''
+ if not rip or 'deleted' in rip:
return None
rip['new_frr_config'] = render_to_string('frr/ripd.frr.tmpl', rip)
-
return None
def apply(rip):
@@ -106,19 +114,22 @@ def apply(rip):
# The route-map used for the FIB (zebra) is part of the zebra daemon
frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'^ip protocol rip route-map [-a-zA-Z0-9.]+$', '')
+ frr_cfg.modify_section('^ip protocol rip route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
frr_cfg.commit_configuration(zebra_daemon)
frr_cfg.load_configuration(rip_daemon)
- frr_cfg.modify_section(r'key chain \S+', '')
- frr_cfg.modify_section(r'interface \S+', '')
- frr_cfg.modify_section('^router rip$', '')
+ frr_cfg.modify_section('^key chain \S+', stop_pattern='^exit', remove_stop_mark=True)
+ frr_cfg.modify_section('^router rip', stop_pattern='^exit', remove_stop_mark=True)
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rip['new_frr_config'])
- frr_cfg.commit_configuration(rip_daemon)
+ for key in ['interface', 'interface_removed']:
+ if key not in rip:
+ continue
+ for interface in rip[key]:
+ frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
+ if 'new_frr_config' in rip:
+ frr_cfg.add_before(frr.default_add_before, rip['new_frr_config'])
+ frr_cfg.commit_configuration(rip_daemon)
return None
diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py
index bc4954f63..d9b8c0b30 100755
--- a/src/conf_mode/protocols_ripng.py
+++ b/src/conf_mode/protocols_ripng.py
@@ -31,8 +31,6 @@ from vyos import frr
from vyos import airbag
airbag.enable()
-frr_daemon = 'ripngd'
-
def get_config(config=None):
if config:
conf = config
@@ -99,17 +97,24 @@ def generate(ripng):
return None
def apply(ripng):
+ ripng_daemon = 'ripngd'
+ zebra_daemon = 'zebra'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(frr_daemon)
- frr_cfg.modify_section(r'key chain \S+', '')
- frr_cfg.modify_section(r'interface \S+', '')
- frr_cfg.modify_section('router ripng', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ripng['new_frr_config'])
- frr_cfg.commit_configuration(frr_daemon)
-
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
+
+ # The route-map used for the FIB (zebra) is part of the zebra daemon
+ frr_cfg.load_configuration(zebra_daemon)
+ frr_cfg.modify_section('^ipv6 protocol ripng route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ frr_cfg.commit_configuration(zebra_daemon)
+
+ frr_cfg.load_configuration(ripng_daemon)
+ frr_cfg.modify_section('key chain \S+', stop_pattern='^exit', remove_stop_mark=True)
+ frr_cfg.modify_section('interface \S+', stop_pattern='^exit', remove_stop_mark=True)
+ frr_cfg.modify_section('^router ripng', stop_pattern='^exit', remove_stop_mark=True)
+ if 'new_frr_config' in ripng:
+ frr_cfg.add_before(frr.default_add_before, ripng['new_frr_config'])
+ frr_cfg.commit_configuration(ripng_daemon)
return None
diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py
index 947c8ab7a..51ad0d315 100755
--- a/src/conf_mode/protocols_rpki.py
+++ b/src/conf_mode/protocols_rpki.py
@@ -28,8 +28,6 @@ from vyos import frr
from vyos import airbag
airbag.enable()
-frr_daemon = 'bgpd'
-
def get_config(config=None):
if config:
conf = config
@@ -38,7 +36,9 @@ def get_config(config=None):
base = ['protocols', 'rpki']
rpki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ # Bail out early if configuration tree does not exist
if not conf.exists(base):
+ rpki.update({'deleted' : ''})
return rpki
# We have gathered the dict representation of the CLI, but there are default
@@ -79,17 +79,22 @@ def verify(rpki):
return None
def generate(rpki):
+ if not rpki:
+ return
rpki['new_frr_config'] = render_to_string('frr/rpki.frr.tmpl', rpki)
return None
def apply(rpki):
+ bgp_daemon = 'bgpd'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(frr_daemon)
- frr_cfg.modify_section('rpki', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rpki['new_frr_config'])
- frr_cfg.commit_configuration(frr_daemon)
+ frr_cfg.load_configuration(bgp_daemon)
+ frr_cfg.modify_section('^rpki', stop_pattern='^exit', remove_stop_mark=True)
+ if 'new_frr_config' in rpki:
+ frr_cfg.add_before(frr.default_add_before, rpki['new_frr_config'])
+ frr_cfg.commit_configuration(bgp_daemon)
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index 597fcc443..c1e427b16 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -21,6 +21,7 @@ from sys import argv
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configdict import get_dhcp_interfaces
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_vrf
from vyos.template import render_to_string
@@ -56,6 +57,10 @@ def get_config(config=None):
# Merge policy dict into "regular" config dict
static = dict_merge(tmp, static)
+ # T3680 - get a list of all interfaces currently configured to use DHCP
+ tmp = get_dhcp_interfaces(conf, vrf)
+ if tmp: static['dhcp'] = tmp
+
return static
def verify(static):
@@ -80,6 +85,8 @@ def verify(static):
return None
def generate(static):
+ if not static:
+ return None
static['new_frr_config'] = render_to_string('frr/staticd.frr.tmpl', static)
return None
@@ -92,24 +99,21 @@ def apply(static):
# The route-map used for the FIB (zebra) is part of the zebra daemon
frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'^ip protocol static route-map [-a-zA-Z0-9.]+$', '')
+ frr_cfg.modify_section(r'^ip protocol static route-map [-a-zA-Z0-9.]+', '')
frr_cfg.commit_configuration(zebra_daemon)
-
frr_cfg.load_configuration(static_daemon)
if 'vrf' in static:
vrf = static['vrf']
- frr_cfg.modify_section(f'^vrf {vrf}$', '')
+ frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit', remove_stop_mark=True)
else:
- frr_cfg.modify_section(r'^ip route .*', '')
- frr_cfg.modify_section(r'^ipv6 route .*', '')
+ frr_cfg.modify_section(r'^ip route .*')
+ frr_cfg.modify_section(r'^ipv6 route .*')
- frr_cfg.add_before(r'(interface .*|line vty)', static['new_frr_config'])
+ if 'new_frr_config' in static:
+ frr_cfg.add_before(frr.default_add_before, static['new_frr_config'])
frr_cfg.commit_configuration(static_daemon)
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py
index c920920ed..d31a0c49e 100755
--- a/src/conf_mode/service_mdns-repeater.py
+++ b/src/conf_mode/service_mdns-repeater.py
@@ -28,7 +28,7 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-config_file = r'/etc/default/mdns-repeater'
+config_file = '/run/avahi-daemon/avahi-daemon.conf'
vrrp_running_file = '/run/mdns_vrrp_active'
def get_config(config=None):
@@ -92,12 +92,12 @@ def generate(mdns):
if len(mdns['interface']) < 2:
return None
- render(config_file, 'mdns-repeater/mdns-repeater.tmpl', mdns)
+ render(config_file, 'mdns-repeater/avahi-daemon.tmpl', mdns)
return None
def apply(mdns):
if not mdns or 'disable' in mdns:
- call('systemctl stop mdns-repeater.service')
+ call('systemctl stop avahi-daemon.service')
if os.path.exists(config_file):
os.unlink(config_file)
@@ -106,16 +106,16 @@ def apply(mdns):
else:
if 'vrrp_disable' not in mdns and os.path.exists(vrrp_running_file):
os.unlink(vrrp_running_file)
-
+
if mdns['vrrp_exists'] and 'vrrp_disable' in mdns:
if not os.path.exists(vrrp_running_file):
os.mknod(vrrp_running_file) # vrrp script looks for this file to update mdns repeater
if len(mdns['interface']) < 2:
- call('systemctl stop mdns-repeater.service')
+ call('systemctl stop avahi-daemon.service')
return None
- call('systemctl restart mdns-repeater.service')
+ call('systemctl restart avahi-daemon.service')
return None
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index 9fbd531da..1f31d132d 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -24,8 +24,11 @@ from vyos.configverify import verify_accel_ppp_base_service
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
+from vyos.util import get_interface_config
from vyos import ConfigError
from vyos import airbag
+from vyos.range_regex import range_to_regex
+
airbag.enable()
pppoe_conf = r'/run/accel-pppd/pppoe.conf'
@@ -56,6 +59,11 @@ def verify(pppoe):
if 'interface' not in pppoe:
raise ConfigError('At least one listen interface must be defined!')
+ # Check is interface exists in the system
+ for iface in pppoe['interface']:
+ if not get_interface_config(iface):
+ raise ConfigError(f'Interface {iface} does not exist!')
+
# local ippool and gateway settings config checks
if not (dict_search('client_ip_pool.subnet', pppoe) or
(dict_search('client_ip_pool.start', pppoe) and
@@ -73,6 +81,13 @@ def generate(pppoe):
if not pppoe:
return None
+ # Generate special regex for dynamic interfaces
+ for iface in pppoe['interface']:
+ if 'vlan_range' in pppoe['interface'][iface]:
+ pppoe['interface'][iface]['regex'] = []
+ for vlan_range in pppoe['interface'][iface]['vlan_range']:
+ pppoe['interface'][iface]['regex'].append(range_to_regex(vlan_range))
+
render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe)
if dict_search('authentication.mode', pppoe) == 'local':
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 23e45a5b7..e1852f2ce 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -20,13 +20,17 @@ from sys import exit
from vyos.config import Config
from vyos.configverify import verify_vrf
-from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random
+from vyos.snmpv3_hashgen import plaintext_to_md5
+from vyos.snmpv3_hashgen import plaintext_to_sha1
+from vyos.snmpv3_hashgen import random
from vyos.template import render
from vyos.template import is_ipv4
-from vyos.util import call, chmod_755
+from vyos.util import call
+from vyos.util import chmod_755
from vyos.validate import is_addr_assigned
from vyos.version import get_version_data
-from vyos import ConfigError, airbag
+from vyos import ConfigError
+from vyos import airbag
airbag.enable()
config_file_client = r'/etc/snmp/snmp.conf'
@@ -52,6 +56,7 @@ default_config_data = {
'communities': [],
'smux_peers': [],
'location' : '',
+ 'protocol' : 'udp',
'description' : '',
'contact' : '',
'route_table': 'False',
@@ -151,6 +156,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,20 +412,22 @@ 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
- listen = 'udp:' + addr + ':' + port
+ tmp = f'{protocol}:{addr}:{port}'
elif snmp['ipv6_enabled']:
# example: udp6:[::1]:161
- listen = '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
if is_addr_assigned(addr):
- snmp['listen_on'].append(listen)
+ if tmp: snmp['listen_on'].append(tmp)
else:
- print('WARNING: SNMP listen address {0} not configured!'.format(addr))
+ print(f'WARNING: SNMP listen address {addr} not configured!')
verify_vrf(snmp)
diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py
index a960a4da3..2220d7b66 100755
--- a/src/conf_mode/system-login-banner.py
+++ b/src/conf_mode/system-login-banner.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -22,12 +22,13 @@ from vyos import airbag
airbag.enable()
motd="""
-The programs included with the Debian/VyOS GNU/Linux system are free software;
-the exact distribution terms for each program are described in the
-individual files in /usr/share/doc/*/copyright.
+Check out project news at https://blog.vyos.io
+and feel free to report bugs at https://phabricator.vyos.net
-Debian/VyOS GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
-permitted by applicable law.
+You can change this banner using "set system login banner post-login" command.
+
+VyOS is a free software distribution that includes multiple components,
+you can check individual component licenses under /usr/share/doc/*/copyright
"""
@@ -36,7 +37,7 @@ PRELOGIN_NET_FILE = r'/etc/issue.net'
POSTLOGIN_FILE = r'/etc/motd'
default_config_data = {
- 'issue': 'Welcome to VyOS - \\n \\l\n',
+ 'issue': 'Welcome to VyOS - \\n \\l\n\n',
'issue_net': 'Welcome to VyOS\n',
'motd': motd
}
diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py
index 55cf6b142..b1c63e316 100755
--- a/src/conf_mode/system-option.py
+++ b/src/conf_mode/system-option.py
@@ -126,6 +126,12 @@ def apply(options):
if 'keyboard_layout' in options:
cmd('loadkeys {keyboard_layout}'.format(**options))
+ # Enable/diable root-partition-auto-resize SystemD service
+ if 'root_partition_auto_resize' in options:
+ cmd('systemctl enable root-partition-auto-resize.service')
+ else:
+ cmd('systemctl disable root-partition-auto-resize.service')
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 33a546bd3..19b252513 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -18,9 +18,14 @@ import os
import re
from vyos.config import Config
-from vyos.util import call, read_file, write_file
+from vyos.configdict import dict_merge
+from vyos.util import call
+from vyos.util import read_file
+from vyos.util import write_file
from vyos.template import render
-from vyos import ConfigError, airbag
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
airbag.enable()
by_bus_dir = '/dev/serial/by-bus'
@@ -36,21 +41,27 @@ def get_config(config=None):
console = conf.get_config_dict(base, get_first_key=True)
# bail out early if no serial console is configured
- if 'device' not in console.keys():
+ if 'device' not in console:
return console
# convert CLI values to system values
- for device in console['device'].keys():
- # no speed setting has been configured - use default value
- if not 'speed' in console['device'][device].keys():
- tmp = { 'speed': '' }
- if device.startswith('hvc'):
- tmp['speed'] = 38400
- else:
- tmp['speed'] = 115200
+ default_values = defaults(base + ['device'])
+ for device, device_config in console['device'].items():
+ if 'speed' not in device_config and device.startswith('hvc'):
+ # XEN console has a different default console speed
+ console['device'][device]['speed'] = 38400
+ else:
+ # Merge in XML defaults - the proper way to do it
+ console['device'][device] = dict_merge(default_values,
+ console['device'][device])
+
+ return console
- console['device'][device].update(tmp)
+def verify(console):
+ if not console or 'device' not in console:
+ return None
+ for device in console['device']:
if device.startswith('usb'):
# It is much easiert to work with the native ttyUSBn name when using
# getty, but that name may change across reboots - depending on the
@@ -58,13 +69,13 @@ def get_config(config=None):
# to its dynamic device file - and create a new dict entry for it.
by_bus_device = f'{by_bus_dir}/{device}'
if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device):
- tmp = os.path.basename(os.readlink(by_bus_device))
- # updating the dict must come as last step in the loop!
- console['device'][tmp] = console['device'].pop(device)
+ device = os.path.basename(os.readlink(by_bus_device))
- return console
+ # If the device name still starts with usbXXX no matching tty was found
+ # and it can not be used as a serial interface
+ if device.startswith('usb'):
+ raise ConfigError(f'Device {device} does not support beeing used as tty')
-def verify(console):
return None
def generate(console):
@@ -76,20 +87,29 @@ def generate(console):
call(f'systemctl stop {basename}')
os.unlink(os.path.join(root, basename))
- if not console:
+ if not console or 'device' not in console:
return None
- for device in console['device'].keys():
+ for device, device_config in console['device'].items():
+ if device.startswith('usb'):
+ # It is much easiert to work with the native ttyUSBn name when using
+ # getty, but that name may change across reboots - depending on the
+ # amount of connected devices. We will resolve the fixed device name
+ # to its dynamic device file - and create a new dict entry for it.
+ by_bus_device = f'{by_bus_dir}/{device}'
+ if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device):
+ device = os.path.basename(os.readlink(by_bus_device))
+
config_file = base_dir + f'/serial-getty@{device}.service'
getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service'
- render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device])
+ render(config_file, 'getty/serial-getty.service.tmpl', device_config)
os.symlink(config_file, getty_wants_symlink)
# GRUB
# For existing serial line change speed (if necessary)
# Only applys to ttyS0
- if 'ttyS0' not in console['device'].keys():
+ if 'ttyS0' not in console['device']:
return None
speed = console['device']['ttyS0']['speed']
@@ -98,7 +118,6 @@ def generate(console):
return None
lines = read_file(grub_config).split('\n')
-
p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$')
write = False
newlines = []
@@ -122,9 +141,8 @@ def generate(console):
return None
def apply(console):
- # reset screen blanking
+ # Reset screen blanking
call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux </dev/tty1 >/dev/tty1 2>&1')
-
# Reload systemd manager configuration
call('systemctl daemon-reload')
@@ -136,11 +154,11 @@ def apply(console):
call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1')
# Start getty process on configured serial interfaces
- for device in console['device'].keys():
+ for device in console['device']:
# Only start console if it exists on the running system. If a user
# detaches a USB serial console and reboots - it should not fail!
if os.path.exists(f'/dev/{device}'):
- call(f'systemctl start serial-getty@{device}.service')
+ call(f'systemctl restart serial-getty@{device}.service')
return None
diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py
index 2409eec1f..ef726670c 100755
--- a/src/conf_mode/tftp_server.py
+++ b/src/conf_mode/tftp_server.py
@@ -24,6 +24,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configverify import verify_vrf
from vyos.template import render
from vyos.template import is_ipv4
from vyos.util import call
@@ -65,10 +66,11 @@ def verify(tftpd):
if 'listen_address' not in tftpd:
raise ConfigError('TFTP server listen address must be configured!')
- for address in tftpd['listen_address']:
+ for address, address_config in tftpd['listen_address'].items():
if not is_addr_assigned(address):
print(f'WARNING: TFTP server listen address "{address}" not ' \
'assigned to any interface!')
+ verify_vrf(address_config)
return None
@@ -83,7 +85,7 @@ def generate(tftpd):
return None
idx = 0
- for address in tftpd['listen_address']:
+ for address, address_config in tftpd['listen_address'].items():
config = deepcopy(tftpd)
port = tftpd['port']
if is_ipv4(address):
@@ -91,6 +93,9 @@ def generate(tftpd):
else:
config['listen_address'] = f'[{address}]:{port} -6'
+ if 'vrf' in address_config:
+ config['vrf'] = address_config['vrf']
+
file = config_file + str(idx)
render(file, 'tftp-server/default.tmpl', config)
idx = idx + 1
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index 9c52f77ca..818e8fa0b 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -290,6 +290,8 @@ def get_config(config=None):
# LNS secret
if conf.exists(['lns', 'shared-secret']):
l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret'])
+ if conf.exists(['lns', 'host-name']):
+ l2tp['lns_host_name'] = conf.return_value(['lns', 'host-name'])
if conf.exists(['ccp-disable']):
l2tp['ccp_disable'] = True
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py
index f6db196dc..51ea1f223 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -23,9 +23,11 @@ from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
+from vyos.util import is_systemd_service_running
from vyos.xml import defaults
from vyos import ConfigError
from crypt import crypt, mksalt, METHOD_SHA512
+from time import sleep
from vyos import airbag
airbag.enable()
@@ -172,6 +174,16 @@ def apply(ocserv):
os.unlink(file)
else:
call('systemctl restart ocserv.service')
+ counter = 0
+ while True:
+ # exit early when service runs
+ if is_systemd_service_running("ocserv.service"):
+ break
+ sleep(0.250)
+ if counter > 5:
+ raise ConfigError('openconnect failed to start, check the logs for details')
+ break
+ counter += 1
if __name__ == '__main__':
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index d1a71a5ad..68980e5ab 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -26,6 +26,7 @@ from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
+from vyos.util import write_file
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -34,6 +35,10 @@ cfg_dir = '/run/accel-pppd'
sstp_conf = '/run/accel-pppd/sstp.conf'
sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
+cert_file_path = os.path.join(cfg_dir, 'sstp-cert.pem')
+cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key')
+ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem')
+
def get_config(config=None):
if config:
conf = config
@@ -58,7 +63,7 @@ def verify(sstp):
verify_accel_ppp_base_service(sstp)
- if not sstp['client_ip_pool']:
+ if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp:
raise ConfigError('Client IP subnet required')
#
@@ -72,22 +77,32 @@ def verify(sstp):
ssl = sstp['ssl']
+ # CA
if 'ca_certificate' not in ssl:
raise ConfigError('SSL CA certificate missing on SSTP config')
+ ca_name = ssl['ca_certificate']
+
+ if ca_name not in sstp['pki']['ca']:
+ raise ConfigError('Invalid CA certificate on SSTP config')
+
+ if 'certificate' not in sstp['pki']['ca'][ca_name]:
+ raise ConfigError('Missing certificate data for CA certificate on SSTP config')
+
+ # Certificate
if 'certificate' not in ssl:
raise ConfigError('SSL certificate missing on SSTP config')
cert_name = ssl['certificate']
- if ssl['ca_certificate'] not in sstp['pki']['ca']:
- raise ConfigError('Invalid CA certificate on SSTP config')
-
if cert_name not in sstp['pki']['certificate']:
raise ConfigError('Invalid certificate on SSTP config')
pki_cert = sstp['pki']['certificate'][cert_name]
+ if 'certificate' not in pki_cert:
+ raise ConfigError('Missing certificate data for certificate on SSTP config')
+
if 'private' not in pki_cert or 'key' not in pki_cert['private']:
raise ConfigError('Missing private key for certificate on SSTP config')
@@ -98,27 +113,18 @@ def generate(sstp):
if not sstp:
return None
- cert_file_path = os.path.join(cfg_dir, 'sstp-cert.pem')
- cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key')
- ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem')
+ # accel-cmd reload doesn't work so any change results in a restart of the daemon
+ render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp)
cert_name = sstp['ssl']['certificate']
pki_cert = sstp['pki']['certificate'][cert_name]
- with open(cert_file_path, 'w') as f:
- f.write(wrap_certificate(pki_cert['certificate']))
-
- with open(cert_key_path, 'w') as f:
- f.write(wrap_private_key(pki_cert['private']['key']))
-
ca_cert_name = sstp['ssl']['ca_certificate']
pki_ca = sstp['pki']['ca'][ca_cert_name]
- with open(ca_cert_file_path, 'w') as f:
- f.write(wrap_certificate(pki_ca['certificate']))
-
- # accel-cmd reload doesn't work so any change results in a restart of the daemon
- render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp)
+ write_file(cert_file_path, wrap_certificate(pki_cert['certificate']))
+ write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
+ write_file(ca_cert_file_path, wrap_certificate(pki_ca['certificate']))
if dict_search('authentication.mode', sstp) == 'local':
render(sstp_chap_secrets, 'accel-ppp/chap-secrets.config_dict.tmpl',
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 919083ac4..38c0c4463 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -18,7 +18,6 @@ import os
from sys import exit
from json import loads
-from tempfile import NamedTemporaryFile
from vyos.config import Config
from vyos.configdict import node_changed
@@ -31,10 +30,12 @@ from vyos.util import get_interface_config
from vyos.util import popen
from vyos.util import run
from vyos import ConfigError
+from vyos import frr
from vyos import airbag
airbag.enable()
-config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf'
+config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf'
+nft_vrf_config = '/tmp/nftables-vrf-zones'
def list_rules():
command = 'ip -j -4 rule show'
@@ -128,8 +129,8 @@ def verify(vrf):
def generate(vrf):
render(config_file, 'vrf/vrf.conf.tmpl', vrf)
# Render nftables zones config
- vrf['nft_vrf_zones'] = NamedTemporaryFile().name
- render(vrf['nft_vrf_zones'], 'firewall/nftables-vrf-zones.tmpl', vrf)
+
+ render(nft_vrf_config, 'firewall/nftables-vrf-zones.tmpl', vrf)
return None
@@ -165,8 +166,9 @@ def apply(vrf):
_, err = popen('nft list table inet vrf_zones')
# If not, create a table
if err:
- cmd(f'nft -f {vrf["nft_vrf_zones"]}')
- os.unlink(vrf['nft_vrf_zones'])
+ if os.path.exists(nft_vrf_config):
+ cmd(f'nft -f {nft_vrf_config}')
+ os.unlink(nft_vrf_config)
for name, config in vrf['name'].items():
table = config['table']
diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py
index 87ee8f2d1..1a7bd1f09 100755
--- a/src/conf_mode/vrf_vni.py
+++ b/src/conf_mode/vrf_vni.py
@@ -32,37 +32,26 @@ def get_config(config=None):
else:
conf = Config()
- # This script only works with a passed VRF name
- if len(argv) < 1:
- raise NotImplementedError
- vrf = argv[1]
+ base = ['vrf']
+ vrf = conf.get_config_dict(base, get_first_key=True)
+ return vrf
- # "assemble" dict - easier here then use a full blown get_config_dict()
- # on a single leafNode
- vni = { 'vrf' : vrf }
- tmp = conf.return_value(['vrf', 'name', vrf, 'vni'])
- if tmp: vni.update({ 'vni' : tmp })
-
- return vni
-
-def verify(vni):
+def verify(vrf):
return None
-def generate(vni):
- vni['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vni)
+def generate(vrf):
+ vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vrf)
return None
-def apply(vni):
+def apply(vrf):
# add configuration to FRR
frr_cfg = frr.FRRConfig()
frr_cfg.load_configuration(frr_daemon)
- frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '')
- frr_cfg.add_before(r'(interface .*|line vty)', vni['new_frr_config'])
+ frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True)
+ if 'new_frr_config' in vrf:
+ frr_cfg.add_before(frr.default_add_before, vrf['new_frr_config'])
frr_cfg.commit_configuration(frr_daemon)
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py
index e8f1c1f99..c72efc61f 100755
--- a/src/conf_mode/vrrp.py
+++ b/src/conf_mode/vrrp.py
@@ -28,6 +28,7 @@ from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.util import call
+from vyos.util import is_systemd_service_running
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
@@ -139,7 +140,12 @@ def apply(vrrp):
call(f'systemctl stop {service_name}')
return None
- call(f'systemctl restart {service_name}')
+ # XXX: T3944 - reload keepalived configuration if service is already running
+ # to not cause any service disruption when applying changes.
+ if is_systemd_service_running(service_name):
+ call(f'systemctl reload {service_name}')
+ else:
+ call(f'systemctl restart {service_name}')
return None
if __name__ == '__main__':
diff --git a/src/etc/cron.d/check-wwan b/src/etc/cron.d/check-wwan
new file mode 100644
index 000000000..28190776f
--- /dev/null
+++ b/src/etc/cron.d/check-wwan
@@ -0,0 +1 @@
+*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient
index f737148dc..ae6bf9f16 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient
@@ -23,10 +23,12 @@ if [ -z ${CONTROLLED_STOP} ] ; then
if ([ $dhclient -ne $current_dhclient ] && [ $dhclient -ne $master_dhclient ]); then
# get path to PID-file of dhclient process
local dhclient_pidfile=`ps --no-headers --format args --pid $dhclient | awk 'match(\$0, ".*-pf (/.*pid) .*", PF) { print PF[1] }'`
+ # get path to lease-file of dhclient process
+ local dhclient_leasefile=`ps --no-headers --format args --pid $dhclient | awk 'match(\$0, ".*-lf (/\\\S*leases) .*", LF) { print LF[1] }'`
# stop dhclient with native command - this will run dhclient-script with correct reason unlike simple kill
- logmsg info "Stopping dhclient with PID: ${dhclient}, PID file: $dhclient_pidfile"
+ logmsg info "Stopping dhclient with PID: ${dhclient}, PID file: ${dhclient_pidfile}, Leases file: ${dhclient_leasefile}"
if [[ -e $dhclient_pidfile ]]; then
- dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile
+ dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile -lf $dhclient_leasefile
else
logmsg error "PID file $dhclient_pidfile does not exists, killing dhclient with SIGTERM signal"
kill -s 15 ${dhclient}
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 694d53b6b..fec792b64 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -15,10 +15,16 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
if_metric="$IF_METRIC"
- # try to delete default ip route
+ # try to delete default ip route
for router in $old_routers; do
- logmsg info "Deleting default route: via $router dev ${interface} ${if_metric:+metric $if_metric}"
- ip -4 route del default via $router dev ${interface} ${if_metric:+metric $if_metric}
+ # check if we are bound to a VRF
+ local vrf_name=$(basename /sys/class/net/${interface}/upper_* | sed -e 's/upper_//')
+ if [ "$vrf_name" != "*" ]; then
+ vrf="vrf $vrf_name"
+ fi
+
+ logmsg info "Deleting default route: via $router dev ${interface} ${if_metric:+metric $if_metric} ${vrf}"
+ ip -4 route del default via $router dev ${interface} ${if_metric:+metric $if_metric} ${vrf}
if_metric=$((if_metric+1))
done
diff --git a/src/etc/logrotate.d/vyos-atop b/src/etc/logrotate.d/vyos-atop
new file mode 100644
index 000000000..0c8359c7b
--- /dev/null
+++ b/src/etc/logrotate.d/vyos-atop
@@ -0,0 +1,20 @@
+/var/log/atop/atop.log {
+ daily
+ dateext
+ dateformat _%Y-%m-%d_%H-%M-%S
+ maxsize 10M
+ missingok
+ nocompress
+ nocreate
+ nomail
+ rotate 10
+ prerotate
+ # stop the service
+ systemctl stop atop.service
+ endscript
+ postrotate
+ # start atop service again
+ systemctl start atop.service
+ endscript
+}
+
diff --git a/src/etc/systemd/system/atop.service.d/10-override.conf b/src/etc/systemd/system/atop.service.d/10-override.conf
new file mode 100644
index 000000000..10df15862
--- /dev/null
+++ b/src/etc/systemd/system/atop.service.d/10-override.conf
@@ -0,0 +1,6 @@
+[Service]
+ExecStartPre=
+ExecStart=
+ExecStart=/bin/sh -c 'exec /usr/bin/atop ${LOGOPTS} -w "${LOGPATH}/atop.log" ${LOGINTERVAL}'
+ExecStartPost=
+
diff --git a/src/etc/systemd/system/avahi-daemon.service.d/override.conf b/src/etc/systemd/system/avahi-daemon.service.d/override.conf
new file mode 100644
index 000000000..a9d2085f7
--- /dev/null
+++ b/src/etc/systemd/system/avahi-daemon.service.d/override.conf
@@ -0,0 +1,8 @@
+[Unit]
+After=
+After=vyos-router.service
+ConditionPathExists=/run/avahi-daemon/avahi-daemon.conf
+
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/avahi-daemon --syslog --file /run/avahi-daemon/avahi-daemon.conf \ No newline at end of file
diff --git a/src/etc/systemd/system/openvpn@.service.d/override.conf b/src/etc/systemd/system/openvpn@.service.d/10-override.conf
index 03fe6b587..775a2d7ba 100644
--- a/src/etc/systemd/system/openvpn@.service.d/override.conf
+++ b/src/etc/systemd/system/openvpn@.service.d/10-override.conf
@@ -7,6 +7,7 @@ WorkingDirectory=
WorkingDirectory=/run/openvpn
ExecStart=
ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid
+ExecReload=/bin/kill -HUP $MAINPID
User=openvpn
Group=openvpn
AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE
diff --git a/src/etc/udev/rules.d/62-temporary-interface-rename.rules b/src/etc/udev/rules.d/62-temporary-interface-rename.rules
new file mode 100644
index 000000000..4a579dcab
--- /dev/null
+++ b/src/etc/udev/rules.d/62-temporary-interface-rename.rules
@@ -0,0 +1 @@
+SUBSYSTEM=="net", ACTION=="add", KERNEL=="eth*", DRIVERS=="?*", NAME="e$env{IFINDEX}"
diff --git a/src/etc/udev/rules.d/65-vyos-net.rules b/src/etc/udev/rules.d/65-vyos-net.rules
index c8d5750dd..32ae352de 100644
--- a/src/etc/udev/rules.d/65-vyos-net.rules
+++ b/src/etc/udev/rules.d/65-vyos-net.rules
@@ -4,11 +4,8 @@
ACTION!="add", GOTO="vyos_net_end"
SUBSYSTEM!="net", GOTO="vyos_net_end"
-# ignore the interface if a name has already been set
-NAME=="?*", GOTO="vyos_net_end"
-
# Do name change for ethernet and wireless devices only
-KERNEL!="eth*|wlan*", GOTO="vyos_net_end"
+KERNEL!="eth*|wlan*|e*", GOTO="vyos_net_end"
# ignore "secondary" monitor interfaces of mac80211 drivers
KERNEL=="wlan*", ATTRS{type}=="803", GOTO="vyos_net_end"
diff --git a/src/etc/udev/rules.d/90-vyos-serial.rules b/src/etc/udev/rules.d/90-vyos-serial.rules
index 872fd4fea..30c1d3170 100644
--- a/src/etc/udev/rules.d/90-vyos-serial.rules
+++ b/src/etc/udev/rules.d/90-vyos-serial.rules
@@ -22,7 +22,7 @@ IMPORT{builtin}="path_id", IMPORT{builtin}="usb_id"
# (tr -d -) does the replacement
# - Replace the first group after ":" to represent the bus relation (sed -e 0,/:/s//b/) indicated by "b"
# - Replace the next group after ":" to represent the port relation (sed -e 0,/:/s//p/) indicated by "p"
-ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", PROGRAM="/bin/sh -c 'echo $env{ID_PATH:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result"
-ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", PROGRAM="/bin/sh -c 'echo $env{ID_PATH:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result"
+ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", PROGRAM="/bin/sh -c 'echo $env{ID_PATH} | cut -d- -f3- | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result"
+ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", PROGRAM="/bin/sh -c 'echo $env{ID_PATH} | cut -d- -f3- | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result"
LABEL="serial_end"
diff --git a/src/helpers/strip-private.py b/src/helpers/strip-private.py
index c74a379aa..e4e1fe11d 100755
--- a/src/helpers/strip-private.py
+++ b/src/helpers/strip-private.py
@@ -106,6 +106,7 @@ if __name__ == "__main__":
stripping_rules = [
# Strip passwords
(True, re.compile(r'password \S+'), 'password xxxxxx'),
+ (True, re.compile(r'cisco-authentication \S+'), 'cisco-authentication xxxxxx'),
# Strip public key information
(True, re.compile(r'public-keys \S+'), 'public-keys xxxx@xxx.xxx'),
(True, re.compile(r'type \'ssh-(rsa|dss)\''), 'type ssh-xxx'),
diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py
index c5bf22f10..b9cc87bfa 100755
--- a/src/helpers/vyos-boot-config-loader.py
+++ b/src/helpers/vyos-boot-config-loader.py
@@ -23,12 +23,12 @@ import grp
import traceback
from datetime import datetime
-from vyos.defaults import directories
+from vyos.defaults import directories, config_status
from vyos.configsession import ConfigSession, ConfigSessionError
from vyos.configtree import ConfigTree
from vyos.util import cmd
-STATUS_FILE = '/tmp/vyos-config-status'
+STATUS_FILE = config_status
TRACE_FILE = '/tmp/boot-config-trace'
CFG_GROUP = 'vyattacfg'
diff --git a/src/helpers/vyos-check-wwan.py b/src/helpers/vyos-check-wwan.py
new file mode 100755
index 000000000..2ff9a574f
--- /dev/null
+++ b/src/helpers/vyos-check-wwan.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.configquery import VbashOpRun
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.util import is_wwan_connected
+
+conf = ConfigTreeQuery()
+dict = conf.get_config_dict(['interfaces', 'wwan'], key_mangling=('-', '_'),
+ get_first_key=True)
+
+for interface, interface_config in dict.items():
+ if not is_wwan_connected(interface):
+ if 'disable' in interface_config:
+ # do not restart this interface as it's disabled by the user
+ continue
+
+ op = VbashOpRun()
+ op.run(['connect', 'interface', interface])
+
+exit(0)
diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name
index 0652e98b1..afeef8f2d 100755
--- a/src/helpers/vyos_net_name
+++ b/src/helpers/vyos_net_name
@@ -25,14 +25,13 @@ from sys import argv
from vyos.configtree import ConfigTree
from vyos.defaults import directories
-from vyos.util import cmd
+from vyos.util import cmd, boot_configuration_complete
vyos_udev_dir = directories['vyos_udev_dir']
vyos_log_dir = '/run/udev/log'
vyos_log_file = os.path.join(vyos_log_dir, 'vyos-net-name')
config_path = '/opt/vyatta/etc/config/config.boot'
-config_status = '/tmp/vyos-config-status'
lock = threading.Lock()
@@ -43,13 +42,6 @@ except FileExistsError:
logging.basicConfig(filename=vyos_log_file, level=logging.DEBUG)
-def boot_configuration_complete() -> bool:
- """ Check if vyos-router has completed, hence hotplug event
- """
- if os.path.isfile(config_status):
- return True
- return False
-
def is_available(intfs: dict, intf_name: str) -> bool:
""" Check if interface name is already assigned
"""
@@ -69,6 +61,19 @@ def find_available(intfs: dict, prefix: str) -> str:
return f'{prefix}{len(index_list)}'
+def mod_ifname(ifname: str) -> str:
+ """ Check interface with names eX and return ifname on the next format eth{ifindex} - 2
+ """
+ if re.match("^e[0-9]+$", ifname):
+ intf = ifname.split("e")
+ if intf[1]:
+ if int(intf[1]) >= 2:
+ return "eth" + str(int(intf[1]) - 2)
+ else:
+ return "eth" + str(intf[1])
+
+ return ifname
+
def get_biosdevname(ifname: str) -> str:
""" Use legacy vyatta-biosdevname to query for name
@@ -77,10 +82,12 @@ def get_biosdevname(ifname: str) -> str:
XXX: This throws an error, and likely has for a long time, unnoticed
since vyatta_net_name redirected stderr to /dev/null.
"""
- if 'eth' not in ifname:
- return ifname
+ intf = mod_ifname(ifname)
+
+ if 'eth' not in intf:
+ return intf
if os.path.isdir('/proc/xen'):
- return ifname
+ return intf
time.sleep(1)
@@ -90,7 +97,7 @@ def get_biosdevname(ifname: str) -> str:
logging.error(f'biosdevname error: {e}')
biosname = ''
- return ifname if biosname == '' else biosname
+ return intf if biosname == '' else biosname
def leave_rescan_hint(intf_name: str, hwid: str):
"""Write interface information reported by udev
@@ -129,7 +136,19 @@ def get_configfile_interfaces() -> dict:
logging.critical(f"OSError {e}")
exit(1)
- config = ConfigTree(config_file)
+ try:
+ config = ConfigTree(config_file)
+ except Exception:
+ logging.debug(f"updating component version string syntax")
+ try:
+ # this will update the component version string in place, for
+ # updates 1.2 --> 1.3/1.4
+ os.system(f'/usr/libexec/vyos/run-config-migration.py {config_path} --virtual --set-vintage=vyos')
+ with open(config_path) as f:
+ config_file = f.read()
+ config = ConfigTree(config_file)
+ except Exception as e:
+ logging.critical(f"ConfigTree error: {e}")
base = ['interfaces', 'ethernet']
if config.exists(base):
@@ -186,9 +205,9 @@ def on_boot_event(intf_name: str, hwid: str, predefined: str = '') -> str:
interfaces = get_configfile_interfaces()
logging.debug(f"config file interfaces are {interfaces}")
- if hwid in list(interfaces) and intf_name == interfaces[hwid]:
- logging.info(f"use mapping from config file: '{hwid}' -> '{intf_name}'")
- return intf_name
+ if hwid in list(interfaces):
+ logging.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'")
+ return interfaces[hwid]
add_assigned_interfaces(interfaces)
logging.debug(f"adding assigned interfaces: {interfaces}")
@@ -223,6 +242,7 @@ lock.acquire()
if not boot_configuration_complete():
res = on_boot_event(argv[1], argv[2], predefined=predef_name)
logging.debug(f"on boot, returned name is {res}")
+ print(res)
else:
logging.debug("boot configuration complete")
lock.release()
diff --git a/src/migration-scripts/bgp/1-to-2 b/src/migration-scripts/bgp/1-to-2
new file mode 100755
index 000000000..4c6d5ceb8
--- /dev/null
+++ b/src/migration-scripts/bgp/1-to-2
@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is 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/>.
+
+# T3741: no-ipv4-unicast is now enabled by default
+
+from sys import argv
+from sys import exit
+
+from vyos.configtree import ConfigTree
+from vyos.template import is_ipv4
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['protocols', 'bgp']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+# This is now a default option - simply delete it.
+# As it was configured explicitly - we can also bail out early as we need to
+# do nothing!
+if config.exists(base + ['parameters', 'default', 'no-ipv4-unicast']):
+ config.delete(base + ['parameters', 'default', 'no-ipv4-unicast'])
+
+ # Check if the "default" node is now empty, if so - remove it
+ if len(config.list_nodes(base + ['parameters', 'default'])) == 0:
+ config.delete(base + ['parameters', 'default'])
+
+ # Check if the "default" node is now empty, if so - remove it
+ if len(config.list_nodes(base + ['parameters'])) == 0:
+ config.delete(base + ['parameters'])
+
+ exit(0)
+
+# As we now install a new default option into BGP we need to migrate all
+# existing BGP neighbors and restore the old behavior
+if config.exists(base + ['neighbor']):
+ for neighbor in config.list_nodes(base + ['neighbor']):
+ peer_group = base + ['neighbor', neighbor, 'peer-group']
+ if config.exists(peer_group):
+ peer_group_name = config.return_value(peer_group)
+ # peer group enables old behavior for neighbor - bail out
+ if config.exists(base + ['peer-group', peer_group_name, 'address-family', 'ipv4-unicast']):
+ continue
+
+ afi_ipv4 = base + ['neighbor', neighbor, 'address-family', 'ipv4-unicast']
+ if not config.exists(afi_ipv4):
+ config.set(afi_ipv4)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print(f'Failed to save the modified config: {e}')
+ exit(1)
diff --git a/src/migration-scripts/interfaces/21-to-22 b/src/migration-scripts/interfaces/21-to-22
index 06e07572f..098102102 100755
--- a/src/migration-scripts/interfaces/21-to-22
+++ b/src/migration-scripts/interfaces/21-to-22
@@ -15,131 +15,32 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sys import argv
-from sys import exit
from vyos.configtree import ConfigTree
-def migrate_ospf(config, path, interface):
- path = path + ['ospf']
- if config.exists(path):
- new_base = ['protocols', 'ospf', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
- # if "ip ospf" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
+file_name = argv[1]
+with open(file_name, 'r') as f:
+ config_file = f.read()
-def migrate_ospfv3(config, path, interface):
- path = path + ['ospfv3']
- if config.exists(path):
- new_base = ['protocols', 'ospfv3', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
+config = ConfigTree(config_file)
+base = ['interfaces', 'tunnel']
- # if "ipv6 ospfv3" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
+if not config.exists(base):
+ exit(0)
-def migrate_rip(config, path, interface):
- path = path + ['rip']
+for interface in config.list_nodes(base):
+ path = base + [interface, 'dhcp-interface']
if config.exists(path):
- new_base = ['protocols', 'rip', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
+ tmp = config.return_value(path)
config.delete(path)
-
- # if "ip rip" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
-
-def migrate_ripng(config, path, interface):
- path = path + ['ripng']
- if config.exists(path):
- new_base = ['protocols', 'ripng', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
-
- # if "ipv6 ripng" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
-
-if __name__ == '__main__':
- if (len(argv) < 1):
- print("Must specify file name!")
- exit(1)
-
- file_name = argv[1]
- with open(file_name, 'r') as f:
- config_file = f.read()
-
- config = ConfigTree(config_file)
-
- #
- # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0"
- #
- for type in config.list_nodes(['interfaces']):
- for interface in config.list_nodes(['interfaces', type]):
- ip_base = ['interfaces', type, interface, 'ip']
- ipv6_base = ['interfaces', type, interface, 'ipv6']
- migrate_rip(config, ip_base, interface)
- migrate_ripng(config, ipv6_base, interface)
- migrate_ospf(config, ip_base, interface)
- migrate_ospfv3(config, ipv6_base, interface)
-
- vif_path = ['interfaces', type, interface, 'vif']
- if config.exists(vif_path):
- for vif in config.list_nodes(vif_path):
- vif_ip_base = vif_path + [vif, 'ip']
- vif_ipv6_base = vif_path + [vif, 'ipv6']
- ifname = f'{interface}.{vif}'
-
- migrate_rip(config, vif_ip_base, ifname)
- migrate_ripng(config, vif_ipv6_base, ifname)
- migrate_ospf(config, vif_ip_base, ifname)
- migrate_ospfv3(config, vif_ipv6_base, ifname)
-
-
- vif_s_path = ['interfaces', type, interface, 'vif-s']
- if config.exists(vif_s_path):
- for vif_s in config.list_nodes(vif_s_path):
- vif_s_ip_base = vif_s_path + [vif_s, 'ip']
- vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6']
-
- # vif-c interfaces MUST be migrated before their parent vif-s
- # interface as the migrate_*() functions delete the path!
- vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c']
- if config.exists(vif_c_path):
- for vif_c in config.list_nodes(vif_c_path):
- vif_c_ip_base = vif_c_path + [vif_c, 'ip']
- vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6']
- ifname = f'{interface}.{vif_s}.{vif_c}'
-
- migrate_rip(config, vif_c_ip_base, ifname)
- migrate_ripng(config, vif_c_ipv6_base, ifname)
- migrate_ospf(config, vif_c_ip_base, ifname)
- migrate_ospfv3(config, vif_c_ipv6_base, ifname)
-
-
- ifname = f'{interface}.{vif_s}'
- migrate_rip(config, vif_s_ip_base, ifname)
- migrate_ripng(config, vif_s_ipv6_base, ifname)
- migrate_ospf(config, vif_s_ip_base, ifname)
- migrate_ospfv3(config, vif_s_ipv6_base, ifname)
-
- try:
- with open(file_name, 'w') as f:
- f.write(config.to_string())
- except OSError as e:
- print("Failed to save the modified config: {}".format(e))
- exit(1)
+ config.set(base + [interface, 'source-interface'], value=tmp)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/interfaces/22-to-23 b/src/migration-scripts/interfaces/22-to-23
index d1ec2ad3e..06e07572f 100755
--- a/src/migration-scripts/interfaces/22-to-23
+++ b/src/migration-scripts/interfaces/22-to-23
@@ -14,47 +14,132 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported
-# having a VTI interface in the CLI but no IPSec configuration - drop VTI
-# configuration if this is the case for VyOS 1.4
-
-import sys
+from sys import argv
+from sys import exit
from vyos.configtree import ConfigTree
+def migrate_ospf(config, path, interface):
+ path = path + ['ospf']
+ if config.exists(path):
+ new_base = ['protocols', 'ospf', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ip ospf" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
+def migrate_ospfv3(config, path, interface):
+ path = path + ['ospfv3']
+ if config.exists(path):
+ new_base = ['protocols', 'ospfv3', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ipv6 ospfv3" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
+def migrate_rip(config, path, interface):
+ path = path + ['rip']
+ if config.exists(path):
+ new_base = ['protocols', 'rip', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ip rip" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
+def migrate_ripng(config, path, interface):
+ path = path + ['ripng']
+ if config.exists(path):
+ new_base = ['protocols', 'ripng', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ipv6 ripng" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
if __name__ == '__main__':
- if (len(sys.argv) < 1):
+ if (len(argv) < 1):
print("Must specify file name!")
- sys.exit(1)
-
- file_name = sys.argv[1]
+ exit(1)
+ file_name = argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
config = ConfigTree(config_file)
- base = ['interfaces', 'vti']
- if not config.exists(base):
- # Nothing to do
- sys.exit(0)
-
- ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer']
- for interface in config.list_nodes(base):
- found = False
- if config.exists(ipsec_base):
- for peer in config.list_nodes(ipsec_base):
- if config.exists(ipsec_base + [peer, 'vti', 'bind']):
- tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind'])
- if tmp == interface:
- # Interface was found and we no longer need to search
- # for it in our IPSec peers
- found = True
- break
- if not found:
- config.delete(base + [interface])
+
+ #
+ # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0"
+ #
+ for type in config.list_nodes(['interfaces']):
+ for interface in config.list_nodes(['interfaces', type]):
+ ip_base = ['interfaces', type, interface, 'ip']
+ ipv6_base = ['interfaces', type, interface, 'ipv6']
+ migrate_rip(config, ip_base, interface)
+ migrate_ripng(config, ipv6_base, interface)
+ migrate_ospf(config, ip_base, interface)
+ migrate_ospfv3(config, ipv6_base, interface)
+
+ vif_path = ['interfaces', type, interface, 'vif']
+ if config.exists(vif_path):
+ for vif in config.list_nodes(vif_path):
+ vif_ip_base = vif_path + [vif, 'ip']
+ vif_ipv6_base = vif_path + [vif, 'ipv6']
+ ifname = f'{interface}.{vif}'
+
+ migrate_rip(config, vif_ip_base, ifname)
+ migrate_ripng(config, vif_ipv6_base, ifname)
+ migrate_ospf(config, vif_ip_base, ifname)
+ migrate_ospfv3(config, vif_ipv6_base, ifname)
+
+
+ vif_s_path = ['interfaces', type, interface, 'vif-s']
+ if config.exists(vif_s_path):
+ for vif_s in config.list_nodes(vif_s_path):
+ vif_s_ip_base = vif_s_path + [vif_s, 'ip']
+ vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6']
+
+ # vif-c interfaces MUST be migrated before their parent vif-s
+ # interface as the migrate_*() functions delete the path!
+ vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c']
+ if config.exists(vif_c_path):
+ for vif_c in config.list_nodes(vif_c_path):
+ vif_c_ip_base = vif_c_path + [vif_c, 'ip']
+ vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6']
+ ifname = f'{interface}.{vif_s}.{vif_c}'
+
+ migrate_rip(config, vif_c_ip_base, ifname)
+ migrate_ripng(config, vif_c_ipv6_base, ifname)
+ migrate_ospf(config, vif_c_ip_base, ifname)
+ migrate_ospfv3(config, vif_c_ipv6_base, ifname)
+
+
+ ifname = f'{interface}.{vif_s}'
+ migrate_rip(config, vif_s_ip_base, ifname)
+ migrate_ripng(config, vif_s_ipv6_base, ifname)
+ migrate_ospf(config, vif_s_ip_base, ifname)
+ migrate_ospfv3(config, vif_s_ipv6_base, ifname)
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
except OSError as e:
print("Failed to save the modified config: {}".format(e))
- sys.exit(1)
+ exit(1)
diff --git a/src/migration-scripts/interfaces/23-to-24 b/src/migration-scripts/interfaces/23-to-24
index 93ce9215f..d1ec2ad3e 100755
--- a/src/migration-scripts/interfaces/23-to-24
+++ b/src/migration-scripts/interfaces/23-to-24
@@ -14,356 +14,47 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# Migrate Wireguard to store keys in CLI
-# Migrate EAPoL to PKI configuration
+# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported
+# having a VTI interface in the CLI but no IPSec configuration - drop VTI
+# configuration if this is the case for VyOS 1.4
-import os
import sys
from vyos.configtree import ConfigTree
-from vyos.pki import load_certificate
-from vyos.pki import load_crl
-from vyos.pki import load_dh_parameters
-from vyos.pki import load_private_key
-from vyos.pki import encode_certificate
-from vyos.pki import encode_dh_parameters
-from vyos.pki import encode_private_key
-from vyos.util import run
-def wrapped_pem_to_config_value(pem):
- out = []
- for line in pem.strip().split("\n"):
- if not line or line.startswith("-----") or line[0] == '#':
- continue
- out.append(line)
- return "".join(out)
+if __name__ == '__main__':
+ if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
-def read_file_for_pki(config_auth_path):
- full_path = os.path.join(AUTH_DIR, config_auth_path)
- output = None
+ file_name = sys.argv[1]
- if os.path.isfile(full_path):
- if not os.access(full_path, os.R_OK):
- run(f'sudo chmod 644 {full_path}')
+ with open(file_name, 'r') as f:
+ config_file = f.read()
- with open(full_path, 'r') as f:
- output = f.read()
+ config = ConfigTree(config_file)
+ base = ['interfaces', 'vti']
+ if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
- return output
-
-if (len(sys.argv) < 1):
- print("Must specify file name!")
- sys.exit(1)
-
-file_name = sys.argv[1]
-
-with open(file_name, 'r') as f:
- config_file = f.read()
-
-config = ConfigTree(config_file)
-
-AUTH_DIR = '/config/auth'
-pki_base = ['pki']
-
-# OpenVPN
-base = ['interfaces', 'openvpn']
-
-if config.exists(base):
- for interface in config.list_nodes(base):
- x509_base = base + [interface, 'tls']
- pki_name = f'openvpn_{interface}'
-
- if config.exists(base + [interface, 'shared-secret-key-file']):
- if not config.exists(pki_base + ['openvpn', 'shared-secret']):
- config.set(pki_base + ['openvpn', 'shared-secret'])
- config.set_tag(pki_base + ['openvpn', 'shared-secret'])
-
- key_file = config.return_value(base + [interface, 'shared-secret-key-file'])
- key = read_file_for_pki(key_file)
- key_pki_name = f'{pki_name}_shared'
-
- if key:
- config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
- config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
- config.set(base + [interface, 'shared-secret-key'], value=key_pki_name)
- else:
- print(f'Failed to migrate shared-secret-key on openvpn interface {interface}')
-
- config.delete(base + [interface, 'shared-secret-key-file'])
-
- if not config.exists(base + [interface, 'tls']):
- continue
-
- if config.exists(base + [interface, 'tls', 'auth-file']):
- if not config.exists(pki_base + ['openvpn', 'shared-secret']):
- config.set(pki_base + ['openvpn', 'shared-secret'])
- config.set_tag(pki_base + ['openvpn', 'shared-secret'])
-
- key_file = config.return_value(base + [interface, 'tls', 'auth-file'])
- key = read_file_for_pki(key_file)
- key_pki_name = f'{pki_name}_auth'
-
- if key:
- config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
- config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
- config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name)
- else:
- print(f'Failed to migrate auth-key on openvpn interface {interface}')
-
- config.delete(base + [interface, 'tls', 'auth-file'])
-
- if config.exists(base + [interface, 'tls', 'crypt-file']):
- if not config.exists(pki_base + ['openvpn', 'shared-secret']):
- config.set(pki_base + ['openvpn', 'shared-secret'])
- config.set_tag(pki_base + ['openvpn', 'shared-secret'])
-
- key_file = config.return_value(base + [interface, 'tls', 'crypt-file'])
- key = read_file_for_pki(key_file)
- key_pki_name = f'{pki_name}_crypt'
-
- if key:
- config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
- config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
- config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name)
- else:
- print(f'Failed to migrate crypt-key on openvpn interface {interface}')
-
- config.delete(base + [interface, 'tls', 'crypt-file'])
-
- if config.exists(x509_base + ['ca-cert-file']):
- if not config.exists(pki_base + ['ca']):
- config.set(pki_base + ['ca'])
- config.set_tag(pki_base + ['ca'])
-
- cert_file = config.return_value(x509_base + ['ca-cert-file'])
- cert_path = os.path.join(AUTH_DIR, cert_file)
- cert = None
-
- if os.path.isfile(cert_path):
- if not os.access(cert_path, os.R_OK):
- run(f'sudo chmod 644 {cert_path}')
-
- with open(cert_path, 'r') as f:
- cert_data = f.read()
- cert = load_certificate(cert_data, wrap_tags=False)
-
- if cert:
- cert_pem = encode_certificate(cert)
- config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
- config.set(x509_base + ['ca-certificate'], value=pki_name)
- else:
- print(f'Failed to migrate CA certificate on openvpn interface {interface}')
-
- config.delete(x509_base + ['ca-cert-file'])
-
- if config.exists(x509_base + ['crl-file']):
- if not config.exists(pki_base + ['ca']):
- config.set(pki_base + ['ca'])
- config.set_tag(pki_base + ['ca'])
-
- crl_file = config.return_value(x509_base + ['crl-file'])
- crl_path = os.path.join(AUTH_DIR, crl_file)
- crl = None
-
- if os.path.isfile(crl_path):
- if not os.access(crl_path, os.R_OK):
- run(f'sudo chmod 644 {crl_path}')
-
- with open(crl_path, 'r') as f:
- crl_data = f.read()
- crl = load_crl(crl_data, wrap_tags=False)
-
- if crl:
- crl_pem = encode_certificate(crl)
- config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem))
- else:
- print(f'Failed to migrate CRL on openvpn interface {interface}')
-
- config.delete(x509_base + ['crl-file'])
-
- if config.exists(x509_base + ['cert-file']):
- if not config.exists(pki_base + ['certificate']):
- config.set(pki_base + ['certificate'])
- config.set_tag(pki_base + ['certificate'])
-
- cert_file = config.return_value(x509_base + ['cert-file'])
- cert_path = os.path.join(AUTH_DIR, cert_file)
- cert = None
-
- if os.path.isfile(cert_path):
- if not os.access(cert_path, os.R_OK):
- run(f'sudo chmod 644 {cert_path}')
-
- with open(cert_path, 'r') as f:
- cert_data = f.read()
- cert = load_certificate(cert_data, wrap_tags=False)
-
- if cert:
- cert_pem = encode_certificate(cert)
- config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
- config.set(x509_base + ['certificate'], value=pki_name)
- else:
- print(f'Failed to migrate certificate on openvpn interface {interface}')
-
- config.delete(x509_base + ['cert-file'])
-
- if config.exists(x509_base + ['key-file']):
- key_file = config.return_value(x509_base + ['key-file'])
- key_path = os.path.join(AUTH_DIR, key_file)
- key = None
-
- if os.path.isfile(key_path):
- if not os.access(key_path, os.R_OK):
- run(f'sudo chmod 644 {key_path}')
-
- with open(key_path, 'r') as f:
- key_data = f.read()
- key = load_private_key(key_data, passphrase=None, wrap_tags=False)
-
- if key:
- key_pem = encode_private_key(key, passphrase=None)
- config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
- else:
- print(f'Failed to migrate private key on openvpn interface {interface}')
-
- config.delete(x509_base + ['key-file'])
-
- if config.exists(x509_base + ['dh-file']):
- if not config.exists(pki_base + ['dh']):
- config.set(pki_base + ['dh'])
- config.set_tag(pki_base + ['dh'])
-
- dh_file = config.return_value(x509_base + ['dh-file'])
- dh_path = os.path.join(AUTH_DIR, dh_file)
- dh = None
-
- if os.path.isfile(dh_path):
- if not os.access(dh_path, os.R_OK):
- run(f'sudo chmod 644 {dh_path}')
-
- with open(dh_path, 'r') as f:
- dh_data = f.read()
- dh = load_dh_parameters(dh_data, wrap_tags=False)
-
- if dh:
- dh_pem = encode_dh_parameters(dh)
- config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem))
- config.set(x509_base + ['dh-params'], value=pki_name)
- else:
- print(f'Failed to migrate DH parameters on openvpn interface {interface}')
-
- config.delete(x509_base + ['dh-file'])
-
-# Wireguard
-base = ['interfaces', 'wireguard']
-
-if config.exists(base):
- for interface in config.list_nodes(base):
- private_key_path = base + [interface, 'private-key']
-
- key_file = 'default'
- if config.exists(private_key_path):
- key_file = config.return_value(private_key_path)
-
- full_key_path = f'/config/auth/wireguard/{key_file}/private.key'
-
- if not os.path.exists(full_key_path):
- print(f'Could not find wireguard private key for migration on interface "{interface}"')
- continue
-
- with open(full_key_path, 'r') as f:
- key_data = f.read().strip()
- config.set(private_key_path, value=key_data)
-
- for peer in config.list_nodes(base + [interface, 'peer']):
- config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key')
-
-# Ethernet EAPoL
-base = ['interfaces', 'ethernet']
-
-if config.exists(base):
+ ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer']
for interface in config.list_nodes(base):
- if not config.exists(base + [interface, 'eapol']):
- continue
-
- x509_base = base + [interface, 'eapol']
- pki_name = f'eapol_{interface}'
-
- if config.exists(x509_base + ['ca-cert-file']):
- if not config.exists(pki_base + ['ca']):
- config.set(pki_base + ['ca'])
- config.set_tag(pki_base + ['ca'])
-
- cert_file = config.return_value(x509_base + ['ca-cert-file'])
- cert_path = os.path.join(AUTH_DIR, cert_file)
- cert = None
-
- if os.path.isfile(cert_path):
- if not os.access(cert_path, os.R_OK):
- run(f'sudo chmod 644 {cert_path}')
-
- with open(cert_path, 'r') as f:
- cert_data = f.read()
- cert = load_certificate(cert_data, wrap_tags=False)
-
- if cert:
- cert_pem = encode_certificate(cert)
- config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
- config.set(x509_base + ['ca-certificate'], value=pki_name)
- else:
- print(f'Failed to migrate CA certificate on eapol config for interface {interface}')
-
- config.delete(x509_base + ['ca-cert-file'])
-
- if config.exists(x509_base + ['cert-file']):
- if not config.exists(pki_base + ['certificate']):
- config.set(pki_base + ['certificate'])
- config.set_tag(pki_base + ['certificate'])
-
- cert_file = config.return_value(x509_base + ['cert-file'])
- cert_path = os.path.join(AUTH_DIR, cert_file)
- cert = None
-
- if os.path.isfile(cert_path):
- if not os.access(cert_path, os.R_OK):
- run(f'sudo chmod 644 {cert_path}')
-
- with open(cert_path, 'r') as f:
- cert_data = f.read()
- cert = load_certificate(cert_data, wrap_tags=False)
-
- if cert:
- cert_pem = encode_certificate(cert)
- config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
- config.set(x509_base + ['certificate'], value=pki_name)
- else:
- print(f'Failed to migrate certificate on eapol config for interface {interface}')
-
- config.delete(x509_base + ['cert-file'])
-
- if config.exists(x509_base + ['key-file']):
- key_file = config.return_value(x509_base + ['key-file'])
- key_path = os.path.join(AUTH_DIR, key_file)
- key = None
-
- if os.path.isfile(key_path):
- if not os.access(key_path, os.R_OK):
- run(f'sudo chmod 644 {key_path}')
-
- with open(key_path, 'r') as f:
- key_data = f.read()
- key = load_private_key(key_data, passphrase=None, wrap_tags=False)
-
- if key:
- key_pem = encode_private_key(key, passphrase=None)
- config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
- else:
- print(f'Failed to migrate private key on eapol config for interface {interface}')
-
- config.delete(x509_base + ['key-file'])
-
-try:
- with open(file_name, 'w') as f:
- f.write(config.to_string())
-except OSError as e:
- print("Failed to save the modified config: {}".format(e))
- sys.exit(1)
+ found = False
+ if config.exists(ipsec_base):
+ for peer in config.list_nodes(ipsec_base):
+ if config.exists(ipsec_base + [peer, 'vti', 'bind']):
+ tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind'])
+ if tmp == interface:
+ # Interface was found and we no longer need to search
+ # for it in our IPSec peers
+ found = True
+ break
+ if not found:
+ config.delete(base + [interface])
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/interfaces/24-to-25 b/src/migration-scripts/interfaces/24-to-25
new file mode 100755
index 000000000..93ce9215f
--- /dev/null
+++ b/src/migration-scripts/interfaces/24-to-25
@@ -0,0 +1,369 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Migrate Wireguard to store keys in CLI
+# Migrate EAPoL to PKI configuration
+
+import os
+import sys
+from vyos.configtree import ConfigTree
+from vyos.pki import load_certificate
+from vyos.pki import load_crl
+from vyos.pki import load_dh_parameters
+from vyos.pki import load_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_dh_parameters
+from vyos.pki import encode_private_key
+from vyos.util import run
+
+def wrapped_pem_to_config_value(pem):
+ out = []
+ for line in pem.strip().split("\n"):
+ if not line or line.startswith("-----") or line[0] == '#':
+ continue
+ out.append(line)
+ return "".join(out)
+
+def read_file_for_pki(config_auth_path):
+ full_path = os.path.join(AUTH_DIR, config_auth_path)
+ output = None
+
+ if os.path.isfile(full_path):
+ if not os.access(full_path, os.R_OK):
+ run(f'sudo chmod 644 {full_path}')
+
+ with open(full_path, 'r') as f:
+ output = f.read()
+
+ return output
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+AUTH_DIR = '/config/auth'
+pki_base = ['pki']
+
+# OpenVPN
+base = ['interfaces', 'openvpn']
+
+if config.exists(base):
+ for interface in config.list_nodes(base):
+ x509_base = base + [interface, 'tls']
+ pki_name = f'openvpn_{interface}'
+
+ if config.exists(base + [interface, 'shared-secret-key-file']):
+ if not config.exists(pki_base + ['openvpn', 'shared-secret']):
+ config.set(pki_base + ['openvpn', 'shared-secret'])
+ config.set_tag(pki_base + ['openvpn', 'shared-secret'])
+
+ key_file = config.return_value(base + [interface, 'shared-secret-key-file'])
+ key = read_file_for_pki(key_file)
+ key_pki_name = f'{pki_name}_shared'
+
+ if key:
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
+ config.set(base + [interface, 'shared-secret-key'], value=key_pki_name)
+ else:
+ print(f'Failed to migrate shared-secret-key on openvpn interface {interface}')
+
+ config.delete(base + [interface, 'shared-secret-key-file'])
+
+ if not config.exists(base + [interface, 'tls']):
+ continue
+
+ if config.exists(base + [interface, 'tls', 'auth-file']):
+ if not config.exists(pki_base + ['openvpn', 'shared-secret']):
+ config.set(pki_base + ['openvpn', 'shared-secret'])
+ config.set_tag(pki_base + ['openvpn', 'shared-secret'])
+
+ key_file = config.return_value(base + [interface, 'tls', 'auth-file'])
+ key = read_file_for_pki(key_file)
+ key_pki_name = f'{pki_name}_auth'
+
+ if key:
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
+ config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name)
+ else:
+ print(f'Failed to migrate auth-key on openvpn interface {interface}')
+
+ config.delete(base + [interface, 'tls', 'auth-file'])
+
+ if config.exists(base + [interface, 'tls', 'crypt-file']):
+ if not config.exists(pki_base + ['openvpn', 'shared-secret']):
+ config.set(pki_base + ['openvpn', 'shared-secret'])
+ config.set_tag(pki_base + ['openvpn', 'shared-secret'])
+
+ key_file = config.return_value(base + [interface, 'tls', 'crypt-file'])
+ key = read_file_for_pki(key_file)
+ key_pki_name = f'{pki_name}_crypt'
+
+ if key:
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
+ config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name)
+ else:
+ print(f'Failed to migrate crypt-key on openvpn interface {interface}')
+
+ config.delete(base + [interface, 'tls', 'crypt-file'])
+
+ if config.exists(x509_base + ['ca-cert-file']):
+ if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on openvpn interface {interface}')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+ if config.exists(x509_base + ['crl-file']):
+ if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+ crl_file = config.return_value(x509_base + ['crl-file'])
+ crl_path = os.path.join(AUTH_DIR, crl_file)
+ crl = None
+
+ if os.path.isfile(crl_path):
+ if not os.access(crl_path, os.R_OK):
+ run(f'sudo chmod 644 {crl_path}')
+
+ with open(crl_path, 'r') as f:
+ crl_data = f.read()
+ crl = load_crl(crl_data, wrap_tags=False)
+
+ if crl:
+ crl_pem = encode_certificate(crl)
+ config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem))
+ else:
+ print(f'Failed to migrate CRL on openvpn interface {interface}')
+
+ config.delete(x509_base + ['crl-file'])
+
+ if config.exists(x509_base + ['cert-file']):
+ if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+ cert_file = config.return_value(x509_base + ['cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on openvpn interface {interface}')
+
+ config.delete(x509_base + ['cert-file'])
+
+ if config.exists(x509_base + ['key-file']):
+ key_file = config.return_value(x509_base + ['key-file'])
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=None, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=None)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+ else:
+ print(f'Failed to migrate private key on openvpn interface {interface}')
+
+ config.delete(x509_base + ['key-file'])
+
+ if config.exists(x509_base + ['dh-file']):
+ if not config.exists(pki_base + ['dh']):
+ config.set(pki_base + ['dh'])
+ config.set_tag(pki_base + ['dh'])
+
+ dh_file = config.return_value(x509_base + ['dh-file'])
+ dh_path = os.path.join(AUTH_DIR, dh_file)
+ dh = None
+
+ if os.path.isfile(dh_path):
+ if not os.access(dh_path, os.R_OK):
+ run(f'sudo chmod 644 {dh_path}')
+
+ with open(dh_path, 'r') as f:
+ dh_data = f.read()
+ dh = load_dh_parameters(dh_data, wrap_tags=False)
+
+ if dh:
+ dh_pem = encode_dh_parameters(dh)
+ config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem))
+ config.set(x509_base + ['dh-params'], value=pki_name)
+ else:
+ print(f'Failed to migrate DH parameters on openvpn interface {interface}')
+
+ config.delete(x509_base + ['dh-file'])
+
+# Wireguard
+base = ['interfaces', 'wireguard']
+
+if config.exists(base):
+ for interface in config.list_nodes(base):
+ private_key_path = base + [interface, 'private-key']
+
+ key_file = 'default'
+ if config.exists(private_key_path):
+ key_file = config.return_value(private_key_path)
+
+ full_key_path = f'/config/auth/wireguard/{key_file}/private.key'
+
+ if not os.path.exists(full_key_path):
+ print(f'Could not find wireguard private key for migration on interface "{interface}"')
+ continue
+
+ with open(full_key_path, 'r') as f:
+ key_data = f.read().strip()
+ config.set(private_key_path, value=key_data)
+
+ for peer in config.list_nodes(base + [interface, 'peer']):
+ config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key')
+
+# Ethernet EAPoL
+base = ['interfaces', 'ethernet']
+
+if config.exists(base):
+ for interface in config.list_nodes(base):
+ if not config.exists(base + [interface, 'eapol']):
+ continue
+
+ x509_base = base + [interface, 'eapol']
+ pki_name = f'eapol_{interface}'
+
+ if config.exists(x509_base + ['ca-cert-file']):
+ if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on eapol config for interface {interface}')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+ if config.exists(x509_base + ['cert-file']):
+ if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+ cert_file = config.return_value(x509_base + ['cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on eapol config for interface {interface}')
+
+ config.delete(x509_base + ['cert-file'])
+
+ if config.exists(x509_base + ['key-file']):
+ key_file = config.return_value(x509_base + ['key-file'])
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=None, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=None)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+ else:
+ print(f'Failed to migrate private key on eapol config for interface {interface}')
+
+ config.delete(x509_base + ['key-file'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/ospf/0-to-1 b/src/migration-scripts/ospf/0-to-1
new file mode 100755
index 000000000..678569d9e
--- /dev/null
+++ b/src/migration-scripts/ospf/0-to-1
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is 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/>.
+
+# T3753: upgrade to FRR8 and move CLI options to better fit with the new FRR CLI
+
+from sys import argv
+from vyos.configtree import ConfigTree
+
+def ospf_passive_migration(config, ospf_base):
+ if config.exists(ospf_base):
+ if config.exists(ospf_base + ['passive-interface']):
+ default = False
+ for interface in config.return_values(ospf_base + ['passive-interface']):
+ if interface == 'default':
+ default = True
+ continue
+ config.set(ospf_base + ['interface', interface, 'passive'])
+
+ config.delete(ospf_base + ['passive-interface'])
+ config.set(ospf_base + ['passive-interface'], value='default')
+
+ if config.exists(ospf_base + ['passive-interface-exclude']):
+ for interface in config.return_values(ospf_base + ['passive-interface-exclude']):
+ config.set(ospf_base + ['interface', interface, 'passive', 'disable'])
+ config.delete(ospf_base + ['passive-interface-exclude'])
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+ospfv3_base = ['protocols', 'ospfv3']
+if config.exists(ospfv3_base):
+ area_base = ospfv3_base + ['area']
+ if config.exists(area_base):
+ for area in config.list_nodes(area_base):
+ if not config.exists(area_base + [area, 'interface']):
+ continue
+
+ for interface in config.return_values(area_base + [area, 'interface']):
+ config.set(ospfv3_base + ['interface', interface, 'area'], value=area)
+ config.set_tag(ospfv3_base + ['interface'])
+
+ config.delete(area_base + [area, 'interface'])
+
+# Migrate OSPF syntax in default VRF
+ospf_base = ['protocols', 'ospf']
+ospf_passive_migration(config, ospf_base)
+
+vrf_base = ['vrf', 'name']
+if config.exists(vrf_base):
+ for vrf in config.list_nodes(vrf_base):
+ vrf_ospf_base = vrf_base + [vrf, 'protocols', 'ospf']
+ if config.exists(vrf_ospf_base):
+ ospf_passive_migration(config, vrf_ospf_base)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print(f'Failed to save the modified config: {e}')
+ exit(1)
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py
index a773aa28e..ffc574362 100755
--- a/src/op_mode/connect_disconnect.py
+++ b/src/op_mode/connect_disconnect.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -17,21 +17,19 @@
import os
import argparse
-from sys import exit
from psutil import process_iter
-from time import strftime, localtime, time
from vyos.util import call
+from vyos.util import DEVNULL
+from vyos.util import is_wwan_connected
-def check_interface(interface):
+def check_ppp_interface(interface):
if not os.path.isfile(f'/etc/ppp/peers/{interface}'):
- print(f'Interface {interface}: invalid!')
+ print(f'Interface {interface} does not exist!')
exit(1)
def check_ppp_running(interface):
- """
- Check if ppp process is running in the interface in question
- """
+ """ Check if PPP process is running in the interface in question """
for p in process_iter():
if "pppd" in p.name():
if interface in p.cmdline():
@@ -40,32 +38,46 @@ def check_ppp_running(interface):
return False
def connect(interface):
- """
- Connect PPP interface
- """
- check_interface(interface)
+ """ Connect dialer interface """
- # Check if interface is already dialed
- if os.path.isdir(f'/sys/class/net/{interface}'):
- print(f'Interface {interface}: already connected!')
- elif check_ppp_running(interface):
- print(f'Interface {interface}: connection is beeing established!')
+ if interface.startswith('ppp'):
+ check_ppp_interface(interface)
+ # Check if interface is already dialed
+ if os.path.isdir(f'/sys/class/net/{interface}'):
+ print(f'Interface {interface}: already connected!')
+ elif check_ppp_running(interface):
+ print(f'Interface {interface}: connection is beeing established!')
+ else:
+ print(f'Interface {interface}: connecting...')
+ call(f'systemctl restart ppp@{interface}.service')
+ elif interface.startswith('wwan'):
+ if is_wwan_connected(interface):
+ print(f'Interface {interface}: already connected!')
+ else:
+ call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py')
else:
- print(f'Interface {interface}: connecting...')
- call(f'systemctl restart ppp@{interface}.service')
+ print(f'Unknown interface {interface}, can not connect. Aborting!')
def disconnect(interface):
- """
- Disconnect PPP interface
- """
- check_interface(interface)
+ """ Disconnect dialer interface """
- # Check if interface is already down
- if not check_ppp_running(interface):
- print(f'Interface {interface}: connection is already down')
+ if interface.startswith('ppp'):
+ check_ppp_interface(interface)
+
+ # Check if interface is already down
+ if not check_ppp_running(interface):
+ print(f'Interface {interface}: connection is already down')
+ else:
+ print(f'Interface {interface}: disconnecting...')
+ call(f'systemctl stop ppp@{interface}.service')
+ elif interface.startswith('wwan'):
+ if not is_wwan_connected(interface):
+ print(f'Interface {interface}: connection is already down')
+ else:
+ modem = interface.lstrip('wwan')
+ call(f'mmcli --modem {modem} --simple-disconnect', stdout=DEVNULL)
else:
- print(f'Interface {interface}: disconnecting...')
- call(f'systemctl stop ppp@{interface}.service')
+ print(f'Unknown interface {interface}, can not disconnect. Aborting!')
def main():
parser = argparse.ArgumentParser()
diff --git a/src/op_mode/force_root-partition-auto-resize.sh b/src/op_mode/force_root-partition-auto-resize.sh
new file mode 100755
index 000000000..b39e87560
--- /dev/null
+++ b/src/op_mode/force_root-partition-auto-resize.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# ROOT_PART_DEV – root partition device path
+# ROOT_PART_NAME – root partition device name
+# ROOT_DEV_NAME – disk device name
+# ROOT_DEV – disk device path
+# ROOT_PART_NUM – number of root partition on disk
+# ROOT_DEV_SIZE – disk total size in 512 bytes sectors
+# ROOT_PART_SIZE – root partition total size in 512 bytes sectors
+# ROOT_PART_START – number of 512 bytes sector where root partition starts
+# AVAILABLE_EXTENSION_SIZE – calculation available disk space after root partition in 512 bytes sectors
+ROOT_PART_DEV=$(findmnt /usr/lib/live/mount/persistence -o source -n)
+ROOT_PART_NAME=$(echo "$ROOT_PART_DEV" | cut -d "/" -f 3)
+ROOT_DEV_NAME=$(echo /sys/block/*/"${ROOT_PART_NAME}" | cut -d "/" -f 4)
+ROOT_DEV="/dev/${ROOT_DEV_NAME}"
+ROOT_PART_NUM=$(cat "/sys/block/${ROOT_DEV_NAME}/${ROOT_PART_NAME}/partition")
+ROOT_DEV_SIZE=$(cat "/sys/block/${ROOT_DEV_NAME}/size")
+ROOT_PART_SIZE=$(cat "/sys/block/${ROOT_DEV_NAME}/${ROOT_PART_NAME}/size")
+ROOT_PART_START=$(cat "/sys/block/${ROOT_DEV_NAME}/${ROOT_PART_NAME}/start")
+AVAILABLE_EXTENSION_SIZE=$((ROOT_DEV_SIZE - ROOT_PART_START - ROOT_PART_SIZE - 8))
+
+#
+# Check if device have space for root partition growing up.
+#
+if [ $AVAILABLE_EXTENSION_SIZE -lt 1 ]; then
+ echo "There is no available space for root partition extension"
+ exit 0;
+fi
+
+#
+# Resize the partition and grow the filesystem.
+#
+# "print" and "Fix" directives were added to fix GPT table if it corrupted after virtual drive extension.
+# If GPT table is corrupted we'll get Fix/Ignore dialogue after "print" command.
+# "Fix" will be the answer for this dialogue.
+# If GPT table is fine and no auto-fix dialogue appeared the directive "Fix" simply will print parted utility help info.
+parted -m ${ROOT_DEV} ---pretend-input-tty > /dev/null 2>&1 <<EOF
+print
+Fix
+resizepart
+${ROOT_PART_NUM}
+Yes
+100%
+EOF
+partprobe > /dev/null 2>&1
+resize2fs ${ROOT_PART_DEV} > /dev/null 2>&1
diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py
index df4486bce..b3ba44e87 100755
--- a/src/op_mode/format_disk.py
+++ b/src/op_mode/format_disk.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -17,11 +17,10 @@
import argparse
import os
import re
-import sys
+
from datetime import datetime
-from time import sleep
-from vyos.util import is_admin, ask_yes_no
+from vyos.util import ask_yes_no
from vyos.util import call
from vyos.util import cmd
from vyos.util import DEVNULL
@@ -38,16 +37,17 @@ def list_disks():
def is_busy(disk: str):
"""Check if given disk device is busy by re-reading it's partition table"""
- return call(f'sudo blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0
+ return call(f'blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0
def backup_partitions(disk: str):
"""Save sfdisk partitions output to a backup file"""
- device_path = '/dev/' + disk
- backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M')
- backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts)
- cmd(f'sudo /sbin/sfdisk -d {device_path} > {backup_file}')
+ device_path = f'/dev/{disk}'
+ backup_ts = datetime.now().strftime('%Y%m%d-%H%M')
+ backup_file = f'/var/tmp/backup_{disk}.{backup_ts}'
+ call(f'sfdisk -d {device_path} > {backup_file}')
+ print(f'Partition table backup saved to {backup_file}')
def list_partitions(disk: str):
@@ -65,11 +65,11 @@ def list_partitions(disk: str):
def delete_partition(disk: str, partition_idx: int):
- cmd(f'sudo /sbin/parted /dev/{disk} rm {partition_idx}')
+ cmd(f'parted /dev/{disk} rm {partition_idx}')
def format_disk_like(target: str, proto: str):
- cmd(f'sudo /sbin/sfdisk -d /dev/{proto} | sudo /sbin/sfdisk --force /dev/{target}')
+ cmd(f'sfdisk -d /dev/{proto} | sfdisk --force /dev/{target}')
if __name__ == '__main__':
@@ -79,10 +79,6 @@ if __name__ == '__main__':
group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference')
args = parser.parse_args()
- if not is_admin():
- print('Must be admin or root to format disk')
- sys.exit(1)
-
target_disk = args.target
eligible_target_disks = list_disks()
@@ -90,54 +86,48 @@ if __name__ == '__main__':
eligible_proto_disks = eligible_target_disks.copy()
eligible_proto_disks.remove(target_disk)
- fmt = {
- 'target_disk': target_disk,
- 'proto_disk': proto_disk,
- }
-
if proto_disk == target_disk:
print('The two disk drives must be different.')
- sys.exit(1)
+ exit(1)
- if not os.path.exists('/dev/' + proto_disk):
- print('Device /dev/{proto_disk} does not exist'.format_map(fmt))
- sys.exit(1)
+ if not os.path.exists(f'/dev/{proto_disk}'):
+ print(f'Device /dev/{proto_disk} does not exist')
+ exit(1)
if not os.path.exists('/dev/' + target_disk):
- print('Device /dev/{target_disk} does not exist'.format_map(fmt))
- sys.exit(1)
+ print(f'Device /dev/{target_disk} does not exist')
+ exit(1)
if target_disk not in eligible_target_disks:
- print('Device {target_disk} can not be formatted'.format_map(fmt))
- sys.exit(1)
+ print(f'Device {target_disk} can not be formatted')
+ exit(1)
if proto_disk not in eligible_proto_disks:
- print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt))
- sys.exit(1)
+ print(f'Device {proto_disk} can not be used as a prototype for {target_disk}')
+ exit(1)
if is_busy(target_disk):
- print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt))
- sys.exit(1)
+ print(f'Disk device {target_disk} is busy, unable to format')
+ exit(1)
- print('This will re-format disk {target_disk} so that it has the same disk\n'
- 'partion sizes and offsets as {proto_disk}. This will not copy\n'
- 'data from {proto_disk} to {target_disk}. But this will erase all\n'
- 'data on {target_disk}.\n'.format_map(fmt))
+ print(f'\nThis will re-format disk {target_disk} so that it has the same disk'
+ f'\npartion sizes and offsets as {proto_disk}. This will not copy'
+ f'\ndata from {proto_disk} to {target_disk}. But this will erase all'
+ f'\ndata on {target_disk}.\n')
- if not ask_yes_no("Do you wish to proceed?"):
- print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt))
- sys.exit(0)
+ if not ask_yes_no('Do you wish to proceed?'):
+ print(f'Disk drive {target_disk} will not be re-formated')
+ exit(0)
- print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt))
+ print(f'Re-formating disk drive {target_disk}...')
print('Making backup copy of partitions...')
backup_partitions(target_disk)
- sleep(1)
print('Deleting old partitions...')
for p in list_partitions(target_disk):
delete_partition(disk=target_disk, partition_idx=p)
- print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt))
+ print(f'Creating new partitions on {target_disk} based on {proto_disk}...')
format_disk_like(target=target_disk, proto=proto_disk)
- print('Done.')
+ print('Done!')
diff --git a/src/op_mode/generate_ipsec_debug_archive.sh b/src/op_mode/generate_ipsec_debug_archive.sh
new file mode 100755
index 000000000..53d0a6eaa
--- /dev/null
+++ b/src/op_mode/generate_ipsec_debug_archive.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+# Collecting IPSec Debug Information
+
+DATE=`date +%d-%m-%Y`
+
+a_CMD=(
+ "sudo ipsec status"
+ "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"
+ "sudo ip route show table 220"
+ )
+
+
+echo "DEBUG: ${DATE} on host \"$(hostname)\"" > /tmp/ipsec-status-${DATE}.txt
+date >> /tmp/ipsec-status-${DATE}.txt
+
+# Execute all DEBUG commands and save it to file
+for cmd in "${a_CMD[@]}"; do
+ echo -e "\n### ${cmd} ###" >> /tmp/ipsec-status-${DATE}.txt
+ ${cmd} >> /tmp/ipsec-status-${DATE}.txt 2>/dev/null
+done
+
+# Collect charon logs, build .tgz archive
+sudo journalctl /usr/lib/ipsec/charon > /tmp/journalctl-charon-${DATE}.txt && \
+sudo tar -zcvf /tmp/ipsec-debug-${DATE}.tgz /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt >& /dev/null
+sudo rm -f /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt
+
+echo "Debug file is generated and located in /tmp/ipsec-debug-${DATE}.tgz"
diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py
index 731e71891..b9ebc991a 100755
--- a/src/op_mode/lldp_op.py
+++ b/src/op_mode/lldp_op.py
@@ -55,6 +55,9 @@ def parse_data(data, interface):
if interface is not None and local_if != interface:
continue
for chassis, c_value in values.get('chassis', {}).items():
+ # bail out early if no capabilities found
+ if 'capability' not in c_value:
+ continue
capabilities = c_value['capability']
if isinstance(capabilities, dict):
capabilities = [capabilities]
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index 2283cd820..bc7813052 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -35,6 +35,7 @@ from vyos.pki import verify_certificate
from vyos.xml import defaults
from vyos.util import ask_input, ask_yes_no
from vyos.util import cmd
+from vyos.util import install_into_config
CERT_REQ_END = '-----END CERTIFICATE REQUEST-----'
auth_dir = '/config/auth'
@@ -142,48 +143,50 @@ def get_revoked_by_serial_numbers(serial_numbers=[]):
return certs_out
def install_certificate(name, cert='', private_key=None, key_type=None, key_passphrase=None, is_ca=False):
- # Show conf commands for installing certificate
+ # Show/install conf commands for certificate
prefix = 'ca' if is_ca else 'certificate'
- print('Configure mode commands to install:')
- base = f"set pki {prefix} {name}"
+ base = f"pki {prefix} {name}"
+ config_paths = []
if cert:
cert_pem = "".join(encode_certificate(cert).strip().split("\n")[1:-1])
- print(f"{base} certificate '{cert_pem}'")
+ config_paths.append(f"{base} certificate '{cert_pem}'")
if private_key:
key_pem = "".join(encode_private_key(private_key, passphrase=key_passphrase).strip().split("\n")[1:-1])
- print(f"{base} private key '{key_pem}'")
+ config_paths.append(f"{base} private key '{key_pem}'")
if key_passphrase:
- print(f"{base} private password-protected")
+ config_paths.append(f"{base} private password-protected")
+
+ install_into_config(conf, config_paths)
def install_crl(ca_name, crl):
- # Show conf commands for installing crl
- print("Configure mode commands to install CRL:")
+ # Show/install conf commands for crl
crl_pem = "".join(encode_certificate(crl).strip().split("\n")[1:-1])
- print(f"set pki ca {ca_name} crl '{crl_pem}'")
+ install_into_config(conf, [f"pki ca {ca_name} crl '{crl_pem}'"])
def install_dh_parameters(name, params):
- # Show conf commands for installing dh params
- print("Configure mode commands to install DH parameters:")
+ # Show/install conf commands for dh params
dh_pem = "".join(encode_dh_parameters(params).strip().split("\n")[1:-1])
- print(f"set pki dh {name} parameters '{dh_pem}'")
+ install_into_config(conf, [f"pki dh {name} parameters '{dh_pem}'"])
def install_ssh_key(name, public_key, private_key, passphrase=None):
- # Show conf commands for installing ssh key
+ # Show/install conf commands for ssh key
key_openssh = encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH')
username = os.getlogin()
type_key_split = key_openssh.split(" ")
- base = f"set system login user {username} authentication public-keys {name}"
- print("Configure mode commands to install SSH key:")
- print(f"{base} key '{type_key_split[1]}'")
- print(f"{base} type '{type_key_split[0]}'", end="\n\n")
+ base = f"system login user {username} authentication public-keys {name}"
+ install_into_config(conf, [
+ f"{base} key '{type_key_split[1]}'",
+ f"{base} type '{type_key_split[0]}'"
+ ])
print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase))
def install_keypair(name, key_type, private_key=None, public_key=None, passphrase=None):
- # Show conf commands for installing key-pair
- print("Configure mode commands to install key pair:")
+ # Show/install conf commands for key-pair
+
+ config_paths = []
if public_key:
install_public_key = ask_yes_no('Do you want to install the public key?', default=True)
@@ -191,7 +194,7 @@ def install_keypair(name, key_type, private_key=None, public_key=None, passphras
if install_public_key:
install_public_pem = "".join(public_key_pem.strip().split("\n")[1:-1])
- print(f"set pki key-pair {name} public key '{install_public_pem}'")
+ config_paths.append(f"pki key-pair {name} public key '{install_public_pem}'")
else:
print("Public key:")
print(public_key_pem)
@@ -202,13 +205,15 @@ def install_keypair(name, key_type, private_key=None, public_key=None, passphras
if install_private_key:
install_private_pem = "".join(private_key_pem.strip().split("\n")[1:-1])
- print(f"set pki key-pair {name} private key '{install_private_pem}'")
+ config_paths.append(f"pki key-pair {name} private key '{install_private_pem}'")
if passphrase:
- print(f"set pki key-pair {name} private password-protected")
+ config_paths.append(f"pki key-pair {name} private password-protected")
else:
print("Private key:")
print(private_key_pem)
+ install_into_config(conf, config_paths)
+
def install_wireguard_key(interface, private_key, public_key):
# Show conf commands for installing wireguard key pairs
from vyos.ifconfig import Section
@@ -217,20 +222,10 @@ def install_wireguard_key(interface, private_key, public_key):
exit(1)
# Check if we are running in a config session - if yes, we can directly write to the CLI
- cli_string = f"interfaces wireguard {interface} private-key '{private_key}'"
- if Config().in_session():
- cmd(f"/opt/vyatta/sbin/my_set {cli_string}")
-
- print('"generate" CLI command executed from config session.\nGenerated private-key was imported to CLI!',end='\n\n')
- print(f'Use the following command to verify: show interfaces wireguard {interface}')
- else:
- print('"generate" CLI command executed from operational level.\n'
- 'Generated private-key is not stored to CLI, use configure mode commands to install key:', end='\n\n')
- print(f"set {cli_string}", end="\n\n")
+ install_into_config(conf, [f"interfaces wireguard {interface} private-key '{private_key}'"])
print(f"Corresponding public-key to use on peer system is: '{public_key}'")
-
def install_wireguard_psk(interface, peer, psk):
from vyos.ifconfig import Section
if Section.section(interface) != 'wireguard':
@@ -238,17 +233,7 @@ def install_wireguard_psk(interface, peer, psk):
exit(1)
# Check if we are running in a config session - if yes, we can directly write to the CLI
- cli_string = f"interfaces wireguard {interface} peer {peer} preshared-key '{psk}'"
- if Config().in_session():
- cmd(f"/opt/vyatta/sbin/my_set {cli_string}")
-
- print('"generate" CLI command executed from config session.\nGenerated preshared-key was imported to CLI!',end='\n\n')
- print(f'Use the following command to verify: show interfaces wireguard {interface}')
- else:
- print('"generate" CLI command executed from operational level.\n'
- 'Generated preshared-key is not stored to CLI, use configure mode commands to install key:', end='\n\n')
- print(f"set {cli_string}", end="\n\n")
-
+ install_into_config(conf, [f"interfaces wireguard {interface} peer {peer} preshared-key '{psk}'"])
def ask_passphrase():
passphrase = None
diff --git a/src/op_mode/ppp-server-ctrl.py b/src/op_mode/ppp-server-ctrl.py
index 670cdf879..e93963fdd 100755
--- a/src/op_mode/ppp-server-ctrl.py
+++ b/src/op_mode/ppp-server-ctrl.py
@@ -60,7 +60,7 @@ def main():
output, err = popen(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][args.proto]) + args.action + ses_pattern, stderr=DEVNULL, decode='utf-8')
if not err:
try:
- print(output)
+ print(f' {output}')
except:
sys.exit(0)
else:
diff --git a/src/op_mode/show_configuration_json.py b/src/op_mode/show_configuration_json.py
new file mode 100755
index 000000000..fdece533b
--- /dev/null
+++ b/src/op_mode/show_configuration_json.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import json
+
+from vyos.configquery import ConfigTreeQuery
+
+
+config = ConfigTreeQuery()
+c = config.get_config_dict()
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-p", "--pretty", action="store_true", help="Show pretty configuration in JSON format")
+
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ if args.pretty:
+ print(json.dumps(c, indent=4))
+ else:
+ print(json.dumps(c))
diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py
index 3d50eb938..eac068274 100755
--- a/src/op_mode/show_interfaces.py
+++ b/src/op_mode/show_interfaces.py
@@ -94,10 +94,8 @@ def split_text(text, used=0):
used: number of characted already used in the screen
"""
no_tty = call('tty -s')
- if no_tty:
- return text.split()
- returned = cmd('stty size')
+ returned = cmd('stty size') if not no_tty else ''
if len(returned) == 2:
rows, columns = [int(_) for _ in returned]
else:
diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py
index c964caaeb..e72f0f965 100755
--- a/src/op_mode/show_ipsec_sa.py
+++ b/src/op_mode/show_ipsec_sa.py
@@ -46,7 +46,6 @@ def format_output(conns, sas):
if parent_sa["state"] == b"ESTABLISHED" and installed_sas:
state = "up"
- uptime = vyos.util.seconds_to_human(parent_sa["established"].decode())
remote_host = parent_sa["remote-host"].decode()
remote_id = parent_sa["remote-id"].decode()
@@ -75,6 +74,8 @@ def format_output(conns, sas):
# Remove B from <1K values
pkts_str = re.sub(r'B', r'', pkts_str)
+ uptime = vyos.util.seconds_to_human(isa['install-time'].decode())
+
enc = isa["encr-alg"].decode()
if "encr-keysize" in isa:
key_size = isa["encr-keysize"].decode()
diff --git a/src/op_mode/show_openvpn_mfa.py b/src/op_mode/show_openvpn_mfa.py
new file mode 100755
index 000000000..1ab54600c
--- /dev/null
+++ b/src/op_mode/show_openvpn_mfa.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+# Copyright 2017, 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import socket
+import urllib.parse
+import argparse
+
+from vyos.util import popen
+
+otp_file = '/config/auth/openvpn/{interface}-otp-secrets'
+
+def get_mfa_secret(interface, client):
+ try:
+ with open(otp_file.format(interface=interface), "r") as f:
+ users = f.readlines()
+ for user in users:
+ if re.search('^' + client + ' ', user):
+ return user.split(':')[3]
+ except:
+ pass
+
+def get_mfa_uri(client, secret):
+ hostname = socket.gethostname()
+ fqdn = socket.getfqdn()
+ uri = 'otpauth://totp/{hostname}:{client}@{fqdn}?secret={secret}'
+
+ return urllib.parse.quote(uri.format(hostname=hostname, client=client, fqdn=fqdn, secret=secret), safe='/:@?=')
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(add_help=False, description='Show two-factor authentication information')
+ parser.add_argument('--intf', action="store", type=str, default='', help='only show the specified interface')
+ parser.add_argument('--user', action="store", type=str, default='', help='only show the specified users')
+ parser.add_argument('--action', action="store", type=str, default='show', help='action to perform')
+
+ args = parser.parse_args()
+ secret = get_mfa_secret(args.intf, args.user)
+
+ if args.action == "secret" and secret:
+ print(secret)
+
+ if args.action == "uri" and secret:
+ uri = get_mfa_uri(args.user, secret)
+ print(uri)
+
+ if args.action == "qrcode" and secret:
+ uri = get_mfa_uri(args.user, secret)
+ qrcode,err = popen('qrencode -t ansiutf8', input=uri)
+ print(qrcode)
+
diff --git a/src/op_mode/show_ram.py b/src/op_mode/show_ram.py
new file mode 100755
index 000000000..5818ec132
--- /dev/null
+++ b/src/op_mode/show_ram.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is 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/>.
+#
+
+def get_system_memory():
+ from re import search as re_search
+
+ def find_value(keyword, mem_data):
+ regex = keyword + ':\s+(\d+)'
+ res = re_search(regex, mem_data).group(1)
+ return int(res)
+
+ with open("/proc/meminfo", "r") as f:
+ mem_data = f.read()
+
+ total = find_value('MemTotal', mem_data)
+ available = find_value('MemAvailable', mem_data)
+ buffers = find_value('Buffers', mem_data)
+ cached = find_value('Cached', mem_data)
+
+ used = total - available
+
+ res = {
+ "total": total,
+ "free": available,
+ "used": used,
+ "buffers": buffers,
+ "cached": cached
+ }
+
+ return res
+
+def get_system_memory_human():
+ from vyos.util import bytes_to_human
+
+ mem = get_system_memory()
+
+ for key in mem:
+ # The Linux kernel exposes memory values in kilobytes,
+ # so we need to normalize them
+ mem[key] = bytes_to_human(mem[key], initial_exponent=10)
+
+ return mem
+
+if __name__ == '__main__':
+ mem = get_system_memory_human()
+
+ print("Total: {}".format(mem["total"]))
+ print("Free: {}".format(mem["free"]))
+ print("Used: {}".format(mem["used"]))
+
diff --git a/src/op_mode/show_ram.sh b/src/op_mode/show_ram.sh
deleted file mode 100755
index b013e16f8..000000000
--- a/src/op_mode/show_ram.sh
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/bin/bash
-#
-# Module: vyos-show-ram.sh
-# Displays memory usage information in minimalistic format
-#
-# Copyright (C) 2019 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-MB_DIVISOR=1024
-
-TOTAL=$(cat /proc/meminfo | grep -E "^MemTotal:" | awk -F ' ' '{print $2}')
-FREE=$(cat /proc/meminfo | grep -E "^MemFree:" | awk -F ' ' '{print $2}')
-BUFFERS=$(cat /proc/meminfo | grep -E "^Buffers:" | awk -F ' ' '{print $2}')
-CACHED=$(cat /proc/meminfo | grep -E "^Cached:" | awk -F ' ' '{print $2}')
-
-DISPLAY_FREE=$(( ($FREE + $BUFFERS + $CACHED) / $MB_DIVISOR ))
-DISPLAY_TOTAL=$(( $TOTAL / $MB_DIVISOR ))
-DISPLAY_USED=$(( $DISPLAY_TOTAL - $DISPLAY_FREE ))
-
-echo "Total: $DISPLAY_TOTAL"
-echo "Free: $DISPLAY_FREE"
-echo "Used: $DISPLAY_USED"
diff --git a/src/op_mode/show_uptime.py b/src/op_mode/show_uptime.py
new file mode 100755
index 000000000..c3dea52e6
--- /dev/null
+++ b/src/op_mode/show_uptime.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+def get_uptime_seconds():
+ from re import search
+ from vyos.util import read_file
+
+ data = read_file("/proc/uptime")
+ seconds = search("([0-9\.]+)\s", data).group(1)
+
+ return int(float(seconds))
+
+def get_load_averages():
+ from re import search
+ from vyos.util import cmd
+
+ data = cmd("uptime")
+ matches = search(r"load average:\s*(?P<one>[0-9\.]+)\s*,\s*(?P<five>[0-9\.]+)\s*,\s*(?P<fifteen>[0-9\.]+)\s*", data)
+
+ res = {}
+ res[1] = float(matches["one"])
+ res[5] = float(matches["five"])
+ res[15] = float(matches["fifteen"])
+
+ return res
+
+if __name__ == '__main__':
+ from vyos.util import seconds_to_human
+
+ print("Uptime: {}\n".format(seconds_to_human(get_uptime_seconds())))
+
+ avgs = get_load_averages()
+
+ print("Load averages:")
+ print("1 minute: {:.02f}%".format(avgs[1]*100))
+ print("5 minutes: {:.02f}%".format(avgs[5]*100))
+ print("15 minutes: {:.02f}%".format(avgs[15]*100))
diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py
index 06e227ccf..40854fa8f 100755
--- a/src/op_mode/vpn_ipsec.py
+++ b/src/op_mode/vpn_ipsec.py
@@ -48,7 +48,7 @@ def reset_peer(peer, tunnel):
result = True
for conn in conns:
try:
- call(f'sudo /usr/sbin/ipsec down {conn}', timeout = 10)
+ call(f'sudo /usr/sbin/ipsec down {conn}{{*}}', timeout = 10)
call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10)
except TimeoutExpired as e:
print(f'Timed out while resetting {conn}')
diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql
index a04138010..a3c30b005 100644
--- a/src/services/api/graphql/README.graphql
+++ b/src/services/api/graphql/README.graphql
@@ -10,7 +10,7 @@ to run with that address as default router by requesting these 'mutations'
in the GraphQL playground:
mutation {
- createInterfaceEthernet (data: {interface: "eth1",
+ CreateInterfaceEthernet (data: {interface: "eth1",
address: "192.168.0.1/24",
description: "BOB"}) {
success
@@ -22,10 +22,10 @@ mutation {
}
mutation {
- createDhcpServer(data: {sharedNetworkName: "BOB",
+ CreateDhcpServer(data: {sharedNetworkName: "BOB",
subnet: "192.168.0.0/24",
defaultRouter: "192.168.0.1",
- dnsServer: "192.168.0.1",
+ nameServer: "192.168.0.1",
domainName: "vyos.net",
lease: 86400,
range: 0,
@@ -42,6 +42,52 @@ mutation {
}
}
+To save the configuration, use the following mutation:
+
+mutation {
+ SaveConfigFile(data: {fileName: "/config/config.boot"}) {
+ success
+ errors
+ data {
+ fileName
+ }
+ }
+}
+
+N.B. fileName can be empty (fileName: "") or data can be empty (data: {}) to
+save to /config/config.boot; to save to an alternative path, specify
+fileName.
+
+Similarly, using the same 'endpoint' (meaning the form of the request and
+resolver; the actual enpoint for all GraphQL requests is
+https://hostname/graphql), one can load an arbitrary config file from a
+path.
+
+mutation {
+ LoadConfigFile(data: {fileName: "/home/vyos/config.boot"}) {
+ success
+ errors
+ data {
+ fileName
+ }
+ }
+}
+
+Op-mode 'show' commands may be requested by path, e.g.:
+
+mutation {
+ Show (data: {path: ["interfaces", "ethernet", "detail"]}) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+}
+
+N.B. to see the output the 'data' field 'result' must be present in the
+request.
+
The GraphQL playground will be found at:
https://{{ host_address }}/graphql
@@ -57,22 +103,30 @@ What's here:
services
├── api
│   └── graphql
+│   ├── bindings.py
│   ├── graphql
│   │   ├── directives.py
│   │   ├── __init__.py
│   │   ├── mutations.py
│   │   └── schema
+│   │   ├── config_file.graphql
│   │   ├── dhcp_server.graphql
+│   │   ├── firewall_group.graphql
│   │   ├── interface_ethernet.graphql
-│   │   └── schema.graphql
+│   │   ├── schema.graphql
+│   │   ├── show_config.graphql
+│   │   └── show.graphql
+│   ├── README.graphql
│   ├── recipes
-│   │   ├── dhcp_server.py
│   │   ├── __init__.py
-│   │   ├── interface_ethernet.py
-│   │   ├── recipe.py
+│   │   ├── remove_firewall_address_group_members.py
+│   │   ├── session.py
│   │   └── templates
-│   │   ├── dhcp_server.tmpl
-│   │   └── interface_ethernet.tmpl
+│   │   ├── create_dhcp_server.tmpl
+│   │   ├── create_firewall_address_group.tmpl
+│   │   ├── create_interface_ethernet.tmpl
+│   │   ├── remove_firewall_address_group_members.tmpl
+│   │   └── update_firewall_address_group_members.tmpl
│   └── state.py
├── vyos-configd
├── vyos-hostsd
@@ -90,13 +144,14 @@ the Ur-data; the GraphQL schema is produced from those files, located in
Resolvers for the schema Mutation fields are dynamically generated using a
'directive' added to the respective schema field. The directive,
-'@generate', is handled by the class 'DataDirective' in
-'api/graphql/graphql/directives.py', which calls the 'make_resolver' function in
-'api/graphql/graphql/mutations.py'; the produced resolver calls the appropriate
-wrapper in 'api/graphql/recipes', with base class doing the (overridable)
-configuration steps of calling all defined 'set'/'delete' commands.
-
-Integrating the above with vyos-http-api-server is ~10 lines of code.
+'@configure', is handled by the class 'ConfigureDirective' in
+'api/graphql/graphql/directives.py', which calls the
+'make_configure_resolver' function in 'api/graphql/graphql/mutations.py';
+the produced resolver calls the appropriate wrapper in
+'api/graphql/recipes', with base class doing the (overridable) configuration
+steps of calling all defined 'set'/'delete' commands.
+
+Integrating the above with vyos-http-api-server is 4 lines of code.
What needs to be done:
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py
new file mode 100644
index 000000000..1fbe13d0c
--- /dev/null
+++ b/src/services/api/graphql/bindings.py
@@ -0,0 +1,13 @@
+import vyos.defaults
+from . graphql.mutations import mutation
+from . graphql.directives import directives_dict
+from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
+
+def generate_schema():
+ api_schema_dir = vyos.defaults.directories['api_schema']
+
+ type_defs = load_schema_from_path(api_schema_dir)
+
+ schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives=directives_dict)
+
+ return schema
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
index 651421c35..10bc522db 100644
--- a/src/services/api/graphql/graphql/directives.py
+++ b/src/services/api/graphql/graphql/directives.py
@@ -1,12 +1,11 @@
from ariadne import SchemaDirectiveVisitor, ObjectType
-from . mutations import make_resolver
+from . mutations import *
-class DataDirective(SchemaDirectiveVisitor):
- """
- Class providing implementation of 'generate' directive in schema.
+def non(arg):
+ pass
- """
- def visit_field_definition(self, field, object_type):
+class VyosDirective(SchemaDirectiveVisitor):
+ def visit_field_definition(self, field, object_type, make_resolver=non):
name = f'{field.type}'
# field.type contains the return value of the mutation; trim value
# to produce canonical name
@@ -15,3 +14,50 @@ class DataDirective(SchemaDirectiveVisitor):
func = make_resolver(name)
field.resolve = func
return field
+
+
+class ConfigureDirective(VyosDirective):
+ """
+ Class providing implementation of 'configure' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_configure_resolver)
+
+class ShowConfigDirective(VyosDirective):
+ """
+ Class providing implementation of 'show' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_show_config_resolver)
+
+class ConfigFileDirective(VyosDirective):
+ """
+ Class providing implementation of 'configfile' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_config_file_resolver)
+
+class ShowDirective(VyosDirective):
+ """
+ Class providing implementation of 'show' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_show_resolver)
+
+class ImageDirective(VyosDirective):
+ """
+ Class providing implementation of 'image' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_image_resolver)
+
+directives_dict = {"configure": ConfigureDirective,
+ "showconfig": ShowConfigDirective,
+ "configfile": ConfigFileDirective,
+ "show": ShowDirective,
+ "image": ImageDirective}
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
index 98c665c9a..8e5aab56d 100644
--- a/src/services/api/graphql/graphql/mutations.py
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -6,10 +6,11 @@ from graphql import GraphQLResolveInfo
from makefun import with_signature
from .. import state
+from api.graphql.recipes.session import Session
mutation = ObjectType("Mutation")
-def make_resolver(mutation_name):
+def make_resolver(mutation_name, class_name, session_func):
"""Dynamically generate a resolver for the mutation named in the
schema by 'mutation_name'.
@@ -19,11 +20,11 @@ def make_resolver(mutation_name):
functools.wraps.
:raise Exception:
- encapsulating ConfigErrors, or internal errors
+ raising ConfigErrors, or internal errors
"""
- class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1)
+
func_base_name = convert_camel_case_to_snake(class_name)
- resolver_name = f'resolve_create_{func_base_name}'
+ resolver_name = f'resolve_{func_base_name}'
func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'
@mutation.field(mutation_name)
@@ -40,10 +41,18 @@ def make_resolver(mutation_name):
data = kwargs['data']
session = state.settings['app'].state.vyos_session
- mod = import_module(f'api.graphql.recipes.{func_base_name}')
- klass = getattr(mod, class_name)
+ # one may override the session functions with a local subclass
+ try:
+ mod = import_module(f'api.graphql.recipes.{func_base_name}')
+ klass = getattr(mod, class_name)
+ except ImportError:
+ # otherwise, dynamically generate subclass to invoke subclass
+ # name based templates
+ klass = type(class_name, (Session,), {})
k = klass(session, data)
- k.configure()
+ method = getattr(k, session_func)
+ result = method()
+ data['result'] = result
return {
"success": True,
@@ -57,4 +66,34 @@ def make_resolver(mutation_name):
return func_impl
+def make_configure_resolver(mutation_name):
+ class_name = mutation_name
+ return make_resolver(mutation_name, class_name, 'configure')
+
+def make_show_config_resolver(mutation_name):
+ class_name = mutation_name
+ return make_resolver(mutation_name, class_name, 'show_config')
+
+def make_config_file_resolver(mutation_name):
+ if 'Save' in mutation_name:
+ class_name = mutation_name.replace('Save', '', 1)
+ return make_resolver(mutation_name, class_name, 'save')
+ elif 'Load' in mutation_name:
+ class_name = mutation_name.replace('Load', '', 1)
+ return make_resolver(mutation_name, class_name, 'load')
+ else:
+ raise Exception
+
+def make_show_resolver(mutation_name):
+ class_name = mutation_name
+ return make_resolver(mutation_name, class_name, 'show')
+def make_image_resolver(mutation_name):
+ if 'Add' in mutation_name:
+ class_name = mutation_name.replace('Add', '', 1)
+ return make_resolver(mutation_name, class_name, 'add')
+ elif 'Delete' in mutation_name:
+ class_name = mutation_name.replace('Delete', '', 1)
+ return make_resolver(mutation_name, class_name, 'delete')
+ else:
+ raise Exception
diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql
new file mode 100644
index 000000000..31ab26b9e
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/config_file.graphql
@@ -0,0 +1,27 @@
+input SaveConfigFileInput {
+ fileName: String
+}
+
+type SaveConfigFile {
+ fileName: String
+}
+
+type SaveConfigFileResult {
+ data: SaveConfigFile
+ success: Boolean!
+ errors: [String]
+}
+
+input LoadConfigFileInput {
+ fileName: String!
+}
+
+type LoadConfigFile {
+ fileName: String!
+}
+
+type LoadConfigFileResult {
+ data: LoadConfigFile
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
index a7ee75d40..25f091bfa 100644
--- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql
+++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
@@ -1,8 +1,8 @@
-input dhcpServerConfigInput {
+input DhcpServerConfigInput {
sharedNetworkName: String
subnet: String
defaultRouter: String
- dnsServer: String
+ nameServer: String
domainName: String
lease: Int
range: Int
@@ -13,11 +13,11 @@ input dhcpServerConfigInput {
dnsForwardingListenAddress: String
}
-type dhcpServerConfig {
+type DhcpServerConfig {
sharedNetworkName: String
subnet: String
defaultRouter: String
- dnsServer: String
+ nameServer: String
domainName: String
lease: Int
range: Int
@@ -28,8 +28,8 @@ type dhcpServerConfig {
dnsForwardingListenAddress: String
}
-type createDhcpServerResult {
- data: dhcpServerConfig
+type CreateDhcpServerResult {
+ data: DhcpServerConfig
success: Boolean!
errors: [String]
}
diff --git a/src/services/api/graphql/graphql/schema/firewall_group.graphql b/src/services/api/graphql/graphql/schema/firewall_group.graphql
new file mode 100644
index 000000000..efe7de632
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/firewall_group.graphql
@@ -0,0 +1,47 @@
+input CreateFirewallAddressGroupInput {
+ name: String!
+ address: [String]
+}
+
+type CreateFirewallAddressGroup {
+ name: String!
+ address: [String]
+}
+
+type CreateFirewallAddressGroupResult {
+ data: CreateFirewallAddressGroup
+ success: Boolean!
+ errors: [String]
+}
+
+input UpdateFirewallAddressGroupMembersInput {
+ name: String!
+ address: [String!]!
+}
+
+type UpdateFirewallAddressGroupMembers {
+ name: String!
+ address: [String!]!
+}
+
+type UpdateFirewallAddressGroupMembersResult {
+ data: UpdateFirewallAddressGroupMembers
+ success: Boolean!
+ errors: [String]
+}
+
+input RemoveFirewallAddressGroupMembersInput {
+ name: String!
+ address: [String!]!
+}
+
+type RemoveFirewallAddressGroupMembers {
+ name: String!
+ address: [String!]!
+}
+
+type RemoveFirewallAddressGroupMembersResult {
+ data: RemoveFirewallAddressGroupMembers
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/image.graphql b/src/services/api/graphql/graphql/schema/image.graphql
new file mode 100644
index 000000000..7d1b4f9d0
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/image.graphql
@@ -0,0 +1,29 @@
+input AddSystemImageInput {
+ location: String!
+}
+
+type AddSystemImage {
+ location: String
+ result: String
+}
+
+type AddSystemImageResult {
+ data: AddSystemImage
+ success: Boolean!
+ errors: [String]
+}
+
+input DeleteSystemImageInput {
+ name: String!
+}
+
+type DeleteSystemImage {
+ name: String
+ result: String
+}
+
+type DeleteSystemImageResult {
+ data: DeleteSystemImage
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
index fdcf97bad..32438b315 100644
--- a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
+++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
@@ -1,18 +1,18 @@
-input interfaceEthernetConfigInput {
+input InterfaceEthernetConfigInput {
interface: String
address: String
replace: Boolean = true
description: String
}
-type interfaceEthernetConfig {
+type InterfaceEthernetConfig {
interface: String
address: String
description: String
}
-type createInterfaceEthernetResult {
- data: interfaceEthernetConfig
+type CreateInterfaceEthernetResult {
+ data: InterfaceEthernetConfig
success: Boolean!
errors: [String]
}
diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
index 8a5e17962..c6899bee6 100644
--- a/src/services/api/graphql/graphql/schema/schema.graphql
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -7,9 +7,22 @@ type Query {
_dummy: String
}
-directive @generate on FIELD_DEFINITION
+directive @configure on FIELD_DEFINITION
+directive @configfile on FIELD_DEFINITION
+directive @show on FIELD_DEFINITION
+directive @showconfig on FIELD_DEFINITION
+directive @image on FIELD_DEFINITION
type Mutation {
- createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate
- createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate
+ CreateDhcpServer(data: DhcpServerConfigInput) : CreateDhcpServerResult @configure
+ CreateInterfaceEthernet(data: InterfaceEthernetConfigInput) : CreateInterfaceEthernetResult @configure
+ CreateFirewallAddressGroup(data: CreateFirewallAddressGroupInput) : CreateFirewallAddressGroupResult @configure
+ UpdateFirewallAddressGroupMembers(data: UpdateFirewallAddressGroupMembersInput) : UpdateFirewallAddressGroupMembersResult @configure
+ RemoveFirewallAddressGroupMembers(data: RemoveFirewallAddressGroupMembersInput) : RemoveFirewallAddressGroupMembersResult @configure
+ SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configfile
+ LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configfile
+ Show(data: ShowInput) : ShowResult @show
+ ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig
+ AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @image
+ DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @image
}
diff --git a/src/services/api/graphql/graphql/schema/show.graphql b/src/services/api/graphql/graphql/schema/show.graphql
new file mode 100644
index 000000000..c7709e48b
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/show.graphql
@@ -0,0 +1,14 @@
+input ShowInput {
+ path: [String!]!
+}
+
+type Show {
+ path: [String]
+ result: String
+}
+
+type ShowResult {
+ data: Show
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/show_config.graphql b/src/services/api/graphql/graphql/schema/show_config.graphql
new file mode 100644
index 000000000..34afd2aa9
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/show_config.graphql
@@ -0,0 +1,21 @@
+"""
+Use 'scalar Generic' for show config output, to avoid attempts to
+JSON-serialize in case of JSON output.
+"""
+scalar Generic
+
+input ShowConfigInput {
+ path: [String!]!
+ configFormat: String
+}
+
+type ShowConfig {
+ path: [String]
+ result: Generic
+}
+
+type ShowConfigResult {
+ data: ShowConfig
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py
deleted file mode 100644
index 3edb3028e..000000000
--- a/src/services/api/graphql/recipes/dhcp_server.py
+++ /dev/null
@@ -1,13 +0,0 @@
-
-from . recipe import Recipe
-
-class DhcpServer(Recipe):
- def __init__(self, session, command_file):
- super().__init__(session, command_file)
-
- # Define any custom processing of parameters here by overriding
- # configure:
- #
- # def configure(self):
- # self.data = transform_data(self.data)
- # super().configure()
diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py
deleted file mode 100644
index f88f5924f..000000000
--- a/src/services/api/graphql/recipes/interface_ethernet.py
+++ /dev/null
@@ -1,13 +0,0 @@
-
-from . recipe import Recipe
-
-class InterfaceEthernet(Recipe):
- def __init__(self, session, command_file):
- super().__init__(session, command_file)
-
- # Define any custom processing of parameters here by overriding
- # configure:
- #
- # def configure(self):
- # self.data = transform_data(self.data)
- # super().configure()
diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py
deleted file mode 100644
index 8fbb9e0bf..000000000
--- a/src/services/api/graphql/recipes/recipe.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from ariadne import convert_camel_case_to_snake
-import vyos.defaults
-from vyos.template import render
-
-class Recipe(object):
- def __init__(self, session, data):
- self._session = session
- self.data = data
- self._name = convert_camel_case_to_snake(type(self).__name__)
-
- @property
- def data(self):
- return self.__data
-
- @data.setter
- def data(self, data):
- if isinstance(data, dict):
- self.__data = data
- else:
- raise ValueError("data must be of type dict")
-
- def configure(self):
- session = self._session
- data = self.data
- func_base_name = self._name
-
- tmpl_file = f'{func_base_name}.tmpl'
- cmd_file = f'/tmp/{func_base_name}.cmds'
- tmpl_dir = vyos.defaults.directories['api_templates']
-
- try:
- render(cmd_file, tmpl_file, data, location=tmpl_dir)
- commands = []
- with open(cmd_file) as f:
- lines = f.readlines()
- for line in lines:
- commands.append(line.split())
- for cmd in commands:
- if cmd[0] == 'set':
- session.set(cmd[1:])
- elif cmd[0] == 'delete':
- session.delete(cmd[1:])
- else:
- raise ValueError('Operation must be "set" or "delete"')
- session.commit()
- except Exception as error:
- raise error
-
-
diff --git a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py
new file mode 100644
index 000000000..cde30c27a
--- /dev/null
+++ b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py
@@ -0,0 +1,21 @@
+
+from . session import Session
+
+class RemoveFirewallAddressGroupMembers(Session):
+ def __init__(self, session, data):
+ super().__init__(session, data)
+
+ # Define any custom processing of parameters here by overriding
+ # configure:
+ #
+ # def configure(self):
+ # self._data = transform_data(self._data)
+ # super().configure()
+ # self.clean_up()
+
+ def configure(self):
+ super().configure()
+
+ group_name = self._data['name']
+ path = ['firewall', 'group', 'address-group', group_name]
+ self.delete_path_if_childless(path)
diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py
new file mode 100644
index 000000000..5ece78ee6
--- /dev/null
+++ b/src/services/api/graphql/recipes/session.py
@@ -0,0 +1,123 @@
+import json
+
+from ariadne import convert_camel_case_to_snake
+
+import vyos.defaults
+from vyos.config import Config
+from vyos.configtree import ConfigTree
+from vyos.template import render
+
+class Session:
+ """
+ Wrapper for calling configsession functions based on GraphQL requests.
+ Non-nullable fields in the respective schema allow avoiding a key check
+ in 'data'.
+ """
+ def __init__(self, session, data):
+ self._session = session
+ self._data = data
+ self._name = convert_camel_case_to_snake(type(self).__name__)
+
+ def configure(self):
+ session = self._session
+ data = self._data
+ func_base_name = self._name
+
+ tmpl_file = f'{func_base_name}.tmpl'
+ cmd_file = f'/tmp/{func_base_name}.cmds'
+ tmpl_dir = vyos.defaults.directories['api_templates']
+
+ try:
+ render(cmd_file, tmpl_file, data, location=tmpl_dir)
+ commands = []
+ with open(cmd_file) as f:
+ lines = f.readlines()
+ for line in lines:
+ commands.append(line.split())
+ for cmd in commands:
+ if cmd[0] == 'set':
+ session.set(cmd[1:])
+ elif cmd[0] == 'delete':
+ session.delete(cmd[1:])
+ else:
+ raise ValueError('Operation must be "set" or "delete"')
+ session.commit()
+ except Exception as error:
+ raise error
+
+ def delete_path_if_childless(self, path):
+ session = self._session
+ config = Config(session.get_session_env())
+ if not config.list_nodes(path):
+ session.delete(path)
+ session.commit()
+
+ def show_config(self):
+ session = self._session
+ data = self._data
+ out = ''
+
+ try:
+ out = session.show_config(data['path'])
+ if data.get('config_format', '') == 'json':
+ config_tree = vyos.configtree.ConfigTree(out)
+ out = json.loads(config_tree.to_json())
+ except Exception as error:
+ raise error
+
+ return out
+
+ def save(self):
+ session = self._session
+ data = self._data
+ if 'file_name' not in data or not data['file_name']:
+ data['file_name'] = '/config/config.boot'
+
+ try:
+ session.save_config(data['file_name'])
+ except Exception as error:
+ raise error
+
+ def load(self):
+ session = self._session
+ data = self._data
+
+ try:
+ session.load_config(data['file_name'])
+ session.commit()
+ except Exception as error:
+ raise error
+
+ def show(self):
+ session = self._session
+ data = self._data
+ out = ''
+
+ try:
+ out = session.show(data['path'])
+ except Exception as error:
+ raise error
+
+ return out
+
+ def add(self):
+ session = self._session
+ data = self._data
+
+ try:
+ res = session.install_image(data['location'])
+ except Exception as error:
+ raise error
+
+ return res
+
+ def delete(self):
+ session = self._session
+ data = self._data
+
+ try:
+ res = session.remove_image(data['name'])
+ except Exception as error:
+ raise error
+
+ return res
diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl
index 629ce83c1..70de43183 100644
--- a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl
+++ b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl
@@ -1,5 +1,5 @@
set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }}
-set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} dns-server {{ dns_server }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} name-server {{ name_server }}
set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }}
set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }}
set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }}
diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl
new file mode 100644
index 000000000..a890d0086
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl
@@ -0,0 +1,4 @@
+set firewall group address-group {{ name }}
+{% for add in address %}
+set firewall group address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl
index d9d7ed691..d9d7ed691 100644
--- a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl
+++ b/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl
diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl
new file mode 100644
index 000000000..458f3e5fc
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl
@@ -0,0 +1,3 @@
+{% for add in address %}
+delete firewall group address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl
new file mode 100644
index 000000000..f56c61231
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl
@@ -0,0 +1,3 @@
+{% for add in address %}
+set firewall group address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index 670b6e66a..2d18589ef 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -28,6 +28,7 @@ import zmq
from contextlib import contextmanager
from vyos.defaults import directories
+from vyos.util import boot_config_complete
from vyos.configsource import ConfigSourceString, ConfigSourceError
from vyos.config import Config
from vyos import ConfigError
@@ -186,7 +187,7 @@ def initialization(socket):
session_out = None
# if not a 'live' session, for example on boot, write to file
- if not session_out or not os.path.isfile('/tmp/vyos-config-status'):
+ if not session_out or not boot_config_complete():
session_out = script_stdout_log
session_mode = 'a'
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
index 52753faa5..1e0b37efe 100755
--- a/src/services/vyos-hostsd
+++ b/src/services/vyos-hostsd
@@ -340,7 +340,7 @@ hosts_add_schema = op_type_schema.extend({
'data': {
str: {
str: {
- 'address': str,
+ 'address': [str],
'aliases': [str]
}
}
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index cb4ce4072..aa7ac6708 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -32,16 +32,13 @@ from fastapi.responses import HTMLResponse
from fastapi.exceptions import RequestValidationError
from fastapi.routing import APIRoute
from pydantic import BaseModel, StrictStr, validator
-from starlette.datastructures import FormData, MutableHeaders
+from starlette.datastructures import FormData
from starlette.formparsers import FormParser, MultiPartParser
from multipart.multipart import parse_options_header
-from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
from ariadne.asgi import GraphQL
import vyos.config
-import vyos.defaults
-
from vyos.configsession import ConfigSession, ConfigSessionError
import api.graphql.state
@@ -69,11 +66,11 @@ def load_server_config():
return config
def check_auth(key_list, key):
- id = None
+ key_id = None
for k in key_list:
if k['key'] == key:
- id = k['id']
- return id
+ key_id = k['id']
+ return key_id
def error(code, msg):
resp = {"success": False, "error": msg, "data": None}
@@ -223,10 +220,10 @@ responses = {
def auth_required(data: ApiModel):
key = data.key
api_keys = app.state.vyos_keys
- id = check_auth(api_keys, key)
- if not id:
+ key_id = check_auth(api_keys, key)
+ if not key_id:
raise HTTPException(status_code=401, detail="Valid API key is required")
- app.state.vyos_id = id
+ app.state.vyos_id = key_id
# override Request and APIRoute classes in order to convert form request to json;
# do all explicit validation here, for backwards compatability of error messages;
@@ -613,16 +610,11 @@ def show_op(data: ShowModel):
# GraphQL integration
###
-api.graphql.state.init()
-
-from api.graphql.graphql.mutations import mutation
-from api.graphql.graphql.directives import DataDirective
+from api.graphql.bindings import generate_schema
-api_schema_dir = vyos.defaults.directories['api_schema']
-
-type_defs = load_schema_from_path(api_schema_dir)
+api.graphql.state.init()
-schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective})
+schema = generate_schema()
app.add_route('/graphql', GraphQL(schema, debug=True))
@@ -640,16 +632,16 @@ if __name__ == '__main__':
try:
server_config = load_server_config()
- except Exception as e:
- logger.critical("Failed to load the HTTP API server config: {0}".format(e))
+ except Exception as err:
+ logger.critical(f"Failed to load the HTTP API server config: {err}")
- session = ConfigSession(os.getpid())
+ config_session = ConfigSession(os.getpid())
- app.state.vyos_session = session
+ app.state.vyos_session = config_session
app.state.vyos_keys = server_config['api_keys']
- app.state.vyos_debug = True if server_config['debug'] == 'true' else False
- app.state.vyos_strict = True if server_config['strict'] == 'true' else False
+ app.state.vyos_debug = bool(server_config['debug'] == 'true')
+ app.state.vyos_strict = bool(server_config['strict'] == 'true')
api.graphql.state.settings['app'] = app
@@ -657,6 +649,6 @@ if __name__ == '__main__':
uvicorn.run(app, host=server_config["listen_address"],
port=int(server_config["port"]),
proxy_headers=True)
- except OSError as e:
- logger.critical(f"OSError {e}")
+ except OSError as err:
+ logger.critical(f"OSError {err}")
sys.exit(1)
diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
index 1fba0d75b..cde0c6cc5 100755
--- a/src/system/keepalived-fifo.py
+++ b/src/system/keepalived-fifo.py
@@ -29,6 +29,7 @@ from logging.handlers import SysLogHandler
from vyos.ifconfig.vrrp import VRRP
from vyos.configquery import ConfigTreeQuery
from vyos.util import cmd
+from vyos.util import dict_search
# configure logging
logger = logging.getLogger(__name__)
@@ -52,7 +53,7 @@ class KeepalivedFifo:
# parse arguments
cmd_args = cmd_args_parser.parse_args()
- self._config_load()
+ self.vrrp_config_dict = None
self.pipe_path = cmd_args.PIPE
# create queue for messages and events for syncronization
@@ -69,22 +70,10 @@ class KeepalivedFifo:
raise ValueError()
# Read VRRP configuration directly from CLI
- vrrp_config_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True)
- self.vrrp_config = {'vrrp_groups': {}, 'sync_groups': {}}
- for key in ['group', 'sync_group']:
- if key not in vrrp_config_dict:
- continue
- for group, group_config in vrrp_config_dict[key].items():
- if 'transition_script' not in group_config:
- continue
- self.vrrp_config['vrrp_groups'][group] = {
- 'STOP': group_config['transition_script'].get('stop'),
- 'FAULT': group_config['transition_script'].get('fault'),
- 'BACKUP': group_config['transition_script'].get('backup'),
- 'MASTER': group_config['transition_script'].get('master'),
- }
- logger.info(f'Loaded configuration: {self.vrrp_config}')
+ self.vrrp_config_dict = conf.get_config_dict(base,
+ key_mangling=('-', '_'), get_first_key=True)
+
+ logger.debug(f'Loaded configuration: {self.vrrp_config_dict}')
except Exception as err:
logger.error(f'Unable to load configuration: {err}')
@@ -108,6 +97,11 @@ class KeepalivedFifo:
logger.debug('Message processing start')
regex_notify = re.compile(r'^(?P<type>\w+) "(?P<name>[\w-]+)" (?P<state>\w+) (?P<priority>\d+)$', re.MULTILINE)
while self.stopme.is_set() is False:
+ # XXX: T4059 - do a "late" read of the CLI configuration as this would fail in __init__.
+ # Thus we simply read the configuration the first time it really becomes necessary
+ # and a message requireing the data needs it actually.
+ if self.vrrp_config_dict == None:
+ self._config_load()
# wait for a new message event from pipe_wait
self.message_event.wait()
try:
@@ -129,20 +123,17 @@ class KeepalivedFifo:
if os.path.exists(mdns_running_file):
cmd(mdns_update_command)
- if n_name in self.vrrp_config['vrrp_groups'] and n_state in self.vrrp_config['vrrp_groups'][n_name]:
- n_script = self.vrrp_config['vrrp_groups'][n_name].get(n_state)
- if n_script:
- self._run_command(n_script)
+ tmp = dict_search(f'group.{n_name}.transition_script.{n_state.lower()}', self.vrrp_config_dict)
+ if tmp != None:
+ self._run_command(tmp)
# check and run commands for VRRP sync groups
- # currently, this is not available in VyOS CLI
- if n_type == 'GROUP':
+ elif n_type == 'GROUP':
if os.path.exists(mdns_running_file):
cmd(mdns_update_command)
- if n_name in self.vrrp_config['sync_groups'] and n_state in self.vrrp_config['sync_groups'][n_name]:
- n_script = self.vrrp_config['sync_groups'][n_name].get(n_state)
- if n_script:
- self._run_command(n_script)
+ tmp = dict_search(f'sync_group.{n_name}.transition_script.{n_state.lower()}', self.vrrp_config_dict)
+ if tmp != None:
+ self._run_command(tmp)
# mark task in queue as done
self.message_queue.task_done()
except Exception as err:
diff --git a/src/systemd/dhcp6c@.service b/src/systemd/dhcp6c@.service
index fdd6d7d88..9a97ee261 100644
--- a/src/systemd/dhcp6c@.service
+++ b/src/systemd/dhcp6c@.service
@@ -9,7 +9,7 @@ StartLimitIntervalSec=0
WorkingDirectory=/run/dhcp6c
Type=forking
PIDFile=/run/dhcp6c/dhcp6c.%i.pid
-ExecStart=/usr/sbin/dhcp6c -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i
+ExecStart=/usr/sbin/dhcp6c -D -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i
Restart=on-failure
RestartSec=20
diff --git a/src/systemd/root-partition-auto-resize.service b/src/systemd/root-partition-auto-resize.service
new file mode 100644
index 000000000..a57fbc3d8
--- /dev/null
+++ b/src/systemd/root-partition-auto-resize.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=VyOS root partition auto resizing
+After=multi-user.target
+
+[Service]
+Type=oneshot
+User=root
+Group=root
+ExecStart=/usr/libexec/vyos/op_mode/force_root-partition-auto-resize.sh
+
+[Install]
+WantedBy=vyos.target \ No newline at end of file
diff --git a/src/systemd/tftpd@.service b/src/systemd/tftpd@.service
index 266bc0962..a674bf598 100644
--- a/src/systemd/tftpd@.service
+++ b/src/systemd/tftpd@.service
@@ -7,7 +7,7 @@ RequiresMountsFor=/run
Type=forking
#NotifyAccess=main
EnvironmentFile=-/etc/default/tftpd%I
-ExecStart=/usr/sbin/in.tftpd "$DAEMON_ARGS"
+ExecStart=/bin/sh -c "${VRF_ARGS} /usr/sbin/in.tftpd ${DAEMON_ARGS}"
Restart=on-failure
[Install]
diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service
index ba5df5984..55370b356 100644
--- a/src/systemd/vyos-http-api.service
+++ b/src/systemd/vyos-http-api.service
@@ -1,10 +1,9 @@
[Unit]
Description=VyOS HTTP API service
-After=auditd.service systemd-user-sessions.service time-sync.target vyos-router.service
+After=vyos-router.service
Requires=vyos-router.service
[Service]
-ExecStartPre=/usr/libexec/vyos/init/vyos-config
ExecStart=/usr/libexec/vyos/services/vyos-http-api-server
Type=idle
@@ -18,6 +17,5 @@ User=root
Group=vyattacfg
[Install]
-# Installing in a earlier target leaves ExecStartPre waiting
-WantedBy=getty.target
+WantedBy=vyos.target
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})
diff --git a/src/validators/bgp-large-community-list b/src/validators/bgp-large-community-list
index c07268e81..80112dfdc 100755
--- a/src/validators/bgp-large-community-list
+++ b/src/validators/bgp-large-community-list
@@ -30,7 +30,7 @@ if __name__ == '__main__':
sys.exit(1)
if not (re.match(pattern, sys.argv[1]) and
- (is_ipv4(value[0]) or value[0].isdigit()) and value[1].isdigit()):
+ (is_ipv4(value[0]) or value[0].isdigit()) and (value[1].isdigit() or value[1] == '*')):
sys.exit(1)
sys.exit(0)
diff --git a/src/validators/bgp-route-target b/src/validators/bgp-rd-rt
index e7e4d403f..b2b69c9be 100755
--- a/src/validators/bgp-route-target
+++ b/src/validators/bgp-rd-rt
@@ -19,29 +19,37 @@ from vyos.template import is_ipv4
parser = ArgumentParser()
group = parser.add_mutually_exclusive_group()
-group.add_argument('--single', action='store', help='Validate and allow only one route-target')
-group.add_argument('--multi', action='store', help='Validate multiple, whitespace separated route-targets')
+group.add_argument('--route-distinguisher', action='store', help='Validate BGP route distinguisher')
+group.add_argument('--route-target', action='store', help='Validate one BGP route-target')
+group.add_argument('--route-target-multi', action='store', help='Validate multiple, whitespace separated BGP route-targets')
args = parser.parse_args()
-def is_valid_rt(rt):
- # every route target needs to have a colon and must consists of two parts
+def is_valid(rt):
+ """ Verify BGP RD/RT - both can be verified using the same logic """
+ # every RD/RT (route distinguisher/route target) needs to have a colon and
+ # must consists of two parts
value = rt.split(':')
if len(value) != 2:
return False
- # A route target must either be only numbers, or the first part must be an
- # IPv4 address
+
+ # An RD/RT must either be only numbers, or the first part must be an IPv4
+ # address
if (is_ipv4(value[0]) or value[0].isdigit()) and value[1].isdigit():
return True
return False
if __name__ == '__main__':
- if args.single:
- if not is_valid_rt(args.single):
+ if args.route_distinguisher:
+ if not is_valid(args.route_distinguisher):
+ exit(1)
+
+ elif args.route_target:
+ if not is_valid(args.route_target):
exit(1)
- elif args.multi:
- for rt in args.multi.split(' '):
- if not is_valid_rt(rt):
+ elif args.route_target_multi:
+ for rt in args.route_target_multi.split(' '):
+ if not is_valid(rt):
exit(1)
else:
diff --git a/src/validators/range b/src/validators/range
new file mode 100755
index 000000000..d4c25f3c4
--- /dev/null
+++ b/src/validators/range
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import sys
+import argparse
+
+class MalformedRange(Exception):
+ pass
+
+def validate_range(value, min=None, max=None):
+ try:
+ lower, upper = re.match(r'^(\d+)-(\d+)$', value).groups()
+
+ lower, upper = int(lower), int(upper)
+
+ if int(lower) > int(upper):
+ raise MalformedRange("the lower bound exceeds the upper bound".format(value))
+
+ if min is not None:
+ if lower < min:
+ raise MalformedRange("the lower bound must not be less than {}".format(min))
+
+ if max is not None:
+ if upper > max:
+ raise MalformedRange("the upper bound must not be greater than {}".format(max))
+
+ except (AttributeError, ValueError):
+ raise MalformedRange("range syntax error")
+
+parser = argparse.ArgumentParser(description='Range validator.')
+parser.add_argument('--min', type=int, action='store')
+parser.add_argument('--max', type=int, action='store')
+parser.add_argument('value', action='store')
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ try:
+ validate_range(args.value, min=args.min, max=args.max)
+ except MalformedRange as e:
+ print("Incorrect range '{}': {}".format(args.value, e))
+ sys.exit(1)
diff --git a/src/validators/script b/src/validators/script
index 1d8a27e5c..4ffdeb2a0 100755
--- a/src/validators/script
+++ b/src/validators/script
@@ -36,7 +36,7 @@ if __name__ == '__main__':
# File outside the config dir is just a warning
if not vyos.util.file_is_persistent(script):
- sys.exit(
- f'Warning: file {path} is outside the / config directory\n'
+ sys.exit(0)(
+ f'Warning: file {script} is outside the "/config" directory\n'
'It will not be automatically migrated to a new image on system update'
)