From 7840024af3c8e15cc5dd42fdf976eff950aade5d Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Mon, 29 Sep 2025 09:40:44 +0200 Subject: [PATCH 1/3] feat(kc-compat): add --report flag to output system info before status --- README.md | 22 +++++++++++++++++++++- kc-compat.py | 25 ++++++++++++++++++++++++- test_kc_compat.py | 24 ++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e5a56c6..cc0764a 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ else if needs_update == False { Checks if server is running kernel compatible with KernelCare. Usage: ```bash -python kc-compat.py [--silent|-q] +python kc-compat.py [--silent|-q|--report] ``` Outputs: @@ -82,6 +82,26 @@ Outputs: - `SYSTEM ERROR; ` for file system issues - `UNEXPECTED ERROR; ` for other errors +### Flags: +- `--silent` or `-q`: Silent mode - no output, only exit codes +- `--report`: Generate system information report for support team + +### Report Mode: +When using `--report`, the script outputs detailed system information followed by the compatibility status: + +``` +=== KernelCare Compatibility Report === +Kernel Hash: abcdef1234567890abcdef1234567890abcdef12 +Distribution: centos +Version: 7 +Kernel: Linux version 5.4.0-74-generic (buildd@lcy01-amd64-023) (gcc version 9.3.0) +Environment: Physical/Virtual Machine +===================================== +COMPATIBLE +``` + +This information can be easily shared with the support team for troubleshooting. + If --silent flag is provided -- doesn't print anything Exit codes: diff --git a/kc-compat.py b/kc-compat.py index 3f78c4d..39d4989 100644 --- a/kc-compat.py +++ b/kc-compat.py @@ -111,13 +111,37 @@ def myprint(silent, message): def main(): """ if --silent or -q argument provided, don't print anything, just use exit code + if --report provided, show system information for support otherwise print results (COMPATIBLE or support contact messages) else exit with 0 if COMPATIBLE, 1 or more otherwise """ silent = len(sys.argv) > 1 and (sys.argv[1] == '--silent' or sys.argv[1] == '-q') + report = len(sys.argv) > 1 and sys.argv[1] == '--report' + if inside_vz_container() or inside_lxc_container(): myprint(silent, "UNSUPPORTED; INSIDE CONTAINER") return 2 + + # Get system information once for both report and compatibility checking + kernel_hash = get_kernel_hash() + distro_name = get_distro_info() + + # Show system information for support if --report flag is used + if report: + print("=== KernelCare Compatibility Report ===") + print(f"Kernel Hash: {kernel_hash}") + print(f"Distribution: {distro_name or 'Unknown'}") + print(f"Version: Not available") + + # Get kernel version from /proc/version + try: + with open('/proc/version', 'r') as f: + kernel_version = f.read().strip() + print(f"Kernel: {kernel_version}") + except (IOError, OSError): + print("Kernel: Unable to read /proc/version") + + print("=====================================") try: if is_compat(): @@ -125,7 +149,6 @@ def main(): return 0 else: # Handle 404 case - check if distro is supported - distro_name = get_distro_info() if distro_name and is_distro_supported(distro_name): myprint(silent, "NEEDS REVIEW") myprint(silent, "We support your distribution, but we're having trouble detecting your precise kernel configuration. Please, contact CloudLinux Inc. support by email at support@cloudlinux.com or by request form at https://www.cloudlinux.com/index.php/support") diff --git a/test_kc_compat.py b/test_kc_compat.py index 0eb5e84..c62df34 100644 --- a/test_kc_compat.py +++ b/test_kc_compat.py @@ -200,6 +200,30 @@ def test_main_silent_mode(self, mock_print, mock_compat, mock_lxc, mock_vz): assert result == 0 mock_print.assert_not_called() + @patch('sys.argv', ['kc-compat.py', '--report']) + @patch.object(kc_compat, 'get_kernel_hash', return_value='abcdef123456') + @patch.object(kc_compat, 'get_distro_info', return_value='centos') + @patch.object(kc_compat, 'inside_vz_container', return_value=False) + @patch.object(kc_compat, 'inside_lxc_container', return_value=False) + @patch.object(kc_compat, 'is_compat', return_value=True) + @patch('builtins.open', new_callable=mock_open, read_data='Linux version 5.4.0-test') + @patch('builtins.print') + def test_main_report_mode(self, mock_print, mock_file, mock_compat, mock_lxc, mock_vz, mock_distro, mock_hash): + result = kc_compat.main() + assert result == 0 + # Check that report header and information are printed, followed by COMPATIBLE + expected_calls = [ + (("=== KernelCare Compatibility Report ===",),), + (("Kernel Hash: abcdef123456",),), + (("Distribution: centos",),), + (("Version: Not available",),), + (("Kernel: Linux version 5.4.0-test",),), + (("=====================================",),), + (("COMPATIBLE",),) + ] + mock_print.assert_has_calls(expected_calls) + + @patch('sys.argv', ['kc-compat.py']) @patch.object(kc_compat, 'inside_vz_container', return_value=False) @patch.object(kc_compat, 'inside_lxc_container', return_value=False) From 8b166f3280cf51900d4544ed94f5ecae140b2529 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Mon, 29 Sep 2025 09:57:09 +0200 Subject: [PATCH 2/3] refactor(kc-compat): read proc version once --- kc-compat.py | 36 ++++++++++++----------------- test_kc_compat.py | 58 ++++++++++++++++++++--------------------------- 2 files changed, 40 insertions(+), 54 deletions(-) diff --git a/kc-compat.py b/kc-compat.py index 39d4989..1837c5c 100644 --- a/kc-compat.py +++ b/kc-compat.py @@ -34,17 +34,15 @@ } -def get_kernel_hash(): +def get_kernel_hash_from_data(version_data): try: # noinspection PyCompatibility from hashlib import sha1 except ImportError: from sha import sha as sha1 - f = open('/proc/version', 'rb') - try: - return sha1(f.read()).hexdigest() - finally: - f.close() + return sha1(version_data).hexdigest() + + def inside_vz_container(): @@ -89,8 +87,8 @@ def is_distro_supported(distro_name): return distro_name in SUPPORTED_DISTROS -def is_compat(): - url = 'http://patches.kernelcare.com/' + get_kernel_hash() + '/version' +def is_compat(kernel_hash): + url = 'http://patches.kernelcare.com/' + kernel_hash + '/version' try: urlopen(url) return True @@ -122,29 +120,25 @@ def main(): myprint(silent, "UNSUPPORTED; INSIDE CONTAINER") return 2 - # Get system information once for both report and compatibility checking - kernel_hash = get_kernel_hash() + try: + with open('/proc/version', 'rb') as f: + version_data = f.read() + except (IOError, OSError): + version_data = b'' + + kernel_hash = get_kernel_hash_from_data(version_data) distro_name = get_distro_info() - # Show system information for support if --report flag is used if report: print("=== KernelCare Compatibility Report ===") print(f"Kernel Hash: {kernel_hash}") print(f"Distribution: {distro_name or 'Unknown'}") print(f"Version: Not available") - - # Get kernel version from /proc/version - try: - with open('/proc/version', 'r') as f: - kernel_version = f.read().strip() - print(f"Kernel: {kernel_version}") - except (IOError, OSError): - print("Kernel: Unable to read /proc/version") - + print(f"Kernel: {version_data.decode('utf-8', errors='replace').strip()}") print("=====================================") try: - if is_compat(): + if is_compat(kernel_hash): myprint(silent, "COMPATIBLE") return 0 else: diff --git a/test_kc_compat.py b/test_kc_compat.py index c62df34..50e7973 100644 --- a/test_kc_compat.py +++ b/test_kc_compat.py @@ -11,17 +11,12 @@ class TestGetKernelHash: - @patch('builtins.open', new_callable=mock_open, read_data=b'Linux version 5.4.0-test') - def test_get_kernel_hash_success(self, mock_file): - result = kc_compat.get_kernel_hash() + def test_get_kernel_hash_from_data(self): + version_data = b'Linux version 5.4.0-test' + result = kc_compat.get_kernel_hash_from_data(version_data) assert isinstance(result, str) assert len(result) == 40 # SHA1 hex digest length - mock_file.assert_called_once_with('/proc/version', 'rb') - @patch('builtins.open', side_effect=IOError("File not found")) - def test_get_kernel_hash_file_error(self, mock_file): - with pytest.raises(IOError): - kc_compat.get_kernel_hash() class TestContainerDetection: @@ -86,32 +81,28 @@ def test_is_distro_supported(self): class TestIsCompat: - @patch.object(kc_compat, 'get_kernel_hash', return_value='abcdef123456') @patch.object(kc_compat, 'urlopen') - def test_is_compat_success(self, mock_urlopen, mock_hash): + def test_is_compat_success(self, mock_urlopen): mock_urlopen.return_value = MagicMock() - assert kc_compat.is_compat() == True + assert kc_compat.is_compat('abcdef123456') == True mock_urlopen.assert_called_once_with('http://patches.kernelcare.com/abcdef123456/version') - @patch.object(kc_compat, 'get_kernel_hash', return_value='abcdef123456') @patch.object(kc_compat, 'urlopen') - def test_is_compat_404_error_returns_false(self, mock_urlopen, mock_hash): + def test_is_compat_404_error_returns_false(self, mock_urlopen): mock_urlopen.side_effect = HTTPError(None, 404, 'Not Found', None, None) - assert kc_compat.is_compat() == False + assert kc_compat.is_compat('abcdef123456') == False - @patch.object(kc_compat, 'get_kernel_hash', return_value='abcdef123456') @patch.object(kc_compat, 'urlopen') - def test_is_compat_500_error_raises(self, mock_urlopen, mock_hash): + def test_is_compat_500_error_raises(self, mock_urlopen): mock_urlopen.side_effect = HTTPError(None, 500, 'Server Error', None, None) with pytest.raises(HTTPError): - kc_compat.is_compat() + kc_compat.is_compat('abcdef123456') - @patch.object(kc_compat, 'get_kernel_hash', return_value='abcdef123456') @patch.object(kc_compat, 'urlopen') - def test_is_compat_url_error_raises(self, mock_urlopen, mock_hash): + def test_is_compat_url_error_raises(self, mock_urlopen): mock_urlopen.side_effect = URLError('Connection refused') with pytest.raises(URLError): - kc_compat.is_compat() + kc_compat.is_compat('abcdef123456') class TestMyprint: @@ -201,27 +192,28 @@ def test_main_silent_mode(self, mock_print, mock_compat, mock_lxc, mock_vz): mock_print.assert_not_called() @patch('sys.argv', ['kc-compat.py', '--report']) - @patch.object(kc_compat, 'get_kernel_hash', return_value='abcdef123456') @patch.object(kc_compat, 'get_distro_info', return_value='centos') @patch.object(kc_compat, 'inside_vz_container', return_value=False) @patch.object(kc_compat, 'inside_lxc_container', return_value=False) @patch.object(kc_compat, 'is_compat', return_value=True) - @patch('builtins.open', new_callable=mock_open, read_data='Linux version 5.4.0-test') + @patch('builtins.open', new_callable=mock_open, read_data=b'Linux version 5.4.0-test') @patch('builtins.print') - def test_main_report_mode(self, mock_print, mock_file, mock_compat, mock_lxc, mock_vz, mock_distro, mock_hash): + def test_main_report_mode(self, mock_print, mock_file, mock_compat, mock_lxc, mock_vz, mock_distro): result = kc_compat.main() assert result == 0 # Check that report header and information are printed, followed by COMPATIBLE - expected_calls = [ - (("=== KernelCare Compatibility Report ===",),), - (("Kernel Hash: abcdef123456",),), - (("Distribution: centos",),), - (("Version: Not available",),), - (("Kernel: Linux version 5.4.0-test",),), - (("=====================================",),), - (("COMPATIBLE",),) - ] - mock_print.assert_has_calls(expected_calls) + # We need to check the actual calls made, not exact matches + calls = mock_print.call_args_list + assert len(calls) >= 7 # At least 7 print calls + + # Check specific calls + assert calls[0].args[0] == "=== KernelCare Compatibility Report ===" + assert calls[1].args[0].startswith("Kernel Hash: ") + assert calls[2].args[0] == "Distribution: centos" + assert calls[3].args[0] == "Version: Not available" + assert calls[4].args[0] == "Kernel: Linux version 5.4.0-test" + assert calls[5].args[0] == "=====================================" + assert calls[6].args[0] == "COMPATIBLE" @patch('sys.argv', ['kc-compat.py']) From 294f8ecb1cb9ac9f29d67705558ac359a44ec517 Mon Sep 17 00:00:00 2001 From: Rinat Sabitov Date: Mon, 29 Sep 2025 10:14:14 +0200 Subject: [PATCH 3/3] feat(kc-compat): include distro version in --report --- kc-compat.py | 19 +++++++++++-------- test_kc_compat.py | 19 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/kc-compat.py b/kc-compat.py index 1837c5c..f1ba20c 100644 --- a/kc-compat.py +++ b/kc-compat.py @@ -43,8 +43,6 @@ def get_kernel_hash_from_data(version_data): return sha1(version_data).hexdigest() - - def inside_vz_container(): """ determines if we are inside Virtuozzo container @@ -60,7 +58,7 @@ def inside_lxc_container(): def get_distro_info(): """ Get current distribution name and version - :return: distro name or None if detection fails + :return: tuple of (distro_name, distro_version) or (None, None) if detection fails """ def parse_value(line): @@ -68,16 +66,21 @@ def parse_value(line): os_release_path = '/etc/os-release' if not os.path.exists(os_release_path): - return None + return None, None try: + distro_name = None + distro_version = None with open(os_release_path, 'r') as f: for line in f: line = line.strip() if line.startswith('ID='): - return parse_value(line) + distro_name = parse_value(line) + elif line.startswith('VERSION_ID='): + distro_version = parse_value(line) + return distro_name, distro_version except (IOError, OSError): - return None + return None, None def is_distro_supported(distro_name): @@ -127,13 +130,13 @@ def main(): version_data = b'' kernel_hash = get_kernel_hash_from_data(version_data) - distro_name = get_distro_info() + distro_name, distro_version = get_distro_info() if report: print("=== KernelCare Compatibility Report ===") print(f"Kernel Hash: {kernel_hash}") print(f"Distribution: {distro_name or 'Unknown'}") - print(f"Version: Not available") + print(f"Version: {distro_version or 'Unknown'}") print(f"Kernel: {version_data.decode('utf-8', errors='replace').strip()}") print("=====================================") diff --git a/test_kc_compat.py b/test_kc_compat.py index 50e7973..23cfc72 100644 --- a/test_kc_compat.py +++ b/test_kc_compat.py @@ -19,6 +19,7 @@ def test_get_kernel_hash_from_data(self): + class TestContainerDetection: @patch('os.path.exists') def test_inside_vz_container_true(self, mock_exists): @@ -57,20 +58,22 @@ class TestGetDistroInfo: @patch('os.path.exists', return_value=True) @patch('builtins.open', new_callable=mock_open, read_data='ID=centos\nVERSION_ID="7"\n') def test_get_distro_info_success(self, mock_file, mock_exists): - name = kc_compat.get_distro_info() + name, version = kc_compat.get_distro_info() assert name == 'centos' - + assert version == '7' @patch('os.path.exists', return_value=False) def test_get_distro_info_no_file(self, mock_exists): - name = kc_compat.get_distro_info() + name, version = kc_compat.get_distro_info() assert name is None + assert version is None @patch('os.path.exists', return_value=True) @patch('builtins.open', side_effect=IOError("Permission denied")) def test_get_distro_info_read_error(self, mock_file, mock_exists): - name = kc_compat.get_distro_info() + name, version = kc_compat.get_distro_info() assert name is None + assert version is None class TestIsDistroSupported: @@ -149,7 +152,7 @@ def test_main_compatible(self, mock_print, mock_compat, mock_lxc, mock_vz): @patch.object(kc_compat, 'inside_vz_container', return_value=False) @patch.object(kc_compat, 'inside_lxc_container', return_value=False) @patch.object(kc_compat, 'is_compat', return_value=False) - @patch.object(kc_compat, 'get_distro_info', return_value='centos') + @patch.object(kc_compat, 'get_distro_info', return_value=('centos', '7')) @patch.object(kc_compat, 'is_distro_supported', return_value=True) @patch('builtins.print') def test_main_kernel_not_found_but_distro_supported(self, mock_print, mock_distro_supported, @@ -167,7 +170,7 @@ def test_main_kernel_not_found_but_distro_supported(self, mock_print, mock_distr @patch.object(kc_compat, 'inside_vz_container', return_value=False) @patch.object(kc_compat, 'inside_lxc_container', return_value=False) @patch.object(kc_compat, 'is_compat', return_value=False) - @patch.object(kc_compat, 'get_distro_info', return_value='unknown') + @patch.object(kc_compat, 'get_distro_info', return_value=('unknown', None)) @patch.object(kc_compat, 'is_distro_supported', return_value=False) @patch('builtins.print') def test_main_kernel_not_found_distro_not_supported(self, mock_print, mock_distro_supported, @@ -192,7 +195,7 @@ def test_main_silent_mode(self, mock_print, mock_compat, mock_lxc, mock_vz): mock_print.assert_not_called() @patch('sys.argv', ['kc-compat.py', '--report']) - @patch.object(kc_compat, 'get_distro_info', return_value='centos') + @patch.object(kc_compat, 'get_distro_info', return_value=('centos', '7')) @patch.object(kc_compat, 'inside_vz_container', return_value=False) @patch.object(kc_compat, 'inside_lxc_container', return_value=False) @patch.object(kc_compat, 'is_compat', return_value=True) @@ -210,7 +213,7 @@ def test_main_report_mode(self, mock_print, mock_file, mock_compat, mock_lxc, mo assert calls[0].args[0] == "=== KernelCare Compatibility Report ===" assert calls[1].args[0].startswith("Kernel Hash: ") assert calls[2].args[0] == "Distribution: centos" - assert calls[3].args[0] == "Version: Not available" + assert calls[3].args[0] == "Version: 7" assert calls[4].args[0] == "Kernel: Linux version 5.4.0-test" assert calls[5].args[0] == "=====================================" assert calls[6].args[0] == "COMPATIBLE"