summaryrefslogtreecommitdiff
path: root/src/op_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/op_mode')
-rwxr-xr-xsrc/op_mode/nat.py56
-rwxr-xr-xsrc/op_mode/powerctrl.py6
-rw-r--r--src/op_mode/tech_support.py394
3 files changed, 436 insertions, 20 deletions
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index 16a545cda..c6cf4770a 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -31,6 +31,7 @@ from vyos.utils.dict import dict_search
ArgDirection = typing.Literal['source', 'destination']
ArgFamily = typing.Literal['inet', 'inet6']
+
def _get_xml_translation(direction, family, address=None):
"""
Get conntrack XML output --src-nat|--dst-nat
@@ -99,22 +100,35 @@ def _get_raw_translation(direction, family, address=None):
def _get_formatted_output_rules(data, direction, family):
- def _get_ports_for_output(my_dict):
- # Get and insert all configured ports or port ranges into output string
- for index, port in enumerate(my_dict['set']):
- if 'range' in str(my_dict['set'][index]):
- output = my_dict['set'][index]['range']
- output = '-'.join(map(str, output))
- else:
- output = str(port)
- if index == 0:
- output = str(output)
- else:
- output = ','.join([output,output])
- # Handle case where configured ports are a negated list
- if my_dict['op'] == '!=':
- output = '!' + output
- return(output)
+
+
+ def _get_ports_for_output(rules):
+ """
+ Return: string of configured ports
+ """
+ ports = []
+ if 'set' in rules:
+ for index, port in enumerate(rules['set']):
+ if 'range' in str(rules['set'][index]):
+ output = rules['set'][index]['range']
+ output = '-'.join(map(str, output))
+ else:
+ output = str(port)
+ ports.append(output)
+ # When NAT rule contains port range or single port
+ # JSON will not contain keyword 'set'
+ elif 'range' in rules:
+ output = rules['range']
+ output = '-'.join(map(str, output))
+ ports.append(output)
+ else:
+ output = rules['right']
+ ports.append(str(output))
+ result = ','.join(ports)
+ # Handle case where ports in NAT rule are negated
+ if rules['op'] == '!=':
+ result = '!' + result
+ return(result)
# Add default values before loop
sport, dport, proto = 'any', 'any', 'any'
@@ -132,7 +146,10 @@ def _get_formatted_output_rules(data, direction, family):
if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any'
for index, match in enumerate(jmespath.search('rule.expr[*].match', rule)):
if 'payload' in match['left']:
- if isinstance(match['right'], dict) and ('prefix' in match['right'] or 'set' in match['right']):
+ # Handle NAT rule containing comma-seperated list of ports
+ if (isinstance(match['right'], dict) and
+ ('prefix' in match['right'] or 'set' in match['right'] or
+ 'range' in match['right'])):
# Merge dict src/dst l3_l4 parameters
my_dict = {**match['left']['payload'], **match['right']}
my_dict['op'] = match['op']
@@ -146,6 +163,7 @@ def _get_formatted_output_rules(data, direction, family):
sport = _get_ports_for_output(my_dict)
elif my_dict['field'] == 'dport':
dport = _get_ports_for_output(my_dict)
+ # Handle NAT rule containing a single port
else:
field = jmespath.search('left.payload.field', match)
if field == 'saddr':
@@ -153,9 +171,9 @@ def _get_formatted_output_rules(data, direction, family):
elif field == 'daddr':
daddr = match.get('right')
elif field == 'sport':
- sport = match.get('right')
+ sport = _get_ports_for_output(match)
elif field == 'dport':
- dport = match.get('right')
+ dport = _get_ports_for_output(match)
else:
saddr = '::/0' if family == 'inet6' else '0.0.0.0/0'
daddr = '::/0' if family == 'inet6' else '0.0.0.0/0'
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index cb4a175dd..fb6b54776 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -117,11 +117,15 @@ def check_unsaved_config():
pass
def execute_shutdown(time, reboot=True, ask=True):
+ from vyos.utils.process import cmd
+
check_unsaved_config()
+ host = cmd("hostname --fqdn")
+
action = "reboot" if reboot else "poweroff"
if not ask:
- if not ask_yes_no(f"Are you sure you want to {action} this system?"):
+ if not ask_yes_no(f"Are you sure you want to {action} this system ({host})?"):
exit(0)
action_cmd = "-r" if reboot else "-P"
diff --git a/src/op_mode/tech_support.py b/src/op_mode/tech_support.py
new file mode 100644
index 000000000..f60bb87ff
--- /dev/null
+++ b/src/op_mode/tech_support.py
@@ -0,0 +1,394 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import json
+
+import vyos.opmode
+
+from vyos.utils.process import cmd
+
+def _get_version_data():
+ from vyos.version import get_version_data
+ return get_version_data()
+
+def _get_uptime():
+ from vyos.utils.system import get_uptime_seconds
+
+ return get_uptime_seconds()
+
+def _get_load_average():
+ from vyos.utils.system import get_load_averages
+
+ return get_load_averages()
+
+def _get_cpus():
+ from vyos.utils.cpu import get_cpus
+
+ return get_cpus()
+
+def _get_process_stats():
+ return cmd('top --iterations 1 --batch-mode --accum-time-toggle')
+
+def _get_storage():
+ from vyos.utils.disk import get_persistent_storage_stats
+
+ return get_persistent_storage_stats()
+
+def _get_devices():
+ devices = {}
+ devices["pci"] = cmd("lspci")
+ devices["usb"] = cmd("lsusb")
+
+ return devices
+
+def _get_memory():
+ from vyos.utils.file import read_file
+
+ return read_file("/proc/meminfo")
+
+def _get_processes():
+ res = cmd("ps aux")
+
+ return res
+
+def _get_interrupts():
+ from vyos.utils.file import read_file
+
+ interrupts = read_file("/proc/interrupts")
+ softirqs = read_file("/proc/softirqs")
+
+ return (interrupts, softirqs)
+
+def _get_partitions():
+ # XXX: as of parted 3.5, --json is completely broken
+ # and cannot be used (outputs malformed JSON syntax)
+ res = cmd(f"parted --list")
+
+ return res
+
+def _get_running_config():
+ from os import getpid
+ from vyos.configsession import ConfigSession
+ from vyos.utils.strip_config import strip_config_source
+
+ c = ConfigSession(getpid())
+ return strip_config_source(c.show_config([]))
+
+def _get_boot_config():
+ from vyos.utils.file import read_file
+ from vyos.utils.strip_config import strip_config_source
+
+ config = read_file('/opt/vyatta/etc/config.boot.default')
+
+ return strip_config_source(config)
+
+def _get_config_scripts():
+ from os import listdir
+ from os.path import join
+ from vyos.utils.file import read_file
+
+ scripts = []
+
+ dir = '/config/scripts'
+ for f in listdir(dir):
+ script = {}
+ path = join(dir, f)
+ data = read_file(path)
+ script["path"] = path
+ script["data"] = data
+
+ scripts.append(script)
+
+ return scripts
+
+def _get_nic_data():
+ from vyos.utils.process import ip_cmd
+ link_data = ip_cmd("link show")
+ addr_data = ip_cmd("address show")
+
+ return link_data, addr_data
+
+def _get_routes(proto):
+ from json import loads
+ from vyos.utils.process import ip_cmd
+
+ # Only include complete routing tables if they are not too large
+ # At the moment "too large" is arbitrarily set to 1000
+ MAX_ROUTES = 1000
+
+ data = {}
+
+ summary = cmd(f"vtysh -c 'show {proto} route summary json'")
+ summary = loads(summary)
+
+ data["summary"] = summary
+
+ if summary["routesTotal"] < MAX_ROUTES:
+ rib_routes = cmd(f"vtysh -c 'show {proto} route json'")
+ data["routes"] = loads(rib_routes)
+
+ if summary["routesTotalFib"] < MAX_ROUTES:
+ ip_proto = "-4" if proto == "ip" else "-6"
+ fib_routes = ip_cmd(f"{ip_proto} route show")
+ data["fib_routes"] = fib_routes
+
+ return data
+
+def _get_ip_routes():
+ return _get_routes("ip")
+
+def _get_ipv6_routes():
+ return _get_routes("ipv6")
+
+def _get_ospfv2():
+ # XXX: OSPF output when it's not configured is an empty string,
+ # which is not a valid JSON
+ output = cmd("vtysh -c 'show ip ospf json'")
+ if output:
+ return json.loads(output)
+ else:
+ return {}
+
+def _get_ospfv3():
+ output = cmd("vtysh -c 'show ipv6 ospf6 json'")
+ if output:
+ return json.loads(output)
+ else:
+ return {}
+
+def _get_bgp_summary():
+ output = cmd("vtysh -c 'show bgp summary json'")
+ return json.loads(output)
+
+def _get_isis():
+ output = cmd("vtysh -c 'show isis summary json'")
+ if output:
+ return json.loads(output)
+ else:
+ return {}
+
+def _get_arp_table():
+ from json import loads
+ from vyos.utils.process import cmd
+
+ arp_table = cmd("ip --json -4 neighbor show")
+ return loads(arp_table)
+
+def _get_ndp_table():
+ from json import loads
+
+ arp_table = cmd("ip --json -6 neighbor show")
+ return loads(arp_table)
+
+def _get_nftables_rules():
+ nft_rules = cmd("nft list ruleset")
+ return nft_rules
+
+def _get_connections():
+ from vyos.utils.process import cmd
+
+ return cmd("ss -apO")
+
+def _get_system_packages():
+ from re import split
+ from vyos.utils.process import cmd
+
+ dpkg_out = cmd(''' dpkg-query -W -f='${Package} ${Version} ${Architecture} ${db:Status-Abbrev}\n' ''')
+ pkg_lines = split(r'\n+', dpkg_out)
+
+ # Discard the header, it's five lines long
+ pkg_lines = pkg_lines[5:]
+
+ pkgs = []
+
+ for pl in pkg_lines:
+ parts = split(r'\s+', pl)
+ pkg = {}
+ pkg["name"] = parts[0]
+ pkg["version"] = parts[1]
+ pkg["architecture"] = parts[2]
+ pkg["status"] = parts[3]
+
+ pkgs.append(pkg)
+
+ return pkgs
+
+def _get_image_info():
+ from vyos.system.image import get_images_details
+
+ return get_images_details()
+
+def _get_kernel_modules():
+ from vyos.utils.kernel import lsmod
+
+ return lsmod()
+
+def _get_last_logs(max):
+ from systemd import journal
+
+ r = journal.Reader()
+
+ # Set the reader to use logs from the current boot
+ r.this_boot()
+
+ # Jump to the last logs
+ r.seek_tail()
+
+ # Only get logs of INFO level or more urgent
+ r.log_level(journal.LOG_INFO)
+
+ # Retrieve the entries
+ entries = []
+
+ # I couldn't find a way to just get last/first N entries,
+ # so we'll use the cursor directly.
+ num = max
+ while num >= 0:
+ je = r.get_previous()
+ entry = {}
+
+ # Extract the most useful and serializable fields
+ entry["timestamp"] = je.get("SYSLOG_TIMESTAMP")
+ entry["pid"] = je.get("SYSLOG_PID")
+ entry["identifier"] = je.get("SYSLOG_IDENTIFIER")
+ entry["facility"] = je.get("SYSLOG_FACILITY")
+ entry["systemd_unit"] = je.get("_SYSTEMD_UNIT")
+ entry["message"] = je.get("MESSAGE")
+
+ entries.append(entry)
+
+ num = num - 1
+
+ return entries
+
+
+def _get_raw_data():
+ data = {}
+
+ # VyOS-specific information
+ data["vyos"] = {}
+
+ ## The equivalent of "show version"
+ from vyos.version import get_version_data
+ data["vyos"]["version"] = _get_version_data()
+
+ ## Installed images
+ data["vyos"]["images"] = _get_image_info()
+
+ # System information
+ data["system"] = {}
+
+ ## Uptime and load averages
+ data["system"]["uptime"] = _get_uptime()
+ data["system"]["load_average"] = _get_load_average()
+ data["system"]["process_stats"] = _get_process_stats()
+
+ ## Debian packages
+ data["system"]["packages"] = _get_system_packages()
+
+ ## Kernel modules
+ data["system"]["kernel"] = {}
+ data["system"]["kernel"]["modules"] = _get_kernel_modules()
+
+ ## Processes
+ data["system"]["processes"] = _get_processes()
+
+ ## Interrupts
+ interrupts, softirqs = _get_interrupts()
+ data["system"]["interrupts"] = interrupts
+ data["system"]["softirqs"] = softirqs
+
+ # Hardware
+ data["hardware"] = {}
+ data["hardware"]["cpu"] = _get_cpus()
+ data["hardware"]["storage"] = _get_storage()
+ data["hardware"]["partitions"] = _get_partitions()
+ data["hardware"]["devices"] = _get_devices()
+ data["hardware"]["memory"] = _get_memory()
+
+ # Configuration data
+ data["vyos"]["config"] = {}
+
+ ## Running config text
+ ## We do not encode it so that it's possible to
+ ## see exactly what the user sees and detect any syntax/rendering anomalies —
+ ## exporting the config to JSON could obscure them
+ data["vyos"]["config"]["running"] = _get_running_config()
+
+ ## Default boot config, exactly as in /config/config.boot
+ ## It may be different from the running config
+ ## _and_ may have its own syntax quirks that may point at bugs
+ data["vyos"]["config"]["boot"] = _get_boot_config()
+
+ ## Config scripts
+ data["vyos"]["config"]["scripts"] = _get_config_scripts()
+
+ # Network interfaces
+ data["network_interfaces"] = {}
+
+ # Interface data from iproute2
+ link_data, addr_data = _get_nic_data()
+ data["network_interfaces"]["links"] = link_data
+ data["network_interfaces"]["addresses"] = addr_data
+
+ # Routing table data
+ data["routing"] = {}
+ data["routing"]["ip"] = _get_ip_routes()
+ data["routing"]["ipv6"] = _get_ipv6_routes()
+
+ # Routing protocols
+ data["routing"]["ip"]["ospf"] = _get_ospfv2()
+ data["routing"]["ipv6"]["ospfv3"] = _get_ospfv3()
+
+ data["routing"]["bgp"] = {}
+ data["routing"]["bgp"]["summary"] = _get_bgp_summary()
+
+ data["routing"]["isis"] = _get_isis()
+
+ # ARP and NDP neighbor tables
+ data["neighbor_tables"] = {}
+ data["neighbor_tables"]["arp"] = _get_arp_table()
+ data["neighbor_tables"]["ndp"] = _get_ndp_table()
+
+ # nftables config
+ data["nftables_rules"] = _get_nftables_rules()
+
+ # All connections
+ data["connections"] = _get_connections()
+
+ # Logs
+ data["last_logs"] = _get_last_logs(1000)
+
+ return data
+
+def show(raw: bool):
+ data = _get_raw_data()
+ if raw:
+ return data
+ else:
+ raise vyos.opmode.UnsupportedOperation("Formatted output is not implemented yet")
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
+ except (KeyboardInterrupt, BrokenPipeError):
+ sys.exit(1)