summaryrefslogtreecommitdiff
path: root/src/op_mode/tech_support.py
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@baturin.org>2024-07-01 12:56:25 +0100
committerDaniil Baturin <daniil@baturin.org>2024-07-03 13:00:28 +0100
commit7958c9794813f399a2e113896410c0c03d29663d (patch)
tree6375caded54c44aa0ddbf0ce7927dfc3faef2ec0 /src/op_mode/tech_support.py
parente270712f7ebd76e4e1be598766d999cef4f05e26 (diff)
downloadvyos-1x-7958c9794813f399a2e113896410c0c03d29663d.tar.gz
vyos-1x-7958c9794813f399a2e113896410c0c03d29663d.zip
op-mode: T6498: add machine-readable tech support report script
Diffstat (limited to 'src/op_mode/tech_support.py')
-rw-r--r--src/op_mode/tech_support.py394
1 files changed, 394 insertions, 0 deletions
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)