From 2ceba16d92d556bb37d285e43ebc524b49da5372 Mon Sep 17 00:00:00 2001 From: Samuel FORESTIER Date: Thu, 22 Aug 2024 08:55:17 +0200 Subject: [PATCH] Add support for CloudLinux VM missing os-release and /etc/redhat-release > closes #240 --- src/distro/distro.py | 39 ++++++++++++++--- .../resources/distros/cloudlinuxvm7/bin/uname | 3 ++ .../resources/distros/cloudlinuxvm8/bin/uname | 3 ++ .../resources/distros/cloudlinuxvm9/bin/uname | 3 ++ .../distros/cloudlinuxvm9/proc/modules | 12 ++++++ tests/test_distro.py | 42 +++++++++++++++++++ 6 files changed, 97 insertions(+), 5 deletions(-) create mode 100755 tests/resources/distros/cloudlinuxvm7/bin/uname create mode 100755 tests/resources/distros/cloudlinuxvm8/bin/uname create mode 100755 tests/resources/distros/cloudlinuxvm9/bin/uname create mode 100644 tests/resources/distros/cloudlinuxvm9/proc/modules diff --git a/src/distro/distro.py b/src/distro/distro.py index f83d787..caeb1f7 100755 --- a/src/distro/distro.py +++ b/src/distro/distro.py @@ -42,6 +42,7 @@ Callable, Dict, Iterable, + List, Optional, Sequence, TextIO, @@ -73,6 +74,7 @@ class InfoDict(TypedDict): _UNIXCONFDIR = os.environ.get("UNIXCONFDIR", "/etc") +_UNIXPROCDIR = os.environ.get("UNIXPROCDIR", "/proc") _UNIXUSRLIBDIR = os.environ.get("UNIXUSRLIBDIR", "/usr/lib") _OS_RELEASE_BASENAME = "os-release" @@ -759,6 +761,7 @@ def __init__( """ self.root_dir = root_dir self.etc_dir = os.path.join(root_dir, "etc") if root_dir else _UNIXCONFDIR + self.proc_dir = os.path.join(root_dir, "proc") if root_dir else _UNIXPROCDIR self.usr_lib_dir = ( os.path.join(root_dir, "usr/lib") if root_dir else _UNIXUSRLIBDIR ) @@ -1239,14 +1242,32 @@ def _armbian_version(self) -> str: except FileNotFoundError: return "" - @staticmethod - def _parse_uname_content(lines: Sequence[str]) -> Dict[str, str]: + def _parse_uname_content(self, lines: Sequence[str]) -> Dict[str, str]: if not lines: return {} props = {} - match = re.search(r"^([^\s]+)\s+([\d\.]+)", lines[0].strip()) + match = re.search(r"^([^\s]+)\s+([^\s]+)", lines[0].strip()) if match: - name, version = match.groups() + name, release = match.groups() + + # CloudLinux detection relies on uname release information + release_parts = release.split(".") + if ( + # check penultimate release component contains an "el*" version + len(release_parts) > 1 + and release_parts[-2].startswith("el") + and ( + # CloudLinux < 9 : "lve*" is set in kernel release + any(rc.startswith("lve") for rc in release_parts) + # CloudLinux >= 9 : check whether "kmodlve" is loaded + or "kmodlve" in self._loaded_modules + ) + ): + props["id"] = "cloudlinux" + props["name"] = "CloudLinux" + # strip "el" prefix and replace underscores by dots + props["release"] = release_parts[-2][2:].replace("_", ".") + return props # This is to prevent the Linux kernel version from # appearing as the 'best' version on otherwise @@ -1255,9 +1276,17 @@ def _parse_uname_content(lines: Sequence[str]) -> Dict[str, str]: return {} props["id"] = name.lower() props["name"] = name - props["release"] = version + props["release"] = release.split("-")[0] # only keep version part return props + @cached_property + def _loaded_modules(self) -> List[str]: + try: + with open(os.path.join(self.proc_dir, "modules"), encoding="ascii") as fp: + return [line.split()[0] for line in fp] + except OSError: + return [] + @staticmethod def _to_str(bytestring: bytes) -> str: encoding = sys.getfilesystemencoding() diff --git a/tests/resources/distros/cloudlinuxvm7/bin/uname b/tests/resources/distros/cloudlinuxvm7/bin/uname new file mode 100755 index 0000000..f6e3c50 --- /dev/null +++ b/tests/resources/distros/cloudlinuxvm7/bin/uname @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Linux 3.10.0-962.3.2.lve1.5.24.9.el7.x86_64" diff --git a/tests/resources/distros/cloudlinuxvm8/bin/uname b/tests/resources/distros/cloudlinuxvm8/bin/uname new file mode 100755 index 0000000..10164d0 --- /dev/null +++ b/tests/resources/distros/cloudlinuxvm8/bin/uname @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Linux 4.18.0-513.18.1.lve.2.el8.x86_64" diff --git a/tests/resources/distros/cloudlinuxvm9/bin/uname b/tests/resources/distros/cloudlinuxvm9/bin/uname new file mode 100755 index 0000000..e66e114 --- /dev/null +++ b/tests/resources/distros/cloudlinuxvm9/bin/uname @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Linux 5.14.0-427.20.1.el9_4.x86_64" diff --git a/tests/resources/distros/cloudlinuxvm9/proc/modules b/tests/resources/distros/cloudlinuxvm9/proc/modules new file mode 100644 index 0000000..a1f234b --- /dev/null +++ b/tests/resources/distros/cloudlinuxvm9/proc/modules @@ -0,0 +1,12 @@ +autofs4 23643 2 - Live 0x0000000000000000 +btrfs 21980 0 - Live 0x0000000000000000 +cdrom 3656 2 hfsplus,hfs, Live 0x0000000000000000 +ext4 19980 5 - Live 0x0000000000000000 +fat 31261 2 msdos,vfat, Live 0x0000000000000000 +fuse 20211 9 - Live 0x0000000000000000 +hfs 28747 0 - Live 0x0000000000000000 +hfsplus 4168 0 - Live 0x0000000000000000 +kmodlve 17657856 2 - Live 0x0000000000000000 +msdos 7785 0 - Live 0x0000000000000000 +vfat 7785 1 - Live 0x0000000000000000 +xor 1770 1 btrfs, Live 0x0000000000000000 diff --git a/tests/test_distro.py b/tests/test_distro.py index 78c173e..afddf61 100644 --- a/tests/test_distro.py +++ b/tests/test_distro.py @@ -36,6 +36,7 @@ from distro import distro RELATIVE_UNIXCONFDIR = distro._UNIXCONFDIR[1:] + RELATIVE_UNIXPROCDIR = distro._UNIXPROCDIR[1:] RELATIVE_UNIXUSRLIBDIR = distro._UNIXUSRLIBDIR[1:] MODULE_DISTRO = distro._distro @@ -118,11 +119,13 @@ def setup_method(self, test_method: FunctionType) -> None: # changes it: self._saved_path = os.environ["PATH"] self._saved_UNIXCONFDIR = distro._UNIXCONFDIR + self._saved_UNIXPROCDIR = distro._UNIXPROCDIR self._saved_UNIXUSRLIBDIR = distro._UNIXUSRLIBDIR def teardown_method(self, test_method: FunctionType) -> None: os.environ["PATH"] = self._saved_path distro._UNIXCONFDIR = self._saved_UNIXCONFDIR + distro._UNIXPROCDIR = self._saved_UNIXPROCDIR distro._UNIXUSRLIBDIR = self._saved_UNIXUSRLIBDIR def _setup_for_distro(self, distro_root: str) -> None: @@ -131,6 +134,7 @@ def _setup_for_distro(self, distro_root: str) -> None: # distro that runs this test, so we use a PATH with only one entry: os.environ["PATH"] = distro_bin distro._UNIXCONFDIR = os.path.join(distro_root, RELATIVE_UNIXCONFDIR) + distro._UNIXPROCDIR = os.path.join(distro_root, RELATIVE_UNIXPROCDIR) distro._UNIXUSRLIBDIR = os.path.join(distro_root, RELATIVE_UNIXUSRLIBDIR) @@ -628,6 +632,42 @@ def test_manjaro1512_lsb_release(self) -> None: # } # self._test_outcome(desired_outcome) + def test_cloudlinuxvm7_uname(self) -> None: + self._test_outcome( + { + "id": "cloudlinux", + "name": "CloudLinux", + "version": "7", + "pretty_name": "CloudLinux 7", + "pretty_version": "7", + "best_version": "7", + } + ) + + def test_cloudlinuxvm8_uname(self) -> None: + self._test_outcome( + { + "id": "cloudlinux", + "name": "CloudLinux", + "version": "8", + "pretty_name": "CloudLinux 8", + "pretty_version": "8", + "best_version": "8", + } + ) + + def test_cloudlinuxvm9_uname(self) -> None: + self._test_outcome( + { + "id": "cloudlinux", + "name": "CloudLinux", + "version": "9.4", + "pretty_name": "CloudLinux 9.4", + "pretty_version": "9.4", + "best_version": "9.4", + } + ) + def test_openbsd62_uname(self) -> None: self._test_outcome( { @@ -2387,9 +2427,11 @@ def test_repr(self) -> None: if attr in ( "root_dir", "etc_dir", + "proc_dir", "usr_lib_dir", "_debian_version", "_armbian_version", + "_loaded_modules", ): continue assert f"{attr}=" in repr_str