server, plugin: enhance storage stats for IOPS (#10034)

Adds framework layer change to allow retrieving and storing IOPS stats for storage pools. Custom `PrimaryStoreDriver` can implement method - `getStorageIopsStats` for returning IOPS stats. Existing method `getUsedIops` can also be overridden by such plugins when only used IOPS is returned.
For testing purpose, implementation has been added for simulator hypervisor plugin to return capacity and used IOPS for a pool.
For local storage pool, implementation has been added using iostat to return currently used IOPS.
StoragePoolResponse class has been updated to return IOPS values which allows showing IOPS values in UI for different storage pool related views and APIs.

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2025-01-07 17:17:12 +05:30 committed by GitHub
parent 9bc283e5c2
commit bd488c4bba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 788 additions and 108 deletions

View File

@ -26,4 +26,7 @@ public interface StorageStats {
* @return bytes capacity of the storage server
*/
public long getCapacityBytes();
Long getCapacityIops();
Long getUsedIops();
}

View File

@ -509,6 +509,7 @@ public class ApiConstants {
public static final String URL = "url";
public static final String USAGE_INTERFACE = "usageinterface";
public static final String USED_SUBNETS = "usedsubnets";
public static final String USED_IOPS = "usediops";
public static final String USER_DATA = "userdata";
public static final String USER_DATA_NAME = "userdataname";

View File

@ -97,6 +97,10 @@ public class StoragePoolResponse extends BaseResponseWithAnnotations {
@Param(description = "total min IOPS currently in use by volumes")
private Long allocatedIops;
@SerializedName(ApiConstants.USED_IOPS)
@Param(description = "total IOPS currently in use", since = "4.20.1")
private Long usedIops;
@SerializedName(ApiConstants.STORAGE_CUSTOM_STATS)
@Param(description = "the storage pool custom stats", since = "4.18.1")
private Map<String, String> customStats;
@ -312,6 +316,14 @@ public class StoragePoolResponse extends BaseResponseWithAnnotations {
this.allocatedIops = allocatedIops;
}
public Long getUsedIops() {
return usedIops;
}
public void setUsedIops(Long usedIops) {
this.usedIops = usedIops;
}
public Map<String, String> getCustomStats() {
return customStats;
}

View File

@ -27,24 +27,46 @@ public class GetStorageStatsAnswer extends Answer implements StorageStats {
protected GetStorageStatsAnswer() {
}
protected long used;
protected long usedBytes;
protected long capacity;
protected long capacityBytes;
protected Long capacityIops;
protected Long usedIops;
@Override
public long getByteUsed() {
return used;
return usedBytes;
}
@Override
public long getCapacityBytes() {
return capacity;
return capacityBytes;
}
public GetStorageStatsAnswer(GetStorageStatsCommand cmd, long capacity, long used) {
@Override
public Long getCapacityIops() {
return capacityIops;
}
@Override
public Long getUsedIops() {
return usedIops;
}
public GetStorageStatsAnswer(GetStorageStatsCommand cmd, long capacityBytes, long usedBytes) {
super(cmd, true, null);
this.capacity = capacity;
this.used = used;
this.capacityBytes = capacityBytes;
this.usedBytes = usedBytes;
}
public GetStorageStatsAnswer(GetStorageStatsCommand cmd, long capacityBytes, long usedBytes, Long capacityIops, Long usedIops) {
super(cmd, true, null);
this.capacityBytes = capacityBytes;
this.usedBytes = usedBytes;
this.capacityIops = capacityIops;
this.usedIops = usedIops;
}
public GetStorageStatsAnswer(GetStorageStatsCommand cmd, String details) {

View File

@ -0,0 +1,81 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.agent.api;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class GetStorageStatsAnswerTest {
@Test
public void testDefaultConstructor() {
GetStorageStatsAnswer answer = new GetStorageStatsAnswer();
Assert.assertEquals(0, answer.getByteUsed());
Assert.assertEquals(0, answer.getCapacityBytes());
Assert.assertNull(answer.getCapacityIops());
Assert.assertNull(answer.getUsedIops());
}
@Test
public void testConstructorWithCapacityAndUsedBytes() {
GetStorageStatsCommand mockCmd = new GetStorageStatsCommand();
long capacityBytes = 1024L;
long usedBytes = 512L;
GetStorageStatsAnswer answer = new GetStorageStatsAnswer(mockCmd, capacityBytes, usedBytes);
Assert.assertEquals(capacityBytes, answer.getCapacityBytes());
Assert.assertEquals(usedBytes, answer.getByteUsed());
Assert.assertNull(answer.getCapacityIops());
Assert.assertNull(answer.getUsedIops());
}
@Test
public void testConstructorWithIops() {
GetStorageStatsCommand mockCmd = new GetStorageStatsCommand();
long capacityBytes = 2048L;
long usedBytes = 1024L;
Long capacityIops = 1000L;
Long usedIops = 500L;
GetStorageStatsAnswer answer = new GetStorageStatsAnswer(mockCmd, capacityBytes, usedBytes, capacityIops, usedIops);
Assert.assertEquals(capacityBytes, answer.getCapacityBytes());
Assert.assertEquals(usedBytes, answer.getByteUsed());
Assert.assertEquals(capacityIops, answer.getCapacityIops());
Assert.assertEquals(usedIops, answer.getUsedIops());
}
@Test
public void testErrorConstructor() {
GetStorageStatsCommand mockCmd = new GetStorageStatsCommand();
String errorDetails = "An error occurred";
GetStorageStatsAnswer answer = new GetStorageStatsAnswer(mockCmd, errorDetails);
Assert.assertFalse(answer.getResult());
Assert.assertEquals(errorDetails, answer.getDetails());
Assert.assertEquals(0, answer.getCapacityBytes());
Assert.assertEquals(0, answer.getByteUsed());
Assert.assertNull(answer.getCapacityIops());
Assert.assertNull(answer.getUsedIops());
}
}

2
debian/control vendored
View File

@ -24,7 +24,7 @@ Description: CloudStack server library
Package: cloudstack-agent
Architecture: all
Depends: ${python:Depends}, ${python3:Depends}, openjdk-17-jre-headless | java17-runtime-headless | java17-runtime | zulu-17, cloudstack-common (= ${source:Version}), lsb-base (>= 9), openssh-client, qemu-kvm (>= 2.5) | qemu-system-x86 (>= 5.2), libvirt-bin (>= 1.3) | libvirt-daemon-system (>= 3.0), iproute2, ebtables, vlan, ipset, python3-libvirt, ethtool, iptables, cryptsetup, rng-tools, rsync, lsb-release, ufw, apparmor, cpu-checker, libvirt-daemon-driver-storage-rbd
Depends: ${python:Depends}, ${python3:Depends}, openjdk-17-jre-headless | java17-runtime-headless | java17-runtime | zulu-17, cloudstack-common (= ${source:Version}), lsb-base (>= 9), openssh-client, qemu-kvm (>= 2.5) | qemu-system-x86 (>= 5.2), libvirt-bin (>= 1.3) | libvirt-daemon-system (>= 3.0), iproute2, ebtables, vlan, ipset, python3-libvirt, ethtool, iptables, cryptsetup, rng-tools, rsync, lsb-release, ufw, apparmor, cpu-checker, libvirt-daemon-driver-storage-rbd, sysstat
Recommends: init-system-helpers
Conflicts: cloud-agent, cloud-agent-libs, cloud-agent-deps, cloud-agent-scripts
Description: CloudStack agent

View File

@ -111,6 +111,14 @@ public interface PrimaryDataStoreDriver extends DataStoreDriver {
*/
Pair<Long, Long> getStorageStats(StoragePool storagePool);
/**
* Intended for managed storage
* returns the capacity and used IOPS or null if not supported
*/
default Pair<Long, Long> getStorageIopsStats(StoragePool storagePool) {
return null;
}
/**
* intended for managed storage
* returns true if the storage can provide the volume stats (physical and virtual size)

View File

@ -23,12 +23,16 @@ import java.util.List;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDaoImpl;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.commons.collections.CollectionUtils;
import com.cloud.storage.Storage.StoragePoolType;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.dao.VolumeDaoImpl;
import com.cloud.upgrade.SystemVmTemplateRegistration;
import com.cloud.utils.db.GenericSearchBuilder;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.exception.CloudRuntimeException;
public class Upgrade41700to41710 extends DbUpgradeAbstractImpl implements DbUpgradeSystemVmTemplate {
@ -95,24 +99,58 @@ public class Upgrade41700to41710 extends DbUpgradeAbstractImpl implements DbUpgr
}
}
private void updateStorPoolStorageType() {
protected PrimaryDataStoreDao getStorageDao() {
if (storageDao == null) {
storageDao = new PrimaryDataStoreDaoImpl();
List<StoragePoolVO> storPoolPools = storageDao.findPoolsByProvider("StorPool");
for (StoragePoolVO storagePoolVO : storPoolPools) {
if (StoragePoolType.SharedMountPoint == storagePoolVO.getPoolType()) {
storagePoolVO.setPoolType(StoragePoolType.StorPool);
storageDao.update(storagePoolVO.getId(), storagePoolVO);
}
updateStorageTypeForStorPoolVolumes(storagePoolVO.getId());
}
return storageDao;
}
private void updateStorageTypeForStorPoolVolumes(long storagePoolId) {
protected VolumeDao getVolumeDao() {
if (volumeDao == null) {
volumeDao = new VolumeDaoImpl();
List<VolumeVO> volumes = volumeDao.findByPoolId(storagePoolId, null);
for (VolumeVO volumeVO : volumes) {
volumeVO.setPoolType(StoragePoolType.StorPool);
volumeDao.update(volumeVO.getId(), volumeVO);
}
}
return volumeDao;
}
/*
GenericDao.customSearch using GenericSearchBuilder and GenericDao.update using
GenericDao.createSearchBuilder used here to prevent any future issues when new fields
are added to StoragePoolVO or VolumeVO and this upgrade path starts to fail.
*/
protected void updateStorPoolStorageType() {
StoragePoolVO pool = getStorageDao().createForUpdate();
pool.setPoolType(StoragePoolType.StorPool);
SearchBuilder<StoragePoolVO> sb = getStorageDao().createSearchBuilder();
sb.and("provider", sb.entity().getStorageProviderName(), SearchCriteria.Op.EQ);
sb.and("type", sb.entity().getPoolType(), SearchCriteria.Op.EQ);
sb.done();
SearchCriteria<StoragePoolVO> sc = sb.create();
sc.setParameters("provider", StoragePoolType.StorPool.name());
sc.setParameters("type", StoragePoolType.SharedMountPoint.name());
getStorageDao().update(pool, sc);
GenericSearchBuilder<StoragePoolVO, Long> gSb = getStorageDao().createSearchBuilder(Long.class);
gSb.selectFields(gSb.entity().getId());
gSb.and("provider", gSb.entity().getStorageProviderName(), SearchCriteria.Op.EQ);
gSb.done();
SearchCriteria<Long> gSc = gSb.create();
gSc.setParameters("provider", StoragePoolType.StorPool.name());
List<Long> poolIds = getStorageDao().customSearch(gSc, null);
updateStorageTypeForStorPoolVolumes(poolIds);
}
protected void updateStorageTypeForStorPoolVolumes(List<Long> storagePoolIds) {
if (CollectionUtils.isEmpty(storagePoolIds)) {
return;
}
VolumeVO volume = getVolumeDao().createForUpdate();
volume.setPoolType(StoragePoolType.StorPool);
SearchBuilder<VolumeVO> sb = getVolumeDao().createSearchBuilder();
sb.and("poolId", sb.entity().getPoolId(), SearchCriteria.Op.IN);
sb.done();
SearchCriteria<VolumeVO> sc = sb.create();
sc.setParameters("poolId", storagePoolIds.toArray());
getVolumeDao().update(volume, sc);
}
}

View File

@ -119,6 +119,9 @@ public class StoragePoolVO implements StoragePool {
@Column(name = "capacity_iops", updatable = true, nullable = true)
private Long capacityIops;
@Column(name = "used_iops", updatable = true, nullable = true)
private Long usedIops;
@Column(name = "hypervisor")
@Convert(converter = HypervisorTypeConverter.class)
private HypervisorType hypervisor;
@ -256,6 +259,14 @@ public class StoragePoolVO implements StoragePool {
return capacityIops;
}
public Long getUsedIops() {
return usedIops;
}
public void setUsedIops(Long usedIops) {
this.usedIops = usedIops;
}
@Override
public Long getClusterId() {
return clusterId;

View File

@ -32,3 +32,6 @@ CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.mshost_peer', 'fk_mshost_peer__
-- Add last_id to the volumes table
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'last_id', 'bigint(20) unsigned DEFAULT NULL');
-- Add used_iops column to support IOPS data in storage stats
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.storage_pool', 'used_iops', 'bigint unsigned DEFAULT NULL COMMENT "IOPS currently in use for this storage pool" ');

View File

@ -31,7 +31,9 @@ SELECT
`storage_pool`.`created` AS `created`,
`storage_pool`.`removed` AS `removed`,
`storage_pool`.`capacity_bytes` AS `capacity_bytes`,
`storage_pool`.`used_bytes` AS `used_bytes`,
`storage_pool`.`capacity_iops` AS `capacity_iops`,
`storage_pool`.`used_iops` AS `used_iops`,
`storage_pool`.`scope` AS `scope`,
`storage_pool`.`hypervisor` AS `hypervisor`,
`storage_pool`.`storage_provider_name` AS `storage_provider_name`,

View File

@ -0,0 +1,123 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.upgrade.dao;
import java.util.Collections;
import java.util.List;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDaoImpl;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.storage.Storage;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.dao.VolumeDaoImpl;
import com.cloud.utils.db.GenericSearchBuilder;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@RunWith(MockitoJUnitRunner.class)
public class Upgrade41700to41710Test {
@Spy
Upgrade41700to41710 upgrade41700to41710;
@Test
public void testGetStorageDao_FirstInvocationCreatesInstance() {
PrimaryDataStoreDao dao1 = upgrade41700to41710.getStorageDao();
Assert.assertNotNull(dao1);
Assert.assertTrue(dao1 instanceof PrimaryDataStoreDaoImpl);
}
@Test
public void testGetStorageDao_SubsequentInvocationReturnsSameInstance() {
PrimaryDataStoreDao dao1 = upgrade41700to41710.getStorageDao();
PrimaryDataStoreDao dao2 = upgrade41700to41710.getStorageDao();
Assert.assertSame(dao1, dao2);
}
@Test
public void testGetVolumeDao_FirstInvocationCreatesInstance() {
VolumeDao dao1 = upgrade41700to41710.getVolumeDao();
Assert.assertNotNull(dao1);
Assert.assertTrue(dao1 instanceof VolumeDaoImpl);
}
@Test
public void testGetVolumeDao_SubsequentInvocationReturnsSameInstance() {
VolumeDao dao1 = upgrade41700to41710.getVolumeDao();
VolumeDao dao2 = upgrade41700to41710.getVolumeDao();
Assert.assertSame(dao1, dao2);
}
@Test
public void testUpdateStorPoolStorageType_WithPoolIds() {
PrimaryDataStoreDao storageDao = Mockito.mock(PrimaryDataStoreDao.class);
Mockito.doReturn(storageDao).when(upgrade41700to41710).getStorageDao();
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
SearchBuilder<StoragePoolVO> searchBuilder = Mockito.mock(SearchBuilder.class);
Mockito.when(storageDao.createSearchBuilder()).thenReturn(searchBuilder);
Mockito.when(searchBuilder.entity()).thenReturn(pool);
Mockito.when(searchBuilder.create()).thenReturn(Mockito.mock(SearchCriteria.class));
GenericSearchBuilder<StoragePoolVO, Long> gSb = Mockito.mock(GenericSearchBuilder.class);
Mockito.doReturn(gSb).when(storageDao).createSearchBuilder(Mockito.any());
Mockito.when(gSb.create()).thenReturn(Mockito.mock(SearchCriteria.class));
Mockito.when(gSb.entity()).thenReturn(pool);
Mockito.when(storageDao.createForUpdate()).thenReturn(pool);
Mockito.doNothing().when(upgrade41700to41710).updateStorageTypeForStorPoolVolumes(Mockito.any());
Mockito.when(storageDao.update(Mockito.any(StoragePoolVO.class), Mockito.any())).thenReturn(2);
Mockito.when(storageDao.customSearch(Mockito.any(), Mockito.any())).thenReturn(List.of(1L, 2L));
upgrade41700to41710.updateStorPoolStorageType();
Mockito.verify(storageDao, Mockito.times(1)).update(Mockito.any(StoragePoolVO.class), Mockito.any());
Mockito.verify(upgrade41700to41710, Mockito.times(1)).updateStorageTypeForStorPoolVolumes(Mockito.any());
}
@Test
public void testUpdateStorageTypeForStorPoolVolumes_EmptyPoolIds() {
VolumeDao volumeDao = Mockito.mock(VolumeDao.class);
List<Long> storagePoolIds = Collections.emptyList();
upgrade41700to41710.updateStorageTypeForStorPoolVolumes(storagePoolIds);
Mockito.verify(volumeDao, Mockito.never()).update(Mockito.any(VolumeVO.class), Mockito.any());
}
@Test
public void testUpdateStorageTypeForStorPoolVolumes_WithPoolIds() {
VolumeDao volumeDao = Mockito.mock(VolumeDao.class);
List<Long> storagePoolIds = List.of(1L, 2L, 3L);
VolumeVO volume = Mockito.mock(VolumeVO.class);
SearchBuilder<VolumeVO> searchBuilder = Mockito.mock(SearchBuilder.class);
SearchCriteria<VolumeVO> searchCriteria = Mockito.mock(SearchCriteria.class);
Mockito.when(volumeDao.createForUpdate()).thenReturn(volume);
Mockito.when(volumeDao.createSearchBuilder()).thenReturn(searchBuilder);
Mockito.when(searchBuilder.entity()).thenReturn(volume);
Mockito.when(searchBuilder.create()).thenReturn(searchCriteria);
Mockito.when(volumeDao.update(Mockito.any(VolumeVO.class), Mockito.any())).thenReturn(3);
Mockito.doReturn(volumeDao).when(upgrade41700to41710).getVolumeDao();
upgrade41700to41710.updateStorageTypeForStorPoolVolumes(storagePoolIds);
Mockito.verify(volumeDao).createForUpdate();
Mockito.verify(volume).setPoolType(Storage.StoragePoolType.StorPool);
Mockito.verify(volumeDao).update(Mockito.eq(volume), Mockito.eq(searchCriteria));
Mockito.verify(searchCriteria).setParameters("poolId", storagePoolIds.toArray());
}
}

View File

@ -118,6 +118,7 @@ Requires: cryptsetup
Requires: rng-tools
Requires: (libgcrypt > 1.8.3 or libgcrypt20)
Requires: (selinux-tools if qemu-tools)
Requires: sysstat
Provides: cloud-agent
Group: System Environment/Libraries
%description agent

View File

@ -40,7 +40,8 @@ public final class LibvirtGetStorageStatsCommandWrapper extends CommandWrapper<G
if (sp == null) {
return new GetStorageStatsAnswer(command, "no storage pool to get statistics from");
}
return new GetStorageStatsAnswer(command, sp.getCapacity(), sp.getUsed());
return new GetStorageStatsAnswer(command, sp.getCapacity(), sp.getUsed(), sp.getCapacityIops(),
sp.getUsedIops());
} catch (final CloudRuntimeException e) {
return new GetStorageStatsAnswer(command, e.toString());
}

View File

@ -62,6 +62,14 @@ public interface KVMStoragePool {
public long getUsed();
default Long getCapacityIops() {
return null;
}
default Long getUsedIops() {
return null;
}
public long getAvailable();
public boolean refresh();

View File

@ -16,14 +16,21 @@
// under the License.
package com.cloud.hypervisor.kvm.storage;
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.utils.cryptsetup.KeyFile;
@ -33,9 +40,10 @@ import org.apache.cloudstack.utils.qemu.QemuImgException;
import org.apache.cloudstack.utils.qemu.QemuImgFile;
import org.apache.cloudstack.utils.qemu.QemuObject;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.libvirt.Connect;
import org.libvirt.LibvirtException;
import org.libvirt.Secret;
@ -69,14 +77,6 @@ import com.cloud.storage.StorageLayer;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class LibvirtStorageAdaptor implements StorageAdaptor {
protected Logger logger = LogManager.getLogger(getClass());
private StorageLayer _storageLayer;
@ -521,6 +521,54 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
return this.getStoragePool(uuid, false);
}
protected void updateLocalPoolIops(LibvirtStoragePool pool) {
if (!StoragePoolType.Filesystem.equals(pool.getType()) || StringUtils.isBlank(pool.getLocalPath())) {
return;
}
logger.trace("Updating used IOPS for pool: {}", pool.getName());
// Run script to get data
List<String[]> commands = new ArrayList<>();
commands.add(new String[]{
Script.getExecutableAbsolutePath("bash"),
"-c",
String.format(
"%s %s | %s 'NR==2 {print $1}'",
Script.getExecutableAbsolutePath("df"),
pool.getLocalPath(),
Script.getExecutableAbsolutePath("awk")
)
});
String result = Script.executePipedCommands(commands, 1000).second();
if (StringUtils.isBlank(result)) {
return;
}
result = result.trim();
commands.add(new String[]{
Script.getExecutableAbsolutePath("bash"),
"-c",
String.format(
"%s -z %s 1 2 | %s 'NR==7 {print $2}'",
Script.getExecutableAbsolutePath("iostat"),
result,
Script.getExecutableAbsolutePath("awk")
)
});
result = Script.executePipedCommands(commands, 10000).second();
logger.trace("Pool used IOPS result: {}", result);
if (StringUtils.isBlank(result)) {
return;
}
try {
double doubleValue = Double.parseDouble(result);
pool.setUsedIops((long) doubleValue);
logger.debug("Updated used IOPS: {} for pool: {}", pool.getUsedIops(), pool.getName());
} catch (NumberFormatException e) {
logger.warn(String.format("Unable to parse retrieved used IOPS: %s for pool: %s", result,
pool.getName()));
}
}
@Override
public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) {
logger.info("Trying to fetch storage pool " + uuid + " from libvirt");
@ -591,6 +639,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
}
pool.setCapacity(storage.getInfo().capacity);
pool.setUsed(storage.getInfo().allocation);
updateLocalPoolIops(pool);
pool.setAvailable(storage.getInfo().available);
logger.debug("Successfully refreshed pool " + uuid +

View File

@ -43,6 +43,8 @@ public class LibvirtStoragePool implements KVMStoragePool {
protected String uuid;
protected long capacity;
protected long used;
protected Long capacityIops;
protected Long usedIops;
protected long available;
protected String name;
protected String localPath;
@ -81,20 +83,38 @@ public class LibvirtStoragePool implements KVMStoragePool {
this.used = used;
}
public void setAvailable(long available) {
this.available = available;
}
@Override
public long getUsed() {
return this.used;
}
@Override
public Long getCapacityIops() {
return capacityIops;
}
public void setCapacityIops(Long capacityIops) {
this.capacityIops = capacityIops;
}
@Override
public Long getUsedIops() {
return usedIops;
}
public void setUsedIops(Long usedIops) {
this.usedIops = usedIops;
}
@Override
public long getAvailable() {
return this.available;
}
public void setAvailable(long available) {
this.available = available;
}
public StoragePoolType getStoragePoolType() {
return this.type;
}

View File

@ -17,18 +17,22 @@
package com.cloud.hypervisor.kvm.storage;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import com.cloud.utils.exception.CloudRuntimeException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.libvirt.Connect;
import org.libvirt.StoragePool;
import org.libvirt.StoragePoolInfo;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@ -38,6 +42,9 @@ import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.hypervisor.kvm.resource.LibvirtConnection;
import com.cloud.hypervisor.kvm.resource.LibvirtStoragePoolDef;
import com.cloud.storage.Storage;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
@RunWith(MockitoJUnitRunner.class)
public class LibvirtStorageAdaptorTest {
@ -46,6 +53,11 @@ public class LibvirtStorageAdaptorTest {
private AutoCloseable closeable;
@Mock
LibvirtStoragePool mockPool;
MockedStatic<Script> mockScript;
@Spy
static LibvirtStorageAdaptor libvirtStorageAdaptor = new LibvirtStorageAdaptor(null);
@ -53,11 +65,14 @@ public class LibvirtStorageAdaptorTest {
public void initMocks() {
closeable = MockitoAnnotations.openMocks(this);
libvirtConnectionMockedStatic = Mockito.mockStatic(LibvirtConnection.class);
Mockito.reset(mockPool);
mockScript = Mockito.mockStatic(Script.class);
}
@After
public void tearDown() throws Exception {
libvirtConnectionMockedStatic.close();
mockScript.close();
closeable.close();
}
@ -78,14 +93,87 @@ public class LibvirtStorageAdaptorTest {
Connect conn = Mockito.mock(Connect.class);
StoragePool sp = Mockito.mock(StoragePool.class);
StoragePoolInfo spinfo = Mockito.mock(StoragePoolInfo.class);
Mockito.when(LibvirtConnection.getConnection()).thenReturn(conn);
Mockito.when(conn.storagePoolLookupByUUIDString(uuid)).thenReturn(sp);
Mockito.when(sp.isActive()).thenReturn(1);
Mockito.when(sp.getXMLDesc(0)).thenReturn(poolXml);
Mockito.when(Script.runSimpleBashScriptForExitValue(anyString())).thenReturn(-1);
Map<String, String> details = new HashMap<>();
details.put("nfsmountopts", "vers=4.1, nconnect=4");
KVMStoragePool pool = libvirtStorageAdaptor.createStoragePool(uuid, null, 0, dir, null, Storage.StoragePoolType.NetworkFilesystem, details, true);
}
@Test
public void testUpdateLocalPoolIops_IgnoredForNonFilesystemType() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.SharedMountPoint);
libvirtStorageAdaptor.updateLocalPoolIops(mockPool);
libvirtStorageAdaptor.updateLocalPoolIops(mockPool);
}
@Test
public void testUpdateLocalPoolIops_IgnoredForBlankLocalPath() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.Filesystem);
Mockito.when(mockPool.getLocalPath()).thenReturn("");
Mockito.verify(mockPool, never()).getLocalPath();
libvirtStorageAdaptor.updateLocalPoolIops(mockPool);
Mockito.verify(mockPool, never()).setUsedIops(anyLong());
}
@Test
public void testUpdateLocalPoolIops_NoDevice() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.Filesystem);
Mockito.when(mockPool.getLocalPath()).thenReturn("/mock/path");
Mockito.when(mockPool.getName()).thenReturn("mockPool");
Mockito.when(Script.executePipedCommands(anyList(), Mockito.eq(1000L))).thenReturn(new Pair<>(0, "\n"));
libvirtStorageAdaptor.updateLocalPoolIops(mockPool);
Mockito.verify(mockPool, never()).setUsedIops(anyLong());
}
@Test
public void testUpdateLocalPoolIops_SuccessfulUpdate() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.Filesystem);
Mockito.when(mockPool.getLocalPath()).thenReturn("/mock/path");
Mockito.when(mockPool.getName()).thenReturn("mockPool");
Mockito.when(Script.executePipedCommands(anyList(), Mockito.eq(1000L))).thenReturn(new Pair<>(0, "sda\n"));
Mockito.when(Script.executePipedCommands(anyList(), Mockito.eq(10000L))).thenReturn(new Pair<>(0, "42\n"));
libvirtStorageAdaptor.updateLocalPoolIops(mockPool);
Mockito.verify(mockPool).setUsedIops(42L);
}
@Test
public void testUpdateLocalPoolIops_HandlesNumberFormatException() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.Filesystem);
Mockito.when(mockPool.getLocalPath()).thenReturn("/mock/path");
Mockito.when(mockPool.getName()).thenReturn("mockPool");
Mockito.when(Script.executePipedCommands(anyList(), Mockito.eq(1000L))).thenReturn(new Pair<>(0, "sda\n"));
Mockito.when(Script.executePipedCommands(anyList(), Mockito.eq(10000L)))
.thenReturn(new Pair<>(0, "invalid_number"));
libvirtStorageAdaptor.updateLocalPoolIops(mockPool);
Mockito.verify(mockPool, never()).setUsedIops(anyLong());
}
@Test
public void testUpdateLocalPoolIops_NullResultFromScript() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.Filesystem);
Mockito.when(mockPool.getLocalPath()).thenReturn("/mock/path");
Mockito.when(mockPool.getName()).thenReturn("mockPool");
Mockito.when(Script.executePipedCommands(anyList(), Mockito.eq(1000L))).thenReturn(new Pair<>(0, "sda\n"));
Mockito.when(Script.executePipedCommands(anyList(), Mockito.eq(10000L)))
.thenReturn(new Pair<>(0, null));
libvirtStorageAdaptor.updateLocalPoolIops(mockPool);
Mockito.verify(mockPool, never()).setUsedIops(anyLong());
}
}

