summaryrefslogtreecommitdiff
path: root/src/op_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/op_mode')
-rwxr-xr-xsrc/op_mode/accelppp.py133
-rwxr-xr-xsrc/op_mode/bgp.py120
-rwxr-xr-xsrc/op_mode/bridge.py206
-rwxr-xr-xsrc/op_mode/clear_dhcp_lease.py78
-rwxr-xr-xsrc/op_mode/connect_disconnect.py8
-rwxr-xr-xsrc/op_mode/conntrack.py153
-rwxr-xr-xsrc/op_mode/conntrack_sync.py4
-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/dhcp.py278
-rwxr-xr-xsrc/op_mode/dns.py95
-rwxr-xr-xsrc/op_mode/firewall.py56
-rwxr-xr-xsrc/op_mode/flow_accounting_op.py7
-rwxr-xr-xsrc/op_mode/generate_ipsec_debug_archive.py89
-rwxr-xr-xsrc/op_mode/generate_ipsec_debug_archive.sh36
-rwxr-xr-xsrc/op_mode/generate_ssh_server_key.py5
-rwxr-xr-xsrc/op_mode/ikev2_profile_generator.py8
-rwxr-xr-xsrc/op_mode/ipsec.py473
-rwxr-xr-xsrc/op_mode/log.py94
-rwxr-xr-xsrc/op_mode/memory.py (renamed from src/op_mode/show_ram.py)52
-rwxr-xr-xsrc/op_mode/nat.py334
-rwxr-xr-xsrc/op_mode/neighbor.py122
-rwxr-xr-xsrc/op_mode/openconnect-control.py4
-rwxr-xr-xsrc/op_mode/openconnect.py73
-rwxr-xr-xsrc/op_mode/openvpn.py220
-rwxr-xr-xsrc/op_mode/ping.py83
-rwxr-xr-xsrc/op_mode/policy_route.py42
-rwxr-xr-xsrc/op_mode/reset_openvpn.py4
-rwxr-xr-xsrc/op_mode/restart_dhcp_relay.py11
-rwxr-xr-xsrc/op_mode/route.py115
-rwxr-xr-xsrc/op_mode/show_cpu.py72
-rwxr-xr-xsrc/op_mode/show_nat66_rules.py102
-rwxr-xr-xsrc/op_mode/show_nat66_statistics.py2
-rwxr-xr-xsrc/op_mode/show_nat_rules.py123
-rwxr-xr-xsrc/op_mode/show_nat_statistics.py2
-rwxr-xr-xsrc/op_mode/show_nat_translations.py16
-rwxr-xr-xsrc/op_mode/show_neigh.py96
-rwxr-xr-xsrc/op_mode/show_openvpn.py6
-rwxr-xr-xsrc/op_mode/show_vrf.py66
-rwxr-xr-xsrc/op_mode/storage.py78
-rwxr-xr-xsrc/op_mode/system.py92
-rwxr-xr-xsrc/op_mode/traceroute.py85
-rwxr-xr-xsrc/op_mode/uptime.py (renamed from src/op_mode/show_uptime.py)36
-rwxr-xr-xsrc/op_mode/version.py (renamed from src/op_mode/show_version.py)52
-rwxr-xr-xsrc/op_mode/vpn_ike_sa.py2
-rwxr-xr-xsrc/op_mode/vpn_ipsec.py5
-rwxr-xr-xsrc/op_mode/vrf.py95
-rwxr-xr-xsrc/op_mode/vtysh_wrapper.sh5
-rwxr-xr-xsrc/op_mode/webproxy_update_blacklist.sh2
50 files changed, 3332 insertions, 723 deletions
diff --git a/src/op_mode/accelppp.py b/src/op_mode/accelppp.py
new file mode 100755
index 000000000..2fd045dc3
--- /dev/null
+++ b/src/op_mode/accelppp.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import sys
+
+import vyos.accel_ppp
+import vyos.opmode
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import rc_cmd
+
+
+accel_dict = {
+ 'ipoe': {
+ 'port': 2002,
+ 'path': 'service ipoe-server'
+ },
+ 'pppoe': {
+ 'port': 2001,
+ 'path': 'service pppoe-server'
+ },
+ 'pptp': {
+ 'port': 2003,
+ 'path': 'vpn pptp'
+ },
+ 'l2tp': {
+ 'port': 2004,
+ 'path': 'vpn l2tp'
+ },
+ 'sstp': {
+ 'port': 2005,
+ 'path': 'vpn sstp'
+ }
+}
+
+
+def _get_raw_statistics(accel_output, pattern):
+ return vyos.accel_ppp.get_server_statistics(accel_output, pattern, sep=':')
+
+
+def _get_raw_sessions(port):
+ cmd_options = 'show sessions ifname,username,ip,ip6,ip6-dp,type,state,' \
+ 'uptime-raw,calling-sid,called-sid,sid,comp,rx-bytes-raw,' \
+ 'tx-bytes-raw,rx-pkts,tx-pkts'
+ output = vyos.accel_ppp.accel_cmd(port, cmd_options)
+ parsed_data: list[dict[str, str]] = vyos.accel_ppp.accel_out_parse(
+ output.splitlines())
+ return parsed_data
+
+
+def _verify(func):
+ """Decorator checks if accel-ppp protocol
+ ipoe/pppoe/pptp/l2tp/sstp is configured
+
+ for example:
+ service ipoe-server
+ vpn sstp
+ """
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ protocol_list = accel_dict.keys()
+ protocol = kwargs.get('protocol')
+ # unknown or incorrect protocol query
+ if protocol not in protocol_list:
+ unconf_message = f'unknown protocol "{protocol}"'
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ # Check if config does not exist
+ config_protocol_path = accel_dict[protocol]['path']
+ if not config.exists(config_protocol_path):
+ unconf_message = f'"{config_protocol_path}" is not configured'
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+@_verify
+def show_statistics(raw: bool, protocol: str):
+ """show accel-cmd statistics
+ CPU utilization and amount of sessions
+
+ protocol: ipoe/pppoe/ppptp/l2tp/sstp
+ """
+ pattern = f'{protocol}:'
+ port = accel_dict[protocol]['port']
+ rc, output = rc_cmd(f'/usr/bin/accel-cmd -p {port} show stat')
+
+ if raw:
+ return _get_raw_statistics(output, pattern)
+
+ return output
+
+
+@_verify
+def show_sessions(raw: bool, protocol: str):
+ """show accel-cmd sessions
+
+ protocol: ipoe/pppoe/ppptp/l2tp/sstp
+ """
+ port = accel_dict[protocol]['port']
+ if raw:
+ return _get_raw_sessions(port)
+
+ return vyos.accel_ppp.accel_cmd(port,
+ 'show sessions ifname,username,ip,ip6,ip6-dp,'
+ 'calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes')
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/bgp.py b/src/op_mode/bgp.py
new file mode 100755
index 000000000..23001a9d7
--- /dev/null
+++ b/src/op_mode/bgp.py
@@ -0,0 +1,120 @@
+#!/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 bgp neighbors information.
+# Used by the "show bgp (vrf <tag>) ipv4|ipv6 neighbors" commands.
+
+import re
+import sys
+import typing
+
+import jmespath
+from jinja2 import Template
+from humps import decamelize
+
+from vyos.configquery import ConfigTreeQuery
+
+import vyos.opmode
+
+
+frr_command_template = Template("""
+{% if family %}
+ show bgp
+ {{ 'vrf ' ~ vrf if vrf else '' }}
+ {{ 'ipv6' if family == 'inet6' else 'ipv4'}}
+ {{ 'neighbor ' ~ peer if peer else 'summary' }}
+{% endif %}
+
+{% if raw %}
+ json
+{% endif %}
+""")
+
+
+def _verify(func):
+ """Decorator checks if BGP config exists
+ BGP configuration can be present under vrf <tag>
+ If we do npt get arg 'peer' then it can be 'bgp summary'
+ """
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ afi = 'ipv6' if kwargs.get('family') == 'inet6' else 'ipv4'
+ global_vrfs = ['all', 'default']
+ peer = kwargs.get('peer')
+ vrf = kwargs.get('vrf')
+ unconf_message = f'BGP or neighbor is not configured'
+ # Add option to check the specific neighbor if we have arg 'peer'
+ peer_opt = f'neighbor {peer} address-family {afi}-unicast' if peer else ''
+ vrf_opt = ''
+ if vrf and vrf not in global_vrfs:
+ vrf_opt = f'vrf name {vrf}'
+ # Check if config does not exist
+ if not config.exists(f'{vrf_opt} protocols bgp {peer_opt}'):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+@_verify
+def show_neighbors(raw: bool,
+ family: str,
+ peer: typing.Optional[str],
+ vrf: typing.Optional[str]):
+ 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
+ data = loads(output)
+ # Get list of the peers
+ peers = jmespath.search('*.peers | [0]', data)
+ if peers:
+ # Create new dict, delete old key 'peers'
+ # add key 'peers' neighbors to the list
+ list_peers = []
+ new_dict = jmespath.search('* | [0]', data)
+ if 'peers' in new_dict:
+ new_dict.pop('peers')
+
+ for neighbor, neighbor_options in peers.items():
+ neighbor_options['neighbor'] = neighbor
+ list_peers.append(neighbor_options)
+ new_dict['peers'] = list_peers
+ return decamelize(new_dict)
+ data = jmespath.search('* | [0]', data)
+ return decamelize(data)
+
+ else:
+ return output
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py
new file mode 100755
index 000000000..d6098c158
--- /dev/null
+++ b/src/op_mode/bridge.py
@@ -0,0 +1,206 @@
+#!/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, rc_cmd
+from vyos.util import dict_search
+
+import vyos.opmode
+
+
+def _get_json_data():
+ """
+ Get bridge data format JSON
+ """
+ return cmd(f'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('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
+ """
+ code, json_data = rc_cmd(f'bridge --json fdb show br {bridge}')
+ # From iproute2 fdb.c, fdb_show() will only exit(-1) in case of
+ # non-existent bridge device; raise error.
+ if code == 255:
+ raise vyos.opmode.UnconfiguredSubsystem(f"no such bridge device {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, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py
new file mode 100755
index 000000000..250dbcce1
--- /dev/null
+++ b/src/op_mode/clear_dhcp_lease.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+
+import argparse
+import re
+
+from isc_dhcp_leases import Lease
+from isc_dhcp_leases import IscDhcpLeases
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import ask_yes_no
+from vyos.util import call
+from vyos.util import commit_in_progress
+
+
+config = ConfigTreeQuery()
+base = ['service', 'dhcp-server']
+lease_file = '/config/dhcpd.leases'
+
+
+def del_lease_ip(address):
+ """
+ Read lease_file and write data to this file
+ without specific section "lease ip"
+ Delete section "lease x.x.x.x { x;x;x; }"
+ """
+ with open(lease_file, encoding='utf-8') as f:
+ data = f.read().rstrip()
+ lease_config_ip = '{(?P<config>[\s\S]+?)\n}'
+ pattern = rf"lease {address} {lease_config_ip}"
+ # Delete lease for ip block
+ data = re.sub(pattern, '', data)
+
+ # Write new data to original lease_file
+ with open(lease_file, 'w', encoding='utf-8') as f:
+ f.write(data)
+
+def is_ip_in_leases(address):
+ """
+ Return True if address found in the lease file
+ """
+ leases = IscDhcpLeases(lease_file)
+ lease_ips = []
+ for lease in leases.get():
+ lease_ips.append(lease.ip)
+ if address not in lease_ips:
+ print(f'Address "{address}" not found in "{lease_file}"')
+ return False
+ return True
+
+
+if not config.exists(base):
+ print('DHCP-server not configured!')
+ exit(0)
+
+if config.exists(base + ['failover']):
+ print('Lease cannot be reset in failover mode!')
+ exit(0)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--ip', help='IPv4 address', action='store', required=True)
+
+ args = parser.parse_args()
+ address = args.ip
+
+ if not is_ip_in_leases(address):
+ exit(1)
+
+ if commit_in_progress():
+ print('Cannot clear DHCP lease while a commit is in progress')
+ exit(1)
+
+ if not ask_yes_no(f'This will restart DHCP server.\nContinue?'):
+ exit(1)
+ else:
+ del_lease_ip(address)
+ call('systemctl restart isc-dhcp-server.service')
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py
index ffc574362..d39e88bf3 100755
--- a/src/op_mode/connect_disconnect.py
+++ b/src/op_mode/connect_disconnect.py
@@ -20,6 +20,7 @@ import argparse
from psutil import process_iter
from vyos.util import call
+from vyos.util import commit_in_progress
from vyos.util import DEVNULL
from vyos.util import is_wwan_connected
@@ -40,7 +41,7 @@ def check_ppp_running(interface):
def connect(interface):
""" Connect dialer interface """
- if interface.startswith('ppp'):
+ if interface.startswith('pppoe') or interface.startswith('sstpc'):
check_ppp_interface(interface)
# Check if interface is already dialed
if os.path.isdir(f'/sys/class/net/{interface}'):
@@ -61,7 +62,7 @@ def connect(interface):
def disconnect(interface):
""" Disconnect dialer interface """
- if interface.startswith('ppp'):
+ if interface.startswith('pppoe') or interface.startswith('sstpc'):
check_ppp_interface(interface)
# Check if interface is already down
@@ -87,6 +88,9 @@ def main():
args = parser.parse_args()
if args.connect:
+ if commit_in_progress():
+ print('Cannot connect while a commit is in progress')
+ exit(1)
connect(args.connect)
elif args.disconnect:
disconnect(args.disconnect)
diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py
new file mode 100755
index 000000000..fff537936
--- /dev/null
+++ b/src/op_mode/conntrack.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import xmltodict
+
+from tabulate import tabulate
+from vyos.util import cmd
+from vyos.util import run
+
+import vyos.opmode
+
+
+def _get_xml_data(family):
+ """
+ Get conntrack XML output
+ """
+ return cmd(f'sudo conntrack --dump --family {family} --output xml')
+
+
+def _xml_to_dict(xml):
+ """
+ Convert XML to dictionary
+ Return: dictionary
+ """
+ parse = xmltodict.parse(xml, attr_prefix='')
+ # If only one conntrack entry we must change dict
+ if 'meta' in parse['conntrack']['flow']:
+ return dict(conntrack={'flow': [parse['conntrack']['flow']]})
+ return parse
+
+
+def _get_raw_data(family):
+ """
+ Return: dictionary
+ """
+ xml = _get_xml_data(family)
+ if len(xml) == 0:
+ output = {'conntrack':
+ {
+ 'error': True,
+ 'reason': 'entries not found'
+ }
+ }
+ return output
+ return _xml_to_dict(xml)
+
+
+def _get_raw_statistics():
+ entries = []
+ data = cmd('sudo conntrack -S')
+ data = data.replace(' \t', '').split('\n')
+ for entry in data:
+ entries.append(entry.split())
+ return entries
+
+
+def get_formatted_statistics(entries):
+ headers = ["CPU", "Found", "Invalid", "Insert", "Insert fail", "Drop", "Early drop", "Errors", "Search restart"]
+ output = tabulate(entries, headers, numalign="left")
+ return output
+
+
+def get_formatted_output(dict_data):
+ """
+ :param xml:
+ :return: formatted output
+ """
+ data_entries = []
+ if 'error' in dict_data['conntrack']:
+ return 'Entries not found'
+ for entry in dict_data['conntrack']['flow']:
+ orig_src, orig_dst, orig_sport, orig_dport = {}, {}, {}, {}
+ reply_src, reply_dst, reply_sport, reply_dport = {}, {}, {}, {}
+ proto = {}
+ for meta in entry['meta']:
+ direction = meta['direction']
+ if direction in ['original']:
+ if 'layer3' in meta:
+ orig_src = meta['layer3']['src']
+ orig_dst = meta['layer3']['dst']
+ if 'layer4' in meta:
+ if meta.get('layer4').get('sport'):
+ orig_sport = meta['layer4']['sport']
+ if meta.get('layer4').get('dport'):
+ orig_dport = meta['layer4']['dport']
+ proto = meta['layer4']['protoname']
+ if direction in ['reply']:
+ if 'layer3' in meta:
+ reply_src = meta['layer3']['src']
+ reply_dst = meta['layer3']['dst']
+ if 'layer4' in meta:
+ if meta.get('layer4').get('sport'):
+ reply_sport = meta['layer4']['sport']
+ if meta.get('layer4').get('dport'):
+ reply_dport = meta['layer4']['dport']
+ proto = meta['layer4']['protoname']
+ if direction == 'independent':
+ conn_id = meta['id']
+ timeout = meta['timeout']
+ orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src
+ orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst
+ reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src
+ reply_dst = f'{reply_dst}:{reply_dport}' if reply_dport else reply_dst
+ state = meta['state'] if 'state' in meta else ''
+ mark = meta['mark']
+ zone = meta['zone'] if 'zone' in meta else ''
+ data_entries.append(
+ [conn_id, orig_src, orig_dst, reply_src, reply_dst, proto, state, timeout, mark, zone])
+ headers = ["Id", "Original src", "Original dst", "Reply src", "Reply dst", "Protocol", "State", "Timeout", "Mark",
+ "Zone"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+
+def show(raw: bool, family: str):
+ family = 'ipv6' if family == 'inet6' else 'ipv4'
+ conntrack_data = _get_raw_data(family)
+ if raw:
+ return conntrack_data
+ else:
+ return get_formatted_output(conntrack_data)
+
+
+def show_statistics(raw: bool):
+ conntrack_statistics = _get_raw_statistics()
+ if raw:
+ return conntrack_statistics
+ else:
+ return get_formatted_statistics(conntrack_statistics)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py
index e45c38f07..54ecd6d0e 100755
--- a/src/op_mode/conntrack_sync.py
+++ b/src/op_mode/conntrack_sync.py
@@ -22,6 +22,7 @@ from argparse import ArgumentParser
from vyos.configquery import CliShellApiConfigQuery
from vyos.configquery import ConfigTreeQuery
from vyos.util import call
+from vyos.util import commit_in_progress
from vyos.util import cmd
from vyos.util import run
from vyos.template import render_to_string
@@ -86,6 +87,9 @@ if __name__ == '__main__':
if args.restart:
is_configured()
+ if commit_in_progress():
+ print('Cannot restart conntrackd while a commit is in progress')
+ exit(1)
syslog.syslog('Restarting conntrack sync service...')
cmd('systemctl restart conntrackd.service')
diff --git a/src/op_mode/container.py b/src/op_mode/container.py
new file mode 100755
index 000000000..ce466ffc1
--- /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, vyos.opmode.Error) 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..d53663c17
--- /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, vyos.opmode.Error) 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/dhcp.py b/src/op_mode/dhcp.py
new file mode 100755
index 000000000..07e9b7d6c
--- /dev/null
+++ b/src/op_mode/dhcp.py
@@ -0,0 +1,278 @@
+#!/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 ipaddress import ip_address
+import typing
+
+from datetime import datetime
+from sys import exit
+from tabulate import tabulate
+from isc_dhcp_leases import IscDhcpLeases
+
+from vyos.base import Warning
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.util import cmd
+from vyos.util import dict_search
+from vyos.util import is_systemd_service_running
+
+import vyos.opmode
+
+
+config = ConfigTreeQuery()
+pool_key = "shared-networkname"
+
+
+def _in_pool(lease, pool):
+ if pool_key in lease.sets:
+ if lease.sets[pool_key] == pool:
+ return True
+ return False
+
+
+def _utc_to_local(utc_dt):
+ return datetime.fromtimestamp((datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds())
+
+
+def _format_hex_string(in_str):
+ out_str = ""
+ # if input is divisible by 2, add : every 2 chars
+ if len(in_str) > 0 and len(in_str) % 2 == 0:
+ out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2]))
+ else:
+ out_str = in_str
+
+ return out_str
+
+
+def _find_list_of_dict_index(lst, key='ip', value='') -> int:
+ """
+ Find the index entry of list of dict matching the dict value
+ Exampe:
+ % lst = [{'ip': '192.0.2.1'}, {'ip': '192.0.2.2'}]
+ % _find_list_of_dict_index(lst, key='ip', value='192.0.2.2')
+ % 1
+ """
+ idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None)
+ return idx
+
+
+def _get_raw_server_leases(family, pool=None) -> list:
+ """
+ Get DHCP server leases
+ :return list
+ """
+ lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases'
+ data = []
+ leases = IscDhcpLeases(lease_file).get()
+ if pool is not None:
+ if config.exists(f'service dhcp-server shared-network-name {pool}'):
+ leases = list(filter(lambda x: _in_pool(x, pool), leases))
+ for lease in leases:
+ data_lease = {}
+ data_lease['ip'] = lease.ip
+ data_lease['state'] = lease.binding_state
+ data_lease['pool'] = lease.sets.get('shared-networkname', '')
+ data_lease['end'] = lease.end.timestamp()
+
+ if family == 'inet':
+ data_lease['hardware'] = lease.ethernet
+ data_lease['start'] = lease.start.timestamp()
+ data_lease['hostname'] = lease.hostname
+
+ if family == 'inet6':
+ data_lease['last_communication'] = lease.last_communication.timestamp()
+ data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string)
+ lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'}
+ data_lease['type'] = lease_types_long[lease.type]
+
+ data_lease['remaining'] = lease.end - datetime.utcnow()
+
+ if data_lease['remaining'].days >= 0:
+ # substraction gives us a timedelta object which can't be formatted with strftime
+ # so we use str(), split gets rid of the microseconds
+ data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0]
+ else:
+ data_lease['remaining'] = ''
+
+ # Do not add old leases
+ if data_lease['remaining'] != '':
+ data.append(data_lease)
+
+ # deduplicate
+ checked = []
+ for entry in data:
+ addr = entry.get('ip')
+ if addr not in checked:
+ checked.append(addr)
+ else:
+ idx = _find_list_of_dict_index(data, key='ip', value=addr)
+ data.pop(idx)
+
+ return data
+
+
+def _get_formatted_server_leases(raw_data, family):
+ data_entries = []
+ if family == 'inet':
+ for lease in raw_data:
+ ipaddr = lease.get('ip')
+ hw_addr = lease.get('hardware')
+ state = lease.get('state')
+ start = lease.get('start')
+ start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
+ end = lease.get('end')
+ end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S')
+ remain = lease.get('remaining')
+ pool = lease.get('pool')
+ hostname = lease.get('hostname')
+ data_entries.append([ipaddr, hw_addr, state, start, end, remain, pool, hostname])
+
+ headers = ['IP Address', 'Hardware address', 'State', 'Lease start', 'Lease expiration', 'Remaining', 'Pool',
+ 'Hostname']
+
+ if family == 'inet6':
+ for lease in raw_data:
+ ipaddr = lease.get('ip')
+ state = lease.get('state')
+ start = lease.get('last_communication')
+ start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
+ end = lease.get('end')
+ end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S')
+ remain = lease.get('remaining')
+ lease_type = lease.get('type')
+ pool = lease.get('pool')
+ host_identifier = lease.get('iaid_duid')
+ data_entries.append([ipaddr, state, start, end, remain, lease_type, pool, host_identifier])
+
+ headers = ['IPv6 address', 'State', 'Last communication', 'Lease expiration', 'Remaining', 'Type', 'Pool',
+ 'IAID_DUID']
+
+ output = tabulate(data_entries, headers, numalign='left')
+ return output
+
+
+def _get_dhcp_pools(family='inet') -> list:
+ v = 'v6' if family == 'inet6' else ''
+ pools = config.list_nodes(f'service dhcp{v}-server shared-network-name')
+ return pools
+
+
+def _get_pool_size(pool, family='inet'):
+ v = 'v6' if family == 'inet6' else ''
+ base = f'service dhcp{v}-server shared-network-name {pool}'
+ size = 0
+ subnets = config.list_nodes(f'{base} subnet')
+ for subnet in subnets:
+ if family == 'inet6':
+ ranges = config.list_nodes(f'{base} subnet {subnet} address-range start')
+ else:
+ ranges = config.list_nodes(f'{base} subnet {subnet} range')
+ for range in ranges:
+ if family == 'inet6':
+ start = config.list_nodes(f'{base} subnet {subnet} address-range start')[0]
+ stop = config.value(f'{base} subnet {subnet} address-range start {start} stop')
+ else:
+ start = config.value(f'{base} subnet {subnet} range {range} start')
+ stop = config.value(f'{base} subnet {subnet} range {range} stop')
+ # Add +1 because both range boundaries are inclusive
+ size += int(ip_address(stop)) - int(ip_address(start)) + 1
+ return size
+
+
+def _get_raw_pool_statistics(family='inet', pool=None):
+ if pool is None:
+ pool = _get_dhcp_pools(family=family)
+ else:
+ pool = [pool]
+
+ v = 'v6' if family == 'inet6' else ''
+ stats = []
+ for p in pool:
+ subnet = config.list_nodes(f'service dhcp{v}-server shared-network-name {p} subnet')
+ size = _get_pool_size(family=family, pool=p)
+ leases = len(_get_raw_server_leases(family=family, pool=p))
+ use_percentage = round(leases / size * 100) if size != 0 else 0
+ pool_stats = {'pool': p, 'size': size, 'leases': leases,
+ 'available': (size - leases), 'use_percentage': use_percentage, 'subnet': subnet}
+ stats.append(pool_stats)
+ return stats
+
+
+def _get_formatted_pool_statistics(pool_data, family='inet'):
+ data_entries = []
+ for entry in pool_data:
+ pool = entry.get('pool')
+ size = entry.get('size')
+ leases = entry.get('leases')
+ available = entry.get('available')
+ use_percentage = entry.get('use_percentage')
+ use_percentage = f'{use_percentage}%'
+ data_entries.append([pool, size, leases, available, use_percentage])
+
+ headers = ['Pool', 'Size','Leases', 'Available', 'Usage']
+ output = tabulate(data_entries, headers, numalign='left')
+ return output
+
+
+def _verify(func):
+ """Decorator checks if DHCP(v6) config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ family = kwargs.get('family')
+ v = 'v6' if family == 'inet6' else ''
+ unconf_message = f'DHCP{v} server is not configured'
+ # Check if config does not exist
+ if not config.exists(f'service dhcp{v}-server'):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+ return _wrapper
+
+
+@_verify
+def show_pool_statistics(raw: bool, family: str, pool: typing.Optional[str]):
+ pool_data = _get_raw_pool_statistics(family=family, pool=pool)
+ if raw:
+ return pool_data
+ else:
+ return _get_formatted_pool_statistics(pool_data, family=family)
+
+
+@_verify
+def show_server_leases(raw: bool, family: str):
+ # if dhcp server is down, inactive leases may still be shown as active, so warn the user.
+ if not is_systemd_service_running('isc-dhcp-server.service'):
+ Warning('DHCP server is configured but not started. Data may be stale.')
+
+ leases = _get_raw_server_leases(family)
+ if raw:
+ return leases
+ else:
+ return _get_formatted_server_leases(leases, family)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py
new file mode 100755
index 000000000..a0e47d7ad
--- /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('rec_control --socket-dir=/run/powerdns get-all')
+ data = _data_to_dict(command)
+ data['cache-size'] = "{0:.2f}".format( int(
+ cmd('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, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index 3146fc357..46bda5f7e 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -24,43 +24,33 @@ from vyos.config import Config
from vyos.util import cmd
from vyos.util import dict_search_args
-def get_firewall_interfaces(conf, firewall, name=None, ipv6=False):
- interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
-
+def get_firewall_interfaces(firewall, name=None, ipv6=False):
directions = ['in', 'out', 'local']
- def parse_if(ifname, if_conf):
- if 'firewall' in if_conf:
+ if 'interface' in firewall:
+ for ifname, if_conf in firewall['interface'].items():
for direction in directions:
- if direction in if_conf['firewall']:
- fw_conf = if_conf['firewall'][direction]
- name_str = f'({ifname},{direction})'
-
- if 'name' in fw_conf:
- fw_name = fw_conf['name']
+ if direction not in if_conf:
+ continue
- if not name:
- firewall['name'][fw_name]['interface'].append(name_str)
- elif not ipv6 and name == fw_name:
- firewall['interface'].append(name_str)
+ fw_conf = if_conf[direction]
+ name_str = f'({ifname},{direction})'
- if 'ipv6_name' in fw_conf:
- fw_name = fw_conf['ipv6_name']
+ if 'name' in fw_conf:
+ fw_name = fw_conf['name']
- if not name:
- firewall['ipv6_name'][fw_name]['interface'].append(name_str)
- elif ipv6 and name == fw_name:
- firewall['interface'].append(name_str)
+ if not name:
+ firewall['name'][fw_name]['interface'].append(name_str)
+ elif not ipv6 and name == fw_name:
+ firewall['interface'].append(name_str)
- for iftype in ['vif', 'vif_s', 'vif_c']:
- if iftype in if_conf:
- for vifname, vif_conf in if_conf[iftype].items():
- parse_if(f'{ifname}.{vifname}', vif_conf)
+ if 'ipv6_name' in fw_conf:
+ fw_name = fw_conf['ipv6_name']
- for iftype, iftype_conf in interfaces.items():
- for ifname, if_conf in iftype_conf.items():
- parse_if(ifname, if_conf)
+ if not name:
+ firewall['ipv6_name'][fw_name]['interface'].append(name_str)
+ elif ipv6 and name == fw_name:
+ firewall['interface'].append(name_str)
return firewall
@@ -73,7 +63,7 @@ def get_config_firewall(conf, name=None, ipv6=False, interfaces=True):
get_first_key=True, no_tag_node_value_mangle=True)
if firewall and interfaces:
if name:
- firewall['interface'] = []
+ firewall['interface'] = {}
else:
if 'name' in firewall:
for fw_name, name_conf in firewall['name'].items():
@@ -83,13 +73,13 @@ def get_config_firewall(conf, name=None, ipv6=False, interfaces=True):
for fw_name, name_conf in firewall['ipv6_name'].items():
name_conf['interface'] = []
- get_firewall_interfaces(conf, firewall, name, ipv6)
+ get_firewall_interfaces(firewall, name, ipv6)
return firewall
def get_nftables_details(name, ipv6=False):
suffix = '6' if ipv6 else ''
name_prefix = 'NAME6_' if ipv6 else 'NAME_'
- command = f'sudo nft list chain ip{suffix} filter {name_prefix}{name}'
+ command = f'sudo nft list chain ip{suffix} vyos_filter {name_prefix}{name}'
try:
results = cmd(command)
except:
@@ -270,7 +260,7 @@ def show_firewall_group(name=None):
references = find_references(group_type, group_name)
row = [group_name, group_type, '\n'.join(references) or 'N/A']
if 'address' in group_conf:
- row.append("\n".join(sorted(group_conf['address'], key=ipaddress.ip_address)))
+ row.append("\n".join(sorted(group_conf['address'])))
elif 'network' in group_conf:
row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network)))
elif 'mac_address' in group_conf:
diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py
index 6586cbceb..514143cd7 100755
--- a/src/op_mode/flow_accounting_op.py
+++ b/src/op_mode/flow_accounting_op.py
@@ -22,7 +22,9 @@ import ipaddress
import os.path
from tabulate import tabulate
from json import loads
-from vyos.util import cmd, run
+from vyos.util import cmd
+from vyos.util import commit_in_progress
+from vyos.util import run
from vyos.logger import syslog
# some default values
@@ -224,6 +226,9 @@ if not _uacctd_running():
# restart pmacct daemon
if cmd_args.action == 'restart':
+ if commit_in_progress():
+ print('Cannot restart flow-accounting while a commit is in progress')
+ exit(1)
# run command to restart flow-accounting
cmd('systemctl restart uacctd.service',
message='Failed to restart flow-accounting')
diff --git a/src/op_mode/generate_ipsec_debug_archive.py b/src/op_mode/generate_ipsec_debug_archive.py
new file mode 100755
index 000000000..1422559a8
--- /dev/null
+++ b/src/op_mode/generate_ipsec_debug_archive.py
@@ -0,0 +1,89 @@
+#!/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 datetime import datetime
+from pathlib import Path
+from shutil import rmtree
+from socket import gethostname
+from sys import exit
+from tarfile import open as tar_open
+from vyos.util import rc_cmd
+
+# define a list of commands that needs to be executed
+CMD_LIST: list[str] = [
+ 'ipsec status',
+ 'swanctl -L',
+ 'swanctl -l',
+ 'swanctl -P',
+ 'ip x sa show',
+ 'ip x policy show',
+ 'ip tunnel show',
+ 'ip address',
+ 'ip rule show',
+ 'ip route | head -100',
+ 'ip route show table 220'
+]
+JOURNALCTL_CMD: str = 'journalctl -b -n 10000 /usr/lib/ipsec/charon'
+
+# execute a command and save the output to a file
+def save_stdout(command: str, file: Path) -> None:
+ rc, stdout = rc_cmd(command)
+ body: str = f'''### {command} ###
+Command: {command}
+Exit code: {rc}
+Stdout:
+{stdout}
+
+'''
+ with file.open(mode='a') as f:
+ f.write(body)
+
+
+# get local host name
+hostname: str = gethostname()
+# get current time
+time_now: str = datetime.now().isoformat(timespec='seconds')
+
+# define a temporary directory for logs and collected data
+tmp_dir: Path = Path(f'/tmp/ipsec_debug_{time_now}')
+# set file paths
+ipsec_status_file: Path = Path(f'{tmp_dir}/ipsec_status.txt')
+journalctl_charon_file: Path = Path(f'{tmp_dir}/journalctl_charon.txt')
+archive_file: str = f'/tmp/ipsec_debug_{time_now}.tar.bz2'
+
+# create files
+tmp_dir.mkdir()
+ipsec_status_file.touch()
+journalctl_charon_file.touch()
+
+try:
+ # execute all commands
+ for command in CMD_LIST:
+ save_stdout(command, ipsec_status_file)
+ save_stdout(JOURNALCTL_CMD, journalctl_charon_file)
+
+ # create an archive
+ with tar_open(name=archive_file, mode='x:bz2') as tar_file:
+ tar_file.add(tmp_dir)
+
+ # inform user about success
+ print(f'Debug file is generated and located in {archive_file}')
+except Exception as err:
+ print(f'Error during generating a debug file: {err}')
+finally:
+ # cleanup
+ rmtree(tmp_dir)
+ exit()
diff --git a/src/op_mode/generate_ipsec_debug_archive.sh b/src/op_mode/generate_ipsec_debug_archive.sh
deleted file mode 100755
index 53d0a6eaa..000000000
--- a/src/op_mode/generate_ipsec_debug_archive.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env bash
-
-# Collecting IPSec Debug Information
-
-DATE=`date +%d-%m-%Y`
-
-a_CMD=(
- "sudo ipsec status"
- "sudo swanctl -L"
- "sudo swanctl -l"
- "sudo swanctl -P"
- "sudo ip x sa show"
- "sudo ip x policy show"
- "sudo ip tunnel show"
- "sudo ip address"
- "sudo ip rule show"
- "sudo ip route"
- "sudo ip route show table 220"
- )
-
-
-echo "DEBUG: ${DATE} on host \"$(hostname)\"" > /tmp/ipsec-status-${DATE}.txt
-date >> /tmp/ipsec-status-${DATE}.txt
-
-# Execute all DEBUG commands and save it to file
-for cmd in "${a_CMD[@]}"; do
- echo -e "\n### ${cmd} ###" >> /tmp/ipsec-status-${DATE}.txt
- ${cmd} >> /tmp/ipsec-status-${DATE}.txt 2>/dev/null
-done
-
-# Collect charon logs, build .tgz archive
-sudo journalctl /usr/lib/ipsec/charon > /tmp/journalctl-charon-${DATE}.txt && \
-sudo tar -zcvf /tmp/ipsec-debug-${DATE}.tgz /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt >& /dev/null
-sudo rm -f /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt
-
-echo "Debug file is generated and located in /tmp/ipsec-debug-${DATE}.tgz"
diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py
index cbc9ef973..43e94048d 100755
--- a/src/op_mode/generate_ssh_server_key.py
+++ b/src/op_mode/generate_ssh_server_key.py
@@ -17,10 +17,15 @@
from sys import exit
from vyos.util import ask_yes_no
from vyos.util import cmd
+from vyos.util import commit_in_progress
if not ask_yes_no('Do you really want to remove the existing SSH host keys?'):
exit(0)
+if commit_in_progress():
+ print('Cannot restart SSH while a commit is in progress')
+ exit(1)
+
cmd('rm -v /etc/ssh/ssh_host_*')
cmd('dpkg-reconfigure openssh-server')
cmd('systemctl restart ssh.service')
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
index 21561d16f..a22f04c45 100755
--- a/src/op_mode/ikev2_profile_generator.py
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -119,7 +119,7 @@ config_base = ipsec_base + ['remote-access', 'connection']
pki_base = ['pki']
conf = ConfigTreeQuery()
if not conf.exists(config_base):
- exit('IPSec remote-access is not configured!')
+ exit('IPsec remote-access is not configured!')
profile_name = 'VyOS IKEv2 Profile'
if args.profile:
@@ -131,7 +131,7 @@ if args.name:
conn_base = config_base + [args.connection]
if not conf.exists(conn_base):
- exit(f'IPSec remote-access connection "{args.connection}" does not exist!')
+ exit(f'IPsec remote-access connection "{args.connection}" does not exist!')
data = conf.get_config_dict(conn_base, key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
@@ -178,7 +178,7 @@ for _, proposal in ike_proposal.items():
proposal['hash'] in set(vyos2client_integrity) and
proposal['dh_group'] in set(supported_dh_groups)):
- # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme
+ # We 're-code' from the VyOS IPsec proposals to the Apple naming scheme
proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
@@ -191,7 +191,7 @@ count = 1
for _, proposal in esp_proposals.items():
if {'encryption', 'hash'} <= set(proposal):
if proposal['encryption'] in set(vyos2client_cipher) and proposal['hash'] in set(vyos2client_integrity):
- # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme
+ # We 're-code' from the VyOS IPsec proposals to the Apple naming scheme
proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py
new file mode 100755
index 000000000..e0d204a0a
--- /dev/null
+++ b/src/op_mode/ipsec.py
@@ -0,0 +1,473 @@
+#!/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 re
+import sys
+import typing
+
+from collections import OrderedDict
+from hurry import filesize
+from re import split as re_split
+from tabulate import tabulate
+from subprocess import TimeoutExpired
+
+from vyos.util import call
+from vyos.util import convert_data
+from vyos.util import seconds_to_human
+
+import vyos.opmode
+
+
+SWANCTL_CONF = '/etc/swanctl/swanctl.conf'
+
+
+def _convert(text):
+ return int(text) if text.isdigit() else text.lower()
+
+
+def _alphanum_key(key):
+ return [_convert(c) for c in re_split('([0-9]+)', str(key))]
+
+
+def _get_vici_sas():
+ from vici import Session as vici_session
+
+ try:
+ session = vici_session()
+ except Exception:
+ raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized")
+ sas = list(session.list_sas())
+ return sas
+
+
+def _get_raw_data_sas():
+ get_sas = _get_vici_sas()
+ sas = convert_data(get_sas)
+ return sas
+
+
+def _get_formatted_output_sas(sas):
+ sa_data = []
+ for sa in sas:
+ for parent_sa in sa.values():
+ # create an item for each child-sa
+ for child_sa in parent_sa.get('child-sas', {}).values():
+ # prepare a list for output data
+ sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = sa_out_packets = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A'
+
+ # collect raw data
+ sa_name = child_sa.get('name')
+ sa_state = child_sa.get('state')
+ sa_uptime = child_sa.get('install-time')
+ sa_bytes_in = child_sa.get('bytes-in')
+ sa_bytes_out = child_sa.get('bytes-out')
+ sa_packets_in = child_sa.get('packets-in')
+ sa_packets_out = child_sa.get('packets-out')
+ sa_remote_addr = parent_sa.get('remote-host')
+ sa_remote_id = parent_sa.get('remote-id')
+ sa_proposal_encr_alg = child_sa.get('encr-alg')
+ sa_proposal_integ_alg = child_sa.get('integ-alg')
+ sa_proposal_encr_keysize = child_sa.get('encr-keysize')
+ sa_proposal_dh_group = child_sa.get('dh-group')
+
+ # format data to display
+ if sa_name:
+ sa_out_name = sa_name
+ if sa_state:
+ if sa_state == 'INSTALLED':
+ sa_out_state = 'up'
+ else:
+ sa_out_state = 'down'
+ if sa_uptime:
+ sa_out_uptime = seconds_to_human(sa_uptime)
+ if sa_bytes_in and sa_bytes_out:
+ bytes_in = filesize.size(int(sa_bytes_in))
+ bytes_out = filesize.size(int(sa_bytes_out))
+ sa_out_bytes = f'{bytes_in}/{bytes_out}'
+ if sa_packets_in and sa_packets_out:
+ packets_in = filesize.size(int(sa_packets_in),
+ system=filesize.si)
+ packets_out = filesize.size(int(sa_packets_out),
+ system=filesize.si)
+ packets_str = f'{packets_in}/{packets_out}'
+ sa_out_packets = re.sub(r'B', r'', packets_str)
+ if sa_remote_addr:
+ sa_out_remote_addr = sa_remote_addr
+ if sa_remote_id:
+ sa_out_remote_id = sa_remote_id
+ # format proposal
+ if sa_proposal_encr_alg:
+ sa_out_proposal = sa_proposal_encr_alg
+ if sa_proposal_encr_keysize:
+ sa_proposal_encr_keysize_str = sa_proposal_encr_keysize
+ sa_out_proposal = f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}'
+ if sa_proposal_integ_alg:
+ sa_proposal_integ_alg_str = sa_proposal_integ_alg
+ sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_integ_alg_str}'
+ if sa_proposal_dh_group:
+ sa_proposal_dh_group_str = sa_proposal_dh_group
+ sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_dh_group_str}'
+
+ # add a new item to output data
+ sa_data.append([
+ sa_out_name, sa_out_state, sa_out_uptime, sa_out_bytes,
+ sa_out_packets, sa_out_remote_addr, sa_out_remote_id,
+ sa_out_proposal
+ ])
+
+ headers = [
+ "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out",
+ "Remote address", "Remote ID", "Proposal"
+ ]
+ sa_data = sorted(sa_data, key=_alphanum_key)
+ output = tabulate(sa_data, headers)
+ return output
+
+
+# Connections block
+def _get_vici_connections():
+ from vici import Session as vici_session
+
+ try:
+ session = vici_session()
+ except Exception:
+ raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized")
+ connections = list(session.list_conns())
+ return connections
+
+
+def _get_convert_data_connections():
+ get_connections = _get_vici_connections()
+ connections = convert_data(get_connections)
+ return connections
+
+
+def _get_parent_sa_proposal(connection_name: str, data: list) -> dict:
+ """Get parent SA proposals by connection name
+ if connections not in the 'down' state
+
+ Args:
+ connection_name (str): Connection name
+ data (list): List of current SAs from vici
+
+ Returns:
+ str: Parent SA connection proposal
+ AES_CBC/256/HMAC_SHA2_256_128/MODP_1024
+ """
+ if not data:
+ return {}
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return {}
+ if 'encr-alg' in sa[connection_name]:
+ encr_alg = sa.get(connection_name, '').get('encr-alg')
+ cipher = encr_alg.split('_')[0]
+ mode = encr_alg.split('_')[1]
+ encr_keysize = sa.get(connection_name, '').get('encr-keysize')
+ integ_alg = sa.get(connection_name, '').get('integ-alg')
+ # prf_alg = sa.get(connection_name, '').get('prf-alg')
+ dh_group = sa.get(connection_name, '').get('dh-group')
+ proposal = {
+ 'cipher': cipher,
+ 'mode': mode,
+ 'key_size': encr_keysize,
+ 'hash': integ_alg,
+ 'dh': dh_group
+ }
+ return proposal
+ return {}
+
+
+def _get_parent_sa_state(connection_name: str, data: list) -> str:
+ """Get parent SA state by connection name
+
+ Args:
+ connection_name (str): Connection name
+ data (list): List of current SAs from vici
+
+ Returns:
+ Parent SA connection state
+ """
+ if not data:
+ return 'down'
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return 'down'
+ if sa[connection_name]['state'].lower() == 'established':
+ return 'up'
+ else:
+ return 'down'
+
+
+def _get_child_sa_state(connection_name: str, tunnel_name: str,
+ data: list) -> str:
+ """Get child SA state by connection and tunnel name
+
+ Args:
+ connection_name (str): Connection name
+ tunnel_name (str): Tunnel name
+ data (list): List of current SAs from vici
+
+ Returns:
+ str: `up` if child SA state is 'installed' otherwise `down`
+ """
+ if not data:
+ return 'down'
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return 'down'
+ child_sas = sa[connection_name]['child-sas']
+ # Get all child SA states
+ # there can be multiple SAs per tunnel
+ child_sa_states = [
+ v['state'] for k, v in child_sas.items() if v['name'] == tunnel_name
+ ]
+ return 'up' if 'INSTALLED' in child_sa_states else 'down'
+
+
+def _get_child_sa_info(connection_name: str, tunnel_name: str,
+ data: list) -> dict:
+ """Get child SA installed info by connection and tunnel name
+
+ Args:
+ connection_name (str): Connection name
+ tunnel_name (str): Tunnel name
+ data (list): List of current SAs from vici
+
+ Returns:
+ dict: Info of the child SA in the dictionary format
+ """
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return {}
+ child_sas = sa[connection_name]['child-sas']
+ # Get all child SA data
+ # Skip temp SA name (first key), get only SA values as dict
+ # {'OFFICE-B-tunnel-0-46': {'name': 'OFFICE-B-tunnel-0'}...}
+ # i.e get all data after 'OFFICE-B-tunnel-0-46'
+ child_sa_info = [
+ v for k, v in child_sas.items() if 'name' in v and
+ v['name'] == tunnel_name and v['state'] == 'INSTALLED'
+ ]
+ return child_sa_info[-1] if child_sa_info else {}
+
+
+def _get_child_sa_proposal(child_sa_data: dict) -> dict:
+ if child_sa_data and 'encr-alg' in child_sa_data:
+ encr_alg = child_sa_data.get('encr-alg')
+ cipher = encr_alg.split('_')[0]
+ mode = encr_alg.split('_')[1]
+ key_size = child_sa_data.get('encr-keysize')
+ integ_alg = child_sa_data.get('integ-alg')
+ dh_group = child_sa_data.get('dh-group')
+ proposal = {
+ 'cipher': cipher,
+ 'mode': mode,
+ 'key_size': key_size,
+ 'hash': integ_alg,
+ 'dh': dh_group
+ }
+ return proposal
+ return {}
+
+
+def _get_raw_data_connections(list_connections: list, list_sas: list) -> list:
+ """Get configured VPN IKE connections and IPsec states
+
+ Args:
+ list_connections (list): List of configured connections from vici
+ list_sas (list): List of current SAs from vici
+
+ Returns:
+ list: List and status of IKE/IPsec connections/tunnels
+ """
+ base_dict = []
+ for connections in list_connections:
+ base_list = {}
+ for connection, conn_conf in connections.items():
+ base_list['ike_connection_name'] = connection
+ base_list['ike_connection_state'] = _get_parent_sa_state(
+ connection, list_sas)
+ base_list['ike_remote_address'] = conn_conf['remote_addrs']
+ base_list['ike_proposal'] = _get_parent_sa_proposal(
+ connection, list_sas)
+ base_list['local_id'] = conn_conf.get('local-1', '').get('id')
+ base_list['remote_id'] = conn_conf.get('remote-1', '').get('id')
+ base_list['version'] = conn_conf.get('version', 'IKE')
+ base_list['children'] = []
+ children = conn_conf['children']
+ for tunnel, tun_options in children.items():
+ state = _get_child_sa_state(connection, tunnel, list_sas)
+ local_ts = tun_options.get('local-ts')
+ remote_ts = tun_options.get('remote-ts')
+ dpd_action = tun_options.get('dpd_action')
+ close_action = tun_options.get('close_action')
+ sa_info = _get_child_sa_info(connection, tunnel, list_sas)
+ esp_proposal = _get_child_sa_proposal(sa_info)
+ base_list['children'].append({
+ 'name': tunnel,
+ 'state': state,
+ 'local_ts': local_ts,
+ 'remote_ts': remote_ts,
+ 'dpd_action': dpd_action,
+ 'close_action': close_action,
+ 'sa': sa_info,
+ 'esp_proposal': esp_proposal
+ })
+ base_dict.append(base_list)
+ return base_dict
+
+
+def _get_raw_connections_summary(list_conn, list_sas):
+ import jmespath
+ data = _get_raw_data_connections(list_conn, list_sas)
+ match = '[*].children[]'
+ child = jmespath.search(match, data)
+ tunnels_down = len([k for k in child if k['state'] == 'down'])
+ tunnels_up = len([k for k in child if k['state'] == 'up'])
+ tun_dict = {
+ 'tunnels': child,
+ 'total': len(child),
+ 'down': tunnels_down,
+ 'up': tunnels_up
+ }
+ return tun_dict
+
+
+def _get_formatted_output_conections(data):
+ from tabulate import tabulate
+ data_entries = ''
+ connections = []
+ for entry in data:
+ tunnels = []
+ ike_name = entry['ike_connection_name']
+ ike_state = entry['ike_connection_state']
+ conn_type = entry.get('version', 'IKE')
+ remote_addrs = ','.join(entry['ike_remote_address'])
+ local_ts, remote_ts = '-', '-'
+ local_id = entry['local_id']
+ remote_id = entry['remote_id']
+ proposal = '-'
+ if entry.get('ike_proposal'):
+ proposal = (f'{entry["ike_proposal"]["cipher"]}_'
+ f'{entry["ike_proposal"]["mode"]}/'
+ f'{entry["ike_proposal"]["key_size"]}/'
+ f'{entry["ike_proposal"]["hash"]}/'
+ f'{entry["ike_proposal"]["dh"]}')
+ connections.append([
+ ike_name, ike_state, conn_type, remote_addrs, local_ts, remote_ts,
+ local_id, remote_id, proposal
+ ])
+ for tun in entry['children']:
+ tun_name = tun.get('name')
+ tun_state = tun.get('state')
+ conn_type = 'IPsec'
+ local_ts = '\n'.join(tun.get('local_ts'))
+ remote_ts = '\n'.join(tun.get('remote_ts'))
+ proposal = '-'
+ if tun.get('esp_proposal'):
+ proposal = (f'{tun["esp_proposal"]["cipher"]}_'
+ f'{tun["esp_proposal"]["mode"]}/'
+ f'{tun["esp_proposal"]["key_size"]}/'
+ f'{tun["esp_proposal"]["hash"]}/'
+ f'{tun["esp_proposal"]["dh"]}')
+ connections.append([
+ tun_name, tun_state, conn_type, remote_addrs, local_ts,
+ remote_ts, local_id, remote_id, proposal
+ ])
+ connection_headers = [
+ 'Connection', 'State', 'Type', 'Remote address', 'Local TS',
+ 'Remote TS', 'Local id', 'Remote id', 'Proposal'
+ ]
+ output = tabulate(connections, connection_headers, numalign='left')
+ return output
+
+
+# Connections block end
+
+
+def get_peer_connections(peer, tunnel):
+ search = rf'^[\s]*({peer}-(tunnel-[\d]+|vti)).*'
+ matches = []
+ if not os.path.exists(SWANCTL_CONF):
+ raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized")
+ suffix = None if tunnel is None else (f'tunnel-{tunnel}' if
+ tunnel.isnumeric() else tunnel)
+ with open(SWANCTL_CONF, 'r') as f:
+ for line in f.readlines():
+ result = re.match(search, line)
+ if result:
+ if tunnel is None:
+ matches.append(result[1])
+ else:
+ if result[2] == suffix:
+ matches.append(result[1])
+ return matches
+
+
+def reset_peer(peer: str, tunnel:typing.Optional[str]):
+ conns = get_peer_connections(peer, tunnel)
+
+ if not conns:
+ raise vyos.opmode.IncorrectValue('Peer or tunnel(s) not found, aborting')
+
+ 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:
+ raise vyos.opmode.InternalError(f'Timed out while resetting {conn}')
+
+ print('Peer reset result: success')
+
+
+def show_sa(raw: bool):
+ sa_data = _get_raw_data_sas()
+ if raw:
+ return sa_data
+ return _get_formatted_output_sas(sa_data)
+
+
+def show_connections(raw: bool):
+ list_conns = _get_convert_data_connections()
+ list_sas = _get_raw_data_sas()
+ if raw:
+ return _get_raw_data_connections(list_conns, list_sas)
+
+ connections = _get_raw_data_connections(list_conns, list_sas)
+ return _get_formatted_output_conections(connections)
+
+
+def show_connections_summary(raw: bool):
+ list_conns = _get_convert_data_connections()
+ list_sas = _get_raw_data_sas()
+ if raw:
+ return _get_raw_connections_summary(list_conns, list_sas)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/log.py b/src/op_mode/log.py
new file mode 100755
index 000000000..b0abd6191
--- /dev/null
+++ b/src/op_mode/log.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import re
+import sys
+import typing
+
+from jinja2 import Template
+
+from vyos.util import rc_cmd
+
+import vyos.opmode
+
+journalctl_command_template = Template("""
+--no-hostname
+--quiet
+
+{% if boot %}
+ --boot
+{% endif %}
+
+{% if count %}
+ --lines={{ count }}
+{% endif %}
+
+{% if reverse %}
+ --reverse
+{% endif %}
+
+{% if since %}
+ --since={{ since }}
+{% endif %}
+
+{% if unit %}
+ --unit={{ unit }}
+{% endif %}
+
+{% if utc %}
+ --utc
+{% endif %}
+
+{% if raw %}
+{# By default show 100 only lines for raw option if count does not set #}
+{# Protection from parsing the full log by default #}
+{% if not boot %}
+ --lines={{ '' ~ count if count else '100' }}
+{% endif %}
+ --no-pager
+ --output=json
+{% endif %}
+""")
+
+
+def show(raw: bool,
+ boot: typing.Optional[bool],
+ count: typing.Optional[int],
+ facility: typing.Optional[str],
+ reverse: typing.Optional[bool],
+ utc: typing.Optional[bool],
+ unit: typing.Optional[str]):
+ kwargs = dict(locals())
+
+ journalctl_options = journalctl_command_template.render(kwargs)
+ journalctl_options = re.sub(r'\s+', ' ', journalctl_options)
+ rc, output = rc_cmd(f'journalctl {journalctl_options}')
+ if raw:
+ # Each 'journalctl --output json' line is a separate JSON object
+ # So we should return list of dict
+ return [json.loads(line) for line in output.split('\n')]
+ return output
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/show_ram.py b/src/op_mode/memory.py
index 2b0be3965..7666de646 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_raw_data():
from re import search as re_search
def find_value(keyword, mem_data):
@@ -33,7 +38,7 @@ def get_system_memory():
used = total - available
- res = {
+ mem_data = {
"total": total,
"free": available,
"used": used,
@@ -41,25 +46,20 @@ def get_system_memory():
"cached": cached
}
- return res
-
-def get_system_memory_human():
- from vyos.util import bytes_to_human
-
- mem = get_system_memory()
-
- for key in mem:
+ for key in mem_data:
# The Linux kernel exposes memory values in kilobytes,
# so we need to normalize them
- mem[key] = bytes_to_human(mem[key], initial_exponent=10)
+ mem_data[key] = mem_data[key] * 1024
- return mem
+ return mem_data
-def get_raw_data():
- return get_system_memory_human()
+def _get_formatted_output(mem):
+ from vyos.util import bytes_to_human
-def get_formatted_output():
- mem = get_raw_data()
+ # For human-readable outputs, we convert bytes to more convenient units
+ # (100M, 1.3G...)
+ for key in mem:
+ mem[key] = bytes_to_human(mem[key])
out = "Total: {}\n".format(mem["total"])
out += "Free: {}\n".format(mem["free"])
@@ -67,5 +67,21 @@ def get_formatted_output():
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, vyos.opmode.Error) 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..f899eb3dc
--- /dev/null
+++ b/src/op_mode/nat.py
@@ -0,0 +1,334 @@
+#!/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 xmltodict
+
+from sys import exit
+from tabulate import tabulate
+
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.util import cmd
+from vyos.util import dict_search
+
+import vyos.opmode
+
+
+base = 'nat'
+unconf_message = 'NAT is not configured'
+
+
+def _get_xml_translation(direction, family):
+ """
+ Get conntrack XML output --src-nat|--dst-nat
+ """
+ if direction == 'source':
+ opt = '--src-nat'
+ if direction == 'destination':
+ opt = '--dst-nat'
+ return cmd(f'sudo conntrack --dump --family {family} {opt} --output xml')
+
+
+def _xml_to_dict(xml):
+ """
+ Convert XML to dictionary
+ Return: dictionary
+ """
+ parse = xmltodict.parse(xml, attr_prefix='')
+ # If only one conntrack entry we must change dict
+ if 'meta' in parse['conntrack']['flow']:
+ return dict(conntrack={'flow': [parse['conntrack']['flow']]})
+ return parse
+
+
+def _get_json_data(direction, family):
+ """
+ Get NAT format JSON
+ """
+ if direction == 'source':
+ chain = 'POSTROUTING'
+ if direction == 'destination':
+ chain = 'PREROUTING'
+ family = 'ip6' if family == 'inet6' else 'ip'
+ return cmd(f'sudo nft --json list chain {family} vyos_nat {chain}')
+
+
+def _get_raw_data_rules(direction, family):
+ """Get interested rules
+ :returns dict
+ """
+ data = _get_json_data(direction, family)
+ 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_raw_translation(direction, family):
+ """
+ Return: dictionary
+ """
+ xml = _get_xml_translation(direction, family)
+ if len(xml) == 0:
+ output = {'conntrack':
+ {
+ 'error': True,
+ 'reason': 'entries not found'
+ }
+ }
+ return output
+ return _xml_to_dict(xml)
+
+
+def _get_formatted_output_rules(data, direction, family):
+ # Add default values before loop
+ sport, dport, proto = 'any', 'any', 'any'
+ saddr = '::/0' if family == 'inet6' else '0.0.0.0/0'
+ daddr = '::/0' if family == 'inet6' else '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 isinstance(match['right'], dict) and ('prefix' in match['right'] or 'set' in match['right']):
+ # Merge dict src/dst l3_l4 parameters
+ my_dict = {**match['left']['payload'], **match['right']}
+ my_dict['op'] = match['op']
+ op = '!' if my_dict.get('op') == '!=' else ''
+ proto = my_dict.get('protocol').upper()
+ if my_dict['field'] == 'saddr':
+ saddr = f'{op}{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}'
+ elif my_dict['field'] == 'daddr':
+ daddr = f'{op}{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:
+ field = jmespath.search('left.payload.field', match)
+ if field == 'saddr':
+ saddr = match.get('right')
+ elif field == 'daddr':
+ daddr = match.get('right')
+ elif field == 'sport':
+ sport = match.get('right')
+ elif field == 'dport':
+ dport = match.get('right')
+ else:
+ saddr = '::/0' if family == 'inet6' else '0.0.0.0/0'
+ daddr = '::/0' if family == 'inet6' else '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 _get_formatted_translation(dict_data, nat_direction, family):
+ data_entries = []
+ if 'error' in dict_data['conntrack']:
+ return 'Entries not found'
+ for entry in dict_data['conntrack']['flow']:
+ orig_src, orig_dst, orig_sport, orig_dport = {}, {}, {}, {}
+ reply_src, reply_dst, reply_sport, reply_dport = {}, {}, {}, {}
+ proto = {}
+ for meta in entry['meta']:
+ direction = meta['direction']
+ if direction in ['original']:
+ if 'layer3' in meta:
+ orig_src = meta['layer3']['src']
+ orig_dst = meta['layer3']['dst']
+ if 'layer4' in meta:
+ if meta.get('layer4').get('sport'):
+ orig_sport = meta['layer4']['sport']
+ if meta.get('layer4').get('dport'):
+ orig_dport = meta['layer4']['dport']
+ proto = meta['layer4']['protoname']
+ if direction in ['reply']:
+ if 'layer3' in meta:
+ reply_src = meta['layer3']['src']
+ reply_dst = meta['layer3']['dst']
+ if 'layer4' in meta:
+ if meta.get('layer4').get('sport'):
+ reply_sport = meta['layer4']['sport']
+ if meta.get('layer4').get('dport'):
+ reply_dport = meta['layer4']['dport']
+ proto = meta['layer4']['protoname']
+ if direction == 'independent':
+ conn_id = meta['id']
+ timeout = meta['timeout']
+ orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src
+ orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst
+ reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src
+ reply_dst = f'{reply_dst}:{reply_dport}' if reply_dport else reply_dst
+ state = meta['state'] if 'state' in meta else ''
+ mark = meta['mark']
+ zone = meta['zone'] if 'zone' in meta else ''
+ if nat_direction == 'source':
+ data_entries.append(
+ [orig_src, reply_dst, proto, timeout, mark, zone])
+ elif nat_direction == 'destination':
+ data_entries.append(
+ [orig_dst, reply_src, proto, timeout, mark, zone])
+
+ headers = ["Pre-NAT", "Post-NAT", "Proto", "Timeout", "Mark", "Zone"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+
+def _verify(func):
+ """Decorator checks if NAT config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ if not config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+ return _wrapper
+
+
+@_verify
+def show_rules(raw: bool, direction: str, family: str):
+ nat_rules = _get_raw_data_rules(direction, family)
+ if raw:
+ return nat_rules
+ else:
+ return _get_formatted_output_rules(nat_rules, direction, family)
+
+
+@_verify
+def show_statistics(raw: bool, direction: str, family: str):
+ nat_statistics = _get_raw_data_rules(direction, family)
+ if raw:
+ return nat_statistics
+ else:
+ return _get_formatted_output_statistics(nat_statistics, direction)
+
+
+@_verify
+def show_translations(raw: bool, direction: str, family: str):
+ family = 'ipv6' if family == 'inet6' else 'ipv4'
+ nat_translation = _get_raw_translation(direction, family)
+ if raw:
+ return nat_translation
+ else:
+ return _get_formatted_translation(nat_translation, direction, family)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/neighbor.py b/src/op_mode/neighbor.py
new file mode 100755
index 000000000..264dbdc72
--- /dev/null
+++ b/src/op_mode/neighbor.py
@@ -0,0 +1,122 @@
+#!/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/>.
+
+# Sample output of `ip --json neigh list`:
+#
+# [
+# {
+# "dst": "192.168.1.1",
+# "dev": "eth0", # Missing if `dev ...` option is used
+# "lladdr": "00:aa:bb:cc:dd:ee", # May be missing for failed entries
+# "state": [
+# "REACHABLE"
+# ]
+# },
+# ]
+
+import sys
+import typing
+
+import vyos.opmode
+
+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 interface:
+ if not interface_exists(interface):
+ raise ValueError(f"Interface '{interface}' does not exist in the system")
+ interface = f"dev {interface}"
+ else:
+ interface = ""
+
+ if state:
+ state = f"nud {state}"
+ else:
+ state = ""
+
+ neigh_cmd = f"ip --family {family} --json neighbor list {interface} {state}"
+
+ data = loads(cmd(neigh_cmd))
+
+ return data
+
+def format_neighbors(neighs, interface=None):
+ from tabulate import tabulate
+
+ def entry_to_list(e, intf=None):
+ dst = e["dst"]
+
+ # State is always a list in the iproute2 output
+ state = ", ".join(e["state"])
+
+ # Link layer address is absent from e.g. FAILED entries
+ if "lladdr" in e:
+ lladdr = e["lladdr"]
+ else:
+ lladdr = None
+
+ # Device field is absent from outputs of `ip neigh list dev ...`
+ if "dev" in e:
+ dev = e["dev"]
+ elif interface:
+ dev = interface
+ else:
+ raise ValueError("interface is not defined")
+
+ return [dst, dev, lladdr, state]
+
+ neighs = map(entry_to_list, neighs)
+
+ headers = ["Address", "Interface", "Link layer address", "State"]
+ return tabulate(neighs, headers)
+
+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)
+
+ if raw:
+ return data
+ else:
+ return format_neighbors(data, interface)
+
+def reset(family: str, interface: typing.Optional[str], address: typing.Optional[str]):
+ from vyos.util import run
+
+ 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:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
+
diff --git a/src/op_mode/openconnect-control.py b/src/op_mode/openconnect-control.py
index c3cd25186..20c50e779 100755
--- a/src/op_mode/openconnect-control.py
+++ b/src/op_mode/openconnect-control.py
@@ -19,7 +19,9 @@ import argparse
import json
from vyos.config import Config
-from vyos.util import popen, run, DEVNULL
+from vyos.util import popen
+from vyos.util import run
+from vyos.util import DEVNULL
from tabulate import tabulate
occtl = '/usr/bin/occtl'
diff --git a/src/op_mode/openconnect.py b/src/op_mode/openconnect.py
new file mode 100755
index 000000000..b21890728
--- /dev/null
+++ b/src/op_mode/openconnect.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import json
+
+from tabulate import tabulate
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import rc_cmd
+
+import vyos.opmode
+
+
+occtl = '/usr/bin/occtl'
+occtl_socket = '/run/ocserv/occtl.socket'
+
+
+def _get_raw_data_sessions():
+ rc, out = rc_cmd(f'sudo {occtl} --json --socket-file {occtl_socket} show users')
+ if rc != 0:
+ raise vyos.opmode.DataUnavailable(out)
+
+ sessions = json.loads(out)
+ return sessions
+
+
+def _get_formatted_sessions(data):
+ headers = ["Interface", "Username", "IP", "Remote IP", "RX", "TX", "State", "Uptime"]
+ ses_list = []
+ for ses in data:
+ ses_list.append([
+ ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"],
+ ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"]
+ ])
+ if len(ses_list) > 0:
+ output = tabulate(ses_list, headers)
+ else:
+ output = 'No active openconnect sessions'
+ return output
+
+
+def show_sessions(raw: bool):
+ config = ConfigTreeQuery()
+ if not config.exists('vpn openconnect'):
+ raise vyos.opmode.UnconfiguredSubsystem('Openconnect is not configured')
+
+ openconnect_data = _get_raw_data_sessions()
+ if raw:
+ return openconnect_data
+ return _get_formatted_sessions(openconnect_data)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py
new file mode 100755
index 000000000..3797a7153
--- /dev/null
+++ b/src/op_mode/openvpn.py
@@ -0,0 +1,220 @@
+#!/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
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.util import bytes_to_human
+from vyos.util import commit_in_progress
+from vyos.util import call
+from vyos.config import Config
+
+def _get_tunnel_address(peer_host, peer_port, status_file):
+ peer = peer_host + ':' + peer_port
+ lst = []
+
+ with open(status_file, 'r') as f:
+ lines = f.readlines()
+ for line in lines:
+ if peer in line:
+ lst.append(line)
+
+ # filter out subnet entries if iroute:
+ # in the case that one sets, say:
+ # [ ..., 'vtun10', 'server', 'client', 'client1', 'subnet','10.10.2.0/25']
+ # the status file will have an entry:
+ # 10.10.2.0/25,client1,...
+ lst = [l for l in lst[1:] if '/' not in l.split(',')[0]]
+
+ tunnel_ip = lst[0].split(',')[0]
+
+ return tunnel_ip
+
+def _get_interface_status(mode: str, interface: str) -> dict:
+ status_file = f'/run/openvpn/{interface}.status'
+
+ data = {
+ 'mode': mode,
+ 'intf': interface,
+ 'local_host': '',
+ 'local_port': '',
+ 'date': '',
+ 'clients': [],
+ }
+
+ if not os.path.exists(status_file):
+ raise vyos.opmode.DataUnavailable('No information for interface {interface}')
+
+ with open(status_file, 'r') as f:
+ lines = f.readlines()
+ for line_no, line in enumerate(lines):
+ # remove trailing newline character first
+ line = line.rstrip('\n')
+
+ # check first line header
+ if line_no == 0:
+ if mode == 'server':
+ if not line == 'OpenVPN CLIENT LIST':
+ raise vyos.opmode.InternalError('Expected "OpenVPN CLIENT LIST"')
+ else:
+ if not line == 'OpenVPN STATISTICS':
+ raise vyos.opmode.InternalError('Expected "OpenVPN STATISTICS"')
+
+ continue
+
+ # second line informs us when the status file has been last updated
+ if line_no == 1:
+ data['date'] = line.lstrip('Updated,').rstrip('\n')
+ continue
+
+ if mode == 'server':
+ # for line_no > 1, lines appear as follows:
+ #
+ # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
+ # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019
+ # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019
+ # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019
+ # ...
+ # ROUTING TABLE
+ # ...
+ if line_no >= 3:
+ # indicator that there are no more clients
+ if line == 'ROUTING TABLE':
+ break
+ # otherwise, get client data
+ remote = (line.split(',')[1]).rsplit(':', maxsplit=1)
+
+ client = {
+ 'name': line.split(',')[0],
+ 'remote_host': remote[0],
+ 'remote_port': remote[1],
+ 'tunnel': 'N/A',
+ 'rx_bytes': bytes_to_human(int(line.split(',')[2]),
+ precision=1),
+ 'tx_bytes': bytes_to_human(int(line.split(',')[3]),
+ precision=1),
+ 'online_since': line.split(',')[4]
+ }
+ client['tunnel'] = _get_tunnel_address(client['remote_host'],
+ client['remote_port'],
+ status_file)
+ data['clients'].append(client)
+ continue
+ else: # mode == 'client' or mode == 'site-to-site'
+ if line_no == 2:
+ client = {
+ 'name': 'N/A',
+ 'remote_host': 'N/A',
+ 'remote_port': 'N/A',
+ 'tunnel': 'N/A',
+ 'rx_bytes': bytes_to_human(int(line.split(',')[1]),
+ precision=1),
+ 'tx_bytes': '',
+ 'online_since': 'N/A'
+ }
+ continue
+
+ if line_no == 3:
+ client['tx_bytes'] = bytes_to_human(int(line.split(',')[1]),
+ precision=1)
+ data['clients'].append(client)
+ break
+
+ return data
+
+def _get_raw_data(mode: str) -> dict:
+ data = {}
+ conf = Config()
+ conf_dict = conf.get_config_dict(['interfaces', 'openvpn'],
+ get_first_key=True)
+ if not conf_dict:
+ return data
+
+ interfaces = [x for x in list(conf_dict) if conf_dict[x]['mode'] == mode]
+ for intf in interfaces:
+ data[intf] = _get_interface_status(mode, intf)
+ d = data[intf]
+ d['local_host'] = conf_dict[intf].get('local-host', '')
+ d['local_port'] = conf_dict[intf].get('local-port', '')
+ if mode in ['client', 'site-to-site']:
+ for client in d['clients']:
+ if 'shared-secret-key-file' in list(conf_dict[intf]):
+ client['name'] = 'None (PSK)'
+ client['remote_host'] = conf_dict[intf].get('remote-host', [''])[0]
+ client['remote_port'] = conf_dict[intf].get('remote-port', '1194')
+
+ return data
+
+def _format_openvpn(data: dict) -> str:
+ if not data:
+ out = 'No OpenVPN interfaces configured'
+ return out
+
+ headers = ['Client CN', 'Remote Host', 'Tunnel IP', 'Local Host',
+ 'TX bytes', 'RX bytes', 'Connected Since']
+
+ out = ''
+ data_out = []
+ for intf in list(data):
+ l_host = data[intf]['local_host']
+ l_port = data[intf]['local_port']
+ for client in list(data[intf]['clients']):
+ r_host = client['remote_host']
+ r_port = client['remote_port']
+
+ out += f'\nOpenVPN status on {intf}\n\n'
+ name = client['name']
+ remote = r_host + ':' + r_port if r_host and r_port else 'N/A'
+ tunnel = client['tunnel']
+ local = l_host + ':' + l_port if l_host and l_port else 'N/A'
+ tx_bytes = client['tx_bytes']
+ rx_bytes = client['rx_bytes']
+ online_since = client['online_since']
+ data_out.append([name, remote, tunnel, local, tx_bytes,
+ rx_bytes, online_since])
+
+ out += tabulate(data_out, headers)
+
+ return out
+
+def show(raw: bool, mode: str) -> str:
+ openvpn_data = _get_raw_data(mode)
+
+ if raw:
+ return openvpn_data
+
+ return _format_openvpn(openvpn_data)
+
+def reset(interface: str):
+ if os.path.isfile(f'/run/openvpn/{interface}.conf'):
+ if commit_in_progress():
+ raise vyos.opmode.CommitInProgress('Retry OpenVPN reset: commit in progress.')
+ call(f'systemctl restart openvpn@{interface}.service')
+ else:
+ raise vyos.opmode.IncorrectValue(f'OpenVPN interface "{interface}" does not exist!')
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py
index 60bbc0c78..610e63cb3 100755
--- a/src/op_mode/ping.py
+++ b/src/op_mode/ping.py
@@ -18,6 +18,25 @@ import os
import sys
import socket
import ipaddress
+from vyos.util import get_all_vrfs
+from vyos.ifconfig import Section
+
+
+def interface_list() -> list:
+ """
+ Get list of interfaces in system
+ :rtype: list
+ """
+ return Section.interfaces()
+
+
+def vrf_list() -> list:
+ """
+ Get list of VRFs in system
+ :rtype: list
+ """
+ return list(get_all_vrfs().keys())
+
options = {
'audible': {
@@ -63,6 +82,7 @@ options = {
'interface': {
'ping': '{command} -I {value}',
'type': '<interface>',
+ 'helpfunction': interface_list,
'help': 'Source interface'
},
'interval': {
@@ -128,6 +148,7 @@ options = {
'ping': 'sudo ip vrf exec {value} {command}',
'type': '<vrf>',
'help': 'Use specified VRF table',
+ 'helpfunction': vrf_list,
'dflt': 'default',
},
'verbose': {
@@ -142,20 +163,33 @@ ping = {
}
-class List (list):
- def first (self):
+class List(list):
+ def first(self):
return self.pop(0) if self else ''
def last(self):
return self.pop() if self else ''
- def prepend(self,value):
- self.insert(0,value)
+ def prepend(self, value):
+ self.insert(0, value)
+
+
+def completion_failure(option: str) -> None:
+ """
+ Shows failure message after TAB when option is wrong
+ :param option: failure option
+ :type str:
+ """
+ sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option))
+ sys.stdout.write('<nocomps>')
+ sys.exit(1)
def expension_failure(option, completions):
reason = 'Ambiguous' if completions else 'Invalid'
- sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option))
+ sys.stderr.write(
+ '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv),
+ option))
if completions:
sys.stderr.write(' Possible completions:\n ')
sys.stderr.write('\n '.join(completions))
@@ -196,28 +230,44 @@ if __name__ == '__main__':
if host == '--get-options':
args.first() # pop ping
args.first() # pop IP
+ usedoptionslist = []
while args:
- option = args.first()
-
- matched = complete(option)
+ option = args.first() # pop option
+ matched = complete(option) # get option parameters
+ usedoptionslist.append(option) # list of used options
+ # Select options
if not args:
+ # remove from Possible completions used options
+ for o in usedoptionslist:
+ if o in matched:
+ matched.remove(o)
sys.stdout.write(' '.join(matched))
sys.exit(0)
- if len(matched) > 1 :
+ if len(matched) > 1:
sys.stdout.write(' '.join(matched))
sys.exit(0)
+ # If option doesn't have value
+ if matched:
+ if options[matched[0]]['type'] == 'noarg':
+ continue
+ else:
+ # Unexpected option
+ completion_failure(option)
- if options[matched[0]]['type'] == 'noarg':
- continue
-
- value = args.first()
+ value = args.first() # pop option's value
if not args:
matched = complete(option)
- sys.stdout.write(options[matched[0]]['type'])
+ helplines = options[matched[0]]['type']
+ # Run helpfunction to get list of possible values
+ if 'helpfunction' in options[matched[0]]:
+ result = options[matched[0]]['helpfunction']()
+ if result:
+ helplines = '\n' + ' '.join(result)
+ sys.stdout.write(helplines)
sys.exit(0)
- for name,option in options.items():
+ for name, option in options.items():
if 'dflt' in option and name not in args:
args.append(name)
args.append(option['dflt'])
@@ -234,8 +284,7 @@ if __name__ == '__main__':
except ValueError:
sys.exit(f'ping: Unknown host: {host}')
- command = convert(ping[version],args)
+ command = convert(ping[version], args)
# print(f'{command} {host}')
os.system(f'{command} {host}')
-
diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py
index 5be40082f..5953786f3 100755
--- a/src/op_mode/policy_route.py
+++ b/src/op_mode/policy_route.py
@@ -22,53 +22,13 @@ from vyos.config import Config
from vyos.util import cmd
from vyos.util import dict_search_args
-def get_policy_interfaces(conf, policy, name=None, ipv6=False):
- interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
-
- routes = ['route', 'route6']
-
- def parse_if(ifname, if_conf):
- if 'policy' in if_conf:
- for route in routes:
- if route in if_conf['policy']:
- route_name = if_conf['policy'][route]
- name_str = f'({ifname},{route})'
-
- if not name:
- policy[route][route_name]['interface'].append(name_str)
- elif not ipv6 and name == route_name:
- policy['interface'].append(name_str)
-
- for iftype in ['vif', 'vif_s', 'vif_c']:
- if iftype in if_conf:
- for vifname, vif_conf in if_conf[iftype].items():
- parse_if(f'{ifname}.{vifname}', vif_conf)
-
- for iftype, iftype_conf in interfaces.items():
- for ifname, if_conf in iftype_conf.items():
- parse_if(ifname, if_conf)
-
-def get_config_policy(conf, name=None, ipv6=False, interfaces=True):
+def get_config_policy(conf, name=None, ipv6=False):
config_path = ['policy']
if name:
config_path += ['route6' if ipv6 else 'route', name]
policy = conf.get_config_dict(config_path, key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
- if policy and interfaces:
- if name:
- policy['interface'] = []
- else:
- if 'route' in policy:
- for route_name, route_conf in policy['route'].items():
- route_conf['interface'] = []
-
- if 'route6' in policy:
- for route_name, route_conf in policy['route6'].items():
- route_conf['interface'] = []
-
- get_policy_interfaces(conf, policy, name, ipv6)
return policy
diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py
index dbd3eb4d1..efbf65083 100755
--- a/src/op_mode/reset_openvpn.py
+++ b/src/op_mode/reset_openvpn.py
@@ -17,6 +17,7 @@
import os
from sys import argv, exit
from vyos.util import call
+from vyos.util import commit_in_progress
if __name__ == '__main__':
if (len(argv) < 1):
@@ -25,6 +26,9 @@ if __name__ == '__main__':
interface = argv[1]
if os.path.isfile(f'/run/openvpn/{interface}.conf'):
+ if commit_in_progress():
+ print('Cannot restart OpenVPN while a commit is in progress')
+ exit(1)
call(f'systemctl restart openvpn@{interface}.service')
else:
print(f'OpenVPN interface "{interface}" does not exist!')
diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py
index af4fb2d15..9203c009f 100755
--- a/src/op_mode/restart_dhcp_relay.py
+++ b/src/op_mode/restart_dhcp_relay.py
@@ -24,6 +24,7 @@ import os
import vyos.config
from vyos.util import call
+from vyos.util import commit_in_progress
parser = argparse.ArgumentParser()
@@ -39,7 +40,10 @@ if __name__ == '__main__':
if not c.exists_effective('service dhcp-relay'):
print("DHCP relay service not configured")
else:
- call('systemctl restart isc-dhcp-server.service')
+ if commit_in_progress():
+ print('Cannot restart DHCP relay while a commit is in progress')
+ exit(1)
+ call('systemctl restart isc-dhcp-relay.service')
sys.exit(0)
elif args.ipv6:
@@ -47,7 +51,10 @@ if __name__ == '__main__':
if not c.exists_effective('service dhcpv6-relay'):
print("DHCPv6 relay service not configured")
else:
- call('systemctl restart isc-dhcp-server6.service')
+ if commit_in_progress():
+ print('Cannot restart DHCPv6 relay while commit is in progress')
+ exit(1)
+ call('systemctl restart isc-dhcp-relay6.service')
sys.exit(0)
else:
diff --git a/src/op_mode/route.py b/src/op_mode/route.py
new file mode 100755
index 000000000..d07a34180
--- /dev/null
+++ b/src/op_mode/route.py
@@ -0,0 +1,115 @@
+#!/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_summary(raw: bool):
+ from vyos.util import cmd
+
+ if raw:
+ from json import loads
+
+ output = cmd(f"vtysh -c 'show ip route summary json'")
+ return loads(output)
+ else:
+ output = cmd(f"vtysh -c 'show ip route summary'")
+ return output
+
+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
+ d = loads(output)
+ collect = []
+ for k,_ in d.items():
+ for l in d[k]:
+ collect.append(l)
+ return collect
+ else:
+ return output
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) 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_nat66_rules.py b/src/op_mode/show_nat66_rules.py
deleted file mode 100755
index 967ec9d37..000000000
--- a/src/op_mode/show_nat66_rules.py
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2021 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import 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 ip6 nat')
- tmp = json.loads(tmp)
-
- format_nat66_rule = '{0: <10} {1: <50} {2: <50} {3: <10}'
- print(format_nat66_rule.format("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface"))
- print(format_nat66_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-NAT66-', 'DST-NAT66-']:
- if comment_prefix in comment:
- continue_rule = False
- if continue_rule:
- continue
-
- # When log is detected from the second index of expr, then this rule should be ignored
- if 'log' in data['expr'][2]:
- continue
-
- rule = comment.replace('SRC-NAT66-','')
- rule = rule.replace('DST-NAT66-','')
- 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 = dict_search('match.right.prefix.addr', data['expr'][2])
- if srcdest:
- addr_tmp = dict_search('match.right.prefix.len', data['expr'][2])
- if addr_tmp:
- srcdest = srcdest + '/' + str(addr_tmp)
- else:
- srcdest = dict_search('match.right', data['expr'][2])
-
- tran_addr_json = dict_search('snat.addr' if args.source else 'dnat.addr', data['expr'][3])
- if tran_addr_json:
- if isinstance(srcdest_json,str):
- tran_addr = tran_addr_json
-
- if 'prefix' in tran_addr_json:
- 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:
- tran_addr = addr_tmp + '/' + str(len_tmp)
- else:
- if 'masquerade' in data['expr'][3]:
- tran_addr = 'masquerade'
-
- print(format_nat66_rule.format(rule, srcdest, tran_addr, interface))
-
- exit(0)
-else:
- parser.print_help()
- exit(1)
-
diff --git a/src/op_mode/show_nat66_statistics.py b/src/op_mode/show_nat66_statistics.py
index bc81692ae..cb10aed9f 100755
--- a/src/op_mode/show_nat66_statistics.py
+++ b/src/op_mode/show_nat66_statistics.py
@@ -44,7 +44,7 @@ group.add_argument("--destination", help="Show statistics for configured destina
args = parser.parse_args()
if args.source or args.destination:
- tmp = cmd('sudo nft -j list table ip6 nat')
+ tmp = cmd('sudo nft -j list table ip6 vyos_nat')
tmp = json.loads(tmp)
source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
diff --git a/src/op_mode/show_nat_rules.py b/src/op_mode/show_nat_rules.py
deleted file mode 100755
index 98adb31dd..000000000
--- a/src/op_mode/show_nat_rules.py
+++ /dev/null
@@ -1,123 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2021 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import 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 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 = ''
- 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_nat_statistics.py b/src/op_mode/show_nat_statistics.py
index c568c8305..be41e083b 100755
--- a/src/op_mode/show_nat_statistics.py
+++ b/src/op_mode/show_nat_statistics.py
@@ -44,7 +44,7 @@ group.add_argument("--destination", help="Show statistics for configured destina
args = parser.parse_args()
if args.source or args.destination:
- tmp = cmd('sudo nft -j list table ip nat')
+ tmp = cmd('sudo nft -j list table ip vyos_nat')
tmp = json.loads(tmp)
source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
diff --git a/src/op_mode/show_nat_translations.py b/src/op_mode/show_nat_translations.py
index 25091e9fc..508845e23 100755
--- a/src/op_mode/show_nat_translations.py
+++ b/src/op_mode/show_nat_translations.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -83,11 +83,23 @@ def pipe():
return xml
+def xml_to_dict(xml):
+ """
+ Convert XML to dictionary
+ Return: dictionary
+ """
+ parse = xmltodict.parse(xml)
+ # If only one NAT entry we must change dict T4499
+ if 'meta' in parse['conntrack']['flow']:
+ return dict(conntrack={'flow': [parse['conntrack']['flow']]})
+ return parse
+
+
def process(data, stats, protocol, pipe, verbose, flowtype=''):
if not data:
return
- parsed = xmltodict.parse(data)
+ parsed = xml_to_dict(data)
print(headers(verbose, pipe))
diff --git a/src/op_mode/show_neigh.py b/src/op_mode/show_neigh.py
deleted file mode 100755
index 94e745493..000000000
--- a/src/op_mode/show_neigh.py
+++ /dev/null
@@ -1,96 +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/>.
-
-#ip -j -f inet neigh list | jq
-#[
- #{
- #"dst": "192.168.101.8",
- #"dev": "enp0s25",
- #"lladdr": "78:d2:94:72:77:7e",
- #"state": [
- #"STALE"
- #]
- #},
- #{
- #"dst": "192.168.101.185",
- #"dev": "enp0s25",
- #"lladdr": "34:46:ec:76:f8:9b",
- #"state": [
- #"STALE"
- #]
- #},
- #{
- #"dst": "192.168.101.225",
- #"dev": "enp0s25",
- #"lladdr": "c2:cb:fa:bf:a0:35",
- #"state": [
- #"STALE"
- #]
- #},
- #{
- #"dst": "192.168.101.1",
- #"dev": "enp0s25",
- #"lladdr": "00:98:2b:f8:3f:11",
- #"state": [
- #"REACHABLE"
- #]
- #},
- #{
- #"dst": "192.168.101.181",
- #"dev": "enp0s25",
- #"lladdr": "d8:9b:3b:d5:88:22",
- #"state": [
- #"STALE"
- #]
- #}
-#]
-
-import sys
-import argparse
-import json
-from vyos.util import cmd
-
-def main():
- #parese args
- parser = argparse.ArgumentParser()
- parser.add_argument('--family', help='Protocol family', required=True)
- args = parser.parse_args()
-
- neigh_raw_json = cmd(f'ip -j -f {args.family} neigh list')
- neigh_raw_json = neigh_raw_json.lower()
- neigh_json = json.loads(neigh_raw_json)
-
- format_neigh = '%-50s %-10s %-20s %s'
- print(format_neigh % ("IP Address", "Device", "State", "LLADDR"))
- print(format_neigh % ("----------", "------", "-----", "------"))
-
- if neigh_json is not None:
- for neigh_item in neigh_json:
- dev = neigh_item['dev']
- dst = neigh_item['dst']
- lladdr = neigh_item['lladdr'] if 'lladdr' in neigh_item else ''
- state = neigh_item['state']
-
- i = 0
- for state_item in state:
- if i == 0:
- print(format_neigh % (dst, dev, state_item, lladdr))
- else:
- print(format_neigh % ('', '', state_item, ''))
- i+=1
-
-if __name__ == '__main__':
- main()
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
index 9a5adcffb..e29e594a5 100755
--- a/src/op_mode/show_openvpn.py
+++ b/src/op_mode/show_openvpn.py
@@ -59,7 +59,11 @@ def get_vpn_tunnel_address(peer, interface):
for line in lines:
if peer in line:
lst.append(line)
- tunnel_ip = lst[1].split(',')[0]
+
+ # filter out subnet entries
+ lst = [l for l in lst[1:] if '/' not in l.split(',')[0]]
+
+ tunnel_ip = lst[0].split(',')[0]
return tunnel_ip
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/storage.py b/src/op_mode/storage.py
new file mode 100755
index 000000000..d16e271bd
--- /dev/null
+++ b/src/op_mode/storage.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import sys
+
+import vyos.opmode
+from vyos.util import cmd
+
+# FIY: As of coreutils from Debian Buster and Bullseye,
+# the outpt looks like this:
+#
+# $ df -h -t ext4 --output=source,size,used,avail,pcent
+# Filesystem Size Used Avail Use%
+# /dev/sda1 16G 7.6G 7.3G 51%
+#
+# Those field names are automatically normalized by vyos.opmode.run,
+# so we don't touch them here,
+# and only normalize values.
+
+def _get_system_storage(only_persistent=False):
+ if not only_persistent:
+ cmd_str = 'df -h -x squashf'
+ else:
+ cmd_str = 'df -h -t ext4 --output=source,size,used,avail,pcent'
+
+ res = cmd(cmd_str)
+
+ return res
+
+def _get_raw_data():
+ from re import sub as re_sub
+ from vyos.util import human_to_bytes
+
+ out = _get_system_storage(only_persistent=True)
+ lines = out.splitlines()
+ lists = [l.split() for l in lines]
+ res = {lists[0][i]: lists[1][i] for i in range(len(lists[0]))}
+
+ res["Size"] = human_to_bytes(res["Size"])
+ res["Used"] = human_to_bytes(res["Used"])
+ res["Avail"] = human_to_bytes(res["Avail"])
+ res["Use%"] = re_sub(r'%', '', res["Use%"])
+
+ return res
+
+def _get_formatted_output():
+ return _get_system_storage()
+
+def show(raw: bool):
+ if raw:
+ return _get_raw_data()
+
+ return _get_formatted_output()
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
+
diff --git a/src/op_mode/system.py b/src/op_mode/system.py
new file mode 100755
index 000000000..11a3a8730
--- /dev/null
+++ b/src/op_mode/system.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import jmespath
+import json
+import sys
+import requests
+import typing
+
+from sys import exit
+
+from vyos.configquery import ConfigTreeQuery
+
+import vyos.opmode
+import vyos.version
+
+config = ConfigTreeQuery()
+base = ['system', 'update-check']
+
+
+def _compare_version_raw():
+ url = config.value(base + ['url'])
+ local_data = vyos.version.get_full_version_data()
+ remote_data = vyos.version.get_remote_version(url)
+ if not remote_data:
+ return {"error": True,
+ "reason": "Unable to get remote version"}
+ if local_data.get('version') and remote_data:
+ local_version = local_data.get('version')
+ remote_version = jmespath.search('[0].version', remote_data)
+ image_url = jmespath.search('[0].url', remote_data)
+ if local_data.get('version') != remote_version:
+ return {"error": False,
+ "update_available": True,
+ "local_version": local_version,
+ "remote_version": remote_version,
+ "url": image_url}
+ return {"update_available": False,
+ "local_version": local_version,
+ "remote_version": remote_version}
+
+
+def _formatted_compare_version(data):
+ local_version = data.get('local_version')
+ remote_version = data.get('remote_version')
+ url = data.get('url')
+ if {'update_available','local_version', 'remote_version', 'url'} <= set(data):
+ return f'Current version: {local_version}\n\nUpdate available: {remote_version}\nUpdate URL: {url}'
+ elif local_version == remote_version and remote_version is not None:
+ return f'No available updates for your system \n' \
+ f'current version: {local_version}\nremote version: {remote_version}'
+ else:
+ return 'Update not found'
+
+
+def _verify():
+ if not config.exists(base):
+ return False
+ return True
+
+
+def show_update(raw: bool):
+ if not _verify():
+ raise vyos.opmode.UnconfiguredSubsystem("system update-check not configured")
+ data = _compare_version_raw()
+ if raw:
+ return data
+ else:
+ return _formatted_compare_version(data)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/traceroute.py b/src/op_mode/traceroute.py
index 4299d6e5f..6c7030ea0 100755
--- a/src/op_mode/traceroute.py
+++ b/src/op_mode/traceroute.py
@@ -18,6 +18,25 @@ import os
import sys
import socket
import ipaddress
+from vyos.util import get_all_vrfs
+from vyos.ifconfig import Section
+
+
+def interface_list() -> list:
+ """
+ Get list of interfaces in system
+ :rtype: list
+ """
+ return Section.interfaces()
+
+
+def vrf_list() -> list:
+ """
+ Get list of VRFs in system
+ :rtype: list
+ """
+ return list(get_all_vrfs().keys())
+
options = {
'backward-hops': {
@@ -48,6 +67,7 @@ options = {
'interface': {
'traceroute': '{command} -i {value}',
'type': '<interface>',
+ 'helpfunction': interface_list,
'help': 'Source interface'
},
'lookup-as': {
@@ -99,6 +119,7 @@ options = {
'traceroute': 'sudo ip vrf exec {value} {command}',
'type': '<vrf>',
'help': 'Use specified VRF table',
+ 'helpfunction': vrf_list,
'dflt': 'default'}
}
@@ -108,20 +129,33 @@ traceroute = {
}
-class List (list):
- def first (self):
+class List(list):
+ def first(self):
return self.pop(0) if self else ''
def last(self):
return self.pop() if self else ''
- def prepend(self,value):
- self.insert(0,value)
+ def prepend(self, value):
+ self.insert(0, value)
+
+
+def completion_failure(option: str) -> None:
+ """
+ Shows failure message after TAB when option is wrong
+ :param option: failure option
+ :type str:
+ """
+ sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option))
+ sys.stdout.write('<nocomps>')
+ sys.exit(1)
def expension_failure(option, completions):
reason = 'Ambiguous' if completions else 'Invalid'
- sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option))
+ sys.stderr.write(
+ '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv),
+ option))
if completions:
sys.stderr.write(' Possible completions:\n ')
sys.stderr.write('\n '.join(completions))
@@ -160,30 +194,46 @@ if __name__ == '__main__':
sys.exit("traceroute: Missing host")
if host == '--get-options':
- args.first() # pop traceroute
+ args.first() # pop ping
args.first() # pop IP
+ usedoptionslist = []
while args:
- option = args.first()
-
- matched = complete(option)
+ option = args.first() # pop option
+ matched = complete(option) # get option parameters
+ usedoptionslist.append(option) # list of used options
+ # Select options
if not args:
+ # remove from Possible completions used options
+ for o in usedoptionslist:
+ if o in matched:
+ matched.remove(o)
sys.stdout.write(' '.join(matched))
sys.exit(0)
- if len(matched) > 1 :
+ if len(matched) > 1:
sys.stdout.write(' '.join(matched))
sys.exit(0)
+ # If option doesn't have value
+ if matched:
+ if options[matched[0]]['type'] == 'noarg':
+ continue
+ else:
+ # Unexpected option
+ completion_failure(option)
- if options[matched[0]]['type'] == 'noarg':
- continue
-
- value = args.first()
+ value = args.first() # pop option's value
if not args:
matched = complete(option)
- sys.stdout.write(options[matched[0]]['type'])
+ helplines = options[matched[0]]['type']
+ # Run helpfunction to get list of possible values
+ if 'helpfunction' in options[matched[0]]:
+ result = options[matched[0]]['helpfunction']()
+ if result:
+ helplines = '\n' + ' '.join(result)
+ sys.stdout.write(helplines)
sys.exit(0)
- for name,option in options.items():
+ for name, option in options.items():
if 'dflt' in option and name not in args:
args.append(name)
args.append(option['dflt'])
@@ -200,8 +250,7 @@ if __name__ == '__main__':
except ValueError:
sys.exit(f'traceroute: Unknown host: {host}')
- command = convert(traceroute[version],args)
+ command = convert(traceroute[version], args)
# print(f'{command} {host}')
os.system(f'{command} {host}')
-
diff --git a/src/op_mode/show_uptime.py b/src/op_mode/uptime.py
index b70c60cf8..2ebe6783b 100755
--- a/src/op_mode/show_uptime.py
+++ b/src/op_mode/uptime.py
@@ -14,7 +14,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-def get_uptime_seconds():
+import sys
+
+import vyos.opmode
+
+def _get_uptime_seconds():
from re import search
from vyos.util import read_file
@@ -23,7 +27,7 @@ def get_uptime_seconds():
return int(float(seconds))
-def get_load_averages():
+def _get_load_averages():
from re import search
from vyos.util import cmd
from vyos.cpu import get_core_count
@@ -40,19 +44,17 @@ def get_load_averages():
return res
-def get_raw_data():
+def _get_raw_data():
from vyos.util import seconds_to_human
res = {}
- res["uptime_seconds"] = get_uptime_seconds()
- res["uptime"] = seconds_to_human(get_uptime_seconds())
- res["load_average"] = get_load_averages()
+ res["uptime_seconds"] = _get_uptime_seconds()
+ res["uptime"] = seconds_to_human(_get_uptime_seconds())
+ res["load_average"] = _get_load_averages()
return res
-def get_formatted_output():
- data = get_raw_data()
-
+def _get_formatted_output(data):
out = "Uptime: {}\n\n".format(data["uptime"])
avgs = data["load_average"]
out += "Load averages:\n"
@@ -62,5 +64,19 @@ def get_formatted_output():
return out
+def show(raw: bool):
+ uptime_data = _get_raw_data()
+
+ if raw:
+ return uptime_data
+ else:
+ return _get_formatted_output(uptime_data)
+
if __name__ == '__main__':
- print(get_formatted_output())
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/show_version.py b/src/op_mode/version.py
index b82ab6eca..ad0293aca 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, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
- if args.funny:
- print(vyos.limericks.get_random())
diff --git a/src/op_mode/vpn_ike_sa.py b/src/op_mode/vpn_ike_sa.py
index 00f34564a..4b44c5c15 100755
--- a/src/op_mode/vpn_ike_sa.py
+++ b/src/op_mode/vpn_ike_sa.py
@@ -71,7 +71,7 @@ if __name__ == '__main__':
args = parser.parse_args()
if not process_named_running('charon'):
- print("IPSec Process NOT Running")
+ print("IPsec Process NOT Running")
sys.exit(0)
ike_sa(args.peer, args.nat)
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..a9a416761
--- /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('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'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'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, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/vtysh_wrapper.sh b/src/op_mode/vtysh_wrapper.sh
index 09980e14f..25d09ce77 100755
--- a/src/op_mode/vtysh_wrapper.sh
+++ b/src/op_mode/vtysh_wrapper.sh
@@ -1,5 +1,6 @@
#!/bin/sh
declare -a tmp
-# FRR uses ospf6 where we use ospfv3, thus alter the command
-tmp=$(echo $@ | sed -e "s/ospfv3/ospf6/")
+# FRR uses ospf6 where we use ospfv3, and we use reset over clear for BGP,
+# thus alter the commands
+tmp=$(echo $@ | sed -e "s/ospfv3/ospf6/" | sed -e "s/^reset bgp/clear bgp/" | sed -e "s/^reset ip bgp/clear ip bgp/")
vtysh -c "$tmp"
diff --git a/src/op_mode/webproxy_update_blacklist.sh b/src/op_mode/webproxy_update_blacklist.sh
index 43a4b79fc..d5f301b75 100755
--- a/src/op_mode/webproxy_update_blacklist.sh
+++ b/src/op_mode/webproxy_update_blacklist.sh
@@ -88,7 +88,7 @@ if [[ -n $update ]] && [[ $update -eq "yes" ]]; then
# fix permissions
chown -R proxy:proxy ${db_dir}
- chmod 2770 ${db_dir}
+ chmod 755 ${db_dir}
logger --priority WARNING "webproxy blacklist entries updated (${count_before}/${count_after})"