Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inhibit unsupported x86-64 microarchitecture RHEL9 #1059

Merged
merged 1 commit into from
May 17, 2023
Merged
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
13 changes: 12 additions & 1 deletion repos/system_upgrade/common/actors/scancpu/libraries/scancpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -137,4 +142,10 @@ def process():
api.produce(*_find_deprecation_data_entries(lscpu))
# Backwards compatibility
machine_type = lscpu.get('Machine type')
dkubek marked this conversation as resolved.
Show resolved Hide resolved
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
)
)
74 changes: 60 additions & 14 deletions repos/system_upgrade/common/actors/scancpu/tests/test_scancpu.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -22,24 +67,25 @@ def __call__(self):
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
4 changes: 2 additions & 2 deletions repos/system_upgrade/common/models/cpuinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)