summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile6
-rw-r--r--data/op-mode-standardized.json7
-rw-r--r--data/templates/ids/fastnetmon.j210
-rw-r--r--data/templates/ids/fastnetmon_excluded_networks_list.j25
-rw-r--r--data/templates/ids/fastnetmon_networks_list.j24
-rw-r--r--data/templates/pmacct/uacctd.conf.j28
-rw-r--r--data/templates/router-advert/radvd.conf.j22
-rw-r--r--data/templates/ssh/override.conf.j23
-rw-r--r--debian/vyos-1x.postinst16
-rw-r--r--interface-definitions/https.xml.in13
-rw-r--r--interface-definitions/include/interface/enable-directed-broadcast.xml.i8
-rw-r--r--interface-definitions/include/interface/ipv4-options.xml.i1
-rw-r--r--interface-definitions/interfaces-bonding.xml.in17
-rw-r--r--interface-definitions/interfaces-macsec.xml.in7
-rw-r--r--interface-definitions/policy.xml.in4
-rw-r--r--interface-definitions/service-ids-ddos-protection.xml.in40
-rw-r--r--interface-definitions/service-router-advert.xml.in12
-rw-r--r--interface-definitions/system-ip.xml.in6
-rw-r--r--interface-definitions/xml-component-version.xml.in1
-rw-r--r--op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i2
-rw-r--r--op-mode-definitions/ipv4-route.xml.in10
-rw-r--r--op-mode-definitions/ipv6-route.xml.in6
-rw-r--r--op-mode-definitions/monitor-log.xml.in27
-rw-r--r--op-mode-definitions/nat.xml.in4
-rw-r--r--op-mode-definitions/show-arp.xml.in4
-rw-r--r--op-mode-definitions/show-bridge.xml.in8
-rw-r--r--op-mode-definitions/show-conntrack.xml.in33
-rw-r--r--op-mode-definitions/show-hardware.xml.in10
-rw-r--r--op-mode-definitions/show-ip.xml.in6
-rw-r--r--op-mode-definitions/show-log.xml.in21
-rw-r--r--op-mode-definitions/show-system.xml.in2
-rw-r--r--op-mode-definitions/show-version.xml.in4
-rw-r--r--op-mode-definitions/show-vrf.xml.in4
-rw-r--r--op-mode-definitions/vpn-ipsec.xml.in9
-rw-r--r--python/vyos/configdict.py30
-rw-r--r--python/vyos/configverify.py12
-rw-r--r--python/vyos/cpu.py3
-rw-r--r--python/vyos/defaults.py2
-rw-r--r--python/vyos/ifconfig/bond.py75
-rw-r--r--python/vyos/ifconfig/interface.py52
-rw-r--r--python/vyos/opmode.py128
-rw-r--r--python/vyos/util.py21
-rw-r--r--python/vyos/validate.py19
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py4
-rwxr-xr-xsmoketest/scripts/cli/test_component_version.py18
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bonding.py67
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bridge.py31
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py26
-rwxr-xr-xsmoketest/scripts/cli/test_service_https.py57
-rwxr-xr-xsmoketest/scripts/cli/test_service_ids.py26
-rwxr-xr-xsmoketest/scripts/cli/test_service_monitoring_telegraf.py11
-rwxr-xr-xsmoketest/scripts/cli/test_service_router-advert.py62
-rwxr-xr-xsmoketest/scripts/cli/test_system_flow-accounting.py22
-rwxr-xr-xsmoketest/scripts/cli/test_system_ip.py13
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py9
-rwxr-xr-xsrc/conf_mode/http-api.py8
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py23
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py14
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py2
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py2
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py2
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py73
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py2
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py1
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py2
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py7
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py2
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py2
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py2
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py21
-rwxr-xr-xsrc/conf_mode/service_ids_fastnetmon.py58
-rwxr-xr-xsrc/conf_mode/system-ip.py5
-rwxr-xr-xsrc/conf_mode/system-login.py2
-rwxr-xr-xsrc/conf_mode/vrf.py6
-rwxr-xr-xsrc/etc/opennhrp/opennhrp-script.py66
-rw-r--r--src/etc/sysctl.d/30-vyos-router.conf6
-rw-r--r--src/etc/systemd/system/fastnetmon.service.d/override.conf12
-rw-r--r--src/etc/systemd/system/frr.service.d/override.conf11
-rwxr-xr-xsrc/op_mode/bridge.py202
-rwxr-xr-xsrc/op_mode/conntrack.py121
-rwxr-xr-xsrc/op_mode/cpu.py82
-rwxr-xr-xsrc/op_mode/cpu_summary.py48
-rwxr-xr-xsrc/op_mode/ipsec.py71
-rwxr-xr-xsrc/op_mode/memory.py (renamed from src/op_mode/show_ram.py)39
-rwxr-xr-xsrc/op_mode/nat.py191
-rwxr-xr-xsrc/op_mode/neighbor.py (renamed from src/op_mode/show_neigh.py)60
-rw-r--r--src/op_mode/route.py98
-rwxr-xr-xsrc/op_mode/show_cpu.py72
-rwxr-xr-xsrc/op_mode/show_nat_rules.py9
-rwxr-xr-xsrc/op_mode/show_nat_translations.py16
-rwxr-xr-xsrc/op_mode/show_vrf.py66
-rwxr-xr-xsrc/op_mode/version.py (renamed from src/op_mode/show_version.py)52
-rwxr-xr-xsrc/op_mode/vpn_ipsec.py5
-rwxr-xr-xsrc/op_mode/vrf.py92
-rw-r--r--src/services/api/graphql/bindings.py3
-rw-r--r--src/services/api/graphql/graphql/directives.py29
-rw-r--r--src/services/api/graphql/graphql/mutations.py22
-rw-r--r--src/services/api/graphql/graphql/queries.py26
-rw-r--r--src/services/api/graphql/graphql/schema/config_file.graphql2
-rw-r--r--src/services/api/graphql/graphql/schema/dhcp_server.graphql1
-rw-r--r--src/services/api/graphql/graphql/schema/firewall_group.graphql6
-rw-r--r--src/services/api/graphql/graphql/schema/image.graphql2
-rw-r--r--src/services/api/graphql/graphql/schema/interface_ethernet.graphql1
-rw-r--r--src/services/api/graphql/graphql/schema/schema.graphql6
-rw-r--r--src/services/api/graphql/graphql/schema/show.graphql1
-rw-r--r--src/services/api/graphql/graphql/schema/show_config.graphql2
-rw-r--r--src/services/api/graphql/graphql/schema/system_status.graphql18
-rw-r--r--src/services/api/graphql/key_auth.py18
-rwxr-xr-xsrc/services/api/graphql/recipes/queries/system_status.py38
-rw-r--r--src/services/api/graphql/recipes/session.py75
-rwxr-xr-xsrc/services/api/graphql/utils/schema_from_op_mode.py161
-rw-r--r--src/services/api/graphql/utils/util.py55
-rwxr-xr-xsrc/services/vyos-http-api-server7
-rw-r--r--src/systemd/wpa_supplicant-macsec@.service4
114 files changed, 2553 insertions, 485 deletions
diff --git a/Makefile b/Makefile
index 56d446a27..0a968563c 100644
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,7 @@ SHIM_DIR := src/shim
XDP_DIR := src/xdp
LIBS := -lzmq
CFLAGS :=
+BUILD_ARCH := $(shell dpkg-architecture -q DEB_BUILD_ARCH)
J2LINT := $(shell command -v j2lint 2> /dev/null)
@@ -49,6 +50,11 @@ interface_definitions: $(config_xml_obj)
# could mask help strings or mandatory priority statements
find $(TMPL_DIR) -name node.def -type f -empty -exec false {} + || sh -c 'echo "There are empty node.def files! Check your interface definitions." && exit 1'
+ifeq ($(BUILD_ARCH),arm64)
+ # There is currently no telegraf support in VyOS for ARM64, remove CLI definitions
+ rm -rf $(TMPL_DIR)/service/monitoring/telegraf
+endif
+
.PHONY: op_mode_definitions
.ONESHELL:
op_mode_definitions: $(op_xml_obj)
diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json
new file mode 100644
index 000000000..4dccbba7f
--- /dev/null
+++ b/data/op-mode-standardized.json
@@ -0,0 +1,7 @@
+[
+"cpu.py",
+"memory.py",
+"neighbor.py",
+"route.py",
+"version.py"
+]
diff --git a/data/templates/ids/fastnetmon.j2 b/data/templates/ids/fastnetmon.j2
index c482002fa..b9f77a257 100644
--- a/data/templates/ids/fastnetmon.j2
+++ b/data/templates/ids/fastnetmon.j2
@@ -1,21 +1,25 @@
# enable this option if you want to send logs to local syslog facility
+logging:logging_level = debug
logging:local_syslog_logging = on
# list of all your networks in CIDR format
-networks_list_path = /etc/networks_list
+networks_list_path = /run/fastnetmon/networks_list
# list networks in CIDR format which will be not monitored for attacks
-white_list_path = /etc/networks_whitelist
+white_list_path = /run/fastnetmon/excluded_networks_list
# Enable/Disable any actions in case of attack
enable_ban = on
+enable_ban_ipv6 = on
## How many packets will be collected from attack traffic
ban_details_records_count = 500
## How long (in seconds) we should keep an IP in blocked state
## If you set 0 here it completely disables unban capability
-ban_time = 1900
+{% if ban_time is vyos_defined %}
+ban_time = {{ ban_time }}
+{% endif %}
# Check if the attack is still active, before triggering an unban callback with this option
# If the attack is still active, check each run of the unban watchdog
diff --git a/data/templates/ids/fastnetmon_excluded_networks_list.j2 b/data/templates/ids/fastnetmon_excluded_networks_list.j2
new file mode 100644
index 000000000..c88a1c527
--- /dev/null
+++ b/data/templates/ids/fastnetmon_excluded_networks_list.j2
@@ -0,0 +1,5 @@
+{% if excluded_network is vyos_defined %}
+{% for net in excluded_network %}
+{{ net }}
+{% endfor %}
+{% endif %}
diff --git a/data/templates/ids/fastnetmon_networks_list.j2 b/data/templates/ids/fastnetmon_networks_list.j2
index 1c81180be..5f1b3ba4d 100644
--- a/data/templates/ids/fastnetmon_networks_list.j2
+++ b/data/templates/ids/fastnetmon_networks_list.j2
@@ -1,6 +1,4 @@
-{% if network is vyos_defined(var_type=str) %}
-{{ network }}
-{% else %}
+{% if network is vyos_defined() %}
{% for net in network %}
{{ net }}
{% endfor %}
diff --git a/data/templates/pmacct/uacctd.conf.j2 b/data/templates/pmacct/uacctd.conf.j2
index a5016691f..8fbc09e83 100644
--- a/data/templates/pmacct/uacctd.conf.j2
+++ b/data/templates/pmacct/uacctd.conf.j2
@@ -21,13 +21,13 @@ imt_mem_pools_number: 169
{% set plugin = [] %}
{% if netflow.server is vyos_defined %}
{% for server in netflow.server %}
-{% set nf_server_key = 'nf_' ~ server | replace(':', '.') %}
+{% set nf_server_key = 'nf_' ~ server | dot_colon_to_dash %}
{% set _ = plugin.append('nfprobe['~ nf_server_key ~ ']') %}
{% endfor %}
{% endif %}
{% if sflow.server is vyos_defined %}
{% for server in sflow.server %}
-{% set sf_server_key = 'sf_' ~ server | replace(':', '.') %}
+{% set sf_server_key = 'sf_' ~ server | dot_colon_to_dash %}
{% set _ = plugin.append('sfprobe[' ~ sf_server_key ~ ']') %}
{% endfor %}
{% endif %}
@@ -40,7 +40,7 @@ plugins: {{ plugin | join(',') }}
# NetFlow servers
{% for server, server_config in netflow.server.items() %}
{# # prevent pmacct syntax error when using IPv6 flow collectors #}
-{% set nf_server_key = 'nf_' ~ server | replace(':', '.') %}
+{% set nf_server_key = 'nf_' ~ server | dot_colon_to_dash %}
nfprobe_receiver[{{ nf_server_key }}]: {{ server | bracketize_ipv6 }}:{{ server_config.port }}
nfprobe_version[{{ nf_server_key }}]: {{ netflow.version }}
{% if netflow.engine_id is vyos_defined %}
@@ -66,7 +66,7 @@ nfprobe_timeouts[{{ nf_server_key }}]: expint={{ netflow.timeout.expiry_interval
# sFlow servers
{% for server, server_config in sflow.server.items() %}
{# # prevent pmacct syntax error when using IPv6 flow collectors #}
-{% set sf_server_key = 'sf_' ~ server | replace(':', '.') %}
+{% set sf_server_key = 'sf_' ~ server | dot_colon_to_dash %}
sfprobe_receiver[{{ sf_server_key }}]: {{ server | bracketize_ipv6 }}:{{ server_config.port }}
sfprobe_agentip[{{ sf_server_key }}]: {{ sflow.agent_address }}
{% if sflow.sampling_rate is vyos_defined %}
diff --git a/data/templates/router-advert/radvd.conf.j2 b/data/templates/router-advert/radvd.conf.j2
index ed15b32f0..a464795ad 100644
--- a/data/templates/router-advert/radvd.conf.j2
+++ b/data/templates/router-advert/radvd.conf.j2
@@ -50,6 +50,8 @@ interface {{ iface }} {
AdvValidLifetime {{ prefix_options.valid_lifetime }};
AdvOnLink {{ 'off' if prefix_options.no_on_link_flag is vyos_defined else 'on' }};
AdvPreferredLifetime {{ prefix_options.preferred_lifetime }};
+ DeprecatePrefix {{ 'on' if prefix_options.deprecate_prefix is vyos_defined else 'off' }};
+ DecrementLifetimes {{ 'on' if prefix_options.decrement_lifetime is vyos_defined else 'off' }};
};
{% endfor %}
{% endif %}
diff --git a/data/templates/ssh/override.conf.j2 b/data/templates/ssh/override.conf.j2
index e4d6f51cb..4454ad1b8 100644
--- a/data/templates/ssh/override.conf.j2
+++ b/data/templates/ssh/override.conf.j2
@@ -5,8 +5,9 @@ After=vyos-router.service
ConditionPathExists={{ config_file }}
[Service]
+EnvironmentFile=
ExecStart=
-ExecStart={{ vrf_command }}/usr/sbin/sshd -f {{ config_file }} -D $SSHD_OPTS
+ExecStart={{ vrf_command }}/usr/sbin/sshd -f {{ config_file }}
Restart=always
RestartPreventExitStatus=
RestartSec=10
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index 1ca6687a3..6879b6e4f 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -13,6 +13,7 @@ if ! grep -q '^minion' /etc/passwd; then
adduser --quiet minion dip
adduser --quiet minion disk
adduser --quiet minion users
+ adduser --quiet minion frr
fi
# OpenVPN should get its own user
@@ -45,6 +46,7 @@ if ! grep -q '^radius_priv_user' /etc/passwd; then
adduser --quiet radius_priv_user dip
adduser --quiet radius_priv_user disk
adduser --quiet radius_priv_user users
+ adduser --quiet radius_priv_user frr
fi
# add hostsd group for vyos-hostsd
@@ -86,11 +88,17 @@ fi
# Remove unwanted daemon files from /etc
# conntackd
+# pmacct
+# fastnetmon
+# ntp
DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd
- /etc/default/pmacctd /etc/pmacct"
-for file in $DELETE; do
- if [ -f ${file} ]; then
- rm -f ${file}
+ /etc/default/pmacctd /etc/pmacct
+ /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf
+ /etc/ntp.conf /etc/default/ssh
+ /etc/powerdns /etc/default/pdns-recursor"
+for tmp in $DELETE; do
+ if [ -e ${tmp} ]; then
+ rm -rf ${tmp}
fi
done
diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in
index d2c393036..d096c4ff1 100644
--- a/interface-definitions/https.xml.in
+++ b/interface-definitions/https.xml.in
@@ -107,6 +107,19 @@
<valueless/>
</properties>
</leafNode>
+ <node name="gql">
+ <properties>
+ <help>GraphQL support</help>
+ </properties>
+ <children>
+ <leafNode name="introspection">
+ <properties>
+ <help>Schema introspection</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
<node name="cors">
<properties>
<help>Set CORS options</help>
diff --git a/interface-definitions/include/interface/enable-directed-broadcast.xml.i b/interface-definitions/include/interface/enable-directed-broadcast.xml.i
new file mode 100644
index 000000000..a87395806
--- /dev/null
+++ b/interface-definitions/include/interface/enable-directed-broadcast.xml.i
@@ -0,0 +1,8 @@
+<!-- include start from interface/enable-directed-broadcast.xml.i -->
+<leafNode name="enable-directed-broadcast">
+ <properties>
+ <help>Enable directed broadcast forwarding on this interface</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/ipv4-options.xml.i b/interface-definitions/include/interface/ipv4-options.xml.i
index bca1229c6..eda77e851 100644
--- a/interface-definitions/include/interface/ipv4-options.xml.i
+++ b/interface-definitions/include/interface/ipv4-options.xml.i
@@ -8,6 +8,7 @@
#include <include/interface/arp-cache-timeout.xml.i>
#include <include/interface/disable-arp-filter.xml.i>
#include <include/interface/disable-forwarding.xml.i>
+ #include <include/interface/enable-directed-broadcast.xml.i>
#include <include/interface/enable-arp-accept.xml.i>
#include <include/interface/enable-arp-announce.xml.i>
#include <include/interface/enable-arp-ignore.xml.i>
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in
index 96dede723..8b6c6ef62 100644
--- a/interface-definitions/interfaces-bonding.xml.in
+++ b/interface-definitions/interfaces-bonding.xml.in
@@ -94,6 +94,23 @@
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
#include <include/interface/mac.xml.i>
+ <leafNode name="mii-mon-interval">
+ <properties>
+ <help>Specifies the MII link monitoring frequency in milliseconds</help>
+ <valueHelp>
+ <format>u32:0</format>
+ <description>Disable MII link monitoring</description>
+ </valueHelp>
+ <valueHelp>
+ <format>u32:50-1000</format>
+ <description>MII link monitoring frequency in milliseconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-0 --range 50-1000"/>
+ </constraint>
+ </properties>
+ <defaultValue>100</defaultValue>
+ </leafNode>
<leafNode name="min-links">
<properties>
<help>Minimum number of member interfaces required up before enabling bond</help>
diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in
index dbb989588..adb48813f 100644
--- a/interface-definitions/interfaces-macsec.xml.in
+++ b/interface-definitions/interfaces-macsec.xml.in
@@ -63,11 +63,12 @@
<properties>
<help>Secure Connectivity Association Key</help>
<valueHelp>
- <format>key</format>
- <description>16-byte (128-bit) hex-string (32 hex-digits)</description>
+ <format>txt</format>
+ <description>16-byte (128-bit) hex-string (32 hex-digits) for gcm-aes-128 or 32-byte (256-bit) hex-string (64 hex-digits) for gcm-aes-256</description>
</valueHelp>
<constraint>
<regex>[A-Fa-f0-9]{32}</regex>
+ <regex>[A-Fa-f0-9]{64}</regex>
</constraint>
</properties>
</leafNode>
@@ -75,7 +76,7 @@
<properties>
<help>Secure Connectivity Association Key Name</help>
<valueHelp>
- <format>key</format>
+ <format>txt</format>
<description>32-byte (256-bit) hex-string (64 hex-digits)</description>
</valueHelp>
<constraint>
diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in
index 0d0ada591..cc1de609d 100644
--- a/interface-definitions/policy.xml.in
+++ b/interface-definitions/policy.xml.in
@@ -639,7 +639,7 @@
</leafNode>
<leafNode name="prefix-len">
<properties>
- <help>IP prefix-length to match</help>
+ <help>IP prefix-length to match (can be used for kernel routes only)</help>
<valueHelp>
<format>u32:0-32</format>
<description>Prefix length</description>
@@ -809,7 +809,7 @@
</leafNode>
<leafNode name="prefix-len">
<properties>
- <help>IPv6 prefix-length to match</help>
+ <help>IPv6 prefix-length to match (can be used for kernel routes only)</help>
<valueHelp>
<format>u32:0-128</format>
<description>Prefix length</description>
diff --git a/interface-definitions/service-ids-ddos-protection.xml.in b/interface-definitions/service-ids-ddos-protection.xml.in
index 5e65d3106..86fc4dffa 100644
--- a/interface-definitions/service-ids-ddos-protection.xml.in
+++ b/interface-definitions/service-ids-ddos-protection.xml.in
@@ -18,6 +18,19 @@
<help>Path to fastnetmon alert script</help>
</properties>
</leafNode>
+ <leafNode name="ban-time">
+ <properties>
+ <help>How long we should keep an IP in blocked state</help>
+ <valueHelp>
+ <format>u32:1-4294967294</format>
+ <description>Time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967294"/>
+ </constraint>
+ </properties>
+ <defaultValue>1900</defaultValue>
+ </leafNode>
<leafNode name="direction">
<properties>
<help>Direction for processing traffic</help>
@@ -30,6 +43,24 @@
<multi/>
</properties>
</leafNode>
+ <leafNode name="excluded-network">
+ <properties>
+ <help>Specify IPv4 and IPv6 networks which are going to be excluded from protection</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix(es) to exclude</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 prefix(es) to exclude</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
<leafNode name="listen-interface">
<properties>
<help>Listen interface for mirroring traffic</help>
@@ -55,13 +86,18 @@
</node>
<leafNode name="network">
<properties>
- <help>Define monitoring networks</help>
+ <help>Specify IPv4 and IPv6 networks which belong to you</help>
<valueHelp>
<format>ipv4net</format>
- <description>Processed network</description>
+ <description>Your IPv4 prefix(es)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>Your IPv6 prefix(es)</description>
</valueHelp>
<constraint>
<validator name="ipv4-prefix"/>
+ <validator name="ipv6-prefix"/>
</constraint>
<multi/>
</properties>
diff --git a/interface-definitions/service-router-advert.xml.in b/interface-definitions/service-router-advert.xml.in
index 258b7b749..87ec512d6 100644
--- a/interface-definitions/service-router-advert.xml.in
+++ b/interface-definitions/service-router-advert.xml.in
@@ -249,6 +249,18 @@
<valueless/>
</properties>
</leafNode>
+ <leafNode name="deprecate-prefix">
+ <properties>
+ <help>Upon shutdown, this option will deprecate the prefix by announcing it in the shutdown RA</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="decrement-lifetime">
+ <properties>
+ <help>Lifetime is decremented by the number of seconds since the last RA - use in conjunction with a DHCPv6-PD prefix</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<leafNode name="preferred-lifetime">
<properties>
<help>Time in seconds that the prefix will remain preferred</help>
diff --git a/interface-definitions/system-ip.xml.in b/interface-definitions/system-ip.xml.in
index 21d70694b..e00dbf252 100644
--- a/interface-definitions/system-ip.xml.in
+++ b/interface-definitions/system-ip.xml.in
@@ -23,6 +23,12 @@
<valueless/>
</properties>
</leafNode>
+ <leafNode name="disable-directed-broadcast">
+ <properties>
+ <help>Disable IPv4 directed broadcast forwarding on all interfaces</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<node name="multipath">
<properties>
<help>IPv4 multipath settings</help>
diff --git a/interface-definitions/xml-component-version.xml.in b/interface-definitions/xml-component-version.xml.in
index b7f063a6c..cf86f83d6 100644
--- a/interface-definitions/xml-component-version.xml.in
+++ b/interface-definitions/xml-component-version.xml.in
@@ -20,6 +20,7 @@
#include <include/version/l2tp-version.xml.i>
#include <include/version/lldp-version.xml.i>
#include <include/version/mdns-version.xml.i>
+ #include <include/version/monitoring-version.xml.i>
#include <include/version/nat66-version.xml.i>
#include <include/version/nat-version.xml.i>
#include <include/version/ntp-version.xml.i>
diff --git a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i
index 084f5da83..d2804e3b3 100644
--- a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i
+++ b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i
@@ -151,7 +151,7 @@
</leafNode>
<tagNode name="neighbors">
<properties>
- <help>Show detailed BGP IPv4 unicast neighbor information</help>
+ <help>Show BGP information for specified neighbor</help>
<completionHelp>
<script>vtysh -c 'show bgp summary' | awk '{print $1'} | grep -e '^[0-9a-f]'</script>
</completionHelp>
diff --git a/op-mode-definitions/ipv4-route.xml.in b/op-mode-definitions/ipv4-route.xml.in
index 8f001d5bb..660b34496 100644
--- a/op-mode-definitions/ipv4-route.xml.in
+++ b/op-mode-definitions/ipv4-route.xml.in
@@ -39,7 +39,7 @@
<list>&lt;x.x.x.x&gt;</list>
</completionHelp>
</properties>
- <command>sudo ip neigh flush to "$5"</command>
+ <command>sudo ${vyos_op_scripts_dir}/neighbor.py reset --family inet --address "$5"</command>
</tagNode>
<tagNode name="interface">
<properties>
@@ -48,8 +48,14 @@
<script>${vyos_completion_dir}/list_interfaces.py</script>
</completionHelp>
</properties>
- <command>sudo ip neigh flush dev "$5"</command>
+ <command>sudo ${vyos_op_scripts_dir}/neighbor.py reset --family inet --interface "$5"</command>
</tagNode>
+ <node name="table">
+ <properties>
+ <help>Flush the ARP cache completely</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/neighbor.py reset --family inet</command>
+ </node>
</children>
</node>
<node name="route">
diff --git a/op-mode-definitions/ipv6-route.xml.in b/op-mode-definitions/ipv6-route.xml.in
index 4f8792f9f..d75caf308 100644
--- a/op-mode-definitions/ipv6-route.xml.in
+++ b/op-mode-definitions/ipv6-route.xml.in
@@ -20,7 +20,7 @@
<properties>
<help>Show IPv6 neighbor (NDP) table</help>
</properties>
- <command>${vyos_op_scripts_dir}/show_neigh.py --family inet6</command>
+ <command>${vyos_op_scripts_dir}/neighbor.py show --family inet6</command>
<children>
<tagNode name="interface">
<properties>
@@ -29,7 +29,7 @@
<script>${vyos_completion_dir}/list_interfaces.py -b</script>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/show_neigh.py --family inet6 --interface "$5"</command>
+ <command>${vyos_op_scripts_dir}/neighbor.py show --family inet6 --interface "$5"</command>
</tagNode>
<tagNode name="state">
<properties>
@@ -38,7 +38,7 @@
<list>reachable stale failed permanent</list>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/show_neigh.py --family inet6 --state "$5"</command>
+ <command>${vyos_op_scripts_dir}/neighbor.py show --family inet6 --state "$5"</command>
</tagNode>
</children>
</node>
diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in
index f5e0ede59..fbe37c9db 100644
--- a/op-mode-definitions/monitor-log.xml.in
+++ b/op-mode-definitions/monitor-log.xml.in
@@ -14,6 +14,19 @@
</properties>
<command>grc journalctl --no-hostname --follow --boot</command>
</node>
+ <node name="ids">
+ <properties>
+ <help>Monitor log for Intrusion Detection System</help>
+ </properties>
+ <children>
+ <leafNode name="ddos-protection">
+ <properties>
+ <help>Monitor last lines of DDOS protection</help>
+ </properties>
+ <command>journalctl --no-hostname --follow --boot --unit fastnetmon.service</command>
+ </leafNode>
+ </children>
+ </node>
<node name="dhcp">
<properties>
<help>Monitor last lines of Dynamic Host Control Protocol (DHCP)</help>
@@ -111,7 +124,7 @@
</node>
<node name="protocol">
<properties>
- <help>Monitor log for Routing Protocols</help>
+ <help>Monitor log for Routing Protocol</help>
</properties>
<children>
<leafNode name="ospf">
@@ -182,6 +195,18 @@
</leafNode>
</children>
</node>
+ <leafNode name="snmp">
+ <properties>
+ <help>Monitor last lines of Simple Network Monitoring Protocol (SNMP)</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --follow --unit snmpd.service</command>
+ </leafNode>
+ <leafNode name="ssh">
+ <properties>
+ <help>Monitor last lines of Secure Shell (SSH)</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --follow --unit ssh.service</command>
+ </leafNode>
</children>
</node>
</children>
diff --git a/op-mode-definitions/nat.xml.in b/op-mode-definitions/nat.xml.in
index 084e2e7e3..b0ec8989f 100644
--- a/op-mode-definitions/nat.xml.in
+++ b/op-mode-definitions/nat.xml.in
@@ -16,13 +16,13 @@
<properties>
<help>Show configured source NAT rules</help>
</properties>
- <command>${vyos_op_scripts_dir}/show_nat_rules.py --source</command>
+ <command>${vyos_op_scripts_dir}/nat.py show_rules --direction source</command>
</node>
<node name="statistics">
<properties>
<help>Show statistics for configured source NAT rules</help>
</properties>
- <command>${vyos_op_scripts_dir}/show_nat_statistics.py --source</command>
+ <command>${vyos_op_scripts_dir}/nat.py show_statistics --direction source</command>
</node>
<node name="translations">
<properties>
diff --git a/op-mode-definitions/show-arp.xml.in b/op-mode-definitions/show-arp.xml.in
index 58cc6e45e..8662549fc 100644
--- a/op-mode-definitions/show-arp.xml.in
+++ b/op-mode-definitions/show-arp.xml.in
@@ -6,7 +6,7 @@
<properties>
<help>Show Address Resolution Protocol (ARP) information</help>
</properties>
- <command>${vyos_op_scripts_dir}/show_neigh.py --family inet</command>
+ <command>${vyos_op_scripts_dir}/neighbor.py show --family inet</command>
<children>
<tagNode name="interface">
<properties>
@@ -15,7 +15,7 @@
<script>${vyos_completion_dir}/list_interfaces.py -b</script>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --interface "$4"</command>
+ <command>${vyos_op_scripts_dir}/neighbor.py show --family inet --interface "$4"</command>
</tagNode>
</children>
</node>
diff --git a/op-mode-definitions/show-bridge.xml.in b/op-mode-definitions/show-bridge.xml.in
index 0f8d3064d..dd2a28931 100644
--- a/op-mode-definitions/show-bridge.xml.in
+++ b/op-mode-definitions/show-bridge.xml.in
@@ -11,7 +11,7 @@
<properties>
<help>View the VLAN filter settings of the bridge</help>
</properties>
- <command>bridge -c vlan show</command>
+ <command>${vyos_op_scripts_dir}/bridge.py show_vlan</command>
</leafNode>
</children>
</node>
@@ -19,7 +19,7 @@
<properties>
<help>Show bridging information</help>
</properties>
- <command>bridge -c link show</command>
+ <command>${vyos_op_scripts_dir}/bridge.py show</command>
</leafNode>
<tagNode name="bridge">
<properties>
@@ -34,13 +34,13 @@
<properties>
<help>Displays the multicast group database for the bridge</help>
</properties>
- <command>bridge -c mdb show dev $3</command>
+ <command>${vyos_op_scripts_dir}/bridge.py show_mdb --interface=$3</command>
</leafNode>
<leafNode name="fdb">
<properties>
<help>Show the forwarding database of the bridge</help>
</properties>
- <command>bridge -c fdb show br $3</command>
+ <command>${vyos_op_scripts_dir}/bridge.py show_fdb --interface=$3</command>
</leafNode>
</children>
</tagNode>
diff --git a/op-mode-definitions/show-conntrack.xml.in b/op-mode-definitions/show-conntrack.xml.in
new file mode 100644
index 000000000..8d921e6a5
--- /dev/null
+++ b/op-mode-definitions/show-conntrack.xml.in
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="conntrack">
+ <properties>
+ <help>Show conntrack tables entries</help>
+ </properties>
+ <children>
+ <node name="table">
+ <properties>
+ <help>Show conntrack entries for table</help>
+ </properties>
+ <children>
+ <node name="ipv4">
+ <properties>
+ <help>Show conntrack entries for IPv4 protocol</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/conntrack.py show --family inet</command>
+ </node>
+ <node name="ipv6">
+ <properties>
+ <help>Show conntrack entries for IPv6 protocol</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/conntrack.py show --family inet6</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-hardware.xml.in b/op-mode-definitions/show-hardware.xml.in
index 20fdd753d..ebd806ba5 100644
--- a/op-mode-definitions/show-hardware.xml.in
+++ b/op-mode-definitions/show-hardware.xml.in
@@ -9,21 +9,21 @@
<children>
<node name="cpu">
<properties>
- <help>Show CPU info</help>
+ <help>Show CPU informaion</help>
</properties>
- <command>lscpu</command>
+ <command>${vyos_op_scripts_dir}/cpu.py show</command>
<children>
<node name="detail">
<properties>
- <help> Show system CPU details</help>
+ <help>Show system CPU details</help>
</properties>
<command>cat /proc/cpuinfo</command>
</node>
<node name="summary">
<properties>
- <help>Show system CPUs</help>
+ <help>Show system CPUs summary</help>
</properties>
- <command>${vyos_op_scripts_dir}/cpu_summary.py</command>
+ <command>${vyos_op_scripts_dir}/cpu.py show_summary</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/show-ip.xml.in b/op-mode-definitions/show-ip.xml.in
index d342ac192..0751c50cb 100644
--- a/op-mode-definitions/show-ip.xml.in
+++ b/op-mode-definitions/show-ip.xml.in
@@ -11,7 +11,7 @@
<properties>
<help>Show IPv4 neighbor (ARP) table</help>
</properties>
- <command>${vyos_op_scripts_dir}/show_neigh.py --family inet</command>
+ <command>${vyos_op_scripts_dir}/neighbor.py show --family inet</command>
<children>
<tagNode name="interface">
<properties>
@@ -20,7 +20,7 @@
<script>${vyos_completion_dir}/list_interfaces.py -b</script>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --interface "$5"</command>
+ <command>${vyos_op_scripts_dir}/neighbor.py show --family inet --interface "$5"</command>
</tagNode>
<tagNode name="state">
<properties>
@@ -29,7 +29,7 @@
<list>reachable stale failed permanent</list>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --state "$5"</command>
+ <command>${vyos_op_scripts_dir}/neighbor.py show --family inet --state "$5"</command>
</tagNode>
</children>
</node>
diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in
index 76879e5d6..7769c5f52 100644
--- a/op-mode-definitions/show-log.xml.in
+++ b/op-mode-definitions/show-log.xml.in
@@ -32,6 +32,19 @@
</properties>
<command>journalctl --no-hostname --boot --unit conntrackd.service</command>
</leafNode>
+ <node name="ids">
+ <properties>
+ <help>Show log for for Intrusion Detection System</help>
+ </properties>
+ <children>
+ <leafNode name="ddos-protection">
+ <properties>
+ <help>Show log for DDOS protection</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit fastnetmon.service</command>
+ </leafNode>
+ </children>
+ </node>
<node name="dhcp">
<properties>
<help>Show log for Dynamic Host Control Protocol (DHCP)</help>
@@ -243,7 +256,7 @@
</node>
<node name="protocol">
<properties>
- <help>Show log for Routing Protocols</help>
+ <help>Show log for Routing Protocol</help>
</properties>
<children>
<leafNode name="ospf">
@@ -320,6 +333,12 @@
</properties>
<command>journalctl --no-hostname --boot --unit snmpd.service</command>
</leafNode>
+ <leafNode name="ssh">
+ <properties>
+ <help>Show log for Secure Shell (SSH)</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit ssh.service</command>
+ </leafNode>
<tagNode name="tail">
<properties>
<help>Show last n changes to messages</help>
diff --git a/op-mode-definitions/show-system.xml.in b/op-mode-definitions/show-system.xml.in
index 68b473bc1..6f05d0c12 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.py</command>
+ <command>${vyos_op_scripts_dir}/memory.py show</command>
<children>
<leafNode name="cache">
<properties>
diff --git a/op-mode-definitions/show-version.xml.in b/op-mode-definitions/show-version.xml.in
index 8b7cc7e58..d9c4738af 100644
--- a/op-mode-definitions/show-version.xml.in
+++ b/op-mode-definitions/show-version.xml.in
@@ -6,13 +6,13 @@
<properties>
<help>Show system version information</help>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/show_version.py</command>
+ <command>sudo ${vyos_op_scripts_dir}/version.py show</command>
<children>
<leafNode name="funny">
<properties>
<help>Show system version and some fun stuff</help>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/show_version.py --funny</command>
+ <command>sudo ${vyos_op_scripts_dir}/version.py show --funny</command>
</leafNode>
<leafNode name="all">
<properties>
diff --git a/op-mode-definitions/show-vrf.xml.in b/op-mode-definitions/show-vrf.xml.in
index 9c38c30fe..0e0370445 100644
--- a/op-mode-definitions/show-vrf.xml.in
+++ b/op-mode-definitions/show-vrf.xml.in
@@ -6,7 +6,7 @@
<properties>
<help>Show VRF information</help>
</properties>
- <command>${vyos_op_scripts_dir}/show_vrf.py -e</command>
+ <command>${vyos_op_scripts_dir}/vrf.py show</command>
</node>
<tagNode name="vrf">
<properties>
@@ -15,7 +15,7 @@
<path>vrf name</path>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command>
+ <command>${vyos_op_scripts_dir}/vrf.py show --name="$3"</command>
<children>
<leafNode name="processes">
<properties>
diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in
index 3d997c143..a98cf8ff2 100644
--- a/op-mode-definitions/vpn-ipsec.xml.in
+++ b/op-mode-definitions/vpn-ipsec.xml.in
@@ -19,16 +19,16 @@
<properties>
<help>Reset a specific tunnel for given peer</help>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="reset-peer" --name="$4" --tunnel="$6"</command>
+ <command>sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="$6"</command>
</tagNode>
<node name="vti">
<properties>
<help>Reset the VTI tunnel for given peer</help>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="reset-peer" --name="$4" --tunnel="vti"</command>
+ <command>sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="vti"</command>
</node>
</children>
- <command>sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="reset-peer" --name="$4" --tunnel="all"</command>
+ <command>sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="all"</command>
</tagNode>
<tagNode name="ipsec-profile">
<properties>
@@ -76,6 +76,9 @@
<tagNode name="peer">
<properties>
<help>Show debugging information for a peer</help>
+ <completionHelp>
+ <path>vpn ipsec site-to-site peer</path>
+ </completionHelp>
</properties>
<children>
<tagNode name="tunnel">
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 78225f8d4..a61666afc 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -201,11 +201,12 @@ def is_member(conf, interface, intftype=None):
intftype is optional, if not passed it will search all known types
(currently bridge and bonding)
- Returns:
- None -> Interface is not a member
- interface name -> Interface is a member of this interface
- False -> interface type cannot have members
+ Returns: dict
+ empty -> Interface is not a member
+ key -> Interface is a member of this interface
"""
+ from vyos.ifconfig import Section
+
ret_val = {}
intftypes = ['bonding', 'bridge']
@@ -221,9 +222,24 @@ def is_member(conf, interface, intftype=None):
for intf in conf.list_nodes(base):
member = base + [intf, 'member', 'interface', interface]
if conf.exists(member):
- tmp = conf.get_config_dict(member, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
- ret_val.update({intf : tmp})
+ member_type = Section.section(interface)
+ # Check if it's a VLAN (QinQ) interface
+ interface = interface.split('.')
+ if len(interface) == 3:
+ if conf.exists(['interfaces', member_type, interface[0], 'vif-s', interface[1], 'vif-c', interface[2]]):
+ tmp = conf.get_config_dict(['interfaces', member_type, interface[0]],
+ key_mangling=('-', '_'), get_first_key=True)
+ ret_val.update({intf : tmp})
+ elif len(interface) == 2:
+ if conf.exists(['interfaces', member_type, interface[0], 'vif', interface[1]]):
+ tmp = conf.get_config_dict(['interfaces', member_type, interface[0]],
+ key_mangling=('-', '_'), get_first_key=True)
+ ret_val.update({intf : tmp})
+ else:
+ if conf.exists(['interfaces', member_type, interface[0]]):
+ tmp = conf.get_config_dict(['interfaces', member_type, interface[0]],
+ key_mangling=('-', '_'), get_first_key=True)
+ ret_val.update({intf : tmp})
return ret_val
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 438485d98..137eb9f79 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -99,6 +99,18 @@ def verify_vrf(config):
'Interface "{ifname}" cannot be both a member of VRF "{vrf}" '
'and bridge "{is_bridge_member}"!'.format(**config))
+def verify_bond_bridge_member(config):
+ """
+ Checks if interface has a VRF configured and is also part of a bond or
+ bridge, which is not allowed!
+ """
+ if 'vrf' in config:
+ ifname = config['ifname']
+ if 'is_bond_member' in config:
+ raise ConfigError(f'Can not add interface "{ifname}" to bond, it has a VRF assigned!')
+ if 'is_bridge_member' in config:
+ raise ConfigError(f'Can not add interface "{ifname}" to bridge, it has a VRF assigned!')
+
def verify_tunnel(config):
"""
This helper is used to verify the common part of the tunnel
diff --git a/python/vyos/cpu.py b/python/vyos/cpu.py
index a0ef864be..488ae79fb 100644
--- a/python/vyos/cpu.py
+++ b/python/vyos/cpu.py
@@ -32,7 +32,8 @@ import re
def _read_cpuinfo():
with open('/proc/cpuinfo', 'r') as f:
- return f.readlines()
+ lines = f.read().strip()
+ return re.split(r'\n+', lines)
def _split_line(l):
l = l.strip()
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index fcb6a7fbc..09ae73eac 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -18,6 +18,7 @@ import os
directories = {
"data": "/usr/share/vyos/",
"conf_mode": "/usr/libexec/vyos/conf_mode",
+ "op_mode": "/usr/libexec/vyos/op_mode",
"config": "/opt/vyatta/etc/config",
"current": "/opt/vyatta/etc/config-migrate/current",
"migrate": "/opt/vyatta/etc/config-migrate/migrate",
@@ -49,6 +50,7 @@ api_data = {
'socket' : False,
'strict' : False,
'gql' : False,
+ 'introspection' : False,
'debug' : False,
'api_keys' : [ {"id": "testapp", "key": "qwerty"} ]
}
diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py
index 2b9afe109..98bf6162b 100644
--- a/python/vyos/ifconfig/bond.py
+++ b/python/vyos/ifconfig/bond.py
@@ -179,6 +179,21 @@ class BondIf(Interface):
"""
self.set_interface('bond_lacp_rate', slow_fast)
+ def set_miimon_interval(self, interval):
+ """
+ Specifies the MII link monitoring frequency in milliseconds. This
+ determines how often the link state of each slave is inspected for link
+ failures. A value of zero disables MII link monitoring. A value of 100
+ is a good starting point.
+
+ The default value is 0.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_miimon_interval('100')
+ """
+ return self.set_interface('bond_miimon', interval)
+
def set_arp_interval(self, interval):
"""
Specifies the ARP link monitoring frequency in milliseconds.
@@ -202,16 +217,7 @@ class BondIf(Interface):
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').set_arp_interval('100')
"""
- if int(interval) == 0:
- """
- Specifies the MII link monitoring frequency in milliseconds.
- This determines how often the link state of each slave is
- inspected for link failures. A value of zero disables MII
- link monitoring. A value of 100 is a good starting point.
- """
- return self.set_interface('bond_miimon', interval)
- else:
- return self.set_interface('bond_arp_interval', interval)
+ return self.set_interface('bond_arp_interval', interval)
def get_arp_ip_target(self):
"""
@@ -381,26 +387,9 @@ class BondIf(Interface):
if 'shutdown_required' in config:
self.set_admin_state('down')
- # ARP monitor targets need to be synchronized between sysfs and CLI.
- # Unfortunately an address can't be send twice to sysfs as this will
- # result in the following exception: OSError: [Errno 22] Invalid argument.
- #
- # We remove ALL addresses prior to adding new ones, this will remove
- # addresses manually added by the user too - but as we are limited to 16 adresses
- # from the kernel side this looks valid to me. We won't run into an error
- # when a user added manual adresses which would result in having more
- # then 16 adresses in total.
- arp_tgt_addr = list(map(str, self.get_arp_ip_target().split()))
- for addr in arp_tgt_addr:
- self.set_arp_ip_target('-' + addr)
-
- # Add configured ARP target addresses
- value = dict_search('arp_monitor.target', config)
- if isinstance(value, str):
- value = [value]
- if value:
- for addr in value:
- self.set_arp_ip_target('+' + addr)
+ # Specifies the MII link monitoring frequency in milliseconds
+ value = config.get('mii_mon_interval')
+ self.set_miimon_interval(value)
# Bonding transmit hash policy
value = config.get('hash_policy')
@@ -430,6 +419,32 @@ class BondIf(Interface):
if mode == '802.3ad':
self.set_lacp_rate(config.get('lacp_rate'))
+ if mode not in ['802.3ad', 'balance-tlb', 'balance-alb']:
+ tmp = dict_search('arp_monitor.interval', config)
+ value = tmp if (tmp != None) else '0'
+ self.set_arp_interval(value)
+
+ # ARP monitor targets need to be synchronized between sysfs and CLI.
+ # Unfortunately an address can't be send twice to sysfs as this will
+ # result in the following exception: OSError: [Errno 22] Invalid argument.
+ #
+ # We remove ALL addresses prior to adding new ones, this will remove
+ # addresses manually added by the user too - but as we are limited to 16 adresses
+ # from the kernel side this looks valid to me. We won't run into an error
+ # when a user added manual adresses which would result in having more
+ # then 16 adresses in total.
+ arp_tgt_addr = list(map(str, self.get_arp_ip_target().split()))
+ for addr in arp_tgt_addr:
+ self.set_arp_ip_target('-' + addr)
+
+ # Add configured ARP target addresses
+ value = dict_search('arp_monitor.target', config)
+ if isinstance(value, str):
+ value = [value]
+ if value:
+ for addr in value:
+ self.set_arp_ip_target('+' + addr)
+
# Add (enslave) interfaces to bond
value = dict_search('member.interface', config)
for interface in (value or []):
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 22441d1d2..555494f80 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -168,6 +168,10 @@ class Interface(Control):
'validate': assert_boolean,
'location': '/proc/sys/net/ipv4/conf/{ifname}/forwarding',
},
+ 'ipv4_directed_broadcast': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding',
+ },
'rp_filter': {
'validate': lambda flt: assert_range(flt,0,3),
'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter',
@@ -234,6 +238,9 @@ class Interface(Control):
'ipv4_forwarding': {
'location': '/proc/sys/net/ipv4/conf/{ifname}/forwarding',
},
+ 'ipv4_directed_broadcast': {
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding',
+ },
'rp_filter': {
'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter',
},
@@ -713,6 +720,13 @@ class Interface(Control):
return None
return self.set_interface('ipv4_forwarding', forwarding)
+ def set_ipv4_directed_broadcast(self, forwarding):
+ """ Configure IPv4 directed broadcast forwarding. """
+ tmp = self.get_interface('ipv4_directed_broadcast')
+ if tmp == forwarding:
+ return None
+ return self.set_interface('ipv4_directed_broadcast', forwarding)
+
def set_ipv4_source_validation(self, value):
"""
Help prevent attacks used by Spoofing IP Addresses. Reverse path
@@ -1305,8 +1319,9 @@ class Interface(Control):
# clear existing ingess - ignore errors (e.g. "Error: Cannot find specified
# qdisc on specified device") - we simply cleanup all stuff here
- self._popen(f'tc qdisc del dev {source_if} parent ffff: 2>/dev/null');
- self._popen(f'tc qdisc del dev {source_if} parent 1: 2>/dev/null');
+ if not 'traffic_policy' in self._config:
+ self._popen(f'tc qdisc del dev {source_if} parent ffff: 2>/dev/null');
+ self._popen(f'tc qdisc del dev {source_if} parent 1: 2>/dev/null');
# Apply interface mirror policy
if mirror_config:
@@ -1439,14 +1454,22 @@ class Interface(Control):
if dhcpv6pd:
self.set_dhcpv6(True)
- # There are some items in the configuration which can only be applied
- # if this instance is not bound to a bridge. This should be checked
- # by the caller but better save then sorry!
- if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config):
- # Bind interface to given VRF or unbind it if vrf node is not set.
- # unbinding will call 'ip link set dev eth0 nomaster' which will
- # also drop the interface out of a bridge or bond - thus this is
- # checked before
+ # XXX: Bind interface to given VRF or unbind it if vrf is not set. Unbinding
+ # will call 'ip link set dev eth0 nomaster' which will also drop the
+ # interface out of any bridge or bond - thus this is checked before.
+ if 'is_bond_member' in config:
+ bond_if = next(iter(config['is_bond_member']))
+ tmp = get_interface_config(config['ifname'])
+ if 'master' in tmp and tmp['master'] != bond_if:
+ self.set_vrf('')
+
+ elif 'is_bridge_member' in config:
+ bridge_if = next(iter(config['is_bridge_member']))
+ tmp = get_interface_config(config['ifname'])
+ if 'master' in tmp and tmp['master'] != bridge_if:
+ self.set_vrf('')
+
+ else:
self.set_vrf(config.get('vrf', ''))
# Add this section after vrf T4331
@@ -1498,6 +1521,11 @@ class Interface(Control):
value = '0' if (tmp != None) else '1'
self.set_ipv4_forwarding(value)
+ # IPv4 directed broadcast forwarding
+ tmp = dict_search('ip.enable_directed_broadcast', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_ipv4_directed_broadcast(value)
+
# IPv4 source-validation
tmp = dict_search('ip.source_validation', config)
value = tmp if (tmp != None) else '0'
@@ -1555,8 +1583,8 @@ class Interface(Control):
# re-add ourselves to any bridge we might have fallen out of
if 'is_bridge_member' in config:
- bridge_dict = config.get('is_bridge_member')
- self.add_to_bridge(bridge_dict)
+ tmp = config.get('is_bridge_member')
+ self.add_to_bridge(tmp)
# eXpress Data Path - highly experimental
self.set_xdp('xdp' in config)
diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py
new file mode 100644
index 000000000..0af4359c6
--- /dev/null
+++ b/python/vyos/opmode.py
@@ -0,0 +1,128 @@
+# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# 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 sys
+import typing
+
+
+def _is_op_mode_function_name(name):
+ if re.match(r"^(show|clear|reset|restart)", name):
+ return True
+ else:
+ return False
+
+def _is_show(name):
+ if re.match(r"^show", name):
+ return True
+ else:
+ return False
+
+def _get_op_mode_functions(module):
+ from inspect import getmembers, isfunction
+
+ # Get all functions in that module
+ funcs = getmembers(module, isfunction)
+
+ # getmembers returns (name, func) tuples
+ funcs = list(filter(lambda ft: _is_op_mode_function_name(ft[0]), funcs))
+
+ funcs_dict = {}
+ for (name, thunk) in funcs:
+ funcs_dict[name] = thunk
+
+ return funcs_dict
+
+def _is_optional_type(t):
+ # Optional[t] is internally an alias for Union[t, NoneType]
+ # and there's no easy way to get union members it seems
+ if (type(t) == typing._UnionGenericAlias):
+ if (len(t.__args__) == 2):
+ if t.__args__[1] == type(None):
+ return True
+
+ return False
+
+def _get_arg_type(t):
+ """ Returns the type itself if it's a primitive type,
+ or the "real" type of typing.Optional
+
+ Doesn't work with anything else at the moment!
+ """
+ if _is_optional_type(t):
+ return t.__args__[0]
+ else:
+ return t
+
+def run(module):
+ from argparse import ArgumentParser
+
+ functions = _get_op_mode_functions(module)
+
+ parser = ArgumentParser()
+ subparsers = parser.add_subparsers(dest="subcommand")
+
+ for function_name in functions:
+ subparser = subparsers.add_parser(function_name, help=functions[function_name].__doc__)
+
+ type_hints = typing.get_type_hints(functions[function_name])
+ for opt in type_hints:
+ th = type_hints[opt]
+
+ if _get_arg_type(th) == bool:
+ subparser.add_argument(f"--{opt}", action='store_true')
+ else:
+ if _is_optional_type(th):
+ subparser.add_argument(f"--{opt}", type=_get_arg_type(th), default=None)
+ else:
+ subparser.add_argument(f"--{opt}", type=_get_arg_type(th), required=True)
+
+ # Get options as a dict rather than a namespace,
+ # so that we can modify it and pack for passing to functions
+ args = vars(parser.parse_args())
+
+ if not args["subcommand"]:
+ print("Subcommand required!")
+ parser.print_usage()
+ sys.exit(1)
+
+ function_name = args["subcommand"]
+ func = functions[function_name]
+
+ # Remove the subcommand from the arguments,
+ # it would cause an extra argument error when we pass the dict to a function
+ del args["subcommand"]
+
+ # Show commands must always get the "raw" argument,
+ # but other commands (clear/reset/restart) should not,
+ # because they produce no output and it makes no sense for them.
+ if ("raw" not in args) and _is_show(function_name):
+ args["raw"] = False
+
+ if re.match(r"^show", function_name):
+ # Show commands are slightly special:
+ # they may return human-formatted output
+ # or a raw dict that we need to serialize in JSON for printing
+ res = func(**args)
+ if not args["raw"]:
+ return res
+ else:
+ from json import dumps
+ return dumps(res, indent=4)
+ else:
+ # Other functions should not return anything,
+ # although they may print their own warnings or status messages
+ func(**args)
+
diff --git a/python/vyos/util.py b/python/vyos/util.py
index bee5d7aec..b86b1949c 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -164,6 +164,27 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
return decoded
+def rc_cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=PIPE, stderr=STDOUT, decode='utf-8'):
+ """
+ A wrapper around popen, which returns the return code
+ of a command and stdout
+
+ % rc_cmd('uname')
+ (0, 'Linux')
+ % rc_cmd('ip link show dev eth99')
+ (1, 'Device "eth99" does not exist.')
+ """
+ out, code = popen(
+ command, flag,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ decode=decode,
+ )
+ return code, out
+
+
def call(command, flag='', shell=None, input=None, timeout=None, env=None,
stdout=PIPE, stderr=PIPE, decode='utf-8'):
"""
diff --git a/python/vyos/validate.py b/python/vyos/validate.py
index e005da0e4..a83193363 100644
--- a/python/vyos/validate.py
+++ b/python/vyos/validate.py
@@ -264,3 +264,22 @@ def has_address_configured(conf, intf):
conf.set_level(old_level)
return ret
+
+def has_vrf_configured(conf, intf):
+ """
+ Checks if interface has a VRF configured.
+
+ Returns True if interface has VRF configured, False if it doesn't.
+ """
+ from vyos.ifconfig import Section
+ ret = False
+
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ tmp = ['interfaces', Section.get_config_path(intf), 'vrf']
+ if conf.exists(tmp):
+ ret = True
+
+ conf.set_level(old_level)
+ return ret
diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index 8acf52243..55343b893 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -638,6 +638,7 @@ class BasicInterfaceTest:
self.cli_set(path + ['ip', 'arp-cache-timeout', arp_tmo])
self.cli_set(path + ['ip', 'disable-arp-filter'])
self.cli_set(path + ['ip', 'disable-forwarding'])
+ self.cli_set(path + ['ip', 'enable-directed-broadcast'])
self.cli_set(path + ['ip', 'enable-arp-accept'])
self.cli_set(path + ['ip', 'enable-arp-announce'])
self.cli_set(path + ['ip', 'enable-arp-ignore'])
@@ -674,6 +675,9 @@ class BasicInterfaceTest:
tmp = read_file(f'{proc_base}/forwarding')
self.assertEqual('0', tmp)
+ tmp = read_file(f'{proc_base}/bc_forwarding')
+ self.assertEqual('1', tmp)
+
tmp = read_file(f'{proc_base}/proxy_arp')
self.assertEqual('1', tmp)
diff --git a/smoketest/scripts/cli/test_component_version.py b/smoketest/scripts/cli/test_component_version.py
index 777379bdd..1355c1f94 100755
--- a/smoketest/scripts/cli/test_component_version.py
+++ b/smoketest/scripts/cli/test_component_version.py
@@ -26,11 +26,25 @@ class TestComponentVersion(unittest.TestCase):
def setUp(self):
self.legacy_d = get_system_versions()
self.xml_d = get_system_component_version()
+ self.set_legacy_d = set(self.legacy_d)
+ self.set_xml_d = set(self.xml_d)
def test_component_version(self):
- self.assertTrue(set(self.legacy_d).issubset(set(self.xml_d)))
+ bool_issubset = (self.set_legacy_d.issubset(self.set_xml_d))
+ if not bool_issubset:
+ missing = self.set_legacy_d.difference(self.set_xml_d)
+ print(f'\n\ncomponents in legacy but not in XML: {missing}')
+ print('new components must be listed in xml-component-version.xml.in')
+ self.assertTrue(bool_issubset)
+
+ bad_component_version = False
for k, v in self.legacy_d.items():
- self.assertTrue(v <= self.xml_d[k])
+ bool_inequality = (v <= self.xml_d[k])
+ if not bool_inequality:
+ print(f'\n\n{k} has not been updated in XML component versions:')
+ print(f'legacy version {v}; XML version {self.xml_d[k]}')
+ bad_component_version = True
+ self.assertFalse(bad_component_version)
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py
index 237abb487..cd3995ed9 100755
--- a/smoketest/scripts/cli/test_interfaces_bonding.py
+++ b/smoketest/scripts/cli/test_interfaces_bonding.py
@@ -49,7 +49,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
if not '.' in tmp:
cls._members.append(tmp)
- cls._options['bond0'] = []
+ cls._options = {'bond0' : []}
for member in cls._members:
cls._options['bond0'].append(f'member interface {member}')
cls._interfaces = list(cls._options)
@@ -136,7 +136,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
def test_bonding_hash_policy(self):
# Define available bonding hash policies
- hash_policies = ['layer2', 'layer2+3', 'layer2+3', 'encap2+3', 'encap3+4']
+ hash_policies = ['layer2', 'layer2+3', 'encap2+3', 'encap3+4']
for hash_policy in hash_policies:
for interface in self._interfaces:
for option in self._options.get(interface, []):
@@ -151,6 +151,29 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
defined_policy = read_file(f'/sys/class/net/{interface}/bonding/xmit_hash_policy').split()
self.assertEqual(defined_policy[0], hash_policy)
+ def test_bonding_mii_monitoring_interval(self):
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_commit()
+
+ # verify default
+ for interface in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{interface}/bonding/miimon').split()
+ self.assertIn('100', tmp)
+
+ mii_mon = '250'
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'mii-mon-interval', mii_mon])
+
+ self.cli_commit()
+
+ # verify new CLI value
+ for interface in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{interface}/bonding/miimon').split()
+ self.assertIn(mii_mon, tmp)
+
def test_bonding_multi_use_member(self):
# Define available bonding hash policies
for interface in ['bond10', 'bond20']:
@@ -165,6 +188,46 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_commit()
+ def test_bonding_source_interface(self):
+ # Re-use member interface that is already a source-interface
+ bond = 'bond99'
+ pppoe = 'pppoe98756'
+ member = next(iter(self._members))
+
+ self.cli_set(self._base_path + [bond, 'member', 'interface', member])
+ self.cli_set(['interfaces', 'pppoe', pppoe, 'source-interface', member])
+
+ # check validate() - can not add interface to bond, it is the source-interface of ...
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(['interfaces', 'pppoe', pppoe])
+ self.cli_commit()
+
+ # verify config
+ slaves = read_file(f'/sys/class/net/{bond}/bonding/slaves').split()
+ self.assertIn(member, slaves)
+
+ def test_bonding_source_bridge_interface(self):
+ # Re-use member interface that is already a source-interface
+ bond = 'bond1097'
+ bridge = 'br6327'
+ member = next(iter(self._members))
+
+ self.cli_set(self._base_path + [bond, 'member', 'interface', member])
+ self.cli_set(['interfaces', 'bridge', bridge, 'member', 'interface', member])
+
+ # check validate() - can not add interface to bond, it is a member of bridge ...
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(['interfaces', 'bridge', bridge])
+ self.cli_commit()
+
+ # verify config
+ slaves = read_file(f'/sys/class/net/{bond}/bonding/slaves').split()
+ self.assertIn(member, slaves)
+
def test_bonding_uniq_member_description(self):
ethernet_path = ['interfaces', 'ethernet']
for interface in self._interfaces:
diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py
index 26d3a23c9..664dc48bc 100755
--- a/smoketest/scripts/cli/test_interfaces_bridge.py
+++ b/smoketest/scripts/cli/test_interfaces_bridge.py
@@ -304,7 +304,7 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_delete(self._base_path + [interface, 'member'])
- def test_bridge_vlan_members(self):
+ def test_bridge_vif_members(self):
# T2945: ensure that VIFs are not dropped from bridge
vifs = ['300', '400']
for interface in self._interfaces:
@@ -329,5 +329,34 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_delete(['interfaces', 'ethernet', member, 'vif', vif])
self.cli_delete(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif}'])
+ def test_bridge_vif_s_vif_c_members(self):
+ # T2945: ensure that VIFs are not dropped from bridge
+ vifs = ['300', '400']
+ vifc = ['301', '401']
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif_s in vifs:
+ for vif_c in vifc:
+ self.cli_set(['interfaces', 'ethernet', member, 'vif-s', vif_s, 'vif-c', vif_c])
+ self.cli_set(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif_s}.{vif_c}'])
+
+ self.cli_commit()
+
+ # Verify config
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif_s in vifs:
+ for vif_c in vifc:
+ # member interface must be assigned to the bridge
+ self.assertTrue(os.path.exists(f'/sys/class/net/{interface}/lower_{member}.{vif_s}.{vif_c}'))
+
+ # delete all members
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif_s in vifs:
+ self.cli_delete(['interfaces', 'ethernet', member, 'vif-s', vif_s])
+ for vif_c in vifc:
+ self.cli_delete(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif_s}.{vif_c}'])
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py
index 9c0c93779..009dbc803 100755
--- a/smoketest/scripts/cli/test_protocols_bgp.py
+++ b/smoketest/scripts/cli/test_protocols_bgp.py
@@ -921,5 +921,31 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' neighbor {peer_group} peer-group', frrconfig)
self.assertIn(f' neighbor {peer_group} remote-as {remote_asn}', frrconfig)
+ def test_bgp_15_local_as_ebgp(self):
+ # https://phabricator.vyos.net/T4560
+ # local-as allowed only for ebgp peers
+
+ neighbor = '192.0.2.99'
+ remote_asn = '500'
+ local_asn = '400'
+
+ self.cli_set(base_path + ['local-as', ASN])
+ self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', ASN])
+ self.cli_set(base_path + ['neighbor', neighbor, 'local-as', local_asn])
+
+ # check validate() - local-as allowed only for ebgp peers
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', remote_asn])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' neighbor {neighbor} remote-as {remote_asn}', frrconfig)
+ self.assertIn(f' neighbor {neighbor} local-as {local_asn}', frrconfig)
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py
index 71fb3e177..72c1d4e43 100755
--- a/smoketest/scripts/cli/test_service_https.py
+++ b/smoketest/scripts/cli/test_service_https.py
@@ -138,5 +138,62 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
# Must get HTTP code 401 on missing key (Unauthorized)
self.assertEqual(r.status_code, 401)
+ # GraphQL auth test: a missing key will return status code 400, as
+ # 'key' is a non-nullable field in the schema; an incorrect key is
+ # caught by the resolver, and returns success 'False', so one must
+ # check the return value.
+
+ self.cli_set(base_path + ['api', 'gql'])
+ self.cli_commit()
+
+ gql_url = f'https://{address}/graphql'
+
+ query_valid_key = f"""
+ {{
+ SystemStatus (data: {{key: "{key}"}}) {{
+ success
+ errors
+ data {{
+ result
+ }}
+ }}
+ }}
+ """
+
+ r = request('POST', gql_url, verify=False, headers=headers, json={'query': query_valid_key})
+ success = r.json()['data']['SystemStatus']['success']
+ self.assertTrue(success)
+
+ query_invalid_key = """
+ {
+ SystemStatus (data: {key: "invalid"}) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+ }
+ """
+
+ r = request('POST', gql_url, verify=False, headers=headers, json={'query': query_invalid_key})
+ success = r.json()['data']['SystemStatus']['success']
+ self.assertFalse(success)
+
+ query_no_key = """
+ {
+ SystemStatus (data: {}) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+ }
+ """
+
+ r = request('POST', gql_url, verify=False, headers=headers, json={'query': query_no_key})
+ self.assertEqual(r.status_code, 400)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_ids.py b/smoketest/scripts/cli/test_service_ids.py
index 18f1b8ec5..d471eeaed 100755
--- a/smoketest/scripts/cli/test_service_ids.py
+++ b/smoketest/scripts/cli/test_service_ids.py
@@ -24,7 +24,9 @@ from vyos.util import process_named_running
from vyos.util import read_file
PROCESS_NAME = 'fastnetmon'
-FASTNETMON_CONF = '/etc/fastnetmon.conf'
+FASTNETMON_CONF = '/run/fastnetmon/fastnetmon.conf'
+NETWORKS_CONF = '/run/fastnetmon/networks_list'
+EXCLUDED_NETWORKS_CONF = '/run/fastnetmon/excluded_networks_list'
base_path = ['service', 'ids', 'ddos-protection']
class TestServiceIDS(VyOSUnitTestSHIM.TestCase):
@@ -48,7 +50,8 @@ class TestServiceIDS(VyOSUnitTestSHIM.TestCase):
self.assertFalse(process_named_running(PROCESS_NAME))
def test_fastnetmon(self):
- networks = ['10.0.0.0/24', '10.5.5.0/24']
+ networks = ['10.0.0.0/24', '10.5.5.0/24', '2001:db8:10::/64', '2001:db8:20::/64']
+ excluded_networks = ['10.0.0.1/32', '2001:db8:10::1/128']
interfaces = ['eth0', 'eth1']
fps = '3500'
mbps = '300'
@@ -61,6 +64,12 @@ class TestServiceIDS(VyOSUnitTestSHIM.TestCase):
for tmp in networks:
self.cli_set(base_path + ['network', tmp])
+ # optional excluded-network!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for tmp in excluded_networks:
+ self.cli_set(base_path + ['excluded-network', tmp])
+
# Required interface(s)!
with self.assertRaises(ConfigSessionError):
self.cli_commit()
@@ -86,9 +95,22 @@ class TestServiceIDS(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'threshold_mbps = {mbps}', config)
self.assertIn(f'ban_for_pps = on', config)
self.assertIn(f'threshold_pps = {pps}', config)
+ # default
+ self.assertIn(f'enable_ban = on', config)
+ self.assertIn(f'enable_ban_ipv6 = on', config)
+ self.assertIn(f'ban_time = 1900', config)
tmp = ','.join(interfaces)
self.assertIn(f'interfaces = {tmp}', config)
+
+ network_config = read_file(NETWORKS_CONF)
+ for tmp in networks:
+ self.assertIn(f'{tmp}', network_config)
+
+ excluded_network_config = read_file(EXCLUDED_NETWORKS_CONF)
+ for tmp in excluded_networks:
+ self.assertIn(f'{tmp}', excluded_network_config)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_monitoring_telegraf.py b/smoketest/scripts/cli/test_service_monitoring_telegraf.py
index f9d875e83..1c8cc9759 100755
--- a/smoketest/scripts/cli/test_service_monitoring_telegraf.py
+++ b/smoketest/scripts/cli/test_service_monitoring_telegraf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -35,9 +35,15 @@ inputs = ['cpu', 'disk', 'mem', 'net', 'system', 'kernel', 'interrupts', 'syslog
class TestMonitoringTelegraf(VyOSUnitTestSHIM.TestCase):
def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
self.cli_delete(base_path)
self.cli_commit()
+ # Check for not longer running process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
def test_01_basic_config(self):
self.cli_set(base_path + ['influxdb', 'authentication', 'organization', org])
self.cli_set(base_path + ['influxdb', 'authentication', 'token', token])
@@ -47,9 +53,6 @@ class TestMonitoringTelegraf(VyOSUnitTestSHIM.TestCase):
# commit changes
self.cli_commit()
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
config = read_file(TELEGRAF_CONF)
# Check telegraf config
diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py
index 1168c05cd..873be7df0 100755
--- a/smoketest/scripts/cli/test_service_router-advert.py
+++ b/smoketest/scripts/cli/test_service_router-advert.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -23,11 +23,13 @@ from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.util import read_file
from vyos.util import process_named_running
+PROCESS_NAME = 'radvd'
RADVD_CONF = '/run/radvd/radvd.conf'
interface = 'eth1'
base_path = ['service', 'router-advert', 'interface', interface]
address_base = ['interfaces', 'ethernet', interface, 'address']
+prefix = '::/64'
def get_config_value(key):
tmp = read_file(RADVD_CONF)
@@ -35,18 +37,36 @@ def get_config_value(key):
return tmp[0].split()[0].replace(';','')
class TestServiceRADVD(VyOSUnitTestSHIM.TestCase):
- def setUp(self):
- self.cli_set(address_base + ['2001:db8::1/64'])
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceRADVD, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, ['service', 'router-advert'])
+
+ cls.cli_set(cls, address_base + ['2001:db8::1/64'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, address_base)
+ super(TestServiceRADVD, cls).tearDownClass()
def tearDown(self):
- self.cli_delete(address_base)
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
self.cli_delete(base_path)
self.cli_commit()
+ # Check for no longer running process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
def test_common(self):
- self.cli_set(base_path + ['prefix', '::/64', 'no-on-link-flag'])
- self.cli_set(base_path + ['prefix', '::/64', 'no-autonomous-flag'])
- self.cli_set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity'])
+ self.cli_set(base_path + ['prefix', prefix, 'no-on-link-flag'])
+ self.cli_set(base_path + ['prefix', prefix, 'no-autonomous-flag'])
+ self.cli_set(base_path + ['prefix', prefix, 'valid-lifetime', 'infinity'])
self.cli_set(base_path + ['other-config-flag'])
# commit changes
@@ -57,7 +77,7 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase):
self.assertEqual(tmp, interface)
tmp = get_config_value('prefix')
- self.assertEqual(tmp, '::/64')
+ self.assertEqual(tmp, prefix)
tmp = get_config_value('AdvOtherConfigFlag')
self.assertEqual(tmp, 'on')
@@ -88,15 +108,19 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase):
tmp = get_config_value('AdvOnLink')
self.assertEqual(tmp, 'off')
- # Check for running process
- self.assertTrue(process_named_running('radvd'))
+ tmp = get_config_value('DeprecatePrefix')
+ self.assertEqual(tmp, 'off')
+
+ tmp = get_config_value('DecrementLifetimes')
+ self.assertEqual(tmp, 'off')
+
def test_dns(self):
nameserver = ['2001:db8::1', '2001:db8::2']
dnssl = ['vyos.net', 'vyos.io']
ns_lifetime = '599'
- self.cli_set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity'])
+ self.cli_set(base_path + ['prefix', prefix, 'valid-lifetime', 'infinity'])
self.cli_set(base_path + ['other-config-flag'])
for ns in nameserver:
@@ -127,5 +151,21 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase):
self.assertIn(tmp, config)
+ def test_deprecate_prefix(self):
+ self.cli_set(base_path + ['prefix', prefix, 'valid-lifetime', 'infinity'])
+ self.cli_set(base_path + ['prefix', prefix, 'deprecate-prefix'])
+ self.cli_set(base_path + ['prefix', prefix, 'decrement-lifetime'])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(RADVD_CONF)
+
+ tmp = get_config_value('DeprecatePrefix')
+ self.assertEqual(tmp, 'on')
+
+ tmp = get_config_value('DecrementLifetimes')
+ self.assertEqual(tmp, 'on')
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py
index a6eef3fb6..df60b9613 100755
--- a/smoketest/scripts/cli/test_system_flow-accounting.py
+++ b/smoketest/scripts/cli/test_system_flow-accounting.py
@@ -144,14 +144,15 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
self.assertNotIn(f'plugins: memory', uacctd)
for server, server_config in sflow_server.items():
+ plugin_name = server.replace('.', '-')
if 'port' in server_config:
- self.assertIn(f'sfprobe_receiver[sf_{server}]: {server}', uacctd)
+ self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}', uacctd)
else:
- self.assertIn(f'sfprobe_receiver[sf_{server}]: {server}:6343', uacctd)
+ self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}:6343', uacctd)
- self.assertIn(f'sfprobe_agentip[sf_{server}]: {agent_address}', uacctd)
- self.assertIn(f'sampling_rate[sf_{server}]: {sampling_rate}', uacctd)
- self.assertIn(f'sfprobe_source_ip[sf_{server}]: {source_address}', uacctd)
+ self.assertIn(f'sfprobe_agentip[sf_{plugin_name}]: {agent_address}', uacctd)
+ self.assertIn(f'sampling_rate[sf_{plugin_name}]: {sampling_rate}', uacctd)
+ self.assertIn(f'sfprobe_source_ip[sf_{plugin_name}]: {source_address}', uacctd)
self.cli_delete(['interfaces', 'dummy', dummy_if])
@@ -194,8 +195,7 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
for server, server_config in sflow_server.items():
tmp_srv = server
- if is_ipv6(tmp_srv):
- tmp_srv = tmp_srv.replace(':', '.')
+ tmp_srv = tmp_srv.replace(':', '-')
if 'port' in server_config:
self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd)
@@ -265,16 +265,16 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
tmp = []
for server, server_config in netflow_server.items():
tmp_srv = server
- if is_ipv6(tmp_srv):
- tmp_srv = tmp_srv.replace(':', '.')
+ tmp_srv = tmp_srv.replace('.', '-')
+ tmp_srv = tmp_srv.replace(':', '-')
tmp.append(f'nfprobe[nf_{tmp_srv}]')
tmp.append('memory')
self.assertIn('plugins: ' + ','.join(tmp), uacctd)
for server, server_config in netflow_server.items():
tmp_srv = server
- if is_ipv6(tmp_srv):
- tmp_srv = tmp_srv.replace(':', '.')
+ tmp_srv = tmp_srv.replace('.', '-')
+ tmp_srv = tmp_srv.replace(':', '-')
self.assertIn(f'nfprobe_engine[nf_{tmp_srv}]: {engine_id}', uacctd)
self.assertIn(f'nfprobe_maxflows[nf_{tmp_srv}]: {max_flows}', uacctd)
diff --git a/smoketest/scripts/cli/test_system_ip.py b/smoketest/scripts/cli/test_system_ip.py
index 83df9d99e..f71ef5b3f 100755
--- a/smoketest/scripts/cli/test_system_ip.py
+++ b/smoketest/scripts/cli/test_system_ip.py
@@ -28,7 +28,7 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase):
def test_system_ip_forwarding(self):
# Test if IPv4 forwarding can be disabled globally, default is '1'
- # which means forwearding enabled
+ # which means forwarding enabled
all_forwarding = '/proc/sys/net/ipv4/conf/all/forwarding'
self.assertEqual(read_file(all_forwarding), '1')
@@ -37,6 +37,17 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase):
self.assertEqual(read_file(all_forwarding), '0')
+ def test_system_ip_directed_broadcast_forwarding(self):
+ # Test if IPv4 directed broadcast forwarding can be disabled globally,
+ # default is '1' which means forwarding enabled
+ bc_forwarding = '/proc/sys/net/ipv4/conf/all/bc_forwarding'
+ self.assertEqual(read_file(bc_forwarding), '1')
+
+ self.cli_set(base_path + ['disable-directed-broadcast'])
+ self.cli_commit()
+
+ self.assertEqual(read_file(bc_forwarding), '0')
+
def test_system_ip_multipath(self):
# Test IPv4 multipathing options, options default to off -> '0'
use_neigh = '/proc/sys/net/ipv4/fib_multipath_use_neigh'
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 7750c1247..7e16235c1 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -192,6 +192,11 @@ def verify(flow_config):
raise ConfigError("All sFlow servers must use the same IP protocol")
else:
sflow_collector_ipver = ip_address(server).version
+
+ # check if vrf is defined for Sflow
+ sflow_vrf = None
+ if 'vrf' in flow_config:
+ sflow_vrf = flow_config['vrf']
# check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa
for server in flow_config['sflow']['server']:
@@ -203,12 +208,12 @@ def verify(flow_config):
if 'agent_address' in flow_config['sflow']:
tmp = flow_config['sflow']['agent_address']
- if not is_addr_assigned(tmp):
+ if not is_addr_assigned(tmp, sflow_vrf):
raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!')
# Check if configured netflow source-address exist in the system
if 'source_address' in flow_config['sflow']:
- if not is_addr_assigned(flow_config['sflow']['source_address']):
+ if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf):
tmp = flow_config['sflow']['source_address']
raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!')
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 4a7906c17..04113fc09 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -66,14 +66,10 @@ def get_config(config=None):
if conf.exists('debug'):
http_api['debug'] = True
- # this node is not available by CLI by default, and is reserved for
- # the graphql tools. One can enable it for testing, with the warning
- # that this will open an unauthenticated server. To do so
- # mkdir /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql
- # touch /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql/node.def
- # and configure; editing the config alone is insufficient.
if conf.exists('gql'):
http_api['gql'] = True
+ if conf.exists('gql introspection'):
+ http_api['introspection'] = True
if conf.exists('socket'):
http_api['socket'] = True
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 4167594e3..7e146f446 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -36,6 +36,7 @@ from vyos.ifconfig import BondIf
from vyos.ifconfig import Section
from vyos.util import dict_search
from vyos.validate import has_address_configured
+from vyos.validate import has_vrf_configured
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -109,20 +110,26 @@ def get_config(config=None):
for interface, interface_config in bond['member']['interface'].items():
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
- if tmp: interface_config.update({'is_bridge_member' : tmp})
+ if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp})
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
- if tmp and bond['ifname'] not in tmp:
- interface_config.update({'is_bond_member' : tmp})
+ for tmp in is_member(conf, interface, 'bonding'):
+ if bond['ifname'] == tmp:
+ continue
+ bond['member']['interface'][interface].update({'is_bond_member' : tmp})
# Check if member interface is used as source-interface on another interface
tmp = is_source_interface(conf, interface)
- if tmp: interface_config.update({'is_source_interface' : tmp})
+ if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp})
# bond members must not have an assigned address
tmp = has_address_configured(conf, interface)
- if tmp: interface_config.update({'has_address' : ''})
+ if tmp: bond['member']['interface'][interface].update({'has_address' : {}})
+
+ # bond members must not have a VRF attached
+ tmp = has_vrf_configured(conf, interface)
+ if tmp: bond['member']['interface'][interface].update({'has_vrf' : {}})
return bond
@@ -167,11 +174,11 @@ def verify(bond):
raise ConfigError(error_msg + 'it does not exist!')
if 'is_bridge_member' in interface_config:
- tmp = next(iter(interface_config['is_bridge_member']))
+ tmp = interface_config['is_bridge_member']
raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
if 'is_bond_member' in interface_config:
- tmp = next(iter(interface_config['is_bond_member']))
+ tmp = interface_config['is_bond_member']
raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
if 'is_source_interface' in interface_config:
@@ -181,6 +188,8 @@ def verify(bond):
if 'has_address' in interface_config:
raise ConfigError(error_msg + 'it has an address assigned!')
+ if 'has_vrf' in interface_config:
+ raise ConfigError(error_msg + 'it has a VRF assigned!')
if 'primary' in bond:
if bond['primary'] not in bond['member']['interface']:
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 38ae727c1..cd0d9003b 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -31,6 +31,7 @@ from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
from vyos.ifconfig import BridgeIf
from vyos.validate import has_address_configured
+from vyos.validate import has_vrf_configured
from vyos.xml import defaults
from vyos.util import cmd
@@ -93,6 +94,10 @@ def get_config(config=None):
tmp = has_address_configured(conf, interface)
if tmp: bridge['member']['interface'][interface].update({'has_address' : ''})
+ # Bridge members must not have a VRF attached
+ tmp = has_vrf_configured(conf, interface)
+ if tmp: bridge['member']['interface'][interface].update({'has_vrf' : ''})
+
# VLAN-aware bridge members must not have VLAN interface configuration
tmp = has_vlan_subinterface_configured(conf,interface)
if 'enable_vlan' in bridge and tmp:
@@ -118,11 +123,11 @@ def verify(bridge):
raise ConfigError('Loopback interface "lo" can not be added to a bridge')
if 'is_bridge_member' in interface_config:
- tmp = next(iter(interface_config['is_bridge_member']))
+ tmp = interface_config['is_bridge_member']
raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
if 'is_bond_member' in interface_config:
- tmp = next(iter(interface_config['is_bond_member']))
+ tmp = interface_config['is_bond_member']
raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
if 'is_source_interface' in interface_config:
@@ -132,9 +137,12 @@ def verify(bridge):
if 'has_address' in interface_config:
raise ConfigError(error_msg + 'it has an address assigned!')
+ if 'has_vrf' in interface_config:
+ raise ConfigError(error_msg + 'it has a VRF assigned!')
+
if 'enable_vlan' in bridge:
if 'has_vlan' in interface_config:
- raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!')
+ raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!')
if 'wlan' in interface:
raise ConfigError(error_msg + 'VLAN aware cannot be set!')
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index fec4456fb..30e7a2af7 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -31,6 +31,7 @@ from vyos.configverify import verify_mtu
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_bond_bridge_member
from vyos.ethtool import Ethtool
from vyos.ifconfig import EthernetIf
from vyos.pki import find_chain
@@ -83,6 +84,7 @@ def verify(ethernet):
verify_dhcpv6(ethernet)
verify_address(ethernet)
verify_vrf(ethernet)
+ verify_bond_bridge_member(ethernet)
verify_eapol(ethernet)
verify_mirror_redirect(ethernet)
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index b9cf2fa3c..08cc3a48d 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -27,6 +27,7 @@ from vyos.configverify import verify_address
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import GeneveIf
from vyos import ConfigError
@@ -64,6 +65,7 @@ def verify(geneve):
verify_mtu_ipv6(geneve)
verify_address(geneve)
+ verify_bond_bridge_member(geneve)
verify_mirror_redirect(geneve)
if 'remote' not in geneve:
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 6a486f969..ca321e01d 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -26,6 +26,7 @@ from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import L2TPv3If
from vyos.util import check_kmod
from vyos.validate import is_addr_assigned
@@ -77,6 +78,7 @@ def verify(l2tpv3):
verify_mtu_ipv6(l2tpv3)
verify_address(l2tpv3)
+ verify_bond_bridge_member(l2tpv3)
verify_mirror_redirect(l2tpv3)
return None
diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
index 279dd119b..03a010086 100755
--- a/src/conf_mode/interfaces-macsec.py
+++ b/src/conf_mode/interfaces-macsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -21,16 +21,20 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
-from vyos.ifconfig import MACsecIf
-from vyos.ifconfig import Interface
-from vyos.template import render
-from vyos.util import call
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import MACsecIf
+from vyos.ifconfig import Interface
+from vyos.template import render
+from vyos.util import call
+from vyos.util import dict_search
+from vyos.util import is_systemd_service_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -55,6 +59,12 @@ def get_config(config=None):
source_interface = conf.return_effective_value(['source-interface'])
macsec.update({'source_interface': source_interface})
+ if is_node_changed(conf, base + [ifname, 'security']):
+ macsec.update({'shutdown_required': {}})
+
+ if is_node_changed(conf, base + [ifname, 'source_interface']):
+ macsec.update({'shutdown_required': {}})
+
return macsec
@@ -67,22 +77,15 @@ def verify(macsec):
verify_vrf(macsec)
verify_mtu_ipv6(macsec)
verify_address(macsec)
+ verify_bond_bridge_member(macsec)
verify_mirror_redirect(macsec)
- if not (('security' in macsec) and
- ('cipher' in macsec['security'])):
- raise ConfigError(
- 'Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))
+ if dict_search('security.cipher', macsec) == None:
+ raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))
- if (('security' in macsec) and
- ('encrypt' in macsec['security'])):
- tmp = macsec.get('security')
-
- if not (('mka' in tmp) and
- ('cak' in tmp['mka']) and
- ('ckn' in tmp['mka'])):
- raise ConfigError('Missing mandatory MACsec security '
- 'keys as encryption is enabled!')
+ if dict_search('security.encrypt', macsec) != None:
+ if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None:
+ raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!')
if 'source_interface' in macsec:
# MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad
@@ -97,33 +100,35 @@ def verify(macsec):
def generate(macsec):
- render(wpa_suppl_conf.format(**macsec),
- 'macsec/wpa_supplicant.conf.j2', macsec)
+ render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec)
return None
def apply(macsec):
- # Remove macsec interface
- if 'deleted' in macsec:
- call('systemctl stop wpa_supplicant-macsec@{source_interface}'
- .format(**macsec))
+ systemd_service = 'wpa_supplicant-macsec@{source_interface}'.format(**macsec)
+
+ # Remove macsec interface on deletion or mandatory parameter change
+ if 'deleted' in macsec or 'shutdown_required' in macsec:
+ call(f'systemctl stop {systemd_service}')
if macsec['ifname'] in interfaces():
tmp = MACsecIf(macsec['ifname'])
tmp.remove()
- # delete configuration on interface removal
- if os.path.isfile(wpa_suppl_conf.format(**macsec)):
- os.unlink(wpa_suppl_conf.format(**macsec))
+ if 'deleted' in macsec:
+ # delete configuration on interface removal
+ if os.path.isfile(wpa_suppl_conf.format(**macsec)):
+ os.unlink(wpa_suppl_conf.format(**macsec))
- else:
- # It is safe to "re-create" the interface always, there is a sanity
- # check that the interface will only be create if its non existent
- i = MACsecIf(**macsec)
- i.update(macsec)
+ return None
+
+ # It is safe to "re-create" the interface always, there is a sanity
+ # check that the interface will only be create if its non existent
+ i = MACsecIf(**macsec)
+ i.update(macsec)
- call('systemctl restart wpa_supplicant-macsec@{source_interface}'
- .format(**macsec))
+ if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec:
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 280a62b9a..ef745d737 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -36,6 +36,7 @@ from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import VTunIf
from vyos.pki import load_dh_parameters
from vyos.pki import load_private_key
@@ -503,6 +504,7 @@ def verify(openvpn):
raise ConfigError('Username for authentication is missing')
verify_vrf(openvpn)
+ verify_bond_bridge_member(openvpn)
verify_mirror_redirect(openvpn)
return None
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index 1cd3fe276..f26a50a0e 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -26,6 +26,7 @@ from vyos.configverify import verify_source_interface
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_mtu_parent
from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import MACVLANIf
from vyos import ConfigError
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index eff7f373c..acef1fda7 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -29,6 +29,7 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
from vyos.configverify import verify_tunnel
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.ifconfig import TunnelIf
@@ -158,6 +159,7 @@ def verify(tunnel):
verify_mtu_ipv6(tunnel)
verify_address(tunnel)
verify_vrf(tunnel)
+ verify_bond_bridge_member(tunnel)
verify_mirror_redirect(tunnel)
if 'source_interface' in tunnel:
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index f44d754ba..af2d0588d 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -29,6 +29,7 @@ from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import Interface
from vyos.ifconfig import VXLANIf
from vyos.template import is_ipv6
@@ -117,6 +118,11 @@ def verify(vxlan):
# in use.
vxlan_overhead += 20
+ # If source_address is not used - check IPv6 'remote' list
+ elif 'remote' in vxlan:
+ if any(is_ipv6(a) for a in vxlan['remote']):
+ vxlan_overhead += 20
+
lower_mtu = Interface(vxlan['source_interface']).get_mtu()
if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead):
raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
@@ -144,6 +150,7 @@ def verify(vxlan):
verify_mtu_ipv6(vxlan)
verify_address(vxlan)
+ verify_bond_bridge_member(vxlan)
verify_mirror_redirect(vxlan)
return None
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 180ffa507..61bab2feb 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -29,6 +29,7 @@ from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import WireGuardIf
from vyos.util import check_kmod
from vyos.util import check_port_availability
@@ -71,6 +72,7 @@ def verify(wireguard):
verify_mtu_ipv6(wireguard)
verify_address(wireguard)
verify_vrf(wireguard)
+ verify_bond_bridge_member(wireguard)
verify_mirror_redirect(wireguard)
if 'private_key' not in wireguard:
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index d34297063..dd798b5a2 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -30,6 +30,7 @@ from vyos.configverify import verify_source_interface
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import WiFiIf
from vyos.template import render
from vyos.util import call
@@ -194,6 +195,7 @@ def verify(wifi):
verify_address(wifi)
verify_vrf(wifi)
+ verify_bond_bridge_member(wifi)
verify_mirror_redirect(wifi)
# use common function to verify VLAN configuration
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index e275ace84..97b3a6396 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -76,7 +76,7 @@ def get_config(config=None):
# We need to know the amount of other WWAN interfaces as ModemManager needs
# to be started or stopped.
conf.set_level(base)
- _, wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'),
+ wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'),
get_first_key=True,
no_tag_node_value_mangle=True)
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 01f14df61..7d3687094 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -101,6 +101,17 @@ def verify_remote_as(peer_config, bgp_config):
return None
+def verify_afi(peer_config, bgp_config):
+ if 'address_family' in peer_config:
+ return True
+
+ if 'peer_group' in peer_config:
+ peer_group_name = peer_config['peer_group']
+ tmp = dict_search(f'peer_group.{peer_group_name}.address_family', bgp_config)
+ if tmp: return True
+
+ return False
+
def verify(bgp):
if not bgp or 'deleted' in bgp:
if 'dependent_vrfs' in bgp:
@@ -165,6 +176,9 @@ def verify(bgp):
if not verify_remote_as(peer_config, bgp):
raise ConfigError(f'Neighbor "{peer}" remote-as must be set!')
+ if not verify_afi(peer_config, bgp):
+ Warning(f'BGP neighbor "{peer}" requires address-family!')
+
# Peer-group member cannot override remote-as of peer-group
if 'peer_group' in peer_config:
peer_group = peer_config['peer_group']
@@ -199,8 +213,11 @@ def verify(bgp):
if 'source_interface' in peer_config['interface']:
raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"')
- if 'address_family' not in peer_config:
- Warning(f'BGP neighbor "{peer}" requires address-family!')
+ # Local-AS allowed only for EBGP peers
+ if 'local_as' in peer_config:
+ remote_as = verify_remote_as(peer_config, bgp)
+ if remote_as == bgp['local_as']:
+ raise ConfigError(f'local-as configured for "{peer}", allowed only for eBGP peers!')
for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec',
'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec',
diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py
index ae7e582ec..c58f8db9a 100755
--- a/src/conf_mode/service_ids_fastnetmon.py
+++ b/src/conf_mode/service_ids_fastnetmon.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -19,14 +19,17 @@ import os
from sys import exit
from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call
+from vyos.configdict import dict_merge
from vyos.template import render
+from vyos.util import call
+from vyos.xml import defaults
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
-config_file = r'/etc/fastnetmon.conf'
-networks_list = r'/etc/networks_list'
+config_file = r'/run/fastnetmon/fastnetmon.conf'
+networks_list = r'/run/fastnetmon/networks_list'
+excluded_networks_list = r'/run/fastnetmon/excluded_networks_list'
def get_config(config=None):
if config:
@@ -34,50 +37,55 @@ def get_config(config=None):
else:
conf = Config()
base = ['service', 'ids', 'ddos-protection']
+ if not conf.exists(base):
+ return None
+
fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ fastnetmon = dict_merge(default_values, fastnetmon)
+
return fastnetmon
def verify(fastnetmon):
if not fastnetmon:
return None
- if not "mode" in fastnetmon:
- raise ConfigError('ddos-protection mode is mandatory!')
-
- if not "network" in fastnetmon:
- raise ConfigError('Required define network!')
+ if 'mode' not in fastnetmon:
+ raise ConfigError('Specify operating mode!')
- if not "listen_interface" in fastnetmon:
- raise ConfigError('Define listen-interface is mandatory!')
+ if 'listen_interface' not in fastnetmon:
+ raise ConfigError('Specify interface(s) for traffic capture')
- if "alert_script" in fastnetmon:
- if os.path.isfile(fastnetmon["alert_script"]):
+ if 'alert_script' in fastnetmon:
+ if os.path.isfile(fastnetmon['alert_script']):
# Check script permissions
- if not os.access(fastnetmon["alert_script"], os.X_OK):
- raise ConfigError('Script {0} does not have permissions for execution'.format(fastnetmon["alert_script"]))
+ if not os.access(fastnetmon['alert_script'], os.X_OK):
+ raise ConfigError('Script "{alert_script}" is not executable!'.format(fastnetmon['alert_script']))
else:
- raise ConfigError('File {0} does not exists!'.format(fastnetmon["alert_script"]))
+ raise ConfigError('File "{alert_script}" does not exists!'.format(fastnetmon))
def generate(fastnetmon):
if not fastnetmon:
- if os.path.isfile(config_file):
- os.unlink(config_file)
- if os.path.isfile(networks_list):
- os.unlink(networks_list)
+ for file in [config_file, networks_list]:
+ if os.path.isfile(file):
+ os.unlink(file)
- return
+ return None
render(config_file, 'ids/fastnetmon.j2', fastnetmon)
render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon)
-
+ render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon)
return None
def apply(fastnetmon):
+ systemd_service = 'fastnetmon.service'
if not fastnetmon:
# Stop fastnetmon service if removed
- call('systemctl stop fastnetmon.service')
+ call(f'systemctl stop {systemd_service}')
else:
- call('systemctl restart fastnetmon.service')
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 05fc3a97a..0c5063ed3 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.py
@@ -64,6 +64,11 @@ def apply(opt):
value = '0' if (tmp != None) else '1'
write_file('/proc/sys/net/ipv4/conf/all/forwarding', value)
+ # enable/disable IPv4 directed broadcast forwarding
+ tmp = dict_search('disable_directed_broadcast', opt)
+ value = '0' if (tmp != None) else '1'
+ write_file('/proc/sys/net/ipv4/conf/all/bc_forwarding', value)
+
# configure multipath
tmp = dict_search('multipath.ignore_unreachable_nexthops', opt)
value = '1' if (tmp != None) else '0'
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index c717286ae..3dcbc995c 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -231,7 +231,7 @@ def apply(login):
if tmp: command += f" --home '{tmp}'"
else: command += f" --home '/home/{user}'"
- command += f' --groups frrvty,vyattacfg,sudo,adm,dip,disk {user}'
+ command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk {user}'
try:
cmd(command)
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 972d0289b..1b4156895 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -113,8 +113,14 @@ def verify(vrf):
f'static routes installed!')
if 'name' in vrf:
+ reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", "get", "inet", "mtu", "link", "type",
+ "vrf"]
table_ids = []
for name, config in vrf['name'].items():
+ # Reserved VRF names
+ if name in reserved_names:
+ raise ConfigError(f'VRF name "{name}" is reserved and connot be used!')
+
# table id is mandatory
if 'table' not in config:
raise ConfigError(f'VRF "{name}" table id is mandatory!')
diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py
index f7487ee5f..8274e6564 100755
--- a/src/etc/opennhrp/opennhrp-script.py
+++ b/src/etc/opennhrp/opennhrp-script.py
@@ -14,16 +14,17 @@
# 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 pprint import pprint
import os
import re
import sys
import vici
+from json import loads
from vyos.util import cmd
from vyos.util import process_named_running
-NHRP_CONFIG="/run/opennhrp/opennhrp.conf"
+NHRP_CONFIG = "/run/opennhrp/opennhrp.conf"
+
def parse_type_ipsec(interface):
with open(NHRP_CONFIG, 'r') as f:
@@ -35,6 +36,50 @@ def parse_type_ipsec(interface):
return m[1], m[2]
return None, None
+
+def add_peer_route(nbma_src: str, nbma_dst: str, mtu: str) -> None:
+ """Add a route to a NBMA peer
+
+ Args:
+ nmba_src (str): a local IP address
+ nbma_dst (str): a remote IP address
+ mtu (str): a MTU for a route
+ """
+ # Find routes to a peer
+ route_get_cmd = f'sudo ip -j route get {nbma_dst} from {nbma_src}'
+ try:
+ route_info_data = loads(cmd(route_get_cmd))
+ except Exception as err:
+ print(f'Unable to find a route to {nbma_dst}: {err}')
+
+ # Check if an output has an expected format
+ if not isinstance(route_info_data, list):
+ print(f'Garbage returned from the "{route_get_cmd}" command: \
+ {route_info_data}')
+ return
+
+ # Add static routes to a peer
+ for route_item in route_info_data:
+ route_dev = route_item.get('dev')
+ route_dst = route_item.get('dst')
+ route_gateway = route_item.get('gateway')
+ # Prepare a command to add a route
+ route_add_cmd = 'sudo ip route add'
+ if route_dst:
+ route_add_cmd = f'{route_add_cmd} {route_dst}'
+ if route_gateway:
+ route_add_cmd = f'{route_add_cmd} via {route_gateway}'
+ if route_dev:
+ route_add_cmd = f'{route_add_cmd} dev {route_dev}'
+ route_add_cmd = f'{route_add_cmd} proto 42 mtu {mtu}'
+ # Add a route
+ try:
+ cmd(route_add_cmd)
+ except Exception as err:
+ print(f'Unable to add a route using command "{route_add_cmd}": \
+ {err}')
+
+
def vici_initiate(conn, child_sa, src_addr, dest_addr):
try:
session = vici.Session()
@@ -52,6 +97,7 @@ def vici_initiate(conn, child_sa, src_addr, dest_addr):
except:
return None
+
def vici_terminate(conn, child_sa, src_addr, dest_addr):
try:
session = vici.Session()
@@ -69,25 +115,27 @@ def vici_terminate(conn, child_sa, src_addr, dest_addr):
except:
return None
+
def iface_up(interface):
cmd(f'sudo ip route flush proto 42 dev {interface}')
cmd(f'sudo ip neigh flush dev {interface}')
+
def peer_up(dmvpn_type, conn):
- src_addr = os.getenv('NHRP_SRCADDR')
+ # src_addr = os.getenv('NHRP_SRCADDR')
src_nbma = os.getenv('NHRP_SRCNBMA')
- dest_addr = os.getenv('NHRP_DESTADDR')
+ # dest_addr = os.getenv('NHRP_DESTADDR')
dest_nbma = os.getenv('NHRP_DESTNBMA')
dest_mtu = os.getenv('NHRP_DESTMTU')
if dest_mtu:
- args = cmd(f'sudo ip route get {dest_nbma} from {src_nbma}')
- cmd(f'sudo ip route add {args} proto 42 mtu {dest_mtu}')
+ add_peer_route(src_nbma, dest_nbma, dest_mtu)
if conn and dmvpn_type == 'spoke' and process_named_running('charon'):
vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma)
vici_initiate(conn, 'dmvpn', src_nbma, dest_nbma)
+
def peer_down(dmvpn_type, conn):
src_nbma = os.getenv('NHRP_SRCNBMA')
dest_nbma = os.getenv('NHRP_DESTNBMA')
@@ -97,14 +145,17 @@ def peer_down(dmvpn_type, conn):
cmd(f'sudo ip route del {dest_nbma} src {src_nbma} proto 42')
+
def route_up(interface):
dest_addr = os.getenv('NHRP_DESTADDR')
dest_prefix = os.getenv('NHRP_DESTPREFIX')
next_hop = os.getenv('NHRP_NEXTHOP')
- cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 via {next_hop} dev {interface}')
+ cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 \
+ via {next_hop} dev {interface}')
cmd('sudo ip route flush cache')
+
def route_down(interface):
dest_addr = os.getenv('NHRP_DESTADDR')
dest_prefix = os.getenv('NHRP_DESTPREFIX')
@@ -112,6 +163,7 @@ def route_down(interface):
cmd(f'sudo ip route del {dest_addr}/{dest_prefix} proto 42')
cmd('sudo ip route flush cache')
+
if __name__ == '__main__':
action = sys.argv[1]
interface = os.getenv('NHRP_INTERFACE')
diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf
index e03d3a29c..4feb7e09a 100644
--- a/src/etc/sysctl.d/30-vyos-router.conf
+++ b/src/etc/sysctl.d/30-vyos-router.conf
@@ -27,6 +27,12 @@ net.ipv4.conf.all.arp_announce=2
# Enable packet forwarding for IPv4
net.ipv4.ip_forward=1
+# Enable directed broadcast forwarding feature described in rfc1812#section-5.3.5.2 and rfc2644.
+# Note that setting the 'all' entry to 1 doesn't enable directed broadcast forwarding on all interfaces.
+# To enable directed broadcast forwarding on an interface, both the 'all' entry and the input interface entry should be set to 1.
+net.ipv4.conf.all.bc_forwarding=1
+net.ipv4.conf.default.bc_forwarding=0
+
# if a primary address is removed from an interface promote the
# secondary address if available
net.ipv4.conf.all.promote_secondaries=1
diff --git a/src/etc/systemd/system/fastnetmon.service.d/override.conf b/src/etc/systemd/system/fastnetmon.service.d/override.conf
new file mode 100644
index 000000000..841666070
--- /dev/null
+++ b/src/etc/systemd/system/fastnetmon.service.d/override.conf
@@ -0,0 +1,12 @@
+[Unit]
+RequiresMountsFor=/run
+ConditionPathExists=/run/fastnetmon/fastnetmon.conf
+After=
+After=vyos-router.service
+
+[Service]
+Type=simple
+WorkingDirectory=/run/fastnetmon
+PIDFile=/run/fastnetmon.pid
+ExecStart=
+ExecStart=/usr/sbin/fastnetmon --configuration_file /run/fastnetmon/fastnetmon.conf
diff --git a/src/etc/systemd/system/frr.service.d/override.conf b/src/etc/systemd/system/frr.service.d/override.conf
new file mode 100644
index 000000000..69eb1a86a
--- /dev/null
+++ b/src/etc/systemd/system/frr.service.d/override.conf
@@ -0,0 +1,11 @@
+[Unit]
+Before=
+Before=vyos-router.service
+
+[Service]
+ExecStartPre=/bin/bash -c 'mkdir -p /run/frr/config; \
+ echo "log syslog" > /run/frr/config/frr.conf; \
+ echo "log facility local7" >> /run/frr/config/frr.conf; \
+ chown frr:frr /run/frr/config/frr.conf; \
+ chmod 664 /run/frr/config/frr.conf; \
+ mount --bind /run/frr/config/frr.conf /etc/frr/frr.conf'
diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py
new file mode 100755
index 000000000..411aa06d1
--- /dev/null
+++ b/src/op_mode/bridge.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import jmespath
+import json
+import sys
+import typing
+
+from sys import exit
+from tabulate import tabulate
+
+from vyos.util import cmd
+from vyos.util import dict_search
+
+import vyos.opmode
+
+
+def _get_json_data():
+ """
+ Get bridge data format JSON
+ """
+ return cmd(f'sudo bridge --json link show')
+
+
+def _get_raw_data_summary():
+ """Get interested rules
+ :returns dict
+ """
+ data = _get_json_data()
+ data_dict = json.loads(data)
+ return data_dict
+
+
+def _get_raw_data_vlan():
+ """
+ :returns dict
+ """
+ json_data = cmd('sudo bridge --json --compressvlans vlan show')
+ data_dict = json.loads(json_data)
+ return data_dict
+
+
+def _get_raw_data_fdb(bridge):
+ """Get MAC-address for the bridge brX
+ :returns list
+ """
+ json_data = cmd(f'sudo bridge --json fdb show br {bridge}')
+ data_dict = json.loads(json_data)
+ return data_dict
+
+
+def _get_raw_data_mdb(bridge):
+ """Get MAC-address multicast gorup for the bridge brX
+ :return list
+ """
+ json_data = cmd(f'bridge --json mdb show br {bridge}')
+ data_dict = json.loads(json_data)
+ return data_dict
+
+
+def _get_bridge_members(bridge: str) -> list:
+ """
+ Get list of interface bridge members
+ :param bridge: str
+ :default: ['n/a']
+ :return: list
+ """
+ data = _get_raw_data_summary()
+ members = jmespath.search(f'[?master == `{bridge}`].ifname', data)
+ return [member for member in members] if members else ['n/a']
+
+
+def _get_member_options(bridge: str):
+ data = _get_raw_data_summary()
+ options = jmespath.search(f'[?master == `{bridge}`]', data)
+ return options
+
+
+def _get_formatted_output_summary(data):
+ data_entries = ''
+ bridges = set(jmespath.search('[*].master', data))
+ for bridge in bridges:
+ member_options = _get_member_options(bridge)
+ member_entries = []
+ for option in member_options:
+ interface = option.get('ifname')
+ ifindex = option.get('ifindex')
+ state = option.get('state')
+ mtu = option.get('mtu')
+ flags = ','.join(option.get('flags')).lower()
+ prio = option.get('priority')
+ member_entries.append([interface, state, mtu, flags, prio])
+ member_headers = ["Member", "State", "MTU", "Flags", "Prio"]
+ output_members = tabulate(member_entries, member_headers, numalign="left")
+ output_bridge = f"""Bridge interface {bridge}:
+{output_members}
+
+"""
+ data_entries += output_bridge
+ output = data_entries
+ return output
+
+
+def _get_formatted_output_vlan(data):
+ data_entries = []
+ for entry in data:
+ interface = entry.get('ifname')
+ vlans = entry.get('vlans')
+ for vlan_entry in vlans:
+ vlan = vlan_entry.get('vlan')
+ if vlan_entry.get('vlanEnd'):
+ vlan_end = vlan_entry.get('vlanEnd')
+ vlan = f'{vlan}-{vlan_end}'
+ flags = ', '.join(vlan_entry.get('flags')).lower()
+ data_entries.append([interface, vlan, flags])
+
+ headers = ["Interface", "Vlan", "Flags"]
+ output = tabulate(data_entries, headers)
+ return output
+
+
+def _get_formatted_output_fdb(data):
+ data_entries = []
+ for entry in data:
+ interface = entry.get('ifname')
+ mac = entry.get('mac')
+ state = entry.get('state')
+ flags = ','.join(entry['flags'])
+ data_entries.append([interface, mac, state, flags])
+
+ headers = ["Interface", "Mac address", "State", "Flags"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+
+def _get_formatted_output_mdb(data):
+ data_entries = []
+ for entry in data:
+ for mdb_entry in entry['mdb']:
+ interface = mdb_entry.get('port')
+ group = mdb_entry.get('grp')
+ state = mdb_entry.get('state')
+ flags = ','.join(mdb_entry.get('flags'))
+ data_entries.append([interface, group, state, flags])
+ headers = ["Interface", "Group", "State", "Flags"]
+ output = tabulate(data_entries, headers)
+ return output
+
+
+def show(raw: bool):
+ bridge_data = _get_raw_data_summary()
+ if raw:
+ return bridge_data
+ else:
+ return _get_formatted_output_summary(bridge_data)
+
+
+def show_vlan(raw: bool):
+ bridge_vlan = _get_raw_data_vlan()
+ if raw:
+ return bridge_vlan
+ else:
+ return _get_formatted_output_vlan(bridge_vlan)
+
+
+def show_fdb(raw: bool, interface: str):
+ fdb_data = _get_raw_data_fdb(interface)
+ if raw:
+ return fdb_data
+ else:
+ return _get_formatted_output_fdb(fdb_data)
+
+
+def show_mdb(raw: bool, interface: str):
+ mdb_data = _get_raw_data_mdb(interface)
+ if raw:
+ return mdb_data
+ else:
+ return _get_formatted_output_mdb(mdb_data)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except ValueError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py
new file mode 100755
index 000000000..1441d110f
--- /dev/null
+++ b/src/op_mode/conntrack.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import xmltodict
+
+from tabulate import tabulate
+from vyos.util import cmd
+from vyos.util import run
+
+import vyos.opmode
+
+
+def _get_xml_data(family):
+ """
+ Get conntrack XML output
+ """
+ return cmd(f'sudo conntrack --dump --family {family} --output xml')
+
+
+def _xml_to_dict(xml):
+ """
+ Convert XML to dictionary
+ Return: dictionary
+ """
+ parse = xmltodict.parse(xml, attr_prefix='')
+ # If only one conntrack entry we must change dict
+ if 'meta' in parse['conntrack']['flow']:
+ return dict(conntrack={'flow': [parse['conntrack']['flow']]})
+ return parse
+
+
+def _get_raw_data(family):
+ """
+ Return: dictionary
+ """
+ xml = _get_xml_data(family)
+ return _xml_to_dict(xml)
+
+
+def get_formatted_output(dict_data):
+ """
+ :param xml:
+ :return: formatted output
+ """
+ data_entries = []
+ #dict_data = _get_raw_data(family)
+ for entry in dict_data['conntrack']['flow']:
+ orig_src, orig_dst, orig_sport, orig_dport = {}, {}, {}, {}
+ reply_src, reply_dst, reply_sport, reply_dport = {}, {}, {}, {}
+ proto = {}
+ for meta in entry['meta']:
+ direction = meta['direction']
+ if direction in ['original']:
+ if 'layer3' in meta:
+ orig_src = meta['layer3']['src']
+ orig_dst = meta['layer3']['dst']
+ if 'layer4' in meta:
+ if meta.get('layer4').get('sport'):
+ orig_sport = meta['layer4']['sport']
+ if meta.get('layer4').get('dport'):
+ orig_dport = meta['layer4']['dport']
+ proto = meta['layer4']['protoname']
+ if direction in ['reply']:
+ if 'layer3' in meta:
+ reply_src = meta['layer3']['src']
+ reply_dst = meta['layer3']['dst']
+ if 'layer4' in meta:
+ if meta.get('layer4').get('sport'):
+ reply_sport = meta['layer4']['sport']
+ if meta.get('layer4').get('dport'):
+ reply_dport = meta['layer4']['dport']
+ proto = meta['layer4']['protoname']
+ if direction == 'independent':
+ conn_id = meta['id']
+ timeout = meta['timeout']
+ orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src
+ orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst
+ reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src
+ reply_dst = f'{reply_dst}:{reply_dport}' if reply_dport else reply_dst
+ state = meta['state'] if 'state' in meta else ''
+ mark = meta['mark']
+ zone = meta['zone'] if 'zone' in meta else ''
+ data_entries.append(
+ [conn_id, orig_src, orig_dst, reply_src, reply_dst, proto, state, timeout, mark, zone])
+ headers = ["Id", "Original src", "Original dst", "Reply src", "Reply dst", "Protocol", "State", "Timeout", "Mark",
+ "Zone"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+
+def show(raw: bool, family: str):
+ family = 'ipv6' if family == 'inet6' else 'ipv4'
+ conntrack_data = _get_raw_data(family)
+ if raw:
+ return conntrack_data
+ else:
+ return get_formatted_output(conntrack_data)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except ValueError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/cpu.py b/src/op_mode/cpu.py
new file mode 100755
index 000000000..f9c425826
--- /dev/null
+++ b/src/op_mode/cpu.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016-2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+
+import vyos.cpu
+import vyos.opmode
+
+from jinja2 import Template
+
+cpu_template = Template("""
+{% for cpu in cpus %}
+{% if 'physical id' in cpu %}CPU socket: {{cpu['physical id']}}{% endif %}
+{% if 'vendor_id' in cpu %}CPU Vendor: {{cpu['vendor_id']}}{% endif %}
+{% if 'model name' in cpu %}Model: {{cpu['model name']}}{% endif %}
+{% if 'cpu cores' in cpu %}Cores: {{cpu['cpu cores']}}{% endif %}
+{% if 'cpu MHz' in cpu %}Current MHz: {{cpu['cpu MHz']}}{% endif %}
+{% endfor %}
+""")
+
+cpu_summary_template = Template("""
+Physical CPU cores: {{count}}
+CPU model(s): {{models | join(", ")}}
+""")
+
+def _get_raw_data():
+ return vyos.cpu.get_cpus()
+
+def _format_cpus(cpu_data):
+ env = {'cpus': cpu_data}
+ return cpu_template.render(env).strip()
+
+def _get_summary_data():
+ count = vyos.cpu.get_core_count()
+ cpu_data = vyos.cpu.get_cpus()
+ models = [c['model name'] for c in cpu_data]
+ env = {'count': count, "models": models}
+
+ return env
+
+def _format_cpu_summary(summary_data):
+ return cpu_summary_template.render(summary_data).strip()
+
+def show(raw: bool):
+ cpu_data = _get_raw_data()
+
+ if raw:
+ return cpu_data
+ else:
+ return _format_cpus(cpu_data)
+
+def show_summary(raw: bool):
+ cpu_summary_data = _get_summary_data()
+
+ if raw:
+ return cpu_summary_data
+ else:
+ return _format_cpu_summary(cpu_summary_data)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except ValueError as e:
+ print(e)
+ sys.exit(1)
+
diff --git a/src/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py
deleted file mode 100755
index 3bdf5a718..000000000
--- a/src/op_mode/cpu_summary.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2022 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import re
-from vyos.util import colon_separated_to_dict
-
-FILE_NAME = '/proc/cpuinfo'
-
-def get_raw_data():
- with open(FILE_NAME, 'r') as f:
- data_raw = f.read()
-
- data = colon_separated_to_dict(data_raw)
-
- # Accumulate all data in a dict for future support for machine-readable output
- cpu_data = {}
- cpu_data['cpu_number'] = len(data['processor'])
- cpu_data['models'] = list(set(data['model name']))
-
- # Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that
- cpu_data['models'] = list(map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models']))
-
- return cpu_data
-
-def get_formatted_output():
- cpu_data = get_raw_data()
-
- out = "CPU(s): {0}\n".format(cpu_data['cpu_number'])
- out += "CPU model(s): {0}".format(",".join(cpu_data['models']))
-
- return out
-
-if __name__ == '__main__':
- print(get_formatted_output())
-
diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py
new file mode 100755
index 000000000..432856585
--- /dev/null
+++ b/src/op_mode/ipsec.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import sys
+from vyos.util import call
+import vyos.opmode
+
+
+SWANCTL_CONF = '/etc/swanctl/swanctl.conf'
+
+
+def get_peer_connections(peer, tunnel, return_all = False):
+ peer = peer.replace(':', '-')
+ search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*'
+ matches = []
+ with open(SWANCTL_CONF, 'r') as f:
+ for line in f.readlines():
+ result = re.match(search, line)
+ if result:
+ suffix = f'tunnel_{tunnel}' if tunnel.isnumeric() else tunnel
+ if return_all or (result[2] == suffix):
+ matches.append(result[1])
+ return matches
+
+
+def reset_peer(peer: str, tunnel:str):
+ if not peer:
+ print('Invalid peer, aborting')
+ return
+
+ conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all'))
+
+ if not conns:
+ print('Tunnel(s) not found, aborting')
+ return
+
+ result = True
+ for conn in conns:
+ try:
+ 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}')
+ result = False
+
+
+ print('Peer reset result: ' + ('success' if result else 'failed'))
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except ValueError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/show_ram.py b/src/op_mode/memory.py
index 2b0be3965..a3870e498 100755
--- a/src/op_mode/show_ram.py
+++ b/src/op_mode/memory.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -15,7 +15,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-def get_system_memory():
+import sys
+
+import vyos.opmode
+
+
+def _get_system_memory():
from re import search as re_search
def find_value(keyword, mem_data):
@@ -43,10 +48,10 @@ def get_system_memory():
return res
-def get_system_memory_human():
+def _get_system_memory_human():
from vyos.util import bytes_to_human
- mem = get_system_memory()
+ mem = _get_system_memory()
for key in mem:
# The Linux kernel exposes memory values in kilobytes,
@@ -55,17 +60,31 @@ def get_system_memory_human():
return mem
-def get_raw_data():
- return get_system_memory_human()
-
-def get_formatted_output():
- mem = get_raw_data()
+def _get_raw_data():
+ return _get_system_memory_human()
+def _get_formatted_output(mem):
out = "Total: {}\n".format(mem["total"])
out += "Free: {}\n".format(mem["free"])
out += "Used: {}".format(mem["used"])
return out
+def show(raw: bool):
+ ram_data = _get_raw_data()
+
+ if raw:
+ return ram_data
+ else:
+ return _get_formatted_output(ram_data)
+
+
if __name__ == '__main__':
- print(get_formatted_output())
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except ValueError as e:
+ print(e)
+ sys.exit(1)
+
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
new file mode 100755
index 000000000..4b54ecf31
--- /dev/null
+++ b/src/op_mode/nat.py
@@ -0,0 +1,191 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import jmespath
+import json
+import sys
+
+from sys import exit
+from tabulate import tabulate
+
+from vyos.util import cmd
+from vyos.util import dict_search
+
+import vyos.opmode
+
+
+def _get_json_data(direction):
+ """
+ Get NAT format JSON
+ """
+ if direction == 'source':
+ chain = 'POSTROUTING'
+ if direction == 'destination':
+ chain = 'PREROUTING'
+ return cmd(f'sudo nft --json list chain ip nat {chain}')
+
+
+def _get_raw_data_rules(direction):
+ """Get interested rules
+ :returns dict
+ """
+ data = _get_json_data(direction)
+ data_dict = json.loads(data)
+ rules = []
+ for rule in data_dict['nftables']:
+ if 'rule' in rule and 'comment' in rule['rule']:
+ rules.append(rule)
+ return rules
+
+
+def _get_formatted_output_rules(data, direction):
+ # Add default values before loop
+ sport, dport, proto = 'any', 'any', 'any'
+ saddr, daddr = '0.0.0.0/0', '0.0.0.0/0'
+ data_entries = []
+ for rule in data:
+ if 'comment' in rule['rule']:
+ comment = rule.get('rule').get('comment')
+ rule_number = comment.split('-')[-1]
+ rule_number = rule_number.split(' ')[0]
+ if 'expr' in rule['rule']:
+ interface = rule.get('rule').get('expr')[0].get('match').get('right') \
+ if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any'
+ for index, match in enumerate(jmespath.search('rule.expr[*].match', rule)):
+ if 'payload' in match['left']:
+ if 'prefix' in match['right'] or 'set' in match['right']:
+ # Merge dict src/dst l3_l4 parameters
+ my_dict = {**match['left']['payload'], **match['right']}
+ proto = my_dict.get('protocol').upper()
+ if my_dict['field'] == 'saddr':
+ saddr = f'{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}'
+ elif my_dict['field'] == 'daddr':
+ daddr = f'{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}'
+ elif my_dict['field'] == 'sport':
+ # Port range or single port
+ if jmespath.search('set[*].range', my_dict):
+ sport = my_dict['set'][0]['range']
+ sport = '-'.join(map(str, sport))
+ else:
+ sport = my_dict.get('set')
+ sport = ','.join(map(str, sport))
+ elif my_dict['field'] == 'dport':
+ # Port range or single port
+ if jmespath.search('set[*].range', my_dict):
+ dport = my_dict["set"][0]["range"]
+ dport = '-'.join(map(str, dport))
+ else:
+ dport = my_dict.get('set')
+ dport = ','.join(map(str, dport))
+ else:
+ if jmespath.search('left.payload.field', match) == 'saddr':
+ saddr = match.get('right')
+ if jmespath.search('left.payload.field', match) == 'daddr':
+ daddr = match.get('right')
+ else:
+ saddr = '0.0.0.0/0'
+ daddr = '0.0.0.0/0'
+ sport = 'any'
+ dport = 'any'
+ proto = 'any'
+
+ source = f'''{saddr}
+sport {sport}'''
+ destination = f'''{daddr}
+dport {dport}'''
+
+ if jmespath.search('left.payload.field', match) == 'protocol':
+ field_proto = match.get('right').upper()
+
+ for expr in rule.get('rule').get('expr'):
+ if 'snat' in expr:
+ translation = dict_search('snat.addr', expr)
+ if expr['snat'] and 'port' in expr['snat']:
+ if jmespath.search('snat.port.range', expr):
+ port = dict_search('snat.port.range', expr)
+ port = '-'.join(map(str, port))
+ else:
+ port = expr['snat']['port']
+ translation = f'''{translation}
+port {port}'''
+
+ elif 'masquerade' in expr:
+ translation = 'masquerade'
+ if expr['masquerade'] and 'port' in expr['masquerade']:
+ if jmespath.search('masquerade.port.range', expr):
+ port = dict_search('masquerade.port.range', expr)
+ port = '-'.join(map(str, port))
+ else:
+ port = expr['masquerade']['port']
+
+ translation = f'''{translation}
+port {port}'''
+ else:
+ translation = 'exclude'
+ # Overwrite match loop 'proto' if specified filed 'protocol' exist
+ if 'protocol' in jmespath.search('rule.expr[*].match.left.payload.field', rule):
+ proto = jmespath.search('rule.expr[0].match.right', rule).upper()
+
+ data_entries.append([rule_number, source, destination, proto, interface, translation])
+
+ interface_header = 'Out-Int' if direction == 'source' else 'In-Int'
+ headers = ["Rule", "Source", "Destination", "Proto", interface_header, "Translation"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+
+def _get_formatted_output_statistics(data, direction):
+ data_entries = []
+ for rule in data:
+ if 'comment' in rule['rule']:
+ comment = rule.get('rule').get('comment')
+ rule_number = comment.split('-')[-1]
+ rule_number = rule_number.split(' ')[0]
+ if 'expr' in rule['rule']:
+ interface = rule.get('rule').get('expr')[0].get('match').get('right') \
+ if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any'
+ packets = jmespath.search('rule.expr[*].counter.packets | [0]', rule)
+ _bytes = jmespath.search('rule.expr[*].counter.bytes | [0]', rule)
+ data_entries.append([rule_number, packets, _bytes, interface])
+ headers = ["Rule", "Packets", "Bytes", "Interface"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+
+def show_rules(raw: bool, direction: str):
+ nat_rules = _get_raw_data_rules(direction)
+ if raw:
+ return nat_rules
+ else:
+ return _get_formatted_output_rules(nat_rules, direction)
+
+
+def show_statistics(raw: bool, direction: str):
+ nat_statistics = _get_raw_data_rules(direction)
+ if raw:
+ return nat_statistics
+ else:
+ return _get_formatted_output_statistics(nat_statistics, direction)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except ValueError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/show_neigh.py b/src/op_mode/neighbor.py
index d874bd544..d86a372ac 100755
--- a/src/op_mode/show_neigh.py
+++ b/src/op_mode/neighbor.py
@@ -28,29 +28,37 @@
# ]
import sys
+import typing
+import vyos.opmode
-def get_raw_data(family, device=None, state=None):
+def interface_exists(interface):
+ import os
+ return os.path.exists(f'/sys/class/net/{interface}')
+
+def get_raw_data(family, interface=None, state=None):
from json import loads
from vyos.util import cmd
- if device:
- device = f"dev {device}"
+ if interface:
+ if not interface_exists(interface):
+ raise ValueError(f"Interface '{interface}' does not exist in the system")
+ interface = f"dev {interface}"
else:
- device = ""
+ interface = ""
if state:
state = f"nud {state}"
else:
state = ""
- neigh_cmd = f"ip --family {family} --json neighbor list {device} {state}"
+ neigh_cmd = f"ip --family {family} --json neighbor list {interface} {state}"
data = loads(cmd(neigh_cmd))
return data
-def get_formatted_output(family, device=None, state=None):
+def format_neighbors(neighs, interface=None):
from tabulate import tabulate
def entry_to_list(e, intf=None):
@@ -68,35 +76,47 @@ def get_formatted_output(family, device=None, state=None):
# Device field is absent from outputs of `ip neigh list dev ...`
if "dev" in e:
dev = e["dev"]
- elif device:
- dev = device
+ elif interface:
+ dev = interface
else:
raise ValueError("interface is not defined")
return [dst, dev, lladdr, state]
- neighs = get_raw_data(family, device=device, state=state)
neighs = map(entry_to_list, neighs)
headers = ["Address", "Interface", "Link layer address", "State"]
return tabulate(neighs, headers)
-if __name__ == '__main__':
- from argparse import ArgumentParser
+def show(raw: bool, family: str, interface: typing.Optional[str], state: typing.Optional[str]):
+ """ Display neighbor table contents """
+ data = get_raw_data(family, interface, state=state)
- parser = ArgumentParser()
- parser.add_argument("-f", "--family", type=str, default="inet", help="Address family")
- parser.add_argument("-i", "--interface", type=str, help="Network interface")
- parser.add_argument("-s", "--state", type=str, help="Neighbor table entry state")
+ if raw:
+ return data
+ else:
+ return format_neighbors(data, interface)
- args = parser.parse_args()
+def reset(family: str, interface: typing.Optional[str], address: typing.Optional[str]):
+ from vyos.util import run
- if args.state:
- if args.state not in ["reachable", "failed", "stale", "permanent"]:
- raise ValueError(f"""Incorrect state "{args.state}"! Must be one of: reachable, stale, failed, permanent""")
+ if address and interface:
+ raise ValueError("interface and address parameters are mutually exclusive")
+ elif address:
+ run(f"""ip --family {family} neighbor flush to {address}""")
+ elif interface:
+ run(f"""ip --family {family} neighbor flush dev {interface}""")
+ else:
+ # Flush an entire neighbor table
+ run(f"""ip --family {family} neighbor flush""")
+
+if __name__ == '__main__':
try:
- print(get_formatted_output(args.family, device=args.interface, state=args.state))
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
except ValueError as e:
print(e)
sys.exit(1)
+
diff --git a/src/op_mode/route.py b/src/op_mode/route.py
new file mode 100644
index 000000000..3bb06adac
--- /dev/null
+++ b/src/op_mode/route.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Purpose:
+# Displays routing table information.
+# Used by the "run <ip|ipv6> route *" commands.
+
+import re
+import sys
+import typing
+
+from jinja2 import Template
+
+import vyos.opmode
+
+frr_command_template = Template("""
+{% if family == "inet" %}
+ show ip route
+{% else %}
+ show ipv6 route
+{% endif %}
+
+{% if table %}
+ table {{table}}
+{% endif %}
+
+{% if vrf %}
+ vrf {{table}}
+{% endif %}
+
+{% if tag %}
+ tag {{tag}}
+{% elif net %}
+ {{net}}
+{% elif protocol %}
+ {{protocol}}
+{% endif %}
+
+{% if raw %}
+ json
+{% endif %}
+""")
+
+def show(raw: bool,
+ family: str,
+ net: typing.Optional[str],
+ table: typing.Optional[int],
+ protocol: typing.Optional[str],
+ vrf: typing.Optional[str],
+ tag: typing.Optional[str]):
+ if net and protocol:
+ raise ValueError("net and protocol are mutually exclusive")
+ elif table and vrf:
+ raise ValueError("table and vrf are mutually exclusive")
+ elif (family == 'inet6') and (protocol == 'rip'):
+ raise ValueError("rip is not a valid protocol for family inet6")
+ elif (family == 'inet') and (protocol == 'ripng'):
+ raise ValueError("rip is not a valid protocol for family inet6")
+ else:
+ if (family == 'inet6') and (protocol == 'ospf'):
+ protocol = 'ospf6'
+
+ kwargs = dict(locals())
+
+ frr_command = frr_command_template.render(kwargs)
+ frr_command = re.sub(r'\s+', ' ', frr_command)
+
+ from vyos.util import cmd
+ output = cmd(f"vtysh -c '{frr_command}'")
+
+ if raw:
+ from json import loads
+ return loads(output)
+ else:
+ return output
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except ValueError as e:
+ print(e)
+ sys.exit(1)
+
diff --git a/src/op_mode/show_cpu.py b/src/op_mode/show_cpu.py
deleted file mode 100755
index 9973d9789..000000000
--- a/src/op_mode/show_cpu.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2016-2020 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import json
-
-from jinja2 import Template
-from sys import exit
-from vyos.util import popen, DEVNULL
-
-OUT_TMPL_SRC = """
-{%- if cpu -%}
-{% if 'vendor' in cpu %}CPU Vendor: {{cpu.vendor}}{% endif %}
-{% if 'model' in cpu %}Model: {{cpu.model}}{% endif %}
-{% if 'cpus' in cpu %}Total CPUs: {{cpu.cpus}}{% endif %}
-{% if 'sockets' in cpu %}Sockets: {{cpu.sockets}}{% endif %}
-{% if 'cores' in cpu %}Cores: {{cpu.cores}}{% endif %}
-{% if 'threads' in cpu %}Threads: {{cpu.threads}}{% endif %}
-{% if 'mhz' in cpu %}Current MHz: {{cpu.mhz}}{% endif %}
-{% if 'mhz_min' in cpu %}Minimum MHz: {{cpu.mhz_min}}{% endif %}
-{% if 'mhz_max' in cpu %}Maximum MHz: {{cpu.mhz_max}}{% endif %}
-{%- endif -%}
-"""
-
-def get_raw_data():
- cpu = {}
- cpu_json, code = popen('lscpu -J', stderr=DEVNULL)
-
- if code == 0:
- cpu_info = json.loads(cpu_json)
- if len(cpu_info) > 0 and 'lscpu' in cpu_info:
- for prop in cpu_info['lscpu']:
- if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data']
- if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data']
- if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data']
- if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data']
- if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data']
- if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data']
- if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data']
- if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data']
- if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data']
-
- return cpu
-
-def get_formatted_output():
- cpu = get_raw_data()
-
- tmp = {'cpu':cpu}
- tmpl = Template(OUT_TMPL_SRC)
- return tmpl.render(tmp)
-
-if __name__ == '__main__':
- cpu = get_raw_data()
-
- if len(cpu) > 0:
- print(get_formatted_output())
- else:
- print('CPU information could not be determined\n')
- exit(1)
-
diff --git a/src/op_mode/show_nat_rules.py b/src/op_mode/show_nat_rules.py
index 98adb31dd..60a4bdd13 100755
--- a/src/op_mode/show_nat_rules.py
+++ b/src/op_mode/show_nat_rules.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -99,8 +99,9 @@ if args.source or args.destination:
if addr_tmp and len_tmp:
tran_addr += addr_tmp + '/' + str(len_tmp) + ' '
- if isinstance(tran_addr_json['port'],int):
- tran_addr += 'port ' + str(tran_addr_json['port'])
+ if tran_addr_json.get('port'):
+ if isinstance(tran_addr_json['port'],int):
+ tran_addr += 'port ' + str(tran_addr_json['port'])
else:
if 'masquerade' in data['expr'][i]:
@@ -111,6 +112,8 @@ if args.source or args.destination:
if srcdest != '':
srcdests.append(srcdest)
srcdest = ''
+ else:
+ srcdests.append('any')
print(format_nat_rule.format(rule, srcdests[0], tran_addr, interface))
for i in range(1, len(srcdests)):
diff --git a/src/op_mode/show_nat_translations.py b/src/op_mode/show_nat_translations.py
index 25091e9fc..508845e23 100755
--- a/src/op_mode/show_nat_translations.py
+++ b/src/op_mode/show_nat_translations.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -83,11 +83,23 @@ def pipe():
return xml
+def xml_to_dict(xml):
+ """
+ Convert XML to dictionary
+ Return: dictionary
+ """
+ parse = xmltodict.parse(xml)
+ # If only one NAT entry we must change dict T4499
+ if 'meta' in parse['conntrack']['flow']:
+ return dict(conntrack={'flow': [parse['conntrack']['flow']]})
+ return parse
+
+
def process(data, stats, protocol, pipe, verbose, flowtype=''):
if not data:
return
- parsed = xmltodict.parse(data)
+ parsed = xml_to_dict(data)
print(headers(verbose, pipe))
diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py
deleted file mode 100755
index 3c7a90205..000000000
--- a/src/op_mode/show_vrf.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2020 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import argparse
-import jinja2
-from json import loads
-
-from vyos.util import cmd
-
-vrf_out_tmpl = """VRF name state mac address flags interfaces
--------- ----- ----------- ----- ----------
-{%- for v in vrf %}
-{{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}}
-{%- endfor %}
-
-"""
-
-def list_vrfs():
- command = 'ip -j -br link show type vrf'
- answer = loads(cmd(command))
- return [_ for _ in answer if _]
-
-def list_vrf_members(vrf):
- command = f'ip -j -br link show master {vrf}'
- answer = loads(cmd(command))
- return [_ for _ in answer if _]
-
-parser = argparse.ArgumentParser()
-group = parser.add_mutually_exclusive_group()
-group.add_argument("-e", "--extensive", action="store_true",
- help="provide detailed vrf informatio")
-parser.add_argument('interface', metavar='I', type=str, nargs='?',
- help='interface to display')
-
-args = parser.parse_args()
-
-if args.extensive:
- data = { 'vrf': [] }
- for vrf in list_vrfs():
- name = vrf['ifname']
- if args.interface and name != args.interface:
- continue
-
- vrf['members'] = []
- for member in list_vrf_members(name):
- vrf['members'].append(member['ifname'])
- data['vrf'].append(vrf)
-
- tmpl = jinja2.Template(vrf_out_tmpl)
- print(tmpl.render(data))
-
-else:
- print(" ".join([vrf['ifname'] for vrf in list_vrfs()]))
diff --git a/src/op_mode/show_version.py b/src/op_mode/version.py
index b82ab6eca..06208c3e5 100755
--- a/src/op_mode/show_version.py
+++ b/src/op_mode/version.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2016-2020 VyOS maintainers and contributors
+# Copyright (C) 2016-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -18,13 +18,14 @@
# Displays image version and system information.
# Used by the "run show version" command.
-import argparse
+import sys
+import typing
+
+import vyos.opmode
import vyos.version
import vyos.limericks
from jinja2 import Template
-from sys import exit
-from vyos.util import call
version_output_tmpl = """
Version: VyOS {{version}}
@@ -45,32 +46,39 @@ Hardware S/N: {{hardware_serial}}
Hardware UUID: {{hardware_uuid}}
Copyright: VyOS maintainers and contributors
+{%- if limerick %}
+{{limerick}}
+{% endif -%}
"""
-def get_raw_data():
+def _get_raw_data(funny=False):
version_data = vyos.version.get_full_version_data()
+
+ if funny:
+ version_data["limerick"] = vyos.limericks.get_random()
+
return version_data
-def get_formatted_output():
- version_data = get_raw_data()
+def _get_formatted_output(version_data):
tmpl = Template(version_output_tmpl)
- return tmpl.render(version_data)
+ return tmpl.render(version_data).strip()
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
- parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
+def show(raw: bool, funny: typing.Optional[bool]):
+ """ Display neighbor table contents """
+ version_data = _get_raw_data(funny=funny)
- args = parser.parse_args()
+ if raw:
+ return version_data
+ else:
+ return _get_formatted_output(version_data)
- version_data = vyos.version.get_full_version_data()
- if args.json:
- import json
- print(json.dumps(version_data))
- exit(0)
- else:
- print(get_formatted_output())
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except ValueError as e:
+ print(e)
+ sys.exit(1)
- if args.funny:
- print(vyos.limericks.get_random())
diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py
index 8955e5a59..68dc5bc45 100755
--- a/src/op_mode/vpn_ipsec.py
+++ b/src/op_mode/vpn_ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -87,6 +87,7 @@ def reset_profile(profile, tunnel):
print('Profile reset result: ' + ('success' if result == 0 else 'failed'))
def debug_peer(peer, tunnel):
+ peer = peer.replace(':', '-')
if not peer or peer == "all":
debug_commands = [
"sudo ipsec statusall",
@@ -109,7 +110,7 @@ def debug_peer(peer, tunnel):
if not tunnel or tunnel == 'all':
tunnel = ''
- conn = get_peer_connections(peer, tunnel)
+ conns = get_peer_connections(peer, tunnel, return_all = (tunnel == '' or tunnel == 'all'))
if not conns:
print('Peer not found, aborting')
diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py
new file mode 100755
index 000000000..f86516786
--- /dev/null
+++ b/src/op_mode/vrf.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import sys
+import typing
+
+from tabulate import tabulate
+from vyos.util import cmd
+
+import vyos.opmode
+
+
+def _get_raw_data(name=None):
+ """
+ If vrf name is not set - get all VRFs
+ If vrf name is set - get only this name data
+ If vrf name set and not found - return []
+ """
+ output = cmd('sudo ip --json --brief link show type vrf')
+ data = json.loads(output)
+ if not data:
+ return []
+ if name:
+ is_vrf_exists = True if [vrf for vrf in data if vrf.get('ifname') == name] else False
+ if is_vrf_exists:
+ output = cmd(f'sudo ip --json --brief link show dev {name}')
+ data = json.loads(output)
+ return data
+ return []
+ return data
+
+
+def _get_vrf_members(vrf: str) -> list:
+ """
+ Get list of interface VRF members
+ :param vrf: str
+ :return: list
+ """
+ output = cmd(f'sudo ip --json --brief link show master {vrf}')
+ answer = json.loads(output)
+ interfaces = []
+ for data in answer:
+ if 'ifname' in data:
+ interfaces.append(data.get('ifname'))
+ return interfaces if len(interfaces) > 0 else ['n/a']
+
+
+def _get_formatted_output(raw_data):
+ data_entries = []
+ for vrf in raw_data:
+ name = vrf.get('ifname')
+ state = vrf.get('operstate').lower()
+ hw_address = vrf.get('address')
+ flags = ','.join(vrf.get('flags')).lower()
+ members = ','.join(_get_vrf_members(name))
+ data_entries.append([name, state, hw_address, flags, members])
+
+ headers = ["Name", "State", "MAC address", "Flags", "Interfaces"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+
+def show(raw: bool, name: typing.Optional[str]):
+ vrf_data = _get_raw_data(name=name)
+ if raw:
+ return vrf_data
+ else:
+ return _get_formatted_output(vrf_data)
+
+
+if __name__ == "__main__":
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except ValueError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py
index 84d719fda..049d59de7 100644
--- a/src/services/api/graphql/bindings.py
+++ b/src/services/api/graphql/bindings.py
@@ -17,11 +17,14 @@ import vyos.defaults
from . graphql.queries import query
from . graphql.mutations import mutation
from . graphql.directives import directives_dict
+from . utils.schema_from_op_mode import generate_op_mode_definitions
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']
+ generate_op_mode_definitions()
+
type_defs = load_schema_from_path(api_schema_dir)
schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers, directives=directives_dict)
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
index 0a9298f55..d8ceefae6 100644
--- a/src/services/api/graphql/graphql/directives.py
+++ b/src/services/api/graphql/graphql/directives.py
@@ -48,6 +48,14 @@ class ShowConfigDirective(VyosDirective):
super().visit_field_definition(field, object_type,
make_resolver=make_show_config_resolver)
+class SystemStatusDirective(VyosDirective):
+ """
+ Class providing implementation of 'system_status' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_system_status_resolver)
+
class ConfigFileDirective(VyosDirective):
"""
Class providing implementation of 'configfile' directive in schema.
@@ -72,8 +80,27 @@ class ImageDirective(VyosDirective):
super().visit_field_definition(field, object_type,
make_resolver=make_image_resolver)
+class GenOpQueryDirective(VyosDirective):
+ """
+ Class providing implementation of 'genopquery' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_gen_op_query_resolver)
+
+class GenOpMutationDirective(VyosDirective):
+ """
+ Class providing implementation of 'genopmutation' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_gen_op_mutation_resolver)
+
directives_dict = {"configure": ConfigureDirective,
"showconfig": ShowConfigDirective,
+ "systemstatus": SystemStatusDirective,
"configfile": ConfigFileDirective,
"show": ShowDirective,
- "image": ImageDirective}
+ "image": ImageDirective,
+ "genopquery": GenOpQueryDirective,
+ "genopmutation": GenOpMutationDirective}
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
index 0c3eb702a..3e89fb239 100644
--- a/src/services/api/graphql/graphql/mutations.py
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -1,4 +1,4 @@
-# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -20,6 +20,7 @@ from graphql import GraphQLResolveInfo
from makefun import with_signature
from .. import state
+from .. import key_auth
from api.graphql.recipes.session import Session
mutation = ObjectType("Mutation")
@@ -53,6 +54,19 @@ def make_mutation_resolver(mutation_name, class_name, session_func):
}
data = kwargs['data']
+ key = data['key']
+
+ auth = key_auth.auth_required(key)
+ if auth is None:
+ return {
+ "success": False,
+ "errors": ['invalid API key']
+ }
+
+ # We are finished with the 'key' entry, and may remove so as to
+ # pass the rest of data (if any) to function.
+ del data['key']
+
session = state.settings['app'].state.vyos_session
# one may override the session functions with a local subclass
@@ -61,7 +75,7 @@ def make_mutation_resolver(mutation_name, class_name, session_func):
klass = getattr(mod, class_name)
except ImportError:
# otherwise, dynamically generate subclass to invoke subclass
- # name based templates
+ # name based functions
klass = type(class_name, (Session,), {})
k = klass(session, data)
method = getattr(k, session_func)
@@ -97,3 +111,7 @@ def make_config_file_resolver(mutation_name):
def make_image_resolver(mutation_name):
return make_prefix_resolver(mutation_name, prefix=['add', 'delete'])
+
+def make_gen_op_mutation_resolver(mutation_name):
+ class_name = mutation_name
+ return make_mutation_resolver(mutation_name, class_name, 'gen_op_mutation')
diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py
index e1868091e..f6544709e 100644
--- a/src/services/api/graphql/graphql/queries.py
+++ b/src/services/api/graphql/graphql/queries.py
@@ -1,4 +1,4 @@
-# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -20,6 +20,7 @@ from graphql import GraphQLResolveInfo
from makefun import with_signature
from .. import state
+from .. import key_auth
from api.graphql.recipes.session import Session
query = ObjectType("Query")
@@ -53,6 +54,19 @@ def make_query_resolver(query_name, class_name, session_func):
}
data = kwargs['data']
+ key = data['key']
+
+ auth = key_auth.auth_required(key)
+ if auth is None:
+ return {
+ "success": False,
+ "errors": ['invalid API key']
+ }
+
+ # We are finished with the 'key' entry, and may remove so as to
+ # pass the rest of data (if any) to function.
+ del data['key']
+
session = state.settings['app'].state.vyos_session
# one may override the session functions with a local subclass
@@ -61,7 +75,7 @@ def make_query_resolver(query_name, class_name, session_func):
klass = getattr(mod, class_name)
except ImportError:
# otherwise, dynamically generate subclass to invoke subclass
- # name based templates
+ # name based functions
klass = type(class_name, (Session,), {})
k = klass(session, data)
method = getattr(k, session_func)
@@ -84,6 +98,14 @@ def make_show_config_resolver(query_name):
class_name = query_name
return make_query_resolver(query_name, class_name, 'show_config')
+def make_system_status_resolver(query_name):
+ class_name = query_name
+ return make_query_resolver(query_name, class_name, 'system_status')
+
def make_show_resolver(query_name):
class_name = query_name
return make_query_resolver(query_name, class_name, 'show')
+
+def make_gen_op_query_resolver(query_name):
+ class_name = query_name
+ return make_query_resolver(query_name, class_name, 'gen_op_query')
diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql
index 31ab26b9e..a7263114b 100644
--- a/src/services/api/graphql/graphql/schema/config_file.graphql
+++ b/src/services/api/graphql/graphql/schema/config_file.graphql
@@ -1,4 +1,5 @@
input SaveConfigFileInput {
+ key: String!
fileName: String
}
@@ -13,6 +14,7 @@ type SaveConfigFileResult {
}
input LoadConfigFileInput {
+ key: String!
fileName: String!
}
diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
index 25f091bfa..345c349ac 100644
--- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql
+++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
@@ -1,4 +1,5 @@
input DhcpServerConfigInput {
+ key: String!
sharedNetworkName: String
subnet: String
defaultRouter: String
diff --git a/src/services/api/graphql/graphql/schema/firewall_group.graphql b/src/services/api/graphql/graphql/schema/firewall_group.graphql
index d89904b9e..9454d2997 100644
--- a/src/services/api/graphql/graphql/schema/firewall_group.graphql
+++ b/src/services/api/graphql/graphql/schema/firewall_group.graphql
@@ -1,4 +1,5 @@
input CreateFirewallAddressGroupInput {
+ key: String!
name: String!
address: [String]
}
@@ -15,6 +16,7 @@ type CreateFirewallAddressGroupResult {
}
input UpdateFirewallAddressGroupMembersInput {
+ key: String!
name: String!
address: [String!]!
}
@@ -31,6 +33,7 @@ type UpdateFirewallAddressGroupMembersResult {
}
input RemoveFirewallAddressGroupMembersInput {
+ key: String!
name: String!
address: [String!]!
}
@@ -47,6 +50,7 @@ type RemoveFirewallAddressGroupMembersResult {
}
input CreateFirewallAddressIpv6GroupInput {
+ key: String!
name: String!
address: [String]
}
@@ -63,6 +67,7 @@ type CreateFirewallAddressIpv6GroupResult {
}
input UpdateFirewallAddressIpv6GroupMembersInput {
+ key: String!
name: String!
address: [String!]!
}
@@ -79,6 +84,7 @@ type UpdateFirewallAddressIpv6GroupMembersResult {
}
input RemoveFirewallAddressIpv6GroupMembersInput {
+ key: String!
name: String!
address: [String!]!
}
diff --git a/src/services/api/graphql/graphql/schema/image.graphql b/src/services/api/graphql/graphql/schema/image.graphql
index 7d1b4f9d0..485033875 100644
--- a/src/services/api/graphql/graphql/schema/image.graphql
+++ b/src/services/api/graphql/graphql/schema/image.graphql
@@ -1,4 +1,5 @@
input AddSystemImageInput {
+ key: String!
location: String!
}
@@ -14,6 +15,7 @@ type AddSystemImageResult {
}
input DeleteSystemImageInput {
+ key: String!
name: String!
}
diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
index 32438b315..8a17d919f 100644
--- a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
+++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
@@ -1,4 +1,5 @@
input InterfaceEthernetConfigInput {
+ key: String!
interface: String
address: String
replace: Boolean = true
diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
index 952e46f34..624be2620 100644
--- a/src/services/api/graphql/graphql/schema/schema.graphql
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -7,11 +7,17 @@ directive @configure on FIELD_DEFINITION
directive @configfile on FIELD_DEFINITION
directive @show on FIELD_DEFINITION
directive @showconfig on FIELD_DEFINITION
+directive @systemstatus on FIELD_DEFINITION
directive @image on FIELD_DEFINITION
+directive @genopquery on FIELD_DEFINITION
+directive @genopmutation on FIELD_DEFINITION
+
+scalar Generic
type Query {
Show(data: ShowInput) : ShowResult @show
ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig
+ SystemStatus(data: SystemStatusInput) : SystemStatusResult @systemstatus
}
type Mutation {
diff --git a/src/services/api/graphql/graphql/schema/show.graphql b/src/services/api/graphql/graphql/schema/show.graphql
index c7709e48b..278ed536b 100644
--- a/src/services/api/graphql/graphql/schema/show.graphql
+++ b/src/services/api/graphql/graphql/schema/show.graphql
@@ -1,4 +1,5 @@
input ShowInput {
+ key: String!
path: [String!]!
}
diff --git a/src/services/api/graphql/graphql/schema/show_config.graphql b/src/services/api/graphql/graphql/schema/show_config.graphql
index 34afd2aa9..5a1fe43da 100644
--- a/src/services/api/graphql/graphql/schema/show_config.graphql
+++ b/src/services/api/graphql/graphql/schema/show_config.graphql
@@ -2,9 +2,9 @@
Use 'scalar Generic' for show config output, to avoid attempts to
JSON-serialize in case of JSON output.
"""
-scalar Generic
input ShowConfigInput {
+ key: String!
path: [String!]!
configFormat: String
}
diff --git a/src/services/api/graphql/graphql/schema/system_status.graphql b/src/services/api/graphql/graphql/schema/system_status.graphql
new file mode 100644
index 000000000..be8d87535
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/system_status.graphql
@@ -0,0 +1,18 @@
+"""
+Use 'scalar Generic' for system status output, to avoid attempts to
+JSON-serialize in case of JSON output.
+"""
+
+input SystemStatusInput {
+ key: String!
+}
+
+type SystemStatus {
+ result: Generic
+}
+
+type SystemStatusResult {
+ data: SystemStatus
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/key_auth.py b/src/services/api/graphql/key_auth.py
new file mode 100644
index 000000000..f756ed6d8
--- /dev/null
+++ b/src/services/api/graphql/key_auth.py
@@ -0,0 +1,18 @@
+
+from . import state
+
+def check_auth(key_list, key):
+ if not key_list:
+ return None
+ key_id = None
+ for k in key_list:
+ if k['key'] == key:
+ key_id = k['id']
+ return key_id
+
+def auth_required(key):
+ api_keys = None
+ api_keys = state.settings['app'].state.vyos_keys
+ key_id = check_auth(api_keys, key)
+ state.settings['app'].state.vyos_id = key_id
+ return key_id
diff --git a/src/services/api/graphql/recipes/queries/system_status.py b/src/services/api/graphql/recipes/queries/system_status.py
new file mode 100755
index 000000000..8dadcc9f3
--- /dev/null
+++ b/src/services/api/graphql/recipes/queries/system_status.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import os
+import sys
+import json
+import importlib.util
+
+from vyos.defaults import directories
+
+from api.graphql.utils.util import load_op_mode_as_module
+
+def get_system_version() -> dict:
+ show_version = load_op_mode_as_module('version.py')
+ return show_version.show(raw=True, funny=False)
+
+def get_system_uptime() -> dict:
+ show_uptime = load_op_mode_as_module('show_uptime.py')
+ return show_uptime.get_raw_data()
+
+def get_system_ram_usage() -> dict:
+ show_ram = load_op_mode_as_module('memory.py')
+ return show_ram.show(raw=True)
diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py
index 1f844ff70..6b580af01 100644
--- a/src/services/api/graphql/recipes/session.py
+++ b/src/services/api/graphql/recipes/session.py
@@ -1,4 +1,4 @@
-# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -13,15 +13,20 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this library. If not, see <http://www.gnu.org/licenses/>.
+import os
import json
from ariadne import convert_camel_case_to_snake
-import vyos.defaults
from vyos.config import Config
from vyos.configtree import ConfigTree
+from vyos.defaults import directories
from vyos.template import render
+from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name
+
+op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json')
+
class Session:
"""
Wrapper for calling configsession functions based on GraphQL requests.
@@ -33,6 +38,12 @@ class Session:
self._data = data
self._name = convert_camel_case_to_snake(type(self).__name__)
+ try:
+ with open(op_mode_include_file) as f:
+ self._op_mode_list = f.read()
+ except Exception:
+ self._op_mode_list = None
+
def configure(self):
session = self._session
data = self._data
@@ -40,7 +51,7 @@ class Session:
tmpl_file = f'{func_base_name}.tmpl'
cmd_file = f'/tmp/{func_base_name}.cmds'
- tmpl_dir = vyos.defaults.directories['api_templates']
+ tmpl_dir = directories['api_templates']
try:
render(cmd_file, tmpl_file, data, location=tmpl_dir)
@@ -136,3 +147,61 @@ class Session:
raise error
return res
+
+ def system_status(self):
+ import api.graphql.recipes.queries.system_status as system_status
+
+ session = self._session
+ data = self._data
+
+ status = {}
+ status['host_name'] = session.show(['host', 'name']).strip()
+ status['version'] = system_status.get_system_version()
+ status['uptime'] = system_status.get_system_uptime()
+ status['ram'] = system_status.get_system_ram_usage()
+
+ return status
+
+ def gen_op_query(self):
+ session = self._session
+ data = self._data
+ name = self._name
+ op_mode_list = self._op_mode_list
+
+ # handle the case that the op-mode file contains underscores:
+ if op_mode_list is None:
+ raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'")
+ (func_name, basename) = split_compound_op_mode_name(name, op_mode_list)
+ if basename == '':
+ raise FileNotFoundError(f"No op-mode file basename in string '{name}'")
+
+ mod = load_op_mode_as_module(f'{basename}.py')
+ func = getattr(mod, func_name)
+ if len(list(data)) > 0:
+ res = func(True, **data)
+ else:
+ res = func(True)
+
+ return res
+
+ def gen_op_mutation(self):
+ session = self._session
+ data = self._data
+ name = self._name
+ op_mode_list = self._op_mode_list
+
+ # handle the case that the op-mode file name contains underscores:
+ if op_mode_list is None:
+ raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'")
+ (func_name, basename) = split_compound_op_mode_name(name, op_mode_list)
+ if basename == '':
+ raise FileNotFoundError(f"No op-mode file basename in string '{name}'")
+
+ mod = load_op_mode_as_module(f'{basename}.py')
+ func = getattr(mod, func_name)
+ if len(list(data)) > 0:
+ res = func(**data)
+ else:
+ res = func()
+
+ return res
diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/utils/schema_from_op_mode.py
new file mode 100755
index 000000000..cdde5f187
--- /dev/null
+++ b/src/services/api/graphql/utils/schema_from_op_mode.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+# A utility to generate GraphQL schema defintions from standardized op-mode
+# scripts.
+
+import os
+import json
+import typing
+from inspect import signature, getmembers, isfunction
+from jinja2 import Template
+
+from vyos.defaults import directories
+from . util import load_as_module, is_op_mode_function_name, is_show_function_name
+
+OP_MODE_PATH = directories['op_mode']
+SCHEMA_PATH = directories['api_schema']
+DATA_DIR = directories['data']
+
+op_mode_include_file = os.path.join(DATA_DIR, 'op-mode-standardized.json')
+
+schema_data: dict = {'schema_name': '',
+ 'schema_fields': []}
+
+query_template = """
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Query {
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopquery
+}
+"""
+
+mutation_template = """
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopmutation
+}
+"""
+
+def _snake_to_pascal_case(name: str) -> str:
+ res = ''.join(map(str.title, name.split('_')))
+ return res
+
+def _map_type_name(type_name: type, optional: bool = False) -> str:
+ if type_name == str:
+ return 'String!' if not optional else 'String = null'
+ if type_name == int:
+ return 'Int!' if not optional else 'Int = null'
+ if type_name == bool:
+ return 'Boolean!' if not optional else 'Boolean = false'
+ if typing.get_origin(type_name) == list:
+ if not optional:
+ return f'[{_map_type_name(typing.get_args(type_name)[0])}]!'
+ return f'[{_map_type_name(typing.get_args(type_name)[0])}]'
+ # typing.Optional is typing.Union[_, NoneType]
+ if (typing.get_origin(type_name) is typing.Union and
+ typing.get_args(type_name)[1] == type(None)):
+ return f'{_map_type_name(typing.get_args(type_name)[0], optional=True)}'
+
+ # scalar 'Generic' is defined in schema.graphql
+ return 'Generic'
+
+def create_schema(func_name: str, base_name: str, func: callable) -> str:
+ sig = signature(func)
+
+ field_dict = {}
+ for k in sig.parameters:
+ field_dict[sig.parameters[k].name] = _map_type_name(sig.parameters[k].annotation)
+
+ # It is assumed that if one is generating a schema for a 'show_*'
+ # function, that 'get_raw_data' is present and 'raw' is desired.
+ if 'raw' in list(field_dict):
+ del field_dict['raw']
+
+ schema_fields = []
+ for k,v in field_dict.items():
+ schema_fields.append(k+': '+v)
+
+ schema_data['schema_name'] = _snake_to_pascal_case(func_name + '_' + base_name)
+ schema_data['schema_fields'] = schema_fields
+
+ if is_show_function_name(func_name):
+ j2_template = Template(query_template)
+ else:
+ j2_template = Template(mutation_template)
+
+ res = j2_template.render(schema_data)
+
+ return res
+
+def generate_op_mode_definitions():
+ with open(op_mode_include_file) as f:
+ op_mode_files = json.load(f)
+
+ for file in op_mode_files:
+ basename = os.path.splitext(file)[0]
+ module = load_as_module(basename, os.path.join(OP_MODE_PATH, file))
+
+ funcs = getmembers(module, isfunction)
+ funcs = list(filter(lambda ft: is_op_mode_function_name(ft[0]), funcs))
+
+ funcs_dict = {}
+ for (name, thunk) in funcs:
+ funcs_dict[name] = thunk
+
+ results = []
+ for name,func in funcs_dict.items():
+ res = create_schema(name, basename, func)
+ results.append(res)
+
+ out = '\n'.join(results)
+ with open(f'{SCHEMA_PATH}/{basename}.graphql', 'w') as f:
+ f.write(out)
+
+if __name__ == '__main__':
+ generate_op_mode_definitions()
diff --git a/src/services/api/graphql/utils/util.py b/src/services/api/graphql/utils/util.py
new file mode 100644
index 000000000..e3dea31bf
--- /dev/null
+++ b/src/services/api/graphql/utils/util.py
@@ -0,0 +1,55 @@
+# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import importlib.util
+
+from vyos.defaults import directories
+
+def load_as_module(name: str, path: str):
+ spec = importlib.util.spec_from_file_location(name, path)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
+
+def load_op_mode_as_module(name: str):
+ path = os.path.join(directories['op_mode'], name)
+ name = os.path.splitext(name)[0]
+ return load_as_module(name, path)
+
+def is_op_mode_function_name(name):
+ if re.match(r"^(show|clear|reset|restart)", name):
+ return True
+ return False
+
+def is_show_function_name(name):
+ if re.match(r"^show", name):
+ return True
+ return False
+
+def _nth_rsplit(delim: str, n: int, s: str):
+ groups = s.split(delim)
+ l = len(groups)
+ if n > l-1:
+ return ('', s)
+ return (delim.join(groups[:l-n]), delim.join(groups[l-n:]))
+
+def split_compound_op_mode_name(name: str, files: list):
+ for i in range(1, name.count('_') + 1):
+ pair = _nth_rsplit('_', i, name)
+ if pair[1] in files:
+ return pair
+ return (name, '')
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index e9b904ba8..af8837e1e 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -654,11 +654,13 @@ def graphql_init(fast_api_app):
schema = generate_schema()
+ in_spec = app.state.vyos_introspection
+
if app.state.vyos_origins:
origins = app.state.vyos_origins
- app.add_route('/graphql', CORSMiddleware(GraphQL(schema, debug=True), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS")))
+ app.add_route('/graphql', CORSMiddleware(GraphQL(schema, debug=True, introspection=in_spec), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS")))
else:
- app.add_route('/graphql', GraphQL(schema, debug=True))
+ app.add_route('/graphql', GraphQL(schema, debug=True, introspection=in_spec))
###
@@ -684,6 +686,7 @@ if __name__ == '__main__':
app.state.vyos_debug = server_config['debug']
app.state.vyos_gql = server_config['gql']
+ app.state.vyos_introspection = server_config['introspection']
app.state.vyos_strict = server_config['strict']
app.state.vyos_origins = server_config.get('cors', {}).get('origins', [])
diff --git a/src/systemd/wpa_supplicant-macsec@.service b/src/systemd/wpa_supplicant-macsec@.service
index 7e0bee8e1..93bebd9d9 100644
--- a/src/systemd/wpa_supplicant-macsec@.service
+++ b/src/systemd/wpa_supplicant-macsec@.service
@@ -1,12 +1,10 @@
[Unit]
-Description=WPA supplicant daemon (macsec-specific version)
+Description=WPA supplicant daemon (MACsec-specific version)
Requires=sys-subsystem-net-devices-%i.device
ConditionPathExists=/run/wpa_supplicant/%I.conf
After=vyos-router.service
RequiresMountsFor=/run
-# NetworkManager users will probably want the dbus version instead.
-
[Service]
Type=simple
WorkingDirectory=/run/wpa_supplicant