Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -82,6 +82,26 @@ Outputs:
- `SYSTEM ERROR; <error>` for file system issues
- `UNEXPECTED ERROR; <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
Copy link
Preview

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation shows 'Version: 7' in the example output, but the actual code on line 136 in kc-compat.py always prints 'Version: Not available'. This inconsistency could confuse users about what to expect from the --report output.

Suggested change
Version: 7
Version: Not available

Copilot uses AI. Check for mistakes.

Kernel: Linux version 5.4.0-74-generic (buildd@lcy01-amd64-023) (gcc version 9.3.0)
Environment: Physical/Virtual Machine
Copy link
Preview

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation shows 'Environment: Physical/Virtual Machine' in the example output, but this line is not implemented in the actual code. The code only outputs kernel hash, distribution, version (hardcoded as 'Not available'), and kernel information.

Suggested change
Environment: Physical/Virtual Machine

Copilot uses AI. Check for mistakes.

=====================================
COMPATIBLE
```

This information can be easily shared with the support team for troubleshooting.

If --silent flag is provided -- doesn't print anything

Exit codes:
Expand Down
48 changes: 34 additions & 14 deletions kc-compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -62,24 +58,29 @@ 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):
return line.split('=', 1)[1].strip().strip('"\'')

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):
Expand All @@ -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
Expand All @@ -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'
Comment on lines 117 to +120
Copy link
Preview

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag parsing logic doesn't handle multiple arguments correctly. If both --silent and --report are provided, or if --report is provided as a second argument, the current logic will fail to detect it properly. Consider using argparse or at least checking all argv elements instead of just argv[1].

Suggested change
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'
silent = ('--silent' in sys.argv[1:]) or ('-q' in sys.argv[1:])
report = '--report' in sys.argv[1:]

Copilot uses AI. Check for mistakes.

Comment on lines 119 to +120
Copy link
Preview

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current argument parsing logic only checks the first argument, making it impossible to use both --silent and --report flags together or handle multiple arguments properly. Consider using argparse for proper command-line argument handling.

Copilot uses AI. Check for mistakes.


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")
Expand Down
71 changes: 45 additions & 26 deletions test_kc_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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)
Expand Down
Loading