View File

@ -32,13 +32,14 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import org.springframework.stereotype.Component;
import org.apache.cloudstack.storage.command.DeleteCommand;
import org.apache.cloudstack.storage.command.DownloadCommand;
import org.apache.cloudstack.storage.command.DownloadProgressCommand;
import org.apache.cloudstack.storage.command.UploadStatusAnswer;
import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus;
import org.apache.cloudstack.storage.command.UploadStatusCommand;
import org.apache.cloudstack.utils.bytescale.ByteScaleUtils;
import org.springframework.stereotype.Component;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.AttachIsoCommand;
@ -639,8 +640,11 @@ public class MockStorageManagerImpl extends ManagerBase implements MockStorageMa
if (totalUsed == null) {
totalUsed = 0L;
}
// Mock IOPS stats
long capacityIops = Math.min(Math.max(ByteScaleUtils.bytesToGibibytes(pool.getCapacity()), 1), 1000) * 1000L;
long usedIops = capacityIops / 2;
txn.commit();
return new GetStorageStatsAnswer(cmd, pool.getCapacity(), totalUsed);
return new GetStorageStatsAnswer(cmd, pool.getCapacity(), totalUsed, capacityIops, usedIops);
}
} catch (Exception ex) {
txn.rollback();

View File

@ -31,4 +31,6 @@ public interface MockVolumeDao extends GenericDao<MockVolumeVO, Long> {
public MockVolumeVO findByName(String volumeName);
Long findTotalStorageId(long id);
int countForStorageId(long id);
}

View File

@ -36,6 +36,7 @@ public class MockVolumeDaoImpl extends GenericDaoBase<MockVolumeVO, Long> implem
protected final SearchBuilder<MockVolumeVO> namePoolSearch;
protected final SearchBuilder<MockVolumeVO> nameSearch;
protected final GenericSearchBuilder<MockVolumeVO, Long> totalSearch;
protected final SearchBuilder<MockVolumeVO> countSearch;
@Override
public List<MockVolumeVO> findByStorageIdAndType(long id, MockVolumeType type) {
@ -53,6 +54,13 @@ public class MockVolumeDaoImpl extends GenericDaoBase<MockVolumeVO, Long> implem
return customSearch(sc, null).get(0);
}
@Override
public int countForStorageId(long id) {
SearchCriteria<MockVolumeVO> sc = countSearch.create();
sc.setParameters("poolId", id);
return getCount(sc);
}
@Override
public MockVolumeVO findByStoragePathAndType(String path) {
SearchCriteria<MockVolumeVO> sc = pathTypeSearch.create();
@ -99,5 +107,9 @@ public class MockVolumeDaoImpl extends GenericDaoBase<MockVolumeVO, Long> implem
totalSearch.and("poolId", totalSearch.entity().getPoolId(), SearchCriteria.Op.EQ);
totalSearch.done();
countSearch = createSearchBuilder();
countSearch.and("poolId", countSearch.entity().getPoolId(), SearchCriteria.Op.EQ);
countSearch.done();
}
}

