diff --git a/sos/cleaner/__init__.py b/sos/cleaner/__init__.py index f7b5b001dc..5bfa0983f1 100644 --- a/sos/cleaner/__init__.py +++ b/sos/cleaner/__init__.py @@ -410,6 +410,43 @@ def review_parser_values(self): self.opts.skip_cleaning_files = [fnmatch.translate(p) for p in self.opts.skip_cleaning_files] + def display_cleaner_results(self, final_path, map_path): + """Print the location of the obfuscated output. + + Size is reported only for a packed archive. For a directory, + os.stat().st_size is filesystem metadata (commonly 4KiB), not + the size of the obfuscated data, so omit it. This follows the + same archive-vs-directory split as Policy.display_results. + + :param final_path: Path to the obfuscated archive or directory + :type final_path: ``str`` + + :param map_path: Path to the private mapping file + :type map_path: ``str`` + """ + arcstat = os.stat(final_path) + + # while these messages won't be included in the log file in the + # archive some facilities, such as our avocado test suite, will + # sometimes not capture print() output, so leverage the ui_log + # to print to console + self.ui_log.info( + f"A mapping of obfuscated elements is available at\n\t{map_path}" + ) + self.ui_log.info( + f"\nThe obfuscated archive is available at\n\t{final_path}\n" + ) + + if os.path.isfile(final_path): + self.ui_log.info( + f"\tSize\t{get_human_readable(arcstat.st_size)}" + ) + self.ui_log.info(f"\tOwner\t{getpwuid(arcstat.st_uid).pw_name}\n") + self.ui_log.info( + "Please send the obfuscated archive to your support\n" + "representative and keep the mapping file private." + ) + def execute(self): """SoSCleaner will begin by inspecting the TARGET option to determine if it is a directory, archive, or archive of archives. @@ -500,24 +537,7 @@ def execute(self): self.obfuscate_string(arc_path.split('/')[-1]) ) shutil.move(arc_path, final_path) - arcstat = os.stat(final_path) - - # while these messages won't be included in the log file in the archive - # some facilities, such as our avocado test suite, will sometimes not - # capture print() output, so leverage the ui_log to print to console - self.ui_log.info( - f"A mapping of obfuscated elements is available at\n\t{map_path}" - ) - self.ui_log.info( - f"\nThe obfuscated archive is available at\n\t{final_path}\n" - ) - - self.ui_log.info(f"\tSize\t{get_human_readable(arcstat.st_size)}") - self.ui_log.info(f"\tOwner\t{getpwuid(arcstat.st_uid).pw_name}\n") - self.ui_log.info( - "Please send the obfuscated archive to your support\n" - "representative and keep the mapping file private." - ) + self.display_cleaner_results(final_path, map_path) self.cleanup() return None diff --git a/tests/cleaner_tests/directory_size.py b/tests/cleaner_tests/directory_size.py new file mode 100644 index 0000000000..8f3ae09668 --- /dev/null +++ b/tests/cleaner_tests/directory_size.py @@ -0,0 +1,43 @@ +# This file is part of the sos project: https://github.com/sosreport/sos +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# version 2 of the GNU General Public License. +# +# See the LICENSE file in the source distribution for further information. + +import os + +from sos_tests import StageTwoReportTest + + +class ExistingDirectoryCleanSizeTest(StageTwoReportTest): + """Ensure sos clean on a directory does not report Size from the + directory inode. That value is filesystem metadata (often 4KiB), + not the obfuscated data size. + + :avocado: tags=stagetwo + """ + + sos_cmd = '' + sos_component = 'clean' + + def pre_sos_setup(self): + src = os.path.join(self.tmpdir, 'src', + 'sosreport-cleanertest-dirsize') + os.makedirs(os.path.join(src, 'sos_logs')) + with open(os.path.join(src, 'hostname'), 'w', + encoding='utf-8') as hfile: + hfile.write('cleanertest.example.com\n') + with open(os.path.join(src, 'sos_logs', 'sos.log'), 'w', + encoding='utf-8') as lfile: + lfile.write('test log\n') + map_file = os.path.join(self.tmpdir, 'default_mapping') + self.sos_cmd = f'--no-update --map-file {map_file} {src}' + + def test_directory_output_omits_size(self): + self.assertOutputContains( + 'The obfuscated archive is available at' + ) + self.assertOutputNotContains('\tSize\t') + self.assertOutputContains('\tOwner\t') diff --git a/tests/unittests/cleaner_tests.py b/tests/unittests/cleaner_tests.py index 12f41c7a51..fdcf73deab 100644 --- a/tests/unittests/cleaner_tests.py +++ b/tests/unittests/cleaner_tests.py @@ -6,12 +6,16 @@ # # See the LICENSE file in the source distribution for further information. +import os +import tempfile import unittest from ipaddress import ip_interface from os.path import join from unittest import mock import sos.policies +from sos.cleaner import SoSCleaner +from sos.utilities import get_human_readable from sos.cleaner.parsers.ip_parser import SoSIPParser from sos.cleaner.parsers.mac_parser import SoSMacParser from sos.cleaner.parsers.hostname_parser import SoSHostnameParser @@ -723,3 +727,54 @@ def test_packed_dirs_empty_for_premature_manifest(self): with mock.patch.object(self.archive, 'get_file_content', return_value='{"components": {"report": {}}}'): self.assertEqual(self.archive._load_packed_dirs(), []) + + +class CleanerDisplayResultsTests(unittest.TestCase): + """Verify sos clean reports Size only for packed archives. + + Directory output must not use os.stat().st_size, which is inode + metadata (often 4KiB) rather than the obfuscated data size. + """ + + def setUp(self): + self.cleaner = mock.Mock() + self.cleaner.ui_log = mock.Mock() + + def _info_messages(self): + return [call.args[0] for call in + self.cleaner.ui_log.info.call_args_list] + + def test_archive_reports_file_size(self): + with tempfile.NamedTemporaryFile(delete=False) as tfile: + tfile.write(b'x' * 2048) + tfile.flush() + path = tfile.name + try: + SoSCleaner.display_cleaner_results( + self.cleaner, path, '/tmp/private_map' + ) + msgs = self._info_messages() + expected = f"\tSize\t{get_human_readable(os.stat(path).st_size)}" + self.assertIn(expected, msgs) + self.assertTrue(any('\tOwner\t' in msg for msg in msgs)) + self.assertTrue(any(path in msg for msg in msgs)) + finally: + os.unlink(path) + + def test_directory_omits_size(self): + with tempfile.TemporaryDirectory() as tmpdir: + with open(os.path.join(tmpdir, 'data'), 'wb') as dfile: + dfile.write(b'x' * 8192) + SoSCleaner.display_cleaner_results( + self.cleaner, tmpdir, '/tmp/private_map' + ) + msgs = self._info_messages() + self.assertFalse( + any('\tSize\t' in msg for msg in msgs), + f"Directory output unexpectedly included Size: {msgs}" + ) + self.assertTrue(any('\tOwner\t' in msg for msg in msgs)) + self.assertTrue(any(tmpdir in msg for msg in msgs)) + self.assertTrue( + any('private_map' in msg for msg in msgs) + )