summaryrefslogtreecommitdiff
path: root/src/op_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/op_mode')
-rwxr-xr-xsrc/op_mode/anyconnect-control.py67
-rwxr-xr-xsrc/op_mode/clear_conntrack.py26
-rwxr-xr-xsrc/op_mode/connect_disconnect.py87
-rwxr-xr-xsrc/op_mode/cpu_summary.py36
-rwxr-xr-xsrc/op_mode/dns_forwarding_reset.py54
-rwxr-xr-xsrc/op_mode/dns_forwarding_restart.sh8
-rwxr-xr-xsrc/op_mode/dns_forwarding_statistics.py32
-rwxr-xr-xsrc/op_mode/dynamic_dns.py104
-rwxr-xr-xsrc/op_mode/flow_accounting_op.py252
-rwxr-xr-xsrc/op_mode/format_disk.py143
-rwxr-xr-xsrc/op_mode/generate_ssh_server_key.py26
-rwxr-xr-xsrc/op_mode/ipoe-control.py65
-rwxr-xr-xsrc/op_mode/lldp_op.py125
-rwxr-xr-xsrc/op_mode/maya_date.py208
-rwxr-xr-xsrc/op_mode/ping.py230
-rwxr-xr-xsrc/op_mode/powerctrl.py193
-rwxr-xr-xsrc/op_mode/ppp-server-ctrl.py71
-rwxr-xr-xsrc/op_mode/reset_openvpn.py31
-rwxr-xr-xsrc/op_mode/reset_vpn.py72
-rwxr-xr-xsrc/op_mode/restart_dhcp_relay.py55
-rwxr-xr-xsrc/op_mode/restart_frr.py197
-rwxr-xr-xsrc/op_mode/show_acceleration.py116
-rwxr-xr-xsrc/op_mode/show_configuration_files.sh10
-rwxr-xr-xsrc/op_mode/show_cpu.py61
-rwxr-xr-xsrc/op_mode/show_current_user.sh18
-rwxr-xr-xsrc/op_mode/show_dhcp.py264
-rwxr-xr-xsrc/op_mode/show_dhcpv6.py219
-rwxr-xr-xsrc/op_mode/show_disk_format.sh8
-rwxr-xr-xsrc/op_mode/show_igmpproxy.py241
-rwxr-xr-xsrc/op_mode/show_interfaces.py304
-rwxr-xr-xsrc/op_mode/show_ipsec_sa.py111
-rwxr-xr-xsrc/op_mode/show_nat_statistics.py63
-rwxr-xr-xsrc/op_mode/show_nat_translations.py200
-rwxr-xr-xsrc/op_mode/show_openvpn.py178
-rwxr-xr-xsrc/op_mode/show_raid.sh17
-rwxr-xr-xsrc/op_mode/show_ram.sh33
-rwxr-xr-xsrc/op_mode/show_sensors.py27
-rwxr-xr-xsrc/op_mode/show_usb_serial.py57
-rwxr-xr-xsrc/op_mode/show_users.py111
-rwxr-xr-xsrc/op_mode/show_version.py73
-rwxr-xr-xsrc/op_mode/show_vpn_ra.py56
-rwxr-xr-xsrc/op_mode/show_vrf.py67
-rwxr-xr-xsrc/op_mode/show_wireless.py156
-rwxr-xr-xsrc/op_mode/snmp.py78
-rwxr-xr-xsrc/op_mode/snmp_ifmib.py121
-rwxr-xr-xsrc/op_mode/snmp_v3.py180
-rwxr-xr-xsrc/op_mode/snmp_v3_showcerts.sh8
-rwxr-xr-xsrc/op_mode/system_integrity.py70
-rwxr-xr-xsrc/op_mode/toggle_help_binding.sh25
-rwxr-xr-xsrc/op_mode/vrrp.py55
-rwxr-xr-xsrc/op_mode/wireguard.py159
51 files changed, 5168 insertions, 0 deletions
diff --git a/src/op_mode/anyconnect-control.py b/src/op_mode/anyconnect-control.py
new file mode 100755
index 000000000..6382016b7
--- /dev/null
+++ b/src/op_mode/anyconnect-control.py
@@ -0,0 +1,67 @@
+#!/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 sys
+import argparse
+import json
+
+from vyos.config import Config
+from vyos.util import popen, run, DEVNULL
+from tabulate import tabulate
+
+occtl = '/usr/bin/occtl'
+occtl_socket = '/run/ocserv/occtl.socket'
+
+def show_sessions():
+ out, code = popen("sudo {0} -j -s {1} show users".format(occtl, occtl_socket),stderr=DEVNULL)
+ if code:
+ sys.exit('Cannot get anyconnect users information')
+ else:
+ headers = ["interface", "username", "ip", "remote IP", "RX", "TX", "state", "uptime"]
+ sessions = json.loads(out)
+ ses_list = []
+ for ses in sessions:
+ 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:
+ print(tabulate(ses_list, headers))
+ else:
+ print("No active anyconnect sessions")
+
+def is_ocserv_configured():
+ if not Config().exists_effective('vpn anyconnect'):
+ print("vpn anyconnect server is not configured")
+ sys.exit(1)
+
+def main():
+ #parese args
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--action', help='Control action', required=True)
+ parser.add_argument('--selector', help='Selector username|ifname|sid', required=False)
+ parser.add_argument('--target', help='Target must contain username|ifname|sid', required=False)
+ args = parser.parse_args()
+
+
+ # Check is IPoE configured
+ is_ocserv_configured()
+
+ if args.action == "restart":
+ run("systemctl restart ocserv")
+ sys.exit(0)
+ elif args.action == "show_sessions":
+ show_sessions()
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py
new file mode 100755
index 000000000..423694187
--- /dev/null
+++ b/src/op_mode/clear_conntrack.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+
+from vyos.util import ask_yes_no
+from vyos.util import cmd, DEVNULL
+
+if not ask_yes_no("This will clear all currently tracked and expected connections. Continue?"):
+ sys.exit(1)
+else:
+ cmd('/usr/sbin/conntrack -F', stderr=DEVNULL)
+ cmd('/usr/sbin/conntrack -F expect', stderr=DEVNULL)
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py
new file mode 100755
index 000000000..a773aa28e
--- /dev/null
+++ b/src/op_mode/connect_disconnect.py
@@ -0,0 +1,87 @@
+#!/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 os
+import argparse
+
+from sys import exit
+from psutil import process_iter
+from time import strftime, localtime, time
+
+from vyos.util import call
+
+def check_interface(interface):
+ if not os.path.isfile(f'/etc/ppp/peers/{interface}'):
+ print(f'Interface {interface}: invalid!')
+ exit(1)
+
+def check_ppp_running(interface):
+ """
+ Check if ppp process is running in the interface in question
+ """
+ for p in process_iter():
+ if "pppd" in p.name():
+ if interface in p.cmdline():
+ return True
+
+ return False
+
+def connect(interface):
+ """
+ Connect PPP interface
+ """
+ check_interface(interface)
+
+ # Check if interface is already dialed
+ if os.path.isdir(f'/sys/class/net/{interface}'):
+ print(f'Interface {interface}: already connected!')
+ elif check_ppp_running(interface):
+ print(f'Interface {interface}: connection is beeing established!')
+ else:
+ print(f'Interface {interface}: connecting...')
+ call(f'systemctl restart ppp@{interface}.service')
+
+def disconnect(interface):
+ """
+ Disconnect PPP interface
+ """
+ check_interface(interface)
+
+ # Check if interface is already down
+ if not check_ppp_running(interface):
+ print(f'Interface {interface}: connection is already down')
+ else:
+ print(f'Interface {interface}: disconnecting...')
+ call(f'systemctl stop ppp@{interface}.service')
+
+def main():
+ parser = argparse.ArgumentParser()
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument("--connect", help="Bring up a connection-oriented network interface", action="store")
+ group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store")
+ args = parser.parse_args()
+
+ if args.connect:
+ connect(args.connect)
+ elif args.disconnect:
+ disconnect(args.disconnect)
+ else:
+ parser.print_help()
+
+ exit(0)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py
new file mode 100755
index 000000000..cfd321522
--- /dev/null
+++ b/src/op_mode/cpu_summary.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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'
+
+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'] = map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models'])
+
+print("CPU(s): {0}".format(cpu_data['cpu_number']))
+print("CPU model(s): {0}".format(",".join(cpu_data['models'])))
diff --git a/src/op_mode/dns_forwarding_reset.py b/src/op_mode/dns_forwarding_reset.py
new file mode 100755
index 000000000..bfc640a26
--- /dev/null
+++ b/src/op_mode/dns_forwarding_reset.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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/>.
+#
+# File: vyos-show-version
+# Purpose:
+# Displays image version and system information.
+# Used by the "run show version" command.
+
+
+import os
+import argparse
+
+from sys import exit
+from vyos.config import Config
+from vyos.util import call
+
+PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns'
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-a", "--all", action="store_true", help="Reset all cache")
+parser.add_argument("domain", type=str, nargs="?", help="Domain to reset cache entries for")
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective(['service', 'dns', 'forwarding']):
+ print("DNS forwarding is not configured")
+ exit(0)
+
+ if args.all:
+ call(f"{PDNS_CMD} wipe-cache \'.$\'")
+ exit(0)
+
+ elif args.domain:
+ call(f"{PDNS_CMD} wipe-cache \'{0}$\'".format(args.domain))
+
+ else:
+ parser.print_help()
+ exit(1)
diff --git a/src/op_mode/dns_forwarding_restart.sh b/src/op_mode/dns_forwarding_restart.sh
new file mode 100755
index 000000000..64cc92115
--- /dev/null
+++ b/src/op_mode/dns_forwarding_restart.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if cli-shell-api existsEffective service dns forwarding; then
+ echo "Restarting the DNS forwarding service"
+ systemctl restart pdns-recursor.service
+else
+ echo "DNS forwarding is not configured"
+fi
diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py
new file mode 100755
index 000000000..1fb61d263
--- /dev/null
+++ b/src/op_mode/dns_forwarding_statistics.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+import jinja2
+from sys import exit
+
+from vyos.config import Config
+from vyos.util import cmd
+
+PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns'
+
+OUT_TMPL_SRC = """
+DNS forwarding statistics:
+
+Cache entries: {{ cache_entries -}}
+Cache size: {{ cache_size }} kbytes
+
+"""
+
+if __name__ == '__main__':
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service dns forwarding'):
+ print("DNS forwarding is not configured")
+ exit(0)
+
+ data = {}
+
+ data['cache_entries'] = cmd(f'{PDNS_CMD} get cache-entries')
+ data['cache_size'] = "{0:.2f}".format( int(cmd(f'{PDNS_CMD} get cache-bytes')) / 1024 )
+
+ tmpl = jinja2.Template(OUT_TMPL_SRC)
+ print(tmpl.render(data))
diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py
new file mode 100755
index 000000000..021acfd73
--- /dev/null
+++ b/src/op_mode/dynamic_dns.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-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 os
+import argparse
+import jinja2
+import sys
+import time
+
+from vyos.config import Config
+from vyos.util import call
+
+cache_file = r'/run/ddclient/ddclient.cache'
+
+OUT_TMPL_SRC = """
+{%- for entry in hosts -%}
+ip address : {{ entry.ip }}
+host-name : {{ entry.host }}
+last update : {{ entry.time }}
+update-status: {{ entry.status }}
+
+{% endfor -%}
+"""
+
+def show_status():
+ data = {
+ 'hosts': []
+ }
+
+ with open(cache_file, 'r') as f:
+ for line in f:
+ if line.startswith('#'):
+ continue
+
+ outp = {
+ 'host': '',
+ 'ip': '',
+ 'time': ''
+ }
+
+ if 'host=' in line:
+ host = line.split('host=')[1]
+ if host:
+ outp['host'] = host.split(',')[0]
+
+ if 'ip=' in line:
+ ip = line.split('ip=')[1]
+ if ip:
+ outp['ip'] = ip.split(',')[0]
+
+ if 'atime=' in line:
+ atime = line.split('atime=')[1]
+ if atime:
+ tmp = atime.split(',')[0]
+ outp['time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(int(tmp, base=10)))
+
+ if 'status=' in line:
+ status = line.split('status=')[1]
+ if status:
+ outp['status'] = status.split(',')[0]
+
+ data['hosts'].append(outp)
+
+ tmpl = jinja2.Template(OUT_TMPL_SRC)
+ print(tmpl.render(data))
+
+
+def update_ddns():
+ call('systemctl stop ddclient.service')
+ if os.path.exists(cache_file):
+ os.remove(cache_file)
+ call('systemctl start ddclient.service')
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument("--status", help="Show DDNS status", action="store_true")
+ group.add_argument("--update", help="Update DDNS on a given interface", action="store_true")
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service dns dynamic'):
+ print("Dynamic DNS not configured")
+ sys.exit(1)
+
+ if args.status:
+ show_status()
+ elif args.update:
+ update_ddns()
diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py
new file mode 100755
index 000000000..6586cbceb
--- /dev/null
+++ b/src/op_mode/flow_accounting_op.py
@@ -0,0 +1,252 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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 argparse
+import re
+import ipaddress
+import os.path
+from tabulate import tabulate
+from json import loads
+from vyos.util import cmd, run
+from vyos.logger import syslog
+
+# some default values
+uacctd_pidfile = '/var/run/uacctd.pid'
+uacctd_pipefile = '/tmp/uacctd.pipe'
+
+def parse_port(port):
+ try:
+ port_num = int(port)
+ if (port_num >= 0) and (port_num <= 65535):
+ return port_num
+ else:
+ raise ValueError("out of the 0-65535 range".format(port))
+ except ValueError as e:
+ raise ValueError("Incorrect port number \'{0}\': {1}".format(port, e))
+
+def parse_ports(arg):
+ if re.match(r'^\d+$', arg):
+ # Single port
+ port = parse_port(arg)
+ return {"type": "single", "value": port}
+ elif re.match(r'^\d+\-\d+$', arg):
+ # Port range
+ ports = arg.split("-")
+ ports = list(map(parse_port, ports))
+ if ports[0] > ports[1]:
+ raise ValueError("Malformed port range \'{0}\': lower end is greater than the higher".format(arg))
+ else:
+ return {"type": "range", "value": (ports[0], ports[1])}
+ elif re.match(r'^\d+,.*\d$', arg):
+ # Port list
+ ports = re.split(r',+', arg) # This allows duplicate commad like '1,,2,3,4'
+ ports = list(map(parse_port, ports))
+ return {"type": "list", "value": ports}
+ else:
+ raise ValueError("Malformed port spec \'{0}\'".format(arg))
+
+# check if host argument have correct format
+def check_host(host):
+ # define regex for checking
+ if not ipaddress.ip_address(host):
+ raise ValueError("Invalid host \'{}\', must be a valid IP or IPv6 address".format(host))
+
+# check if flow-accounting running
+def _uacctd_running():
+ command = 'systemctl status uacctd.service > /dev/null'
+ return run(command) == 0
+
+
+# get list of interfaces
+def _get_ifaces_dict():
+ # run command to get ifaces list
+ out = cmd('/bin/ip link show')
+
+ # read output
+ ifaces_out = out.splitlines()
+
+ # make a dictionary with interfaces and indexes
+ ifaces_dict = {}
+ regex_filter = re.compile(r'^(?P<iface_index>\d+):\ (?P<iface_name>[\w\d\.]+)[:@].*$')
+ for iface_line in ifaces_out:
+ if regex_filter.search(iface_line):
+ ifaces_dict[int(regex_filter.search(iface_line).group('iface_index'))] = regex_filter.search(iface_line).group('iface_name')
+
+ # return dictioanry
+ return ifaces_dict
+
+
+# get list of flows
+def _get_flows_list():
+ # run command to get flows list
+ out = cmd(f'/usr/bin/pmacct -s -O json -T flows -p {uacctd_pipefile}',
+ message='Failed to get flows list')
+
+ # read output
+ flows_out = out.splitlines()
+
+ # make a list with flows
+ flows_list = []
+ for flow_line in flows_out:
+ try:
+ flows_list.append(loads(flow_line))
+ except Exception as err:
+ syslog.error('Unable to read flow info: {}'.format(err))
+
+ # return list of flows
+ return flows_list
+
+
+# filter and format flows
+def _flows_filter(flows, ifaces):
+ # predefine filtered flows list
+ flows_filtered = []
+
+ # add interface names to flows
+ for flow in flows:
+ if flow['iface_in'] in ifaces:
+ flow['iface_in_name'] = ifaces[flow['iface_in']]
+ else:
+ flow['iface_in_name'] = 'unknown'
+
+ # iterate through flows list
+ for flow in flows:
+ # filter by interface
+ if cmd_args.interface:
+ if flow['iface_in_name'] != cmd_args.interface:
+ continue
+ # filter by host
+ if cmd_args.host:
+ if flow['ip_src'] != cmd_args.host and flow['ip_dst'] != cmd_args.host:
+ continue
+ # filter by ports
+ if cmd_args.ports:
+ if cmd_args.ports['type'] == 'single':
+ if flow['port_src'] != cmd_args.ports['value'] and flow['port_dst'] != cmd_args.ports['value']:
+ continue
+ else:
+ if flow['port_src'] not in cmd_args.ports['value'] and flow['port_dst'] not in cmd_args.ports['value']:
+ continue
+ # add filtered flows to new list
+ flows_filtered.append(flow)
+
+ # stop adding if we already reached top count
+ if cmd_args.top:
+ if len(flows_filtered) == cmd_args.top:
+ break
+
+ # return filtered flows
+ return flows_filtered
+
+
+# print flow table
+def _flows_table_print(flows):
+ # define headers and body
+ table_headers = ['IN_IFACE', 'SRC_MAC', 'DST_MAC', 'SRC_IP', 'DST_IP', 'SRC_PORT', 'DST_PORT', 'PROTOCOL', 'TOS', 'PACKETS', 'FLOWS', 'BYTES']
+ table_body = []
+ # convert flows to list
+ for flow in flows:
+ table_line = [
+ flow.get('iface_in_name'),
+ flow.get('mac_src'),
+ flow.get('mac_dst'),
+ flow.get('ip_src'),
+ flow.get('ip_dst'),
+ flow.get('port_src'),
+ flow.get('port_dst'),
+ flow.get('ip_proto'),
+ flow.get('tos'),
+ flow.get('packets'),
+ flow.get('flows'),
+ flow.get('bytes')
+ ]
+ table_body.append(table_line)
+ # configure and fill table
+ table = tabulate(table_body, table_headers, tablefmt="simple")
+
+ # print formatted table
+ try:
+ print(table)
+ except IOError:
+ sys.exit(0)
+ except KeyboardInterrupt:
+ sys.exit(0)
+
+
+# check if in-memory table is active
+def _check_imt():
+ if not os.path.exists(uacctd_pipefile):
+ print("In-memory table is not available")
+ sys.exit(1)
+
+
+# define program arguments
+cmd_args_parser = argparse.ArgumentParser(description='show flow-accounting')
+cmd_args_parser.add_argument('--action', choices=['show', 'clear', 'restart'], required=True, help='command to flow-accounting daemon')
+cmd_args_parser.add_argument('--filter', choices=['interface', 'host', 'ports', 'top'], required=False, nargs='*', help='filter flows to display')
+cmd_args_parser.add_argument('--interface', required=False, help='interface name for output filtration')
+cmd_args_parser.add_argument('--host', type=str, required=False, help='host address for output filtering')
+cmd_args_parser.add_argument('--ports', type=str, required=False, help='port number, range or list for output filtering')
+cmd_args_parser.add_argument('--top', type=int, required=False, help='top records for output filtering')
+# parse arguments
+cmd_args = cmd_args_parser.parse_args()
+
+try:
+ if cmd_args.host:
+ check_host(cmd_args.host)
+
+ if cmd_args.ports:
+ cmd_args.ports = parse_ports(cmd_args.ports)
+except ValueError as e:
+ print(e)
+ sys.exit(1)
+
+# main logic
+# do nothing if uacctd daemon is not running
+if not _uacctd_running():
+ print("flow-accounting is not active")
+ sys.exit(1)
+
+# restart pmacct daemon
+if cmd_args.action == 'restart':
+ # run command to restart flow-accounting
+ cmd('systemctl restart uacctd.service',
+ message='Failed to restart flow-accounting')
+
+# clear in-memory collected flows
+if cmd_args.action == 'clear':
+ _check_imt()
+ # run command to clear flows
+ cmd(f'/usr/bin/pmacct -e -p {uacctd_pipefile}',
+ message='Failed to clear flows')
+
+# show table with flows
+if cmd_args.action == 'show':
+ _check_imt()
+ # get interfaces index and names
+ ifaces_dict = _get_ifaces_dict()
+ # get flows
+ flows_list = _get_flows_list()
+
+ # filter and format flows
+ tabledata = _flows_filter(flows_list, ifaces_dict)
+
+ # print flows
+ _flows_table_print(tabledata)
+
+sys.exit(0)
diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py
new file mode 100755
index 000000000..df4486bce
--- /dev/null
+++ b/src/op_mode/format_disk.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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 os
+import re
+import sys
+from datetime import datetime
+from time import sleep
+
+from vyos.util import is_admin, ask_yes_no
+from vyos.util import call
+from vyos.util import cmd
+from vyos.util import DEVNULL
+
+def list_disks():
+ disks = set()
+ with open('/proc/partitions') as partitions_file:
+ for line in partitions_file:
+ fields = line.strip().split()
+ if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name':
+ disks.add(fields[3])
+ return disks
+
+
+def is_busy(disk: str):
+ """Check if given disk device is busy by re-reading it's partition table"""
+ return call(f'sudo blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0
+
+
+def backup_partitions(disk: str):
+ """Save sfdisk partitions output to a backup file"""
+
+ device_path = '/dev/' + disk
+ backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M')
+ backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts)
+ cmd(f'sudo /sbin/sfdisk -d {device_path} > {backup_file}')
+
+
+def list_partitions(disk: str):
+ """List partition numbers of a given disk"""
+
+ parts = set()
+ part_num_expr = re.compile(disk + '([0-9]+)')
+ with open('/proc/partitions') as partitions_file:
+ for line in partitions_file:
+ fields = line.strip().split()
+ if len(fields) == 4 and fields[3] != 'name' and part_num_expr.match(fields[3]):
+ part_idx = part_num_expr.match(fields[3]).group(1)
+ parts.add(int(part_idx))
+ return parts
+
+
+def delete_partition(disk: str, partition_idx: int):
+ cmd(f'sudo /sbin/parted /dev/{disk} rm {partition_idx}')
+
+
+def format_disk_like(target: str, proto: str):
+ cmd(f'sudo /sbin/sfdisk -d /dev/{proto} | sudo /sbin/sfdisk --force /dev/{target}')
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ group = parser.add_argument_group()
+ group.add_argument('-t', '--target', type=str, required=True, help='Target device to format')
+ group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference')
+ args = parser.parse_args()
+
+ if not is_admin():
+ print('Must be admin or root to format disk')
+ sys.exit(1)
+
+ target_disk = args.target
+ eligible_target_disks = list_disks()
+
+ proto_disk = args.proto
+ eligible_proto_disks = eligible_target_disks.copy()
+ eligible_proto_disks.remove(target_disk)
+
+ fmt = {
+ 'target_disk': target_disk,
+ 'proto_disk': proto_disk,
+ }
+
+ if proto_disk == target_disk:
+ print('The two disk drives must be different.')
+ sys.exit(1)
+
+ if not os.path.exists('/dev/' + proto_disk):
+ print('Device /dev/{proto_disk} does not exist'.format_map(fmt))
+ sys.exit(1)
+
+ if not os.path.exists('/dev/' + target_disk):
+ print('Device /dev/{target_disk} does not exist'.format_map(fmt))
+ sys.exit(1)
+
+ if target_disk not in eligible_target_disks:
+ print('Device {target_disk} can not be formatted'.format_map(fmt))
+ sys.exit(1)
+
+ if proto_disk not in eligible_proto_disks:
+ print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt))
+ sys.exit(1)
+
+ if is_busy(target_disk):
+ print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt))
+ sys.exit(1)
+
+ print('This will re-format disk {target_disk} so that it has the same disk\n'
+ 'partion sizes and offsets as {proto_disk}. This will not copy\n'
+ 'data from {proto_disk} to {target_disk}. But this will erase all\n'
+ 'data on {target_disk}.\n'.format_map(fmt))
+
+ if not ask_yes_no("Do you wish to proceed?"):
+ print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt))
+ sys.exit(0)
+
+ print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt))
+
+ print('Making backup copy of partitions...')
+ backup_partitions(target_disk)
+ sleep(1)
+
+ print('Deleting old partitions...')
+ for p in list_partitions(target_disk):
+ delete_partition(disk=target_disk, partition_idx=p)
+
+ print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt))
+ format_disk_like(target=target_disk, proto=proto_disk)
+ print('Done.')
diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py
new file mode 100755
index 000000000..cbc9ef973
--- /dev/null
+++ b/src/op_mode/generate_ssh_server_key.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from sys import exit
+from vyos.util import ask_yes_no
+from vyos.util import cmd
+
+if not ask_yes_no('Do you really want to remove the existing SSH host keys?'):
+ exit(0)
+
+cmd('rm -v /etc/ssh/ssh_host_*')
+cmd('dpkg-reconfigure openssh-server')
+cmd('systemctl restart ssh.service')
diff --git a/src/op_mode/ipoe-control.py b/src/op_mode/ipoe-control.py
new file mode 100755
index 000000000..7111498b2
--- /dev/null
+++ b/src/op_mode/ipoe-control.py
@@ -0,0 +1,65 @@
+#!/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 sys
+import argparse
+
+from vyos.config import Config
+from vyos.util import popen, run
+
+cmd_dict = {
+ 'cmd_base' : '/usr/bin/accel-cmd -p 2002 ',
+ 'selector' : ['if', 'username', 'sid'],
+ 'actions' : {
+ 'show_sessions' : 'show sessions',
+ 'show_stat' : 'show stat',
+ 'terminate' : 'teminate'
+ }
+}
+
+def is_ipoe_configured():
+ if not Config().exists_effective('service ipoe-server'):
+ print("Service IPoE is not configured")
+ sys.exit(1)
+
+def main():
+ #parese args
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--action', help='Control action', required=True)
+ parser.add_argument('--selector', help='Selector username|ifname|sid', required=False)
+ parser.add_argument('--target', help='Target must contain username|ifname|sid', required=False)
+ args = parser.parse_args()
+
+
+ # Check is IPoE configured
+ is_ipoe_configured()
+
+ if args.action == "restart":
+ run(cmd_dict['cmd_base'] + "restart")
+ sys.exit(0)
+
+ if args.action in cmd_dict['actions']:
+ if args.selector in cmd_dict['selector'] and args.target:
+ run(cmd_dict['cmd_base'] + "{0} {1} {2}".format(args.action, args.selector, args.target))
+ else:
+ output, err = popen(cmd_dict['cmd_base'] + cmd_dict['actions'][args.action], decode='utf-8')
+ if not err:
+ print(output)
+ else:
+ print("IPoE server is not running")
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py
new file mode 100755
index 000000000..06958c605
--- /dev/null
+++ b/src/op_mode/lldp_op.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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
+import json
+
+from sys import exit
+from tabulate import tabulate
+
+from vyos.util import cmd
+from vyos.config import Config
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces")
+parser.add_argument("-d", "--detail", action="store_true", help="Show detailes LLDP neighbor information on all interfaces")
+parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface")
+
+# Please be careful if you edit the template.
+lldp_out = """Capability Codes: R - Router, B - Bridge, W - Wlan r - Repeater, S - Station
+ D - Docsis, T - Telephone, O - Other
+
+Device ID Local Proto Cap Platform Port ID
+--------- ----- ----- --- -------- -------
+{% for neighbor in neighbors %}
+{% for local_if, info in neighbor.items() %}
+{{ "%-25s" | format(info.chassis) }} {{ "%-9s" | format(local_if) }} {{ "%-6s" | format(info.proto) }} {{ "%-5s" | format(info.capabilities) }} {{ "%-20s" | format(info.platform[:18]) }} {{ info.remote_if }}
+{% endfor %}
+{% endfor %}
+"""
+
+def get_neighbors():
+ return cmd('/usr/sbin/lldpcli -f json show neighbors')
+
+def parse_data(data):
+ output = []
+ for tmp in data:
+ for local_if, values in tmp.items():
+ for chassis, c_value in values.get('chassis', {}).items():
+ capabilities = c_value['capability']
+ if isinstance(capabilities, dict):
+ capabilities = [capabilities]
+
+ cap = ''
+ for capability in capabilities:
+ if capability['enabled']:
+ if capability['type'] == 'Router':
+ cap += 'R'
+ if capability['type'] == 'Bridge':
+ cap += 'B'
+ if capability['type'] == 'Wlan':
+ cap += 'W'
+ if capability['type'] == 'Station':
+ cap += 'S'
+ if capability['type'] == 'Repeater':
+ cap += 'r'
+ if capability['type'] == 'Telephone':
+ cap += 'T'
+ if capability['type'] == 'Docsis':
+ cap += 'D'
+ if capability['type'] == 'Other':
+ cap += 'O'
+
+
+ remote_if = 'Unknown'
+ if 'descr' in values.get('port', {}):
+ remote_if = values.get('port', {}).get('descr')
+ elif 'id' in values.get('port', {}):
+ remote_if = values.get('port', {}).get('id').get('value', 'Unknown')
+
+ output.append({local_if: {'chassis': chassis,
+ 'remote_if': remote_if,
+ 'proto': values.get('via','Unknown'),
+ 'platform': c_value.get('descr', 'Unknown'),
+ 'capabilities': cap}})
+
+
+ output = {'neighbors': output}
+ return output
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+ tmp = { 'neighbors' : [] }
+
+ c = Config()
+ if not c.exists_effective(['service', 'lldp']):
+ print('Service LLDP is not configured')
+ exit(0)
+
+ if args.detail:
+ print(cmd('/usr/sbin/lldpctl -f plain'))
+ exit(0)
+ elif args.all or args.interface:
+ tmp = json.loads(get_neighbors())
+
+ if args.all:
+ neighbors = tmp['lldp']['interface']
+ elif args.interface:
+ neighbors = []
+ for neighbor in tmp['lldp']['interface']:
+ if args.interface in neighbor:
+ neighbors.append(neighbor)
+
+ else:
+ parser.print_help()
+ exit(1)
+
+ tmpl = jinja2.Template(lldp_out, trim_blocks=True)
+ config_text = tmpl.render(parse_data(neighbors))
+ print(config_text)
+
+ exit(0)
diff --git a/src/op_mode/maya_date.py b/src/op_mode/maya_date.py
new file mode 100755
index 000000000..847b543e0
--- /dev/null
+++ b/src/op_mode/maya_date.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2013, 2018 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
+
+class MayaDate(object):
+ """ Converts number of days since UNIX epoch
+ to the Maya calendar date.
+
+ Ancient Maya people used three independent calendars for
+ different purposes.
+
+ The long count calendar is for recording historical events.
+ It represents the number of days passed
+ since some date in the past the Maya believed is the day
+ our world was created.
+
+ Tzolkin calendar is for religious purposes, it has
+ two independent cycles of 13 and 20 days, where 13 day
+ cycle days are numbered, and 20 day cycle days are named.
+
+ Haab calendar is for agriculture and daily life, it's a
+ 365 day calendar with 18 months 20 days each, and 5
+ nameless days.
+
+ The smallest unit of the long count calendar is one day (kin).
+ """
+
+ """ The long count calendar uses five different base 18 or base 20
+ cycles. Modern scholars write long count calendar dates in a dot separated format
+ from longest to shortest cycle,
+ <baktun>.<katun>.<tun>.<winal>.<kin>
+ for example, "13.0.0.9.2".
+
+ Classic version actually used by the ancient Maya wraps around
+ every 13th baktun, but modern historians often use longer cycles
+ such as piktun = 20 baktun.
+
+ """
+ kin = 1
+ winal = 20 # 20 kin
+ tun = 360 # 18 winal
+ katun = 7200 # 20 tun
+ baktun = 144000 # 20 katun
+
+ """ Tzolk'in date is composed of two independent cycles.
+ Dates repeat every 260 days, 13 Ajaw is considered the end
+ of tzolk'in.
+
+ Every day of the 20 day cycle has unique name, we number
+ them from zero so it's easier to map the remainder to day:
+ """
+ tzolkin_days = { 0: "Imix'",
+ 1: "Ik'",
+ 2: "Ak'b'al",
+ 3: "K'an",
+ 4: "Chikchan",
+ 5: "Kimi",
+ 6: "Manik'",
+ 7: "Lamat",
+ 8: "Muluk",
+ 9: "Ok",
+ 10: "Chuwen",
+ 11: "Eb'",
+ 12: "B'en",
+ 13: "Ix",
+ 14: "Men",
+ 15: "Kib'",
+ 16: "Kab'an",
+ 17: "Etz'nab'",
+ 18: "Kawak",
+ 19: "Ajaw" }
+
+ """ As said above, haab (year) has 19 months. Only 18 are
+ true months of 20 days each, the remaining 5 days called "wayeb"
+ do not really belong to any month, but we think of them as a pseudo-month
+ for convenience.
+
+ Also, note that days of the month are actually numbered from 0, not from 1,
+ it's not for technical reasons.
+ """
+ haab_months = { 0: "Pop",
+ 1: "Wo'",
+ 2: "Sip",
+ 3: "Sotz'",
+ 4: "Sek",
+ 5: "Xul",
+ 6: "Yaxk'in'",
+ 7: "Mol",
+ 8: "Ch'en",
+ 9: "Yax",
+ 10: "Sak'",
+ 11: "Keh",
+ 12: "Mak",
+ 13: "K'ank'in",
+ 14: "Muwan'",
+ 15: "Pax",
+ 16: "K'ayab",
+ 17: "Kumk'u",
+ 18: "Wayeb'" }
+
+ """ Now we need to map the beginning of UNIX epoch
+ (Jan 1 1970 00:00 UTC) to the beginning of the long count
+ calendar (0.0.0.0.0, 4 Ajaw, 8 Kumk'u).
+
+ The problem with mapping the long count calendar to
+ any other is that its start date is not known exactly.
+
+ The most widely accepted hypothesis suggests it was
+ August 11, 3114 BC gregorian date. In this case UNIX epoch
+ starts on 12.17.16.7.5, 13 Chikchan, 3 K'ank'in
+
+ It's known as Goodman-Martinez-Thompson (GMT) correlation
+ constant.
+ """
+ start_days = 1856305
+
+ """ Seconds in day, for conversion from timestamp """
+ seconds_in_day = 60 * 60 * 24
+
+ def __init__(self, timestamp):
+ if timestamp is None:
+ self.days = self.start_days
+ else:
+ self.days = self.start_days + (int(timestamp) // self.seconds_in_day)
+
+ def long_count_date(self):
+ """ Returns long count date string """
+ days = self.days
+
+ cur_baktun = days // self.baktun
+ days = days % self.baktun
+
+ cur_katun = days // self.katun
+ days = days % self.katun
+
+ cur_tun = days // self.tun
+ days = days % self.tun
+
+ cur_winal = days // self.winal
+ days = days % self.winal
+
+ cur_kin = days
+
+ longcount_string = "{0}.{1}.{2}.{3}.{4}".format( cur_baktun,
+ cur_katun,
+ cur_tun,
+ cur_winal,
+ cur_kin )
+ return(longcount_string)
+
+ def tzolkin_date(self):
+ """ Returns tzolkin date string """
+ days = self.days
+
+ """ The start date is not the beginning of both cycles,
+ it's 4 Ajaw. So we need to add 4 to the 13 days cycle day,
+ and substract 1 from the 20 day cycle to get correct result.
+ """
+ tzolkin_13 = (days + 4) % 13
+ tzolkin_20 = (days - 1) % 20
+
+ tzolkin_string = "{0} {1}".format(tzolkin_13, self.tzolkin_days[tzolkin_20])
+
+ return(tzolkin_string)
+
+ def haab_date(self):
+ """ Returns haab date string.
+
+ The time start on 8 Kumk'u rather than 0 Pop, which is
+ 17 days before the new haab, so we need to substract 17
+ from the current date to get correct result.
+ """
+ days = self.days
+
+ haab_day = (days - 17) % 365
+ haab_month = haab_day // 20
+ haab_day_of_month = haab_day % 20
+
+ haab_string = "{0} {1}".format(haab_day_of_month, self.haab_months[haab_month])
+
+ return(haab_string)
+
+ def date(self):
+ return("{0}, {1}, {2}".format( self.long_count_date(), self.tzolkin_date(), self.haab_date() ))
+
+if __name__ == '__main__':
+ try:
+ timestamp = sys.argv[1]
+ except:
+ print("Please specify timestamp in the argument")
+ sys.exit(1)
+
+ maya_date = MayaDate(timestamp)
+ print(maya_date.date())
diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py
new file mode 100755
index 000000000..29b430d53
--- /dev/null
+++ b/src/op_mode/ping.py
@@ -0,0 +1,230 @@
+#! /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 os
+import sys
+import socket
+import ipaddress
+
+options = {
+ 'audible': {
+ 'ping': '{command} -a',
+ 'type': 'noarg',
+ 'help': 'Make a noise on ping'
+ },
+ 'adaptive': {
+ 'ping': '{command} -A',
+ 'type': 'noarg',
+ 'help': 'Adativly set interpacket interval'
+ },
+ 'allow-broadcast': {
+ 'ping': '{command} -b',
+ 'type': 'noarg',
+ 'help': 'Ping broadcast address'
+ },
+ 'bypass-route': {
+ 'ping': '{command} -r',
+ 'type': 'noarg',
+ 'help': 'Bypass normal routing tables'
+ },
+ 'count': {
+ 'ping': '{command} -c {value}',
+ 'type': '<requests>',
+ 'help': 'Number of requests to send'
+ },
+ 'deadline': {
+ 'ping': '{command} -w {value}',
+ 'type': '<seconds>',
+ 'help': 'Number of seconds before ping exits'
+ },
+ 'flood': {
+ 'ping': 'sudo {command} -f',
+ 'type': 'noarg',
+ 'help': 'Send 100 requests per second'
+ },
+ 'interface': {
+ 'ping': '{command} -I {value}',
+ 'type': '<interface> <X.X.X.X> <h:h:h:h:h:h:h:h>',
+ 'help': 'Interface to use as source for ping'
+ },
+ 'interval': {
+ 'ping': '{command} -i {value}',
+ 'type': '<seconds>',
+ 'help': 'Number of seconds to wait between requests'
+ },
+ 'mark': {
+ 'ping': '{command} -m {value}',
+ 'type': '<fwmark>',
+ 'help': 'Mark request for special processing'
+ },
+ 'numeric': {
+ 'ping': '{command} -n',
+ 'type': 'noarg',
+ 'help': 'Do not resolve DNS names'
+ },
+ 'no-loopback': {
+ 'ping': '{command} -L',
+ 'type': 'noarg',
+ 'help': 'Supress loopback of multicast pings'
+ },
+ 'pattern': {
+ 'ping': '{command} -p {value}',
+ 'type': '<pattern>',
+ 'help': 'Pattern to fill out the packet'
+ },
+ 'timestamp': {
+ 'ping': '{command} -D',
+ 'type': 'noarg',
+ 'help': 'Print timestamp of output'
+ },
+ 'tos': {
+ 'ping': '{command} -Q {value}',
+ 'type': '<tos>',
+ 'help': 'Mark packets with specified TOS'
+ },
+ 'quiet': {
+ 'ping': '{command} -q',
+ 'type': 'noarg',
+ 'help': 'Only print summary lines'
+ },
+ 'record-route': {
+ 'ping': '{command} -R',
+ 'type': 'noarg',
+ 'help': 'Record route the packet takes'
+ },
+ 'size': {
+ 'ping': '{command} -s {value}',
+ 'type': '<bytes>',
+ 'help': 'Number of bytes to send'
+ },
+ 'ttl': {
+ 'ping': '{command} -t {value}',
+ 'type': '<ttl>',
+ 'help': 'Maximum packet lifetime'
+ },
+ 'vrf': {
+ 'ping': 'sudo ip vrf exec {value} {command}',
+ 'type': '<vrf>',
+ 'help': 'Use specified VRF table',
+ 'dflt': 'default',
+ },
+ 'verbose': {
+ 'ping': '{command} -v',
+ 'type': 'noarg',
+ 'help': 'Verbose output'}
+}
+
+ping = {
+ 4: '/bin/ping',
+ 6: '/bin/ping6',
+}
+
+
+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 expension_failure(option, completions):
+ reason = 'Ambiguous' if completions else 'Invalid'
+ 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))
+ sys.stderr.write('\n')
+ sys.stdout.write('<nocomps>')
+ sys.exit(1)
+
+
+def complete(prefix):
+ return [o for o in options if o.startswith(prefix)]
+
+
+def convert(command, args):
+ while args:
+ shortname = args.first()
+ longnames = complete(shortname)
+ if len(longnames) != 1:
+ expension_failure(shortname, longnames)
+ longname = longnames[0]
+ if options[longname]['type'] == 'noarg':
+ command = options[longname]['ping'].format(
+ command=command, value='')
+ elif not args:
+ sys.exit(f'ping: missing argument for {longname} option')
+ else:
+ command = options[longname]['ping'].format(
+ command=command, value=args.first())
+ return command
+
+
+if __name__ == '__main__':
+ args = List(sys.argv[1:])
+ host = args.first()
+
+ if not host:
+ sys.exit("ping: Missing host")
+
+ if host == '--get-options':
+ args.first() # pop ping
+ args.first() # pop IP
+ while args:
+ option = args.first()
+
+ matched = complete(option)
+ if not args:
+ sys.stdout.write(' '.join(matched))
+ sys.exit(0)
+
+ if len(matched) > 1 :
+ sys.stdout.write(' '.join(matched))
+ sys.exit(0)
+
+ if options[matched[0]]['type'] == 'noarg':
+ continue
+
+ value = args.first()
+ if not args:
+ matched = complete(option)
+ sys.stdout.write(options[matched[0]]['type'])
+ sys.exit(0)
+
+ for name,option in options.items():
+ if 'dflt' in option and name not in args:
+ args.append(name)
+ args.append(option['dflt'])
+
+ try:
+ ip = socket.gethostbyname(host)
+ except socket.gaierror:
+ ip = host
+
+ try:
+ version = ipaddress.ip_address(ip).version
+ except ValueError:
+ sys.exit(f'ping: Unknown host: {host}')
+
+ command = convert(ping[version],args)
+
+ # print(f'{command} {host}')
+ os.system(f'{command} {host}')
+
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
new file mode 100755
index 000000000..69af427ec
--- /dev/null
+++ b/src/op_mode/powerctrl.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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
+
+from argparse import ArgumentParser
+from datetime import datetime, timedelta, time as type_time, date as type_date
+from sys import exit
+from time import time
+
+from vyos.util import ask_yes_no, cmd, call, run, STDOUT
+
+systemd_sched_file = "/run/systemd/shutdown/scheduled"
+
+def utc2local(datetime):
+ now = time()
+ offs = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now)
+ return datetime + offs
+
+def parse_time(s):
+ try:
+ if re.match(r'^\d{1,2}$', s):
+ return datetime.strptime(s, "%M").time()
+ else:
+ return datetime.strptime(s, "%H:%M").time()
+ except ValueError:
+ return None
+
+
+def parse_date(s):
+ for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]:
+ try:
+ return datetime.strptime(s, fmt).date()
+ except ValueError:
+ continue
+ # If nothing matched...
+ return None
+
+
+def get_shutdown_status():
+ if os.path.exists(systemd_sched_file):
+ # Get scheduled from systemd file
+ with open(systemd_sched_file, 'r') as f:
+ data = f.read().rstrip('\n')
+ r_data = {}
+ for line in data.splitlines():
+ tmp_split = line.split("=")
+ if tmp_split[0] == "USEC":
+ # Convert USEC to human readable format
+ r_data['DATETIME'] = datetime.utcfromtimestamp(
+ int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S')
+ else:
+ r_data[tmp_split[0]] = tmp_split[1]
+ return r_data
+ return None
+
+
+def check_shutdown():
+ output = get_shutdown_status()
+ if output and 'MODE' in output:
+ dt = datetime.strptime(output['DATETIME'], '%Y-%m-%d %H:%M:%S')
+ if output['MODE'] == 'reboot':
+ print("Reboot is scheduled", utc2local(dt))
+ elif output['MODE'] == 'poweroff':
+ print("Poweroff is scheduled", utc2local(dt))
+ else:
+ print("Reboot or poweroff is not scheduled")
+
+
+def cancel_shutdown():
+ output = get_shutdown_status()
+ if output and 'MODE' in output:
+ timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ try:
+ run('/sbin/shutdown -c --no-wall')
+ except OSError as e:
+ exit("Could not cancel a reboot or poweroff: %s" % e)
+
+ message = 'Scheduled {} has been cancelled {}'.format(output['MODE'], timenow)
+ run(f'wall {message} > /dev/null 2>&1')
+ else:
+ print("Reboot or poweroff is not scheduled")
+
+
+def execute_shutdown(time, reboot=True, ask=True):
+ if not ask:
+ action = "reboot" if reboot else "poweroff"
+ if not ask_yes_no("Are you sure you want to %s this system?" % action):
+ exit(0)
+
+ action = "-r" if reboot else "-P"
+
+ if len(time) == 0:
+ # T870 legacy reboot job support
+ chk_vyatta_based_reboots()
+ ###
+
+ out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT)
+ print(out.split(",", 1)[0])
+ return
+ elif len(time) == 1:
+ # Assume the argument is just time
+ ts = parse_time(time[0])
+ if ts:
+ cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT)
+ else:
+ exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
+ elif len(time) == 2:
+ # Assume it's date and time
+ ts = parse_time(time[0])
+ ds = parse_date(time[1])
+ if ts and ds:
+ t = datetime.combine(ds, ts)
+ td = t - datetime.now()
+ t2 = 1 + int(td.total_seconds())//60 # Get total minutes
+ cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT)
+ else:
+ if not ts:
+ exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
+ else:
+ exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1]))
+ else:
+ exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM")
+ check_shutdown()
+
+
+def chk_vyatta_based_reboots():
+ # T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed
+ # legacy scheduled reboot s are using at and store the is as /var/run/<name>.job
+ # name is the node of scheduled the job, commit-confirm checks for that
+
+ f = r'/var/run/confirm.job'
+ if os.path.exists(f):
+ jid = open(f).read().strip()
+ if jid != 0:
+ call(f'sudo atrm {jid}')
+ os.remove(f)
+
+
+def main():
+ parser = ArgumentParser()
+ parser.add_argument("--yes", "-y",
+ help="Do not ask for confirmation",
+ action="store_true",
+ dest="yes")
+ action = parser.add_mutually_exclusive_group(required=True)
+ action.add_argument("--reboot", "-r",
+ help="Reboot the system",
+ nargs="*",
+ metavar="Minutes|HH:MM")
+
+ action.add_argument("--poweroff", "-p",
+ help="Poweroff the system",
+ nargs="*",
+ metavar="Minutes|HH:MM")
+
+ action.add_argument("--cancel", "-c",
+ help="Cancel pending shutdown",
+ action="store_true")
+
+ action.add_argument("--check",
+ help="Check pending chutdown",
+ action="store_true")
+ args = parser.parse_args()
+
+ try:
+ if args.reboot is not None:
+ execute_shutdown(args.reboot, reboot=True, ask=args.yes)
+ if args.poweroff is not None:
+ execute_shutdown(args.poweroff, reboot=False, ask=args.yes)
+ if args.cancel:
+ cancel_shutdown()
+ if args.check:
+ check_shutdown()
+ except KeyboardInterrupt:
+ exit("Interrupted")
+
+if __name__ == "__main__":
+ main()
diff --git a/src/op_mode/ppp-server-ctrl.py b/src/op_mode/ppp-server-ctrl.py
new file mode 100755
index 000000000..171107b4a
--- /dev/null
+++ b/src/op_mode/ppp-server-ctrl.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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 argparse
+
+from vyos.config import Config
+from vyos.util import popen, DEVNULL
+
+cmd_dict = {
+ 'cmd_base' : '/usr/bin/accel-cmd -p {} ',
+ 'vpn_types' : {
+ 'pppoe' : 2001,
+ 'pptp' : 2003,
+ 'l2tp' : 2004,
+ 'sstp' : 2005
+ },
+ 'conf_proto' : {
+ 'pppoe' : 'service pppoe-server',
+ 'pptp' : 'vpn pptp remote-access',
+ 'l2tp' : 'vpn l2tp remote-access',
+ 'sstp' : 'vpn sstp'
+ }
+}
+
+def is_service_configured(proto):
+ if not Config().exists_effective(cmd_dict['conf_proto'][proto]):
+ print("Service {} is not configured".format(proto))
+ sys.exit(1)
+
+def main():
+ #parese args
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--proto', help='Possible protocols pppoe|pptp|l2tp|sstp', required=True)
+ parser.add_argument('--action', help='Action command', required=True)
+ args = parser.parse_args()
+
+ if args.proto in cmd_dict['vpn_types'] and args.action:
+ # Check is service configured
+ is_service_configured(args.proto)
+
+ if args.action == "show sessions":
+ ses_pattern = " ifname,username,ip,ip6,ip6-dp,calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes"
+ else:
+ ses_pattern = ""
+
+ output, err = popen(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][args.proto]) + args.action + ses_pattern, stderr=DEVNULL, decode='utf-8')
+ if not err:
+ print(output)
+ else:
+ print("{} server is not running".format(args.proto))
+
+ else:
+ print("Param --proto and --action required")
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py
new file mode 100755
index 000000000..dbd3eb4d1
--- /dev/null
+++ b/src/op_mode/reset_openvpn.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-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 os
+from sys import argv, exit
+from vyos.util import call
+
+if __name__ == '__main__':
+ if (len(argv) < 1):
+ print('Must specify OpenVPN interface name!')
+ exit(1)
+
+ interface = argv[1]
+ if os.path.isfile(f'/run/openvpn/{interface}.conf'):
+ call(f'systemctl restart openvpn@{interface}.service')
+ else:
+ print(f'OpenVPN interface "{interface}" does not exist!')
+ exit(1)
diff --git a/src/op_mode/reset_vpn.py b/src/op_mode/reset_vpn.py
new file mode 100755
index 000000000..3a0ad941c
--- /dev/null
+++ b/src/op_mode/reset_vpn.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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 argparse
+
+from vyos.util import run
+
+cmd_dict = {
+ 'cmd_base' : '/usr/bin/accel-cmd -p {} terminate {} {}',
+ 'vpn_types' : {
+ 'pptp' : 2003,
+ 'l2tp' : 2004,
+ 'sstp' : 2005
+ }
+}
+
+def terminate_sessions(username='', interface='', protocol=''):
+
+ # Reset vpn connections by username
+ if protocol in cmd_dict['vpn_types']:
+ if username == "all_users":
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'all', ''))
+ else:
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'username', username))
+
+ # Reset vpn connections by ifname
+ elif interface:
+ for proto in cmd_dict['vpn_types']:
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'if', interface))
+
+ elif username:
+ # Reset all vpn connections
+ if username == "all_users":
+ for proto in cmd_dict['vpn_types']:
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'all', ''))
+ else:
+ for proto in cmd_dict['vpn_types']:
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'username', username))
+
+def main():
+ #parese args
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--username', help='Terminate by username (all_users used for disconnect all users)', required=False)
+ parser.add_argument('--interface', help='Terminate by interface', required=False)
+ parser.add_argument('--protocol', help='Set protocol (pptp|l2tp|sstp)', required=False)
+ args = parser.parse_args()
+
+ if args.username or args.interface:
+ terminate_sessions(username=args.username, interface=args.interface, protocol=args.protocol)
+ else:
+ print("Param --username or --interface required")
+ sys.exit(1)
+
+ terminate_sessions()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py
new file mode 100755
index 000000000..af4fb2d15
--- /dev/null
+++ b/src/op_mode/restart_dhcp_relay.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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/>.
+
+# File: restart_dhcp_relay.py
+# Purpose:
+# Restart IPv4 and IPv6 DHCP relay instances of dhcrelay service
+
+import sys
+import argparse
+import os
+
+import vyos.config
+from vyos.util import call
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--ipv4", action="store_true", help="Restart IPv4 DHCP relay")
+parser.add_argument("--ipv6", action="store_true", help="Restart IPv6 DHCP relay")
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+ c = vyos.config.Config()
+
+ if args.ipv4:
+ # Do nothing if service is not configured
+ if not c.exists_effective('service dhcp-relay'):
+ print("DHCP relay service not configured")
+ else:
+ call('systemctl restart isc-dhcp-server.service')
+
+ sys.exit(0)
+ elif args.ipv6:
+ # Do nothing if service is not configured
+ if not c.exists_effective('service dhcpv6-relay'):
+ print("DHCPv6 relay service not configured")
+ else:
+ call('systemctl restart isc-dhcp-server6.service')
+
+ sys.exit(0)
+ else:
+ parser.print_help()
+ sys.exit(1)
diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py
new file mode 100755
index 000000000..d1b66b33f
--- /dev/null
+++ b/src/op_mode/restart_frr.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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 argparse
+import logging
+from logging.handlers import SysLogHandler
+from pathlib import Path
+import psutil
+
+from vyos.util import call
+
+# some default values
+watchfrr = '/usr/lib/frr/watchfrr.sh'
+vtysh = '/usr/bin/vtysh'
+frrconfig_tmp = '/tmp/frr_restart'
+
+# configure logging
+logger = logging.getLogger(__name__)
+logs_handler = SysLogHandler('/dev/log')
+logs_handler.setFormatter(logging.Formatter('%(filename)s: %(message)s'))
+logger.addHandler(logs_handler)
+logger.setLevel(logging.INFO)
+
+# check if it is safe to restart FRR
+def _check_safety():
+ try:
+ # print warning
+ answer = input("WARNING: This is a potentially unsafe function! You may lose the connection to the router or active configuration after running this command. Use it at your own risk! Continue? [y/N]: ")
+ if not answer.lower() == "y":
+ logger.error("User aborted command")
+ return False
+
+ # check if another restart process already running
+ if len([process for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']) if 'python' in process.info['name'] and 'restart_frr.py' in process.info['cmdline'][1]]) > 1:
+ logger.error("Another restart_frr.py already running")
+ answer = input("Another restart_frr.py process is already running. It is unsafe to continue. Do you want to process anyway? [y/N]: ")
+ if not answer.lower() == "y":
+ return False
+
+ # check if watchfrr.sh is running
+ for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']):
+ if 'bash' in process.info['name'] and watchfrr in process.info['cmdline']:
+ logger.error("Another {} already running".format(watchfrr))
+ answer = input("Another {} process is already running. It is unsafe to continue. Do you want to process anyway? [y/N]: ".format(watchfrr))
+ if not answer.lower() == "y":
+ return False
+
+ # check if vtysh is running
+ for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']):
+ if 'vtysh' in process.info['name']:
+ logger.error("The vtysh is running by another task")
+ answer = input("The vtysh is running by another task. It is unsafe to continue. Do you want to process anyway? [y/N]: ")
+ if not answer.lower() == "y":
+ return False
+
+ # check if temporary directory exists
+ if Path(frrconfig_tmp).exists():
+ logger.error("The temporary directory \"{}\" already exists".format(frrconfig_tmp))
+ answer = input("The temporary directory \"{}\" already exists. It is unsafe to continue. Do you want to process anyway? [y/N]: ".format(frrconfig_tmp))
+ if not answer.lower() == "y":
+ return False
+ except:
+ logger.error("Something goes wrong in _check_safety()")
+ return False
+
+ # return True if all check was passed or user confirmed to ignore they results
+ return True
+
+# write active config to file
+def _write_config():
+ # create temporary directory
+ Path(frrconfig_tmp).mkdir(parents=False, exist_ok=True)
+ # save frr.conf to it
+ command = "{} -n -w --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp)
+ return_code = call(command)
+ if not return_code == 0:
+ logger.error("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code))
+ return False
+ logger.info("Active config saved to {}".format(frrconfig_tmp))
+ return True
+
+# clear and remove temporary directory
+def _cleanup():
+ tmpdir = Path(frrconfig_tmp)
+ try:
+ if tmpdir.exists():
+ for file in tmpdir.iterdir():
+ file.unlink()
+ tmpdir.rmdir()
+ except:
+ logger.error("Failed to remove temporary directory {}".format(frrconfig_tmp))
+ print("Failed to remove temporary directory {}".format(frrconfig_tmp))
+
+# check if daemon is running
+def _daemon_check(daemon):
+ command = "{} print_status {}".format(watchfrr, daemon)
+ return_code = call(command)
+ if not return_code == 0:
+ logger.error("Daemon \"{}\" is not running".format(daemon))
+ return False
+
+ # return True if all checks were passed
+ return True
+
+# restart daemon
+def _daemon_restart(daemon):
+ command = "{} restart {}".format(watchfrr, daemon)
+ return_code = call(command)
+ if not return_code == 0:
+ logger.error("Failed to restart daemon \"{}\"".format(daemon))
+ return False
+
+ # return True if restarted successfully
+ logger.info("Daemon \"{}\" restarted".format(daemon))
+ return True
+
+# reload old config
+def _reload_config(daemon):
+ if daemon != '':
+ command = "{} -n -b --config_dir {} -d {} 2> /dev/null".format(vtysh, frrconfig_tmp, daemon)
+ else:
+ command = "{} -n -b --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp)
+
+ return_code = call(command)
+ if not return_code == 0:
+ logger.error("Failed to reinstall configuration")
+ return False
+
+ # return True if restarted successfully
+ logger.info("Configuration reinstalled successfully")
+ return True
+
+# check all daemons if they are running
+def _check_args_daemon(daemons):
+ for daemon in daemons:
+ if not _daemon_check(daemon):
+ return False
+ return True
+
+# define program arguments
+cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons')
+cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons')
+cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons')
+# parse arguments
+cmd_args = cmd_args_parser.parse_args()
+
+
+# main logic
+# restart daemon
+if cmd_args.action == 'restart':
+ # check if it is safe to restart FRR
+ if not _check_safety():
+ print("\nOne of the safety checks was failed or user aborted command. Exiting.")
+ sys.exit(1)
+
+ if not _write_config():
+ print("Failed to save active config")
+ _cleanup()
+ sys.exit(1)
+
+ # a little trick to make further commands more clear
+ if not cmd_args.daemon:
+ cmd_args.daemon = ['']
+
+ # check all daemons if they are running
+ if cmd_args.daemon != ['']:
+ if not _check_args_daemon(cmd_args.daemon):
+ print("Warning: some of listed daemons are not running")
+
+ # run command to restart daemon
+ for daemon in cmd_args.daemon:
+ if not _daemon_restart(daemon):
+ print("Failed to restart daemon: {}".format(daemon))
+ _cleanup()
+ sys.exit(1)
+ # reinstall old configuration
+ _reload_config(daemon)
+
+ # cleanup after all actions
+ _cleanup()
+
+sys.exit(0)
diff --git a/src/op_mode/show_acceleration.py b/src/op_mode/show_acceleration.py
new file mode 100755
index 000000000..752db3deb
--- /dev/null
+++ b/src/op_mode/show_acceleration.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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 os
+import re
+import argparse
+
+from vyos.config import Config
+from vyos.util import popen
+from vyos.util import call
+
+
+def detect_qat_dev():
+ output, err = popen('sudo lspci -nn', decode='utf-8')
+ if not err:
+ data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output)
+ #If QAT devices found
+ if data:
+ return
+ print("\t No QAT device found")
+ sys.exit(1)
+
+def show_qat_status():
+ detect_qat_dev()
+
+ # Check QAT service
+ if not os.path.exists('/etc/init.d/qat_service'):
+ print("\t QAT service not installed")
+ sys.exit(1)
+
+ # Show QAT service
+ call('sudo /etc/init.d/qat_service status')
+
+# Return QAT devices
+def get_qat_devices():
+ data_st, err = popen('sudo /etc/init.d/qat_service status', decode='utf-8')
+ if not err:
+ elm_lst = re.findall('qat_dev\d', data_st)
+ print('\n'.join(elm_lst))
+
+# Return QAT path in sysfs
+def get_qat_proc_path(qat_dev):
+ q_type = ""
+ q_bsf = ""
+ output, err = popen('sudo /etc/init.d/qat_service status', decode='utf-8')
+ if not err:
+ # Parse QAT service output
+ data_st = output.split("\n")
+ for elm_str in range(len(data_st)):
+ if re.search(qat_dev, data_st[elm_str]):
+ elm_list = data_st[elm_str].split(", ")
+ for elm in range(len(elm_list)):
+ if re.search('type', elm_list[elm]):
+ q_list = elm_list[elm].split(": ")
+ q_type=q_list[1]
+ elif re.search('bsf', elm_list[elm]):
+ q_list = elm_list[elm].split(": ")
+ q_bsf = q_list[1]
+ return "/sys/kernel/debug/qat_"+q_type+"_"+q_bsf+"/"
+
+# Check if QAT service confgured
+def check_qat_if_conf():
+ if not Config().exists_effective('system acceleration qat'):
+ print("\t system acceleration qat is not configured")
+ sys.exit(1)
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("--hw", action="store_true", help="Show Intel QAT HW")
+group.add_argument("--dev_list", action="store_true", help="Return Intel QAT devices")
+group.add_argument("--flow", action="store_true", help="Show Intel QAT flows")
+group.add_argument("--interrupts", action="store_true", help="Show Intel QAT interrupts")
+group.add_argument("--status", action="store_true", help="Show Intel QAT status")
+group.add_argument("--conf", action="store_true", help="Show Intel QAT configuration")
+
+parser.add_argument("--dev", type=str, help="Selected QAT device")
+
+args = parser.parse_args()
+
+if args.hw:
+ detect_qat_dev()
+ # Show availible Intel QAT devices
+ call('sudo lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'')
+elif args.flow and args.dev:
+ check_qat_if_conf()
+ call('sudo cat '+get_qat_proc_path(args.dev)+"fw_counters")
+elif args.interrupts:
+ check_qat_if_conf()
+ # Delete _dev from args.dev
+ call('sudo cat /proc/interrupts | grep qat')
+elif args.status:
+ check_qat_if_conf()
+ show_qat_status()
+elif args.conf and args.dev:
+ check_qat_if_conf()
+ call('sudo cat '+get_qat_proc_path(args.dev)+"dev_cfg")
+elif args.dev_list:
+ get_qat_devices()
+else:
+ parser.print_help()
+ sys.exit(1)
diff --git a/src/op_mode/show_configuration_files.sh b/src/op_mode/show_configuration_files.sh
new file mode 100755
index 000000000..ad8e0747c
--- /dev/null
+++ b/src/op_mode/show_configuration_files.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# Wrapper script for the show configuration files command
+find ${vyatta_sysconfdir}/config/ \
+ -type f \
+ -not -name ".*" \
+ -not -name "config.boot.*" \
+ -printf "%f\t(%Tc)\t%T@\n" \
+ | sort -r -k3 \
+ | awk -F"\t" '{printf ("%-20s\t%s\n", $1,$2) ;}'
diff --git a/src/op_mode/show_cpu.py b/src/op_mode/show_cpu.py
new file mode 100755
index 000000000..0a540da1d
--- /dev/null
+++ b/src/op_mode/show_cpu.py
@@ -0,0 +1,61 @@
+#!/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 %}
+"""
+
+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']
+
+if len(cpu) > 0:
+ tmp = { 'cpu':cpu }
+ tmpl = Template(OUT_TMPL_SRC)
+ print(tmpl.render(tmp))
+ exit(0)
+else:
+ print('CPU information could not be determined\n')
+ exit(1)
diff --git a/src/op_mode/show_current_user.sh b/src/op_mode/show_current_user.sh
new file mode 100755
index 000000000..93e6efa61
--- /dev/null
+++ b/src/op_mode/show_current_user.sh
@@ -0,0 +1,18 @@
+#! /bin/bash
+
+echo -n "login : " ; who -m
+
+if [ -n "$VYATTA_USER_LEVEL_DIR" ]
+then
+ echo -n "level : "
+ basename $VYATTA_USER_LEVEL_DIR
+fi
+
+echo -n "user : " ; id -un
+echo -n "groups : " ; id -Gn
+
+if id -Z >/dev/null 2>&1
+then
+ echo -n "context : "
+ id -Z
+fi
diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py
new file mode 100755
index 000000000..ff1e3cc56
--- /dev/null
+++ b/src/op_mode/show_dhcp.py
@@ -0,0 +1,264 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-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/>.
+#
+# TODO: merge with show_dhcpv6.py
+
+from json import dumps
+from argparse import ArgumentParser
+from ipaddress import ip_address
+from tabulate import tabulate
+from sys import exit
+from collections import OrderedDict
+from datetime import datetime
+
+from isc_dhcp_leases import Lease, IscDhcpLeases
+
+from vyos.config import Config
+from vyos.util import call
+
+
+lease_file = "/config/dhcpd.leases"
+pool_key = "shared-networkname"
+
+lease_display_fields = OrderedDict()
+lease_display_fields['ip'] = 'IP address'
+lease_display_fields['hardware_address'] = 'Hardware address'
+lease_display_fields['state'] = 'State'
+lease_display_fields['start'] = 'Lease start'
+lease_display_fields['end'] = 'Lease expiration'
+lease_display_fields['remaining'] = 'Remaining'
+lease_display_fields['pool'] = 'Pool'
+lease_display_fields['hostname'] = 'Hostname'
+
+lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup']
+
+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((utc_dt - datetime(1970,1,1)).total_seconds())
+
+def get_lease_data(lease):
+ data = {}
+
+ # isc-dhcp lease times are in UTC so we need to convert them to local time to display
+ try:
+ data["start"] = utc_to_local(lease.start).strftime("%Y/%m/%d %H:%M:%S")
+ except:
+ data["start"] = ""
+
+ try:
+ data["end"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S")
+ except:
+ data["end"] = ""
+
+ try:
+ data["remaining"] = lease.end - datetime.utcnow()
+ # negative timedelta prints wrong so bypass it
+ if (data["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["remaining"] = str(data["remaining"]).split('.')[0]
+ else:
+ data["remaining"] = ""
+ except:
+ data["remaining"] = ""
+
+ # currently not used but might come in handy
+ # todo: parse into datetime string
+ for prop in ['tstp', 'tsfp', 'atsfp', 'cltt']:
+ if prop in lease.data:
+ data[prop] = lease.data[prop]
+ else:
+ data[prop] = ''
+
+ data["hardware_address"] = lease.ethernet
+ data["hostname"] = lease.hostname
+
+ data["state"] = lease.binding_state
+ data["ip"] = lease.ip
+
+ try:
+ data["pool"] = lease.sets[pool_key]
+ except:
+ data["pool"] = ""
+
+ return data
+
+def get_leases(config, leases, state, pool=None, sort='ip'):
+ # get leases from file
+ leases = IscDhcpLeases(lease_file).get()
+
+ # filter leases by state
+ if 'all' not in state:
+ leases = list(filter(lambda x: x.binding_state in state, leases))
+
+ # filter leases by pool name
+ if pool is not None:
+ if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)):
+ leases = list(filter(lambda x: in_pool(x, pool), leases))
+ else:
+ print("Pool {0} does not exist.".format(pool))
+ exit(0)
+
+ # should maybe filter all state=active by lease.valid here?
+
+ # sort by start time to dedupe (newest lease overrides older)
+ leases = sorted(leases, key = lambda lease: lease.start)
+
+ # dedupe by converting to dict
+ leases_dict = {}
+ for lease in leases:
+ # dedupe by IP
+ leases_dict[lease.ip] = lease
+
+ # convert the lease data
+ leases = list(map(get_lease_data, leases_dict.values()))
+
+ # apply output/display sort
+ if sort == 'ip':
+ leases = sorted(leases, key = lambda lease: int(ip_address(lease['ip'])))
+ else:
+ leases = sorted(leases, key = lambda lease: lease[sort])
+
+ return leases
+
+def show_leases(leases):
+ lease_list = []
+ for l in leases:
+ lease_list_params = []
+ for k in lease_display_fields.keys():
+ lease_list_params.append(l[k])
+ lease_list.append(lease_list_params)
+
+ output = tabulate(lease_list, lease_display_fields.values())
+
+ print(output)
+
+def get_pool_size(config, pool):
+ size = 0
+ subnets = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet".format(pool))
+ for s in subnets:
+ ranges = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet {1} range".format(pool, s))
+ for r in ranges:
+ start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r))
+ stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r))
+
+ # Add +1 because both range boundaries are inclusive
+ size += int(ip_address(stop)) - int(ip_address(start)) + 1
+
+ return size
+
+def show_pool_stats(stats):
+ headers = ["Pool", "Size", "Leases", "Available", "Usage"]
+ output = tabulate(stats, headers)
+
+ print(output)
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases")
+ group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics")
+ group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument")
+
+ parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool")
+ parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by")
+ parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)")
+ parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output")
+
+ args = parser.parse_args()
+
+ conf = Config()
+
+ if args.allowed == 'pool':
+ if conf.exists_effective('service dhcp-server'):
+ print(' '.join(conf.list_effective_nodes("service dhcp-server shared-network-name")))
+ exit(0)
+ elif args.allowed == 'sort':
+ print(' '.join(lease_display_fields.keys()))
+ exit(0)
+ elif args.allowed == 'state':
+ print(' '.join(lease_valid_states))
+ exit(0)
+ elif args.allowed:
+ parser.print_help()
+ exit(1)
+
+ if args.sort not in lease_display_fields.keys():
+ print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}')
+ exit(0)
+
+ if not set(args.state) < set(lease_valid_states):
+ print(f'Invalid lease state, choose from: {lease_valid_states}')
+ exit(0)
+
+ # Do nothing if service is not configured
+ if not conf.exists_effective('service dhcp-server'):
+ print("DHCP service is not configured.")
+ exit(0)
+
+ # if dhcp server is down, inactive leases may still be shown as active, so warn the user.
+ if call('systemctl -q is-active isc-dhcp-server.service') != 0:
+ print("WARNING: DHCP server is configured but not started. Data may be stale.")
+
+ if args.leases:
+ leases = get_leases(conf, lease_file, args.state, args.pool, args.sort)
+
+ if args.json:
+ print(dumps(leases, indent=4))
+ else:
+ show_leases(leases)
+
+ elif args.statistics:
+ pools = []
+
+ # Get relevant pools
+ if args.pool:
+ pools = [args.pool]
+ else:
+ pools = conf.list_effective_nodes("service dhcp-server shared-network-name")
+
+ # Get pool usage stats
+ stats = []
+ for p in pools:
+ size = get_pool_size(conf, p)
+ leases = len(get_leases(conf, lease_file, state='active', pool=p))
+
+ use_percentage = round(leases / size * 100) if size != 0 else 0
+
+ if args.json:
+ pool_stats = {"pool": p, "size": size, "leases": leases,
+ "available": (size - leases), "percentage": use_percentage}
+ else:
+ # For tabulate
+ pool_stats = [p, size, leases, size - leases, "{0}%".format(use_percentage)]
+ stats.append(pool_stats)
+
+ # Print stats
+ if args.json:
+ print(dumps(stats, indent=4))
+ else:
+ show_pool_stats(stats)
+
+ else:
+ parser.print_help()
+ exit(1)
diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py
new file mode 100755
index 000000000..ac211fb0a
--- /dev/null
+++ b/src/op_mode/show_dhcpv6.py
@@ -0,0 +1,219 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-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/>.
+#
+# TODO: merge with show_dhcp.py
+
+from json import dumps
+from argparse import ArgumentParser
+from ipaddress import ip_address
+from tabulate import tabulate
+from sys import exit
+from collections import OrderedDict
+from datetime import datetime
+
+from isc_dhcp_leases import Lease, IscDhcpLeases
+
+from vyos.config import Config
+from vyos.util import call
+
+lease_file = "/config/dhcpdv6.leases"
+pool_key = "shared-networkname"
+
+lease_display_fields = OrderedDict()
+lease_display_fields['ip'] = 'IPv6 address'
+lease_display_fields['state'] = 'State'
+lease_display_fields['last_comm'] = 'Last communication'
+lease_display_fields['expires'] = 'Lease expiration'
+lease_display_fields['remaining'] = 'Remaining'
+lease_display_fields['type'] = 'Type'
+lease_display_fields['pool'] = 'Pool'
+lease_display_fields['iaid_duid'] = 'IAID_DUID'
+
+lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup']
+
+def in_pool(lease, pool):
+ if pool_key in lease.sets:
+ if lease.sets[pool_key] == pool:
+ return True
+
+ return False
+
+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 utc_to_local(utc_dt):
+ return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds())
+
+def get_lease_data(lease):
+ data = {}
+
+ # isc-dhcp lease times are in UTC so we need to convert them to local time to display
+ try:
+ data["expires"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S")
+ except:
+ data["expires"] = ""
+
+ try:
+ data["last_comm"] = utc_to_local(lease.last_communication).strftime("%Y/%m/%d %H:%M:%S")
+ except:
+ data["last_comm"] = ""
+
+ try:
+ data["remaining"] = lease.end - datetime.utcnow()
+ # negative timedelta prints wrong so bypass it
+ if (data["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["remaining"] = str(data["remaining"]).split('.')[0]
+ else:
+ data["remaining"] = ""
+ except:
+ data["remaining"] = ""
+
+ # isc-dhcp records lease declarations as ia_{na|ta|pd} IAID_DUID {...}
+ # where IAID_DUID is the combined IAID and DUID
+ data["iaid_duid"] = format_hex_string(lease.host_identifier_string)
+
+ lease_types_long = {"na": "non-temporary", "ta": "temporary", "pd": "prefix delegation"}
+ data["type"] = lease_types_long[lease.type]
+
+ data["state"] = lease.binding_state
+ data["ip"] = lease.ip
+
+ try:
+ data["pool"] = lease.sets[pool_key]
+ except:
+ data["pool"] = ""
+
+ return data
+
+def get_leases(config, leases, state, pool=None, sort='ip'):
+ leases = IscDhcpLeases(lease_file).get()
+
+ # filter leases by state
+ if 'all' not in state:
+ leases = list(filter(lambda x: x.binding_state in state, leases))
+
+ # filter leases by pool name
+ if pool is not None:
+ if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)):
+ leases = list(filter(lambda x: in_pool(x, pool), leases))
+ else:
+ print("Pool {0} does not exist.".format(pool))
+ exit(0)
+
+ # should maybe filter all state=active by lease.valid here?
+
+ # sort by last_comm time to dedupe (newest lease overrides older)
+ leases = sorted(leases, key = lambda lease: lease.last_communication)
+
+ # dedupe by converting to dict
+ leases_dict = {}
+ for lease in leases:
+ # dedupe by IP
+ leases_dict[lease.ip] = lease
+
+ # convert the lease data
+ leases = list(map(get_lease_data, leases_dict.values()))
+
+ # apply output/display sort
+ if sort == 'ip':
+ leases = sorted(leases, key = lambda k: int(ip_address(k['ip'])))
+ else:
+ leases = sorted(leases, key = lambda k: k[sort])
+
+ return leases
+
+def show_leases(leases):
+ lease_list = []
+ for l in leases:
+ lease_list_params = []
+ for k in lease_display_fields.keys():
+ lease_list_params.append(l[k])
+ lease_list.append(lease_list_params)
+
+ output = tabulate(lease_list, lease_display_fields.values())
+
+ print(output)
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument("-l", "--leases", action="store_true", help="Show DHCPv6 leases")
+ group.add_argument("-s", "--statistics", action="store_true", help="Show DHCPv6 statistics")
+ group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument")
+
+ parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool")
+ parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by")
+ parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)")
+ parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output")
+
+ args = parser.parse_args()
+
+ conf = Config()
+
+ if args.allowed == 'pool':
+ if conf.exists_effective('service dhcpv6-server'):
+ print(' '.join(conf.list_effective_nodes("service dhcpv6-server shared-network-name")))
+ exit(0)
+ elif args.allowed == 'sort':
+ print(' '.join(lease_display_fields.keys()))
+ exit(0)
+ elif args.allowed == 'state':
+ print(' '.join(lease_valid_states))
+ exit(0)
+ elif args.allowed:
+ parser.print_help()
+ exit(1)
+
+ if args.sort not in lease_display_fields.keys():
+ print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}')
+ exit(0)
+
+ if not set(args.state) < set(lease_valid_states):
+ print(f'Invalid lease state, choose from: {lease_valid_states}')
+ exit(0)
+
+ # Do nothing if service is not configured
+ if not conf.exists_effective('service dhcpv6-server'):
+ print("DHCPv6 service is not configured")
+ exit(0)
+
+ # if dhcp server is down, inactive leases may still be shown as active, so warn the user.
+ if call('systemctl -q is-active isc-dhcp-server6.service') != 0:
+ print("WARNING: DHCPv6 server is configured but not started. Data may be stale.")
+
+ if args.leases:
+ leases = get_leases(conf, lease_file, args.state, args.pool, args.sort)
+
+ if args.json:
+ print(dumps(leases, indent=4))
+ else:
+ show_leases(leases)
+ elif args.statistics:
+ print("DHCPv6 statistics option is not available")
+ else:
+ parser.print_help()
+ exit(1)
diff --git a/src/op_mode/show_disk_format.sh b/src/op_mode/show_disk_format.sh
new file mode 100755
index 000000000..61b15a52b
--- /dev/null
+++ b/src/op_mode/show_disk_format.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+disk_dev="/dev/$1"
+if [ ! -b "$disk_dev" ];then
+ echo "$3 is not a disk device"
+ exit 1
+fi
+sudo /sbin/fdisk -l "$disk_dev"
diff --git a/src/op_mode/show_igmpproxy.py b/src/op_mode/show_igmpproxy.py
new file mode 100755
index 000000000..5ccc16287
--- /dev/null
+++ b/src/op_mode/show_igmpproxy.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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/>.
+
+# File: show_igmpproxy.py
+# Purpose:
+# Display istatistics from IPv4 IGMP proxy.
+# Used by the "run show ip multicast" command tree.
+
+import sys
+import jinja2
+import argparse
+import ipaddress
+import socket
+
+import vyos.config
+
+# Output Template for "show ip multicast interface" command
+#
+# Example:
+# Interface BytesIn PktsIn BytesOut PktsOut Local
+# eth0 0.0b 0 0.0b 0 xxx.xxx.xxx.65
+# eth1 0.0b 0 0.0b 0 xxx.xxx.xx.201
+# eth0.3 0.0b 0 0.0b 0 xxx.xxx.x.7
+# tun1 0.0b 0 0.0b 0 xxx.xxx.xxx.2
+vif_out_tmpl = """
+{%- for r in data %}
+{{ "%-10s"|format(r.interface) }} {{ "%-12s"|format(r.bytes_in) }} {{ "%-12s"|format(r.pkts_in) }} {{ "%-12s"|format(r.bytes_out) }} {{ "%-12s"|format(r.pkts_out) }} {{ "%-15s"|format(r.loc) }}
+{%- endfor %}
+"""
+
+# Output Template for "show ip multicast mfc" command
+#
+# Example:
+# Group Origin In Out Pkts Bytes Wrong
+# xxx.xxx.xxx.250 xxx.xx.xxx.75 --
+# xxx.xxx.xx.124 xx.xxx.xxx.26 --
+mfc_out_tmpl = """
+{%- for r in data %}
+{{ "%-15s"|format(r.group) }} {{ "%-15s"|format(r.origin) }} {{ "%-12s"|format(r.pkts) }} {{ "%-12s"|format(r.bytes) }} {{ "%-12s"|format(r.wrong) }} {{ "%-10s"|format(r.iif) }} {{ "%-20s"|format(r.oifs|join(', ')) }}
+{%- endfor %}
+"""
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--interface", action="store_true", help="Interface Statistics")
+parser.add_argument("--mfc", action="store_true", help="Multicast Forwarding Cache")
+
+def byte_string(size):
+ # convert size to integer
+ size = int(size)
+
+ # One Terrabyte
+ s_TB = 1024 * 1024 * 1024 * 1024
+ # One Gigabyte
+ s_GB = 1024 * 1024 * 1024
+ # One Megabyte
+ s_MB = 1024 * 1024
+ # One Kilobyte
+ s_KB = 1024
+ # One Byte
+ s_B = 1
+
+ if size > s_TB:
+ return str(round((size/s_TB), 2)) + 'TB'
+ elif size > s_GB:
+ return str(round((size/s_GB), 2)) + 'GB'
+ elif size > s_MB:
+ return str(round((size/s_MB), 2)) + 'MB'
+ elif size > s_KB:
+ return str(round((size/s_KB), 2)) + 'KB'
+ else:
+ return str(round((size/s_B), 2)) + 'b'
+
+ return None
+
+def kernel2ip(addr):
+ """
+ Convert any given addr from Linux Kernel to a proper, IPv4 address
+ using the correct host byte order.
+ """
+
+ # Convert from hex 'FE000A0A' to decimal '4261415434'
+ addr = int(addr, 16)
+ # Kernel ABI _always_ uses network byteorder
+ addr = socket.ntohl(addr)
+
+ return ipaddress.IPv4Address( addr )
+
+def do_mr_vif():
+ """
+ Read contents of file /proc/net/ip_mr_vif and print a more human
+ friendly version to the command line. IPv4 addresses present as
+ 32bit integers in hex format are converted to IPv4 notation, too.
+ """
+
+ with open('/proc/net/ip_mr_vif', 'r') as f:
+ lines = len(f.readlines())
+ if lines < 2:
+ return None
+
+ result = {
+ 'data': []
+ }
+
+ # Build up table format string
+ table_format = {
+ 'interface': 'Interface',
+ 'pkts_in' : 'PktsIn',
+ 'pkts_out' : 'PktsOut',
+ 'bytes_in' : 'BytesIn',
+ 'bytes_out': 'BytesOut',
+ 'loc' : 'Local'
+ }
+ result['data'].append(table_format)
+
+ # read and parse information from /proc filesystema
+ with open('/proc/net/ip_mr_vif', 'r') as f:
+ header_line = next(f)
+ for line in f:
+ data = {
+ 'interface': line.split()[1],
+ 'pkts_in' : line.split()[3],
+ 'pkts_out' : line.split()[5],
+
+ # convert raw byte number to something more human readable
+ # Note: could be replaced by Python3 hurry.filesize module
+ 'bytes_in' : byte_string( line.split()[2] ),
+ 'bytes_out': byte_string( line.split()[4] ),
+
+ # convert IP address from hex 'FE000A0A' to decimal '4261415434'
+ 'loc' : kernel2ip( line.split()[7] ),
+ }
+ result['data'].append(data)
+
+ return result
+
+def do_mr_mfc():
+ """
+ Read contents of file /proc/net/ip_mr_cache and print a more human
+ friendly version to the command line. IPv4 addresses present as
+ 32bit integers in hex format are converted to IPv4 notation, too.
+ """
+
+ with open('/proc/net/ip_mr_cache', 'r') as f:
+ lines = len(f.readlines())
+ if lines < 2:
+ return None
+
+ # We need this to convert from interface index to a real interface name
+ # Thus we also skip the format identifier on list index 0
+ vif = do_mr_vif()['data'][1:]
+
+ result = {
+ 'data': []
+ }
+
+ # Build up table format string
+ table_format = {
+ 'group' : 'Group',
+ 'origin': 'Origin',
+ 'iif' : 'In',
+ 'oifs' : ['Out'],
+ 'pkts' : 'Pkts',
+ 'bytes' : 'Bytes',
+ 'wrong' : 'Wrong'
+ }
+ result['data'].append(table_format)
+
+ # read and parse information from /proc filesystem
+ with open('/proc/net/ip_mr_cache', 'r') as f:
+ header_line = next(f)
+ for line in f:
+ data = {
+ # convert IP address from hex 'FE000A0A' to decimal '4261415434'
+ 'group' : kernel2ip( line.split()[0] ),
+ 'origin': kernel2ip( line.split()[1] ),
+
+ 'iif' : '--',
+ 'pkts' : '',
+ 'bytes' : '',
+ 'wrong' : '',
+ 'oifs' : []
+ }
+
+ iif = int( line.split()[2] )
+ if not ((iif == -1) or (iif == 65535)):
+ data['pkts'] = line.split()[3]
+ data['bytes'] = byte_string( line.split()[4] )
+ data['wrong'] = line.split()[5]
+
+ # convert index to real interface name
+ data['iif'] = vif[iif]['interface']
+
+ # convert each output interface index to a real interface name
+ for oif in line.split()[6:]:
+ idx = int( oif.split(':')[0] )
+ data['oifs'].append( vif[idx]['interface'] )
+
+ result['data'].append(data)
+
+ return result
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = vyos.config.Config()
+ if not c.exists_effective('protocols igmp-proxy'):
+ print("IGMP proxy is not configured")
+ sys.exit(0)
+
+ if args.interface:
+ data = do_mr_vif()
+ if data:
+ tmpl = jinja2.Template(vif_out_tmpl)
+ print(tmpl.render(data))
+
+ sys.exit(0)
+ elif args.mfc:
+ data = do_mr_mfc()
+ if data:
+ tmpl = jinja2.Template(mfc_out_tmpl)
+ print(tmpl.render(data))
+
+ sys.exit(0)
+ else:
+ parser.print_help()
+ sys.exit(1)
+
diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py
new file mode 100755
index 000000000..d4dae3cd1
--- /dev/null
+++ b/src/op_mode/show_interfaces.py
@@ -0,0 +1,304 @@
+#!/usr/bin/env python3
+
+# Copyright 2017, 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import sys
+import glob
+import datetime
+import argparse
+import netifaces
+
+from vyos.ifconfig import Section
+from vyos.ifconfig import Interface
+from vyos.ifconfig import VRRP
+from vyos.util import cmd
+
+
+# interfaces = Sections.reserved()
+interfaces = ['eno', 'ens', 'enp', 'enx', 'eth', 'vmnet', 'lo', 'tun', 'wan', 'pppoe', 'pppoa', 'adsl']
+glob_ifnames = '/sys/class/net/({})*'.format('|'.join(interfaces))
+
+
+actions = {}
+def register (name):
+ """
+ decorator to register a function into actions with a name
+ it allows to use actions[name] to call the registered function
+ """
+ def _register(function):
+ actions[name] = function
+ return function
+ return _register
+
+
+def filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ """
+ get all the interfaces from the OS and returns them
+ ifnames can be used to filter which interfaces should be considered
+
+ ifnames: a list of interfaces names to consider, empty do not filter
+ return an instance of the interface class
+ """
+ allnames = Section.interfaces()
+
+ vrrp_interfaces = VRRP.active_interfaces() if vrrp else []
+
+ for ifname in allnames:
+ if ifnames and ifname not in ifnames:
+ continue
+
+ # return the class which can handle this interface name
+ klass = Section.klass(ifname)
+ # connect to the interface
+ interface = klass(ifname, create=False, debug=False)
+
+ if iftypes and interface.definition['section'] not in iftypes:
+ continue
+
+ if vif and not '.' in ifname:
+ continue
+
+ if vrrp and ifname not in vrrp_interfaces:
+ continue
+
+ yield interface
+
+
+def split_text(text, used=0):
+ """
+ take a string and attempt to split it to fit with the width of the screen
+
+ text: the string to split
+ used: number of characted already used in the screen
+ """
+ returned = cmd('stty size')
+ if len(returned) == 2:
+ rows, columns = [int(_) for _ in returned]
+ else:
+ rows, columns = (40, 80)
+
+ desc_len = columns - used
+
+ line = ''
+ for word in text.split():
+ if len(line) + len(word) < desc_len:
+ line = f'{line} {word}'
+ continue
+ if line:
+ yield line[1:]
+ else:
+ line = f'{line} {word}'
+
+ yield line[1:]
+
+
+def get_vrrp_intf():
+ return [intf for intf in Section.interfaces() if intf.is_vrrp()]
+
+
+def get_counter_val(clear, now):
+ """
+ attempt to correct a counter if it wrapped, copied from perl
+
+ clear: previous counter
+ now: the current counter
+ """
+ # This function has to deal with both 32 and 64 bit counters
+ if clear == 0:
+ return now
+
+ # device is using 64 bit values assume they never wrap
+ value = now - clear
+ if (now >> 32) != 0:
+ return value
+
+ # The counter has rolled. If the counter has rolled
+ # multiple times since the clear value, then this math
+ # is meaningless.
+ if (value < 0):
+ value = (4294967296 - clear) + now
+
+ return value
+
+
+@register('help')
+def usage(*args):
+ print(f"Usage: {sys.argv[0]} [intf=NAME|intf-type=TYPE|vif|vrrp] action=ACTION")
+ print(f" NAME = " + ' | '.join(Section.interfaces()))
+ print(f" TYPE = " + ' | '.join(Section.sections()))
+ print(f" ACTION = " + ' | '.join(actions))
+ sys.exit(1)
+
+
+@register('allowed')
+def run_allowed(**kwarg):
+ sys.stdout.write(' '.join(Section.interfaces()))
+
+
+def pppoe(ifname):
+ out = cmd(f'ps -C pppd -f')
+ if ifname in out:
+ return 'C'
+ elif ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]:
+ return 'D'
+ return ''
+
+
+@register('show')
+def run_show_intf(ifnames, iftypes, vif, vrrp):
+ handled = []
+ for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ handled.append(interface.ifname)
+ cache = interface.operational.load_counters()
+
+ out = cmd(f'ip addr show {interface.ifname}')
+ out = re.sub(f'^\d+:\s+','',out)
+ if re.search("link/tunnel6", out):
+ tunnel = cmd(f'ip -6 tun show {interface.ifname}')
+ # tun0: ip/ipv6 remote ::2 local ::1 encaplimit 4 hoplimit 64 tclass inherit flowlabel inherit (flowinfo 0x00000000)
+ tunnel = re.sub('.*encap', 'encap', tunnel)
+ out = re.sub('(\n\s+)(link/tunnel6)', f'\g<1>{tunnel}\g<1>\g<2>', out)
+
+ print(out)
+
+ timestamp = int(cache.get('timestamp', 0))
+ if timestamp:
+ when = interface.operational.strtime(timestamp)
+ print(f' Last clear: {when}')
+
+ description = interface.get_alias()
+ if description:
+ print(f' Description: {description}')
+
+ print()
+ print(interface.operational.formated_stats())
+
+ for ifname in ifnames:
+ if ifname not in handled and ifname.startswith('pppoe'):
+ state = pppoe(ifname)
+ if not state:
+ continue
+ string = {
+ 'C': 'Coming up',
+ 'D': 'Link down',
+ }[state]
+ print('{}: {}'.format(ifname, string))
+
+
+@register('show-brief')
+def run_show_intf_brief(ifnames, iftypes, vif, vrrp):
+ format1 = '%-16s %-33s %-4s %s'
+ format2 = '%-16s %s'
+
+ print('Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down')
+ print(format1 % ("Interface", "IP Address", "S/L", "Description"))
+ print(format1 % ("---------", "----------", "---", "-----------"))
+
+ handled = []
+ for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ handled.append(interface.ifname)
+
+ oper_state = interface.operational.get_state()
+ admin_state = interface.get_admin_state()
+
+ intf = [interface.ifname,]
+ oper = ['u', ] if oper_state in ('up', 'unknown') else ['A', ]
+ admin = ['u', ] if oper_state in ('up', 'unknown') else ['D', ]
+ addrs = [_ for _ in interface.get_addr() if not _.startswith('fe80::')] or ['-', ]
+ descs = list(split_text(interface.get_alias(),0))
+
+ while intf or oper or admin or addrs or descs:
+ i = intf.pop(0) if intf else ''
+ a = addrs.pop(0) if addrs else ''
+ d = descs.pop(0) if descs else ''
+ s = [oper.pop(0)] if oper else []
+ l = [admin.pop(0)] if admin else []
+ if len(a) < 33:
+ print(format1 % (i, a, '/'.join(s+l), d))
+ else:
+ print(format2 % (i, a))
+ print(format1 % ('', '', '/'.join(s+l), d))
+
+ for ifname in ifnames:
+ if ifname not in handled and ifname.startswith('pppoe'):
+ state = pppoe(ifname)
+ if not state:
+ continue
+ string = {
+ 'C': 'u/D',
+ 'D': 'A/D',
+ }[state]
+ print(format1 % (ifname, '', string, ''))
+
+
+@register('show-count')
+def run_show_counters(ifnames, iftypes, vif, vrrp):
+ formating = '%-12s %10s %10s %10s %10s'
+ print(formating % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes'))
+
+ for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ oper = interface.operational.get_state()
+
+ if oper not in ('up','unknown'):
+ continue
+
+ stats = interface.operational.get_stats()
+ cache = interface.operational.load_counters()
+ print(formating % (
+ interface.ifname,
+ get_counter_val(cache['rx_packets'], stats['rx_packets']),
+ get_counter_val(cache['rx_bytes'], stats['rx_bytes']),
+ get_counter_val(cache['tx_packets'], stats['tx_packets']),
+ get_counter_val(cache['tx_bytes'], stats['tx_bytes']),
+ ))
+
+
+@register('clear')
+def run_clear_intf(ifnames, iftypes, vif, vrrp):
+ for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ print(f'Clearing {interface.ifname}')
+ interface.operational.clear_counters()
+
+
+@register('reset')
+def run_reset_intf(ifnames, iftypes, vif, vrrp):
+ for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ interface.operational.reset_counters()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(add_help=False, description='Show interface information')
+ parser.add_argument('--intf', action="store", type=str, default='', help='only show the specified interface(s)')
+ parser.add_argument('--intf-type', action="store", type=str, default='', help='only show the specified interface type')
+ parser.add_argument('--action', action="store", type=str, default='show', help='action to perform')
+ parser.add_argument('--vif', action='store_true', default=False, help="only show vif interfaces")
+ parser.add_argument('--vrrp', action='store_true', default=False, help="only show vrrp interfaces")
+ parser.add_argument('--help', action='store_true', default=False, help="show help")
+
+ args = parser.parse_args()
+
+ def missing(*args):
+ print('Invalid action [{args.action}]')
+ usage()
+
+ actions.get(args.action, missing)(
+ [_ for _ in args.intf.split(' ') if _],
+ [_ for _ in args.intf_type.split(' ') if _],
+ args.vif,
+ args.vrrp
+ )
diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py
new file mode 100755
index 000000000..e319cc38d
--- /dev/null
+++ b/src/op_mode/show_ipsec_sa.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import sys
+
+import vici
+import tabulate
+import hurry.filesize
+
+import vyos.util
+
+
+try:
+ session = vici.Session()
+ sas = session.list_sas()
+except PermissionError:
+ print("You do not have a permission to connect to the IPsec daemon")
+ sys.exit(1)
+except ConnectionRefusedError:
+ print("IPsec is not runing")
+ sys.exit(1)
+except Exception as e:
+ print("An error occured: {0}".format(e))
+ sys.exit(1)
+
+sa_data = []
+
+for sa in sas:
+ # list_sas() returns a list of single-item dicts
+ for peer in sa:
+ parent_sa = sa[peer]
+
+ if parent_sa["state"] == b"ESTABLISHED":
+ state = "up"
+ else:
+ state = "down"
+
+ if state == "up":
+ uptime = vyos.util.seconds_to_human(parent_sa["established"].decode())
+ else:
+ uptime = "N/A"
+
+ remote_host = parent_sa["remote-host"].decode()
+ remote_id = parent_sa["remote-id"].decode()
+
+ if remote_host == remote_id:
+ remote_id = "N/A"
+
+ # The counters can only be obtained from the child SAs
+ child_sas = parent_sa["child-sas"]
+ installed_sas = {k: v for k, v in child_sas.items() if v["state"] == b"INSTALLED"}
+
+ if not installed_sas:
+ data = [peer, state, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"]
+ sa_data.append(data)
+ else:
+ for csa in installed_sas:
+ isa = installed_sas[csa]
+
+ bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode()))
+ bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode()))
+ bytes_str = "{0}/{1}".format(bytes_in, bytes_out)
+
+ pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si)
+ pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si)
+ pkts_str = "{0}/{1}".format(pkts_in, pkts_out)
+ # Remove B from <1K values
+ pkts_str = re.sub(r'B', r'', pkts_str)
+
+ enc = isa["encr-alg"].decode()
+ if "encr-keysize" in isa:
+ key_size = isa["encr-keysize"].decode()
+ else:
+ key_size = ""
+ if "integ-alg" in isa:
+ hash = isa["integ-alg"].decode()
+ else:
+ hash = ""
+ if "dh-group" in isa:
+ dh_group = isa["dh-group"].decode()
+ else:
+ dh_group = ""
+
+ proposal = enc
+ if key_size:
+ proposal = "{0}_{1}".format(proposal, key_size)
+ if hash:
+ proposal = "{0}/{1}".format(proposal, hash)
+ if dh_group:
+ proposal = "{0}/{1}".format(proposal, dh_group)
+
+ data = [peer, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal]
+ sa_data.append(data)
+
+headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"]
+output = tabulate.tabulate(sa_data, headers)
+print(output)
diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py
new file mode 100755
index 000000000..0b53112f2
--- /dev/null
+++ b/src/op_mode/show_nat_statistics.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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
+
+OUT_TMPL_SRC="""
+rule pkts bytes interface
+---- ---- ----- ---------
+{% for r in output %}
+{%- if r.comment -%}
+{%- set packets = r.counter.packets -%}
+{%- set bytes = r.counter.bytes -%}
+{%- set interface = r.interface -%}
+{# remove rule comment prefix #}
+{%- set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') | replace(' tcp_udp', '') -%}
+{{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }}
+{%- endif %}
+{% endfor %}
+"""
+
+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 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] }"
+ destination = r"nftables[?rule.chain=='PREROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
+ data = {
+ 'output' : jmespath.search(source if args.source else destination, tmp),
+ 'direction' : 'source' if args.source else 'destination'
+ }
+
+ tmpl = Template(OUT_TMPL_SRC, lstrip_blocks=True)
+ print(tmpl.render(data))
+ exit(0)
+else:
+ parser.print_help()
+ exit(1)
+
diff --git a/src/op_mode/show_nat_translations.py b/src/op_mode/show_nat_translations.py
new file mode 100755
index 000000000..3af33b78e
--- /dev/null
+++ b/src/op_mode/show_nat_translations.py
@@ -0,0 +1,200 @@
+#!/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/>.
+
+'''
+show nat translations
+'''
+
+import os
+import sys
+import ipaddress
+import argparse
+import xmltodict
+
+from vyos.util import popen
+from vyos.util import DEVNULL
+
+conntrack = '/usr/sbin/conntrack'
+
+verbose_format = "%-20s %-18s %-20s %-18s"
+normal_format = "%-20s %-20s %-4s %-8s %s"
+
+
+def headers(verbose, pipe):
+ if verbose:
+ return verbose_format % ('Pre-NAT src', 'Pre-NAT dst', 'Post-NAT src', 'Post-NAT dst')
+ return normal_format % ('Pre-NAT', 'Post-NAT', 'Prot', 'Timeout', 'Type' if pipe else '')
+
+
+def command(srcdest, proto, ipaddr):
+ command = f'{conntrack} -o xml -L'
+
+ if proto:
+ command += f' -p {proto}'
+
+ if srcdest == 'source':
+ command += ' -n'
+ if ipaddr:
+ command += f' --orig-src {ipaddr}'
+ if srcdest == 'destination':
+ command += ' -g'
+
+ return command
+
+
+def run(command):
+ xml, code = popen(command,stderr=DEVNULL)
+ if code:
+ sys.exit('conntrack failed')
+ return xml
+
+
+def content(xmlfile):
+ xml = ''
+ with open(xmlfile,'r') as r:
+ xml += r.read()
+ return xml
+
+
+def pipe():
+ xml = ''
+ while True:
+ line = sys.stdin.readline()
+ xml += line
+ if '</conntrack>' in line:
+ break
+
+ sys.stdin = open('/dev/tty')
+ return xml
+
+
+def process(data, stats, protocol, pipe, verbose, flowtype=''):
+ if not data:
+ return
+
+ parsed = xmltodict.parse(data)
+
+ print(headers(verbose, pipe))
+
+ # to help the linter to detect typos
+ ORIGINAL = 'original'
+ REPLY = 'reply'
+ INDEPENDANT = 'independent'
+ SPORT = 'sport'
+ DPORT = 'dport'
+ SRC = 'src'
+ DST = 'dst'
+
+ for rule in parsed['conntrack']['flow']:
+ src, dst, sport, dport, proto = {}, {}, {}, {}, {}
+ packet_count, byte_count = {}, {}
+ timeout, use = 0, 0
+
+ rule_type = rule.get('type', '')
+
+ for meta in rule['meta']:
+ # print(meta)
+ direction = meta['@direction']
+
+ if direction in (ORIGINAL, REPLY):
+ if 'layer3' in meta:
+ l3 = meta['layer3']
+ src[direction] = l3[SRC]
+ dst[direction] = l3[DST]
+
+ if 'layer4' in meta:
+ l4 = meta['layer4']
+ sp = l4.get(SPORT, '')
+ dp = l4.get(DPORT, '')
+ if sp:
+ sport[direction] = sp
+ if dp:
+ dport[direction] = dp
+ proto[direction] = l4.get('@protoname','')
+
+ if stats and 'counters' in meta:
+ packet_count[direction] = meta['packets']
+ byte_count[direction] = meta['bytes']
+ continue
+
+ if direction == INDEPENDANT:
+ timeout = meta['timeout']
+ use = meta['use']
+ continue
+
+ in_src = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if ORIGINAL in sport else src[ORIGINAL]
+ in_dst = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if ORIGINAL in dport else dst[ORIGINAL]
+
+ # inverted the the perl code !!?
+ out_dst = '%s:%s' % (dst[REPLY], dport[REPLY]) if REPLY in dport else dst[REPLY]
+ out_src = '%s:%s' % (src[REPLY], sport[REPLY]) if REPLY in sport else src[REPLY]
+
+ if flowtype == 'source':
+ v = ORIGINAL in sport and REPLY in dport
+ f = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if v else src[ORIGINAL]
+ t = '%s:%s' % (dst[REPLY], dport[REPLY]) if v else dst[REPLY]
+ else:
+ v = ORIGINAL in dport and REPLY in sport
+ f = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if v else dst[ORIGINAL]
+ t = '%s:%s' % (src[REPLY], sport[REPLY]) if v else src[REPLY]
+
+ # Thomas: I do not believe proto should be an option
+ p = proto.get('original', '')
+ if protocol and p != protocol:
+ continue
+
+ if verbose:
+ msg = verbose_format % (in_src, in_dst, out_dst, out_src)
+ p = f'{p}: ' if p else ''
+ msg += f'\n {p}{f} ==> {t}'
+ msg += f' timeout: {timeout}' if timeout else ''
+ msg += f' use: {use} ' if use else ''
+ msg += f' type: {rule_type}' if rule_type else ''
+ print(msg)
+ else:
+ print(normal_format % (f, t, p, timeout, rule_type if rule_type else ''))
+
+ if stats:
+ for direction in ('original', 'reply'):
+ if direction in packet_count:
+ print(' %-8s: packets %s, bytes %s' % direction, packet_count[direction], byte_count[direction])
+
+
+def main():
+ parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__)
+ parser.add_argument('--verbose', help='provide more details about the flows', action='store_true')
+ parser.add_argument('--proto', help='filter by protocol', default='', type=str)
+ parser.add_argument('--file', help='read the conntrack xml from a file', type=str)
+ parser.add_argument('--stats', help='add usage statistics', action='store_true')
+ parser.add_argument('--type', help='NAT type (source, destination)', required=True, type=str)
+ parser.add_argument('--ipaddr', help='source ip address to filter on', type=ipaddress.ip_address)
+ parser.add_argument('--pipe', help='read conntrack xml data from stdin', action='store_true')
+
+ arg = parser.parse_args()
+
+ if arg.type not in ('source', 'destination'):
+ sys.exit('Unknown NAT type!')
+
+ if arg.pipe:
+ process(pipe(), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type)
+ elif arg.file:
+ process(content(arg.file), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type)
+ else:
+ process(run(command(arg.type, arg.proto, arg.ipaddr)), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
new file mode 100755
index 000000000..32918ddce
--- /dev/null
+++ b/src/op_mode/show_openvpn.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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 jinja2
+import argparse
+
+from sys import exit
+from vyos.config import Config
+
+outp_tmpl = """
+{% if clients %}
+OpenVPN status on {{ intf }}
+
+Client CN Remote Host Local Host TX bytes RX bytes Connected Since
+--------- ----------- ---------- -------- -------- ---------------
+{%- for c in clients %}
+{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-21s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.rx_bytes) }} {{ c.online_since }}
+{%- endfor %}
+{% endif %}
+"""
+
+def bytes2HR(size):
+ # we need to operate in integers
+ size = int(size)
+
+ suff = ['B', 'KB', 'MB', 'GB', 'TB']
+ suffIdx = 0
+
+ while size > 1024:
+ # incr. suffix index
+ suffIdx += 1
+ # divide
+ size = size/1024.0
+
+ output="{0:.1f} {1}".format(size, suff[suffIdx])
+ return output
+
+def get_status(mode, interface):
+ status_file = '/opt/vyatta/etc/openvpn/status/{}.status'.format(interface)
+ # this is an empirical value - I assume we have no more then 999999
+ # current OpenVPN connections
+ routing_table_line = 999999
+
+ data = {
+ 'mode': mode,
+ 'intf': interface,
+ 'local': 'N/A',
+ 'date': '',
+ 'clients': [],
+ }
+
+ if not os.path.exists(status_file):
+ return data
+
+ 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 NameError('Expected "OpenVPN CLIENT LIST"')
+ else:
+ if not line == 'OpenVPN STATISTICS':
+ raise NameError('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':
+ # followed by line3 giving output information and the actual output data
+ #
+ # 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
+ if (line_no >= 3) and (line_no < routing_table_line):
+ # indicator that there are no more clients and we will continue with the
+ # routing table
+ if line == 'ROUTING TABLE':
+ routing_table_line = line_no
+ continue
+
+ client = {
+ 'name': line.split(',')[0],
+ 'remote': line.split(',')[1],
+ 'rx_bytes': bytes2HR(line.split(',')[2]),
+ 'tx_bytes': bytes2HR(line.split(',')[3]),
+ 'online_since': line.split(',')[4]
+ }
+
+ data['clients'].append(client)
+ continue
+ else:
+ if line_no == 2:
+ client = {
+ 'name': 'N/A',
+ 'remote': 'N/A',
+ 'rx_bytes': bytes2HR(line.split(',')[1]),
+ 'tx_bytes': '',
+ 'online_since': 'N/A'
+ }
+ continue
+
+ if line_no == 3:
+ client['tx_bytes'] = bytes2HR(line.split(',')[1])
+ data['clients'].append(client)
+ break
+
+ return data
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-m', '--mode', help='OpenVPN operation mode (server, client, site-2-site)', required=True)
+
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ config = Config()
+ if len(config.list_effective_nodes('interfaces openvpn')) == 0:
+ print("No OpenVPN interfaces configured")
+ exit(0)
+
+ # search all OpenVPN interfaces and add those with a matching mode to our
+ # interfaces list
+ interfaces = []
+ for intf in config.list_effective_nodes('interfaces openvpn'):
+ # get interface type (server, client, site-to-site)
+ mode = config.return_effective_value('interfaces openvpn {} mode'.format(intf))
+ if args.mode == mode:
+ interfaces.append(intf)
+
+ for intf in interfaces:
+ data = get_status(args.mode, intf)
+ local_host = config.return_effective_value('interfaces openvpn {} local-host'.format(intf))
+ local_port = config.return_effective_value('interfaces openvpn {} local-port'.format(intf))
+ if local_host and local_port:
+ data['local'] = local_host + ':' + local_port
+
+ if args.mode in ['client', 'site-to-site']:
+ for client in data['clients']:
+ if config.exists_effective('interfaces openvpn {} shared-secret-key-file'.format(intf)):
+ client['name'] = "None (PSK)"
+
+ remote_host = config.return_effective_values('interfaces openvpn {} remote-host'.format(intf))
+ remote_port = config.return_effective_value('interfaces openvpn {} remote-port'.format(intf))
+
+ if not remote_port:
+ remote_port = '1194'
+
+ if len(remote_host) >= 1:
+ client['remote'] = str(remote_host[0]) + ':' + remote_port
+
+ tmpl = jinja2.Template(outp_tmpl)
+ print(tmpl.render(data))
+
diff --git a/src/op_mode/show_raid.sh b/src/op_mode/show_raid.sh
new file mode 100755
index 000000000..ba4174692
--- /dev/null
+++ b/src/op_mode/show_raid.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+raid_set_name=$1
+raid_sets=`cat /proc/partitions | grep md | awk '{ print $4 }'`
+valid_set=`echo $raid_sets | grep $raid_set_name`
+if [ -z $valid_set ]; then
+ echo "$raid_set_name is not a RAID set"
+else
+ if [ -r /dev/${raid_set_name} ]; then
+ # This should work without sudo because we have read
+ # access to the dev, but for some reason mdadm must be
+ # run as root in order to succeed.
+ sudo /sbin/mdadm --detail /dev/${raid_set_name}
+ else
+ echo "Must be administrator or root to display RAID status"
+ fi
+fi
diff --git a/src/op_mode/show_ram.sh b/src/op_mode/show_ram.sh
new file mode 100755
index 000000000..b013e16f8
--- /dev/null
+++ b/src/op_mode/show_ram.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+#
+# Module: vyos-show-ram.sh
+# Displays memory usage information in minimalistic format
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+MB_DIVISOR=1024
+
+TOTAL=$(cat /proc/meminfo | grep -E "^MemTotal:" | awk -F ' ' '{print $2}')
+FREE=$(cat /proc/meminfo | grep -E "^MemFree:" | awk -F ' ' '{print $2}')
+BUFFERS=$(cat /proc/meminfo | grep -E "^Buffers:" | awk -F ' ' '{print $2}')
+CACHED=$(cat /proc/meminfo | grep -E "^Cached:" | awk -F ' ' '{print $2}')
+
+DISPLAY_FREE=$(( ($FREE + $BUFFERS + $CACHED) / $MB_DIVISOR ))
+DISPLAY_TOTAL=$(( $TOTAL / $MB_DIVISOR ))
+DISPLAY_USED=$(( $DISPLAY_TOTAL - $DISPLAY_FREE ))
+
+echo "Total: $DISPLAY_TOTAL"
+echo "Free: $DISPLAY_FREE"
+echo "Used: $DISPLAY_USED"
diff --git a/src/op_mode/show_sensors.py b/src/op_mode/show_sensors.py
new file mode 100755
index 000000000..6ae477647
--- /dev/null
+++ b/src/op_mode/show_sensors.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+
+import re
+import sys
+from vyos.util import popen
+from vyos.util import DEVNULL
+output,retcode = popen("sensors --no-adapter", stderr=DEVNULL)
+if retcode == 0:
+ print (output)
+ sys.exit(0)
+else:
+ output,retcode = popen("sensors-detect --auto",stderr=DEVNULL)
+ match = re.search(r'#----cut here----(.*)#----cut here----',output, re.DOTALL)
+ if match:
+ for module in match.group(0).split('\n'):
+ if not module.startswith("#"):
+ popen("modprobe {}".format(module.strip()))
+ output,retcode = popen("sensors --no-adapter", stderr=DEVNULL)
+ if retcode == 0:
+ print (output)
+ sys.exit(0)
+
+
+print ("No sensors found")
+sys.exit(1)
+
+
diff --git a/src/op_mode/show_usb_serial.py b/src/op_mode/show_usb_serial.py
new file mode 100755
index 000000000..776898c25
--- /dev/null
+++ b/src/op_mode/show_usb_serial.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from jinja2 import Template
+from pyudev import Context, Devices
+from sys import exit
+
+OUT_TMPL_SRC = """Device Model Vendor
+------ ------ ------
+{%- for d in devices %}
+{{ "%-16s" | format(d.device) }} {{ "%-19s" | format(d.model)}} {{d.vendor}}
+{%- endfor %}
+
+"""
+
+data = {
+ 'devices': []
+}
+
+
+base_directory = '/dev/serial/by-bus'
+if not os.path.isdir(base_directory):
+ print("No USB to serial converter connected")
+ exit(0)
+
+context = Context()
+for root, dirs, files in os.walk(base_directory):
+ for basename in files:
+ os.path.join(root, basename)
+ device = Devices.from_device_file(context, os.path.join(root, basename))
+ tmp = {
+ 'device': basename,
+ 'model': device.properties.get('ID_MODEL'),
+ 'vendor': device.properties.get('ID_VENDOR_FROM_DATABASE')
+ }
+ data['devices'].append(tmp)
+
+data['devices'] = sorted(data['devices'], key = lambda i: i['device'])
+tmpl = Template(OUT_TMPL_SRC)
+print(tmpl.render(data))
+
+exit(0)
diff --git a/src/op_mode/show_users.py b/src/op_mode/show_users.py
new file mode 100755
index 000000000..8e4f12851
--- /dev/null
+++ b/src/op_mode/show_users.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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 pwd
+import spwd
+import struct
+import sys
+from time import ctime
+
+from tabulate import tabulate
+from vyos.config import Config
+
+
+class UserInfo:
+ def __init__(self, uid, name, user_type, is_locked, login_time, tty, host):
+ self.uid = uid
+ self.name = name
+ self.user_type = user_type
+ self.is_locked = is_locked
+ self.login_time = login_time
+ self.tty = tty
+ self.host = host
+
+
+filters = {
+ 'default': lambda user: not user.is_locked, # Default is everything but locked accounts
+ 'vyos': lambda user: user.user_type == 'vyos',
+ 'other': lambda user: user.user_type != 'vyos',
+ 'locked': lambda user: user.is_locked,
+ 'all': lambda user: True
+}
+
+
+def is_locked(user_name: str) -> bool:
+ """Check if a given user has password in shadow db"""
+
+ try:
+ encrypted_password = spwd.getspnam(user_name)[1]
+ return encrypted_password == '*' or encrypted_password.startswith('!')
+ except (KeyError, PermissionError):
+ print('Cannot access shadow database, ensure this script is run with sufficient permissions')
+ sys.exit(1)
+
+
+def decode_lastlog(lastlog_file, uid: int):
+ """Decode last login info of a given user uid from the lastlog file"""
+
+ struct_fmt = '=L32s256s'
+ recordsize = struct.calcsize(struct_fmt)
+ lastlog_file.seek(recordsize * uid)
+ buf = lastlog_file.read(recordsize)
+ if len(buf) < recordsize:
+ return None
+ (time, tty, host) = struct.unpack(struct_fmt, buf)
+ time = 'never logged in' if time == 0 else ctime(time)
+ tty = tty.strip(b'\x00')
+ host = host.strip(b'\x00')
+ return time, tty, host
+
+
+def list_users():
+ cfg = Config()
+ vyos_users = cfg.list_effective_nodes('system login user')
+ users = []
+ with open('/var/log/lastlog', 'rb') as lastlog_file:
+ for (name, _, uid, _, _, _, _) in pwd.getpwall():
+ lastlog_info = decode_lastlog(lastlog_file, uid)
+ if lastlog_info is None:
+ continue
+ user_info = UserInfo(
+ uid, name,
+ user_type='vyos' if name in vyos_users else 'other',
+ is_locked=is_locked(name),
+ login_time=lastlog_info[0],
+ tty=lastlog_info[1],
+ host=lastlog_info[2])
+ users.append(user_info)
+ return users
+
+
+def main():
+ parser = argparse.ArgumentParser(prog=sys.argv[0], add_help=False)
+ parser.add_argument('type', nargs='?', choices=['all', 'vyos', 'other', 'locked'])
+ args = parser.parse_args()
+
+ filter_type = args.type if args.type is not None else 'default'
+ filter_expr = filters[filter_type]
+
+ headers = ['Username', 'Type', 'Locked', 'Tty', 'From', 'Last login']
+ table_data = []
+ for user in list_users():
+ if filter_expr(user):
+ table_data.append([user.name, user.user_type, user.is_locked, user.tty, user.host, user.login_time])
+ print(tabulate(table_data, headers, tablefmt='simple'))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py
new file mode 100755
index 000000000..d0d5c6785
--- /dev/null
+++ b/src/op_mode/show_version.py
@@ -0,0 +1,73 @@
+#!/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/>.
+#
+# Purpose:
+# Displays image version and system information.
+# Used by the "run show version" command.
+
+import argparse
+import vyos.version
+import vyos.limericks
+
+from jinja2 import Template
+from sys import exit
+from vyos.util import call
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-a", "--all", action="store_true", help="Include individual package versions")
+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")
+
+version_output_tmpl = """
+Version: VyOS {{version}}
+Release Train: {{release_train}}
+
+Built by: {{built_by}}
+Built on: {{built_on}}
+Build UUID: {{build_uuid}}
+Build Commit ID: {{build_git}}
+
+Architecture: {{system_arch}}
+Boot via: {{boot_via}}
+System type: {{system_type}}
+
+Hardware vendor: {{hardware_vendor}}
+Hardware model: {{hardware_model}}
+Hardware S/N: {{hardware_serial}}
+Hardware UUID: {{hardware_uuid}}
+
+Copyright: VyOS maintainers and contributors
+"""
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ version_data = vyos.version.get_full_version_data()
+
+ if args.json:
+ import json
+ print(json.dumps(version_data))
+ exit(0)
+
+ tmpl = Template(version_output_tmpl)
+ print(tmpl.render(version_data))
+
+ if args.all:
+ print("Package versions:")
+ call("dpkg -l")
+
+ if args.funny:
+ print(vyos.limericks.get_random())
diff --git a/src/op_mode/show_vpn_ra.py b/src/op_mode/show_vpn_ra.py
new file mode 100755
index 000000000..73688c4ea
--- /dev/null
+++ b/src/op_mode/show_vpn_ra.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import re
+
+from vyos.util import popen
+
+# chech connection to pptp and l2tp daemon
+def get_sessions():
+ absent_pptp = False
+ absent_l2tp = False
+ pptp_cmd = "accel-cmd -p 2003 show sessions"
+ l2tp_cmd = "accel-cmd -p 2004 show sessions"
+ err_pattern = "^Connection.+failed$"
+ # This value for chack only output header without sessions.
+ len_def_header = 170
+
+ # Check pptp
+ output, err = popen(pptp_cmd, decode='utf-8')
+ if not err and len(output) > len_def_header and not re.search(err_pattern, output):
+ print(output)
+ else:
+ absent_pptp = True
+
+ # Check l2tp
+ output, err = popen(l2tp_cmd, decode='utf-8')
+ if not err and len(output) > len_def_header and not re.search(err_pattern, output):
+ print(output)
+ else:
+ absent_l2tp = True
+
+ if absent_l2tp and absent_pptp:
+ print("No active remote access VPN sessions")
+
+
+def main():
+ get_sessions()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py
new file mode 100755
index 000000000..b6bb73d01
--- /dev/null
+++ b/src/op_mode/show_vrf.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import jinja2
+from json import loads
+
+from vyos.util import cmd
+
+vrf_out_tmpl = """
+VRF name state mac address flags interfaces
+-------- ----- ----------- ----- ----------
+{%- for v in vrf %}
+{{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}}
+{%- endfor %}
+
+"""
+
+def list_vrfs():
+ command = 'ip -j -br link show type vrf'
+ answer = loads(cmd(command))
+ return [_ for _ in answer if _]
+
+def list_vrf_members(vrf):
+ command = f'ip -j -br link show master {vrf}'
+ answer = loads(cmd(command))
+ return [_ for _ in answer if _]
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("-e", "--extensive", action="store_true",
+ help="provide detailed vrf informatio")
+parser.add_argument('interface', metavar='I', type=str, nargs='?',
+ help='interface to display')
+
+args = parser.parse_args()
+
+if args.extensive:
+ data = { 'vrf': [] }
+ for vrf in list_vrfs():
+ name = vrf['ifname']
+ if args.interface and name != args.interface:
+ continue
+
+ vrf['members'] = []
+ for member in list_vrf_members(name):
+ vrf['members'].append(member['ifname'])
+ data['vrf'].append(vrf)
+
+ tmpl = jinja2.Template(vrf_out_tmpl)
+ print(tmpl.render(data))
+
+else:
+ print(" ".join([vrf['ifname'] for vrf in list_vrfs()]))
diff --git a/src/op_mode/show_wireless.py b/src/op_mode/show_wireless.py
new file mode 100755
index 000000000..b5ee3aee1
--- /dev/null
+++ b/src/op_mode/show_wireless.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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 re
+
+from sys import exit
+from copy import deepcopy
+
+from vyos.config import Config
+from vyos.util import popen
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-s", "--scan", help="Scan for Wireless APs on given interface, e.g. 'wlan0'")
+parser.add_argument("-b", "--brief", action="store_true", help="Show wireless configuration")
+parser.add_argument("-c", "--stations", help="Show wireless clients connected on interface, e.g. 'wlan0'")
+
+
+def show_brief():
+ config = Config()
+ if len(config.list_effective_nodes('interfaces wireless')) == 0:
+ print("No Wireless interfaces configured")
+ exit(0)
+
+ interfaces = []
+ for intf in config.list_effective_nodes('interfaces wireless'):
+ config.set_level('interfaces wireless {}'.format(intf))
+ data = {
+ 'name': intf,
+ 'type': '',
+ 'ssid': '',
+ 'channel': ''
+ }
+ data['type'] = config.return_effective_value('type')
+ data['ssid'] = config.return_effective_value('ssid')
+ data['channel'] = config.return_effective_value('channel')
+
+ interfaces.append(data)
+
+ return interfaces
+
+def ssid_scan(intf):
+ # XXX: This ignores errors
+ tmp, _ = popen(f'/sbin/iw dev {intf} scan ap-force')
+ networks = []
+ data = {
+ 'ssid': '',
+ 'mac': '',
+ 'channel': '',
+ 'signal': ''
+ }
+ re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})')
+ for line in tmp.splitlines():
+ if line.startswith('BSS '):
+ ssid = deepcopy(data)
+ ssid['mac'] = re.search(re_mac, line).group()
+
+ elif line.lstrip().startswith('SSID: '):
+ # SSID can be " SSID: WLAN-57 6405", thus strip all leading whitespaces
+ ssid['ssid'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('signal: '):
+ # Siganl can be " signal: -67.00 dBm", thus strip all leading whitespaces
+ ssid['signal'] = line.lstrip().split(':')[-1].split()[0]
+
+ elif line.lstrip().startswith('DS Parameter set: channel'):
+ # Channel can be " DS Parameter set: channel 6" , thus
+ # strip all leading whitespaces
+ ssid['channel'] = line.lstrip().split(':')[-1].split()[-1]
+ networks.append(ssid)
+ continue
+
+ return networks
+
+def show_clients(intf):
+ # XXX: This ignores errors
+ tmp, _ = popen(f'/sbin/iw dev {intf} station dump')
+ clients = []
+ data = {
+ 'mac': '',
+ 'signal': '',
+ 'rx_bytes': '',
+ 'rx_packets': '',
+ 'tx_bytes': '',
+ 'tx_packets': ''
+ }
+ re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})')
+ for line in tmp.splitlines():
+ if line.startswith('Station'):
+ client = deepcopy(data)
+ client['mac'] = re.search(re_mac, line).group()
+
+ elif line.lstrip().startswith('signal avg:'):
+ client['signal'] = line.lstrip().split(':')[-1].lstrip().split()[0]
+
+ elif line.lstrip().startswith('rx bytes:'):
+ client['rx_bytes'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('rx packets:'):
+ client['rx_packets'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('tx bytes:'):
+ client['tx_bytes'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('tx packets:'):
+ client['tx_packets'] = line.lstrip().split(':')[-1].lstrip()
+ clients.append(client)
+ continue
+
+ return clients
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ if args.scan:
+ print("Address SSID Channel Signal (dbm)")
+ for network in ssid_scan(args.scan):
+ print("{:<17} {:<32} {:>3} {}".format(network['mac'],
+ network['ssid'],
+ network['channel'],
+ network['signal']))
+ exit(0)
+
+ elif args.brief:
+ print("Interface Type SSID Channel")
+ for intf in show_brief():
+ print("{:<9} {:<12} {:<32} {:>3}".format(intf['name'],
+ intf['type'],
+ intf['ssid'],
+ intf['channel']))
+ exit(0)
+
+ elif args.stations:
+ print("Station Signal RX: bytes packets TX: bytes packets")
+ for client in show_clients(args.stations):
+ print("{:<17} {:>3} {:>15} {:>9} {:>15} {:>10} ".format(client['mac'],
+ client['signal'], client['rx_bytes'], client['rx_packets'], client['tx_bytes'], client['tx_packets']))
+
+ exit(0)
+
+ else:
+ parser.print_help()
+ exit(1)
diff --git a/src/op_mode/snmp.py b/src/op_mode/snmp.py
new file mode 100755
index 000000000..5fae67881
--- /dev/null
+++ b/src/op_mode/snmp.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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/>.
+#
+# File: snmp.py
+# Purpose:
+# Show SNMP community/remote hosts
+# Used by the "run show snmp community" commands.
+
+import os
+import sys
+import argparse
+
+from vyos.config import Config
+from vyos.util import call
+
+config_file_daemon = r'/etc/snmp/snmpd.conf'
+
+parser = argparse.ArgumentParser(description='Retrieve infomration from running SNMP daemon')
+parser.add_argument('--allowed', action="store_true", help='Show available SNMP communities')
+parser.add_argument('--community', action="store", help='Show status of given SNMP community', type=str)
+parser.add_argument('--host', action="store", help='SNMP host to connect to', type=str, default='localhost')
+
+config = {
+ 'communities': [],
+}
+
+def read_config():
+ with open(config_file_daemon, 'r') as f:
+ for line in f:
+ # Only get configured SNMP communitie
+ if line.startswith('rocommunity') or line.startswith('rwcommunity'):
+ string = line.split(' ')
+ # append community to the output list only once
+ c = string[1]
+ if c not in config['communities']:
+ config['communities'].append(c)
+
+def show_all():
+ if len(config['communities']) > 0:
+ print(' '.join(config['communities']))
+
+def show_community(c, h):
+ print('Status of SNMP community {0} on {1}'.format(c, h), flush=True)
+ call('/usr/bin/snmpstatus -t1 -v1 -c {0} {1}'.format(c, h))
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service snmp'):
+ print("SNMP service is not configured")
+ sys.exit(0)
+
+ read_config()
+
+ if args.allowed:
+ show_all()
+ sys.exit(1)
+ elif args.community:
+ show_community(args.community, args.host)
+ sys.exit(1)
+ else:
+ parser.print_help()
+ sys.exit(1)
diff --git a/src/op_mode/snmp_ifmib.py b/src/op_mode/snmp_ifmib.py
new file mode 100755
index 000000000..2479936bd
--- /dev/null
+++ b/src/op_mode/snmp_ifmib.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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/>.
+#
+# File: snmp_ifmib.py
+# Purpose:
+# Show SNMP MIB information
+# Used by the "run show snmp mib" commands.
+
+import sys
+import argparse
+import netifaces
+
+from vyos.config import Config
+from vyos.util import popen
+
+parser = argparse.ArgumentParser(description='Retrieve SNMP interfaces information')
+parser.add_argument('--ifindex', action='store', nargs='?', const='all', help='Show interface index')
+parser.add_argument('--ifalias', action='store', nargs='?', const='all', help='Show interface aliase')
+parser.add_argument('--ifdescr', action='store', nargs='?', const='all', help='Show interface description')
+
+def show_ifindex(intf):
+ out, err = popen(f'/bin/ip link show {intf}', decode='utf-8')
+ index = 'ifIndex = ' + out.split(':')[0]
+ return index.replace('\n', '')
+
+def show_ifalias(intf):
+ out, err = popen(f'/bin/ip link show {intf}', decode='utf-8')
+ alias = out.split('alias')[1].lstrip() if 'alias' in out else intf
+ return 'ifAlias = ' + alias.replace('\n', '')
+
+def show_ifdescr(i):
+ ven_id = ''
+ dev_id = ''
+
+ try:
+ with open(r'/sys/class/net/' + i + '/device/vendor', 'r') as f:
+ ven_id = f.read().replace('\n', '')
+ except FileNotFoundError:
+ pass
+
+ try:
+ with open(r'/sys/class/net/' + i + '/device/device', 'r') as f:
+ dev_id = f.read().replace('\n', '')
+ except FileNotFoundError:
+ pass
+
+ if ven_id == '' and dev_id == '':
+ ret = 'ifDescr = {0}'.format(i)
+ return ret
+
+ device = str(ven_id) + ':' + str(dev_id)
+ out, err = popen(f'/usr/bin/lspci -mm -d {device}', decode='utf-8')
+
+ vendor = ""
+ device = ""
+
+ # convert output to string
+ string = out.split('"')
+ if len(string) > 3:
+ vendor = string[3]
+
+ if len(string) > 5:
+ device = string[5]
+
+ ret = 'ifDescr = {0} {1}'.format(vendor, device)
+ return ret.replace('\n', '')
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service snmp'):
+ print("SNMP service is not configured")
+ sys.exit(0)
+
+ if args.ifindex:
+ if args.ifindex == 'all':
+ for i in netifaces.interfaces():
+ print('{0}: {1}'.format(i, show_ifindex(i)))
+ else:
+ print('{0}: {1}'.format(args.ifindex, show_ifindex(args.ifindex)))
+
+ elif args.ifalias:
+ if args.ifalias == 'all':
+ for i in netifaces.interfaces():
+ print('{0}: {1}'.format(i, show_ifalias(i)))
+ else:
+ print('{0}: {1}'.format(args.ifalias, show_ifalias(args.ifalias)))
+
+ elif args.ifdescr:
+ if args.ifdescr == 'all':
+ for i in netifaces.interfaces():
+ print('{0}: {1}'.format(i, show_ifdescr(i)))
+ else:
+ print('{0}: {1}'.format(args.ifdescr, show_ifdescr(args.ifdescr)))
+
+ else:
+ #eth0: ifIndex = 2
+ # ifAlias = NET-MYBLL-MUCI-BACKBONE
+ # ifDescr = VMware VMXNET3 Ethernet Controller
+ #lo: ifIndex = 1
+ for i in netifaces.interfaces():
+ print('{0}:\t{1}'.format(i, show_ifindex(i)))
+ print('\t{0}'.format(show_ifalias(i)))
+ print('\t{0}'.format(show_ifdescr(i)))
+
+ sys.exit(1)
diff --git a/src/op_mode/snmp_v3.py b/src/op_mode/snmp_v3.py
new file mode 100755
index 000000000..92601f15e
--- /dev/null
+++ b/src/op_mode/snmp_v3.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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/>.
+#
+# File: snmp_v3.py
+# Purpose:
+# Show SNMP v3 information
+# Used by the "run show snmp v3" commands.
+
+import sys
+import jinja2
+import argparse
+
+from vyos.config import Config
+
+parser = argparse.ArgumentParser(description='Retrieve SNMP v3 information')
+parser.add_argument('--all', action="store_true", help='Show all available information')
+parser.add_argument('--group', action="store_true", help='Show the list of configured groups')
+parser.add_argument('--trap', action="store_true", help='Show the list of configured targets')
+parser.add_argument('--user', action="store_true", help='Show the list of configured users')
+parser.add_argument('--view', action="store_true", help='Show the list of configured views')
+
+GROUP_OUTP_TMPL_SRC = """
+SNMPv3 Groups:
+
+ Group View
+ ----- ----
+ {% if group -%}{% for g in group -%}
+ {{ "%-20s" | format(g.name) }}{{ g.view }}({{ g.mode }})
+ {% endfor %}{% endif %}
+"""
+
+TRAPTGT_OUTP_TMPL_SRC = """
+SNMPv3 Trap-targets:
+
+ Tpap-target Port Protocol Auth Priv Type EngineID User
+ ----------- ---- -------- ---- ---- ---- -------- ----
+ {% if trap -%}{% for t in trap -%}
+ {{ "%-20s" | format(t.name) }} {{ t.port }} {{ t.proto }} {{ t.auth }} {{ t.priv }} {{ t.type }} {{ "%-32s" | format(t.engID) }} {{ t.user }}
+ {% endfor %}{% endif %}
+"""
+
+USER_OUTP_TMPL_SRC = """
+SNMPv3 Users:
+
+ User Auth Priv Mode Group
+ ---- ---- ---- ---- -----
+ {% if user -%}{% for u in user -%}
+ {{ "%-20s" | format(u.name) }}{{ u.auth }} {{ u.priv }} {{ u.mode }} {{ u.group }}
+ {% endfor %}{% endif %}
+"""
+
+VIEW_OUTP_TMPL_SRC = """
+SNMPv3 Views:
+ {% if view -%}{% for v in view %}
+ View : {{ v.name }}
+ OIDs : .{{ v.oids | join("\n .")}}
+ {% endfor %}{% endif %}
+"""
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ c = Config()
+ if not c.exists_effective('service snmp v3'):
+ print("SNMP v3 is not configured")
+ sys.exit(0)
+
+ data = {
+ 'group': [],
+ 'trap': [],
+ 'user': [],
+ 'view': []
+ }
+
+ if c.exists_effective('service snmp v3 group'):
+ for g in c.list_effective_nodes('service snmp v3 group'):
+ group = {
+ 'name': g,
+ 'mode': '',
+ 'view': ''
+ }
+ group['mode'] = c.return_effective_value('service snmp v3 group {0} mode'.format(g))
+ group['view'] = c.return_effective_value('service snmp v3 group {0} view'.format(g))
+
+ data['group'].append(group)
+
+ if c.exists_effective('service snmp v3 user'):
+ for u in c.list_effective_nodes('service snmp v3 user'):
+ user = {
+ 'name' : u,
+ 'mode' : '',
+ 'auth' : '',
+ 'priv' : '',
+ 'group': ''
+ }
+ user['mode'] = c.return_effective_value('service snmp v3 user {0} mode'.format(u))
+ user['auth'] = c.return_effective_value('service snmp v3 user {0} auth type'.format(u))
+ user['priv'] = c.return_effective_value('service snmp v3 user {0} privacy type'.format(u))
+ user['group'] = c.return_effective_value('service snmp v3 user {0} group'.format(u))
+
+ data['user'].append(user)
+
+ if c.exists_effective('service snmp v3 view'):
+ for v in c.list_effective_nodes('service snmp v3 view'):
+ view = {
+ 'name': v,
+ 'oids': []
+ }
+ view['oids'] = c.list_effective_nodes('service snmp v3 view {0} oid'.format(v))
+
+ data['view'].append(view)
+
+ if c.exists_effective('service snmp v3 trap-target'):
+ for t in c.list_effective_nodes('service snmp v3 trap-target'):
+ trap = {
+ 'name' : t,
+ 'port' : '',
+ 'proto': '',
+ 'auth' : '',
+ 'priv' : '',
+ 'type' : '',
+ 'engID': '',
+ 'user' : ''
+ }
+ trap['port'] = c.return_effective_value('service snmp v3 trap-target {0} port'.format(t))
+ trap['proto'] = c.return_effective_value('service snmp v3 trap-target {0} protocol'.format(t))
+ trap['auth'] = c.return_effective_value('service snmp v3 trap-target {0} auth type'.format(t))
+ trap['priv'] = c.return_effective_value('service snmp v3 trap-target {0} privacy type'.format(t))
+ trap['type'] = c.return_effective_value('service snmp v3 trap-target {0} type'.format(t))
+ trap['engID'] = c.return_effective_value('service snmp v3 trap-target {0} engineid'.format(t))
+ trap['user'] = c.return_effective_value('service snmp v3 trap-target {0} user'.format(t))
+
+ data['trap'].append(trap)
+
+ print(data)
+ if args.all:
+ # Special case, print all templates !
+ tmpl = jinja2.Template(GROUP_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+ tmpl = jinja2.Template(TRAPTGT_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+ tmpl = jinja2.Template(USER_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+ tmpl = jinja2.Template(VIEW_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.group:
+ tmpl = jinja2.Template(GROUP_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.trap:
+ tmpl = jinja2.Template(TRAPTGT_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.user:
+ tmpl = jinja2.Template(USER_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ elif args.view:
+ tmpl = jinja2.Template(VIEW_OUTP_TMPL_SRC)
+ print(tmpl.render(data))
+
+ else:
+ parser.print_help()
+
+ sys.exit(1)
diff --git a/src/op_mode/snmp_v3_showcerts.sh b/src/op_mode/snmp_v3_showcerts.sh
new file mode 100755
index 000000000..015b2e662
--- /dev/null
+++ b/src/op_mode/snmp_v3_showcerts.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+files=`sudo ls /etc/snmp/tls/certs/ 2> /dev/null`;
+if [ -n "$files" ]; then
+ sudo /usr/bin/net-snmp-cert showcerts --subject --fingerprint
+else
+ echo "You don't have any certificates. Put it in '/etc/snmp/tls/certs/' folder."
+fi
diff --git a/src/op_mode/system_integrity.py b/src/op_mode/system_integrity.py
new file mode 100755
index 000000000..c0e3d1095
--- /dev/null
+++ b/src/op_mode/system_integrity.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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 os
+import re
+import itertools
+from datetime import datetime, timedelta
+
+from vyos.util import cmd
+
+verf = r'/usr/libexec/vyos/op_mode/version.py'
+
+def get_sys_build_version():
+ if not os.path.exists(verf):
+ return None
+
+ a = cmd('/usr/libexec/vyos/op_mode/version.py')
+ if re.search('^Built on:.+',a, re.M) == None:
+ return None
+
+ dt = ( re.sub('Built on: +','', re.search('^Built on:.+',a, re.M).group(0)) )
+ return datetime.strptime(dt,'%a %d %b %Y %H:%M %Z')
+
+def check_pkgs(dt):
+ pkg_diffs = {
+ 'buildtime' : str(dt),
+ 'pkg' : {}
+ }
+
+ pkg_info = os.listdir('/var/lib/dpkg/info/')
+ for file in pkg_info:
+ if re.search('\.list$', file):
+ fts = os.stat('/var/lib/dpkg/info/' + file).st_mtime
+ dt_str = (datetime.utcfromtimestamp(fts).strftime('%Y-%m-%d %H:%M:%S'))
+ fdt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S')
+ if fdt > dt:
+ pkg_diffs['pkg'].update( { str(re.sub('\.list','',file)) : str(fdt)})
+
+ if len(pkg_diffs['pkg']) != 0:
+ return pkg_diffs
+ else:
+ return None
+
+def main():
+ dt = get_sys_build_version()
+ pkgs = check_pkgs(dt)
+ if pkgs != None:
+ print ("The following packages don\'t fit the image creation time\nbuild time:\t" + pkgs['buildtime'])
+ for k, v in pkgs['pkg'].items():
+ print ("installed: " + v + '\t' + k)
+
+if __name__ == '__main__':
+ sys.exit( main() )
+
diff --git a/src/op_mode/toggle_help_binding.sh b/src/op_mode/toggle_help_binding.sh
new file mode 100755
index 000000000..a8708f3da
--- /dev/null
+++ b/src/op_mode/toggle_help_binding.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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/>.
+
+# Script for [un-]binding the question mark key for getting help
+if [ "$1" == 'disable' ]; then
+ sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc
+ echo "bind '\"?\": self-insert' # vyatta key binding" >> $HOME/.bashrc
+ bind '"?": self-insert'
+else
+ sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc
+ bind '"?": possible-completions'
+fi
diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py
new file mode 100755
index 000000000..2c1db20bf
--- /dev/null
+++ b/src/op_mode/vrrp.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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 time
+import argparse
+import json
+import tabulate
+
+import vyos.util
+
+from vyos.ifconfig.vrrp import VRRP
+from vyos.ifconfig.vrrp import VRRPError, VRRPNoData
+
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("-s", "--summary", action="store_true", help="Print VRRP summary")
+group.add_argument("-t", "--statistics", action="store_true", help="Print VRRP statistics")
+group.add_argument("-d", "--data", action="store_true", help="Print detailed VRRP data")
+
+args = parser.parse_args()
+
+# Exit early if VRRP is dead or not configured
+if not VRRP.is_running():
+ print('VRRP is not running')
+ sys.exit(0)
+
+try:
+ if args.summary:
+ print(VRRP.format(VRRP.collect('json')))
+ elif args.statistics:
+ print(VRRP.collect('stats'))
+ elif args.data:
+ print(VRRP.collect('state'))
+ else:
+ parser.print_help()
+ sys.exit(1)
+except VRRPNoData as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py
new file mode 100755
index 000000000..e08bc983a
--- /dev/null
+++ b/src/op_mode/wireguard.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-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 os
+import sys
+import shutil
+import syslog as sl
+import re
+
+from vyos.config import Config
+from vyos.ifconfig import WireGuardIf
+from vyos.util import cmd
+from vyos.util import run
+from vyos.util import check_kmod
+from vyos import ConfigError
+
+dir = r'/config/auth/wireguard'
+psk = dir + '/preshared.key'
+
+k_mod = 'wireguard'
+
+def generate_keypair(pk, pub):
+ """ generates a keypair which is stored in /config/auth/wireguard """
+ old_umask = os.umask(0o027)
+ if run(f'wg genkey | tee {pk} | wg pubkey > {pub}') != 0:
+ raise ConfigError("wireguard key-pair generation failed")
+ else:
+ sl.syslog(
+ sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir)
+ os.umask(old_umask)
+
+
+def genkey(location):
+ """ helper function to check, regenerate the keypair """
+ pk = "{}/private.key".format(location)
+ pub = "{}/public.key".format(location)
+ old_umask = os.umask(0o027)
+ if os.path.exists(pk) and os.path.exists(pub):
+ try:
+ choice = input(
+ "You already have a wireguard key-pair, do you want to re-generate? [y/n] ")
+ if choice == 'y' or choice == 'Y':
+ generate_keypair(pk, pub)
+ except KeyboardInterrupt:
+ sys.exit(0)
+ else:
+ """ if keypair is bing executed from a running iso """
+ if not os.path.exists(location):
+ run(f'sudo mkdir -p {location}')
+ run(f'sudo chgrp vyattacfg {location}')
+ run(f'sudo chmod 750 {location}')
+ generate_keypair(pk, pub)
+ os.umask(old_umask)
+
+
+def showkey(key):
+ """ helper function to show privkey or pubkey """
+ if os.path.exists(key):
+ print (open(key).read().strip())
+ else:
+ print ("{} not found".format(key))
+
+
+def genpsk():
+ """
+ generates a preshared key and shows it on stdout,
+ it's stored only in the cli config
+ """
+
+ psk = cmd('wg genpsk')
+ print(psk)
+
+def list_key_dirs():
+ """ lists all dirs under /config/auth/wireguard """
+ if os.path.exists(dir):
+ nks = next(os.walk(dir))[1]
+ for nk in nks:
+ print (nk)
+
+def del_key_dir(kname):
+ """ deletes /config/auth/wireguard/<kname> """
+ kdir = "{0}/{1}".format(dir,kname)
+ if not os.path.isdir(kdir):
+ print ("named keypair {} not found".format(kname))
+ return 1
+ shutil.rmtree(kdir)
+
+
+if __name__ == '__main__':
+ check_kmod(k_mod)
+ parser = argparse.ArgumentParser(description='wireguard key management')
+ parser.add_argument(
+ '--genkey', action="store_true", help='generate key-pair')
+ parser.add_argument(
+ '--showpub', action="store_true", help='shows public key')
+ parser.add_argument(
+ '--showpriv', action="store_true", help='shows private key')
+ parser.add_argument(
+ '--genpsk', action="store_true", help='generates preshared-key')
+ parser.add_argument(
+ '--location', action="store", help='key location within {}'.format(dir))
+ parser.add_argument(
+ '--listkdir', action="store_true", help='lists named keydirectories')
+ parser.add_argument(
+ '--delkdir', action="store_true", help='removes named keydirectories')
+ parser.add_argument(
+ '--showinterface', action="store", help='shows interface details')
+ args = parser.parse_args()
+
+ try:
+ if args.genkey:
+ if args.location:
+ genkey("{0}/{1}".format(dir, args.location))
+ else:
+ genkey("{}/default".format(dir))
+ if args.showpub:
+ if args.location:
+ showkey("{0}/{1}/public.key".format(dir, args.location))
+ else:
+ showkey("{}/default/public.key".format(dir))
+ if args.showpriv:
+ if args.location:
+ showkey("{0}/{1}/private.key".format(dir, args.location))
+ else:
+ showkey("{}/default/private.key".format(dir))
+ if args.genpsk:
+ genpsk()
+ if args.listkdir:
+ list_key_dirs()
+ if args.showinterface:
+ try:
+ intf = WireGuardIf(args.showinterface, create=False, debug=False)
+ print(intf.operational.show_interface())
+ # the interface does not exists
+ except Exception:
+ pass
+ if args.delkdir:
+ if args.location:
+ del_key_dir(args.location)
+ else:
+ del_key_dir("default")
+
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)