summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2026-03-05 16:21:13 +0100
committerGitHub <noreply@github.com>2026-03-05 16:21:13 +0100
commit5ea47f5c7dab6cee38aef355fd78d434a10dd463 (patch)
tree938a3bed44be5b451178c22e87364be2e592da50 /src
parentfcf57444ab7150e06082627fe196de3c9a87d9e1 (diff)
parent1359f690af841ea7c28028e907b1d5f15f3d90b8 (diff)
downloadvyos-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-xsrc/op_mode/generate_tech-support_archive.py212
-rw-r--r--src/op_mode/show_techsupport_report.py719
-rw-r--r--src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run15
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