"""
OSRelease
=========
The ``OSRelease`` combiner uses the following parsers to try to identify if the
current host is installed with a "Red Hat Enterprise Linux" (RHEL) system.
- :py:class:`insights.parsers.installed_rpms.InstalledRpms`
- :py:class:`insights.parsers.uname.Uname`
- :py:class:`insights.parsers.dmesg.DmesgLineList`
- :py:class:`insights.parsers.os_release.OSRelease`
- :py:class:`insights.parsers.redhat_release.RedhatRelease`
It provides an attribute `is_rhel` that indicates if the host is RHEL or not.
It also provides an attribute `release` which returns the estimated OS release
name of the system, "Unknown" will be returned by default when cannot identify
the OS.
* TODO::
The lists of keywords to identify NON-RHEL system of each sub-combiners are
based on our current knowledge, and may be not sufficient. It needs to be
updated timely according to new found Linux Distributions.
"""
from insights.core.filters import add_filter
from insights.core.plugins import combiner
# TODO: replace DmesgLineList with '/proc/version' (not collected yet)
from insights.parsers.dmesg import DmesgLineList
from insights.parsers.installed_rpms import InstalledRpm, InstalledRpms
from insights.parsers.os_release import OsRelease
from insights.parsers.redhat_release import RedhatRelease
from insights.parsers.uname import Uname
RHEL_KEYS = ['rhel', 'red hat enterprise linux']
OTHER_LINUX_KEYS = {
# other_linux: (dmesg-keywords, release-packages)
'Fedora': (
['fedora'],
['fedora-release']),
'CentOS': (
['centos'],
['centos-stream-release', 'centos-release']),
'Oracle': (
['oracle'],
['enterprise-release', 'oraclelinux-release']),
'CloudLinux': (
['cloudlinux'],
['cloudlinux-release']),
'ClearOS': (
['clearos'],
['clearos-release']),
'AlmaLinux': (
['almalinux'],
['almalinux-release']),
'Rocky': (
['rockylinux', 'rocky'],
['rocky-release']),
'Scientific': (
[], # Empty
['sl-release']),
'SUSE': (
['suse', 'sles', 'novell'],
['sles-release', 'sles_es-release-server']),
}
"""Known NON-RHEL Linux Distributions."""
# TODO:
# Update the CONVERT2RHEL_SUPPORTED list when necessary
CONVERT2RHEL_SUPPORTED = ['CentOS']
# Get "Linux version" from `dmesg`
DMESG_LINUX_BUILD_INFO = 'Linux version'
add_filter(DmesgLineList, DMESG_LINUX_BUILD_INFO)
DmesgLineList.keep_scan("linux_version", DMESG_LINUX_BUILD_INFO, num=1)
# Must-install packages for minimum RHEL system
MINIMUM_RHEL_PKGS = [
# 'kernel' is checked individually (booting kernel)
'audit-libs',
'basesystem',
'bash',
'coreutils',
'dbus',
'dmidecode',
'dnf', # RHEL 8+
'dracut',
'filesystem',
'firewalld',
'glibc',
'gmp',
'krb5-libs',
'libacl',
'libgcc',
'libselinux',
'NetworkManager',
'openssl-libs',
'passwd',
'redhat-release', # RHEL 8+
'redhat-release-server', # RHEL 6/7
'systemd', # RHEL 7+
'util-linux',
'yum', # RHEL 6/7
]
"""Must-install packages for minimum installed RHEL system."""
THRESHOLD = 0.75
"""Threshold of the must-install packages to identify NON-RHEL."""
def _get_release(names):
names = names if isinstance(names, list) else [names]
for rel in OTHER_LINUX_KEYS:
if any(rel.lower() in name.lower() for name in names):
return rel
return names[-1].split()[0]
def _from_os_release(osr):
"""
Identify RHEL by checking the `/etc/os-release`.
"""
def _filter(name):
"""Remove falsy or items contain RHEL info"""
if not name or any(k in name.lower() for k in RHEL_KEYS):
return False
return name
if osr:
names = list(filter(_filter, [osr.get('ID'), osr.get('NAME'),
osr.get('PRETTY_NAME')]))
if names:
# NON-RHEL: /etc/os-release
return dict(other_linux=_get_release(names),
reason='NON-RHEL: os-release')
return dict(other_linux='RHEL')
def _from_redhat_release(rhr):
"""
Identify RHEL by checking the `/etc/redhat-release`.
"""
if rhr:
if not rhr.is_rhel:
return dict(other_linux=_get_release(rhr.product),
reason='NON-RHEL: redhat-release')
return dict(other_linux='RHEL')
def _from_uname(uname):
"""
Identify RHEL by checking the `uname -a`.
1. Oracle kernels may contain 'uek' or 'ol' in the kernel NVR.
2. Fedora kernels contains 'fc' in the NVR.
3. RHEL kernels have '.el' in the NVR
RHEL based Linux kernels may also have the '.el' in the NVR,
but they are also checked in other sub-combiners.
4. Otherwise, flag it as an "Unknown"
"""
LINUX_UNAME_KEYS = [
('Oracle', ['uek', 'ol']),
('Fedora', ['fc']),
('RHEL', ['.el']), # the last item
]
if uname:
kernel = uname.kernel
ret = dict(other_linux='Unknown')
for rel, keys in LINUX_UNAME_KEYS:
if any(key in kernel for key in keys):
ret.update(other_linux=rel)
break
if ret.get('other_linux') != 'RHEL':
ret.update(kernel=kernel)
return ret
def _from_dmesg(dmesg):
"""
Identify RHEL by checking the `dmesg`.
The `dmesg` includes a line containing the kernel build information,
e.g. the build host and GCC version.
If this line doesn't contain 'redhat.com' then we can assume the kernel
wasn't built on a Red Hat machine and this should be flagged.
"""
if dmesg and dmesg.linux_version:
line = dmesg.linux_version[0]['raw_message']
low_line = line.lower()
if 'redhat.com' not in low_line:
release = 'Unknown'
for rel, keys in OTHER_LINUX_KEYS.items():
if any(kw in low_line for kw in keys[0]):
release = rel
break
return dict(other_linux=release, build_info=line)
else:
return dict(other_linux='RHEL')
def _from_installed_rpms(rpms, uname):
"""
Identify RHEL by checking the `installed_rpms`.
Two parts are included, see below:
"""
if not rpms:
return
# Part-1: the known non-rhel-release packages exists
for rel, pkgs in OTHER_LINUX_KEYS.items():
for pkg_name in pkgs[1]:
pkg = rpms.newest(pkg_name)
if pkg:
return dict(other_linux=rel, release=pkg.nvr)
# Part-2: too many must-install packages are NOT from Red Hat
# - more than THRESHOLD packages are not signed and not provided by Red Hat
# faulty_packages >= THRESHOLD * must-install packages
installed_packages = 0
vendor, ng_pkgs = '', set()
if uname:
# check the booting 'kernel' first
installed_packages += 1
boot_kn = InstalledRpm.from_package('kernel-{0}'.format(uname.kernel))
for pkg in rpms.packages.get('kernel', []):
if pkg == boot_kn:
vendor = pkg.vendor
if pkg.redhat_signed is False and vendor != 'Red Hat, Inc.':
ng_pkgs.add(pkg.nvr)
# check the booting kernel only
break
# check other packages
for pkg_name in MINIMUM_RHEL_PKGS:
pkg = rpms.newest(pkg_name)
if pkg:
# count the package only when it's installed
installed_packages += 1
if pkg.redhat_signed is False and pkg.vendor != 'Red Hat, Inc.':
ng_pkgs.add(pkg.nvr)
# check the result
if len(ng_pkgs) >= round(THRESHOLD * installed_packages) > 0:
# NON-RHEL: more than THRESHOLD packages are NOT from Red Hat
ret = dict(other_linux='Unknown', faulty_packages=sorted(ng_pkgs))
if vendor:
ret.update(kernel_vendor=vendor)
# try to get the release from kernel vendor
if 'red hat' not in vendor.lower():
sep = ',' if ',' in vendor else ' '
release = _get_release(vendor.split(sep)[0].strip())
ret.update(other_linux=release)
return ret
return dict(other_linux='RHEL')
[docs]@combiner(optional=[Uname, DmesgLineList, InstalledRpms,
OsRelease, RedhatRelease])
class OSRelease(object):
"""
A Combiner identifies whether the current Linux a Red Hat Enterprise Linux
or not.
Examples:
>>> type(osr)
<class 'insights.combiners.os_release.OSRelease'>
>>> osr.is_rhel
False
>>> osr.release == "Oracle"
True
>>> osr.name == "Oracle Linux Server"
True
>>> osr.is_rhel_compatible
False
>>> 'kernel' in osr.reasons.keys()
False
>>> 'faulty_packages' in osr.reasons.keys()
True
>>> 'glibc-2.28-211.el8' in osr.reasons['faulty_packages']
True
"""
def __init__(self, uname, dmesg, rpms, osr, rhr):
def __identify_non_rhel(ret):
if ret and 'other_linux' in ret:
self._release = ret.pop('other_linux')
self._reasons = ret
if self._release == 'Unknown':
return None # Continue to identify when unknown
return self._release != 'RHEL' # see OTHER_LINUX_KEYS
return None # Nothing to check
self._release = 'Unknown'
self._reasons = dict(reason='Nothing available to check')
# 1. Check `installed_rpms` first.
ret = _from_installed_rpms(rpms, uname)
# We trust the check result of `installed_rpms`: RHEL or NON-RHEL,
# expect for "Unknown".
# `None` indicates "Unknown" or `installed_rpms` is not available
if __identify_non_rhel(ret) is None:
# 2. Only when `installed_rpms` cannot identify it
# a. installed_rpms is not available
# b. identify result is "Unknown"
#
# Check below items with order:
# - uname, dmesg, os_release, and redhat_release
# and any `False` which indicates NON-RHEL to stop.
check_points_funcs = [
(_from_uname, [uname]),
(_from_dmesg, [dmesg]),
(_from_os_release, [osr]),
(_from_redhat_release, [rhr]),
]
for chk_func, args in check_points_funcs:
if __identify_non_rhel(chk_func(*args)):
break
self._name = osr.get('NAME', self._release) if osr else self._release
@property
def is_rhel(self):
"""
Returns True if it's RHEL, False for NON-RHEL.
"""
return self._release == 'RHEL'
@property
def is_rhel_compatible(self):
"""
Returns True if Convert2RHEL could convert
"""
return self._release in CONVERT2RHEL_SUPPORTED
@property
def release(self):
"""
Returns the estimated release name of the running Linux. It's RHEL or
one key of the :const:`OTHER_LINUX_KEYS` when it's NON-RHEL.
"""
return self._release
@property
def name(self):
"""
Returns the name of the OS. Generally it's the ``NAME`` of the
`/etc/os-release` file, or it's the same as the `release` when
the `/etc/os-release` is not available.
"""
return self._name
@property
def reasons(self):
"""
Returns a dict indicating why the host is a NON-RHEL. Empty when
it's an RHEL. The keys include::
kernel (str): the kernel package
build_info (str): the kernel build information
release (str): the release package
faulty_packages (list): the packages that are not signed and not
provided by Red Hat
reason (str): a string when nothing is available to check
"""
return self._reasons
@property
def product(self):
""" Alias of `release`. Keep backward compatible """
return self._release
def __repr__(self):
if self.is_rhel:
return "<release: {0}, name: {1}>".format(self.release, self.name)
return "<release: {0}, name: {1}, reasons: {2}>".format(
self.release, self.name, self.reasons)