diff options
| author | Christian Breunig <christian@breunig.cc> | 2026-03-05 16:21:13 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-05 16:21:13 +0100 |
| commit | 5ea47f5c7dab6cee38aef355fd78d434a10dd463 (patch) | |
| tree | 938a3bed44be5b451178c22e87364be2e592da50 /src | |
| parent | fcf57444ab7150e06082627fe196de3c9a87d9e1 (diff) | |
| parent | 1359f690af841ea7c28028e907b1d5f15f3d90b8 (diff) | |
| download | vyos-1x-5ea47f5c7dab6cee38aef355fd78d434a10dd463.tar.gz vyos-1x-5ea47f5c7dab6cee38aef355fd78d434a10dd463.zip | |
Merge pull request #4967 from alexandr-san4ez/T8215-current
tech-support: T8215: Extend tech-support archive and report generation
Diffstat (limited to 'src')
| -rwxr-xr-x | src/op_mode/generate_tech-support_archive.py | 212 | ||||
| -rw-r--r-- | src/op_mode/show_techsupport_report.py | 719 | ||||
| -rw-r--r-- | src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run | 15 |
3 files changed, 639 insertions, 307 deletions
diff --git a/src/op_mode/generate_tech-support_archive.py b/src/op_mode/generate_tech-support_archive.py index 92442b78d..1304ff04b 100755 --- a/src/op_mode/generate_tech-support_archive.py +++ b/src/op_mode/generate_tech-support_archive.py @@ -13,35 +13,32 @@ # # 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 glob from datetime import datetime from pathlib import Path from shutil import rmtree - from socket import gethostname from sys import exit from tarfile import open as tar_open + +from vyos.defaults import directories from vyos.utils.process import call -from vyos.utils.process import rc_cmd +from vyos.utils.process import cmd +from vyos.utils.file import get_name_from_path from vyos.remote import upload -def op(cmd: str) -> str: - """Returns a command with the VyOS operational mode wrapper.""" - return f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}' - -def save_stdout(command: str, file: Path) -> None: - rc, stdout = rc_cmd(command) - body: str = f'''### {command} ### -Command: {command} -Exit code: {rc} -Stdout: -{stdout} - -''' - with file.open(mode='a') as f: - f.write(body) + +# Example: bdbdd9a4807f_tech-support-archive_2026-02-02T13-53-18 +ARCHIVE_PATTERN = '_tech-support-archive_' +# Example: drops-debug_2026-02-02T13-53-18 +ARCHIVE_TMP_DIR_PATTERN = 'drops-debug_' +DEFAULT_TMP_DIR = '/tmp' +EXCLUDED_ARCHIVE_EXT = ('.iso', '.gz', '.tar', '.zip') + + def __rotate_logs(path: str, log_pattern:str): files_list = glob.glob(f'{path}/{log_pattern}') if len(files_list) > 5: @@ -49,12 +46,59 @@ def __rotate_logs(path: str, log_pattern:str): os.remove(oldest_file) +def __save_show_report_files(reports_dir: Path): + """ + Save result of execution `show tech-support report` command + :param reports_dir: path to the result directory + :type reports_dir: pathlib.Path + """ + + vyos_op_scripts_dir = directories['op_mode'] + script_path = f'{vyos_op_scripts_dir}/show_techsupport_report.py' + arguments = [ + '--launched-from-generate-archive', + '--outdir', + str(reports_dir), + ] + output = cmd([script_path] + arguments) + + if output.strip(): + print(output) + + def __generate_archived_files(location_path: str) -> None: """ Generate arhives of main directories :param location_path: path to temporary directory :type location_path: str """ + + # sync/flush journald before archiving /var/log/journal + cmd(['journalctl', '--sync']) + cmd(['journalctl', '--flush']) + + def __tar_filter(tarinfo): + # path inside tar, because we set arcname=... below + name = tarinfo.name + basename = os.path.basename(name) + + # /var/log: exclude /var/log/messages and /var/log/messages.* + if name.startswith('var/log/messages'): + if basename == 'messages' or basename.startswith('messages.'): + return None + + # /tmp, /home: exclude previous tech-support archives and temporary archive directories + if name.startswith(('tmp/', 'home/')): + if ARCHIVE_PATTERN in name or basename.startswith(ARCHIVE_TMP_DIR_PATTERN): + return None + + # /home, /opt/vyatta/etc/config, /tmp: exclude general archives + if name.startswith(('home/', 'opt/vyatta/etc/config/', 'tmp/')): + if basename.lower().endswith(EXCLUDED_ARCHIVE_EXT): + return None + + return tarinfo + # Dictionary arhive_name:directory_to_arhive archive_dict = { 'etc': '/etc', @@ -63,22 +107,23 @@ def __generate_archived_files(location_path: str) -> None: 'root': '/root', 'tmp': '/tmp', 'core-dump': '/var/core', - 'config': '/opt/vyatta/etc/config' - } - # Dictionary arhive_name:excluding pattern - archive_excludes = { - # Old location of archives - 'config': 'tech-support-archive', - # New locations of arhives - 'tmp': 'tech-support-archive' + 'config': '/opt/vyatta/etc/config', + 'run': '/run', } + for archive_name, path in archive_dict.items(): - archive_file: str = f'{location_path}/{archive_name}.tar.gz' + if not os.path.exists(path): + continue + + arcname = str(path).lstrip('/') # e.g. /etc -> 'etc' + + archive_file = f'{location_path}/{archive_name}.tar.gz' with tar_open(name=archive_file, mode='x:gz') as tar_file: - if archive_name in archive_excludes: - tar_file.add(path, filter=lambda x: None if str(archive_excludes[archive_name]) in str(x.name) else x) - else: - tar_file.add(path) + try: + tar_file.add(path, arcname=arcname, filter=__tar_filter) + except (PermissionError, OSError) as e: + print(f'Unable to read `{path}` to archive files:', e) + continue # skip paths we can't read def __generate_main_archive_file(archive_file: str, tmp_dir_path: str) -> None: @@ -89,8 +134,10 @@ def __generate_main_archive_file(archive_file: str, tmp_dir_path: str) -> None: :param tmp_dir_path: path to arhive memeber :type tmp_dir_path: str """ + + arcname = get_name_from_path(archive_file) with tar_open(name=archive_file, mode='x:gz') as tar_file: - tar_file.add(tmp_dir_path, arcname=os.path.basename(tmp_dir_path)) + tar_file.add(tmp_dir_path, arcname=arcname) def __generate_topology_snapshots(output_dir: Path) -> None: """ @@ -109,61 +156,100 @@ def __generate_topology_snapshots(output_dir: Path) -> None: call(['lstopo', '--logical', '--output-format', 'png', str(logical_topo)]) +def __resolve_main_archive_path(input_path: str, default_archive_name: str) -> Path: + """ + Normalize path for saving a .tar.gz file based on rules: + + Rules: + - file -> file.tar.gz + - file.tar -> file.tar.gz + - file.tgz -> file.tgz + - dir/ -> dir/{default_archive_name} + - ../dir/file.tar.gz -> (unchanged) + - file.zip -> file.tar.gz + + :param input_path: user's provided path to the archive + :param default_archive_name: name of archive if user didn't provide it + """ + + path = Path(input_path) + + # Case 1: default temporary directory -> extend by default name of file + if input_path == DEFAULT_TMP_DIR: + return path / default_archive_name + + # Case 2: already .tar.gz -> return unchanged + if path.name.endswith(('.tar.gz', '.tgz')): + return path + + # Case 3: already .tar -> .tar.gz + if path.name.endswith('.tar'): + return path.with_suffix('.tar.gz') + + # Case 4: directory (explicit trailing slash OR existing directory) + if input_path.endswith(('/', '\\')) or path.is_dir(): + dir_path = path + return dir_path / default_archive_name + + # Default behavior for any other extension + return path.with_suffix('.tar.gz') + + if __name__ == '__main__': - defualt_tmp_dir = '/tmp' parser = argparse.ArgumentParser() - parser.add_argument("path", nargs='?', default=defualt_tmp_dir) + parser.add_argument('path', nargs='?', default=DEFAULT_TMP_DIR) args = parser.parse_args() - location_path = args.path[:-1] if args.path[-1] == '/' else args.path hostname: str = gethostname() - time_now: str = datetime.now().isoformat(timespec='seconds').replace(":", "-") - - remote = False - tmp_path = '' - tmp_dir_path = '' - if 'ftp://' in args.path or 'scp://' in args.path: - remote = True - tmp_path = defualt_tmp_dir + time_now: str = datetime.now().isoformat(timespec='seconds').replace(':', '-') + default_archive_inner_dir = f'{hostname}{ARCHIVE_PATTERN}{time_now}' + default_archive_name = f'{default_archive_inner_dir}.tar.gz' + + is_remote = args.path.startswith(('ftp://', 'scp://')) + if is_remote: + base_tmp_path = DEFAULT_TMP_DIR + archive_dest_path = Path(f'{base_tmp_path}/{default_archive_name}') else: - tmp_path = location_path - archive_pattern = f'_tech-support-archive_' - archive_file_name = f'{hostname}{archive_pattern}{time_now}.tar.gz' + # Define destination path to the main archive file based on a rules + archive_dest_path = __resolve_main_archive_path(args.path, default_archive_name) + base_tmp_path = str(archive_dest_path.parent) + default_archive_name = archive_dest_path.name + default_archive_inner_dir = get_name_from_path(archive_dest_path.name) # Log rotation in tmp directory - if tmp_path == defualt_tmp_dir: - __rotate_logs(tmp_path, f'*{archive_pattern}*') + if base_tmp_path == DEFAULT_TMP_DIR: + __rotate_logs(base_tmp_path, f'*{ARCHIVE_PATTERN}*') # Temporary directory creation - tmp_dir_path = f'{tmp_path}/drops-debug_{time_now}' - tmp_dir: Path = Path(tmp_dir_path) + tmp_dir: Path = Path(f'{base_tmp_path}/{ARCHIVE_TMP_DIR_PATTERN}{time_now}') tmp_dir.mkdir(parents=True) - report_file: Path = Path(f'{tmp_dir_path}/show_tech-support_report.txt') - report_file.touch() + # Directory which contains list of 'tech-support' reports + reports_dir: Path = Path(f'{tmp_dir}/show_tech-support_report') # Call the topology snapshot function here __generate_topology_snapshots(tmp_dir) try: + # Generate files using `show tech-support report` command + __save_show_report_files(reports_dir) - save_stdout(op('show tech-support report'), report_file) # Generate included archives - __generate_archived_files(tmp_dir_path) + __generate_archived_files(tmp_dir) # Generate main archive - __generate_main_archive_file(f'{tmp_path}/{archive_file_name}', tmp_dir_path) - # Delete temporary directory - rmtree(tmp_dir) + __generate_main_archive_file(archive_dest_path, tmp_dir) + # Upload to remote site if it is scpecified - if remote: - upload(f'{tmp_path}/{archive_file_name}', args.path) - print(f'Debug file is generated and located in {location_path}/{archive_file_name}') + if is_remote: + upload_uri = args.path + upload(str(archive_dest_path), upload_uri) except Exception as err: print(f'Error during generating a debug file: {err}') - # cleanup - if tmp_dir.exists(): - rmtree(tmp_dir) + else: + print(f'Debug file is generated and located in {archive_dest_path}') finally: - # cleanup + # Delete temporary directory + if tmp_dir.exists(): + rmtree(tmp_dir, ignore_errors=True) exit() diff --git a/src/op_mode/show_techsupport_report.py b/src/op_mode/show_techsupport_report.py index f22b5054a..2434da29a 100644 --- a/src/op_mode/show_techsupport_report.py +++ b/src/op_mode/show_techsupport_report.py @@ -16,235 +16,321 @@ import os import sys -from typing import List +import argparse +from pathlib import Path +from dataclasses import dataclass +from dataclasses import field +from typing import Callable +from typing import Optional +from typing import Sequence + from vyos.ifconfig import Section from vyos.ifconfig import Interface from vyos.utils.process import rc_cmd +from vyos.utils.process import wrap_op as op -def print_header(command: str) -> None: - """Prints a command with headers '-'. - - Example: +@dataclass(frozen=True) +class BaseSpec: + # Available only for 'show tech-support report' (not 'generate tech-support archive') + report_only: Optional[bool] = field(kw_only=True, default=False) - % print_header('Example command') - --------------- - Example command - --------------- - """ - header_length = len(command) * '-' - print(f"\n{header_length}\n{command}\n{header_length}") +@dataclass(frozen=True) +class CommandSpec(BaseSpec): + header: str # Display header for a command section + command: str # Shell command to execute -def execute_command(command: str, header_text: str) -> None: - """Executes a command and prints the output with a header. +@dataclass(frozen=True) +class FuncSpec(BaseSpec): + name: str # Display name for a function section + fn: Callable[ + ['Runner'], None + ] # Callable that receives a Runner and writes output via it - Example: - % execute_command('uptime', "Uptime of the system") - -------------------- - Uptime of the system - -------------------- - 20:21:57 up 9:04, 5 users, load average: 0.00, 0.00, 0.0 +class OutputSink: + """Writes output to stdout and/or a file, depending on parameters""" - """ - print_header(header_text) - try: - rc, output = rc_cmd(command) - # Enable unbuffered print param to improve responsiveness of printed - # output to end user - print(output, flush=True) - # Exit gracefully when user interrupts program output - # Flush standard streams; redirect remaining output to devnull - # Resolves T5633: Bug #1 and 3 - except (BrokenPipeError, KeyboardInterrupt): - os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) # pylint: disable = no-member - sys.exit(1) - except Exception as e: - print(f"Error executing command: {command}") - print(f"Error message: {e}") + def __init__(self, *, file_path: Optional[Path]): + # Target file path; None means stdout + self._file_path = file_path + self._fh = None + def __enter__(self) -> 'OutputSink': + # Open output file if not writing to stdout + if not self.is_stdout: + self._file_path.parent.mkdir(parents=True, exist_ok=True) + # Use text mode, overwrite per run + self._fh = self._file_path.open('w', encoding='utf-8', errors='replace') + return self -def op(cmd: str) -> str: - """Returns a command with the VyOS operational mode wrapper.""" - return f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}' + def __exit__(self, exc_type, exc, tb): + if self._fh is not None: + self._fh.close() + self._fh = None + @property + def is_stdout(self): + # True when output is directed to stdout + return self._file_path is None -def get_ethernet_interfaces() -> List[Interface]: + def write(self, text: str): + # Enable unbuffered print param to improve responsiveness of printed + # output to end user + if self.is_stdout: + print(text, end='', flush=True) + else: + if self._fh is not None: + self._fh.write(text) + self._fh.flush() + + +class Runner: + """Executes commands and writes output to the provided sink""" + + def __init__(self, sink: OutputSink): + self.sink = sink + + def exec(self, command: str, header: str = None): + texts = [] + + if header: + texts.append(header) + texts.append(f'Command: {command}') + + try: + # Start a new section for this command + self.section('\n'.join(texts)) + + rc, output = rc_cmd(command) + # Ensure output ends with newline when present + if output and not output.endswith('\n'): + output += '\n' + + self.sink.write(output) + if rc not in (0, None): + self.sink.write( + f'Command `{command}` returned non-zero ({rc}) exit status\n' + ) + except (BrokenPipeError, KeyboardInterrupt): + raise + except Exception as e: + self.sink.write(f'Error executing command: {command}\n') + self.sink.write(f'Error message: {e}\n') + + def section(self, title: str): + """Just print a section header without running a command""" + self.sink.write(header_block(title)) + + +def header_block(title: str, delimiter='-') -> str: + """Create an underline/overline header block for multiline text.""" + lines = title.splitlines() + max_len = max(len(line) for line in lines) + line = delimiter * max_len + title_block = '\n'.join(lines) + return f'\n{line}\n{title_block}\n{line}\n' + + +def select_reports(args: argparse.Namespace) -> dict[str, tuple[BaseSpec]]: + reports = REPORTS.copy() + requested = args.reports + + if requested: + missing = [r for r in requested if r not in reports] + if missing: + known = ', '.join(sorted(reports.keys())) + raise SystemExit(f'Unknown report(s): {", ".join(missing)}. Known: {known}') + + reports = {name: reports[name] for name in requested} + + if args.launched_from_generate_archive: + for key in reports.keys(): + items = reports[key] + reports[key] = {item for item in items if not item.report_only} + + return reports + + +def execute_item(item: BaseSpec, runner: Runner) -> None: + if isinstance(item, CommandSpec): + runner.exec(item.command, item.header) + elif isinstance(item, FuncSpec): + runner.section(item.name) + item.fn(runner) + else: + assert False, f'Unsupported report type: {type(item)}' + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + p = argparse.ArgumentParser(description='VyOS tech-support command collector') + p.add_argument( + '--outdir', + type=Path, + default=None, + help=( + 'Directory to write report files into (one file per report group). ' + 'If this option is omitted, files are written to stdout.' + ), + ) + p.add_argument( + '--reports', + nargs='*', + default=[], + help=( + 'Which report groups to run (default: all). ' + 'Example: --reports vyos-main-info' + ), + ) + p.add_argument( + '--launched-from-generate-archive', + action='store_true', + default=False, + help=( + 'A boolean flag indicates that command executed for generating ' + 'tech-support archive (`generate tech-support archive`). ' + 'In this case some sections will be ignored because already available in archive.' + ), + ) + return p.parse_args(argv) + + +def main(argv: Sequence[str]): + args = parse_args(argv) + + # Select which reports to execute + chosen = select_reports(args) + + # Execute each report and write to its own sink + for report_name, commands in chosen.items(): + # Use a per-report output file when `outdir` is provided + file_path = (args.outdir / report_name) if args.outdir else None + + with OutputSink(file_path=file_path) as sink: + runner = Runner(sink) + try: + # Write top-level report header + sink.write(header_block(report_name, delimiter='=')) + + # Execute each item in the report + for item in commands: + execute_item(item, runner) + except (BrokenPipeError, KeyboardInterrupt): + if sink.is_stdout: + # Exit gracefully when user interrupts program output + # Flush standard streams; redirect remaining output to devnull + # Resolves T5633: Bug #1 and 3 + os.dup2( + os.open(os.devnull, os.O_WRONLY), + sys.stdout.fileno(), # pylint: disable = no-member + ) + sys.exit(1) + + +def get_ethernet_interfaces() -> list[Interface]: """Returns a list of Ethernet interfaces.""" return Section.interfaces('ethernet') -def show_version() -> None: - """Prints the VyOS version and package changes.""" - execute_command(op('show version'), 'VyOS Version and Package Changes') - - -def show_config_file() -> None: - """Prints the contents of a configuration file with a header.""" - execute_command('cat /opt/vyatta/etc/config/config.boot', 'Configuration file') - - -def show_running_config() -> None: - """Prints the running configuration.""" - execute_command(op('show configuration'), 'Running configuration') - - -def show_package_repository_config() -> None: - """Prints the package repository configuration file.""" - execute_command('cat /etc/apt/sources.list', 'Package Repository Configuration File') - execute_command('ls -l /etc/apt/sources.list.d/', 'Repositories') - - -def show_user_startup_scripts() -> None: - """Prints the user startup scripts.""" - execute_command('cat /config/scripts/vyos-preconfig-bootup.script', 'User Startup Scripts (Preconfig)') - execute_command('cat /config/scripts/vyos-postconfig-bootup.script', 'User Startup Scripts (Postconfig)') - - -def show_frr_config() -> None: - """Prints the FRR configuration.""" - execute_command('vtysh -c "show run"', 'FRR configuration') - - -def show_interfaces() -> None: - """Prints the interfaces.""" - execute_command(op('show interfaces'), 'Interfaces') - - -def show_interface_statistics() -> None: - """Prints the interface statistics.""" - execute_command('ip -s link show', 'Interface statistics') - - -def show_physical_interface_statistics() -> None: +def show_physical_interface_statistics(r: Runner): """Prints the physical interface statistics.""" - execute_command('/usr/bin/true', 'Physical Interface statistics') + for iface in get_ethernet_interfaces(): - # Exclude vlans - if '.' in iface: + if '.' in iface: # exclude VLANs continue - execute_command(f'ethtool --driver {iface}', f'ethtool --driver {iface}') - execute_command(f'ethtool --statistics {iface}', f'ethtool --statistics {iface}') - execute_command(f'ethtool --show-ring {iface}', f'ethtool --show-ring {iface}') - execute_command(f'ethtool --show-coalesce {iface}', f'ethtool --show-coalesce {iface}') - execute_command(f'ethtool --pause {iface}', f'ethtool --pause {iface}') - execute_command(f'ethtool --show-features {iface}', f'ethtool --show-features {iface}') - execute_command(f'ethtool --phy-statistics {iface}', f'ethtool --phy-statistics {iface}') - execute_command('netstat --interfaces', 'netstat --interfaces') - execute_command('netstat --listening', 'netstat --listening') - execute_command('cat /proc/net/dev', 'cat /proc/net/dev') + r.exec(f'ethtool --driver {iface}') + r.exec(f'ethtool --statistics {iface}') + r.exec(f'ethtool --show-ring {iface}') + r.exec(f'ethtool --show-coalesce {iface}') + r.exec(f'ethtool --pause {iface}') + r.exec(f'ethtool --show-features {iface}') + r.exec(f'ethtool --phy-statistics {iface}') + r.exec(f'ethtool --module-info {iface}') -def show_bridge() -> None: - """Show bridge interfaces.""" - execute_command(op('show bridge'), 'Show bridge') + for path in Path('/run/udev/vyos').glob('*'): + if path.is_file(): + r.exec(f'cat {path.resolve()}') -def show_arp() -> None: - """Prints ARP entries.""" - execute_command(op('show arp'), 'ARP Table (Total entries)') - execute_command(op('show ipv6 neighbors'), 'show ipv6 neighbors') +def _exec_list_op(r: Runner, commands: list): + for command in commands: + r.exec(op(command)) -def show_route() -> None: +def show_route(r: Runner): """Prints routing information.""" - cmd_list_route = [ - "show ip route bgp | head -108", - "show ip route cache", - "show ip route connected", - "show ip route forward", - "show ip route isis | head -108", - "show ip route kernel", - "show ip route ospf | head -108", - "show ip route rip", - "show ip route static", - "show ip route summary", - "show ip route supernets-only", - "show ip route table all", - "show ip route vrf all", - "show ipv6 route bgp | head -108", - "show ipv6 route cache", - "show ipv6 route connected", - "show ipv6 route forward", - "show ipv6 route isis", - "show ipv6 route kernel", - "show ipv6 route ospfv3", - "show ipv6 route rip", - "show ipv6 route static", - "show ipv6 route summary", - "show ipv6 route table all", - "show ipv6 route vrf all", + commands = [ + 'show ip route bgp | head -108', + 'show ip route connected', + 'show ip route forward | head -108', + 'show ip route isis | head -108', + 'show ip route kernel', + 'show ip route ospf | head -108', + 'show ip route rip | head -108', + 'show ip route static', + 'show ip route summary', + 'show ip route supernets-only | head -108', + 'show ip route table all | head -108', + 'show ip route vrf all | head -108', + 'show ipv6 route bgp | head -108', + 'show ipv6 route connected', + 'show ipv6 route forward | head -108', + 'show ipv6 route isis | head -108', + 'show ipv6 route kernel', + 'show ipv6 route ospfv3 | head -108', + 'show ipv6 route rip | head -108', + 'show ipv6 route static', + 'show ipv6 route summary', + 'show ipv6 route table all | head -108', + 'show ipv6 route vrf all | head -108', ] - for command in cmd_list_route: - execute_command(op(command), command) - - -def show_firewall() -> None: - """Prints firweall information.""" - execute_command('sudo nft list ruleset', 'nft list ruleset') - + _exec_list_op(r, commands) -def show_system() -> None: - """Prints system parameters.""" - execute_command(op('show version'), 'Show System Version') - execute_command(op('show system storage'), 'Show System Storage') - execute_command(op('show system image details'), 'Show System Image Details') +def show_evpn(r: Runner): + """Prints EVPN information.""" -def show_date() -> None: - """Print the current date.""" - execute_command('date', 'Current Time') - - -def show_installed_packages() -> None: - """Prints installed packages.""" - execute_command('dpkg --list', 'Installed Packages') - - -def show_loaded_modules() -> None: - """Prints loaded modules /proc/modules""" - execute_command('cat /proc/modules', 'Loaded Modules') - - -def show_cpu_statistics() -> None: - """Prints CPU statistics.""" - execute_command('/usr/bin/true', 'CPU') - execute_command('lscpu', 'Installed CPU\'s') - execute_command('top --iterations 1 --batch-mode --accum-time-toggle', 'Cumulative CPU Time Used by Running Processes') - execute_command('cat /proc/loadavg', 'Load Average') - - -def show_system_interrupts() -> None: - """Prints system interrupts.""" - execute_command('cat /proc/interrupts', 'Hardware Interrupt Counters') - - -def show_soft_irqs() -> None: - """Prints soft IRQ's.""" - execute_command('cat /proc/softirqs', 'Soft IRQ\'s') + commands = [ + 'show evpn mac vni all', + 'show evpn next-hops vni all', + 'show evpn rmac vni all', + 'show evpn access-vlan', + 'show evpn arp-cache vni all', + 'show evpn es', + 'show evpn es-evi', + ] + _exec_list_op(r, commands) -def show_softnet_statistics() -> None: - """Prints softnet statistics.""" - execute_command('cat /proc/net/softnet_stat', 'cat /proc/net/softnet_stat') +def show_mpls(r: Runner): + """Prints MPLS information.""" + commands = [ + 'show mpls pseudowire', + 'show mpls table', + 'show mpls ldp binding', + 'show mpls ldp discovery', + 'show mpls ldp interface', + 'show mpls ldp neighbor', + ] + _exec_list_op(r, commands) -def show_running_processes() -> None: - """Prints current running processes""" - execute_command('ps -ef', 'Running Processes') +def show_rpki(r: Runner): + """Prints RPKI information.""" -def show_memory_usage() -> None: - """Prints memory usage""" - execute_command('/usr/bin/true', 'Memory') - execute_command('cat /proc/meminfo', 'Installed Memory') - execute_command('free', 'Memory Usage') + commands = [ + 'show rpki cache-server', + 'show rpki cache-connection', + ] + _exec_list_op(r, commands) -def list_disks(): +def _list_disks(): disks = set() with open('/proc/partitions') as partitions_file: for line in partitions_file: @@ -254,60 +340,211 @@ def list_disks(): return disks -def show_storage() -> None: +def show_storage(r: Runner): """Prints storage information.""" - execute_command('cat /proc/devices', 'Devices') - execute_command('cat /proc/partitions', 'Partitions') - - for disk in list_disks(): - execute_command(f'fdisk --list /dev/{disk}', f'Partitioning for disk {disk}') - - -def main(): - # Configuration data - show_version() - show_config_file() - show_running_config() - show_package_repository_config() - show_user_startup_scripts() - show_frr_config() - - # Interfaces - show_interfaces() - show_interface_statistics() - show_physical_interface_statistics() - show_bridge() - show_arp() - - # Routing - show_route() - - # Firewall - show_firewall() - - # System - show_system() - show_date() - show_installed_packages() - show_loaded_modules() - - # CPU - show_cpu_statistics() - show_system_interrupts() - show_soft_irqs() - show_softnet_statistics() - - # Memory - show_memory_usage() - - # Storage - show_storage() - - # Processes - show_running_processes() - - # TODO: Get information from clouds - -if __name__ == "__main__": - main() + r.exec('cat /proc/mounts', 'Mount table') + r.exec('cat /proc/partitions', 'Partitions table') + + for disk in _list_disks(): + r.exec(f'fdisk --list /dev/{disk}', f'Partitioning for disk {disk}') + + r.exec('df -ah', 'Filesystem usage') + r.exec('df -ahi', 'Filesystem inode usage') + + +def show_kernel_interface_counters(r: Runner): + """Prints kernel network interface counters by fixed format.""" + + headers = ( + 'bytes', + 'packets', + 'errs', + 'drop', + 'fifo', + 'frame', + 'compressed', + 'multicast', + ) + new_headers = ( + ['Interface'] + [f'RX-{h}' for h in headers] + [f'TX-{h}' for h in headers] + ) + echo_template = ' '.join(new_headers) + awk_template = ','.join([f'${i}' for i in range(1, len(new_headers) + 1)]) + + cmd = ( + """(echo "{0}" && awk 'NR>2 {{print {1}}}' /proc/net/dev) | column -t""".format( + echo_template, awk_template + ) + ) + r.exec(cmd, 'cat /proc/net/dev | column -t') + + +REPORTS: dict[str, tuple[BaseSpec]] = { + 'vyos-main-info': ( + CommandSpec('VyOS version and package info', op('show version')), + CommandSpec( + 'Running configuration (commands)', op('show configuration commands') + ), + CommandSpec('Running configuration (structured)', op('show configuration')), + CommandSpec( + 'Configuration file (config.boot)', + 'cat /opt/vyatta/etc/config/config.boot', + report_only=True, # Ignored because already exists in 'tech-support archive' + ), + CommandSpec('Interfaces summary', op('show interfaces')), + CommandSpec('Bridge status', op('show bridge')), + CommandSpec('ARP table', op('show arp')), + CommandSpec('IPv6 neighbor table', op('show ipv6 neighbors')), + CommandSpec('System storage overview', op('show system storage')), + CommandSpec('Installed images details', op('show system image details')), + CommandSpec( + 'User startup script (preconfig)', + 'cat /config/scripts/vyos-preconfig-bootup.script', + report_only=True, + ), + CommandSpec( + 'User startup script (postconfig)', + 'cat /config/scripts/vyos-postconfig-bootup.script', + report_only=True, + ), + ), + 'routing-info': ( + FuncSpec('Routing table (IPv4/IPv6)', show_route), + CommandSpec('BFD peers', op('show bfd peers')), + FuncSpec('EVPN status', show_evpn), + FuncSpec('MPLS status', show_mpls), + FuncSpec('RPKI status', show_rpki), + ), + 'frr-info': ( + CommandSpec('FRR running configuration', 'vtysh -c "show running-config"'), + CommandSpec('FRR memory usage', 'vtysh -c "show memory"'), + CommandSpec('FRR work queues', 'vtysh -c "show work-queues"'), + CommandSpec('FRR IPv4 nexthop tracking (NHT)', 'vtysh -c "show ip nht"'), + CommandSpec('FRR IPv6 nexthop tracking (NHT)', 'vtysh -c "show ipv6 nht"'), + CommandSpec('FRR DMVPN status', 'vtysh -c "show dmvpn"'), + CommandSpec('FRR event CPU stats', 'vtysh -c "show event cpu"'), + CommandSpec('FRR event poll stats', 'vtysh -c "show event poll"'), + CommandSpec('FRR event timers', 'vtysh -c "show event timers"'), + CommandSpec( + 'FRR SRv6 locator', + 'vtysh -c "show segment-routing srv6 locator"', + ), + CommandSpec( + 'FRR SRv6 manager', + 'vtysh -c "show segment-routing srv6 manager"', + ), + ), + 'proc-and-sysctl-info': ( + CommandSpec('System load average', 'cat /proc/loadavg'), + FuncSpec('Kernel network interface counters', show_kernel_interface_counters), + CommandSpec('Loaded kernel modules', 'cat /proc/modules'), + CommandSpec('Hardware interrupt counters', 'cat /proc/interrupts'), + CommandSpec('SoftIRQ counters', 'cat /proc/softirqs'), + CommandSpec('Softnet statistics', 'cat /proc/net/softnet_stat'), + CommandSpec('Memory info', 'cat /proc/meminfo'), + CommandSpec( + 'NUMA node memory info', + 'cat /sys/devices/system/node/node*/meminfo', + ), + CommandSpec('VM statistics', 'cat /proc/vmstat'), + CommandSpec('Registered character/block devices', 'cat /proc/devices'), + CommandSpec('Kernel command line', 'cat /proc/cmdline'), + CommandSpec('All sysctl values', 'sysctl -a'), + ), + 'net-and-processes-info': ( + CommandSpec('Network interfaces', 'netstat --interfaces'), + CommandSpec('Listening sockets', 'netstat --listening'), + CommandSpec('Socket summary', 'ss -s'), + CommandSpec('All sockets with details', 'ss -a -e -m -p'), + CommandSpec('Full process listing', 'ps -eF'), + ), + 'ethtool-info': ( + CommandSpec( + 'Link details and interface counters', + 'ip -s -d link show', + ), + FuncSpec('Physical interface statistics', show_physical_interface_statistics), + ), + 'lspci-and-numa-info': ( + CommandSpec('PCI devices', 'lspci -knnv'), + CommandSpec('', 'numactl --hardware'), + CommandSpec('', 'numastat -cm'), + ), + 'nftables-info': ( + CommandSpec('nftables ruleset', 'nft list ruleset'), + CommandSpec('VyOS firewall configuration', op('show firewall')), + CommandSpec('VyOS firewall zone policy', op('show firewall zone-policy')), + ), + 'dpkg-and-modules-info': ( + CommandSpec('Installed packages', 'dpkg --list'), + CommandSpec( + 'Diff dpkg status (image vs running system)', + 'diff /usr/lib/live/mount/rootfs/*.squashfs/var/lib/dpkg/status /var/lib/dpkg/status', + ), + CommandSpec('APT sources list', 'cat /etc/apt/sources.list'), + CommandSpec( + 'APT sources.d directory listing', 'ls -l /etc/apt/sources.list.d/' + ), + CommandSpec('Loaded kernel modules', 'lsmod'), + ), + 'system-resources-info': ( + CommandSpec('Current time (date)', 'date'), + CommandSpec('CPU information', 'lscpu'), + CommandSpec( + 'Per-process cumulative CPU usage snapshot (top batch, accumulated)', + 'top --iterations 1 --batch-mode --accum-time-toggle', + ), + CommandSpec('atop snapshot (CPU view)', 'atop -a -y -1 -c -g -C 1 1 | tee'), + CommandSpec('atop snapshot (memory view)', 'atop -a -y -1 -c -m -M 1 1 | tee'), + CommandSpec('atop snapshot (disk view)', 'atop -a -y -1 -c -d -D 1 1 | tee'), + CommandSpec('atop snapshot (network view)', 'atop -a -y -1 -c -n -N 1 1 | tee'), + CommandSpec('Memory usage', 'free -lhv'), + FuncSpec('Storage overview', show_storage), + ), + 'ipsec-debug-info': ( + CommandSpec('strongSwan connections', 'swanctl -L'), + CommandSpec('strongSwan loaded connections', 'swanctl -l'), + CommandSpec('strongSwan policies', 'swanctl -P'), + CommandSpec('XFRM security associations', 'ip x sa show'), + CommandSpec('XFRM policies', 'ip x policy show'), + CommandSpec('XFRM state', 'ip xfrm state'), + CommandSpec('Tunnels', 'ip tunnel show'), + CommandSpec('Addresses', 'ip address'), + CommandSpec('Policy routing rules', 'ip rule show'), + CommandSpec('Routes', 'ip route | head -100'), + CommandSpec('Routes from table 220', 'ip route show table 220'), + ), + 'vpp-info': ( + CommandSpec('', 'cat /run/vpp/vpp.conf'), + CommandSpec('', 'vppctl show version verbose cmdline'), + CommandSpec('', 'vppctl show hardware-interfaces'), + CommandSpec('', 'vppctl show interface address'), + CommandSpec('', 'vppctl show interface'), + CommandSpec('', 'vppctl show errors'), + CommandSpec('', 'vppctl show runtime'), + CommandSpec( + '', + 'vppctl show memory api-segment stats-segment numa-heaps main-heap map verbose', + ), + CommandSpec('', 'vppctl show buffers'), + CommandSpec('', 'vppctl show physmem detail'), + CommandSpec('', 'vppctl show physmem map'), + CommandSpec('', 'vppctl show cpu'), + CommandSpec('', 'vppctl show threads'), + CommandSpec('', 'vppctl show node counters'), + CommandSpec('', 'vppctl show l2fib'), + CommandSpec('', 'vppctl show bridge-domain'), + CommandSpec('', 'vppctl show ip fib | head -100'), + CommandSpec('', 'vppctl show ip neighbors'), + CommandSpec('', 'vppctl show ip6 fib | head -100'), + CommandSpec('', 'vppctl show ip6 neighbors'), + CommandSpec('', 'vppctl show mpls fib'), + CommandSpec('', 'vppctl show mpls tunnel'), + CommandSpec('', 'vppctl show trace'), + ), +} + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run index 53b94ad98..1f233710b 100644 --- a/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run +++ b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run @@ -169,9 +169,18 @@ _vyatta_op_run () _vyatta_op_last_comp=${_vyatta_op_last_comp_init} false; estat=$? - stty echo 2> /dev/null # turn echo on, this is a workaround for bug 7570 - # not a fix we need to look at why the readline library - # is getting confused on paged help text. + + # Only touch terminal settings when `stdout` is a real TTY. + # When output is piped (e.g. via `sudo ... | cat`), running `stty` can interfere + # with non-interactive execution and produce corrupted/indented output. + if [ -t 1 ]; then + + # Turn echo on, this is a workaround for bug 7570 + # not a fix we need to look at why the readline library + # is getting confused on paged help text. + stty echo 2> /dev/null + + fi i=1 declare -a args # array of expanded arguments |
