summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py9
-rwxr-xr-xsrc/conf_mode/http-api.py8
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py10
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py12
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py5
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py2
-rwxr-xr-xsrc/conf_mode/load-balancing-wan.py65
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py6
-rwxr-xr-xsrc/conf_mode/service_ids_fastnetmon.py58
-rwxr-xr-xsrc/conf_mode/service_router-advert.py4
-rwxr-xr-xsrc/etc/opennhrp/opennhrp-script.py66
-rw-r--r--src/etc/systemd/system/fastnetmon.service.d/override.conf12
-rw-r--r--src/etc/systemd/system/wpa_supplicant@.service.d/override.conf3
-rwxr-xr-xsrc/op_mode/bridge.py202
-rwxr-xr-xsrc/op_mode/conntrack.py (renamed from src/op_mode/show_conntrack.py)43
-rwxr-xr-xsrc/op_mode/container.py85
-rwxr-xr-xsrc/op_mode/cpu.py82
-rwxr-xr-xsrc/op_mode/cpu_summary.py48
-rwxr-xr-xsrc/op_mode/dns.py95
-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.py201
-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.py126
-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.py95
-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.py76
-rwxr-xr-xsrc/services/vyos-http-api-server7
-rw-r--r--src/systemd/wpa_supplicant-macsec@.service9
50 files changed, 1764 insertions, 439 deletions
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-bridge.py b/src/conf_mode/interfaces-bridge.py
index cd0d9003b..b961408db 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -61,7 +61,7 @@ def get_config(config=None):
else:
bridge.update({'member' : {'interface_remove' : tmp }})
- if dict_search('member.interface', bridge):
+ if dict_search('member.interface', bridge) != None:
# XXX: T2665: we need a copy of the dict keys for iteration, else we will get:
# RuntimeError: dictionary changed size during iteration
for interface in list(bridge['member']['interface']):
@@ -103,6 +103,14 @@ def get_config(config=None):
if 'enable_vlan' in bridge and tmp:
bridge['member']['interface'][interface].update({'has_vlan' : ''})
+ # delete empty dictionary keys - no need to run code paths if nothing is there to do
+ if 'member' in bridge:
+ if 'interface' in bridge['member'] and len(bridge['member']['interface']) == 0:
+ del bridge['member']['interface']
+
+ if len(bridge['member']) == 0:
+ del bridge['member']
+
return bridge
def verify(bridge):
diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
index 03a010086..65b0612ea 100755
--- a/src/conf_mode/interfaces-macsec.py
+++ b/src/conf_mode/interfaces-macsec.py
@@ -56,7 +56,7 @@ def get_config(config=None):
# Check if interface has been removed
if 'deleted' in macsec:
- source_interface = conf.return_effective_value(['source-interface'])
+ source_interface = conf.return_effective_value(base + [ifname, 'source-interface'])
macsec.update({'source_interface': source_interface})
if is_node_changed(conf, base + [ifname, 'security']):
@@ -87,6 +87,16 @@ def verify(macsec):
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!')
+ cak_len = len(dict_search('security.mka.cak', macsec))
+
+ if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != 32:
+ # gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit
+ raise ConfigError('gcm-aes-128 requires a 128bit long key!')
+
+ elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != 64:
+ # gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit
+ raise ConfigError('gcm-aes-128 requires a 256bit long key!')
+
if 'source_interface' in macsec:
# MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad
# and 802.1q) - we need to check the underlaying MTU if our configured
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index bf0f6840d..af2d0588d 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -118,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} '\
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/load-balancing-wan.py b/src/conf_mode/load-balancing-wan.py
new file mode 100755
index 000000000..11840249f
--- /dev/null
+++ b/src/conf_mode/load-balancing-wan.py
@@ -0,0 +1,65 @@
+#!/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/>.
+
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import node_changed
+from vyos.util import call
+from vyos import ConfigError
+from pprint import pprint
+from vyos import airbag
+airbag.enable()
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['load-balancing', 'wan']
+ lb = conf.get_config_dict(base, get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ pprint(lb)
+ return lb
+
+def verify(lb):
+ return None
+
+
+def generate(lb):
+ if not lb:
+ return None
+
+ return None
+
+
+def apply(lb):
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 5aa643476..7d3687094 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -213,6 +213,12 @@ def verify(bgp):
if 'source_interface' in peer_config['interface']:
raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"')
+ # 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',
'l2vpn_evpn']:
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/service_router-advert.py b/src/conf_mode/service_router-advert.py
index ff7caaa84..1b8377a4a 100755
--- a/src/conf_mode/service_router-advert.py
+++ b/src/conf_mode/service_router-advert.py
@@ -90,8 +90,8 @@ def verify(rtradv):
if preferred_lifetime == 'infinity':
preferred_lifetime = 4294967295
- if not (int(valid_lifetime) > int(preferred_lifetime)):
- raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime')
+ if not (int(valid_lifetime) >= int(preferred_lifetime)):
+ raise ConfigError('Prefix valid-lifetime must be greater then or equal to preferred-lifetime')
if 'name_server_lifetime' in interface_config:
# man page states:
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/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/wpa_supplicant@.service.d/override.conf b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf
index a895e675f..5cffb7987 100644
--- a/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf
+++ b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf
@@ -7,4 +7,5 @@ WorkingDirectory=
WorkingDirectory=/run/wpa_supplicant
PIDFile=/run/wpa_supplicant/%I.pid
ExecStart=
-ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dnl80211,wext -i%I
+ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dnl80211,wext -P/run/wpa_supplicant/%I.pid -i%I
+ExecReload=/bin/kill -HUP $MAINPID
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/show_conntrack.py b/src/op_mode/conntrack.py
index 089a3e454..1441d110f 100755
--- a/src/op_mode/show_conntrack.py
+++ b/src/op_mode/conntrack.py
@@ -14,17 +14,21 @@
# 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_raw_data():
+
+def _get_xml_data(family):
"""
Get conntrack XML output
"""
- return cmd(f'sudo conntrack --dump --output xml')
+ return cmd(f'sudo conntrack --dump --family {family} --output xml')
def _xml_to_dict(xml):
@@ -32,26 +36,34 @@ def _xml_to_dict(xml):
Convert XML to dictionary
Return: dictionary
"""
- parse = xmltodict.parse(xml)
+ 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_formatted_output(xml):
+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 = _xml_to_dict(xml)
+ #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']
+ direction = meta['direction']
if direction in ['original']:
if 'layer3' in meta:
orig_src = meta['layer3']['src']
@@ -61,7 +73,7 @@ def _get_formatted_output(xml):
orig_sport = meta['layer4']['sport']
if meta.get('layer4').get('dport'):
orig_dport = meta['layer4']['dport']
- proto = meta['layer4']['@protoname']
+ proto = meta['layer4']['protoname']
if direction in ['reply']:
if 'layer3' in meta:
reply_src = meta['layer3']['src']
@@ -71,7 +83,7 @@ def _get_formatted_output(xml):
reply_sport = meta['layer4']['sport']
if meta.get('layer4').get('dport'):
reply_dport = meta['layer4']['dport']
- proto = meta['layer4']['@protoname']
+ proto = meta['layer4']['protoname']
if direction == 'independent':
conn_id = meta['id']
timeout = meta['timeout']
@@ -90,13 +102,20 @@ def _get_formatted_output(xml):
return output
-def show(raw: bool):
- conntrack_data = _get_raw_data()
+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)
+ return get_formatted_output(conntrack_data)
if __name__ == '__main__':
- print(show(raw=False))
+ 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/container.py b/src/op_mode/container.py
new file mode 100755
index 000000000..78d42f800
--- /dev/null
+++ b/src/op_mode/container.py
@@ -0,0 +1,85 @@
+#!/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
+
+from sys import exit
+
+from vyos.util import cmd
+
+import vyos.opmode
+
+
+def _get_json_data(command: str) -> list:
+ """
+ Get container command format JSON
+ """
+ return cmd(f'{command} --format json')
+
+
+def _get_raw_data(command: str) -> list:
+ json_data = _get_json_data(command)
+ data = json.loads(json_data)
+ return data
+
+
+def show_container(raw: bool):
+ command = 'sudo podman ps --all'
+ container_data = _get_raw_data(command)
+ if raw:
+ return container_data
+ else:
+ return cmd(command)
+
+
+def show_image(raw: bool):
+ command = 'sudo podman image ls'
+ container_data = _get_raw_data('sudo podman image ls')
+ if raw:
+ return container_data
+ else:
+ return cmd(command)
+
+
+def show_network(raw: bool):
+ command = 'sudo podman network ls'
+ container_data = _get_raw_data(command)
+ if raw:
+ return container_data
+ else:
+ return cmd(command)
+
+
+def restart(name: str):
+ from vyos.util import rc_cmd
+
+ rc, output = rc_cmd(f'sudo podman restart {name}')
+ if rc != 0:
+ print(output)
+ return None
+ print(f'Container name "{name}" restarted!')
+ 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/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/dns.py b/src/op_mode/dns.py
new file mode 100755
index 000000000..717652b9b
--- /dev/null
+++ b/src/op_mode/dns.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import sys
+
+from sys import exit
+from tabulate import tabulate
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import cmd
+
+import vyos.opmode
+
+
+def _data_to_dict(data, sep="\t") -> dict:
+ """
+ Return dictionary from plain text
+ separated by tab
+
+ cache-entries 73
+ cache-hits 0
+ uptime 2148
+ user-msec 172
+
+ {
+ 'cache-entries': '73',
+ 'cache-hits': '0',
+ 'uptime': '2148',
+ 'user-msec': '172'
+ }
+ """
+ dictionary = {}
+ mylist = [line for line in data.split('\n')]
+
+ for line in mylist:
+ if sep in line:
+ key, value = line.split(sep)
+ dictionary[key] = value
+ return dictionary
+
+
+def _get_raw_forwarding_statistics() -> dict:
+ command = cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get-all')
+ data = _data_to_dict(command)
+ data['cache-size'] = "{0:.2f}".format( int(
+ cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 )
+ return data
+
+
+def _get_formatted_forwarding_statistics(data):
+ cache_entries = data.get('cache-entries')
+ max_cache_entries = data.get('max-cache-entries')
+ cache_size = data.get('cache-size')
+ data_entries = [[cache_entries, max_cache_entries, f'{cache_size} kbytes']]
+ headers = ["Cache entries", "Max cache entries" , "Cache size"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+
+def show_forwarding_statistics(raw: bool):
+
+ config = ConfigTreeQuery()
+ if not config.exists('service dns forwarding'):
+ print("DNS forwarding is not configured")
+ exit(0)
+
+ dns_data = _get_raw_forwarding_statistics()
+ if raw:
+ return dns_data
+ else:
+ return _get_formatted_forwarding_statistics(dns_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/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..a98fc4227
--- /dev/null
+++ b/src/op_mode/nat.py
@@ -0,0 +1,201 @@
+#!/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}'''
+ elif 'dnat' in expr:
+ translation = dict_search('dnat.addr', expr)
+ if expr['dnat'] and 'port' in expr['dnat']:
+ if jmespath.search('dnat.port.range', expr):
+ port = dict_search('dnat.port.range', expr)
+ port = '-'.join(map(str, port))
+ else:
+ port = expr['dnat']['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
deleted file mode 100755
index 60a4bdd13..000000000
--- a/src/op_mode/show_nat_rules.py
+++ /dev/null
@@ -1,126 +0,0 @@
-#!/usr/bin/env python3
-#
-# 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
-# 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
-
-from argparse import ArgumentParser
-from jinja2 import Template
-from sys import exit
-from vyos.util import cmd
-from vyos.util import dict_search
-
-parser = ArgumentParser()
-group = parser.add_mutually_exclusive_group()
-group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true")
-group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true")
-args = parser.parse_args()
-
-if args.source or args.destination:
- tmp = cmd('sudo nft -j list table ip nat')
- tmp = json.loads(tmp)
-
- format_nat_rule = '{0: <10} {1: <50} {2: <50} {3: <10}'
- print(format_nat_rule.format("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface"))
- print(format_nat_rule.format("----", "------" if args.source else "-----------", "-----------", "------------------" if args.source else "-----------------"))
-
- data_json = jmespath.search('nftables[?rule].rule[?chain]', tmp)
- for idx in range(0, len(data_json)):
- data = data_json[idx]
-
- # The following key values must exist
- # When the rule JSON does not have some keys, this is not a rule we can work with
- continue_rule = False
- for key in ['comment', 'chain', 'expr']:
- if key not in data:
- continue_rule = True
- continue
- if continue_rule:
- continue
-
- comment = data['comment']
-
- # Check the annotation to see if the annotation format is created by VYOS
- continue_rule = True
- for comment_prefix in ['SRC-NAT-', 'DST-NAT-']:
- if comment_prefix in comment:
- continue_rule = False
- if continue_rule:
- continue
-
- rule = int(''.join(list(filter(str.isdigit, comment))))
- chain = data['chain']
- if not ((args.source and chain == 'POSTROUTING') or (not args.source and chain == 'PREROUTING')):
- continue
- interface = dict_search('match.right', data['expr'][0])
- srcdest = ''
- srcdests = []
- tran_addr = ''
- for i in range(1,len(data['expr']) ):
- srcdest_json = dict_search('match.right', data['expr'][i])
- if srcdest_json:
- if isinstance(srcdest_json,str):
- if srcdest != '':
- srcdests.append(srcdest)
- srcdest = ''
- srcdest = srcdest_json + ' '
- elif 'prefix' in srcdest_json:
- addr_tmp = dict_search('match.right.prefix.addr', data['expr'][i])
- len_tmp = dict_search('match.right.prefix.len', data['expr'][i])
- if addr_tmp and len_tmp:
- srcdest = addr_tmp + '/' + str(len_tmp) + ' '
- elif 'set' in srcdest_json:
- if isinstance(srcdest_json['set'][0],int):
- srcdest += 'port ' + str(srcdest_json['set'][0]) + ' '
- else:
- port_range = srcdest_json['set'][0]['range']
- srcdest += 'port ' + str(port_range[0]) + '-' + str(port_range[1]) + ' '
-
- tran_addr_json = dict_search('snat' if args.source else 'dnat', data['expr'][i])
- if tran_addr_json:
- if isinstance(tran_addr_json['addr'],str):
- tran_addr += tran_addr_json['addr'] + ' '
- elif 'prefix' in tran_addr_json['addr']:
- addr_tmp = dict_search('snat.addr.prefix.addr' if args.source else 'dnat.addr.prefix.addr', data['expr'][3])
- len_tmp = dict_search('snat.addr.prefix.len' if args.source else 'dnat.addr.prefix.len', data['expr'][3])
- if addr_tmp and len_tmp:
- tran_addr += addr_tmp + '/' + str(len_tmp) + ' '
-
- 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]:
- tran_addr = 'masquerade'
- elif 'log' in data['expr'][i]:
- continue
-
- 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)):
- print(format_nat_rule.format(' ', srcdests[i], ' ', ' '))
-
- exit(0)
-else:
- parser.print_help()
- exit(1)
-
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..e3d944d90
--- /dev/null
+++ b/src/op_mode/vrf.py
@@ -0,0 +1,95 @@
+#!/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 jmespath
+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 not jmespath.search('[*].ifname', vrf_data):
+ return "VRF is not configured"
+ 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..ac185beb7 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 = json.loads(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, scriptname) = split_compound_op_mode_name(name, op_mode_list)
+ if scriptname == '':
+ raise FileNotFoundError(f"No op-mode file named in string '{name}'")
+
+ mod = load_op_mode_as_module(f'{scriptname}')
+ 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, scriptname) = split_compound_op_mode_name(name, op_mode_list)
+ if scriptname == '':
+ raise FileNotFoundError(f"No op-mode file named in string '{name}'")
+
+ mod = load_op_mode_as_module(f'{scriptname}')
+ 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..d27586747
--- /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].replace('-', '_')
+ 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..073126853
--- /dev/null
+++ b/src/services/api/graphql/utils/util.py
@@ -0,0 +1,76 @@
+# 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].replace('-', '_')
+ 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_split(delim: str, n: int, s: str):
+ groups = s.split(delim)
+ l = len(groups)
+ if n > l-1 or n < 1:
+ return (s, '')
+ return (delim.join(groups[:n]), delim.join(groups[n:]))
+
+def _nth_rsplit(delim: str, n: int, s: str):
+ groups = s.split(delim)
+ l = len(groups)
+ if n > l-1 or n < 1:
+ return (s, '')
+ return (delim.join(groups[:l-n]), delim.join(groups[l-n:]))
+
+# Since we have mangled possible hyphens in the file name while constructing
+# the snake case of the query/mutation name, we will need to recover the
+# file name by searching with mangling:
+def _filter_on_mangled(test):
+ def func(elem):
+ mangle = os.path.splitext(elem)[0].replace('-', '_')
+ return test == mangle
+ return func
+
+# Find longest name in concatenated string that matches the basename of an
+# op-mode script. Should one prefer to concatenate in the reverse order
+# (script_name + '_' + function_name), use _nth_rsplit.
+def split_compound_op_mode_name(name: str, files: list):
+ for i in range(1, name.count('_') + 1):
+ pair = _nth_split('_', i, name)
+ f = list(filter(_filter_on_mangled(pair[1]), files))
+ if f:
+ pair = (pair[0], f[0])
+ 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..ffb4fe32c 100644
--- a/src/systemd/wpa_supplicant-macsec@.service
+++ b/src/systemd/wpa_supplicant-macsec@.service
@@ -1,17 +1,18 @@
[Unit]
-Description=WPA supplicant daemon (macsec-specific version)
+Description=WPA supplicant daemon (MACsec-specific version)
Requires=sys-subsystem-net-devices-%i.device
ConditionPathExists=/run/wpa_supplicant/%I.conf
After=vyos-router.service
RequiresMountsFor=/run
-# NetworkManager users will probably want the dbus version instead.
-
[Service]
Type=simple
WorkingDirectory=/run/wpa_supplicant
PIDFile=/run/wpa_supplicant/%I.pid
-ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -i%I
+ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -P/run/wpa_supplicant/%I.pid -i%I
+ExecReload=/bin/kill -HUP $MAINPID
+Restart=always
+RestartSec=2
[Install]
WantedBy=multi-user.target