Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,16 @@ public interface StorageManager extends StorageService {
"Storage",
"60",
"Timeout (in secs) for the storage pool client connection timeout (for managed pools). Currently only supported for PowerFlex.",
true,
false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity. Why is STORAGE_POOL_CLIENT_TIMEOUT changed from dynamic to static?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity. Why is STORAGE_POOL_CLIENT_TIMEOUT changed from dynamic to static?

@GabrielBrascher This config is currently support for ScaleIO only. It's value is picked by the ScaleIO client when it is created, which is either on the primary storage addition or management server (re)start (for existing pools). In order to apply this config value for existing pools, it is required to restart the management server. Changing this config to 'static' prompts the operator, to restart the management server to apply the new value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, that makes sense. Thanks for the explanation, @sureshanaparti!

ConfigKey.Scope.StoragePool,
null);

ConfigKey<Integer> STORAGE_POOL_CLIENT_MAX_CONNECTIONS = new ConfigKey<>(Integer.class,
"storage.pool.client.max.connections",
"Storage",
"100",
"Maximum connections for the storage pool client (for managed pools). Currently only supported for PowerFlex.",
false,
ConfigKey.Scope.StoragePool,
null);

Expand Down
6 changes: 6 additions & 0 deletions plugins/storage/volume/scaleio/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
<artifactId>cloud-engine-storage-volume</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>${cs.wiremock.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ public interface ScaleIOGatewayClient {
String STORAGE_POOL_SYSTEM_ID = "powerflex.storagepool.system.id";

static ScaleIOGatewayClient getClient(final String url, final String username, final String password,
final boolean validateCertificate, final int timeout) throws NoSuchAlgorithmException, KeyManagementException, URISyntaxException {
return new ScaleIOGatewayClientImpl(url, username, password, validateCertificate, timeout);
final boolean validateCertificate, final int timeout, final int maxConnections) throws NoSuchAlgorithmException, KeyManagementException, URISyntaxException {
return new ScaleIOGatewayClientImpl(url, username, password, validateCertificate, timeout, maxConnections);
}

// Volume APIs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ public ScaleIOGatewayClient getClient(Long storagePoolId, StoragePoolDetailsDao
final String encryptedPassword = storagePoolDetailsDao.findDetail(storagePoolId, ScaleIOGatewayClient.GATEWAY_API_PASSWORD).getValue();
final String password = DBEncryptionUtil.decrypt(encryptedPassword);
final int clientTimeout = StorageManager.STORAGE_POOL_CLIENT_TIMEOUT.valueIn(storagePoolId);
final int clientMaxConnections = StorageManager.STORAGE_POOL_CLIENT_MAX_CONNECTIONS.valueIn(storagePoolId);

client = new ScaleIOGatewayClientImpl(url, username, password, false, clientTimeout);
client = new ScaleIOGatewayClientImpl(url, username, password, false, clientTimeout, clientMaxConnections);
gatewayClients.put(storagePoolId, client);
LOGGER.debug("Added gateway client for the storage pool: " + storagePoolId);
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ public ScaleIOPrimaryDataStoreLifeCycle() {
private org.apache.cloudstack.storage.datastore.api.StoragePool findStoragePool(String url, String username, String password, String storagePoolName) {
try {
final int clientTimeout = StorageManager.STORAGE_POOL_CLIENT_TIMEOUT.value();
ScaleIOGatewayClient client = ScaleIOGatewayClient.getClient(url, username, password, false, clientTimeout);
final int clientMaxConnections = StorageManager.STORAGE_POOL_CLIENT_MAX_CONNECTIONS.value();
ScaleIOGatewayClient client = ScaleIOGatewayClient.getClient(url, username, password, false, clientTimeout, clientMaxConnections);
List<org.apache.cloudstack.storage.datastore.api.StoragePool> storagePools = client.listStoragePools();
for (org.apache.cloudstack.storage.datastore.api.StoragePool pool : storagePools) {
if (pool.getName().equals(storagePoolName)) {
Expand All @@ -121,9 +122,9 @@ private org.apache.cloudstack.storage.datastore.api.StoragePool findStoragePool(
}
} catch (NoSuchAlgorithmException | KeyManagementException | URISyntaxException e) {
LOGGER.error("Failed to add storage pool", e);
throw new CloudRuntimeException("Failed to establish connection with PowerFlex Gateway to validate storage pool");
throw new CloudRuntimeException("Failed to establish connection with PowerFlex Gateway to find and validate storage pool: " + storagePoolName);
}
throw new CloudRuntimeException("Failed to find the provided storage pool name in discovered PowerFlex storage pools");
throw new CloudRuntimeException("Failed to find the provided storage pool name: " + storagePoolName + " in the discovered PowerFlex storage pools");
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,179 @@

package org.apache.cloudstack.storage.datastore.client;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.unauthorized;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;

import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.storage.datastore.api.Volume;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;

import com.cloud.storage.Storage;
import com.cloud.utils.exception.CloudRuntimeException;
import com.github.tomakehurst.wiremock.client.BasicCredentials;
import com.github.tomakehurst.wiremock.junit.WireMockRule;

@RunWith(MockitoJUnitRunner.class)
public class ScaleIOGatewayClientImplTest {
private final int port = 443;
private final int timeout = 30;
private final int maxConnections = 50;
private final String username = "admin";
private final String password = "P@ssword123";
private final String sessionKey = "YWRtaW46MTYyMzM0OTc4NDk0MTo2MWQ2NGQzZWJhMTVmYTVkNDIwNjZmOWMwZDg0ZGZmOQ";
private ScaleIOGatewayClient client = null;

ScaleIOGatewayClientImpl client;
@Rule
public WireMockRule wireMockRule = new WireMockRule(wireMockConfig()
.httpsPort(port)
.needClientAuth(false)
.basicAdminAuthenticator(username, password)
.bindAddress("localhost"));

@Before
public void setUp() throws Exception {
wireMockRule.stubFor(get("/api/login")
.willReturn(ok()
.withHeader("Content-Type", "application/json;charset=UTF-8")
.withBody(sessionKey)));

client = new ScaleIOGatewayClientImpl("https://localhost/api", username, password, false, timeout, maxConnections);

wireMockRule.stubFor(post("/api/types/Volume/instances")
.willReturn(aResponse()
.withHeader("Content-Type", "application/json;charset=UTF-8")
.withStatus(200)
.withBody("{\"id\":\"c948d0b10000000a\"}")));

wireMockRule.stubFor(get("/api/instances/Volume::c948d0b10000000a")
.willReturn(aResponse()
.withHeader("Content-Type", "application/json;charset=UTF-8")
.withStatus(200)
.withBody("{\"storagePoolId\":\"4daaa55e00000000\",\"dataLayout\":\"MediumGranularity\",\"vtreeId\":\"657e289500000009\","
+ "\"sizeInKb\":8388608,\"snplIdOfAutoSnapshot\":null,\"volumeType\":\"ThinProvisioned\",\"consistencyGroupId\":null,"
+ "\"ancestorVolumeId\":null,\"notGenuineSnapshot\":false,\"accessModeLimit\":\"ReadWrite\",\"secureSnapshotExpTime\":0,"
+ "\"useRmcache\":false,\"managedBy\":\"ScaleIO\",\"lockedAutoSnapshot\":false,\"lockedAutoSnapshotMarkedForRemoval\":false,"
+ "\"autoSnapshotGroupId\":null,\"compressionMethod\":\"Invalid\",\"pairIds\":null,\"timeStampIsAccurate\":false,\"mappedSdcInfo\":null,"
+ "\"retentionLevels\":[],\"snplIdOfSourceVolume\":null,\"volumeReplicationState\":\"UnmarkedForReplication\",\"replicationJournalVolume\":false,"
+ "\"replicationTimeStamp\":0,\"originalExpiryTime\":0,\"creationTime\":1623335880,\"name\":\"testvolume\",\"id\":\"c948d0b10000000a\"}")));
}

@After
public void tearDown() throws Exception {
}

@Test
public void testClientAuthSuccess() {
Assert.assertNotNull(client);
wireMockRule.verify(getRequestedFor(urlEqualTo("/api/login"))
.withBasicAuth(new BasicCredentials(username, password)));

wireMockRule.stubFor(get("/api/types/StoragePool/instances")
.willReturn(aResponse()
.withHeader("Content-Type", "application/json;charset=UTF-8")
.withStatus(200)
.withBody("")));

client.listStoragePools();

wireMockRule.verify(getRequestedFor(urlEqualTo("/api/types/StoragePool/instances"))
.withBasicAuth(new BasicCredentials(username, sessionKey)));
}

@Test(expected = CloudRuntimeException.class)
public void testClient() throws Exception {
client = (ScaleIOGatewayClientImpl) ScaleIOGatewayClient.getClient("https://10.2.3.149/api",
"admin", "P@ssword123", false, 60);
public void testClientAuthFailure() throws Exception {
wireMockRule.stubFor(get("/api/login")
.willReturn(unauthorized()
.withHeader("Content-Type", "application/json;charset=UTF-8")
.withBody("")));

new ScaleIOGatewayClientImpl("https://localhost/api", username, password, false, timeout, maxConnections);
}

@Test(expected = ServerApiException.class)
public void testRequestTimeout() {
Assert.assertNotNull(client);
wireMockRule.verify(getRequestedFor(urlEqualTo("/api/login"))
.withBasicAuth(new BasicCredentials(username, password)));

wireMockRule.stubFor(get("/api/types/StoragePool/instances")
.willReturn(aResponse()
.withHeader("Content-Type", "application/json;charset=UTF-8")
.withStatus(200)
.withFixedDelay(2 * timeout * 1000)
.withBody("")));

client.listStoragePools();
}

@Test
public void testCreateSingleVolume() {
Assert.assertNotNull(client);
wireMockRule.verify(getRequestedFor(urlEqualTo("/api/login"))
.withBasicAuth(new BasicCredentials(username, password)));

final String volumeName = "testvolume";
final String scaleIOStoragePoolId = "4daaa55e00000000";
final int sizeInGb = 8;
Volume scaleIOVolume = client.createVolume(volumeName, scaleIOStoragePoolId, sizeInGb, Storage.ProvisioningType.THIN);

wireMockRule.verify(postRequestedFor(urlEqualTo("/api/types/Volume/instances"))
.withBasicAuth(new BasicCredentials(username, sessionKey))
.withRequestBody(containing("\"name\":\"" + volumeName + "\""))
.withHeader("Content-Type", equalTo("application/json")));
wireMockRule.verify(getRequestedFor(urlEqualTo("/api/instances/Volume::c948d0b10000000a"))
.withBasicAuth(new BasicCredentials(username, sessionKey)));

Assert.assertNotNull(scaleIOVolume);
Assert.assertEquals(scaleIOVolume.getId(), "c948d0b10000000a");
Assert.assertEquals(scaleIOVolume.getName(), volumeName);
Assert.assertEquals(scaleIOVolume.getStoragePoolId(), scaleIOStoragePoolId);
Assert.assertEquals(scaleIOVolume.getSizeInKb(), Long.valueOf(sizeInGb * 1024 * 1024));
Assert.assertEquals(scaleIOVolume.getVolumeType(), Volume.VolumeType.ThinProvisioned);
}

@Test
public void testCreateMultipleVolumes() {
Assert.assertNotNull(client);
wireMockRule.verify(getRequestedFor(urlEqualTo("/api/login"))
.withBasicAuth(new BasicCredentials(username, password)));

final String volumeNamePrefix = "testvolume_";
final String scaleIOStoragePoolId = "4daaa55e00000000";
final int sizeInGb = 8;
final int volumesCount = 1000;

for (int i = 1; i <= volumesCount; i++) {
String volumeName = volumeNamePrefix + i;
Volume scaleIOVolume = client.createVolume(volumeName, scaleIOStoragePoolId, sizeInGb, Storage.ProvisioningType.THIN);

Assert.assertNotNull(scaleIOVolume);
Assert.assertEquals(scaleIOVolume.getId(), "c948d0b10000000a");
Assert.assertEquals(scaleIOVolume.getStoragePoolId(), scaleIOStoragePoolId);
Assert.assertEquals(scaleIOVolume.getSizeInKb(), Long.valueOf(sizeInGb * 1024 * 1024));
Assert.assertEquals(scaleIOVolume.getVolumeType(), Volume.VolumeType.ThinProvisioned);
}

wireMockRule.verify(volumesCount, postRequestedFor(urlEqualTo("/api/types/Volume/instances"))
.withBasicAuth(new BasicCredentials(username, sessionKey))
.withRequestBody(containing("\"name\":\"" + volumeNamePrefix))
.withHeader("Content-Type", equalTo("application/json")));
wireMockRule.verify(volumesCount, getRequestedFor(urlEqualTo("/api/instances/Volume::c948d0b10000000a"))
.withBasicAuth(new BasicCredentials(username, sessionKey)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,9 @@ private void populateConfigValuesForValidationSet() {
configValuesForValidation.add("externaldhcp.vmip.max.retry");
configValuesForValidation.add("externaldhcp.vmipFetch.threadPool.max");
configValuesForValidation.add("remote.access.vpn.psk.length");
configValuesForValidation.add(StorageManager.STORAGE_POOL_DISK_WAIT.key());
configValuesForValidation.add(StorageManager.STORAGE_POOL_CLIENT_TIMEOUT.key());
configValuesForValidation.add(StorageManager.STORAGE_POOL_CLIENT_MAX_CONNECTIONS.key());
}

private void weightBasedParametersForValidation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3130,6 +3130,7 @@ public ConfigKey<?>[] getConfigKeys() {
MaxNumberOfManagedClusteredFileSystems,
STORAGE_POOL_DISK_WAIT,
STORAGE_POOL_CLIENT_TIMEOUT,
STORAGE_POOL_CLIENT_MAX_CONNECTIONS,
PRIMARY_STORAGE_DOWNLOAD_WAIT,
SecStorageMaxMigrateSessions,
MaxDataMigrationWaitTime
Expand Down
11 changes: 11 additions & 0 deletions server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,13 @@ protected void cleanVolumesCache(VolumeVO volume) {
}
}

private void removeVolume(long volumeId) {
final VolumeVO volume = _volsDao.findById(volumeId);
if (volume != null) {
_volsDao.remove(volumeId);
}
}

protected boolean stateTransitTo(Volume vol, Volume.Event event) throws NoTransitionException {
return _volStateMachine.transitTo(vol, event, null, _volsDao);
}
Expand Down Expand Up @@ -1526,6 +1533,7 @@ public Volume destroyVolume(long volumeId, Account caller, boolean expunge, bool
}
}

removeVolume(volume.getId());
return volume;
}

Expand Down Expand Up @@ -1621,6 +1629,9 @@ private Volume orchestrateAttachVolumeToVM(Long vmId, Long volumeId, Long device

if (destPrimaryStorage != null && (volumeToAttach.getState() == Volume.State.Allocated || volumeOnSecondary)) {
try {
if (volumeOnSecondary && destPrimaryStorage.getPoolType() == Storage.StoragePoolType.PowerFlex) {
throw new InvalidParameterValueException("Cannot attach uploaded volume, this operation is unsupported on storage pool type " + destPrimaryStorage.getPoolType());
}
newVolumeOnPrimaryStorage = _volumeMgr.createVolumeOnPrimaryStorage(vm, volumeToAttach, rootDiskHyperType, destPrimaryStorage);
} catch (NoTransitionException e) {
s_logger.debug("Failed to create volume on primary storage", e);
Expand Down