View File

@ -143,7 +143,9 @@ public class StoragePoolJoinDaoImpl extends GenericDaoBase<StoragePoolJoinVO, Lo
}
poolResponse.setDiskSizeTotal(pool.getCapacityBytes());
poolResponse.setDiskSizeAllocated(allocatedSize);
poolResponse.setDiskSizeUsed(pool.getUsedBytes());
poolResponse.setCapacityIops(pool.getCapacityIops());
poolResponse.setUsedIops(pool.getUsedIops());
if (storagePool.isManaged()) {
DataStore store = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary);
@ -159,13 +161,6 @@ public class StoragePoolJoinDaoImpl extends GenericDaoBase<StoragePoolJoinVO, Lo
}
}
// TODO: StatsCollector does not persist data
StorageStats stats = ApiDBUtils.getStoragePoolStatistics(pool.getId());
if (stats != null) {
Long used = stats.getByteUsed();
poolResponse.setDiskSizeUsed(used);
}
poolResponse.setClusterId(pool.getClusterUuid());
poolResponse.setClusterName(pool.getClusterName());
poolResponse.setProvider(pool.getStorageProviderName());

View File

@ -79,6 +79,9 @@ public class StoragePoolJoinVO extends BaseViewVO implements InternalIdentity, I
@Column(name = "capacity_bytes")
private long capacityBytes;
@Column(name = "used_bytes")
private long usedBytes;
@Column(name = "cluster_id")
private long clusterId;
@ -138,6 +141,9 @@ public class StoragePoolJoinVO extends BaseViewVO implements InternalIdentity, I
@Column(name = "capacity_iops")
private Long capacityIops;
@Column(name = "used_iops")
private Long usedIops;
@Column(name = "hypervisor")
@Convert(converter = HypervisorTypeConverter.class)
private HypervisorType hypervisor;
@ -201,10 +207,18 @@ public class StoragePoolJoinVO extends BaseViewVO implements InternalIdentity, I
return capacityBytes;
}
public long getUsedBytes() {
return usedBytes;
}
public Long getCapacityIops() {
return capacityIops;
}
public Long getUsedIops() {
return usedIops;
}
public long getClusterId() {
return clusterId;
}

