From 1809b0c86647f581637ca79c0f9e12713c8d3398 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Sun, 11 Oct 2026 11:27:52 -0300 Subject: [PATCH] kvm: prefer the base speed for AMD CPUs + a few refactorings --- api/src/main/java/com/cloud/host/Host.java | 1 + .../resource/LibvirtComputingResource.java | 4 + .../cloudstack/utils/linux/KVMHostInfo.java | 135 +++++++++++++----- .../utils/linux/KVMHostInfoTest.java | 107 ++++++++++++-- ui/public/locales/en.json | 1 + ui/src/components/view/InfoCard.vue | 3 + 6 files changed, 203 insertions(+), 48 deletions(-) diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index cca250c2a90f..a4664c0859f5 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -75,6 +75,7 @@ public static String[] toStrings(Host.Type... types) { String HOST_VIRTV2V_INPLACE_SUPPORT = "host.virtv2v.inplace.support"; String HOST_QEMU_RBD_SUPPORT = "host.qemu.rbd.support"; String HOST_SSH_PORT = "host.ssh.port"; + String HOST_CPU_MAX_SPEED_MHZ = "host.cpu.max.speed.mhz"; String HOST_CDROM_MAX_COUNT = "host.cdrom.max.count"; String GUEST_OS_CATEGORY_ID = "guest.os.category.id"; String GUEST_OS_RULE = "guest.os.rule"; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 7c3f826be171..626aed19d0a2 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -17,6 +17,7 @@ package com.cloud.hypervisor.kvm.resource; import static com.cloud.host.Host.HOST_CDROM_MAX_COUNT; +import static com.cloud.host.Host.HOST_CPU_MAX_SPEED_MHZ; import static com.cloud.host.Host.HOST_INSTANCE_CONVERSION; import static com.cloud.host.Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM; import static com.cloud.host.Host.HOST_OVFTOOL_VERSION; @@ -4449,6 +4450,9 @@ public StartupCommand[] initialize() { new StartupRoutingCommand(info.getAllocatableCpus(), info.getCpuSpeed(), info.getTotalMemory(), info.getReservedMemory(), capabilities, hypervisorType, RouterPrivateIpStrategy.HostLocal); cmd.setCpuSockets(info.getCpuSockets()); + if (info.getCpuMaxSpeed() > 0) { + cmd.getHostDetails().put(HOST_CPU_MAX_SPEED_MHZ, String.valueOf(info.getCpuMaxSpeed())); + } fillNetworkInformation(cmd); privateIp = cmd.getPrivateIpAddress(); cmd.getHostDetails().putAll(getVersionStrings()); diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/linux/KVMHostInfo.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/linux/KVMHostInfo.java index 1a562f1d330c..04e6cc80a418 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/linux/KVMHostInfo.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/linux/KVMHostInfo.java @@ -22,6 +22,8 @@ import java.io.StringReader; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -43,6 +45,7 @@ import com.cloud.hypervisor.kvm.resource.LibvirtCapXMLParser; import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.utils.NumbersUtil; import com.cloud.utils.script.Script; public class KVMHostInfo { @@ -53,13 +56,20 @@ public class KVMHostInfo { private int allocatableCpus; private int cpusockets; private long cpuSpeed; + private long cpuMaxSpeed; private String cpuArch; private long totalMemory; private long reservedMemory; private long overCommitMemory; private List capabilities = new ArrayList<>(); - private static String cpuArchRetrieveExecutable = "arch"; - private static List cpuInfoFreqFileNames = List.of("/sys/devices/system/cpu/cpu0/cpufreq/base_frequency","/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq"); + private static final String LSCPU_CPU_INFO_COMMAND = "lscpu | grep -iE 'Model name|CPU max MHz|CPU dynamic MHz'"; + private static final String LSCPU_MODEL_NAME_FIELD = "Model name"; + private static final String LSCPU_CPU_MAX_MHZ_FIELD = "CPU max MHz"; + private static final String LSCPU_CPU_DYNAMIC_MHZ_FIELD = "CPU dynamic MHz"; + private static final Pattern LSCPU_MODEL_NAME_FREQUENCY_PATTERN = Pattern.compile("([0-9]\\.[0-9]+)GHz"); + private static final int MHZ_PER_GHZ = 1000; + private static final String cpuArchRetrieveExecutable = "arch"; + private static final List cpuInfoFreqFileNames = List.of("/sys/devices/system/cpu/cpu0/cpufreq/base_frequency","/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq"); public KVMHostInfo(long reservedMemory, long overCommitMemory, long manualSpeed, int reservedCpus) { this.cpuSpeed = manualSpeed; @@ -90,6 +100,14 @@ public long getCpuSpeed() { return this.cpuSpeed; } + /** + * @return the maximum frequency of the CPU, in MHz, which may include the turbo frequency; or 0 if it could not be retrieved. This value is only + * informative; {@link #getCpuSpeed()} is the one used to calculate the capacity of the host. + */ + public long getCpuMaxSpeed() { + return this.cpuMaxSpeed; + } + public long getTotalMemory() { return this.totalMemory; } @@ -114,59 +132,100 @@ public static boolean isHostS390x() { return "s390x".equals(System.getProperty("os.arch")); } - protected static long getCpuSpeed(final String cpabilities, final NodeInfo nodeInfo) { - long speed = 0L; - speed = getCpuSpeedFromCommandLscpu(); - if(speed > 0L) { + protected static long getCpuSpeed(final String lscpuOutput, final String capabilities, final NodeInfo nodeInfo) { + long speed = getBaseCpuSpeedFromLscpuOutput(lscpuOutput); + if (speed > 0L) { return speed; } speed = getCpuSpeedFromFile(); - if(speed > 0L) { + if (speed > 0L) { return speed; } - speed = getCpuSpeedFromHostCapabilities(cpabilities); - if(speed > 0L) { + speed = getCpuMaxSpeedFromLscpuOutput(lscpuOutput); + if (speed > 0L) { return speed; } - LOGGER.info(String.format("Using the value [%s] provided by Libvirt.", nodeInfo.mhz)); - speed = nodeInfo.mhz; - return speed; + speed = getCpuSpeedFromHostCapabilities(capabilities); + if (speed > 0L) { + return speed; + } + + LOGGER.info("Using the value [{}] provided by Libvirt.", nodeInfo.mhz); + return nodeInfo.mhz; } - private static long getCpuSpeedFromCommandLscpu() { - long speed = 0L; - LOGGER.info("Fetching CPU speed from command \"lscpu\"."); - try { - String command = "lscpu | grep -i 'CPU max MHz' | head -n 1 | sed 's/^.*: //' | xargs"; - if(isHostS390x()) { - command = "lscpu | grep 'CPU dynamic MHz' | cut -d ':' -f 2 | tr -d ' ' | awk '{printf \"%.1f\\n\", $1 / 1000}'"; - } - String result = Script.runSimpleBashScript(command); - speed = (long) (Float.parseFloat(result)); - LOGGER.info(String.format("Command [%s] resulted in the value [%s] for CPU speed.", command, speed)); + /** + * Retrieves the base CPU speed from the output of the {@code lscpu} command. The frequency advertised in the model name is preferred because the CPU speed + * is used to calculate the capacity of the host and the CPU quota of the VMs with CPU cap enabled; thus, it must be a frequency that the CPU is able to + * sustain. On s390x, the "CPU dynamic MHz" value is used instead. + * + * @return the base CPU speed, in MHz, or 0 if it could not be retrieved. + */ + protected static long getBaseCpuSpeedFromLscpuOutput(final String lscpuOutput) { + return getBaseCpuSpeedFromLscpuOutput(lscpuOutput, isHostS390x()); + } + + /** + * @param lscpuOutput the output of the {@code lscpu} command. + * @param isS390x whether the host CPU architecture is s390x; on s390x the base speed is read from the "CPU dynamic MHz" field. + * @return the base CPU speed, in MHz, or 0 if it could not be retrieved. + */ + protected static long getBaseCpuSpeedFromLscpuOutput(final String lscpuOutput, final boolean isS390x) { + if (isS390x) { + final long speed = (long) NumbersUtil.parseFloat(getLscpuFieldValue(lscpuOutput, LSCPU_CPU_DYNAMIC_MHZ_FIELD), 0f); + logCpuSpeedFromLscpuField(LSCPU_CPU_DYNAMIC_MHZ_FIELD, speed); return speed; - } catch (NullPointerException | NumberFormatException e) { - LOGGER.error(String.format("Unable to retrieve the CPU speed from lscpu."), e); } - try { - String command = "lscpu | grep -i 'Model name' | head -n 1 | egrep -o '[[:digit:]].[[:digit:]]+GHz' | sed 's/GHz//g'"; - if(isHostS390x()) { - command = "lscpu | grep 'CPU dynamic MHz' | cut -d ':' -f 2 | tr -d ' ' | awk '{printf \"%.1f\\n\", $1 / 1000}'"; - } - String result = Script.runSimpleBashScript(command); - speed = (long) (Float.parseFloat(result) * 1000); - LOGGER.info(String.format("Command [%s] resulted in the value [%s] for CPU speed.", command, speed)); + final String modelName = getLscpuFieldValue(lscpuOutput, LSCPU_MODEL_NAME_FIELD); + final Matcher matcher = LSCPU_MODEL_NAME_FREQUENCY_PATTERN.matcher(StringUtils.defaultString(modelName)); + if (matcher.find()) { + final long speed = (long) (NumbersUtil.parseFloat(matcher.group(1), 0f) * MHZ_PER_GHZ); + logCpuSpeedFromLscpuField(LSCPU_MODEL_NAME_FIELD, speed); return speed; - } catch (NullPointerException | NumberFormatException e) { - LOGGER.error(String.format("Unable to retrieve the CPU speed from lscpu."), e); } + logCpuSpeedFromLscpuField(LSCPU_MODEL_NAME_FIELD, 0L); + return 0L; + } + + /** + * @return the maximum frequency of the CPU, in MHz, from the output of the {@code lscpu} command; or 0 if it could not be retrieved. This value may + * include the turbo frequency; it is only informative. + */ + protected static long getCpuMaxSpeedFromLscpuOutput(final String lscpuOutput) { + final long speed = (long) NumbersUtil.parseFloat(getLscpuFieldValue(lscpuOutput, LSCPU_CPU_MAX_MHZ_FIELD), 0f); + logCpuSpeedFromLscpuField(LSCPU_CPU_MAX_MHZ_FIELD, speed); return speed; } - private static long getCpuSpeedFromFile() { + private static void logCpuSpeedFromLscpuField(final String field, final long speed) { + if (speed > 0L) { + LOGGER.info("Retrieved the CPU speed [{}] MHz from the \"{}\" field of the lscpu command output.", speed, field); + } else { + LOGGER.debug("Unable to retrieve the CPU speed from the \"{}\" field of the lscpu command output.", field); + } + } + + private static String getLscpuFieldValue(final String lscpuOutput, final String field) { + if (StringUtils.isBlank(lscpuOutput)) { + return null; + } + for (final String line : lscpuOutput.split("\n")) { + if (StringUtils.startsWithIgnoreCase(line.trim(), field)) { + return StringUtils.substringAfter(line, ":").trim(); + } + } + return null; + } + + private static String getLscpuOutput() { + LOGGER.info("Fetching CPU speed from command \"lscpu\"."); + return Script.executeCommand(LSCPU_CPU_INFO_COMMAND).first(); + } + + protected static long getCpuSpeedFromFile() { for (final String cpuInfoFreqFileName: cpuInfoFreqFileNames) { LOGGER.info(String.format("Fetching CPU speed from file [%s].", cpuInfoFreqFileName)); try (Reader reader = new FileReader(cpuInfoFreqFileName)) { @@ -213,11 +272,13 @@ private void getHostInfoFromLibvirt() { final Connect conn = LibvirtConnection.getConnection(); final NodeInfo hosts = conn.nodeInfo(); final String capabilities = conn.getCapabilities(); + final String lscpuOutput = getLscpuOutput(); if (this.cpuSpeed == 0) { - this.cpuSpeed = getCpuSpeed(capabilities, hosts); + this.cpuSpeed = getCpuSpeed(lscpuOutput, capabilities, hosts); } else { LOGGER.debug(String.format("Using existing configured CPU frequency %s", this.cpuSpeed)); } + this.cpuMaxSpeed = getCpuMaxSpeedFromLscpuOutput(lscpuOutput); /* * Some CPUs report a single socket and multiple NUMA cells. diff --git a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/linux/KVMHostInfoTest.java b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/linux/KVMHostInfoTest.java index b41e2bf214a3..d93f95b41f64 100644 --- a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/linux/KVMHostInfoTest.java +++ b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/linux/KVMHostInfoTest.java @@ -31,6 +31,25 @@ @RunWith(MockitoJUnitRunner.class) public class KVMHostInfoTest { + private static final String CAPABILITIES_WITH_TSC_FREQUENCY = "\n" + + "8a330742-345f-b0df-7954-c9960b88116c\n" + + " \n" + + " x86_64\n" + + " Opteron_G2\n" + + " AMD\n" + + " \n" + + " \n" + + "\n"; + private static final String CAPABILITIES_WITHOUT_TSC_FREQUENCY = "\n" + + " \n" + + " AMD\n" + + " \n" + + "\n"; + private static final String LSCPU_OUTPUT_WITH_MODEL_NAME_FREQUENCY = "Model name: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz\nCPU max MHz: 3400.0000"; + private static final String LSCPU_OUTPUT_WITHOUT_MODEL_NAME_FREQUENCY = "Model name: AMD EPYC 7742 64-Core Processor\nCPU max MHz: 3400.0000"; + private static final String LSCPU_OUTPUT_WITHOUT_FREQUENCY = "Model name: AMD EPYC 7742 64-Core Processor"; + private static final String LSCPU_OUTPUT_WITH_CPU_DYNAMIC_MHZ = "Model name: IBM/S390\nCPU dynamic MHz: 5000.0000"; + @Test public void getCpuSpeed() { if (!System.getProperty("os.name").equals("Linux")) { @@ -39,21 +58,12 @@ public void getCpuSpeed() { Assume.assumeTrue(SystemUtils.IS_OS_LINUX); NodeInfo nodeInfo = Mockito.mock(NodeInfo.class); nodeInfo.mhz = 1000; - Assert.assertTrue(KVMHostInfo.getCpuSpeed(null, nodeInfo) > 0L); + Assert.assertTrue(KVMHostInfo.getCpuSpeed(null, null, nodeInfo) > 0L); } @Test public void getCpuSpeedFromHostCapabilities() { - String capabilities = "\n" + - "8a330742-345f-b0df-7954-c9960b88116c\n" + - " \n" + - " x86_64\n" + - " Opteron_G2\n" + - " AMD\n" + - " \n" + - " \n" + - "\n"; - Assert.assertEquals(2350L, KVMHostInfo.getCpuSpeedFromHostCapabilities(capabilities)); + Assert.assertEquals(2350L, KVMHostInfo.getCpuSpeedFromHostCapabilities(CAPABILITIES_WITH_TSC_FREQUENCY)); } @Test @@ -102,4 +112,79 @@ public void reservedCpuCoresTest() throws Exception { Assert.assertEquals("Misconfigured/too large CPU reserve", 0, kvmHostInfo.getAllocatableCpus()); } } + + @Test + public void getBaseCpuSpeedFromLscpuOutputPrefersModelNameFrequency() { + Assert.assertEquals(2400L, KVMHostInfo.getBaseCpuSpeedFromLscpuOutput(LSCPU_OUTPUT_WITH_MODEL_NAME_FREQUENCY, false)); + } + + @Test + public void getBaseCpuSpeedFromLscpuOutputReturnsZeroWhenModelNameHasNoFrequency() { + Assert.assertEquals(0L, KVMHostInfo.getBaseCpuSpeedFromLscpuOutput(LSCPU_OUTPUT_WITHOUT_MODEL_NAME_FREQUENCY, false)); + } + + @Test + public void getBaseCpuSpeedFromLscpuOutputUsesCpuDynamicMhzForS390x() { + Assert.assertEquals(5000L, KVMHostInfo.getBaseCpuSpeedFromLscpuOutput(LSCPU_OUTPUT_WITH_CPU_DYNAMIC_MHZ, true)); + } + + @Test + public void getCpuMaxSpeedFromLscpuOutputReturnsCpuMaxMhz() { + Assert.assertEquals(3400L, KVMHostInfo.getCpuMaxSpeedFromLscpuOutput(LSCPU_OUTPUT_WITH_MODEL_NAME_FREQUENCY)); + } + + @Test + public void getCpuMaxSpeedFromLscpuOutputReturnsZeroWhenLscpuHasNoCpuMaxMhz() { + Assert.assertEquals(0L, KVMHostInfo.getCpuMaxSpeedFromLscpuOutput(LSCPU_OUTPUT_WITHOUT_FREQUENCY)); + } + + @Test + public void getCpuSpeedPrefersBaseCpuSpeedFromLscpu() { + try (MockedStatic mocked = Mockito.mockStatic(KVMHostInfo.class, Mockito.CALLS_REAL_METHODS)) { + mocked.when(KVMHostInfo::isHostS390x).thenReturn(false); + Assert.assertEquals(2400L, KVMHostInfo.getCpuSpeed(LSCPU_OUTPUT_WITH_MODEL_NAME_FREQUENCY, CAPABILITIES_WITH_TSC_FREQUENCY, nodeInfoWithMhz(1000))); + } + } + + @Test + public void getCpuSpeedPrefersCpuFrequencyFileOverLscpuMaxMhz() { + try (MockedStatic mocked = Mockito.mockStatic(KVMHostInfo.class, Mockito.CALLS_REAL_METHODS)) { + mocked.when(KVMHostInfo::isHostS390x).thenReturn(false); + mocked.when(KVMHostInfo::getCpuSpeedFromFile).thenReturn(2500L); + Assert.assertEquals(2500L, KVMHostInfo.getCpuSpeed(LSCPU_OUTPUT_WITHOUT_MODEL_NAME_FREQUENCY, CAPABILITIES_WITH_TSC_FREQUENCY, nodeInfoWithMhz(1000))); + } + } + + @Test + public void getCpuSpeedFallsBackToLscpuMaxMhzWhenCpuFrequencyFileIsNotAvailable() { + try (MockedStatic mocked = Mockito.mockStatic(KVMHostInfo.class, Mockito.CALLS_REAL_METHODS)) { + mocked.when(KVMHostInfo::isHostS390x).thenReturn(false); + mocked.when(KVMHostInfo::getCpuSpeedFromFile).thenReturn(0L); + Assert.assertEquals(3400L, KVMHostInfo.getCpuSpeed(LSCPU_OUTPUT_WITHOUT_MODEL_NAME_FREQUENCY, CAPABILITIES_WITH_TSC_FREQUENCY, nodeInfoWithMhz(1000))); + } + } + + @Test + public void getCpuSpeedFallsBackToHostCapabilities() { + try (MockedStatic mocked = Mockito.mockStatic(KVMHostInfo.class, Mockito.CALLS_REAL_METHODS)) { + mocked.when(KVMHostInfo::isHostS390x).thenReturn(false); + mocked.when(KVMHostInfo::getCpuSpeedFromFile).thenReturn(0L); + Assert.assertEquals(2350L, KVMHostInfo.getCpuSpeed(LSCPU_OUTPUT_WITHOUT_FREQUENCY, CAPABILITIES_WITH_TSC_FREQUENCY, nodeInfoWithMhz(1000))); + } + } + + @Test + public void getCpuSpeedFallsBackToLibvirt() { + try (MockedStatic mocked = Mockito.mockStatic(KVMHostInfo.class, Mockito.CALLS_REAL_METHODS)) { + mocked.when(KVMHostInfo::isHostS390x).thenReturn(false); + mocked.when(KVMHostInfo::getCpuSpeedFromFile).thenReturn(0L); + Assert.assertEquals(1000L, KVMHostInfo.getCpuSpeed(LSCPU_OUTPUT_WITHOUT_FREQUENCY, CAPABILITIES_WITHOUT_TSC_FREQUENCY, nodeInfoWithMhz(1000))); + } + } + + private static NodeInfo nodeInfoWithMhz(final int mhz) { + final NodeInfo nodeInfo = Mockito.mock(NodeInfo.class); + nodeInfo.mhz = mhz; + return nodeInfo; + } } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 7396d9c9a247..3a1c7357269b 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1313,6 +1313,7 @@ "label.home": "Home", "label.host": "IP address", "label.host.alerts": "Hosts in alert state", +"label.host.cpu.max.speed": "CPU maximum frequency (turbo)", "label.host.name": "Host name", "label.host.ovftool.version": "OVFTool Version", "label.host.qemu.img.version": "QEMU Image Version", diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 9b08a388982b..965d91dc303f 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -259,6 +259,9 @@ {{ resource.arch }} +
+ {{ $t('label.host.cpu.max.speed') }}: {{ (resource.details['host.cpu.max.speed.mhz'] / 1000.0).toFixed(2) }} GHz +