From 6501879e08bfe7d645c5785c756a7c8bc0b4899c Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 5 May 2026 10:59:10 +0200 Subject: [PATCH 1/3] Routed: get vm network statistics on Routed network --- .../java/com/cloud/server/StatsCollector.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/cloud/server/StatsCollector.java b/server/src/main/java/com/cloud/server/StatsCollector.java index b6637aa87fc3..4844a70eb899 100644 --- a/server/src/main/java/com/cloud/server/StatsCollector.java +++ b/server/src/main/java/com/cloud/server/StatsCollector.java @@ -57,6 +57,7 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.management.ManagementServerHost; +import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; @@ -113,6 +114,8 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.as.AutoScaleManager; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; import com.cloud.org.Cluster; import com.cloud.resource.ResourceManager; import com.cloud.resource.ResourceState; @@ -202,6 +205,11 @@ @Component public class StatsCollector extends ManagerBase implements ComponentMethodInterceptable, Configurable, DbStatsCollection { + @Inject + private NetworkDao networkDao; + @Inject + private RoutedIpv4Manager routedIpv4Manager; + public static enum ExternalStatsProtocol { NONE("none"), GRAPHITE("graphite"), INFLUXDB("influxdb"); String _type; @@ -267,9 +275,9 @@ public String toString() { private static final ConfigKey vmDiskStatsIntervalMin = new ConfigKey<>("Advanced", Integer.class, "vm.disk.stats.interval.min", "300", "Minimal interval (in seconds) to report vm disk statistics. If vm.disk.stats.interval is smaller than this, use this to report vm disk statistics.", false); private static final ConfigKey vmNetworkStatsInterval = new ConfigKey<>("Advanced", Integer.class, "vm.network.stats.interval", "0", - "Interval (in seconds) to report vm network statistics (for Shared networks). Vm network statistics will be disabled if this is set to 0 or less than 0.", false); + "Interval (in seconds) to report vm network statistics (for Shared and Routed networks). Vm network statistics will be disabled if this is set to 0 or less than 0.", false); private static final ConfigKey vmNetworkStatsIntervalMin = new ConfigKey<>("Advanced", Integer.class, "vm.network.stats.interval.min", "300", - "Minimal Interval (in seconds) to report vm network statistics (for Shared networks). If vm.network.stats.interval is smaller than this, use this to report vm network statistics.", + "Minimal Interval (in seconds) to report vm network statistics (for Shared and Routed networks). If vm.network.stats.interval is smaller than this, use this to report vm network statistics.", false); private static final ConfigKey StatsTimeout = new ConfigKey<>("Advanced", Integer.class, "stats.timeout", "60000", "The timeout for stats call in milli seconds.", true, @@ -1582,8 +1590,11 @@ public void doInTransactionWithoutResult(TransactionStatus status) { sc_nic.addAnd("macAddress", SearchCriteria.Op.EQ, vmNetworkStatEntry.getMacAddress()); NicVO nic = _nicDao.search(sc_nic, null).get(0); List vlan = _vlanDao.listVlansByNetworkId(nic.getNetworkId()); - if (vlan == null || vlan.size() == 0 || vlan.get(0).getVlanType() != VlanType.DirectAttached) - continue; // only get network statistics for DirectAttached network (shared networks in Basic zone and Advanced zone with/without SG) + NetworkVO networkVO = networkDao.findById(nic.getNetworkId()); + if (CollectionUtils.isEmpty(vlan) || (vlan.get(0).getVlanType() != VlanType.DirectAttached + && (networkVO == null || !routedIpv4Manager.isRoutedNetwork(networkVO)))) { + continue; // only get network statistics for Shared or Routed network + } UserStatisticsVO previousvmNetworkStats = _userStatsDao.findBy(userVm.getAccountId(), userVm.getDataCenterId(), nic.getNetworkId(), nic.getIPv4Address(), vmId, "UserVm"); if (previousvmNetworkStats == null) { From 729cb0c917a9139ccda79c98f80578088f13e55b Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 6 May 2026 14:25:32 +0200 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- server/src/main/java/com/cloud/server/StatsCollector.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/cloud/server/StatsCollector.java b/server/src/main/java/com/cloud/server/StatsCollector.java index 4844a70eb899..c3382913b090 100644 --- a/server/src/main/java/com/cloud/server/StatsCollector.java +++ b/server/src/main/java/com/cloud/server/StatsCollector.java @@ -1591,8 +1591,10 @@ public void doInTransactionWithoutResult(TransactionStatus status) { NicVO nic = _nicDao.search(sc_nic, null).get(0); List vlan = _vlanDao.listVlansByNetworkId(nic.getNetworkId()); NetworkVO networkVO = networkDao.findById(nic.getNetworkId()); - if (CollectionUtils.isEmpty(vlan) || (vlan.get(0).getVlanType() != VlanType.DirectAttached - && (networkVO == null || !routedIpv4Manager.isRoutedNetwork(networkVO)))) { + boolean isRoutedNetwork = networkVO != null && routedIpv4Manager.isRoutedNetwork(networkVO); + boolean isDirectAttachedNetwork = CollectionUtils.isNotEmpty(vlan) + && vlan.get(0).getVlanType() == VlanType.DirectAttached; + if (!isRoutedNetwork && !isDirectAttachedNetwork) { continue; // only get network statistics for Shared or Routed network } UserStatisticsVO previousvmNetworkStats = _userStatsDao.findBy(userVm.getAccountId(), userVm.getDataCenterId(), nic.getNetworkId(), From bdb4a6b0dabd9cc836271f4c9a18433876063150 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 7 May 2026 12:17:14 +0200 Subject: [PATCH 3/3] Create method isNetworkEligibleForNetworkStats and add unit tests --- .../java/com/cloud/server/StatsCollector.java | 28 ++++-- .../com/cloud/server/StatsCollectorTest.java | 88 +++++++++++++++++++ 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/cloud/server/StatsCollector.java b/server/src/main/java/com/cloud/server/StatsCollector.java index c3382913b090..4f7a76c007a8 100644 --- a/server/src/main/java/com/cloud/server/StatsCollector.java +++ b/server/src/main/java/com/cloud/server/StatsCollector.java @@ -1589,12 +1589,7 @@ public void doInTransactionWithoutResult(TransactionStatus status) { SearchCriteria sc_nic = _nicDao.createSearchCriteria(); sc_nic.addAnd("macAddress", SearchCriteria.Op.EQ, vmNetworkStatEntry.getMacAddress()); NicVO nic = _nicDao.search(sc_nic, null).get(0); - List vlan = _vlanDao.listVlansByNetworkId(nic.getNetworkId()); - NetworkVO networkVO = networkDao.findById(nic.getNetworkId()); - boolean isRoutedNetwork = networkVO != null && routedIpv4Manager.isRoutedNetwork(networkVO); - boolean isDirectAttachedNetwork = CollectionUtils.isNotEmpty(vlan) - && vlan.get(0).getVlanType() == VlanType.DirectAttached; - if (!isRoutedNetwork && !isDirectAttachedNetwork) { + if (!isNetworkEligibleForNetworkStats(nic.getNetworkId())) { continue; // only get network statistics for Shared or Routed network } UserStatisticsVO previousvmNetworkStats = _userStatsDao.findBy(userVm.getAccountId(), userVm.getDataCenterId(), nic.getNetworkId(), @@ -2152,6 +2147,27 @@ protected boolean isCurrentVmDiskStatsDifferentFromPrevious(VmDiskStatisticsVO p return true; } + /** + * Returns {@code true} if the given network is eligible for VM network statistics collection. + * Only Shared (DirectAttached) networks and Routed networks qualify. + * + * @param networkId the network id to evaluate + * @return {@code true} when the network is routed or direct-attached, {@code false} otherwise + */ + protected boolean isNetworkEligibleForNetworkStats(Long networkId) { + if (networkId == null) { + return false; + } + List vlans = _vlanDao.listVlansByNetworkId(networkId); + boolean isDirectAttachedNetwork = CollectionUtils.isNotEmpty(vlans) + && vlans.get(0).getVlanType() == VlanType.DirectAttached; + if (isDirectAttachedNetwork) { + return true; + } + NetworkVO networkVO = networkDao.findById(networkId); + return networkVO != null && routedIpv4Manager.isRoutedNetwork(networkVO); + } + /** * Returns true if all the VmDiskStatsEntry are Zeros (Bytes read, Bytes write, IO read, and IO write must be all equals to zero) */ diff --git a/server/src/test/java/com/cloud/server/StatsCollectorTest.java b/server/src/test/java/com/cloud/server/StatsCollectorTest.java index 3578e6948a45..01d7d64a5ffd 100644 --- a/server/src/test/java/com/cloud/server/StatsCollectorTest.java +++ b/server/src/test/java/com/cloud/server/StatsCollectorTest.java @@ -26,6 +26,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -38,6 +39,7 @@ import com.cloud.utils.DateUtil; import com.google.gson.JsonSyntaxException; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.commons.collections.CollectionUtils; import org.influxdb.InfluxDB; @@ -64,7 +66,12 @@ import com.cloud.agent.api.GetStorageStatsCommand; import com.cloud.agent.api.VmDiskStatsEntry; import com.cloud.agent.api.VmStatsEntry; +import com.cloud.dc.Vlan.VlanType; +import com.cloud.dc.VlanVO; +import com.cloud.dc.dao.VlanDao; import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; import com.cloud.server.StatsCollector.ExternalStatsProtocol; import com.cloud.storage.StorageStats; import com.cloud.storage.VolumeStatsVO; @@ -118,6 +125,15 @@ public class StatsCollectorTest { @Mock private StoragePoolVO mockPool; + @Mock + private RoutedIpv4Manager routedIpv4Manager; + + @Mock + private NetworkDao networkDao; + + @Mock + private VlanDao vlanDao; + private static Gson gson = new Gson(); private Gson msStatsGson; @@ -701,6 +717,78 @@ public void testGsonDateFormatDeserializationWithDifferentDateFormat() throws Ex */ } + // ----------------------------------------------------------------------- + // Tests for isNetworkEligibleForNetworkStats + // ----------------------------------------------------------------------- + + private VlanVO buildVlan(VlanType type) { + VlanVO vlan = Mockito.mock(VlanVO.class); + Mockito.when(vlan.getVlanType()).thenReturn(type); + return vlan; + } + + @Test + public void isNetworkEligibleForNetworkStats_RoutedNetwork_ReturnsTrue() { + Long networkId = 1L; + NetworkVO networkVO = Mockito.mock(NetworkVO.class); + Mockito.when(networkDao.findById(networkId)).thenReturn(networkVO); + Mockito.when(vlanDao.listVlansByNetworkId(networkId)).thenReturn(Collections.emptyList()); + Mockito.when(routedIpv4Manager.isRoutedNetwork(networkVO)).thenReturn(true); + + Assert.assertTrue(statsCollector.isNetworkEligibleForNetworkStats(networkId)); + } + + @Test + public void isNetworkEligibleForNetworkStats_DirectAttachedNetwork_ReturnsTrue() { + Long networkId = 1L; + NetworkVO networkVO = Mockito.mock(NetworkVO.class); + Mockito.when(networkDao.findById(networkId)).thenReturn(networkVO); + Mockito.when(routedIpv4Manager.isRoutedNetwork(networkVO)).thenReturn(false); + List vlans = Collections.singletonList(buildVlan(VlanType.DirectAttached)); + Mockito.when(vlanDao.listVlansByNetworkId(networkId)).thenReturn(vlans); + + Assert.assertTrue(statsCollector.isNetworkEligibleForNetworkStats(networkId)); + } + + @Test + public void isNetworkEligibleForNetworkStats_NeitherRoutedNorDirectAttached_ReturnsFalse() { + Long networkId = 1L; + NetworkVO networkVO = Mockito.mock(NetworkVO.class); + Mockito.when(networkDao.findById(networkId)).thenReturn(networkVO); + Mockito.when(routedIpv4Manager.isRoutedNetwork(networkVO)).thenReturn(false); + List vlans = Collections.singletonList(buildVlan(VlanType.VirtualNetwork)); + Mockito.when(vlanDao.listVlansByNetworkId(networkId)).thenReturn(vlans); + + Assert.assertFalse(statsCollector.isNetworkEligibleForNetworkStats(networkId)); + } + + @Test + public void isNetworkEligibleForNetworkStats_NullNetworkAndEmptyVlans_ReturnsFalse() { + Long networkId = 1L; + Mockito.when(networkDao.findById(networkId)).thenReturn(null); + Mockito.when(vlanDao.listVlansByNetworkId(networkId)).thenReturn(Collections.emptyList()); + + Assert.assertFalse(statsCollector.isNetworkEligibleForNetworkStats(networkId)); + } + + @Test + public void isNetworkEligibleForNetworkStats_NullNetworkButDirectAttached_ReturnsTrue() { + Long networkId = 1L; + Mockito.when(networkDao.findById(networkId)).thenReturn(null); + List vlans = Collections.singletonList(buildVlan(VlanType.DirectAttached)); + Mockito.when(vlanDao.listVlansByNetworkId(networkId)).thenReturn(vlans); + + Assert.assertTrue(statsCollector.isNetworkEligibleForNetworkStats(networkId)); + Mockito.verify(routedIpv4Manager, Mockito.never()).isRoutedNetwork(Mockito.any()); + } + + @Test + public void isNetworkEligibleForNetworkStats_NullNetworkId_ReturnsFalse() { + Assert.assertFalse(statsCollector.isNetworkEligibleForNetworkStats(null)); + Mockito.verify(networkDao, Mockito.never()).findById(Mockito.anyLong()); + Mockito.verify(vlanDao, Mockito.never()).listVlansByNetworkId(Mockito.anyLong()); + } + private static class TestClass { private String str; private int num;