diff --git a/java/code/src/com/redhat/rhn/domain/server/CPU.hbm.xml b/java/code/src/com/redhat/rhn/domain/server/CPU.hbm.xml index 7e4f24aa840a..613e10152d2f 100644 --- a/java/code/src/com/redhat/rhn/domain/server/CPU.hbm.xml +++ b/java/code/src/com/redhat/rhn/domain/server/CPU.hbm.xml @@ -32,6 +32,10 @@ PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" + + + + cpuArchSpecsIn) { final CPU cpu = Optional.ofNullable(server.getCpu()).orElseGet(CPU::new); // os.uname[4] @@ -199,6 +203,13 @@ else if (CpuArchUtil.isAarch64(cpuarch)) { // On s390x this number of active and actual CPUs can be different. cpu.setNrCPU(grains.getValueAsLong("total_num_cpus").orElse(0L)); + try { + cpu.setArchSpecs(new ObjectMapper().writeValueAsString(cpuArchSpecsIn)); + } + catch (JsonProcessingException e) { + LOG.warn("Failed to serialize CPU arch specs, ignoring results.", e); + } + if (arch != null) { cpu.setServer(server); server.setCpu(cpu); diff --git a/java/code/src/com/suse/manager/utils/SaltUtils.java b/java/code/src/com/suse/manager/utils/SaltUtils.java index 7c24d43a3c9f..8ec75963c3a2 100644 --- a/java/code/src/com/suse/manager/utils/SaltUtils.java +++ b/java/code/src/com/suse/manager/utils/SaltUtils.java @@ -1833,7 +1833,7 @@ private static void handleHardwareProfileUpdate(MinionServer server, HardwareMapper hwMapper = new HardwareMapper(server, new ValueMap(result.getGrains())); - hwMapper.mapCpuInfo(new ValueMap(result.getCpuInfo())); + hwMapper.mapCpuInfo(new ValueMap(result.getCpuInfo()), result.getCpuArchSpecs()); server.setRam(hwMapper.getTotalMemory()); server.setSwap(hwMapper.getTotalSwapMemory()); if (CpuArchUtil.isDmiCapable(hwMapper.getCpuArch())) { diff --git a/java/code/src/com/suse/manager/webui/utils/salt/custom/HwProfileUpdateSlsResult.java b/java/code/src/com/suse/manager/webui/utils/salt/custom/HwProfileUpdateSlsResult.java index fcef9f1db6a2..914e31d88030 100644 --- a/java/code/src/com/suse/manager/webui/utils/salt/custom/HwProfileUpdateSlsResult.java +++ b/java/code/src/com/suse/manager/webui/utils/salt/custom/HwProfileUpdateSlsResult.java @@ -45,6 +45,9 @@ public class HwProfileUpdateSlsResult { @SerializedName("mgrcompat_|-cpuinfo_|-status.cpuinfo_|-module_run") private StateApplyResult>> cpuInfo; + @SerializedName("mgrcompat_|-cpu_arch_specs_|-cpuinfo.arch_specs_|-module_run") + private StateApplyResult>> cpuArchSpecs; + @SerializedName(value = "mgrcompat_|-udev_|-udev.exportdb_|-module_run", alternate = {"mgrcompat_|-udevdb_|-udevdb.exportdb_|-module_run"}) private StateApplyResult>>> udevdb; @@ -278,4 +281,15 @@ public String getContainerRuntime() { public String getUname() { return uname.map(ret -> ret.getChanges().getStdout()).orElse(null); } + + /** + * Get the CPU architecture specific information + * @return the specs Map + */ + public Map getCpuArchSpecs() { + if (cpuArchSpecs == null) { + return Collections.emptyMap(); + } + return cpuArchSpecs.getChanges().getRet(); + } } diff --git a/java/code/src/com/suse/scc/model/SCCHwInfoJson.java b/java/code/src/com/suse/scc/model/SCCHwInfoJson.java index 7b6e90ca3cc7..245160df6df7 100644 --- a/java/code/src/com/suse/scc/model/SCCHwInfoJson.java +++ b/java/code/src/com/suse/scc/model/SCCHwInfoJson.java @@ -16,6 +16,7 @@ import com.google.gson.annotations.SerializedName; +import java.util.Map; import java.util.Set; /** @@ -42,6 +43,8 @@ public class SCCHwInfoJson { private String cloudProvider; private Set sap; + @SerializedName("arch_specs") + private Map archSpecs; public int getCpus() { return cpus; @@ -122,4 +125,11 @@ public String getContainerRuntime() { public void setContainerRuntime(String containerRuntimeIn) { containerRuntime = containerRuntimeIn; } + public Map getArchSpecs() { + return archSpecs; + } + + public void setArchSpecs(Map archSpecsIn) { + archSpecs = archSpecsIn; + } } diff --git a/java/code/src/com/suse/scc/registration/SCCSystemRegistrationSystemDataAcquisitor.java b/java/code/src/com/suse/scc/registration/SCCSystemRegistrationSystemDataAcquisitor.java index 83994477c642..f1522b5a570d 100644 --- a/java/code/src/com/suse/scc/registration/SCCSystemRegistrationSystemDataAcquisitor.java +++ b/java/code/src/com/suse/scc/registration/SCCSystemRegistrationSystemDataAcquisitor.java @@ -31,12 +31,17 @@ import com.suse.scc.model.SCCMinProductJson; import com.suse.scc.model.SCCRegisterSystemJson; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.commons.lang3.RandomStringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.security.SecureRandom; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -97,6 +102,20 @@ private Optional getPayload(SCCRegCacheItem rci) { Optional cpu = ofNullable(srv.getCpu()); cpu.flatMap(c -> ofNullable(c.getNrCPU())).ifPresent(c -> hwInfo.setCpus(c.intValue())); cpu.flatMap(c -> ofNullable(c.getNrsocket())).ifPresent(c -> hwInfo.setSockets(c.intValue())); + cpu.ifPresent( + c -> { + try { + var archSpecs = new ObjectMapper() + .readValue(c.getArchSpecs(), new TypeReference>() { }); + hwInfo.setArchSpecs(archSpecs); + } + catch (JsonProcessingException e) { + LOG.warn("Failed to parse archSpecs from CPU entity. Field will be ignored.", e); + LOG.debug("Raw archSpecs JSON stored in the database: {}", c.getArchSpecs()); + } + } + ); + hwInfo.setArch(srv.getServerArch().getLabel().split("-")[0]); if (srv.isVirtualGuest()) { VirtualInstance virtualInstance = srv.getVirtualInstance(); diff --git a/java/spacewalk-java.changes.welder.scc-cpu-telemetry-data b/java/spacewalk-java.changes.welder.scc-cpu-telemetry-data new file mode 100644 index 000000000000..6ab354fbfe6b --- /dev/null +++ b/java/spacewalk-java.changes.welder.scc-cpu-telemetry-data @@ -0,0 +1 @@ +- Send CPU architecture specific data to SCC (jsc#SUMA-406) diff --git a/schema/spacewalk/common/tables/rhnCpu.sql b/schema/spacewalk/common/tables/rhnCpu.sql index f7e168cd90d7..dd9f576a7dd9 100644 --- a/schema/spacewalk/common/tables/rhnCpu.sql +++ b/schema/spacewalk/common/tables/rhnCpu.sql @@ -46,6 +46,7 @@ CREATE TABLE rhnCpu apic VARCHAR(32), apmVersion VARCHAR(32), chipset VARCHAR(64), + arch_specs JSONB, created TIMESTAMPTZ DEFAULT (current_timestamp) NOT NULL, modified TIMESTAMPTZ diff --git a/schema/spacewalk/postgres/triggers/rhnCpu.sql b/schema/spacewalk/postgres/triggers/rhnCpu.sql index 1026431b47d6..f647a5c12dc5 100644 --- a/schema/spacewalk/postgres/triggers/rhnCpu.sql +++ b/schema/spacewalk/postgres/triggers/rhnCpu.sql @@ -29,5 +29,5 @@ create trigger rhn_cpu_up_trig after update on rhnCpu for each row -when (OLD.nrcpu is distinct from NEW.nrcpu OR OLD.nrsocket is distinct from NEW.nrsocket OR OLD.nrcore is distinct from NEW.nrcore OR OLD.nrthread is distinct from NEW.nrthread) +when (OLD.nrcpu is distinct from NEW.nrcpu OR OLD.nrsocket is distinct from NEW.nrsocket OR OLD.nrcore is distinct from NEW.nrcore OR OLD.nrthread is distinct from NEW.nrthread OR OLD.arch_specs is distinct from NEW.arch_specs) execute procedure rhn_cpu_up_trig_fun(); diff --git a/schema/spacewalk/susemanager-schema.changes.welder.cpu-arch-specs b/schema/spacewalk/susemanager-schema.changes.welder.cpu-arch-specs new file mode 100644 index 000000000000..4765c371401f --- /dev/null +++ b/schema/spacewalk/susemanager-schema.changes.welder.cpu-arch-specs @@ -0,0 +1 @@ +- Store CPU architecture specific data (jsc#SUMA-406) diff --git a/schema/spacewalk/upgrade/susemanager-schema-5.1.2-to-susemanager-schema-5.1.3/300-add-arch-specs-column-to-rhnCpu.sql b/schema/spacewalk/upgrade/susemanager-schema-5.1.2-to-susemanager-schema-5.1.3/300-add-arch-specs-column-to-rhnCpu.sql new file mode 100644 index 000000000000..ab65f0ea7158 --- /dev/null +++ b/schema/spacewalk/upgrade/susemanager-schema-5.1.2-to-susemanager-schema-5.1.3/300-add-arch-specs-column-to-rhnCpu.sql @@ -0,0 +1 @@ +ALTER TABLE rhnCpu ADD COLUMN IF NOT EXISTS arch_specs JSONB; diff --git a/schema/spacewalk/upgrade/susemanager-schema-5.1.2-to-susemanager-schema-5.1.3/301-update-sccregcache-trigger.sql b/schema/spacewalk/upgrade/susemanager-schema-5.1.2-to-susemanager-schema-5.1.3/301-update-sccregcache-trigger.sql new file mode 100644 index 000000000000..56d270d46a9e --- /dev/null +++ b/schema/spacewalk/upgrade/susemanager-schema-5.1.2-to-susemanager-schema-5.1.3/301-update-sccregcache-trigger.sql @@ -0,0 +1,8 @@ +drop trigger if exists rhn_cpu_up_trig on rhnCpu; + +create trigger +rhn_cpu_up_trig +after update on rhnCpu +for each row +when (OLD.nrcpu is distinct from NEW.nrcpu OR OLD.nrsocket is distinct from NEW.nrsocket OR OLD.nrcore is distinct from NEW.nrcore OR OLD.nrthread is distinct from NEW.nrthread OR OLD.arch_specs is distinct from NEW.arch_specs) +execute procedure rhn_cpu_up_trig_fun(); diff --git a/susemanager-utils/susemanager-sls/salt/hardware/profileupdate.sls b/susemanager-utils/susemanager-sls/salt/hardware/profileupdate.sls index 56e5bed06719..1ae7c1a803db 100644 --- a/susemanager-utils/susemanager-sls/salt/hardware/profileupdate.sls +++ b/susemanager-utils/susemanager-sls/salt/hardware/profileupdate.sls @@ -188,6 +188,10 @@ uname: container_runtime: mgrcompat.module_run: - name: container_runtime.get_container_runtime + +cpu_arch_specs: + mgrcompat.module_run: + - name: cpuinfo.arch_specs - require: {%- if grains.get('__suse_reserved_saltutil_states_support', False) %} - saltutil: sync_modules diff --git a/susemanager-utils/susemanager-sls/src/modules/cpuinfo.py b/susemanager-utils/susemanager-sls/src/modules/cpuinfo.py new file mode 100644 index 000000000000..db09ad957c2c --- /dev/null +++ b/susemanager-utils/susemanager-sls/src/modules/cpuinfo.py @@ -0,0 +1,114 @@ +""" +CPU Architecture Metadata Detection Module for SaltStack + +This module provides functionality to collect extended CPU architecture-specific metadata from the target system. + +It detects the system architecture and gathers relevant hardware details based on the architecture, such as information +for PowerPC (ppc64), ARM (arm64), and IBM Z (s390) systems. + +References: + - Most of the code was ported from + https://github.com/SUSE/connect-ng/blob/main/internal/collectors/cpu.go +""" + +import logging +import re +import subprocess + +log = logging.getLogger(__name__) + +def arch_specs(): + """ + Collect extended CPU architecture-specific metadata. + """ + specs = {} + arch = _get_architecture() + + if arch in ["ppc64", "ppc64le"]: + _add_ppc64_extras(specs) + elif arch in ["arm64", "aarch64"]: + _add_arm64_extras(specs) + elif arch.startswith("s390"): + _add_z_systems_extras(specs) + + return specs + +def _get_architecture(): + """ + Detect the system architecture. + """ + try: + return subprocess.check_output(["uname", "-m"], stderr=subprocess.PIPE).decode().strip() + except (FileNotFoundError, subprocess.CalledProcessError, UnicodeDecodeError): + log.warning("Failed to determine system architecture. Falling back to 'unknown'.", exc_info=True) + return "unknown" + +def _add_ppc64_extras(specs): + _add_device_tree(specs) + + lparcfg_content = _read_file("/proc/ppc64/lparcfg") + if lparcfg_content: + match = re.search(r"shared_processor_mode\s*=\s*(\d+)", lparcfg_content) + if match: + specs["lpar_mode"] = "shared" if match.group(1) == "1" else "dedicated" + +def _add_arm64_extras(specs): + _add_device_tree(specs) + try: + output = subprocess.check_output(["dmidecode", "-t", "processor"], stderr=subprocess.PIPE).decode() + specs["family"] = _exact_string_match("Family", output) + specs["manufacturer"] = _exact_string_match("Manufacturer", output) + specs["signature"] = _exact_string_match("Signature", output) + except (OSError): + log.warning("Failed to retrieve arm64 CPU details.", exc_info=True) + +def _add_z_systems_extras(specs): + """ + Collect extended metadata for z systems based on `read_values -s`. + """ + try: + output = subprocess.check_output(["read_values", "-s"], stderr=subprocess.PIPE).decode() + if "VM00" in output: + specs["hypervisor"] = "zvm" + specs["type"] = _exact_string_match("Type", output) + specs["layer_type"] = _exact_string_match("VM00 Name", output) + elif "LPAR" in output: + specs["hypervisor"] = "lpar" + specs["type"] = _exact_string_match("Type", output) + specs["layer_type"] = _exact_string_match("LPAR Name", output) + + specs["sockets"] = _exact_string_match("Sockets", output) + + except (FileNotFoundError, subprocess.CalledProcessError): + log.warning("Failed to retrieve z system CPU details.", exc_info=True) + +def _add_device_tree(specs): + """ + Attempts to read the device tree from predefined paths. + """ + device_tree_paths = [ + "/sys/firmware/devicetree/base/compatible", + "/sys/firmware/devicetree/base/hypervisor/compatible", + ] + for path in device_tree_paths: + content = _read_file(path) + if content: + specs["device_tree"] = content.replace("\x00", "").strip() + break + +def _exact_string_match(key, text): + """ + Extract a value based on a key in the text. + """ + match = re.search(rf"{key}\s*:\s*(.*)", text) + return match.group(1).strip() if match else "" + +def _read_file(path): + """ + Helper to read a file and return its content. + """ + try: + with open(path, "r", errors="replace") as f: + return f.read() + except FileNotFoundError: + return "" diff --git a/susemanager-utils/susemanager-sls/src/tests/test_module_cpuinfo.py b/susemanager-utils/susemanager-sls/src/tests/test_module_cpuinfo.py new file mode 100644 index 000000000000..4565491909d2 --- /dev/null +++ b/susemanager-utils/susemanager-sls/src/tests/test_module_cpuinfo.py @@ -0,0 +1,75 @@ +import pytest +from unittest.mock import MagicMock, patch +import subprocess +from ..modules import cpuinfo + +@pytest.fixture +def mock_subprocess(): + with patch("subprocess.check_output") as mock: + yield mock + +@pytest.fixture +def mock_read_file(): + with patch("builtins.open", MagicMock(return_value=MagicMock(read=MagicMock(return_value="test content")))): + yield + +def test_get_architecture_success(mock_subprocess): + mock_subprocess.return_value = "x86_64".encode() + result = cpuinfo._get_architecture() + assert result == "x86_64" + +def test_get_architecture_failure(mock_subprocess): + mock_subprocess.side_effect = subprocess.CalledProcessError(1, "uname") + result = cpuinfo._get_architecture() + assert result == "unknown" + +def test_arch_specs_unknown(mock_subprocess): + mock_subprocess.return_value = "unknown".encode() + specs = cpuinfo.arch_specs() + assert specs == {} + +def test_arch_specs_ppc64(mock_subprocess): + mock_subprocess.return_value = "ppc64".encode() + cpuinfo._read_file = MagicMock( + side_effect=lambda path: "shared_processor_mode = 1" + if path == "/proc/ppc64/lparcfg" + else "device tree content" + ) + specs = cpuinfo.arch_specs() + assert specs == {"lpar_mode": "shared", "device_tree": "device tree content"} + + +def test_arch_specs_arm64(mock_subprocess): + cpuinfo._get_architecture = MagicMock(return_value="arm64") + cpuinfo._read_file = MagicMock(return_value="device tree file content") + mock_subprocess.return_value = "Family: test_family\nManufacturer: test_manufacturer\nSignature: test_signature".encode() + specs = cpuinfo.arch_specs() + assert specs == { + "family": "test_family", + "manufacturer": "test_manufacturer", + "signature": "test_signature", + "device_tree": "device tree file content" + } + +@pytest.mark.parametrize( + "output, expected_specs", + [ + ("VM00 Type: test_type\nVM00 Name: test_layer\nSockets: test_sockets", + {"hypervisor": "zvm", "type": "test_type", "layer_type": "test_layer", "sockets": "test_sockets"}), + ] +) +def test_add_z_systems_extras(output, expected_specs, mock_subprocess): + mock_subprocess.return_value = output.encode() + specs = {} + cpuinfo._add_z_systems_extras(specs) + assert specs == expected_specs + +def test_exact_string_match(): + text = "Family: test_family\nManufacturer: test_manufacturer\nSignature: test_signature" + result = cpuinfo._exact_string_match("Family", text) + assert result == "test_family" + +def test_read_file_failure(): + cpuinfo._read_file = MagicMock(return_value="") + result = cpuinfo._read_file("/path/to/nonexistent/file") + assert result == "" diff --git a/susemanager-utils/susemanager-sls/susemanager-sls.changes.welder.scc-cpu-telemetry-data b/susemanager-utils/susemanager-sls/susemanager-sls.changes.welder.scc-cpu-telemetry-data new file mode 100644 index 000000000000..9b7289542b4c --- /dev/null +++ b/susemanager-utils/susemanager-sls/susemanager-sls.changes.welder.scc-cpu-telemetry-data @@ -0,0 +1,2 @@ +- Collect CPU architecture specific data on hardware + profile update (jsc#SUMA-406)