View File

@ -1723,11 +1723,14 @@ public class StatsCollector extends ManagerBase implements ComponentMethodInterc
try {
Answer answer = _storageManager.sendToPool(pool, command);
if (answer != null && answer.getResult()) {
storagePoolStats.put(pool.getId(), (StorageStats)answer);
StorageStats stats = (StorageStats)answer;
storagePoolStats.put(pool.getId(), stats);
boolean poolNeedsUpdating = false;
long capacityBytes = ((StorageStats)answer).getCapacityBytes();
long usedBytes = ((StorageStats)answer).getByteUsed();
long capacityBytes = stats.getCapacityBytes();
long usedBytes = stats.getByteUsed();
Long capacityIops = stats.getCapacityIops();
Long usedIops = stats.getUsedIops();
// Seems like we have dynamically updated the pool size since the prev. size and the current do not match
if ((_storagePoolStats.get(poolId) != null && _storagePoolStats.get(poolId).getCapacityBytes() != capacityBytes)
|| pool.getCapacityBytes() != capacityBytes) {
@ -1744,6 +1747,7 @@ public class StatsCollector extends ManagerBase implements ComponentMethodInterc
pool.setUsedBytes(usedBytes);
poolNeedsUpdating = true;
}
poolNeedsUpdating = isPoolNeedsIopsStatsUpdate(pool, capacityIops, usedIops) || poolNeedsUpdating;
if (poolNeedsUpdating) {
pool.setUpdateTime(new Date());
_storagePoolDao.update(pool.getId(), pool);
@ -1775,6 +1779,24 @@ public class StatsCollector extends ManagerBase implements ComponentMethodInterc
}
}
protected boolean isPoolNeedsIopsStatsUpdate(StoragePoolVO pool, Long capacityIops, Long usedIops) {
boolean poolNeedsUpdating = false;
long poolId = pool.getId();
if (capacityIops != null && ((_storagePoolStats.get(poolId) != null &&
!capacityIops.equals(_storagePoolStats.get(poolId).getCapacityIops())) ||
!capacityIops.equals(pool.getCapacityIops()))) {
pool.setCapacityIops(capacityIops);
poolNeedsUpdating = true;
}
if (usedIops != null && ((_storagePoolStats.get(poolId) != null &&
!usedIops.equals(_storagePoolStats.get(poolId).getUsedIops())) ||
!usedIops.equals(pool.getUsedIops()))) {
pool.setUsedIops(usedIops);
poolNeedsUpdating = true;
}
return poolNeedsUpdating;
}
class AutoScaleMonitor extends ManagedContextRunnable {
@Override
protected void runInContext() {

View File

@ -553,20 +553,29 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
return answers[0];
}
private GetStorageStatsAnswer getStoragePoolStats(StoragePool pool, GetStorageStatsCommand cmd) {
GetStorageStatsAnswer answer = null;
protected Pair<Long, Long> getStoragePoolIopsStats(PrimaryDataStoreDriver primaryStoreDriver, StoragePool pool) {
Pair<Long, Long> result = primaryStoreDriver.getStorageIopsStats(pool);
if (result != null) {
return result;
}
Long usedIops = primaryStoreDriver.getUsedIops(pool);
if (usedIops <= 0) {
usedIops = null;
}
return new Pair<>(pool.getCapacityIops(), usedIops);
}
private GetStorageStatsAnswer getStoragePoolStats(StoragePool pool, GetStorageStatsCommand cmd) {
DataStoreProvider storeProvider = _dataStoreProviderMgr.getDataStoreProvider(pool.getStorageProviderName());
DataStoreDriver storeDriver = storeProvider.getDataStoreDriver();
PrimaryDataStoreDriver primaryStoreDriver = (PrimaryDataStoreDriver) storeDriver;
Pair<Long, Long> storageStats = primaryStoreDriver.getStorageStats(pool);
if (storageStats == null) {
answer = new GetStorageStatsAnswer((GetStorageStatsCommand) cmd, "Failed to get storage stats for pool: " + pool.getId());
} else {
answer = new GetStorageStatsAnswer((GetStorageStatsCommand) cmd, storageStats.first(), storageStats.second());
return new GetStorageStatsAnswer(cmd, "Failed to get storage stats for pool: " + pool.getId());
}
return answer;
Pair<Long, Long> iopsStats = getStoragePoolIopsStats(primaryStoreDriver, pool);
return new GetStorageStatsAnswer(cmd, storageStats.first(), storageStats.second(),
iopsStats.first(), iopsStats.second());
}
@Override

View File

@ -18,21 +18,23 @@
//
package com.cloud.server;
import com.cloud.agent.api.VmDiskStatsEntry;
import com.cloud.agent.api.VmStatsEntry;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.server.StatsCollector.ExternalStatsProtocol;
import com.cloud.storage.VolumeStatsVO;
import com.cloud.storage.dao.VolumeStatsDao;
import com.cloud.user.VmDiskStatisticsVO;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VmStats;
import com.cloud.vm.VmStatsVO;
import com.cloud.vm.dao.VmStatsDaoImpl;
import com.google.gson.Gson;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import static org.mockito.Mockito.when;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.commons.collections.CollectionUtils;
import org.influxdb.InfluxDB;
import org.influxdb.InfluxDBFactory;
@ -52,20 +54,25 @@ import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import org.springframework.test.util.ReflectionTestUtils;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import static org.mockito.Mockito.when;
import com.cloud.agent.api.GetStorageStatsAnswer;
import com.cloud.agent.api.GetStorageStatsCommand;
import com.cloud.agent.api.VmDiskStatsEntry;
import com.cloud.agent.api.VmStatsEntry;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.server.StatsCollector.ExternalStatsProtocol;
import com.cloud.storage.StorageStats;
import com.cloud.storage.VolumeStatsVO;
import com.cloud.storage.dao.VolumeStatsDao;
import com.cloud.user.VmDiskStatisticsVO;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VmStats;
import com.cloud.vm.VmStatsVO;
import com.cloud.vm.dao.VmStatsDaoImpl;
import com.google.gson.Gson;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
@RunWith(DataProviderRunner.class)
public class StatsCollectorTest {
@ -104,6 +111,9 @@ public class StatsCollectorTest {
@Mock
VolumeStatsDao volumeStatsDao = Mockito.mock(VolumeStatsDao.class);
@Mock
private StoragePoolVO mockPool;
private static Gson gson = new Gson();
private MockedStatic<InfluxDBFactory> influxDBFactoryMocked;
@ -522,4 +532,84 @@ public class StatsCollectorTest {
}
return jsonMap;
}
private void setCollectorIopsStats(long poolId, Long capacityIops, Long usedIops) {
ConcurrentHashMap<Long, StorageStats> storagePoolStats = new ConcurrentHashMap<>();
storagePoolStats.put(poolId, new GetStorageStatsAnswer(Mockito.mock(GetStorageStatsCommand.class),
10L, 2L, capacityIops, usedIops));
ReflectionTestUtils.setField(statsCollector, "_storagePoolStats", storagePoolStats);
}
@Test
public void testPoolNeedsIopsStatsUpdating_NoChanges() {
long poolId = 1L;
long capacityIops = 100L;
long usedIops = 50L;
when(mockPool.getId()).thenReturn(poolId);
when(mockPool.getCapacityIops()).thenReturn(capacityIops);
when(mockPool.getUsedIops()).thenReturn(usedIops);
setCollectorIopsStats(poolId, capacityIops, usedIops);
boolean result = statsCollector.isPoolNeedsIopsStatsUpdate(mockPool, capacityIops, usedIops);
Assert.assertFalse(result);
Mockito.verify(mockPool, Mockito.never()).setCapacityIops(Mockito.anyLong());
Mockito.verify(mockPool, Mockito.never()).setUsedIops(Mockito.anyLong());
}
@Test
public void testPoolNeedsIopsStatsUpdating_CapacityIopsNeedsUpdating() {
long poolId = 1L;
when(mockPool.getId()).thenReturn(poolId);
when(mockPool.getCapacityIops()).thenReturn(100L);
when(mockPool.getUsedIops()).thenReturn(50L);
setCollectorIopsStats(poolId, 90L, 50L);
boolean result = statsCollector.isPoolNeedsIopsStatsUpdate(mockPool, 120L, 50L);
Assert.assertTrue(result);
Mockito.verify(mockPool).setCapacityIops(120L);
Mockito.verify(mockPool, Mockito.never()).setUsedIops(Mockito.anyLong());
}
@Test
public void testPoolNeedsIopsStatsUpdating_UsedIopsNeedsUpdating() {
long poolId = 1L;
when(mockPool.getId()).thenReturn(poolId);
when(mockPool.getCapacityIops()).thenReturn(100L);
when(mockPool.getUsedIops()).thenReturn(50L);
setCollectorIopsStats(poolId, 100L, 45L);
boolean result = statsCollector.isPoolNeedsIopsStatsUpdate(mockPool, 100L, 60L);
Assert.assertTrue(result);
Mockito.verify(mockPool).setUsedIops(60L);
Mockito.verify(mockPool, Mockito.never()).setCapacityIops(Mockito.anyLong());
}
@Test
public void testPoolNeedsIopsStatsUpdating_BothNeedUpdating() {
long poolId = 1L;
when(mockPool.getId()).thenReturn(poolId);
when(mockPool.getCapacityIops()).thenReturn(100L);
when(mockPool.getUsedIops()).thenReturn(50L);
setCollectorIopsStats(poolId, 90L, 45L);
boolean result = statsCollector.isPoolNeedsIopsStatsUpdate(mockPool, 120L, 60L);
Assert.assertTrue(result);
Mockito.verify(mockPool).setCapacityIops(120L);
Mockito.verify(mockPool).setUsedIops(60L);
}
@Test
public void testPoolNeedsIopsStatsUpdating_NullIops() {
long poolId = 1L;
when(mockPool.getId()).thenReturn(poolId);
boolean result = statsCollector.isPoolNeedsIopsStatsUpdate(mockPool, null, null);
Assert.assertFalse(result);
Mockito.verify(mockPool, Mockito.never()).setCapacityIops(Mockito.anyLong());
Mockito.verify(mockPool, Mockito.never()).setUsedIops(Mockito.anyLong());
}
}

View File

@ -22,26 +22,10 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.api.command.admin.storage.ChangeStoragePoolScopeCmd;
import com.cloud.agent.api.StoragePoolInfo;
import com.cloud.dc.ClusterVO;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.exception.ConnectionException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.host.Host;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.user.AccountManagerImpl;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.dao.VMInstanceDao;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.command.admin.storage.ChangeStoragePoolScopeCmd;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver;
import org.apache.cloudstack.framework.config.ConfigDepot;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
@ -65,14 +49,31 @@ import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.StoragePoolInfo;
import com.cloud.capacity.CapacityManager;
import com.cloud.dc.ClusterVO;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.VsphereStoragePolicyVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.VsphereStoragePolicyDao;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.ConnectionException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.StorageUnavailableException;
import com.cloud.host.Host;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.hypervisor.HypervisorGuruManager;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.user.AccountManagerImpl;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.DiskProfile;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.dao.VMInstanceDao;
@RunWith(MockitoJUnitRunner.class)
public class StorageManagerImplTest {
@ -835,4 +836,63 @@ public class StorageManagerImplTest {
boolean result = storageManagerImpl.checkPoolforSpace(pool, allocatedSizeWithTemplate, totalAskingSize, true);
Assert.assertTrue(result);
}
@Test
public void testGetStoragePoolIopsStats_ReturnsDriverResultWhenNotNull() {
StoragePool pool = Mockito.mock(StoragePool.class);
PrimaryDataStoreDriver primaryStoreDriver = Mockito.mock(PrimaryDataStoreDriver.class);
Pair<Long, Long> expectedResult = new Pair<>(1000L, 500L);
Mockito.when(primaryStoreDriver.getStorageIopsStats(pool)).thenReturn(expectedResult);
Pair<Long, Long> result = storageManagerImpl.getStoragePoolIopsStats(primaryStoreDriver, pool);
Assert.assertSame("Should return the result from primaryStoreDriver.getStorageIopsStats", expectedResult, result);
Mockito.verify(primaryStoreDriver, Mockito.never()).getUsedIops(Mockito.any());
Mockito.verify(pool, Mockito.never()).getCapacityIops();
}
@Test
public void testGetStoragePoolIopsStats_UsedIopsPositive() {
StoragePool pool = Mockito.mock(StoragePool.class);
PrimaryDataStoreDriver primaryStoreDriver = Mockito.mock(PrimaryDataStoreDriver.class);
Mockito.when(primaryStoreDriver.getStorageIopsStats(pool)).thenReturn(null);
Mockito.when(primaryStoreDriver.getUsedIops(pool)).thenReturn(500L);
Mockito.when(pool.getCapacityIops()).thenReturn(1000L);
Pair<Long, Long> result = storageManagerImpl.getStoragePoolIopsStats(primaryStoreDriver, pool);
Assert.assertNotNull(result);
Assert.assertEquals("Capacity IOPS should match pool's capacity IOPS", 1000L, result.first().longValue());
Assert.assertEquals("Used IOPS should match the positive value returned", 500L, result.second().longValue());
}
@Test
public void testGetStoragePoolIopsStats_UsedIopsZero() {
StoragePool pool = Mockito.mock(StoragePool.class);
PrimaryDataStoreDriver primaryStoreDriver = Mockito.mock(PrimaryDataStoreDriver.class);
Mockito.when(primaryStoreDriver.getStorageIopsStats(pool)).thenReturn(null);
Mockito.when(primaryStoreDriver.getUsedIops(pool)).thenReturn(0L);
Mockito.when(pool.getCapacityIops()).thenReturn(1000L);
Pair<Long, Long> result = storageManagerImpl.getStoragePoolIopsStats(primaryStoreDriver, pool);
Assert.assertNotNull(result);
Assert.assertEquals("Capacity IOPS should match pool's capacity IOPS", 1000L, result.first().longValue());
Assert.assertNull("Used IOPS should be null when usedIops <= 0", result.second());
}
@Test
public void testGetStoragePoolIopsStats_UsedIopsNegative() {
StoragePool pool = Mockito.mock(StoragePool.class);
PrimaryDataStoreDriver primaryStoreDriver = Mockito.mock(PrimaryDataStoreDriver.class);
Mockito.when(primaryStoreDriver.getStorageIopsStats(pool)).thenReturn(null);
Mockito.when(primaryStoreDriver.getUsedIops(pool)).thenReturn(-100L);
Mockito.when(pool.getCapacityIops()).thenReturn(1000L);
Pair<Long, Long> result = storageManagerImpl.getStoragePoolIopsStats(primaryStoreDriver, pool);
Assert.assertNotNull(result);
Assert.assertEquals("Capacity IOPS should match pool's capacity IOPS", 1000L, result.first().longValue());
Assert.assertNull("Used IOPS should be null when usedIops <= 0", result.second());
}
}

View File

@ -2131,6 +2131,7 @@
"label.srx.firewall": "Juniper SRX firewall",
"label.ssh.key.pairs": "SSH key pairs",
"label.uefi.supported": "UEFI supported",
"label.usediops": "IOPS used",
"label.userdataid": "Userdata ID",
"label.userdataname": "Userdata name",
"label.userdatadetails": "Userdata details",

View File

@ -35,7 +35,7 @@ export default {
fields.push('zonename')
return fields
},
details: ['name', 'id', 'ipaddress', 'type', 'nfsmountopts', 'scope', 'tags', 'path', 'provider', 'hypervisor', 'overprovisionfactor', 'disksizetotal', 'disksizeallocated', 'disksizeused', 'clustername', 'podname', 'zonename', 'created'],
details: ['name', 'id', 'ipaddress', 'type', 'nfsmountopts', 'scope', 'tags', 'path', 'provider', 'hypervisor', 'overprovisionfactor', 'disksizetotal', 'disksizeallocated', 'disksizeused', 'capacityiops', 'usediops', 'clustername', 'podname', 'zonename', 'created'],
related: [{
name: 'volume',
title: 'label.volumes',