summaryrefslogtreecommitdiff
path: root/smoketest/scripts/cli
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 /smoketest/scripts/cli
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 'smoketest/scripts/cli')
-rw-r--r--smoketest/scripts/cli/test_techsupport_archive.py251
-rw-r--r--smoketest/scripts/cli/test_techsupport_report.py94
2 files changed, 345 insertions, 0 deletions
diff --git a/smoketest/scripts/cli/test_techsupport_archive.py b/smoketest/scripts/cli/test_techsupport_archive.py
new file mode 100644
index 000000000..f345d20eb
--- /dev/null
+++ b/smoketest/scripts/cli/test_techsupport_archive.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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 io
+import re
+import contextlib
+import pathlib
+import tarfile
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.process import call
+from vyos.utils.file import get_name_from_path
+
+base_path = ['generate tech-support archive']
+testdir = pathlib.Path('/tmp/_test_techsupport_archive')
+
+
+def tar_gz_paths(archive_path: pathlib.Path, max_depth: int = 5) -> set:
+ """
+ Return all member paths inside `archive_path`.
+
+ Nested archives are expanded recursively and represented as:
+ "inner.tar.gz::path/inside/inner"
+ """
+
+ def is_nested(name: str) -> bool:
+ nested_suffixes = ('.tar.gz', '.tgz', '.tar')
+ return name.lower().endswith(nested_suffixes)
+
+ out = set()
+
+ def walk_bytes(data: bytes, prefix: str, depth: int):
+ if depth > max_depth:
+ return
+
+ bio = io.BytesIO(data)
+ with tarfile.open(fileobj=bio, mode='r:*') as tf:
+ for member in tf.getmembers():
+ out.add(f'{prefix}::{member.name}' if prefix else member.name)
+
+ if member.isreg() and is_nested(member.name) and depth < max_depth:
+ file = tf.extractfile(member)
+ if file is None:
+ continue
+
+ sub_prefix = f'{prefix}::{member.name}' if prefix else member.name
+ walk_bytes(file.read(), sub_prefix, depth + 1)
+
+ walk_bytes(archive_path.read_bytes(), prefix='', depth=0)
+
+ return out
+
+
+@contextlib.contextmanager
+def stub_files(file_paths: list[str]):
+ """
+ Context manager for tests.
+
+ Creates a temporary set of files and removes it after use.
+ Example:
+ with stub_files(['a.txt', 'dir/b.txt']):
+ pass
+ """
+
+ paths = [pathlib.Path(file_path) for file_path in file_paths]
+
+ try:
+ for path in paths:
+ if path.parent.exists():
+ path.touch()
+
+ yield
+
+ finally:
+ for path in paths:
+ path.unlink(missing_ok=True)
+
+
+class TestTechSupportArchive(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ # always forward to base class
+ super().tearDown()
+
+ if testdir.exists():
+ call(f'sudo rm -rf {testdir}')
+
+ def _check_path(self, path: str | pathlib.Path, state: bool, err_message: str):
+ if isinstance(path, str):
+ path = pathlib.Path(path)
+
+ if state:
+ self.assertTrue(path.exists(), err_message)
+ else:
+ self.assertFalse(path.exists(), err_message)
+
+ def _extract_archive_path(self, output: str) -> pathlib.Path:
+ match = re.search(r'located in ([^\s]+\.tar\.gz)', output)
+ path = match.group(1) if match else None
+
+ err_message = (
+ 'It is not possible to extract the path to the resulting '
+ 'archive from stdout:'
+ f'\n```\n{output}\n```'
+ )
+ assert path is not None, err_message
+
+ return pathlib.Path(path)
+
+ def _extract_archive_inner_path_prefix(self, archive_path: pathlib.Path):
+ # Convert
+ # `bdbdd9a4807f_tech-support-archive_2026-02-02T15-33-26.tar.gz`
+ # to
+ # `bdbdd9a4807f_tech-support-archive_2026-02-02T15-33-26`
+
+ return get_name_from_path(archive_path)
+
+ def assertPathExists(self, path: str):
+ err_message = f'Path `{path}` does not exist after generating a archive'
+ self._check_path(path, True, err_message)
+
+ def assertNotPathExists(self, path: str):
+ err_message = f'Path `{path}` still exists after generating a archive'
+ self._check_path(path, False, err_message)
+
+ def assertExpectedArcPaths(
+ self, archive_path: pathlib.Path, expected_paths: list[str]
+ ):
+ actual = tar_gz_paths(archive_path)
+ expected = frozenset(expected_paths)
+
+ missing = sorted(expected - actual)
+
+ if missing:
+ lines = [f'Archive paths mismatch: {archive_path}', 'Missing:']
+ lines += [f' - {p}' for p in missing]
+ raise AssertionError('\n'.join(lines))
+
+ def assertNotExpectedArcPaths(
+ self, archive_path: pathlib.Path, unexpected_paths: list[str]
+ ):
+ actual = tar_gz_paths(archive_path)
+ unexpected = frozenset(unexpected_paths)
+
+ extra = sorted(actual & unexpected)
+
+ if extra:
+ lines = [f'Archive paths mismatch: {archive_path}', 'Unexpected:']
+ lines += [f' - {p}' for p in extra]
+ raise AssertionError('\n'.join(lines))
+
+ def test_general_archive_structure(self):
+ output = self.op_mode(base_path)
+
+ archive_path = self._extract_archive_path(output)
+ self.assertPathExists(archive_path)
+ self.assertEqual(str(archive_path.parent), '/tmp')
+
+ prefix = self._extract_archive_inner_path_prefix(archive_path)
+ self.assertExpectedArcPaths(
+ archive_path,
+ [
+ f'{prefix}/show_tech-support_report/vyos-main-info',
+ f'{prefix}/config.tar.gz::opt/vyatta/etc/config/config.boot',
+ f'{prefix}/core-dump.tar.gz::var/core',
+ f'{prefix}/home.tar.gz::home/vyos/.profile',
+ f'{prefix}/root.tar.gz::root/.profile',
+ f'{prefix}/run.tar.gz::run',
+ f'{prefix}/tmp.tar.gz::tmp/vyos-config-status',
+ f'{prefix}/topology-logical.png',
+ f'{prefix}/topology.png',
+ f'{prefix}/var-log.tar.gz::var/log/journal',
+ ],
+ )
+
+ if archive_path.exists():
+ call(f'sudo rm -f {archive_path}')
+
+ def test_excluded_archive_files(self):
+ fake_files = [
+ '/opt/vyatta/etc/config/_test_fake_vyos.iso',
+ '/home/vyos/_test_fake_vyos.iso',
+ '/tmp/_test_fake_vyos.iso',
+ '/opt/vyatta/etc/config/_test_fake_archive.tar.gz',
+ '/home/vyos/_test_fake_archive.tar.gz',
+ '/tmp/_test_fake_archive.tar.gz',
+ '/home/vyos/drops-debug_9999-12-31T23-59-59',
+ '/home/vyos/ffffffffffff_tech-support-archive_9999-12-31T23-59-59',
+ '/tmp/drops-debug_9999-12-31T23-59-59',
+ '/tmp/ffffffffffff_tech-support-archive_9999-12-31T23-59-59',
+ ]
+ with stub_files(fake_files):
+ output = self.op_mode(base_path)
+
+ archive_path = self._extract_archive_path(output)
+ self.assertPathExists(archive_path)
+
+ prefix = self._extract_archive_inner_path_prefix(archive_path)
+ self.assertNotExpectedArcPaths(
+ archive_path,
+ [
+ f'{prefix}/config.tar.gz::opt/vyatta/etc/config/_test_fake_vyos.iso',
+ f'{prefix}/home.tar.gz::home/vyos/_test_fake_vyos.iso',
+ f'{prefix}/tmp.tar.gz::tmp/_test_fake_vyos.iso',
+ f'{prefix}/var.tar.gz::var/log/messages',
+ f'{prefix}/var.tar.gz::var/log/messages.1',
+ f'{prefix}/config.tar.gz::opt/vyatta/etc/config/_test_fake_archive.tar.gz',
+ f'{prefix}/home.tar.gz::home/vyos/_test_fake_archive.tar.gz',
+ f'{prefix}/tmp.tar.gz::tmp/_test_fake_archive.tar.gz',
+ f'{prefix}/home.tar.gz::home/vyos/drops-debug_9999-12-31T23-59-59',
+ f'{prefix}/home.tar.gz::home/vyos/ffffffffffff_tech-support-archive_9999-12-31T23-59-59',
+ f'{prefix}/tmp.tar.gz::tmp/drops-debug_9999-12-31T23-59-59',
+ f'{prefix}/tmp.tar.gz::tmp/ffffffffffff_tech-support-archive_9999-12-31T23-59-59',
+ ],
+ )
+
+ if archive_path.exists():
+ call(f'sudo rm -f {archive_path}')
+
+ def test_custom_archive_path(self):
+ output = self.op_mode(base_path + [str(testdir / 'foo')])
+
+ archive_path = self._extract_archive_path(output)
+ self.assertPathExists(archive_path)
+ self.assertEqual(archive_path.parent, testdir)
+ self.assertEqual(archive_path.name, 'foo.tar.gz')
+
+ def test_custom_directory_archive_path(self):
+ output = self.op_mode(base_path + [str(testdir) + '/bar/'])
+
+ archive_path = self._extract_archive_path(output)
+ self.assertPathExists(archive_path)
+ self.assertEqual(archive_path.parent.name, 'bar')
+ self.assertEqual(archive_path.suffix, '.gz')
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())
diff --git a/smoketest/scripts/cli/test_techsupport_report.py b/smoketest/scripts/cli/test_techsupport_report.py
new file mode 100644
index 000000000..af81be182
--- /dev/null
+++ b/smoketest/scripts/cli/test_techsupport_report.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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 pathlib
+import shutil
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.defaults import directories
+from vyos.utils.process import cmd
+
+base_path = ['show tech-support report']
+script_path = directories['op_mode'] + '/show_techsupport_report.py'
+testdir = pathlib.Path('/tmp/_test_techsupport_report')
+
+all_blocks = (
+ 'vyos-main-info',
+ 'routing-info',
+ 'frr-info',
+ 'proc-and-sysctl-info',
+ 'net-and-processes-info',
+ 'ethtool-info',
+ 'lspci-and-numa-info',
+ 'nftables-info',
+ 'dpkg-and-modules-info',
+ 'system-resources-info',
+ 'ipsec-debug-info',
+ 'vpp-info',
+)
+
+
+class TestTechSupportReport(VyOSUnitTestSHIM.TestCase):
+ def _gen_section_header(self, name: str) -> str:
+ length = len(name)
+ return '=' * length + '\n' + name + '\n' + '=' * length + '\n'
+
+ def assertSectionIn(self, name: str, report: str):
+ header = self._gen_section_header(name)
+ self.assertIn(header, report, f'Report does not contain section `{name}`')
+
+ def assertSectionNotIn(self, name: str, report: str):
+ header = self._gen_section_header(name)
+ self.assertNotIn(
+ header, report, f'Report contains unnecessary section `{name}`'
+ )
+
+ def test_full_report(self):
+ report = self.op_mode(base_path)
+ for block in all_blocks:
+ self.assertSectionIn(block, report)
+
+ def test_filtered_report(self):
+ blocks = (
+ 'vyos-main-info',
+ 'proc-and-sysctl-info',
+ )
+
+ report = cmd([script_path, '--reports'] + list(blocks))
+
+ for block in blocks:
+ self.assertSectionIn(block, report)
+
+ missing_blocks = frozenset(all_blocks) - frozenset(blocks)
+ for block in missing_blocks:
+ self.assertSectionNotIn(block, report)
+
+ def test_directory_output(self):
+ cmd([script_path, '--outdir', str(testdir)])
+
+ for block in all_blocks:
+ file_path = testdir / block
+ err_message = f'File `{file_path}` does not exist after generating a report'
+
+ self.assertTrue(file_path.exists(), err_message)
+ self.assertSectionIn(block, file_path.read_text())
+
+ shutil.rmtree(testdir, ignore_errors=True)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())