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 * @return bytes capacity of the storage server
*/ */
public long getCapacityBytes(); 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 URL = "url";
public static final String USAGE_INTERFACE = "usageinterface"; public static final String USAGE_INTERFACE = "usageinterface";
public static final String USED_SUBNETS = "usedsubnets"; 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 = "userdata";
public static final String USER_DATA_NAME = "userdataname"; 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") @Param(description = "total min IOPS currently in use by volumes")
private Long allocatedIops; 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) @SerializedName(ApiConstants.STORAGE_CUSTOM_STATS)
@Param(description = "the storage pool custom stats", since = "4.18.1") @Param(description = "the storage pool custom stats", since = "4.18.1")
private Map<String, String> customStats; private Map<String, String> customStats;
@ -312,6 +316,14 @@ public class StoragePoolResponse extends BaseResponseWithAnnotations {
this.allocatedIops = allocatedIops; this.allocatedIops = allocatedIops;
} }
public Long getUsedIops() {
return usedIops;
}
public void setUsedIops(Long usedIops) {
this.usedIops = usedIops;
}
public Map<String, String> getCustomStats() { public Map<String, String> getCustomStats() {
return customStats; return customStats;
} }

View File

@ -27,24 +27,46 @@ public class GetStorageStatsAnswer extends Answer implements StorageStats {
protected GetStorageStatsAnswer() { protected GetStorageStatsAnswer() {
} }
protected long used; protected long usedBytes;
protected long capacity; protected long capacityBytes;
protected Long capacityIops;
protected Long usedIops;
@Override @Override
public long getByteUsed() { public long getByteUsed() {
return used; return usedBytes;
} }
@Override @Override
public long getCapacityBytes() { 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); super(cmd, true, null);
this.capacity = capacity; this.capacityBytes = capacityBytes;
this.used = used; 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) { 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 Package: cloudstack-agent
Architecture: all 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 Recommends: init-system-helpers
Conflicts: cloud-agent, cloud-agent-libs, cloud-agent-deps, cloud-agent-scripts Conflicts: cloud-agent, cloud-agent-libs, cloud-agent-deps, cloud-agent-scripts
Description: CloudStack agent Description: CloudStack agent

View File

@ -111,6 +111,14 @@ public interface PrimaryDataStoreDriver extends DataStoreDriver {
*/ */
Pair<Long, Long> getStorageStats(StoragePool storagePool); 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 * intended for managed storage
* returns true if the storage can provide the volume stats (physical and virtual size) * 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.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDaoImpl; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDaoImpl;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.commons.collections.CollectionUtils;
import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.Storage.StoragePoolType;
import com.cloud.storage.VolumeVO; import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDao; import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.dao.VolumeDaoImpl; import com.cloud.storage.dao.VolumeDaoImpl;
import com.cloud.upgrade.SystemVmTemplateRegistration; 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; import com.cloud.utils.exception.CloudRuntimeException;
public class Upgrade41700to41710 extends DbUpgradeAbstractImpl implements DbUpgradeSystemVmTemplate { 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(); 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(); 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) @Column(name = "capacity_iops", updatable = true, nullable = true)
private Long capacityIops; private Long capacityIops;
@Column(name = "used_iops", updatable = true, nullable = true)
private Long usedIops;
@Column(name = "hypervisor") @Column(name = "hypervisor")
@Convert(converter = HypervisorTypeConverter.class) @Convert(converter = HypervisorTypeConverter.class)
private HypervisorType hypervisor; private HypervisorType hypervisor;
@ -256,6 +259,14 @@ public class StoragePoolVO implements StoragePool {
return capacityIops; return capacityIops;
} }
public Long getUsedIops() {
return usedIops;
}
public void setUsedIops(Long usedIops) {
this.usedIops = usedIops;
}
@Override @Override
public Long getClusterId() { public Long getClusterId() {
return clusterId; 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 -- Add last_id to the volumes table
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'last_id', 'bigint(20) unsigned DEFAULT NULL'); 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`.`created` AS `created`,
`storage_pool`.`removed` AS `removed`, `storage_pool`.`removed` AS `removed`,
`storage_pool`.`capacity_bytes` AS `capacity_bytes`, `storage_pool`.`capacity_bytes` AS `capacity_bytes`,
`storage_pool`.`used_bytes` AS `used_bytes`,
`storage_pool`.`capacity_iops` AS `capacity_iops`, `storage_pool`.`capacity_iops` AS `capacity_iops`,
`storage_pool`.`used_iops` AS `used_iops`,
`storage_pool`.`scope` AS `scope`, `storage_pool`.`scope` AS `scope`,
`storage_pool`.`hypervisor` AS `hypervisor`, `storage_pool`.`hypervisor` AS `hypervisor`,
`storage_pool`.`storage_provider_name` AS `storage_provider_name`, `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: rng-tools
Requires: (libgcrypt > 1.8.3 or libgcrypt20) Requires: (libgcrypt > 1.8.3 or libgcrypt20)
Requires: (selinux-tools if qemu-tools) Requires: (selinux-tools if qemu-tools)
Requires: sysstat
Provides: cloud-agent Provides: cloud-agent
Group: System Environment/Libraries Group: System Environment/Libraries
%description agent %description agent

View File

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

View File

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

View File

@ -16,14 +16,21 @@
// under the License. // under the License.
package com.cloud.hypervisor.kvm.storage; package com.cloud.hypervisor.kvm.storage;
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.utils.cryptsetup.KeyFile; 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.QemuImgFile;
import org.apache.cloudstack.utils.qemu.QemuObject; import org.apache.cloudstack.utils.qemu.QemuObject;
import org.apache.commons.codec.binary.Base64; 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.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.Connect;
import org.libvirt.LibvirtException; import org.libvirt.LibvirtException;
import org.libvirt.Secret; import org.libvirt.Secret;
@ -69,14 +77,6 @@ import com.cloud.storage.StorageLayer;
import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script; 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 { public class LibvirtStorageAdaptor implements StorageAdaptor {
protected Logger logger = LogManager.getLogger(getClass()); protected Logger logger = LogManager.getLogger(getClass());
private StorageLayer _storageLayer; private StorageLayer _storageLayer;
@ -521,6 +521,54 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
return this.getStoragePool(uuid, false); 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 @Override
public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) {
logger.info("Trying to fetch storage pool " + uuid + " from libvirt"); 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.setCapacity(storage.getInfo().capacity);
pool.setUsed(storage.getInfo().allocation); pool.setUsed(storage.getInfo().allocation);
updateLocalPoolIops(pool);
pool.setAvailable(storage.getInfo().available); pool.setAvailable(storage.getInfo().available);
logger.debug("Successfully refreshed pool " + uuid + logger.debug("Successfully refreshed pool " + uuid +

View File

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

View File

@ -17,18 +17,22 @@
package com.cloud.hypervisor.kvm.storage; 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.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import com.cloud.utils.exception.CloudRuntimeException;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.libvirt.Connect; import org.libvirt.Connect;
import org.libvirt.StoragePool; import org.libvirt.StoragePool;
import org.libvirt.StoragePoolInfo; import org.mockito.Mock;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockitoAnnotations; 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.LibvirtConnection;
import com.cloud.hypervisor.kvm.resource.LibvirtStoragePoolDef; import com.cloud.hypervisor.kvm.resource.LibvirtStoragePoolDef;
import com.cloud.storage.Storage; 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) @RunWith(MockitoJUnitRunner.class)
public class LibvirtStorageAdaptorTest { public class LibvirtStorageAdaptorTest {
@ -46,6 +53,11 @@ public class LibvirtStorageAdaptorTest {
private AutoCloseable closeable; private AutoCloseable closeable;
@Mock
LibvirtStoragePool mockPool;
MockedStatic<Script> mockScript;
@Spy @Spy
static LibvirtStorageAdaptor libvirtStorageAdaptor = new LibvirtStorageAdaptor(null); static LibvirtStorageAdaptor libvirtStorageAdaptor = new LibvirtStorageAdaptor(null);
@ -53,11 +65,14 @@ public class LibvirtStorageAdaptorTest {
public void initMocks() { public void initMocks() {
closeable = MockitoAnnotations.openMocks(this); closeable = MockitoAnnotations.openMocks(this);
libvirtConnectionMockedStatic = Mockito.mockStatic(LibvirtConnection.class); libvirtConnectionMockedStatic = Mockito.mockStatic(LibvirtConnection.class);
Mockito.reset(mockPool);
mockScript = Mockito.mockStatic(Script.class);
} }
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
libvirtConnectionMockedStatic.close(); libvirtConnectionMockedStatic.close();
mockScript.close();
closeable.close(); closeable.close();
} }
@ -78,14 +93,87 @@ public class LibvirtStorageAdaptorTest {
Connect conn = Mockito.mock(Connect.class); Connect conn = Mockito.mock(Connect.class);
StoragePool sp = Mockito.mock(StoragePool.class); StoragePool sp = Mockito.mock(StoragePool.class);
StoragePoolInfo spinfo = Mockito.mock(StoragePoolInfo.class);
Mockito.when(LibvirtConnection.getConnection()).thenReturn(conn); Mockito.when(LibvirtConnection.getConnection()).thenReturn(conn);
Mockito.when(conn.storagePoolLookupByUUIDString(uuid)).thenReturn(sp); Mockito.when(conn.storagePoolLookupByUUIDString(uuid)).thenReturn(sp);
Mockito.when(sp.isActive()).thenReturn(1); Mockito.when(sp.isActive()).thenReturn(1);
Mockito.when(sp.getXMLDesc(0)).thenReturn(poolXml); Mockito.when(sp.getXMLDesc(0)).thenReturn(poolXml);
Mockito.when(Script.runSimpleBashScriptForExitValue(anyString())).thenReturn(-1);
Map<String, String> details = new HashMap<>(); Map<String, String> details = new HashMap<>();
details.put("nfsmountopts", "vers=4.1, nconnect=4"); details.put("nfsmountopts", "vers=4.1, nconnect=4");
KVMStoragePool pool = libvirtStorageAdaptor.createStoragePool(uuid, null, 0, dir, null, Storage.StoragePoolType.NetworkFilesystem, details, true); 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.inject.Inject;
import javax.naming.ConfigurationException; import javax.naming.ConfigurationException;
import org.springframework.stereotype.Component;
import org.apache.cloudstack.storage.command.DeleteCommand; import org.apache.cloudstack.storage.command.DeleteCommand;
import org.apache.cloudstack.storage.command.DownloadCommand; import org.apache.cloudstack.storage.command.DownloadCommand;
import org.apache.cloudstack.storage.command.DownloadProgressCommand; import org.apache.cloudstack.storage.command.DownloadProgressCommand;
import org.apache.cloudstack.storage.command.UploadStatusAnswer; import org.apache.cloudstack.storage.command.UploadStatusAnswer;
import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus; import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus;
import org.apache.cloudstack.storage.command.UploadStatusCommand; 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.Answer;
import com.cloud.agent.api.AttachIsoCommand; import com.cloud.agent.api.AttachIsoCommand;
@ -639,8 +640,11 @@ public class MockStorageManagerImpl extends ManagerBase implements MockStorageMa
if (totalUsed == null) { if (totalUsed == null) {
totalUsed = 0L; totalUsed = 0L;
} }
// Mock IOPS stats
long capacityIops = Math.min(Math.max(ByteScaleUtils.bytesToGibibytes(pool.getCapacity()), 1), 1000) * 1000L;
long usedIops = capacityIops / 2;
txn.commit(); txn.commit();
return new GetStorageStatsAnswer(cmd, pool.getCapacity(), totalUsed); return new GetStorageStatsAnswer(cmd, pool.getCapacity(), totalUsed, capacityIops, usedIops);
} }
} catch (Exception ex) { } catch (Exception ex) {
txn.rollback(); txn.rollback();

View File

@ -31,4 +31,6 @@ public interface MockVolumeDao extends GenericDao<MockVolumeVO, Long> {
public MockVolumeVO findByName(String volumeName); public MockVolumeVO findByName(String volumeName);
Long findTotalStorageId(long id); 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> namePoolSearch;
protected final SearchBuilder<MockVolumeVO> nameSearch; protected final SearchBuilder<MockVolumeVO> nameSearch;
protected final GenericSearchBuilder<MockVolumeVO, Long> totalSearch; protected final GenericSearchBuilder<MockVolumeVO, Long> totalSearch;
protected final SearchBuilder<MockVolumeVO> countSearch;
@Override @Override
public List<MockVolumeVO> findByStorageIdAndType(long id, MockVolumeType type) { 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); 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 @Override
public MockVolumeVO findByStoragePathAndType(String path) { public MockVolumeVO findByStoragePathAndType(String path) {
SearchCriteria<MockVolumeVO> sc = pathTypeSearch.create(); 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.and("poolId", totalSearch.entity().getPoolId(), SearchCriteria.Op.EQ);
totalSearch.done(); 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.setDiskSizeTotal(pool.getCapacityBytes());
poolResponse.setDiskSizeAllocated(allocatedSize); poolResponse.setDiskSizeAllocated(allocatedSize);
poolResponse.setDiskSizeUsed(pool.getUsedBytes());
poolResponse.setCapacityIops(pool.getCapacityIops()); poolResponse.setCapacityIops(pool.getCapacityIops());
poolResponse.setUsedIops(pool.getUsedIops());
if (storagePool.isManaged()) { if (storagePool.isManaged()) {
DataStore store = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary); 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.setClusterId(pool.getClusterUuid());
poolResponse.setClusterName(pool.getClusterName()); poolResponse.setClusterName(pool.getClusterName());
poolResponse.setProvider(pool.getStorageProviderName()); poolResponse.setProvider(pool.getStorageProviderName());

View File

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

View File

@ -1723,11 +1723,14 @@ public class StatsCollector extends ManagerBase implements ComponentMethodInterc
try { try {
Answer answer = _storageManager.sendToPool(pool, command); Answer answer = _storageManager.sendToPool(pool, command);
if (answer != null && answer.getResult()) { if (answer != null && answer.getResult()) {
storagePoolStats.put(pool.getId(), (StorageStats)answer); StorageStats stats = (StorageStats)answer;
storagePoolStats.put(pool.getId(), stats);
boolean poolNeedsUpdating = false; boolean poolNeedsUpdating = false;
long capacityBytes = ((StorageStats)answer).getCapacityBytes(); long capacityBytes = stats.getCapacityBytes();
long usedBytes = ((StorageStats)answer).getByteUsed(); 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 // 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) if ((_storagePoolStats.get(poolId) != null && _storagePoolStats.get(poolId).getCapacityBytes() != capacityBytes)
|| pool.getCapacityBytes() != capacityBytes) { || pool.getCapacityBytes() != capacityBytes) {
@ -1744,6 +1747,7 @@ public class StatsCollector extends ManagerBase implements ComponentMethodInterc
pool.setUsedBytes(usedBytes); pool.setUsedBytes(usedBytes);
poolNeedsUpdating = true; poolNeedsUpdating = true;
} }
poolNeedsUpdating = isPoolNeedsIopsStatsUpdate(pool, capacityIops, usedIops) || poolNeedsUpdating;
if (poolNeedsUpdating) { if (poolNeedsUpdating) {
pool.setUpdateTime(new Date()); pool.setUpdateTime(new Date());
_storagePoolDao.update(pool.getId(), pool); _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 { class AutoScaleMonitor extends ManagedContextRunnable {
@Override @Override
protected void runInContext() { protected void runInContext() {

View File

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

View File

@ -18,21 +18,23 @@
// //
package com.cloud.server; package com.cloud.server;
import com.cloud.agent.api.VmDiskStatsEntry; import static org.mockito.Mockito.when;
import com.cloud.agent.api.VmStatsEntry;
import com.cloud.hypervisor.Hypervisor; import java.net.URI;
import com.cloud.server.StatsCollector.ExternalStatsProtocol; import java.net.URISyntaxException;
import com.cloud.storage.VolumeStatsVO; import java.util.ArrayList;
import com.cloud.storage.dao.VolumeStatsDao; import java.util.Arrays;
import com.cloud.user.VmDiskStatisticsVO; import java.util.Date;
import com.cloud.utils.exception.CloudRuntimeException; import java.util.HashMap;
import com.cloud.vm.VmStats; import java.util.List;
import com.cloud.vm.VmStatsVO; import java.util.Map;
import com.cloud.vm.dao.VmStatsDaoImpl; import java.util.Properties;
import com.google.gson.Gson; import java.util.TreeMap;
import com.tngtech.java.junit.dataprovider.DataProvider; import java.util.concurrent.ConcurrentHashMap;
import com.tngtech.java.junit.dataprovider.DataProviderRunner; import java.util.concurrent.TimeUnit;
import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.influxdb.InfluxDB; import org.influxdb.InfluxDB;
import org.influxdb.InfluxDBFactory; import org.influxdb.InfluxDBFactory;
@ -52,20 +54,25 @@ import org.mockito.MockedStatic;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.springframework.test.util.ReflectionTestUtils;
import java.net.URI; import com.cloud.agent.api.GetStorageStatsAnswer;
import java.net.URISyntaxException; import com.cloud.agent.api.GetStorageStatsCommand;
import java.util.ArrayList; import com.cloud.agent.api.VmDiskStatsEntry;
import java.util.Arrays; import com.cloud.agent.api.VmStatsEntry;
import java.util.Date; import com.cloud.hypervisor.Hypervisor;
import java.util.HashMap; import com.cloud.server.StatsCollector.ExternalStatsProtocol;
import java.util.List; import com.cloud.storage.StorageStats;
import java.util.Map; import com.cloud.storage.VolumeStatsVO;
import java.util.Properties; import com.cloud.storage.dao.VolumeStatsDao;
import java.util.TreeMap; import com.cloud.user.VmDiskStatisticsVO;
import java.util.concurrent.TimeUnit; import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VmStats;
import static org.mockito.Mockito.when; 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) @RunWith(DataProviderRunner.class)
public class StatsCollectorTest { public class StatsCollectorTest {
@ -104,6 +111,9 @@ public class StatsCollectorTest {
@Mock @Mock
VolumeStatsDao volumeStatsDao = Mockito.mock(VolumeStatsDao.class); VolumeStatsDao volumeStatsDao = Mockito.mock(VolumeStatsDao.class);
@Mock
private StoragePoolVO mockPool;
private static Gson gson = new Gson(); private static Gson gson = new Gson();
private MockedStatic<InfluxDBFactory> influxDBFactoryMocked; private MockedStatic<InfluxDBFactory> influxDBFactoryMocked;
@ -515,11 +525,91 @@ public class StatsCollectorTest {
private Map<String, String> convertJsonToOrderedMap(String json) { private Map<String, String> convertJsonToOrderedMap(String json) {
Map<String, String> jsonMap = new TreeMap<String, String>(); Map<String, String> jsonMap = new TreeMap<String, String>();
String[] keyValuePairs = json.replace("{", "").replace("}","").split(","); String[] keyValuePairs = json.replace("{", "").replace("}", "").split(",");
for (String pair: keyValuePairs) { for (String pair : keyValuePairs) {
String[] keyValue = pair.split(":"); String[] keyValue = pair.split(":");
jsonMap.put(keyValue[0], keyValue[1]); jsonMap.put(keyValue[0], keyValue[1]);
} }
return jsonMap; 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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.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.ConfigDepot;
import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao; 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.AgentManager;
import com.cloud.agent.api.Command; import com.cloud.agent.api.Command;
import com.cloud.agent.api.StoragePoolInfo;
import com.cloud.capacity.CapacityManager; 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.VsphereStoragePolicyVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.VsphereStoragePolicyDao; import com.cloud.dc.dao.VsphereStoragePolicyDao;
import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.ConnectionException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.OperationTimedoutException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.StorageUnavailableException; import com.cloud.exception.StorageUnavailableException;
import com.cloud.host.Host;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.hypervisor.HypervisorGuruManager; 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.DiskProfile;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.dao.VMInstanceDao;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class StorageManagerImplTest { public class StorageManagerImplTest {
@ -835,4 +836,63 @@ public class StorageManagerImplTest {
boolean result = storageManagerImpl.checkPoolforSpace(pool, allocatedSizeWithTemplate, totalAskingSize, true); boolean result = storageManagerImpl.checkPoolforSpace(pool, allocatedSizeWithTemplate, totalAskingSize, true);
Assert.assertTrue(result); 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.srx.firewall": "Juniper SRX firewall",
"label.ssh.key.pairs": "SSH key pairs", "label.ssh.key.pairs": "SSH key pairs",
"label.uefi.supported": "UEFI supported", "label.uefi.supported": "UEFI supported",
"label.usediops": "IOPS used",
"label.userdataid": "Userdata ID", "label.userdataid": "Userdata ID",
"label.userdataname": "Userdata name", "label.userdataname": "Userdata name",
"label.userdatadetails": "Userdata details", "label.userdatadetails": "Userdata details",

View File

@ -35,7 +35,7 @@ export default {
fields.push('zonename') fields.push('zonename')
return fields 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: [{ related: [{
name: 'volume', name: 'volume',
title: 'label.volumes', title: 'label.volumes',