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..f1ba20c 100644 --- a/kc-compat.py +++ b/kc-compat.py @@ -34,17 +34,13 @@ } -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(): @@ -62,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): @@ -70,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): @@ -89,8 +90,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 @@ -111,21 +112,40 @@ 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 + + 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, 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: {distro_version or 'Unknown'}") + 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: # 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..23cfc72 100644 --- a/test_kc_compat.py +++ b/test_kc_compat.py @@ -11,17 +11,13 @@ 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: @@ -62,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: @@ -86,32 +84,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: @@ -158,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, @@ -176,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, @@ -200,6 +194,31 @@ 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_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) + @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): + result = kc_compat.main() + assert result == 0 + # Check that report header and information are printed, followed by COMPATIBLE + # 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: 7" + 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']) @patch.object(kc_compat, 'inside_vz_container', return_value=False) @patch.object(kc_compat, 'inside_lxc_container', return_value=False)