This is an automated email from the ASF dual-hosted git repository.
nvazquez pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.22 by this push:
new def264402b4 StorPool support importing standalone disk volumes and
importVm from SHARED (#14389)
def264402b4 is described below
commit def264402b448d6a815d2807a16387ca4ad74bf1
Author: slavkap <[email protected]>
AuthorDate: Thu Oct 22 13:54:12 2026 +0300
StorPool support importing standalone disk volumes and importVm from SHARED
(#14389)
* support importing standalone disk volumes and importVm from SHARED
- Add StorPoolPrimaryDataStoreDriver#listVolumesForImport to discover
StorPool volumes eligible for import (global-id named, not already
tagged "cs"), with a path-based lookup for single-volume import and
for unmanage.
- Add volumeManagementChanged() to tag/untag the backing StorPool
volume when a CloudStack volume is imported or unmanaged.
- Wire the KVM shared/local disk import paths to notify the storage
driver via VolumeImportUnmanageService so imported volumes get
tagged correctly.
* Addressed Copilot reviews
* storpool: list volumes via driver API, fix unmanage tag clearing
- VM import from disk (ImportVm) now asks the storage driver to list/
describe the volume (PrimaryDataStoreDriver#listVolumesForImport)
instead of dispatching CheckVolumeCommand to a KVM host agent, when
the pool's driver supports it (StorPool); falls back to the agent
command for every other pool type, unchanged.
- Removed the StorPool-specific logic from
LibvirtCheckVolumeCommandWrapper
(reverted to the upstream 4.22 behavior). Checking a StorPool volume
this way required attaching it to the host first and detaching it
again afterward, just to read its size - an unnecessary attach/detach
cycle that could interfere with the volume's real usage elsewhere.
Since StorPool can report the volume's info directly through its own
API, there is no need to go through the agent at all anymore.
- volumeManagementChanged(imported=false) now clears the uuid/qc/cvm/
vc-policy StorPool tags (previously passed null, which left them
untouched instead of cleared), in addition to cs.
* addressed reviews
* fixed unit tests
---
.../volume/VolumeImportUnmanageService.java | 11 +-
.../api/storage/PrimaryDataStoreDriver.java | 21 +
.../kvm/storage/StorPoolStorageAdaptor.java | 3 +-
.../driver/StorPoolPrimaryDataStoreDriver.java | 130 ++++-
.../storage/datastore/util/StorPoolUtil.java | 10 +-
.../driver/StorPoolPrimaryDataStoreDriverTest.java | 366 +++++++++++++
.../volume/VolumeImportUnmanageManagerImpl.java | 46 +-
.../cloudstack/vm/UnmanagedVMsManagerImpl.java | 93 +++-
.../VolumeImportUnmanageManagerImplTest.java | 90 ++-
.../cloudstack/vm/UnmanagedVMsManagerImplTest.java | 8 +
.../plugins/storpool/TestStorPoolImportVolume.py | 610 +++++++++++++++++++++
tools/marvin/marvin/lib/base.py | 8 +-
12 files changed, 1365 insertions(+), 31 deletions(-)
diff --git
a/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java
b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java
index 13f01b4944a..aa748862386 100644
---
a/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java
+++
b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java
@@ -19,6 +19,8 @@ package org.apache.cloudstack.storage.volume;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.Storage;
+import com.cloud.storage.StoragePool;
+import com.cloud.storage.Volume;
import com.cloud.utils.component.PluggableService;
import org.apache.cloudstack.api.command.admin.volume.ListVolumesForImportCmd;
import org.apache.cloudstack.api.command.admin.volume.ImportVolumeCmd;
@@ -37,7 +39,8 @@ public interface VolumeImportUnmanageService extends
PluggableService, Configura
Arrays.asList(Hypervisor.HypervisorType.KVM,
Hypervisor.HypervisorType.VMware);
List<Storage.StoragePoolType> SUPPORTED_STORAGE_POOL_TYPES_FOR_KVM =
Arrays.asList(Storage.StoragePoolType.NetworkFilesystem,
- Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.RBD,
Storage.StoragePoolType.SharedMountPoint);
+ Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.RBD,
Storage.StoragePoolType.SharedMountPoint,
+ Storage.StoragePoolType.StorPool);
ConfigKey<Boolean> AllowImportVolumeWithBackingFile = new
ConfigKey<>(Boolean.class,
"allow.import.volume.with.backing.file",
@@ -54,4 +57,10 @@ public interface VolumeImportUnmanageService extends
PluggableService, Configura
boolean unmanageVolume(long volumeId);
+ /**
+ * Lets a storage driver (e.g. StorPool) react to a volume becoming
managed/unmanaged, from
+ * this service or from another import path (e.g. VM import). No-op if
unsupported.
+ */
+ void notifyStorageDriverOfVolumeManagementChange(StoragePool pool, Volume
volume, boolean imported);
+
}
diff --git
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreDriver.java
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreDriver.java
index d908ef6af50..a7268a50280 100644
---
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreDriver.java
+++
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreDriver.java
@@ -18,10 +18,12 @@
*/
package org.apache.cloudstack.engine.subsystem.api.storage;
+import java.util.List;
import java.util.Map;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.storage.command.CommandResult;
+import org.apache.cloudstack.storage.volume.VolumeOnStorageTO;
import com.cloud.host.Host;
import com.cloud.offering.DiskOffering;
@@ -221,4 +223,23 @@ public interface PrimaryDataStoreDriver extends
DataStoreDriver {
}
default void updateStorageWithTheNewDiskOffering(Volume volume,
DiskOffering newDiskOffering) {}
+
+ /**
+ * intended for storage that can enumerate its own volumes available for
import without hypervisor involvement
+ * @param volumePath when not blank, restricts the result to the volume
with this path, if found
+ * @param keyword when not blank, restricts the result to volumes whose
name contains this keyword
+ * @param forUnmanage when volume will be unmanaged from CloudStack
+ * @return null if the storage driver does not support listing volumes for
import on its own,
+ * in which case the caller falls back to querying a host via
GetVolumesOnStorageCommand
+ */
+ default List<VolumeOnStorageTO> listVolumesForImport(StoragePool pool,
String volumePath, String keyword, boolean forUnmanage) {
+ return null;
+ }
+
+ /**
+ * Called after a volume is imported into or unmanaged from CloudStack, so
the driver can
+ * mark/unmark it accordingly (e.g. tag it) for future {@link
#listVolumesForImport} calls.
+ */
+ default void volumeManagementChanged(StoragePool pool, Volume volume,
boolean imported) {
+ }
}
diff --git
a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java
b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java
index 545f7b33c5f..4f3f8123a1d 100644
---
a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java
+++
b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java
@@ -109,12 +109,13 @@ public class StorPoolStorageAdaptor implements
StorageAdaptor {
return deleteStoragePool(pool.getUuid());
}
- private static long getDeviceSize(final String devPath) {
+ private static long getDeviceSize(String devPath) {
SP_LOG("StorPoolStorageAdaptor.getDeviceSize: path=%s", devPath);
if (getVolumeNameFromPath(devPath, true) == null) {
return 0;
}
+ devPath = devPath.trim();
File file = new File(devPath);
if (!file.exists()) {
return 0;
diff --git
a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java
b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java
index 15ec4ce8aa4..9a41ff76c66 100644
---
a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java
+++
b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java
@@ -20,12 +20,16 @@ package org.apache.cloudstack.storage.datastore.driver;
import com.cloud.storage.dao.SnapshotDetailsVO;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
import com.cloud.storage.dao.StoragePoolHostDao;
import com.cloud.storage.dao.VMTemplateDetailsDao;
import com.cloud.storage.dao.VolumeDao;
@@ -82,9 +86,11 @@ import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
import org.apache.cloudstack.storage.volume.VolumeObject;
+import org.apache.cloudstack.storage.volume.VolumeOnStorageTO;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.storage.ResizeVolumeAnswer;
@@ -105,6 +111,7 @@ import com.cloud.exception.StorageUnavailableException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
+import com.cloud.hypervisor.Hypervisor;
import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor;
import com.cloud.offering.DiskOffering;
import com.cloud.server.ResourceTag;
@@ -117,6 +124,7 @@ import com.cloud.storage.DataStoreRole;
import com.cloud.storage.ResizeVolumePayload;
import com.cloud.storage.Snapshot;
import com.cloud.storage.SnapshotVO;
+import com.cloud.storage.Storage;
import com.cloud.storage.Storage.StoragePoolType;
import com.cloud.storage.StorageManager;
import com.cloud.storage.StoragePool;
@@ -130,6 +138,7 @@ import com.cloud.storage.dao.SnapshotDetailsDao;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.jetbrains.annotations.NotNull;
public class StorPoolPrimaryDataStoreDriver implements PrimaryDataStoreDriver {
@@ -353,6 +362,7 @@ public class StorPoolPrimaryDataStoreDriver implements
PrimaryDataStoreDriver {
volume.setPoolId(dataStore.getId());
volume.setPath(path);
volume.setPoolType(StoragePoolType.StorPool);
+ volume.setFormat(Storage.ImageFormat.QCOW2);
volumeDao.update(volume.getId(), volume);
}
@@ -895,7 +905,7 @@ public class StorPoolPrimaryDataStoreDriver implements
PrimaryDataStoreDriver {
template = conn.getTemplateName();
}
- StorPoolVolumeDef volumeDef = new StorPoolVolumeDef(null,
size, tags, parentName, null, template, null, null, null);
+ StorPoolVolumeDef volumeDef = new StorPoolVolumeDef(null,
size, tags, parentName, vinfo.getMaxIops(), template, null, null, null);
resp = StorPoolUtil.volumeCreate(volumeDef, conn);
if (resp.getError() == null) {
@@ -1367,6 +1377,120 @@ public class StorPoolPrimaryDataStoreDriver implements
PrimaryDataStoreDriver {
return null;
}
+ /**
+ * Lists StorPool volumes available for import (globalId name, no "cs" tag
unless
+ * {@code forUnmanage}). When {@code volumePath} is given, looks up that
one volume directly
+ * instead of browsing the whole pool; when {@code forUnmanage} is also
true, an already
+ * "cs"-tagged volume is still returned, since the caller is
unmanageVolume() looking for the
+ * very volume it is about to drop from CloudStack.
+ */
+ @Override
+ public List<VolumeOnStorageTO> listVolumesForImport(StoragePool pool,
String volumePath, String keyword, boolean forUnmanage) {
+
StorPoolUtil.spLog("StorPoolPrimaryDataStoreDriver.listVolumesForImport:
pool=%s, volumePath=%s, keyword=%s, forUnmanage=%s", pool.getName(),
volumePath, keyword, forUnmanage);
+ SpConnectionDesc conn = StorPoolUtil.getSpConnection(pool.getUuid(),
pool.getId(), storagePoolDetailsDao, primaryStoreDao);
+ List<VolumeOnStorageTO> volumes = new ArrayList<>();
+
+ if (StringUtils.isNotBlank(volumePath)) {
+ return getVolumeOnStorageByPath(pool, volumePath, conn, volumes,
forUnmanage);
+ }
+
+ SpApiResponse resp = StorPoolUtil.volumesListResponse(conn);
+ if (resp.getError() != null) {
+
StorPoolUtil.spLog("StorPoolPrimaryDataStoreDriver.listVolumesForImport: could
not list volumes for pool=%s. Error: %s", pool.getName(), resp.getError());
+ return volumes;
+ }
+ return getVolumeOnStorage(pool, keyword, resp, volumes);
+ }
+
+ @NotNull
+ private List<VolumeOnStorageTO> getVolumeOnStorage(StoragePool pool,
String keyword, SpApiResponse resp, List<VolumeOnStorageTO> volumes) {
+ JsonArray volumesList =
resp.fullJson.getAsJsonObject().getAsJsonArray("data");
+
+ for (int i = 0; i < volumesList.size(); i++) {
+ JsonObject volume = volumesList.get(i).getAsJsonObject();
+ String rawName = volume.get("name").getAsString();
+ if (isVolumeDeleted(volume) || !isEligibleForBulkImport(volume,
rawName)) {
+ continue;
+ }
+
+ String name = rawName.substring(1);
+
+ if (StringUtils.isNotBlank(keyword) && !name.contains(keyword)) {
+ continue;
+ }
+ volumes.add(toVolumeOnStorageTO(volume, name));
+ }
+
StorPoolUtil.spLog("StorPoolPrimaryDataStoreDriver.listVolumesForImport: found
%s volume(s) for pool=%s", volumes.size(), pool.getName());
+ return volumes;
+ }
+
+ @NotNull
+ private List<VolumeOnStorageTO> getVolumeOnStorageByPath(StoragePool pool,
String volumePath, SpConnectionDesc conn, List<VolumeOnStorageTO> volumes,
boolean forUnmanage) {
+ String name = StorPoolStorageAdaptor.getVolumeNameFromPath(volumePath,
true);
+ SpApiResponse resp = StorPoolUtil.volumeDescribeResponse(name, conn);
+ if (resp.getError() != null) {
+
StorPoolUtil.spLog("StorPoolPrimaryDataStoreDriver.listVolumesForImport: could
not describe volume=%s on pool=%s. Error: %s", name, pool.getName(),
resp.getError());
+ return volumes;
+ }
+ JsonArray data =
resp.fullJson.getAsJsonObject().getAsJsonArray("data");
+ if (data == null || data.isEmpty()) {
+ return volumes;
+ }
+ JsonObject volume = data.get(0).getAsJsonObject();
+ String rawName = volume.get("name").getAsString();
+ if (isVolumeDeleted(volume) || !isGlobalIdVolumeName(rawName) ||
(!forUnmanage && isTaggedCs(volume))) {
+ return volumes;
+ }
+ volumes.add(toVolumeOnStorageTO(volume, rawName.substring(1)));
+ return volumes;
+ }
+
+ private static boolean isVolumeDeleted(JsonObject volume) {
+ return volume.has("deleted") && volume.get("deleted").getAsBoolean();
+ }
+
+ private static boolean isGlobalIdVolumeName(String rawName) {
+ return !rawName.startsWith("*") && !rawName.contains("@") &&
rawName.startsWith("~");
+ }
+
+ private static boolean isTaggedCs(JsonObject volume) {
+ JsonObject tags = volume.has("tags") &&
!volume.get("tags").isJsonNull() ? volume.getAsJsonObject("tags") : null;
+ return tags != null && tags.has("cs");
+ }
+
+ private static boolean isEligibleForBulkImport(JsonObject volume, String
rawName) {
+ return isGlobalIdVolumeName(rawName) && !isTaggedCs(volume);
+ }
+
+ private VolumeOnStorageTO toVolumeOnStorageTO(JsonObject volume, String
name) {
+ String path = StorPoolUtil.devPath(name);
+ long size = volume.get("size").getAsLong();
+ long spaceUsed = volume.has("spaceUsed") ?
volume.get("spaceUsed").getAsLong() : size;
+ StorPoolUtil.spLog("listVolumesForImport: found volume=%s, path=%s,
size=%s, spaceUsed=%s", name, path, size, spaceUsed);
+ return new VolumeOnStorageTO(Hypervisor.HypervisorType.KVM, path,
name, path,
+ Storage.ImageFormat.QCOW2.toString(), spaceUsed, size);
+ }
+
+ @Override
+ public void volumeManagementChanged(StoragePool pool, Volume volume,
boolean imported) {
+
StorPoolUtil.spLog("StorPoolPrimaryDataStoreDriver.volumeManagementChanged:
pool=%s, volume=%s, imported=%s", pool.getName(), volume.getPath(), imported);
+ SpConnectionDesc conn = StorPoolUtil.getSpConnection(pool.getUuid(),
pool.getId(), storagePoolDetailsDao, primaryStoreDao);
+ String name =
StorPoolStorageAdaptor.getVolumeNameFromPath(volume.getPath(), true);
+ Map<String, String> tags;
+ if (imported) {
+ String tier = volume.getDiskOfferingId() != null ?
getTierFromOfferingDetail(volume.getDiskOfferingId()) : null;
+ Long instanceId = volume.getInstanceId();
+ tags = StorPoolHelper.addStorPoolTags(volume.getUuid(),
getVMInstanceUUID(instanceId), "volume", getVcPolicyTag(instanceId), tier);
+ } else {
+ tags = StorPoolHelper.addStorPoolTags("", "", "", "", "");
+ }
+ StorPoolVolumeDef spVolume = new StorPoolVolumeDef(name, null, tags,
null, null, null, null, null, null);
+ SpApiResponse resp = StorPoolUtil.volumeUpdate(spVolume, conn);
+ if (resp.getError() != null) {
+
StorPoolUtil.spLog("StorPoolPrimaryDataStoreDriver.volumeManagementChanged:
could not update tags on volume=%s. Error: %s", name, resp.getError());
+ }
+ }
+
public boolean canHostAccessStoragePool(Host host, StoragePool pool) {
return false;
}
@@ -1398,11 +1522,11 @@ public class StorPoolPrimaryDataStoreDriver implements
PrimaryDataStoreDriver {
SpConnectionDesc conn =
StorPoolUtil.getSpConnection(poolVO.getUuid(), poolVO.getId(),
storagePoolDetailsDao, primaryStoreDao);
String volName =
StorPoolStorageAdaptor.getVolumeNameFromPath(vInfo.getPath(), true);
Pair<String, String> templateAndTier = getTemplateAndTier(vInfo,
conn);
- Map<String, String> tags = StorPoolHelper.addStorPoolTags(volName,
getVMInstanceUUID(vInfo.getInstanceId()), "volume",
getVcPolicyTag(vInfo.getInstanceId()), templateAndTier.first());
+ Map<String, String> tags =
StorPoolHelper.addStorPoolTags(vInfo.getUuid(),
getVMInstanceUUID(vInfo.getInstanceId()), "volume",
getVcPolicyTag(vInfo.getInstanceId()), templateAndTier.first());
if (vInfo.getDeviceId() != null) {
tags.put("disk", vInfo.getDeviceId().toString());
}
- StorPoolVolumeDef spVolume = new StorPoolVolumeDef(volName, null,
tags, null, null, templateAndTier.second(), null, null, null);
+ StorPoolVolumeDef spVolume = new StorPoolVolumeDef(volName, null,
tags, null, null, null, null, null, null);
StorPoolUtil.spLog("Updating volume's tags [%s] with template
[%s]", tags, templateAndTier.second());
SpApiResponse resp = StorPoolUtil.volumeUpdate(spVolume, conn);
if (resp.getError() != null) {
diff --git
a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java
b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java
index dc4dacba450..500aa9a722c 100644
---
a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java
+++
b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java
@@ -452,8 +452,16 @@ public class StorPoolUtil {
return obj.getAsJsonObject(DATA).getAsJsonArray(CLUSTERS);
}
+ public static SpApiResponse volumesListResponse(SpConnectionDesc conn) {
+ return GET("MultiCluster/VolumesList", conn);
+ }
+
+ public static SpApiResponse volumeDescribeResponse(String name,
SpConnectionDesc conn) {
+ return GET("MultiCluster/Volume/" + name, conn);
+ }
+
public static JsonArray volumesList(SpConnectionDesc conn) {
- SpApiResponse resp = GET("MultiCluster/VolumesList", conn);
+ SpApiResponse resp = volumesListResponse(conn);
JsonObject obj = resp.fullJson.getAsJsonObject();
return obj.getAsJsonArray(DATA);
}
diff --git
a/plugins/storage/volume/storpool/src/test/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriverTest.java
b/plugins/storage/volume/storpool/src/test/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriverTest.java
index b86269511fa..50ba37e77e6 100644
---
a/plugins/storage/volume/storpool/src/test/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriverTest.java
+++
b/plugins/storage/volume/storpool/src/test/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriverTest.java
@@ -20,39 +20,56 @@ package org.apache.cloudstack.storage.datastore.driver;
import com.cloud.agent.api.to.DataObjectType;
import com.cloud.storage.DataStoreRole;
+import com.cloud.storage.Storage;
import com.cloud.storage.Storage.StoragePoolType;
+import com.cloud.storage.StoragePool;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.tags.dao.ResourceTagDao;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine.State;
import com.cloud.vm.dao.VMInstanceDao;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
+import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
+import org.apache.cloudstack.resourcedetail.DiskOfferingDetailVO;
+import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao;
+import org.apache.cloudstack.storage.datastore.api.StorPoolVolumeDef;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
+import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.datastore.util.StorPoolUtil;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
+import org.apache.cloudstack.storage.volume.VolumeOnStorageTO;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
+import java.util.List;
import java.util.UUID;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -73,6 +90,10 @@ public class StorPoolPrimaryDataStoreDriverTest {
private StoragePoolDetailsDao detailsDao;
@Mock
private VolumeDao volumeDao;
+ @Mock
+ private StoragePool importPool;
+ @Mock
+ private DiskOfferingDetailsDao diskOfferingDetailsDao;
DataStore srcStore;
DataStore destStore;
@@ -102,6 +123,11 @@ public class StorPoolPrimaryDataStoreDriverTest {
dstTO = mock(VolumeObjectTO.class);
dstPrimaryTo = mock(PrimaryDataStoreTO.class);
+
+
when(importPool.getUuid()).thenReturn("SP_API_HTTP=1.1.1.1:81;SP_AUTH_TOKEN=token;SP_TEMPLATE=template_name");
+ when(importPool.getId()).thenReturn(1L);
+ when(importPool.getName()).thenReturn("sp-pool");
+ when(StorPoolUtil.devPath(anyString())).thenCallRealMethod();
}
@After
@@ -214,6 +240,346 @@ public class StorPoolPrimaryDataStoreDriverTest {
Assert.assertNull(response.getError());
}
+ @Test
+ public void testListVolumesForImportReturnsGlobalIdVolume() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ StorPoolUtil.SpApiResponse resp =
volumesListResponse(volumeJson("~t.t.t", null, false, 1073741824L, 524288000L));
+ when(StorPoolUtil.volumesListResponse(conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool, null, null,
false);
+
+ Assert.assertEquals(1, volumes.size());
+ VolumeOnStorageTO volume = volumes.get(0);
+ Assert.assertEquals("t.t.t", volume.getName());
+ Assert.assertEquals("/dev/storpool-byid/t.t.t", volume.getPath());
+ Assert.assertEquals("/dev/storpool-byid/t.t.t", volume.getFullPath());
+ Assert.assertEquals(Storage.ImageFormat.QCOW2.toString(),
volume.getFormat());
+ Assert.assertEquals(524288000L, volume.getSize());
+ Assert.assertEquals(1073741824L, volume.getVirtualSize());
+ }
+
+ @Test
+ public void testListVolumesForImportSkipsVolumeTaggedCs() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ JsonObject tagged = volumeJsonObject("~t.t.t", 1073741824L,
1073741824L);
+ JsonObject tags = new JsonObject();
+ tags.addProperty("cs", "volume");
+ tagged.add("tags", tags);
+ StorPoolUtil.SpApiResponse resp = volumesListResponse(tagged);
+ when(StorPoolUtil.volumesListResponse(conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool, null, null,
false);
+
+ Assert.assertTrue(volumes.isEmpty());
+ }
+
+ @Test
+ public void testListVolumesForImportSkipsDeletedVolume() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ StorPoolUtil.SpApiResponse resp =
volumesListResponse(volumeJson("~t.t.t", null, true, 1073741824L, 1073741824L));
+ when(StorPoolUtil.volumesListResponse(conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool, null, null,
false);
+
+ Assert.assertTrue(volumes.isEmpty());
+ }
+
+ @Test
+ public void testListVolumesForImportSkipsNonGlobalIdAndCloneNames() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ JsonArray data = new JsonArray();
+ data.add(volumeJsonObject("friendlyname", 1073741824L, 1073741824L));
+ data.add(volumeJsonObject("*s.s.s", 1073741824L, 1073741824L));
+ data.add(volumeJsonObject("[email protected]", 1073741824L, 1073741824L));
+ StorPoolUtil.SpApiResponse resp = volumesListResponse(data);
+ when(StorPoolUtil.volumesListResponse(conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool, null, null,
false);
+
+ Assert.assertTrue(volumes.isEmpty());
+ }
+
+ @Test
+ public void testListVolumesForImportFiltersByKeyword() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ JsonArray data = new JsonArray();
+ data.add(volumeJsonObject("~t.t.t", 1073741824L, 1073741824L));
+ data.add(volumeJsonObject("~s.s.s", 1073741824L, 1073741824L));
+ StorPoolUtil.SpApiResponse resp = volumesListResponse(data);
+ when(StorPoolUtil.volumesListResponse(conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool, null, "s.s.s",
false);
+
+ Assert.assertEquals(1, volumes.size());
+ Assert.assertEquals("s.s.s", volumes.get(0).getName());
+ }
+
+ @Test
+ public void testListVolumesForImportApiErrorReturnsEmptyList() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ StorPoolUtil.SpApiResponse resp = new StorPoolUtil.SpApiResponse();
+ setResponseError(resp);
+ when(StorPoolUtil.volumesListResponse(conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool, null, null,
false);
+
+ Assert.assertNotNull(volumes);
+ Assert.assertTrue(volumes.isEmpty());
+ }
+
+ @Test
+ public void testListVolumesForImportWithPathReturnsVolume() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ StorPoolUtil.SpApiResponse resp =
volumesListResponse(volumeJson("~t.t.t", null, false, 1073741824L, 524288000L));
+ when(StorPoolUtil.volumeDescribeResponse("~t.t.t",
conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool,
"/dev/storpool-byid/t.t.t", null, false);
+
+ Assert.assertEquals(1, volumes.size());
+ VolumeOnStorageTO volume = volumes.get(0);
+ Assert.assertEquals("t.t.t", volume.getName());
+ Assert.assertEquals("/dev/storpool-byid/t.t.t", volume.getPath());
+ Assert.assertEquals("/dev/storpool-byid/t.t.t", volume.getFullPath());
+ Assert.assertEquals(Storage.ImageFormat.QCOW2.toString(),
volume.getFormat());
+ Assert.assertEquals(524288000L, volume.getSize());
+ Assert.assertEquals(1073741824L, volume.getVirtualSize());
+ }
+
+ @Test
+ public void testListVolumesForImportWithPathExcludesVolumeTaggedCs() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ JsonObject tagged = volumeJsonObject("~t.t.t", 1073741824L,
1073741824L);
+ JsonObject tags = new JsonObject();
+ tags.addProperty("cs", "volume");
+ tagged.add("tags", tags);
+ StorPoolUtil.SpApiResponse resp = volumesListResponse(tagged);
+ when(StorPoolUtil.volumeDescribeResponse("~t.t.t",
conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool,
"/dev/storpool-byid/t.t.t", null, false);
+
+ Assert.assertTrue(volumes.isEmpty());
+ }
+
+ @Test
+ public void
testListVolumesForImportWithPathForUnmanageReturnsVolumeEvenIfTaggedCs() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ JsonObject tagged = volumeJsonObject("~t.t.t", 1073741824L,
1073741824L);
+ JsonObject tags = new JsonObject();
+ tags.addProperty("cs", "volume");
+ tagged.add("tags", tags);
+ StorPoolUtil.SpApiResponse resp = volumesListResponse(tagged);
+ when(StorPoolUtil.volumeDescribeResponse("~t.t.t",
conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool,
"/dev/storpool-byid/t.t.t", null, true);
+
+ Assert.assertEquals(1, volumes.size());
+ Assert.assertEquals("t.t.t", volumes.get(0).getName());
+ }
+
+ @Test
+ public void
testListVolumesForImportWithPathForUnmanageSkipsNonGlobalIdName() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ StorPoolUtil.SpApiResponse resp =
volumesListResponse(volumeJsonObject("[email protected]", 1073741824L, 1073741824L));
+ when(StorPoolUtil.volumeDescribeResponse("~t.t.t",
conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool,
"/dev/storpool-byid/t.t.t", null, true);
+
+ Assert.assertTrue(volumes.isEmpty());
+ }
+
+ @Test
+ public void testListVolumesForImportWithPathSkipsDeletedVolume() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ StorPoolUtil.SpApiResponse resp =
volumesListResponse(volumeJson("~t.t.t", null, true, 1073741824L, 1073741824L));
+ when(StorPoolUtil.volumeDescribeResponse("~t.t.t",
conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool,
"/dev/storpool-byid/t.t.t", null, false);
+
+ Assert.assertTrue(volumes.isEmpty());
+ }
+
+ @Test
+ public void testListVolumesForImportWithPathNotFoundReturnsEmptyList() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ StorPoolUtil.SpApiResponse resp = volumesListResponse(new JsonArray());
+ when(StorPoolUtil.volumeDescribeResponse("~t.t.t",
conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool,
"/dev/storpool-byid/t.t.t", null, false);
+
+ Assert.assertNotNull(volumes);
+ Assert.assertTrue(volumes.isEmpty());
+ }
+
+ @Test
+ public void testListVolumesForImportWithPathApiErrorReturnsEmptyList() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ StorPoolUtil.SpApiResponse resp = new StorPoolUtil.SpApiResponse();
+ setResponseError(resp);
+ when(StorPoolUtil.volumeDescribeResponse("~t.t.t",
conn)).thenReturn(resp);
+
+ List<VolumeOnStorageTO> volumes =
storPoolPrimaryDataStoreDriver.listVolumesForImport(importPool,
"/dev/storpool-byid/t.t.t", null, false);
+
+ Assert.assertNotNull(volumes);
+ Assert.assertTrue(volumes.isEmpty());
+ }
+
+ @Test
+ public void testCreateAsyncSetsVolumeFormatToQcow2() {
+ DataStore dataStore = mock(DataStore.class);
+
when(dataStore.getUuid()).thenReturn("SP_API_HTTP=1.1.1.1:81;SP_AUTH_TOKEN=token;SP_TEMPLATE=template_name");
+ when(dataStore.getId()).thenReturn(1L);
+
+ VolumeInfo vinfo = mock(VolumeInfo.class);
+ when(vinfo.getType()).thenReturn(DataObjectType.VOLUME);
+ when(vinfo.getUuid()).thenReturn(UUID.randomUUID().toString());
+ when(vinfo.getId()).thenReturn(101L);
+ when(vinfo.getSize()).thenReturn(1073741824L);
+ when(vinfo.getPassphraseId()).thenReturn(null);
+ when(vinfo.getInstanceId()).thenReturn(null);
+ when(vinfo.getDiskOfferingId()).thenReturn(null);
+
+ when(StorPoolUtil.getSpConnection(dataStore.getUuid(),
dataStore.getId(), detailsDao, storagePool)).thenReturn(conn);
+ StorPoolUtil.SpApiResponse resp = new StorPoolUtil.SpApiResponse();
+ when(StorPoolUtil.volumeCreate(any(StorPoolVolumeDef.class),
eq(conn))).thenReturn(resp);
+ when(StorPoolUtil.getNameFromResponse(resp,
false)).thenReturn("t.t.t");
+
+ VolumeVO volumeVO = mock(VolumeVO.class);
+ when(volumeVO.getId()).thenReturn(101L);
+ when(volumeDao.findById(101L)).thenReturn(volumeVO);
+
+ StoragePoolVO storagePoolVO = mock(StoragePoolVO.class);
+ when(storagePool.findById(1L)).thenReturn(storagePoolVO);
+
+ @SuppressWarnings("unchecked")
+ AsyncCompletionCallback<CreateCmdResult> createCallback =
mock(AsyncCompletionCallback.class);
+
+ storPoolPrimaryDataStoreDriver.createAsync(dataStore, vinfo,
createCallback);
+
+ verify(volumeVO).setFormat(Storage.ImageFormat.QCOW2);
+ verify(volumeDao).update(eq(101L), eq(volumeVO));
+ }
+
+ @Test
+ public void testVolumeImportedTagsVolumeWithCsAndUuid() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ String volumeUuid = UUID.randomUUID().toString();
+ VolumeVO volume = mock(VolumeVO.class);
+ when(volume.getPath()).thenReturn("/dev/storpool-byid/t.t.t");
+ when(volume.getUuid()).thenReturn(volumeUuid);
+ when(volume.getDiskOfferingId()).thenReturn(5L);
+ when(volume.getInstanceId()).thenReturn(null);
+ when(diskOfferingDetailsDao.findDetail(5L,
StorPoolUtil.SP_TIER)).thenReturn(null);
+ StorPoolUtil.SpApiResponse resp = new StorPoolUtil.SpApiResponse();
+ when(StorPoolUtil.volumeUpdate(any(StorPoolVolumeDef.class),
eq(conn))).thenReturn(resp);
+
+ storPoolPrimaryDataStoreDriver.volumeManagementChanged(importPool,
volume, true);
+
+ ArgumentCaptor<StorPoolVolumeDef> captor =
ArgumentCaptor.forClass(StorPoolVolumeDef.class);
+ utilities.verify(() -> StorPoolUtil.volumeUpdate(captor.capture(),
eq(conn)), times(1));
+ StorPoolVolumeDef updated = captor.getValue();
+ Assert.assertEquals("~t.t.t", updated.getName());
+ Assert.assertEquals(volumeUuid, updated.getTags().get("uuid"));
+ Assert.assertEquals("volume", updated.getTags().get("cs"));
+ Assert.assertNull(updated.getTags().get("qc"));
+ Assert.assertNull(updated.getTags().get("cvm"));
+ }
+
+ @Test
+ public void testVolumeImportedSetsCvmTagWhenAttachedToVm() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ VolumeVO volume = mock(VolumeVO.class);
+ when(volume.getPath()).thenReturn("/dev/storpool-byid/t.t.t");
+ when(volume.getUuid()).thenReturn(UUID.randomUUID().toString());
+ when(volume.getInstanceId()).thenReturn(7L);
+ VMInstanceVO vm = mock(VMInstanceVO.class);
+ String vmUuid = UUID.randomUUID().toString();
+ when(vm.getUuid()).thenReturn(vmUuid);
+ when(vmInstanceDao.findById(7L)).thenReturn(vm);
+ StorPoolUtil.SpApiResponse resp = new StorPoolUtil.SpApiResponse();
+ when(StorPoolUtil.volumeUpdate(any(StorPoolVolumeDef.class),
eq(conn))).thenReturn(resp);
+
+ storPoolPrimaryDataStoreDriver.volumeManagementChanged(importPool,
volume, true);
+
+ ArgumentCaptor<StorPoolVolumeDef> captor =
ArgumentCaptor.forClass(StorPoolVolumeDef.class);
+ utilities.verify(() -> StorPoolUtil.volumeUpdate(captor.capture(),
eq(conn)), times(1));
+ Assert.assertEquals(vmUuid, captor.getValue().getTags().get("cvm"));
+ }
+
+ @Test
+ public void testVolumeImportedIncludesQosTierFromDiskOffering() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ VolumeVO volume = mock(VolumeVO.class);
+ when(volume.getPath()).thenReturn("/dev/storpool-byid/t.t.t");
+ when(volume.getUuid()).thenReturn(UUID.randomUUID().toString());
+ when(volume.getDiskOfferingId()).thenReturn(5L);
+ when(volume.getInstanceId()).thenReturn(null);
+ DiskOfferingDetailVO tierDetail = mock(DiskOfferingDetailVO.class);
+ when(tierDetail.getValue()).thenReturn("ssd-tier");
+ when(diskOfferingDetailsDao.findDetail(5L,
StorPoolUtil.SP_TIER)).thenReturn(tierDetail);
+ StorPoolUtil.SpApiResponse resp = new StorPoolUtil.SpApiResponse();
+ when(StorPoolUtil.volumeUpdate(any(StorPoolVolumeDef.class),
eq(conn))).thenReturn(resp);
+
+ storPoolPrimaryDataStoreDriver.volumeManagementChanged(importPool,
volume, true);
+
+ ArgumentCaptor<StorPoolVolumeDef> captor =
ArgumentCaptor.forClass(StorPoolVolumeDef.class);
+ utilities.verify(() -> StorPoolUtil.volumeUpdate(captor.capture(),
eq(conn)), times(1));
+ Assert.assertEquals("ssd-tier", captor.getValue().getTags().get("qc"));
+ }
+
+ @Test
+ public void testVolumeUnmanagedClearsCsUuidAndQosTags() {
+ when(StorPoolUtil.getSpConnection(importPool.getUuid(),
importPool.getId(), detailsDao, storagePool)).thenReturn(conn);
+ VolumeVO volume = mock(VolumeVO.class);
+ when(volume.getPath()).thenReturn("/dev/storpool-byid/t.t.t");
+ StorPoolUtil.SpApiResponse resp = new StorPoolUtil.SpApiResponse();
+ when(StorPoolUtil.volumeUpdate(any(StorPoolVolumeDef.class),
eq(conn))).thenReturn(resp);
+
+ storPoolPrimaryDataStoreDriver.volumeManagementChanged(importPool,
volume, false);
+
+ ArgumentCaptor<StorPoolVolumeDef> captor =
ArgumentCaptor.forClass(StorPoolVolumeDef.class);
+ utilities.verify(() -> StorPoolUtil.volumeUpdate(captor.capture(),
eq(conn)), times(1));
+ StorPoolVolumeDef updated = captor.getValue();
+ Assert.assertEquals("~t.t.t", updated.getName());
+ Assert.assertEquals("", updated.getTags().get("cs"));
+ Assert.assertEquals("",updated.getTags().get("uuid"));
+ Assert.assertEquals("",updated.getTags().get("qc"));
+ Assert.assertEquals("",updated.getTags().get("cvm"));
+
Assert.assertEquals("",updated.getTags().get(StorPoolUtil.SP_VC_POLICY));
+ }
+
+ private static JsonObject volumeJsonObject(String name, Long size, Long
spaceUsed) {
+ JsonObject volume = new JsonObject();
+ volume.addProperty("name", name);
+ volume.addProperty("size", size);
+ if (spaceUsed != null) {
+ volume.addProperty("spaceUsed", spaceUsed);
+ }
+ return volume;
+ }
+
+ private static JsonObject volumeJson(String name, JsonObject tags, boolean
deleted, long size, long spaceUsed) {
+ JsonObject volume = volumeJsonObject(name, size, spaceUsed);
+ volume.addProperty("deleted", deleted);
+ if (tags != null) {
+ volume.add("tags", tags);
+ }
+ return volume;
+ }
+
+ private static StorPoolUtil.SpApiResponse volumesListResponse(JsonObject
volume) {
+ JsonArray data = new JsonArray();
+ data.add(volume);
+ return volumesListResponse(data);
+ }
+
+ private static StorPoolUtil.SpApiResponse volumesListResponse(JsonArray
data) {
+ JsonObject root = new JsonObject();
+ root.add("data", data);
+ StorPoolUtil.SpApiResponse resp = new StorPoolUtil.SpApiResponse();
+ resp.fullJson = new JsonParser().parse(root.toString());
+ return resp;
+ }
+
private void setReturnsWhenSourceAndDestinationAreVolumes(DataStore
srcStore, DataStore destStore, DataObject srcObj, DataObject destObj,
VolumeObjectTO srcTO, VolumeObjectTO dstTO, PrimaryDataStoreTO dstPrimaryTo,
VMInstanceVO vm) {
when(srcStore.getRole()).thenReturn(DataStoreRole.Primary);
when(destStore.getRole()).thenReturn(DataStoreRole.Primary);
diff --git
a/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java
b/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java
index 4a69e32e77e..40265df384d 100644
---
a/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java
+++
b/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java
@@ -39,6 +39,7 @@ import com.cloud.resourcelimit.ReservationHelper;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.Storage;
+import com.cloud.storage.StoragePool;
import com.cloud.storage.StoragePoolHostVO;
import com.cloud.storage.StoragePoolStatus;
import com.cloud.storage.Volume;
@@ -69,6 +70,9 @@ import
org.apache.cloudstack.api.response.VolumeForImportResponse;
import org.apache.cloudstack.api.response.VolumeResponse;
import org.apache.cloudstack.context.CallContext;
import
org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
+import
org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.resourcelimit.Reserver;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
@@ -120,6 +124,8 @@ public class VolumeImportUnmanageManagerImpl implements
VolumeImportUnmanageServ
private VolumeApiService volumeApiService;
@Inject
private SnapshotDataStoreDao snapshotDataStoreDao;
+ @Inject
+ private DataStoreManager dataStoreMgr;
static final String DEFAULT_DISK_OFFERING_NAME = "Default Custom Offering
for Volume Import";
static final String DEFAULT_DISK_OFFERING_UNIQUE_NAME = "Volume-Import";
@@ -195,7 +201,7 @@ public class VolumeImportUnmanageManagerImpl implements
VolumeImportUnmanageServ
}
// 4. get volume info on storage through host and check
- VolumeOnStorageTO volume = getVolumeOnStorageAndCheck(pool,
volumePath);
+ VolumeOnStorageTO volume = getVolumeOnStorageAndCheck(pool,
volumePath, false);
if (checkIfVolumeForSnapshot(pool, volume.getFullPath())) {
logFailureAndThrowException("Volume is a reference of snapshot on
primary: " + volume.getFullPath());
@@ -219,6 +225,9 @@ public class VolumeImportUnmanageManagerImpl implements
VolumeImportUnmanageServ
String volumeName = StringUtils.isNotBlank(cmd.getName()) ?
cmd.getName().trim() : volumePath;
VolumeVO volumeVO = importVolumeInternal(volume, diskOffering,
owner, pool, volumeName);
+ // 7b. let the storage driver mark the volume as now owned by
CloudStack (e.g. StorPool's cs/uuid tags)
+ notifyStorageDriverOfVolumeManagementChange(pool, volumeVO, true);
+
// 8. Update resource count
updateResourceLimitForVolumeImport(volumeVO);
@@ -232,9 +241,9 @@ public class VolumeImportUnmanageManagerImpl implements
VolumeImportUnmanageServ
}
}
- protected VolumeOnStorageTO getVolumeOnStorageAndCheck(StoragePoolVO pool,
String volumePath) {
+ protected VolumeOnStorageTO getVolumeOnStorageAndCheck(StoragePoolVO pool,
String volumePath, boolean forUnmanage) {
// send a command to hypervisor to check
- List<VolumeOnStorageTO> volumes = listVolumesForImportInternal(pool,
volumePath, null);
+ List<VolumeOnStorageTO> volumes = listVolumesForImportInternal(pool,
volumePath, null, forUnmanage);
if (CollectionUtils.isEmpty(volumes)) {
logFailureAndThrowException("Cannot find volume on storage pool: "
+ volumePath);
}
@@ -250,6 +259,15 @@ public class VolumeImportUnmanageManagerImpl implements
VolumeImportUnmanageServ
}
protected List<VolumeOnStorageTO>
listVolumesForImportInternal(StoragePoolVO pool, String volumePath, String
keyword) {
+ return listVolumesForImportInternal(pool, volumePath, keyword, false);
+ }
+
+ protected List<VolumeOnStorageTO>
listVolumesForImportInternal(StoragePoolVO pool, String volumePath, String
keyword, boolean forUnmanage) {
+ List<VolumeOnStorageTO> volumes = listVolumesForImportFromDriver(pool,
volumePath, keyword, forUnmanage);
+ if (volumes != null) {
+ return volumes;
+ }
+
Pair<HostVO, String> hostAndLocalPath =
findHostAndLocalPathForVolumeImport(pool);
HostVO host = hostAndLocalPath.first();
checkIfHostAndPoolSupported(host, pool);
@@ -266,6 +284,23 @@ public class VolumeImportUnmanageManagerImpl implements
VolumeImportUnmanageServ
return ((GetVolumesOnStorageAnswer) answer).getVolumes();
}
+ private List<VolumeOnStorageTO>
listVolumesForImportFromDriver(StoragePoolVO pool, String volumePath, String
keyword, boolean forUnmanage) {
+ DataStore dataStore = dataStoreMgr.getPrimaryDataStore(pool.getId());
+ if (dataStore == null || !(dataStore.getDriver() instanceof
PrimaryDataStoreDriver)) {
+ return null;
+ }
+ return ((PrimaryDataStoreDriver)
dataStore.getDriver()).listVolumesForImport(pool, volumePath, keyword,
forUnmanage);
+ }
+
+ @Override
+ public void notifyStorageDriverOfVolumeManagementChange(StoragePool pool,
Volume volume, boolean imported) {
+ DataStore dataStore = dataStoreMgr.getPrimaryDataStore(pool.getId());
+ if (dataStore == null || !(dataStore.getDriver() instanceof
PrimaryDataStoreDriver)) {
+ return;
+ }
+ ((PrimaryDataStoreDriver)
dataStore.getDriver()).volumeManagementChanged(pool, volume, imported);
+ }
+
@Override
public boolean unmanageVolume(long volumeId) {
// 1. check if volume can be unmanaged
@@ -275,7 +310,7 @@ public class VolumeImportUnmanageManagerImpl implements
VolumeImportUnmanageServ
StoragePoolVO pool = checkIfPoolAvailable(volume.getPoolId());
// 3. unmanage volume internally
- getVolumeOnStorageAndCheck(pool, volume.getPath());
+ getVolumeOnStorageAndCheck(pool, volume.getPath(), true);
// 3. Update resource count
updateResourceLimitForVolumeUnmanage(volume);
@@ -286,6 +321,9 @@ public class VolumeImportUnmanageManagerImpl implements
VolumeImportUnmanageServ
// 5. update the state/removed of record
unmanageVolumeFromDatabase(volume);
+ // 6. let the storage driver clear any CloudStack ownership marker
(e.g. StorPool's cs/uuid tags)
+ notifyStorageDriverOfVolumeManagementChange(pool, volume, false);
+
return true;
}
diff --git
a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
index 585f00d80eb..355457d7c8a 100644
--- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
@@ -164,6 +164,7 @@ import
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationSe
import
org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
+import
org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.reservation.dao.ReservationDao;
@@ -172,6 +173,7 @@ import
org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
+import org.apache.cloudstack.storage.volume.VolumeImportUnmanageService;
import org.apache.cloudstack.storage.volume.VolumeOnStorageTO;
import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo;
import org.apache.commons.collections.CollectionUtils;
@@ -318,6 +320,8 @@ public class UnmanagedVMsManagerImpl implements
UnmanagedVMsManager {
@Inject
private DataStoreManager dataStoreManager;
@Inject
+ private VolumeImportUnmanageService volumeImportUnmanageService;
+ @Inject
private ImportVmTasksManager importVmTasksManager;
protected Gson gson;
@@ -816,6 +820,8 @@ public class UnmanagedVMsManagerImpl implements
UnmanagedVMsManager {
DiskProfile profile = volumeManager.updateImportedVolume(type,
diskOffering, vm, template, deviceId,
storagePool.getId(), storagePool.getPoolType(), diskPath,
null, diskProfile, format);
+ notifyStorageDriverOfVolumeImport(storagePool, profile.getVolumeId());
+
return new Pair<>(profile, storagePool);
}
@@ -828,9 +834,20 @@ public class UnmanagedVMsManagerImpl implements
UnmanagedVMsManager {
DiskProfile profile = volumeManager.updateImportedVolume(type,
diskOffering, vm, template, deviceId,
poolId, storagePool.getPoolType(), diskPath, null,
diskProfile, format);
+ notifyStorageDriverOfVolumeImport(storagePool, profile.getVolumeId());
+
return new Pair<>(profile, storagePool);
}
+ private void notifyStorageDriverOfVolumeImport(StoragePool pool, long
volumeId) {
+ Volume volume = volumeDao.findById(volumeId);
+ if (volume == null) {
+ logger.warn(String.format("Could not find volume with id [%s] to
notify storage driver after import", volumeId));
+ return;
+ }
+
volumeImportUnmanageService.notifyStorageDriverOfVolumeManagementChange(pool,
volume, true);
+ }
+
/**
* Reads the image format the hypervisor reported for an existing volume,
so that the imported
* volume records what is actually on the pool (raw on RBD, qcow2 on file
based pools) instead of
@@ -2959,6 +2976,10 @@ public class UnmanagedVMsManagerImpl implements
UnmanagedVMsManager {
// check then runs against whichever pool that cluster does have.
Both are null for a zone
// wide pool, which every host can see.
StoragePoolVO importStoragePool =
primaryDataStoreDao.findById(poolId);
+ if (importStoragePool == null) {
+ cleanupFailedImportVM(userVm);
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
String.format("Import failed for Vm: %s. Storage pool with id: %s not found",
userVm.getInstanceName(), poolId));
+ }
final DataCenterDeployment plan = new
DataCenterDeployment(zone.getId(), importStoragePool.getPodId(),
importStoragePool.getClusterId(), hostId, poolId, null);
DeployDestination dest = null;
@@ -2976,27 +2997,16 @@ public class UnmanagedVMsManagerImpl implements
UnmanagedVMsManager {
Map<Volume, StoragePool> storage = dest.getStorageForDisks();
Volume volume = volumeDao.findById(diskProfile.getVolumeId());
StoragePool storagePool = storage.get(volume);
- CheckVolumeCommand checkVolumeCommand = new CheckVolumeCommand();
- checkVolumeCommand.setSrcFile(diskPath);
- StorageFilerTO storageTO = new StorageFilerTO(storagePool);
- checkVolumeCommand.setStorageFilerTO(storageTO);
- Answer answer = agentManager.easySend(dest.getHost().getId(),
checkVolumeCommand);
- if (!(answer instanceof CheckVolumeAnswer)) {
- cleanupFailedImportVM(userVm);
- throw new CloudRuntimeException("Disk not found or is
invalid");
- }
- CheckVolumeAnswer checkVolumeAnswer = (CheckVolumeAnswer) answer;
+
+ Pair<Map<VolumeOnStorageTO.Detail, String>, Long>
volumeDetailsAndSize;
try {
- checkVolume(checkVolumeAnswer.getVolumeDetails());
+ volumeDetailsAndSize = getVolumeDetailsAndSize(storagePool,
diskPath, dest.getHost().getId());
} catch (CloudRuntimeException e) {
cleanupFailedImportVM(userVm);
throw e;
}
- if (!checkVolumeAnswer.getResult()) {
- cleanupFailedImportVM(userVm);
- throw new CloudRuntimeException("Disk not found or is
invalid");
- }
- diskProfile.setSize(checkVolumeAnswer.getSize());
+ Map<VolumeOnStorageTO.Detail, String> volumeDetails =
volumeDetailsAndSize.first();
+ diskProfile.setSize(volumeDetailsAndSize.second());
CheckedReservation primaryStorageReservation = new
CheckedReservation(owner, Resource.ResourceType.primary_storage,
resourceLimitStorageTags,
CollectionUtils.isNotEmpty(resourceLimitStorageTags) ?
diskProfile.getSize() : 0L, reservationDao, resourceLimitService);
@@ -3005,7 +3015,10 @@ public class UnmanagedVMsManagerImpl implements
UnmanagedVMsManager {
List<Pair<DiskProfile, StoragePool>> diskProfileStoragePoolList =
new ArrayList<>();
try {
long deviceId = 1L;
- Storage.ImageFormat diskFormat =
getImageFormatFromVolumeDetails(checkVolumeAnswer.getVolumeDetails());
+ Storage.ImageFormat diskFormat =
getImageFormatFromVolumeDetails(volumeDetails);
+ if (storagePool.getPoolType() ==
Storage.StoragePoolType.StorPool) {
+ diskFormat = Storage.ImageFormat.QCOW2;
+ }
if(ImportSource.SHARED == importSource) {
diskProfileStoragePoolList.add(importKVMSharedDisk(userVm,
diskOffering, Volume.Type.ROOT,
template, deviceId, poolId, diskPath, diskProfile,
diskFormat));
@@ -3068,6 +3081,52 @@ public class UnmanagedVMsManagerImpl implements
UnmanagedVMsManager {
}
}
+ /**
+ * @return null if the pool's storage driver does not support enumerating
its own volumes
+ * (the caller should fall back to a host agent command), otherwise the
(possibly empty)
+ * list of volumes the driver found at volumePath.
+ */
+ private List<VolumeOnStorageTO> listVolumesForImportFromDriver(StoragePool
pool, String volumePath) {
+ DataStore dataStore =
dataStoreManager.getPrimaryDataStore(pool.getId());
+ if (dataStore == null || !(dataStore.getDriver() instanceof
PrimaryDataStoreDriver)) {
+ return null;
+ }
+ return ((PrimaryDataStoreDriver)
dataStore.getDriver()).listVolumesForImport(pool, volumePath, null, false);
+ }
+
+ /**
+ * Retrieves the details and the virtual size of the disk at diskPath,
either from the pool's
+ * storage driver (if it can enumerate its own volumes) or from the host
agent.
+ * @throws CloudRuntimeException if the disk is not found, is invalid or
fails the volume checks
+ */
+ private Pair<Map<VolumeOnStorageTO.Detail, String>, Long>
getVolumeDetailsAndSize(StoragePool storagePool, String diskPath, long hostId) {
+ List<VolumeOnStorageTO> volumesFromDriver =
listVolumesForImportFromDriver(storagePool, diskPath);
+ if (volumesFromDriver != null) {
+ // The storage driver can enumerate its own volumes (e.g.
StorPool, via its
+ // management API), so there is no need to reach the host agent
just to read size.
+ if (CollectionUtils.isEmpty(volumesFromDriver)) {
+ throw new CloudRuntimeException("Disk not found or is
invalid");
+ }
+ VolumeOnStorageTO volumeOnStorage = volumesFromDriver.get(0);
+ checkVolume(volumeOnStorage.getDetails());
+ return new Pair<>(volumeOnStorage.getDetails(),
volumeOnStorage.getVirtualSize());
+ }
+
+ CheckVolumeCommand checkVolumeCommand = new CheckVolumeCommand();
+ checkVolumeCommand.setSrcFile(diskPath);
+ checkVolumeCommand.setStorageFilerTO(new StorageFilerTO(storagePool));
+ Answer answer = agentManager.easySend(hostId, checkVolumeCommand);
+ if (!(answer instanceof CheckVolumeAnswer)) {
+ throw new CloudRuntimeException("Disk not found or is invalid");
+ }
+ CheckVolumeAnswer checkVolumeAnswer = (CheckVolumeAnswer) answer;
+ checkVolume(checkVolumeAnswer.getVolumeDetails());
+ if (!checkVolumeAnswer.getResult()) {
+ throw new CloudRuntimeException("Disk not found or is invalid");
+ }
+ return new Pair<>(checkVolumeAnswer.getVolumeDetails(),
checkVolumeAnswer.getSize());
+ }
+
private void checkVolume(Map<VolumeOnStorageTO.Detail, String>
volumeDetails) {
if (MapUtils.isEmpty(volumeDetails)) {
return;
diff --git
a/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java
b/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java
index 08bcaeaced3..e1c977e8e1a 100644
---
a/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java
+++
b/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java
@@ -62,6 +62,9 @@ import
org.apache.cloudstack.api.response.VolumeForImportResponse;
import org.apache.cloudstack.api.response.VolumeResponse;
import org.apache.cloudstack.context.CallContext;
import
org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
+import
org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
@@ -134,6 +137,8 @@ public class VolumeImportUnmanageManagerImplTest {
private VolumeApiService volumeApiService;
@Mock
private SnapshotDataStoreDao snapshotDataStoreDao;
+ @Mock
+ private DataStoreManager dataStoreMgr;
@Mock
StoragePoolVO storagePoolVO;
@@ -229,7 +234,7 @@ public class VolumeImportUnmanageManagerImplTest {
volumeOnStorageTO.setQemuEncryptFormat(encryptFormat);
List<VolumeOnStorageTO> volumesOnStorageTO = new ArrayList<>();
volumesOnStorageTO.add(volumeOnStorageTO);
-
doReturn(volumesOnStorageTO).when(volumeImportUnmanageManager).listVolumesForImportInternal(storagePoolVO,
path, null);
+
doReturn(volumesOnStorageTO).when(volumeImportUnmanageManager).listVolumesForImportInternal(storagePoolVO,
path, null, false);
ListResponse<VolumeForImportResponse> listResponses =
volumeImportUnmanageManager.listVolumesForImport(cmd);
Assert.assertEquals(1, listResponses.getResponses().size());
@@ -262,7 +267,7 @@ public class VolumeImportUnmanageManagerImplTest {
List<VolumeOnStorageTO> volumesOnStorageTO = new ArrayList<>();
volumesOnStorageTO.add(volumeOnStorageTO);
-
doReturn(volumesOnStorageTO).when(volumeImportUnmanageManager).listVolumesForImportInternal(storagePoolVO,
path, null);
+
doReturn(volumesOnStorageTO).when(volumeImportUnmanageManager).listVolumesForImportInternal(storagePoolVO,
path, null, false);
doNothing().when(volumeImportUnmanageManager).checkIfVolumeIsLocked(volumeOnStorageTO);
doNothing().when(volumeImportUnmanageManager).checkIfVolumeIsEncrypted(volumeOnStorageTO);
@@ -290,6 +295,57 @@ public class VolumeImportUnmanageManagerImplTest {
}
}
+ @Test
+ public void testImportVolumeNotifiesStorageDriverOfImport() throws
ResourceAllocationException {
+ ImportVolumeCmd cmd = mock(ImportVolumeCmd.class);
+ when(cmd.getPath()).thenReturn(path);
+ when(cmd.getStorageId()).thenReturn(poolId);
+ when(cmd.getDiskOfferingId()).thenReturn(diskOfferingId);
+ when(volumeDao.findByPoolIdAndPath(poolId, path)).thenReturn(null);
+ when(templatePoolDao.findByPoolPath(poolId, path)).thenReturn(null);
+
+ VolumeOnStorageTO volumeOnStorageTO = new
VolumeOnStorageTO(hypervisorType, path, name, fullPath,
+ format, size, virtualSize);
+ volumeOnStorageTO.setQemuEncryptFormat(encryptFormat);
+ List<VolumeOnStorageTO> volumesOnStorageTO = new ArrayList<>();
+ volumesOnStorageTO.add(volumeOnStorageTO);
+
+
doReturn(volumesOnStorageTO).when(volumeImportUnmanageManager).listVolumesForImportInternal(storagePoolVO,
path, null, false);
+
+
doNothing().when(volumeImportUnmanageManager).checkIfVolumeIsLocked(volumeOnStorageTO);
+
doNothing().when(volumeImportUnmanageManager).checkIfVolumeIsEncrypted(volumeOnStorageTO);
+
doNothing().when(volumeImportUnmanageManager).checkIfVolumeHasBackingFile(volumeOnStorageTO);
+
+ DiskOfferingVO diskOffering = mock(DiskOfferingVO.class);
+ when(diskOffering.isCustomized()).thenReturn(true);
+
doReturn(diskOffering).when(volumeImportUnmanageManager).getOrCreateDiskOffering(account,
diskOfferingId, zoneId, isLocal);
+
doNothing().when(volumeApiService).validateCustomDiskOfferingSizeRange(anyLong());
+
doReturn(true).when(volumeApiService).doesStoragePoolSupportDiskOffering(any(),
any());
+ doReturn(diskProfile).when(volumeManager).importVolume(any(),
anyString(), any(), eq(virtualSize), isNull(), isNull(), anyLong(),
+ any(), isNull(), isNull(), any(), isNull(), anyLong(), any(),
anyString(), isNull(), eq(Storage.ImageFormat.QCOW2));
+ when(diskProfile.getVolumeId()).thenReturn(volumeId);
+ when(volumeDao.findById(volumeId)).thenReturn(volumeVO);
+ String volumeUuid = UUID.randomUUID().toString();
+ when(volumeVO.getUuid()).thenReturn(volumeUuid);
+
+
doNothing().when(resourceLimitService).incrementResourceCount(accountId,
Resource.ResourceType.volume);
+
doNothing().when(resourceLimitService).incrementResourceCount(accountId,
Resource.ResourceType.primary_storage, virtualSize);
+
+ DataStore dataStore = mock(DataStore.class);
+ PrimaryDataStoreDriver driver = mock(PrimaryDataStoreDriver.class);
+ when(dataStore.getDriver()).thenReturn(driver);
+ when(dataStoreMgr.getPrimaryDataStore(poolId)).thenReturn(dataStore);
+
+ VolumeResponse response = mock(VolumeResponse.class);
+
doReturn(response).when(responseGenerator).createVolumeResponse(ResponseObject.ResponseView.Full,
volumeVO);
+ try (MockedStatic<UsageEventUtils> ignored =
Mockito.mockStatic(UsageEventUtils.class);
+ MockedStatic<ActionEventUtils> ignoredtoo =
Mockito.mockStatic(ActionEventUtils.class)) {
+ volumeImportUnmanageManager.importVolume(cmd);
+ }
+
+ verify(driver).volumeManagementChanged(storagePoolVO, volumeVO, true);
+ }
+
@Test
public void testGetImageFormat() {
Assert.assertNull(volumeImportUnmanageManager.getImageFormat(null));
@@ -301,6 +357,9 @@ public class VolumeImportUnmanageManagerImplTest {
@Test
public void testListVolumesForImportInternal() {
+ // no primary storage driver available for this pool -> fall back to
the host command path
+ when(dataStoreMgr.getPrimaryDataStore(poolId)).thenReturn(null);
+
Pair<HostVO, String> hostAndLocalPath = mock(Pair.class);
doReturn(hostAndLocalPath).when(volumeImportUnmanageManager).findHostAndLocalPathForVolumeImport(storagePoolVO);
when(hostAndLocalPath.first()).thenReturn(hostVO);
@@ -370,7 +429,7 @@ public class VolumeImportUnmanageManagerImplTest {
when(volumeVO.getPath()).thenReturn(path);
VolumeOnStorageTO volumeOnStorageTO = new
VolumeOnStorageTO(hypervisorType, path, name, fullPath,
format, size, virtualSize);
-
doReturn(volumeOnStorageTO).when(volumeImportUnmanageManager).getVolumeOnStorageAndCheck(storagePoolVO,
path);
+
doReturn(volumeOnStorageTO).when(volumeImportUnmanageManager).getVolumeOnStorageAndCheck(storagePoolVO,
path, true);
doNothing().when(resourceLimitService).decrementResourceCount(accountId,
Resource.ResourceType.volume);
doNothing().when(resourceLimitService).decrementResourceCount(accountId,
Resource.ResourceType.primary_storage, virtualSize);
@@ -384,6 +443,31 @@ public class VolumeImportUnmanageManagerImplTest {
verify(volumeDao).update(eq(volumeId), any());
}
+ @Test
+ public void testUnmanageVolumeNotifiesStorageDriverOfUnmanage() {
+ when(volumeVO.getState()).thenReturn(Volume.State.Ready);
+ when(volumeVO.getPoolId()).thenReturn(poolId);
+ when(volumeVO.getInstanceId()).thenReturn(null);
+ when(volumeVO.getPath()).thenReturn(path);
+ VolumeOnStorageTO volumeOnStorageTO = new
VolumeOnStorageTO(hypervisorType, path, name, fullPath,
+ format, size, virtualSize);
+
doReturn(volumeOnStorageTO).when(volumeImportUnmanageManager).getVolumeOnStorageAndCheck(storagePoolVO,
path, true);
+
doNothing().when(resourceLimitService).decrementResourceCount(accountId,
Resource.ResourceType.volume);
+
doNothing().when(resourceLimitService).decrementResourceCount(accountId,
Resource.ResourceType.primary_storage, virtualSize);
+
+ DataStore dataStore = mock(DataStore.class);
+ PrimaryDataStoreDriver driver = mock(PrimaryDataStoreDriver.class);
+ when(dataStore.getDriver()).thenReturn(driver);
+ when(dataStoreMgr.getPrimaryDataStore(poolId)).thenReturn(dataStore);
+
+ try (MockedStatic<UsageEventUtils> ignored =
Mockito.mockStatic(UsageEventUtils.class);
+ MockedStatic<ActionEventUtils> ignoredtoo =
Mockito.mockStatic(ActionEventUtils.class)) {
+ volumeImportUnmanageManager.unmanageVolume(volumeId);
+ }
+
+ verify(driver).volumeManagementChanged(storagePoolVO, volumeVO, false);
+ }
+
@Test
public void testUnmanageVolumeNotExist() {
try {
diff --git
a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
index 30bc769d5d4..b6376201c73 100644
---
a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
+++
b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
@@ -63,6 +63,7 @@ import
org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
+import org.apache.cloudstack.storage.volume.VolumeImportUnmanageService;
import org.jetbrains.annotations.NotNull;
import org.junit.After;
import org.apache.cloudstack.storage.volume.VolumeOnStorageTO;
@@ -253,6 +254,8 @@ public class UnmanagedVMsManagerImplTest {
private ImportVmTasksManager importVmTasksManager;
@Mock
private SnapshotDao snapshotDao;
+ @Mock
+ private VolumeImportUnmanageService volumeImportUnmanageService;
@Mock
private VMInstanceVO virtualMachine;
@@ -1015,6 +1018,8 @@ public class UnmanagedVMsManagerImplTest {
DiskProfile diskProfile = Mockito.mock(DiskProfile.class);
when(volumeManager.allocateRawVolume(any(), any(), any(), any(),
any(), any(), any(), any(), any(), any(), anyBoolean()))
.thenReturn(diskProfile);
+ when(volumeManager.updateImportedVolume(any(), any(), any(), any(),
anyLong(), anyLong(), any(), anyString(), any(), any(), any()))
+ .thenReturn(diskProfile);
Map<Volume, StoragePool> storage = new HashMap<>();
VolumeVO volume = Mockito.mock(VolumeVO.class);
StoragePoolVO storagePool = Mockito.mock(StoragePoolVO.class);
@@ -1045,6 +1050,9 @@ public class UnmanagedVMsManagerImplTest {
verify(volumeManager).updateImportedVolume(any(), any(), any(), any(),
anyLong(), anyLong(), Mockito.eq(poolType),
Mockito.eq(diskPath), Mockito.isNull(), any(),
Mockito.eq(expectedFormat));
+ // the storage driver must be told the volume is now imported, so e.g.
StorPool can tag it
+
verify(volumeImportUnmanageService).notifyStorageDriverOfVolumeManagementChange(storagePool,
volume, true);
+
// the plan must be confined to the pod and cluster of the pool the
caller asked for, so that the
// volume check cannot land on a host in another cluster that has no
access to that pool
ArgumentCaptor<DeploymentPlan> planCaptor =
ArgumentCaptor.forClass(DeploymentPlan.class);
diff --git a/test/integration/plugins/storpool/TestStorPoolImportVolume.py
b/test/integration/plugins/storpool/TestStorPoolImportVolume.py
new file mode 100644
index 00000000000..aac0b5d2ba4
--- /dev/null
+++ b/test/integration/plugins/storpool/TestStorPoolImportVolume.py
@@ -0,0 +1,610 @@
+# 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.
+""" Smoke test for the StorPool-specific
listVolumesForImport/importVolume/unmanageVolume flow.
+
+Exercises the StorPool primary storage driver fast path
(StorPoolPrimaryDataStoreDriver
+.listVolumesForImport, used instead of dispatching a command to a KVM host)
and verifies
+that importVolume tags the underlying StorPool volume with "cs"/"uuid" (and
the disk
+offering's StorPool QoS tier, if set), while unmanageVolume clears those tags
again so the
+volume is reported again by listVolumesForImport.
+
+Also covers importing a volume/VM whose disk offering carries the StorPool QoS
class detail
+(SP_QOSCLASS), which must end up as the "qc" tag on the StorPool volume.
+
+The importVm tests use a volume created from a snapshot of a real VM's ROOT
disk (rather than
+a blank data volume), so the imported VM actually has an OS on it, and verify
over SSH that it
+boots - not just that the import API calls succeeded.
+"""
+import math
+import uuid
+
+from marvin.cloudstackAPI import unmanageVolume, listVolumesForImport,
importVolume, importVm
+from marvin.cloudstackTestCase import cloudstackTestCase, unittest
+from marvin.codes import FAILED
+from marvin.lib.base import (Account,
+ DiskOffering,
+ ServiceOffering,
+ Snapshot,
+ Tag,
+ VirtualMachine,
+ Volume)
+from marvin.lib.common import (get_domain,
+ get_zone,
+ get_suitable_test_template,
+ list_clusters,
+ list_networks,
+ list_storage_pools)
+from nose.plugins.attrib import attr
+from storpool import spapi, sptypes
+
+from sp_util import TestData, StorPoolHelper
+
+get_suitable_test_template.__test__ = False
+
+_multiprocess_shared_ = True
+
+
+class TestStorPoolImportVolume(cloudstackTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ testClient = super(TestStorPoolImportVolume, cls).getClsTestClient()
+ cls.apiclient = testClient.getApiClient()
+ cls.testdata = testClient.getParsedTestDataConfig()
+ cls.hypervisor = testClient.getHypervisorInfo()
+ if cls.hypervisor.lower() != "kvm":
+ raise unittest.SkipTest("StorPool import/unmanage volume is only
available for KVM")
+
+ config = cls.getClsConfig()
+ zone = config.zones[0]
+ StorPoolHelper.logger = cls
+ cls.helper = StorPoolHelper()
+ sp_pools = cls.helper.get_pool(zone)
+ if sp_pools is None:
+ raise unittest.SkipTest("This test needs at least 2 StorPool
primary storages configured in the zone")
+ cls.spapi = spapi.Api(host=zone.spEndpoint, port=zone.spEndpointPort,
auth=zone.spAuthToken, multiCluster=True)
+
+ sp_testdata = TestData().testdata
+ cls.pool_tag = sp_testdata[TestData.sp_template_1]["tags"]
+
+ storage_pools = list_storage_pools(cls.apiclient,
name=sp_pools[0]["name"])
+ if not storage_pools:
+ raise unittest.SkipTest("StorPool primary storage %s is not
registered in CloudStack yet" % sp_pools[0]["name"])
+ cls.primary_storage = storage_pools[0]
+
+ cls.domain = get_domain(cls.apiclient)
+ cls.zone = get_zone(cls.apiclient)
+ cls._cleanup = []
+
+ cls.account = Account.create(
+ cls.apiclient,
+ cls.testdata["account"],
+ domainid=cls.domain.id
+ )
+ cls._cleanup.append(cls.account)
+
+ cls.disk_offering = DiskOffering.create(
+ cls.apiclient,
+ sp_testdata[TestData.diskOfferingSmall]
+ )
+ cls._cleanup.append(cls.disk_offering)
+
+ cls.qos_tag = cls.pool_tag
+ cls.disk_offering_qos = DiskOffering.create(
+ cls.apiclient,
+ sp_testdata[TestData.diskOfferingSmall],
+ details=[{"SP_QOSCLASS": cls.qos_tag}]
+ )
+ cls._cleanup.append(cls.disk_offering_qos)
+
+ service_offering_data = dict(cls.testdata["service_offerings"]["tiny"])
+ service_offering_data["name"] = "StorPoolImportTest-%s" % uuid.uuid4()
+ service_offering_data["tags"] = cls.pool_tag
+ cls.service_offering = ServiceOffering.create(cls.apiclient,
service_offering_data)
+ cls._cleanup.append(cls.service_offering)
+
+ service_offering_qos_data =
dict(cls.testdata["service_offerings"]["tiny"])
+ service_offering_qos_data["name"] = "StorPoolImportQosTest-%s" %
uuid.uuid4()
+ service_offering_qos_data["tags"] = cls.pool_tag
+ cls.service_offering_qos = ServiceOffering.create(
+ cls.apiclient,
+ service_offering_qos_data,
+ diskofferingid=cls.disk_offering_qos.id
+ )
+ cls._cleanup.append(cls.service_offering_qos)
+
+ template = get_suitable_test_template(
+ cls.apiclient,
+ cls.primary_storage.zoneid,
+ cls.testdata["ostype"],
+ cls.hypervisor
+ )
+ if template == FAILED:
+ assert False, "get_suitable_test_template() failed to return a
template"
+
+ cls.virtual_machine = VirtualMachine.create(
+ cls.apiclient,
+ cls.testdata["virtual_machine"],
+ templateid=template.id,
+ zoneid=cls.primary_storage.zoneid,
+ accountid=cls.account.name,
+ domainid=cls.account.domainid,
+ serviceofferingid=cls.service_offering.id,
+ mode=cls.zone.networktype
+ )
+ cls._cleanup.append(cls.virtual_machine)
+ cls.virtual_machine.stop(cls.apiclient, forced=True)
+
+ root_volume = Volume.list(cls.apiclient,
virtualmachineid=cls.virtual_machine.id,
+ type="ROOT", listall=True)[0]
+ cls.root_snapshot = Snapshot.create(
+ cls.apiclient,
+ volume_id=root_volume.id,
+ account=cls.account.name,
+ domainid=cls.account.domainid
+ )
+ cls._cleanup.append(cls.root_snapshot)
+
+ disk_offering_boot_data = dict(sp_testdata[TestData.diskOfferingSmall])
+ disk_offering_boot_data["name"] = "StorPoolImportBootTest"
+ disk_offering_boot_data["disksize"] =
int(math.ceil(cls.root_snapshot.virtualsize / (1024.0 ** 3)))
+ cls.disk_offering_boot = DiskOffering.create(cls.apiclient,
disk_offering_boot_data)
+ cls._cleanup.append(cls.disk_offering_boot)
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestStorPoolImportVolume, cls).tearDownClass()
+
+ def setUp(self):
+ self.apiclient = self.testClient.getApiClient()
+ self.cleanup = []
+
+ def storpool_volume(self, path):
+ name = path.split("/")[3]
+ volumes = self.spapi.volumeList(volumeName="~" + name)
+ self.assertTrue(len(volumes) > 0, "Could not find StorPool volume for
path %s" % path)
+ return volumes[0]
+
+ def create_storpool_volume(self, diskname, disk_offering_id):
+ return Volume.create(
+ self.apiclient,
+ {"diskname": diskname},
+ zoneid=self.primary_storage.zoneid,
+ account=self.account.name,
+ domainid=self.account.domainid,
+ diskofferingid=disk_offering_id,
+ storageid=self.primary_storage.id
+ )
+
+ def create_bootable_storpool_volume(self, diskname):
+ """ Clones cls.root_snapshot (a snapshot of a real VM's ROOT disk)
into a new volume, so
+ it has an actual OS on it - unlike create_storpool_volume()'s blank
data volume, this is
+ usable as a VM's ROOT disk after import. storageid can't be combined
with snapshotid
+ (CreateVolumeCmd.getStorageId() rejects that combination), so
disk_offering_boot's StorPool
+ tag is what pins the resulting volume onto cls.primary_storage
instead. """
+ return Volume.create(
+ self.apiclient,
+ {"diskname": diskname},
+ zoneid=self.primary_storage.zoneid,
+ account=self.account.name,
+ domainid=self.account.domainid,
+ diskofferingid=self.disk_offering_boot.id,
+ snapshotid=self.root_snapshot.id
+ )
+
+ def assert_vm_boots_over_ssh(self, vm):
+ """ Starts the (powered-off, just-imported) VM and runs a command over
SSH, to prove it
+ actually boots the cloned OS disk - not just that the import API calls
succeeded. """
+ vm.start(self.apiclient)
+ ssh_client = vm.get_ssh_client(ipaddress=vm.ipaddress, retries=30)
+ result = ssh_client.execute("echo storpool-import-boot-check")
+ self.assertEqual(["storpool-import-boot-check"], result,
+ "The imported VM should boot and be reachable over
SSH")
+
+ def create_sp_volume_directly(self, tags):
+ """ Creates a volume on StorPool without going through CloudStack at
all - simulates a
+ volume that pre-exists on the pool (e.g. provisioned by external
tooling), the realistic
+ "import pre-existing storage" scenario, as opposed to
create_storpool_volume()'s
+ CloudStack-created-then-unmanaged volume. Returns (sp_volume_name,
path); the caller must
+ delete the volume directly via self.spapi.volumeDelete() if it is
never imported, since
+ self.cleanup only drains CloudStack-tracked marvin.lib.base.Volume
instances. """
+ volume_desc = sptypes.VolumeCreateDesc(
+ size=1 * 1024 * 1024 * 1024,
+ template=self.pool_tag,
+ tags=tags
+ )
+ created = self.spapi.volumeCreate(volume_desc)
+ sp_volume_name = created.name
+ path = "/dev/storpool-byid/%s" % sp_volume_name.lstrip("~")
+ return sp_volume_name, path
+
+ @attr(tags=["advanced", "basic", "sg"], required_hardware=False)
+ def test_01_import_volume_tags_storpool_volume_with_cs_and_uuid(self):
+ """ importVolume should tag the StorPool volume with cs/uuid (and
unmanageVolume
+ should clear those tags again), so a volume can round-trip between
CloudStack-managed
+ and available-for-import. """
+
+ volume = self.create_storpool_volume("StorPoolImportTest",
self.disk_offering.id)
+
+ sp_volume = self.storpool_volume(volume.path)
+ self.assertEqual("volume", sp_volume.tags.get("cs"),
+ "A CloudStack-managed volume should carry the
cs=volume StorPool tag")
+
+ cmd = unmanageVolume.unmanageVolumeCmd()
+ cmd.id = volume.id
+ self.apiclient.unmanageVolume(cmd)
+
+ sp_volume = self.storpool_volume(volume.path)
+ self.assertFalse(sp_volume.tags.get("cs"),
+ "unmanageVolume should clear the StorPool cs tag")
+
+ cmd = listVolumesForImport.listVolumesForImportCmd()
+ cmd.storageid = self.primary_storage.id
+ volumes_for_import = self.apiclient.listVolumesForImport(cmd)
+ self.assertTrue(isinstance(volumes_for_import, list),
+ "listVolumesForImport should return a valid list")
+ matching = [v for v in volumes_for_import if v.path == volume.path]
+ self.assertEqual(1, len(matching),
+ "The unmanaged volume should be listed as available
for import")
+ self.assertEqual("QCOW2", matching[0].format,
+ "StorPool volumes are always reported as QCOW2")
+
+ cmd = importVolume.importVolumeCmd()
+ cmd.storageid = self.primary_storage.id
+ cmd.path = volume.path
+ cmd.diskofferingid = self.disk_offering.id
+ imported = self.apiclient.importVolume(cmd)
+ self.assertIsNotNone(imported, "importVolume should return the
imported volume")
+ self.cleanup.append(Volume(imported.__dict__))
+
+ sp_volume = self.storpool_volume(volume.path)
+ self.assertEqual("volume", sp_volume.tags.get("cs"),
+ "importVolume should re-tag the StorPool volume as
cs=volume")
+ self.assertEqual(imported.id, sp_volume.tags.get("uuid"),
+ "importVolume should tag the StorPool volume with
the new CloudStack volume uuid")
+
+ cmd = listVolumesForImport.listVolumesForImportCmd()
+ cmd.storageid = self.primary_storage.id
+ volumes_for_import = self.apiclient.listVolumesForImport(cmd)
+ matching = [v for v in (volumes_for_import or []) if v.path ==
volume.path]
+ self.assertEqual(0, len(matching),
+ "A re-imported volume should no longer be listed as
available for import")
+
+ @attr(tags=["advanced", "basic", "sg"], required_hardware=False)
+ def test_02_list_volumes_for_import_by_path(self):
+ """ listVolumesForImport with a specific path should look up that one
StorPool volume
+ directly (StorPoolPrimaryDataStoreDriver's single-volume Describe
lookup), instead of
+ browsing/filtering the whole pool. """
+
+ volume = self.create_storpool_volume("StorPoolImportPathTest",
self.disk_offering.id)
+
+ cmd = unmanageVolume.unmanageVolumeCmd()
+ cmd.id = volume.id
+ self.apiclient.unmanageVolume(cmd)
+
+ cmd = listVolumesForImport.listVolumesForImportCmd()
+ cmd.storageid = self.primary_storage.id
+ cmd.path = volume.path
+ volumes_for_import = self.apiclient.listVolumesForImport(cmd)
+ self.assertTrue(isinstance(volumes_for_import, list) and
len(volumes_for_import) == 1,
+ "listVolumesForImport with a path should return
exactly that one volume")
+ self.assertEqual(volume.path, volumes_for_import[0].path,
+ "listVolumesForImport with a path should return the
matching volume")
+ self.assertEqual("QCOW2", volumes_for_import[0].format,
+ "StorPool volumes are always reported as QCOW2")
+
+ cmd = importVolume.importVolumeCmd()
+ cmd.storageid = self.primary_storage.id
+ cmd.path = volume.path
+ cmd.diskofferingid = self.disk_offering.id
+ imported = self.apiclient.importVolume(cmd)
+ self.assertIsNotNone(imported, "importVolume should return the
imported volume")
+ self.cleanup.append(Volume(imported.__dict__))
+
+ @attr(tags=["advanced", "basic", "sg"], required_hardware=False)
+ def test_03_import_vm_from_shared_storage_on_storpool(self):
+ """ importVm(importSource=SHARED) against a disk already sitting on a
StorPool pool
+ should record the disk as the VM's ROOT volume, and tag the StorPool
volume with
+ cs/uuid/cvm, since the disk is now owned by (and attached to) a
CloudStack VM. The disk
+ is a clone of a real VM's ROOT disk, so the imported VM is actually
bootable, and is
+ verified to boot over SSH. """
+
+ volume = self.create_bootable_storpool_volume("StorPoolImportVmTest")
+
+ cmd = unmanageVolume.unmanageVolumeCmd()
+ cmd.id = volume.id
+ self.apiclient.unmanageVolume(cmd)
+
+ networks = list_networks(self.apiclient, account=self.account.name,
domainid=self.account.domainid)
+ self.assertTrue(isinstance(networks, list) and len(networks) > 0,
"account should have a network")
+ clusters = list_clusters(self.apiclient,
zoneid=self.primary_storage.zoneid)
+ self.assertTrue(isinstance(clusters, list) and len(clusters) > 0,
"zone should have a cluster")
+
+ cmd = importVm.importVmCmd()
+ cmd.zoneid = self.primary_storage.zoneid
+ cmd.hypervisor = "KVM"
+ cmd.importsource = "shared"
+ cmd.clusterid = clusters[0].id
+ cmd.name = "StorPoolImportVm-%s" % uuid.uuid4()
+ cmd.serviceofferingid = self.service_offering.id
+ cmd.diskpath = volume.path
+ cmd.networkid = networks[0].id
+ cmd.storageid = self.primary_storage.id
+ cmd.account = self.account.name
+ cmd.domainid = self.account.domainid
+ imported = self.apiclient.importVm(cmd)
+ self.assertIsNotNone(imported, "importVm should return the imported
VM")
+ imported_vm = VirtualMachine(imported.__dict__, self.testdata)
+
+ try:
+ root_volumes = Volume.list(self.apiclient,
virtualmachineid=imported.id, type="ROOT", listall=True)
+ self.assertTrue(isinstance(root_volumes, list) and
len(root_volumes) > 0,
+ "the imported VM should have a ROOT volume")
+ root_volume = root_volumes[0]
+ self.assertEqual(volume.path, root_volume.path,
+ "the imported ROOT volume should be the disk
that was unmanaged")
+
+ sp_volume = self.storpool_volume(root_volume.path)
+ self.assertEqual("volume", sp_volume.tags.get("cs"),
+ "importVm should tag the StorPool volume as
cs=volume")
+ self.assertEqual(root_volume.id, sp_volume.tags.get("uuid"),
+ "importVm should tag the StorPool volume with
the new CloudStack volume uuid")
+ self.assertEqual(imported.id, sp_volume.tags.get("cvm"),
+ "importVm should tag the StorPool volume with
the owning VM's uuid")
+
+ self.assert_vm_boots_over_ssh(imported_vm)
+ finally:
+ imported_vm.delete(self.apiclient, expunge=True)
+
+ @attr(tags=["advanced", "basic", "sg"], required_hardware=False)
+ def test_04_import_volume_tags_storpool_volume_with_qos_class(self):
+ """ importVolume should tag the StorPool volume with "qc" set to the
QOS_CLASS
+ (SP_QOSCLASS) detail configured on the disk offering used for the
import. """
+
+ volume = self.create_storpool_volume("StorPoolImportQosTest",
self.disk_offering_qos.id)
+
+ cmd = unmanageVolume.unmanageVolumeCmd()
+ cmd.id = volume.id
+ self.apiclient.unmanageVolume(cmd)
+
+ cmd = importVolume.importVolumeCmd()
+ cmd.storageid = self.primary_storage.id
+ cmd.path = volume.path
+ cmd.diskofferingid = self.disk_offering_qos.id
+ imported = self.apiclient.importVolume(cmd)
+ self.assertIsNotNone(imported, "importVolume should return the
imported volume")
+ self.cleanup.append(Volume(imported.__dict__))
+
+ sp_volume = self.storpool_volume(volume.path)
+ self.assertEqual(self.qos_tag, sp_volume.tags.get("qc"),
+ "importVolume should tag the StorPool volume with
the disk offering's "
+ "QOS_CLASS (SP_QOSCLASS) detail")
+
+ @attr(tags=["advanced", "basic", "sg"], required_hardware=False)
+ def test_05_import_vm_from_shared_storage_on_storpool_with_qos_class(self):
+ """ importVm(importSource=SHARED) using a service offering linked to a
disk offering
+ that has a QOS_CLASS (SP_QOSCLASS) detail should tag the imported ROOT
volume's
+ StorPool volume with cs/uuid/cvm, as well as "qc" set to that
QOS_CLASS value. The disk
+ is a clone of a real VM's ROOT disk, so the imported VM is actually
bootable, and is
+ verified to boot over SSH. """
+
+ volume =
self.create_bootable_storpool_volume("StorPoolImportVmQosTest")
+
+ cmd = unmanageVolume.unmanageVolumeCmd()
+ cmd.id = volume.id
+ self.apiclient.unmanageVolume(cmd)
+
+ networks = list_networks(self.apiclient, account=self.account.name,
domainid=self.account.domainid)
+ self.assertTrue(isinstance(networks, list) and len(networks) > 0,
"account should have a network")
+ clusters = list_clusters(self.apiclient,
zoneid=self.primary_storage.zoneid)
+ self.assertTrue(isinstance(clusters, list) and len(clusters) > 0,
"zone should have a cluster")
+
+ cmd = importVm.importVmCmd()
+ cmd.zoneid = self.primary_storage.zoneid
+ cmd.hypervisor = "KVM"
+ cmd.importsource = "shared"
+ cmd.clusterid = clusters[0].id
+ cmd.name = "StorPoolImportVmQos-%s" % uuid.uuid4()
+ cmd.serviceofferingid = self.service_offering_qos.id
+ cmd.diskpath = volume.path
+ cmd.networkid = networks[0].id
+ cmd.storageid = self.primary_storage.id
+ cmd.account = self.account.name
+ cmd.domainid = self.account.domainid
+ imported = self.apiclient.importVm(cmd)
+ self.assertIsNotNone(imported, "importVm should return the imported
VM")
+ imported_vm = VirtualMachine(imported.__dict__, self.testdata)
+
+ try:
+ root_volumes = Volume.list(self.apiclient,
virtualmachineid=imported.id, type="ROOT", listall=True)
+ self.assertTrue(isinstance(root_volumes, list) and
len(root_volumes) > 0,
+ "the imported VM should have a ROOT volume")
+ root_volume = root_volumes[0]
+ self.assertEqual(volume.path, root_volume.path,
+ "the imported ROOT volume should be the disk
that was unmanaged")
+
+ sp_volume = self.storpool_volume(root_volume.path)
+ self.assertEqual("volume", sp_volume.tags.get("cs"),
+ "importVm should tag the StorPool volume as
cs=volume")
+ self.assertEqual(root_volume.id, sp_volume.tags.get("uuid"),
+ "importVm should tag the StorPool volume with
the new CloudStack volume uuid")
+ self.assertEqual(imported.id, sp_volume.tags.get("cvm"),
+ "importVm should tag the StorPool volume with
the owning VM's uuid")
+ self.assertEqual(self.qos_tag, sp_volume.tags.get("qc"),
+ "importVm should tag the StorPool volume with
the linked disk offering's "
+ "QOS_CLASS (SP_QOSCLASS) detail")
+
+ self.assert_vm_boots_over_ssh(imported_vm)
+ finally:
+ imported_vm.delete(self.apiclient, expunge=True)
+
+ @attr(tags=["advanced", "basic", "sg"], required_hardware=False)
+ def test_06_list_volumes_for_import_by_path_excludes_managed_volume(self):
+ """ listVolumesForImport with a path should never report an already
CloudStack-managed
+ (StorPool "cs"-tagged) volume as available for import -
StorPoolPrimaryDataStoreDriver's
+ single-volume Describe lookup must apply the same "already managed"
exclusion the
+ browse-all case does, unlike the internal lookup unmanageVolume() uses
to find that same
+ volume (see test_01, which unmanages successfully, exercising the same
volume while it is
+ still managed, the way this test's path-lookup check does). """
+
+ volume =
self.create_storpool_volume("StorPoolListImportExcludesManagedTest",
self.disk_offering.id)
+ self.cleanup.append(volume)
+
+ sp_volume = self.storpool_volume(volume.path)
+ self.assertEqual("volume", sp_volume.tags.get("cs"),
+ "A CloudStack-managed volume should carry the
cs=volume StorPool tag")
+
+ cmd = listVolumesForImport.listVolumesForImportCmd()
+ cmd.storageid = self.primary_storage.id
+ cmd.path = volume.path
+ volumes_for_import = self.apiclient.listVolumesForImport(cmd)
+ matching = [v for v in (volumes_for_import or []) if v.path ==
volume.path]
+ self.assertEqual(0, len(matching),
+ "A still-managed volume should not be listed as
available for import, "
+ "even when looked up by its exact path")
+
+ @attr(tags=["advanced", "basic", "sg"], required_hardware=False)
+ def test_07_import_volume_created_directly_on_storpool(self):
+ """ A volume StorPool provisioned directly - one CloudStack never
created and so never
+ tagged cs/uuid on - should show up via listVolumesForImport and be
importable, exactly
+ like a CloudStack-created-then-unmanaged volume. Every other test here
only ever exercises
+ the latter (create via CloudStack, unmanage, re-import), never a
genuinely external volume,
+ which is the realistic "import pre-existing storage" scenario this
feature targets. """
+
+ sp_volume_name, path = self.create_sp_volume_directly({"createdby":
"storpool-direct-test"})
+
+ imported = None
+ try:
+ sp_volume = self.storpool_volume(path)
+ self.assertIsNone(sp_volume.tags.get("cs"),
+ "A volume StorPool created directly should not
carry CloudStack's cs tag")
+
+ cmd = listVolumesForImport.listVolumesForImportCmd()
+ cmd.storageid = self.primary_storage.id
+ volumes_for_import = self.apiclient.listVolumesForImport(cmd)
+ matching = [v for v in (volumes_for_import or []) if v.path ==
path]
+ self.assertEqual(1, len(matching),
+ "A volume created directly on StorPool should be
listed as "
+ "available for import")
+ self.assertEqual("QCOW2", matching[0].format,
+ "StorPool volumes are always reported as QCOW2")
+
+ cmd = importVolume.importVolumeCmd()
+ cmd.storageid = self.primary_storage.id
+ cmd.path = path
+ cmd.diskofferingid = self.disk_offering.id
+ imported = self.apiclient.importVolume(cmd)
+ self.assertIsNotNone(imported, "importVolume should return the
imported volume")
+ self.cleanup.append(Volume(imported.__dict__))
+
+ sp_volume = self.storpool_volume(path)
+ self.assertEqual("volume", sp_volume.tags.get("cs"),
+ "importVolume should tag the StorPool volume as
cs=volume")
+ self.assertEqual(imported.id, sp_volume.tags.get("uuid"),
+ "importVolume should tag the StorPool volume
with the new CloudStack volume uuid")
+ finally:
+ if imported is None:
+ self.spapi.volumeDelete(volumeName=sp_volume_name)
+
+ @attr(tags=["advanced", "basic", "sg"], required_hardware=False)
+ def
test_08_list_volumes_for_import_excludes_externally_cs_tagged_volume(self):
+ """ listVolumesForImport must exclude a volume tagged cs=volume
regardless of whether
+ CloudStack itself ever created or knew about it - the StorPool "cs"
tag alone is the
+ source of truth for "already managed", not a CloudStack DB record
(contrast test_07,
+ where the same kind of directly-created, CloudStack-unknown volume -
but untagged - is
+ correctly importable). This is checked browsing all volumes, by exact
path, and then by
+ actually attempting importVolume against the same disk - all three
must reject it. """
+
+ sp_volume_name, path = self.create_sp_volume_directly({"cs": "volume"})
+ try:
+ cmd = listVolumesForImport.listVolumesForImportCmd()
+ cmd.storageid = self.primary_storage.id
+ volumes_for_import = self.apiclient.listVolumesForImport(cmd)
+ matching = [v for v in (volumes_for_import or []) if v.path ==
path]
+ self.assertEqual(0, len(matching),
+ "A volume tagged cs=volume should not be listed
as available for "
+ "import, even though CloudStack has no record of
it")
+
+ cmd = listVolumesForImport.listVolumesForImportCmd()
+ cmd.storageid = self.primary_storage.id
+ cmd.path = path
+ volumes_for_import = self.apiclient.listVolumesForImport(cmd)
+ matching = [v for v in (volumes_for_import or []) if v.path ==
path]
+ self.assertEqual(0, len(matching),
+ "A volume tagged cs=volume should also be
excluded when looked up "
+ "by its exact path")
+
+ cmd = importVolume.importVolumeCmd()
+ cmd.storageid = self.primary_storage.id
+ cmd.path = path
+ cmd.diskofferingid = self.disk_offering.id
+ try:
+ self.apiclient.importVolume(cmd)
+ self.fail("importVolume should fail for a volume already
tagged cs=volume")
+ except Exception as e:
+ self.debug("importVolume correctly failed for a cs-tagged
volume: %s" % e)
+ finally:
+ self.spapi.volumeDelete(volumeName=sp_volume_name)
+
+ @attr(tags=["advanced", "basic", "sg"], required_hardware=False)
+ def test_09_unmanage_volume_clears_all_storpool_tags(self):
+ """ unmanageVolume should clear every StorPool tag CloudStack set -
cs, uuid, qc, cvm
+ and vc-policy - checked in isolation from a subsequent re-import
(contrast test_01,
+ which only asserts on cs/uuid, and only as part of an
unmanage-then-reimport round
+ trip). The volume is attached to cls.virtual_machine (so it picks up
the cvm tag) and
+ the VM is given a vc-policy resource tag (so the volume picks that up
too) before being
+ detached and unmanaged. CloudStack rejects unmanageVolume on a
still-attached volume, so
+ the volume has to be detached first anyway - the tags are checked as
cleared here too in
+ case that invariant ever changes. """
+
+ volume = self.create_storpool_volume("StorPoolUnmanageClearsTagsTest",
self.disk_offering_qos.id)
+
+ self.virtual_machine.attach_volume(self.apiclient, volume)
+ Tag.create(
+ self.apiclient,
+ resourceIds=self.virtual_machine.id,
+ resourceType='UserVm',
+ tags={'vc-policy': 'test'}
+ )
+
+ sp_volume = self.storpool_volume(volume.path)
+ self.assertEqual("volume", sp_volume.tags.get("cs"),
+ "A CloudStack-managed volume should carry the
cs=volume StorPool tag")
+ self.assertEqual(volume.id, sp_volume.tags.get("uuid"),
+ "A CloudStack-managed volume should carry its
CloudStack uuid as a StorPool tag")
+ self.assertEqual(self.qos_tag, sp_volume.tags.get("qc"),
+ "A CloudStack-managed volume on a QoS disk offering
should carry the qc tag")
+ self.assertEqual(self.virtual_machine.id, sp_volume.tags.get("cvm"),
+ "A volume attached to a VM should carry the owning
VM's uuid as the cvm tag")
+ self.assertEqual("test", sp_volume.tags.get("vc-policy"),
+ "A vc-policy tag set on the owning VM should be
pushed to the StorPool volume")
+
+ try:
+ self.virtual_machine.detach_volume(self.apiclient, volume)
+
+ cmd = unmanageVolume.unmanageVolumeCmd()
+ cmd.id = volume.id
+ self.apiclient.unmanageVolume(cmd)
+
+ sp_volume = self.storpool_volume(volume.path)
+ self.assertFalse(sp_volume.tags.get("cs"), "unmanageVolume should
clear the cs tag")
+ self.assertFalse(sp_volume.tags.get("uuid"), "unmanageVolume
should clear the uuid tag")
+ self.assertFalse(sp_volume.tags.get("qc"), "unmanageVolume should
clear the qc tag")
+ self.assertFalse(sp_volume.tags.get("cvm"), "unmanageVolume should
clear the cvm tag")
+ self.assertFalse(sp_volume.tags.get("vc-policy"), "unmanageVolume
should clear the vc-policy tag")
+ finally:
+ # The volume is unmanaged, so it is no longer a CloudStack-tracked
resource for
+ # self.cleanup to delete - remove it directly on StorPool instead.
+ self.spapi.volumeDelete(volumeName="~" + volume.path.split("/")[3])
diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py
index 2405d54162a..314e65acb9a 100755
--- a/tools/marvin/marvin/lib/base.py
+++ b/tools/marvin/marvin/lib/base.py
@@ -1150,7 +1150,8 @@ class Volume:
@classmethod
def create(cls, apiclient, services, zoneid=None, account=None,
- domainid=None, diskofferingid=None, projectid=None, size=None,
snapshotid=None):
+ domainid=None, diskofferingid=None, projectid=None, size=None,
snapshotid=None,
+ storageid=None):
"""Create Volume"""
cmd = createVolume.createVolumeCmd()
cmd.name = "-".join([services["diskname"], random_gen()])
@@ -1184,6 +1185,11 @@ class Volume:
if snapshotid:
cmd.snapshotid = snapshotid
+ if storageid:
+ cmd.storageid = storageid
+ elif "storageid" in services:
+ cmd.storageid = services["storageid"]
+
return Volume(apiclient.createVolume(cmd).__dict__)
def update(self, apiclient, **kwargs):