From 777e0a641739add1fca50af774d6d924af5550d7 Mon Sep 17 00:00:00 2001 From: David Kubek Date: Tue, 14 Mar 2023 11:54:18 +0100 Subject: [PATCH 25/30] Inhibit unsupported x86-64 microarchitecture RHEL9 As per [x86-64-ABI][1] In addition to the AMD64 baseline architecture, several micro-architecture levels implemented by later CPU modules have been defined, starting at level ``x86-64-v2``. RHEL9 has a higher CPU requirement than older versions, it now requires a CPU compatible with ``x86-64-v2`` instruction set or higher. Until now, there was no check for this and the upgrade crashed unexpectedly. This commit handles this issue and provides the user with a report explaining the problem. The CPU Features are gathered using the ``lscpu`` command by way of using the ``ScanCPU`` actor. The ``ScanCPU`` actor had to be also modified to provide the required flags. The mapping of CPU Features to flags provided by ``lscpu`` has been determined by using the ``/arch/x86/include/asm/cpufeatures.h`` file from the linux kernel. [1]: https://gitlab.com/x86-psABIs/x86-64-ABI.git --- .../actors/scancpu/libraries/scancpu.py | 13 +++- .../actors/scancpu/tests/test_scancpu.py | 74 +++++++++++++++---- repos/system_upgrade/common/models/cpuinfo.py | 4 +- .../actors/checkmicroarchitecture/actor.py | 63 ++++++++++++++++ .../libraries/checkmicroarchitecture.py | 47 ++++++++++++ .../tests/test_checkmicroarchitecture.py | 65 ++++++++++++++++ 6 files changed, 249 insertions(+), 17 deletions(-) create mode 100644 repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/actor.py create mode 100644 repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/libraries/checkmicroarchitecture.py create mode 100644 repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/tests/test_checkmicroarchitecture.py diff --git a/repos/system_upgrade/common/actors/scancpu/libraries/scancpu.py b/repos/system_upgrade/common/actors/scancpu/libraries/scancpu.py index 68f5623b..e5555f99 100644 --- a/repos/system_upgrade/common/actors/scancpu/libraries/scancpu.py +++ b/repos/system_upgrade/common/actors/scancpu/libraries/scancpu.py @@ -17,6 +17,11 @@ def _get_lscpu_output(): return '' +def _get_cpu_flags(lscpu): + flags = lscpu.get('Flags', '') + return flags.split() + + def _get_cpu_entries_for(arch_prefix): result = [] for message in api.consume(DeviceDriverDeprecationData): @@ -137,4 +142,10 @@ def process(): api.produce(*_find_deprecation_data_entries(lscpu)) # Backwards compatibility machine_type = lscpu.get('Machine type') - api.produce(CPUInfo(machine_type=int(machine_type) if machine_type else None)) + flags = _get_cpu_flags(lscpu) + api.produce( + CPUInfo( + machine_type=int(machine_type) if machine_type else None, + flags=flags + ) + ) diff --git a/repos/system_upgrade/common/actors/scancpu/tests/test_scancpu.py b/repos/system_upgrade/common/actors/scancpu/tests/test_scancpu.py index 44d4de87..894fae08 100644 --- a/repos/system_upgrade/common/actors/scancpu/tests/test_scancpu.py +++ b/repos/system_upgrade/common/actors/scancpu/tests/test_scancpu.py @@ -1,14 +1,59 @@ import os +import pytest + from leapp.libraries.actor import scancpu from leapp.libraries.common import testutils +from leapp.libraries.common.config.architecture import ( + ARCH_ARM64, + ARCH_PPC64LE, + ARCH_S390X, + ARCH_SUPPORTED, + ARCH_X86_64 +) from leapp.libraries.stdlib import api from leapp.models import CPUInfo CUR_DIR = os.path.dirname(os.path.abspath(__file__)) +LSCPU = { + ARCH_ARM64: { + "machine_type": None, + "flags": ['fp', 'asimd', 'evtstrm', 'aes', 'pmull', 'sha1', 'sha2', 'crc32', 'cpuid'], + }, + ARCH_PPC64LE: { + "machine_type": None, + "flags": [] + }, + ARCH_S390X: { + "machine_type": + 2827, + "flags": [ + 'esan3', 'zarch', 'stfle', 'msa', 'ldisp', 'eimm', 'dfp', 'edat', 'etf3eh', 'highgprs', 'te', 'vx', 'vxd', + 'vxe', 'gs', 'vxe2', 'vxp', 'sort', 'dflt', 'sie' + ] + }, + ARCH_X86_64: { + "machine_type": + None, + "flags": [ + 'fpu', 'vme', 'de', 'pse', 'tsc', 'msr', 'pae', 'mce', 'cx8', 'apic', 'sep', 'mtrr', 'pge', 'mca', 'cmov', + 'pat', 'pse36', 'clflush', 'dts', 'acpi', 'mmx', 'fxsr', 'sse', 'sse2', 'ss', 'ht', 'tm', 'pbe', 'syscall', + 'nx', 'pdpe1gb', 'rdtscp', 'lm', 'constant_tsc', 'arch_perfmon', 'pebs', 'bts', 'rep_good', 'nopl', + 'xtopology', 'nonstop_tsc', 'cpuid', 'aperfmperf', 'pni', 'pclmulqdq', 'dtes64', 'monitor', 'ds_cpl', + 'vmx', 'smx', 'est', 'tm2', 'ssse3', 'sdbg', 'fma', 'cx16', 'xtpr', 'pdcm', 'pcid', 'dca', 'sse4_1', + 'sse4_2', 'x2apic', 'movbe', 'popcnt', 'tsc_deadline_timer', 'aes', 'xsave', 'avx', 'f16c', 'rdrand', + 'lahf_lm', 'abm', 'cpuid_fault', 'epb', 'invpcid_single', 'pti', 'ssbd', 'ibrs', 'ibpb', 'stibp', + 'tpr_shadow', 'vnmi', 'flexpriority', 'ept', 'vpid', 'ept_ad', 'fsgsbase', 'tsc_adjust', 'bmi1', 'avx2', + 'smep', 'bmi2', 'erms', 'invpcid', 'cqm', 'xsaveopt', 'cqm_llc', 'cqm_occup_llc', 'dtherm', 'ida', 'arat', + 'pln', 'pts', 'md_clear', 'flush_l1d' + ] + }, +} + class mocked_get_cpuinfo(object): + def __init__(self, filename): self.filename = filename @@ -22,24 +67,25 @@ class mocked_get_cpuinfo(object): return '\n'.join(fp.read().splitlines()) -def test_machine_type(monkeypatch): +@pytest.mark.parametrize("arch", ARCH_SUPPORTED) +def test_scancpu(monkeypatch, arch): - # cpuinfo doesn't contain a machine field - mocked_cpuinfo = mocked_get_cpuinfo('lscpu_x86_64') + mocked_cpuinfo = mocked_get_cpuinfo('lscpu_' + arch) monkeypatch.setattr(scancpu, '_get_lscpu_output', mocked_cpuinfo) monkeypatch.setattr(api, 'produce', testutils.produce_mocked()) - current_actor = testutils.CurrentActorMocked(arch=testutils.architecture.ARCH_X86_64) + current_actor = testutils.CurrentActorMocked(arch=arch) monkeypatch.setattr(api, 'current_actor', current_actor) - scancpu.process() - assert api.produce.called == 1 - assert CPUInfo() == api.produce.model_instances[0] - # cpuinfo contains a machine field - api.produce.called = 0 - api.produce.model_instances = [] - current_actor = testutils.CurrentActorMocked(arch=testutils.architecture.ARCH_S390X) - monkeypatch.setattr(api, 'current_actor', current_actor) - mocked_cpuinfo.filename = 'lscpu_s390x' scancpu.process() + + expected = CPUInfo(machine_type=LSCPU[arch]["machine_type"], flags=LSCPU[arch]["flags"]) + produced = api.produce.model_instances[0] + assert api.produce.called == 1 - assert CPUInfo(machine_type=2827) == api.produce.model_instances[0] + + # Produced what was expected + assert expected.machine_type == produced.machine_type + assert sorted(expected.flags) == sorted(produced.flags) + + # Did not produce anything extra + assert expected == produced diff --git a/repos/system_upgrade/common/models/cpuinfo.py b/repos/system_upgrade/common/models/cpuinfo.py index e3e52838..ee245563 100644 --- a/repos/system_upgrade/common/models/cpuinfo.py +++ b/repos/system_upgrade/common/models/cpuinfo.py @@ -32,8 +32,8 @@ class CPUInfo(Model): # byte_order = fields.StringEnum(['Little Endian', 'Big Endian']) # """ Byte order of the CPU: 'Little Endian' or 'Big Endian' """ - # flags = fields.List(fields.String(), default=[]) - # """ Specifies flags/features of the CPU. """ + flags = fields.List(fields.String(), default=[]) + """ Specifies flags/features of the CPU. """ # hypervisor = fields.Nullable(fields.String()) # hypervisor_vendor = fields.Nullable(fields.String()) diff --git a/repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/actor.py b/repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/actor.py new file mode 100644 index 00000000..98ffea80 --- /dev/null +++ b/repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/actor.py @@ -0,0 +1,63 @@ +import leapp.libraries.actor.checkmicroarchitecture as checkmicroarchitecture +from leapp.actors import Actor +from leapp.models import CPUInfo +from leapp.reporting import Report +from leapp.tags import ChecksPhaseTag, IPUWorkflowTag + + +class CheckMicroarchitecture(Actor): + """ + Inhibit if RHEL9 microarchitecture requirements are not satisfied + + + As per `x86-64-ABI`_ In addition to the AMD64 baseline architecture, several + micro-architecture levels implemented by later CPU modules have been + defined, starting at level ``x86-64-v2``. The levels are cumulative in the + sense that features from previous levels are implicitly included in later + levels. + + RHEL9 has a higher CPU requirement than older versions, it now requires a + CPU compatible with ``x86-64-v2`` instruction set or higher. + + .. table:: Required CPU features by microarchitecure level with a + corresponding flag as shown by ``lscpu``. + + +------------+-------------+--------------------+ + | Version | CPU Feature | flag (lscpu) | + +============+=============+====================+ + | (baseline) | CMOV | cmov | + | | CX8 | cx8 | + | | FPU | fpu | + | | FXSR | fxsr | + | | MMX | mmx | + | | OSFXSR | (common with FXSR) | + | | SCE | syscall | + | | SSE | sse | + | | SSE2 | sse2 | + +------------+-------------+--------------------+ + | x86-64-v2 | CMPXCHG16B | cx16 | + | | LAHF-SAHF | lahf_lm | + | | POPCNT | popcnt | + | | SSE3 | pni | + | | SSE4_1 | sse4_1 | + | | SSE4_2 | sse4_2 | + | | SSSE3 | ssse3 | + +------------+-------------+--------------------+ + | ... | | | + +------------+-------------+--------------------+ + + Note: To get the corresponding flag for the CPU feature consult the file + ``/arch/x86/include/asm/cpufeatures.h`` in the linux kernel. + + + .. _x86-64-ABI: https://gitlab.com/x86-psABIs/x86-64-ABI.git + + """ + + name = 'check_microarchitecture' + consumes = (CPUInfo,) + produces = (Report,) + tags = (ChecksPhaseTag, IPUWorkflowTag,) + + def process(self): + checkmicroarchitecture.process() diff --git a/repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/libraries/checkmicroarchitecture.py b/repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/libraries/checkmicroarchitecture.py new file mode 100644 index 00000000..0f1f1fca --- /dev/null +++ b/repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/libraries/checkmicroarchitecture.py @@ -0,0 +1,47 @@ +from leapp import reporting +from leapp.libraries.common.config.architecture import ARCH_X86_64, matches_architecture +from leapp.libraries.stdlib import api +from leapp.models import CPUInfo + +X86_64_BASELINE_FLAGS = ['cmov', 'cx8', 'fpu', 'fxsr', 'mmx', 'syscall', 'sse', 'sse2'] +X86_64_V2_FLAGS = ['cx16', 'lahf_lm', 'popcnt', 'pni', 'sse4_1', 'sse4_2', 'ssse3'] + + +def _inhibit_upgrade(missing_flags): + title = 'Current x86-64 microarchitecture is unsupported in RHEL9' + summary = ('RHEL9 has a higher CPU requirement than older versions, it now requires a CPU ' + 'compatible with x86-64-v2 instruction set or higher.\n\n' + 'Missings flags detected are: {}\n'.format(', '.join(missing_flags))) + + reporting.create_report([ + reporting.Title(title), + reporting.Summary(summary), + reporting.ExternalLink(title='Building Red Hat Enterprise Linux 9 for the x86-64-v2 microarchitecture level', + url=('https://developers.redhat.com/blog/2021/01/05/' + 'building-red-hat-enterprise-linux-9-for-the-x86-64-v2-microarchitecture-level')), + reporting.Severity(reporting.Severity.HIGH), + reporting.Groups([reporting.Groups.INHIBITOR]), + reporting.Groups([reporting.Groups.SANITY]), + reporting.Remediation(hint=('If case of using virtualization, virtualization platforms often allow ' + 'configuring a minimum denominator CPU model for compatibility when migrating ' + 'between different CPU models. Ensure that minimum requirements are not below ' + 'that of RHEL9\n')), + ]) + + +def process(): + """ + Check whether the processor matches the required microarchitecture. + """ + + if not matches_architecture(ARCH_X86_64): + api.current_logger().info('Architecture not x86-64. Skipping microarchitecture test.') + return + + cpuinfo = next(api.consume(CPUInfo)) + + required_flags = X86_64_BASELINE_FLAGS + X86_64_V2_FLAGS + missing_flags = [flag for flag in required_flags if flag not in cpuinfo.flags] + api.current_logger().debug('Required flags missing: %s', missing_flags) + if missing_flags: + _inhibit_upgrade(missing_flags) diff --git a/repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/tests/test_checkmicroarchitecture.py b/repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/tests/test_checkmicroarchitecture.py new file mode 100644 index 00000000..b7c850d9 --- /dev/null +++ b/repos/system_upgrade/el8toel9/actors/checkmicroarchitecture/tests/test_checkmicroarchitecture.py @@ -0,0 +1,65 @@ +import pytest + +from leapp import reporting +from leapp.libraries.actor import checkmicroarchitecture +from leapp.libraries.common.config.architecture import ARCH_SUPPORTED, ARCH_X86_64 +from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked +from leapp.libraries.stdlib import api +from leapp.models import CPUInfo +from leapp.utils.report import is_inhibitor + + +@pytest.mark.parametrize("arch", [arch for arch in ARCH_SUPPORTED if not arch == ARCH_X86_64]) +def test_not_x86_64_passes(monkeypatch, arch): + """ + Test no report is generated on an architecture different from x86-64 + """ + + monkeypatch.setattr(reporting, "create_report", create_report_mocked()) + monkeypatch.setattr(api, 'current_logger', logger_mocked()) + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch=arch)) + + checkmicroarchitecture.process() + + assert 'Architecture not x86-64. Skipping microarchitecture test.' in api.current_logger.infomsg + assert not reporting.create_report.called + + +def test_valid_microarchitecture(monkeypatch): + """ + Test no report is generated on a valid microarchitecture + """ + + monkeypatch.setattr(reporting, "create_report", create_report_mocked()) + monkeypatch.setattr(api, 'current_logger', logger_mocked()) + + required_flags = checkmicroarchitecture.X86_64_BASELINE_FLAGS + checkmicroarchitecture.X86_64_V2_FLAGS + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch=ARCH_X86_64, + msgs=[CPUInfo(flags=required_flags)])) + + checkmicroarchitecture.process() + + assert 'Architecture not x86-64. Skipping microarchitecture test.' not in api.current_logger.infomsg + assert not reporting.create_report.called + + +def test_invalid_microarchitecture(monkeypatch): + """ + Test report is generated on x86-64 architecture with invalid microarchitecture and the upgrade is inhibited + """ + + monkeypatch.setattr(reporting, "create_report", create_report_mocked()) + monkeypatch.setattr(api, 'current_logger', logger_mocked()) + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch=ARCH_X86_64, msgs=[CPUInfo()])) + + checkmicroarchitecture.process() + + produced_title = reporting.create_report.report_fields.get('title') + produced_summary = reporting.create_report.report_fields.get('summary') + + assert 'Architecture not x86-64. Skipping microarchitecture test.' not in api.current_logger().infomsg + assert reporting.create_report.called == 1 + assert 'microarchitecture is unsupported' in produced_title + assert 'RHEL9 has a higher CPU requirement' in produced_summary + assert reporting.create_report.report_fields['severity'] == reporting.Severity.HIGH + assert is_inhibitor(reporting.create_report.report_fields) -- 2.40.1