From 0acc66f51d7a0fddc59b7e416056a5e744e321dd Mon Sep 17 00:00:00 2001 From: Vishesh Date: Fri, 23 Jun 2023 13:46:22 +0530 Subject: [PATCH 1/6] server: Add check on host's status while deleting config drive on host cache (#7584) This PR adds a check on host's status. Without this if the agent is not in Up or Connecting state, expunging of a VM fails. Steps to reproduce: - Enable vm.configdrive.force.host.cache.use in Global Configuration. - Create a L2 network with config drive - Deploy a vm with the L2 network created in previous step - Stop the vm and destroy vm (not expunge it) - Stop the cloudstack-agent on the VM's host - Expunge the vm Fixes: #7428 --- .../cloud/network/element/ConfigDriveNetworkElement.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java b/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java index be536226034..83900ff2d43 100644 --- a/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java +++ b/server/src/main/java/com/cloud/network/element/ConfigDriveNetworkElement.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.network.element; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,6 +51,7 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.exception.UnsupportedServiceException; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; +import com.cloud.host.Status; import com.cloud.hypervisor.HypervisorGuruManager; import com.cloud.network.Network; import com.cloud.network.Network.Capability; @@ -573,6 +575,10 @@ public class ConfigDriveNetworkElement extends AdapterBase implements NetworkEle LOG.warn(String.format("Host %s appears to be unavailable, skipping deletion of config-drive ISO on host cache", hostId)); return false; } + if (!Arrays.asList(Status.Up, Status.Connecting).contains(hostVO.getStatus())) { + LOG.warn(String.format("Host status %s is not Up or Connecting, skipping deletion of config-drive ISO on host cache", hostId)); + return false; + } final HandleConfigDriveIsoAnswer answer = (HandleConfigDriveIsoAnswer) agentManager.easySend(hostId, configDriveIsoCommand); if (answer == null) { From c80920124731c2ff4f12f0178bbe1707b06a94e5 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Fri, 23 Jun 2023 07:22:15 -0300 Subject: [PATCH 2/6] Fix: Volumes on lost local storage cannot be removed (#7594) --- .../java/com/cloud/storage/dao/VolumeDao.java | 2 + .../com/cloud/storage/dao/VolumeDaoImpl.java | 11 ++++ .../cloud/resource/ResourceManagerImpl.java | 27 ++++++++++ .../cloud/storage/VolumeApiServiceImpl.java | 21 ++++++++ .../resource/ResourceManagerImplTest.java | 51 +++++++++++++++++++ .../storage/VolumeApiServiceImplTest.java | 18 ++++++- 6 files changed, 129 insertions(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java index 2001cf05ab9..3cdaa3b05ad 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java @@ -147,4 +147,6 @@ public interface VolumeDao extends GenericDao, StateDao findByDiskOfferingId(long diskOfferingId); VolumeVO getInstanceRootVolume(long instanceId); + + void updateAndRemoveVolume(VolumeVO volume); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index 3c865bac663..eb3206fc42e 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -766,4 +766,15 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol sc.setParameters("vType", Volume.Type.ROOT); return findOneBy(sc); } + + @Override + public void updateAndRemoveVolume(VolumeVO volume) { + if (volume.getState() != Volume.State.Destroy) { + volume.setState(Volume.State.Destroy); + volume.setPoolId(null); + volume.setInstanceId(null); + update(volume.getId(), volume); + remove(volume.getId()); + } + } } diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index 34629082748..d552c12e1a7 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -38,6 +38,9 @@ import javax.naming.ConfigurationException; import com.cloud.exception.StorageConflictException; import com.cloud.exception.StorageUnavailableException; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants; @@ -294,6 +297,8 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, private UserVmDetailsDao userVmDetailsDao; @Inject private AnnotationDao annotationDao; + @Inject + private VolumeDao volumeDao; private final long _nodeId = ManagementServerNode.getManagementServerId(); @@ -979,6 +984,7 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, final Long poolId = pool.getPoolId(); final StoragePoolVO storagePool = _storagePoolDao.findById(poolId); if (storagePool.isLocal() && isForceDeleteStorage) { + destroyLocalStoragePoolVolumes(poolId); storagePool.setUuid(null); storagePool.setClusterId(null); _storagePoolDao.update(poolId, storagePool); @@ -1011,6 +1017,27 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, return true; } + private void addVolumesToList(List volumes, List volumesToAdd) { + if (CollectionUtils.isNotEmpty(volumesToAdd)) { + volumes.addAll(volumesToAdd); + } + } + + protected void destroyLocalStoragePoolVolumes(long poolId) { + List rootDisks = volumeDao.findByPoolId(poolId); + List dataVolumes = volumeDao.findByPoolId(poolId, Volume.Type.DATADISK); + + List volumes = new ArrayList<>(); + addVolumesToList(volumes, rootDisks); + addVolumesToList(volumes, dataVolumes); + + if (CollectionUtils.isNotEmpty(volumes)) { + for (VolumeVO volume : volumes) { + volumeDao.updateAndRemoveVolume(volume); + } + } + } + /** * Returns true if host can be deleted.
* A host can be deleted either if it is in Maintenance or "Degraded" state. diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 646b81960d6..fd234b24794 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -36,6 +36,7 @@ import javax.inject.Inject; import org.apache.cloudstack.api.ApiConstants.IoDriverPolicy; import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd; import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd; @@ -88,6 +89,7 @@ import org.apache.cloudstack.storage.command.AttachAnswer; import org.apache.cloudstack.storage.command.AttachCommand; import org.apache.cloudstack.storage.command.DettachCommand; import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; @@ -273,6 +275,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic @Inject private PrimaryDataStoreDao _storagePoolDao; @Inject + private ImageStoreDao imageStoreDao; + @Inject private DiskOfferingDao _diskOfferingDao; @Inject private ServiceOfferingDao _serviceOfferingDao; @@ -1619,6 +1623,12 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } private void expungeVolumesInPrimaryOrSecondary(VolumeVO volume, DataStoreRole role) throws InterruptedException, ExecutionException { + if (!canAccessVolumeStore(volume, role)) { + s_logger.debug(String.format("Cannot access the storage pool with role: %s " + + "for the volume: %s, skipping expunge from storage", + role.name(), volume.getName())); + return; + } VolumeInfo volOnStorage = volFactory.getVolume(volume.getId(), role); if (volOnStorage != null) { s_logger.info("Expunging volume " + volume.getId() + " from " + role + " data store"); @@ -1638,6 +1648,17 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } } } + + private boolean canAccessVolumeStore(VolumeVO volume, DataStoreRole role) { + if (volume == null) { + throw new CloudRuntimeException("No volume given, cannot check access to volume store"); + } + InternalIdentity pool = role == DataStoreRole.Primary ? + _storagePoolDao.findById(volume.getPoolId()) : + imageStoreDao.findById(volume.getPoolId()); + return pool != null; + } + /** * Clean volumes cache entries (if they exist). */ diff --git a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java index 1793456ab91..a7ddd16462e 100644 --- a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java +++ b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java @@ -34,9 +34,13 @@ import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.UUID; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; import org.apache.cloudstack.api.command.admin.host.CancelHostAsDegradedCmd; import org.apache.cloudstack.api.command.admin.host.DeclareHostAsDegradedCmd; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -98,6 +102,8 @@ public class ResourceManagerImplTest { private VMInstanceDao vmInstanceDao; @Mock private ConfigurationDao configurationDao; + @Mock + private VolumeDao volumeDao; @Spy @InjectMocks @@ -119,6 +125,13 @@ public class ResourceManagerImplTest { @Mock private GetVncPortCommand getVncPortCommandVm2; + @Mock + private VolumeVO rootDisk1; + @Mock + private VolumeVO rootDisk2; + @Mock + private VolumeVO dataDisk; + @Mock private Connection sshConnection; @@ -138,6 +151,10 @@ public class ResourceManagerImplTest { private static String vm2VncAddress = "10.2.2.2"; private static int vm2VncPort = 5901; + private static long poolId = 1L; + private List rootDisks; + private List dataDisks; + @Before public void setup() throws Exception { MockitoAnnotations.initMocks(this); @@ -179,6 +196,11 @@ public class ResourceManagerImplTest { willReturn(new SSHCmdHelper.SSHCmdResult(0,"","")); when(configurationDao.getValue(ResourceManager.KvmSshToAgentEnabled.key())).thenReturn("true"); + + rootDisks = Arrays.asList(rootDisk1, rootDisk2); + dataDisks = Collections.singletonList(dataDisk); + when(volumeDao.findByPoolId(poolId)).thenReturn(rootDisks); + when(volumeDao.findByPoolId(poolId, Volume.Type.DATADISK)).thenReturn(dataDisks); } @Test @@ -527,4 +549,33 @@ public class ResourceManagerImplTest { return new HostVO(1L, "host01", Host.Type.Routing, "192.168.1.1", "255.255.255.0", null, null, null, null, null, null, null, null, null, null, UUID.randomUUID().toString(), hostStatus, "1.0", null, null, 1L, null, 0, 0, null, 0, null); } + + @Test + public void testDestroyLocalStoragePoolVolumesBothRootDisksAndDataDisks() { + resourceManager.destroyLocalStoragePoolVolumes(poolId); + verify(volumeDao, times(rootDisks.size() + dataDisks.size())) + .updateAndRemoveVolume(any(VolumeVO.class)); + } + + @Test + public void testDestroyLocalStoragePoolVolumesOnlyRootDisks() { + when(volumeDao.findByPoolId(poolId, Volume.Type.DATADISK)).thenReturn(null); + resourceManager.destroyLocalStoragePoolVolumes(poolId); + verify(volumeDao, times(rootDisks.size())).updateAndRemoveVolume(any(VolumeVO.class)); + } + + @Test + public void testDestroyLocalStoragePoolVolumesOnlyDataDisks() { + when(volumeDao.findByPoolId(poolId)).thenReturn(null); + resourceManager.destroyLocalStoragePoolVolumes(poolId); + verify(volumeDao, times(dataDisks.size())).updateAndRemoveVolume(any(VolumeVO.class)); + } + + @Test + public void testDestroyLocalStoragePoolVolumesNoDisks() { + when(volumeDao.findByPoolId(poolId)).thenReturn(null); + when(volumeDao.findByPoolId(poolId, Volume.Type.DATADISK)).thenReturn(null); + resourceManager.destroyLocalStoragePoolVolumes(poolId); + verify(volumeDao, never()).updateAndRemoveVolume(any(VolumeVO.class)); + } } diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index 16c7887ef91..e5b85c829e4 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -52,6 +52,8 @@ import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext; import org.apache.cloudstack.framework.jobs.AsyncJobManager; import org.apache.cloudstack.framework.jobs.dao.AsyncJobJoinMapDao; import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; +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.datastore.db.VolumeDataStoreDao; @@ -143,6 +145,12 @@ public class VolumeApiServiceImplTest { @Mock private PrimaryDataStoreDao primaryDataStoreDaoMock; @Mock + private ImageStoreDao imageStoreDao; + @Mock + private ImageStoreVO imageStoreVO; + @Mock + private StoragePoolVO storagePoolVO; + @Mock private VMSnapshotDao _vmSnapshotDao; @Mock private AsyncJobManager _jobMgr; @@ -214,6 +222,7 @@ public class VolumeApiServiceImplTest { private long volumeMockId = 12313l; private long vmInstanceMockId = 1123l; private long volumeSizeMock = 456789921939l; + private static long imageStoreId = 10L; private String projectMockUuid = "projectUuid"; private long projecMockId = 13801801923810L; @@ -239,6 +248,10 @@ public class VolumeApiServiceImplTest { Mockito.when(storagePoolMock.getId()).thenReturn(storagePoolMockId); + Mockito.when(volumeVoMock.getPoolId()).thenReturn(storagePoolMockId); + Mockito.when(imageStoreDao.findById(imageStoreId)).thenReturn(imageStoreVO); + Mockito.when(primaryDataStoreDaoMock.findById(storagePoolMockId)).thenReturn(storagePoolVO); + volumeApiServiceImpl._gson = GsonHelper.getGsonLogger(); // mock caller context @@ -914,7 +927,7 @@ public class VolumeApiServiceImplTest { @Test public void expungeVolumesInSecondaryStorageIfNeededTestVolumeNotFoundInSecondaryStorage() throws InterruptedException, ExecutionException { Mockito.lenient().doReturn(asyncCallFutureVolumeapiResultMock).when(volumeServiceMock).expungeVolumeAsync(volumeInfoMock); - Mockito.doReturn(null).when(volumeDataFactoryMock).getVolume(volumeMockId, DataStoreRole.Image); + Mockito.lenient().doReturn(null).when(imageStoreDao).findById(imageStoreId); Mockito.lenient().doNothing().when(resourceLimitServiceMock).decrementResourceCount(accountMockId, ResourceType.secondary_storage, volumeSizeMock); Mockito.lenient().doReturn(accountMockId).when(volumeInfoMock).getAccountId(); Mockito.lenient().doReturn(volumeSizeMock).when(volumeInfoMock).getSize(); @@ -933,6 +946,7 @@ public class VolumeApiServiceImplTest { Mockito.doNothing().when(resourceLimitServiceMock).decrementResourceCount(accountMockId, ResourceType.secondary_storage, volumeSizeMock); Mockito.doReturn(accountMockId).when(volumeInfoMock).getAccountId(); Mockito.doReturn(volumeSizeMock).when(volumeInfoMock).getSize(); + Mockito.doReturn(imageStoreId).when(volumeVoMock).getPoolId(); volumeApiServiceImpl.expungeVolumesInSecondaryStorageIfNeeded(volumeVoMock); @@ -948,6 +962,7 @@ public class VolumeApiServiceImplTest { Mockito.lenient().doNothing().when(resourceLimitServiceMock).decrementResourceCount(accountMockId, ResourceType.secondary_storage, volumeSizeMock); Mockito.lenient().doReturn(accountMockId).when(volumeInfoMock).getAccountId(); Mockito.lenient().doReturn(volumeSizeMock).when(volumeInfoMock).getSize(); + Mockito.doReturn(imageStoreId).when(volumeVoMock).getPoolId(); Mockito.doThrow(InterruptedException.class).when(asyncCallFutureVolumeapiResultMock).get(); @@ -962,6 +977,7 @@ public class VolumeApiServiceImplTest { Mockito.lenient().doNothing().when(resourceLimitServiceMock).decrementResourceCount(accountMockId, ResourceType.secondary_storage, volumeSizeMock); Mockito.lenient().doReturn(accountMockId).when(volumeInfoMock).getAccountId(); Mockito.lenient().doReturn(volumeSizeMock).when(volumeInfoMock).getSize(); + Mockito.doReturn(imageStoreId).when(volumeVoMock).getPoolId(); Mockito.doThrow(ExecutionException.class).when(asyncCallFutureVolumeapiResultMock).get(); From faaf72b1a4a4f242e16439763ec0ed34b8adb1fa Mon Sep 17 00:00:00 2001 From: slavkap <51903378+slavkap@users.noreply.github.com> Date: Mon, 26 Jun 2023 12:24:51 +0300 Subject: [PATCH 3/6] Volume encryption support for StorPool plug-in (#7539) Supported Virtual machine operations: - live migration of VM to another host - virtual machine snapshots (group snapshot without memory) - revert VM snapshot - delete VM snapshot Supported Volume operations: - attach/detach volume - live migrate volume between two StorPool primary storages - volume snapshot - delete snapshot - revert snapshot --- .../main/java/com/cloud/storage/Storage.java | 2 +- .../main/java/com/cloud/host/dao/HostDao.java | 2 + .../java/com/cloud/host/dao/HostDaoImpl.java | 26 + plugins/storage/volume/storpool/README.md | 9 + .../StorPoolSetVolumeEncryptionAnswer.java | 47 ++ .../StorPoolSetVolumeEncryptionCommand.java | 70 ++ ...PoolSetVolumeEncryptionCommandWrapper.java | 161 +++++ .../StorPoolPrimaryDataStoreDriver.java | 117 ++- .../motion/StorPoolDataMotionStrategy.java | 3 + .../cloud/storage/VolumeApiServiceImpl.java | 15 +- .../plugins/storpool/TestEncryptedVolumes.py | 681 ++++++++++++++++++ test/integration/plugins/storpool/sp_util.py | 20 + 12 files changed, 1126 insertions(+), 27 deletions(-) create mode 100644 plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolSetVolumeEncryptionAnswer.java create mode 100644 plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolSetVolumeEncryptionCommand.java create mode 100644 plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolSetVolumeEncryptionCommandWrapper.java create mode 100644 test/integration/plugins/storpool/TestEncryptedVolumes.py diff --git a/api/src/main/java/com/cloud/storage/Storage.java b/api/src/main/java/com/cloud/storage/Storage.java index 7e63462b9da..c6dee56fa22 100644 --- a/api/src/main/java/com/cloud/storage/Storage.java +++ b/api/src/main/java/com/cloud/storage/Storage.java @@ -149,7 +149,7 @@ public class Storage { ManagedNFS(true, false, false), Linstor(true, true, false), DatastoreCluster(true, true, false), // for VMware, to abstract pool of clusters - StorPool(true, true, false); + StorPool(true, true, true); private final boolean shared; private final boolean overprovisioning; diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java index e1392bab541..6dfe75c5634 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java @@ -85,6 +85,8 @@ public interface HostDao extends GenericDao, StateDao findByClusterId(Long clusterId); + List findByClusterIdAndEncryptionSupport(Long clusterId); + /** * Returns hosts that are 'Up' and 'Enabled' from the given Data Center/Zone */ diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java index 31958d418b1..15c87d639cc 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java @@ -1150,6 +1150,32 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao return listBy(sc); } + @Override + public List findByClusterIdAndEncryptionSupport(Long clusterId) { + SearchBuilder hostCapabilitySearch = _detailsDao.createSearchBuilder(); + DetailVO tagEntity = hostCapabilitySearch.entity(); + hostCapabilitySearch.and("capability", tagEntity.getName(), SearchCriteria.Op.EQ); + hostCapabilitySearch.and("value", tagEntity.getValue(), SearchCriteria.Op.EQ); + + SearchBuilder hostSearch = createSearchBuilder(); + HostVO entity = hostSearch.entity(); + hostSearch.and("cluster", entity.getClusterId(), SearchCriteria.Op.EQ); + hostSearch.and("status", entity.getStatus(), SearchCriteria.Op.EQ); + hostSearch.join("hostCapabilitySearch", hostCapabilitySearch, entity.getId(), tagEntity.getHostId(), JoinBuilder.JoinType.INNER); + + SearchCriteria sc = hostSearch.create(); + sc.setJoinParameters("hostCapabilitySearch", "value", Boolean.toString(true)); + sc.setJoinParameters("hostCapabilitySearch", "capability", Host.HOST_VOLUME_ENCRYPTION); + + if (clusterId != null) { + sc.setParameters("cluster", clusterId); + } + sc.setParameters("status", Status.Up.toString()); + sc.setParameters("resourceState", ResourceState.Enabled.toString()); + + return listBy(sc); + } + @Override public HostVO findByPublicIp(String publicIp) { SearchCriteria sc = PublicIpAddressSearch.create(); diff --git a/plugins/storage/volume/storpool/README.md b/plugins/storage/volume/storpool/README.md index 7f710732e08..b79fc64017d 100644 --- a/plugins/storage/volume/storpool/README.md +++ b/plugins/storage/volume/storpool/README.md @@ -342,3 +342,12 @@ Max IOPS are kept in StorPool's volumes with the help of custom service offering corresponding system disk offering. CloudStack has no way to specify max BW. Do they want to be able to specify max BW only is sufficient. + +## Supported operations for Volume encryption + +Supported Virtual machine operations - live migration of VM to another host, virtual machine snapshots (group snapshot without memory), revert VM snapshot, delete VM snapshot + +Supported Volume operations - attach/detach volume, live migrate volume between two StorPool primary storages, volume snapshot, delete snapshot, revert snapshot + +Note: volume snapshot are allowed only when `sp.bypass.secondary.storage` is set to `true`. This means that the snapshots are not backed up to secondary storage + diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolSetVolumeEncryptionAnswer.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolSetVolumeEncryptionAnswer.java new file mode 100644 index 00000000000..9429457cac2 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolSetVolumeEncryptionAnswer.java @@ -0,0 +1,47 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.agent.api.storage; + +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +public class StorPoolSetVolumeEncryptionAnswer extends Answer { + private VolumeObjectTO volume; + + public StorPoolSetVolumeEncryptionAnswer(Command command, boolean success, String details) { + super(command, success, details); + } + + public StorPoolSetVolumeEncryptionAnswer(VolumeObjectTO volume) { + super(); + this.volume = volume; + this.result = true; + } + + public VolumeObjectTO getVolume() { + return volume; + } + + public void setVolume(VolumeObjectTO volume) { + this.volume = volume; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolSetVolumeEncryptionCommand.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolSetVolumeEncryptionCommand.java new file mode 100644 index 00000000000..4c02f462ee7 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolSetVolumeEncryptionCommand.java @@ -0,0 +1,70 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.agent.api.storage; + +import org.apache.cloudstack.storage.command.StorageSubSystemCommand; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +public class StorPoolSetVolumeEncryptionCommand extends StorageSubSystemCommand { + private boolean isDataDisk; + private VolumeObjectTO volumeObjectTO; + private String srcVolumeName; + + public StorPoolSetVolumeEncryptionCommand(VolumeObjectTO volumeObjectTO, String srcVolumeName, + boolean isDataDisk) { + this.volumeObjectTO = volumeObjectTO; + this.srcVolumeName = srcVolumeName; + this.isDataDisk = isDataDisk; + } + + public VolumeObjectTO getVolumeObjectTO() { + return volumeObjectTO; + } + + public void setVolumeObjectTO(VolumeObjectTO volumeObjectTO) { + this.volumeObjectTO = volumeObjectTO; + } + + public void setIsDataDisk(boolean isDataDisk) { + this.isDataDisk = isDataDisk; + } + + public boolean isDataDisk() { + return isDataDisk; + } + + public String getSrcVolumeName() { + return srcVolumeName; + } + + public void setSrcVolumeName(String srcVolumeName) { + this.srcVolumeName = srcVolumeName; + } + + @Override + public void setExecuteInSequence(boolean inSeq) { + inSeq = false; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolSetVolumeEncryptionCommandWrapper.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolSetVolumeEncryptionCommandWrapper.java new file mode 100644 index 00000000000..8fdc28efc74 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolSetVolumeEncryptionCommandWrapper.java @@ -0,0 +1,161 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.cryptsetup.CryptSetup; +import org.apache.cloudstack.utils.cryptsetup.CryptSetupException; +import org.apache.cloudstack.utils.cryptsetup.KeyFile; +import org.apache.cloudstack.utils.qemu.QemuImageOptions; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.cloudstack.utils.qemu.QemuObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; +import org.libvirt.LibvirtException; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.storage.StorPoolSetVolumeEncryptionAnswer; +import com.cloud.agent.api.storage.StorPoolSetVolumeEncryptionCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.exception.CloudRuntimeException; + +@ResourceWrapper(handles = StorPoolSetVolumeEncryptionCommand.class) +public class StorPoolSetVolumeEncryptionCommandWrapper extends + CommandWrapper { + private static final Logger logger = Logger.getLogger(StorPoolSetVolumeEncryptionCommandWrapper.class); + + @Override + public StorPoolSetVolumeEncryptionAnswer execute(StorPoolSetVolumeEncryptionCommand command, + LibvirtComputingResource serverResource) { + VolumeObjectTO volume = command.getVolumeObjectTO(); + String srcVolumeName = command.getSrcVolumeName(); + try { + StorPoolStorageAdaptor.attachOrDetachVolume("attach", "volume", volume.getPath()); + KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO) volume.getDataStore(); + KVMStoragePool pool = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), primaryStore.getUuid()); + KVMPhysicalDisk disk = pool.getPhysicalDisk(volume.getPath()); + if (command.isDataDisk()) { + encryptDataDisk(volume, disk); + } else { + disk = encryptRootDisk(command, volume, srcVolumeName, pool, disk); + } + logger.debug(String.format("StorPoolSetVolumeEncryptionCommandWrapper disk=%s", disk)); + } catch (Exception e) { + new Answer(command, e); + } finally { + StorPoolStorageAdaptor.attachOrDetachVolume("detach", "volume", volume.getPath()); + volume.clearPassphrase(); + } + return new StorPoolSetVolumeEncryptionAnswer(volume); + } + + private KVMPhysicalDisk encryptRootDisk(StorPoolSetVolumeEncryptionCommand command, VolumeObjectTO volume, + String srcVolumeName, KVMStoragePool pool, KVMPhysicalDisk disk) { + StorPoolStorageAdaptor.attachOrDetachVolume("attach", "snapshot", srcVolumeName); + KVMPhysicalDisk srcVolume = pool.getPhysicalDisk(srcVolumeName); + disk = copyPhysicalDisk(srcVolume, disk, command.getWait() * 1000, null, volume.getPassphrase()); + disk.setQemuEncryptFormat(QemuObject.EncryptFormat.LUKS); + disk.setFormat(QemuImg.PhysicalDiskFormat.RAW); + volume.setEncryptFormat(disk.getQemuEncryptFormat().toString()); + StorPoolStorageAdaptor.attachOrDetachVolume("detach", "snapshot", srcVolumeName); + return disk; + } + + private void encryptDataDisk(VolumeObjectTO volume, KVMPhysicalDisk disk) throws CryptSetupException { + CryptSetup crypt = new CryptSetup(); + crypt.luksFormat(volume.getPassphrase(), CryptSetup.LuksType.LUKS, disk.getPath()); + disk.setQemuEncryptFormat(QemuObject.EncryptFormat.LUKS); + disk.setFormat(QemuImg.PhysicalDiskFormat.RAW); + volume.setEncryptFormat(disk.getQemuEncryptFormat().toString()); + } + + private KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, KVMPhysicalDisk destDisk, int timeout, + byte[] srcPassphrase, byte[] dstPassphrase) { + + logger.debug("Copy physical disk with size: " + disk.getSize() + ", virtualsize: " + disk.getVirtualSize() + + ", format: " + disk.getFormat()); + + destDisk.setVirtualSize(disk.getVirtualSize()); + destDisk.setSize(disk.getSize()); + + QemuImg qemu = null; + QemuImgFile srcQemuFile = null; + QemuImgFile destQemuFile = null; + String srcKeyName = "sec0"; + String destKeyName = "sec1"; + List qemuObjects = new ArrayList<>(); + Map options = new HashMap<>(); + + try (KeyFile srcKey = new KeyFile(srcPassphrase); KeyFile dstKey = new KeyFile(dstPassphrase)) { + qemu = new QemuImg(timeout, true, false); + String srcPath = disk.getPath(); + String destPath = destDisk.getPath(); + + QemuImageOptions qemuImageOpts = new QemuImageOptions(srcPath); + + srcQemuFile = new QemuImgFile(srcPath, disk.getFormat()); + destQemuFile = new QemuImgFile(destPath); + + if (srcKey.isSet()) { + qemuObjects.add(QemuObject.prepareSecretForQemuImg(disk.getFormat(), disk.getQemuEncryptFormat(), + srcKey.toString(), srcKeyName, options)); + qemuImageOpts = new QemuImageOptions(disk.getFormat(), srcPath, srcKeyName); + } + + if (dstKey.isSet()) { + qemu.setSkipZero(false); + destDisk.setFormat(QemuImg.PhysicalDiskFormat.RAW); + destQemuFile.setFormat(QemuImg.PhysicalDiskFormat.LUKS); + qemuObjects.add(QemuObject.prepareSecretForQemuImg(destDisk.getFormat(), QemuObject.EncryptFormat.LUKS, + dstKey.toString(), destKeyName, options)); + destDisk.setQemuEncryptFormat(QemuObject.EncryptFormat.LUKS); + } + + qemu.convert(srcQemuFile, destQemuFile, options, qemuObjects, qemuImageOpts, null, true); + logger.debug("Successfully converted source disk image " + srcQemuFile.getFileName() + + " to StorPool volume: " + destDisk.getPath()); + + } catch (QemuImgException | LibvirtException | IOException e) { + + String errMsg = String.format("Unable to convert/copy from %s to %s, due to: %s", disk.getName(), + destDisk.getName(), ((StringUtils.isEmpty(e.getMessage())) ? "an unknown error" : e.getMessage())); + logger.error(errMsg); + throw new CloudRuntimeException(errMsg, e); + } + + return destDisk; + } +} 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 6eced6fc5d0..896e12a3bc3 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 @@ -26,12 +26,15 @@ import com.cloud.agent.api.storage.StorPoolCopyVolumeToSecondaryCommand; import com.cloud.agent.api.storage.StorPoolDownloadTemplateCommand; import com.cloud.agent.api.storage.StorPoolDownloadVolumeCommand; import com.cloud.agent.api.storage.StorPoolResizeVolumeCommand; +import com.cloud.agent.api.storage.StorPoolSetVolumeEncryptionAnswer; +import com.cloud.agent.api.storage.StorPoolSetVolumeEncryptionCommand; import com.cloud.agent.api.to.DataObjectType; import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.StorageFilerTO; import com.cloud.dc.dao.ClusterDao; import com.cloud.host.Host; +import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; import com.cloud.server.ResourceTag; @@ -93,9 +96,12 @@ 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.commons.collections4.CollectionUtils; import org.apache.log4j.Logger; import javax.inject.Inject; + +import java.util.List; import java.util.Map; public class StorPoolPrimaryDataStoreDriver implements PrimaryDataStoreDriver { @@ -217,12 +223,12 @@ public class StorPoolPrimaryDataStoreDriver implements PrimaryDataStoreDriver { @Override public void createAsync(DataStore dataStore, DataObject data, AsyncCompletionCallback callback) { String path = null; - String err = null; + Answer answer; if (data.getType() == DataObjectType.VOLUME) { try { VolumeInfo vinfo = (VolumeInfo)data; String name = vinfo.getUuid(); - Long size = vinfo.getSize(); + Long size = vinfo.getPassphraseId() == null ? vinfo.getSize() : vinfo.getSize() + 2097152; SpConnectionDesc conn = StorPoolUtil.getSpConnection(dataStore.getUuid(), dataStore.getId(), storagePoolDetailsDao, primaryStoreDao); StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriver.createAsync volume: name=%s, uuid=%s, isAttached=%s vm=%s, payload=%s, template: %s", vinfo.getName(), vinfo.getUuid(), vinfo.isAttachedVM(), vinfo.getAttachedVmName(), vinfo.getpayload(), conn.getTemplateName()); @@ -231,30 +237,66 @@ public class StorPoolPrimaryDataStoreDriver implements PrimaryDataStoreDriver { String volumeName = StorPoolUtil.getNameFromResponse(resp, false); path = StorPoolUtil.devPath(volumeName); - VolumeVO volume = volumeDao.findById(vinfo.getId()); - volume.setPoolId(dataStore.getId()); - volume.setPath(path); - volumeDao.update(volume.getId(), volume); + updateVolume(dataStore, path, vinfo); + if (vinfo.getPassphraseId() != null) { + VolumeObjectTO volume = updateVolumeObjectTO(vinfo, resp); + answer = createEncryptedVolume(dataStore, data, vinfo, size, volume, null, true); + } else { + answer = new Answer(null, true, null); + } updateStoragePool(dataStore.getId(), size); StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriver.createAsync volume: name=%s, uuid=%s, isAttached=%s vm=%s, payload=%s, template: %s", volumeName, vinfo.getUuid(), vinfo.isAttachedVM(), vinfo.getAttachedVmName(), vinfo.getpayload(), conn.getTemplateName()); } else { - err = String.format("Could not create StorPool volume %s. Error: %s", name, resp.getError()); + answer = new Answer(null, false, String.format("Could not create StorPool volume %s. Error: %s", name, resp.getError())); } } catch (Exception e) { - err = String.format("Could not create volume due to %s", e.getMessage()); + answer = new Answer(null, false, String.format("Could not create volume due to %s", e.getMessage())); } } else { - err = String.format("Invalid object type \"%s\" passed to createAsync", data.getType()); + answer = new Answer(null, false, String.format("Invalid object type \"%s\" passed to createAsync", data.getType())); } - - CreateCmdResult res = new CreateCmdResult(path, new Answer(null, err == null, err)); - res.setResult(err); + CreateCmdResult res = new CreateCmdResult(path, answer); + res.setResult(answer.getDetails()); if (callback != null) { callback.complete(res); } } + private void updateVolume(DataStore dataStore, String path, VolumeInfo vinfo) { + VolumeVO volume = volumeDao.findById(vinfo.getId()); + volume.setPoolId(dataStore.getId()); + volume.setPath(path); + volume.setPoolType(StoragePoolType.StorPool); + volumeDao.update(volume.getId(), volume); + } + + private StorPoolSetVolumeEncryptionAnswer createEncryptedVolume(DataStore dataStore, DataObject data, VolumeInfo vinfo, Long size, VolumeObjectTO volume, String parentName, boolean isDataDisk) { + StorPoolSetVolumeEncryptionAnswer ans; + EndPoint ep = null; + if (parentName == null) { + ep = selector.select(data, vinfo.getPassphraseId() != null); + } else { + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(parentName, clusterDao); + if (clusterId == null) { + ep = selector.select(data, vinfo.getPassphraseId() != null); + } else { + List hosts = hostDao.findByClusterIdAndEncryptionSupport(clusterId); + ep = CollectionUtils.isNotEmpty(hosts) ? RemoteHostEndPoint.getHypervisorHostEndPoint(hosts.get(0)) : ep; + } + } + if (ep == null) { + ans = new StorPoolSetVolumeEncryptionAnswer(null, false, "Could not find a host with volume encryption"); + } else { + StorPoolSetVolumeEncryptionCommand cmd = new StorPoolSetVolumeEncryptionCommand(volume, parentName, isDataDisk); + ans = (StorPoolSetVolumeEncryptionAnswer) ep.sendMessage(cmd); + if (ans.getResult()) { + updateStoragePool(dataStore.getId(), size); + } + } + return ans; + } + @Override public void resize(DataObject data, AsyncCompletionCallback callback) { String path = null; @@ -623,30 +665,42 @@ public class StorPoolPrimaryDataStoreDriver implements PrimaryDataStoreDriver { // create volume from template on Storpool PRIMARY TemplateInfo tinfo = (TemplateInfo)srcData; - VolumeInfo vinfo = (VolumeInfo)dstData; - VMTemplateStoragePoolVO templStoragePoolVO = StorPoolHelper.findByPoolTemplate(vinfo.getPoolId(), tinfo.getId()); - final String parentName = templStoragePoolVO.getLocalDownloadPath() !=null ? StorPoolStorageAdaptor.getVolumeNameFromPath(templStoragePoolVO.getLocalDownloadPath(), true) : StorPoolStorageAdaptor.getVolumeNameFromPath(templStoragePoolVO.getInstallPath(), true); + VolumeInfo vinfo = (VolumeInfo) dstData; + VMTemplateStoragePoolVO templStoragePoolVO = StorPoolHelper.findByPoolTemplate(vinfo.getPoolId(), + tinfo.getId()); + final String parentName = templStoragePoolVO.getLocalDownloadPath() != null + ? StorPoolStorageAdaptor.getVolumeNameFromPath(templStoragePoolVO.getLocalDownloadPath(), true) + : StorPoolStorageAdaptor.getVolumeNameFromPath(templStoragePoolVO.getInstallPath(), true); final String name = vinfo.getUuid(); - SpConnectionDesc conn = StorPoolUtil.getSpConnection(vinfo.getDataStore().getUuid(), vinfo.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(vinfo.getDataStore().getUuid(), + vinfo.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); Long snapshotSize = templStoragePoolVO.getTemplateSize(); - long size = vinfo.getSize(); + boolean withoutEncryption = vinfo.getPassphraseId() == null; + long size = withoutEncryption ? vinfo.getSize() : vinfo.getSize() + 2097152; if (snapshotSize != null && size < snapshotSize) { StorPoolUtil.spLog(String.format("provided size is too small for snapshot. Provided %d, snapshot %d. Using snapshot size", size, snapshotSize)); - size = snapshotSize; + size = withoutEncryption ? snapshotSize : snapshotSize + 2097152; } StorPoolUtil.spLog(String.format("volume size is: %d", size)); Long vmId = vinfo.getInstanceId(); - SpApiResponse resp = StorPoolUtil.volumeCreate(name, parentName, size, getVMInstanceUUID(vmId), - getVcPolicyTag(vmId), "volume", vinfo.getMaxIops(), conn); + SpApiResponse resp = StorPoolUtil.volumeCreate(name, parentName, size, getVMInstanceUUID(vmId), getVcPolicyTag(vmId), + "volume", vinfo.getMaxIops(), conn); if (resp.getError() == null) { updateStoragePool(dstData.getDataStore().getId(), vinfo.getSize()); + updateVolumePoolType(vinfo); - VolumeObjectTO to = (VolumeObjectTO) vinfo.getTO(); - to.setSize(vinfo.getSize()); - to.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); - - answer = new CopyCmdAnswer(to); + if (withoutEncryption) { + VolumeObjectTO to = updateVolumeObjectTO(vinfo, resp); + answer = new CopyCmdAnswer(to); + } else { + VolumeObjectTO volume = updateVolumeObjectTO(vinfo, resp); + String snapshotPath = StorPoolUtil.devPath(parentName.split("~")[1]); + answer = createEncryptedVolume(dstData.getDataStore(), dstData, vinfo, size, volume, snapshotPath, false); + if (answer.getResult()) { + answer = new CopyCmdAnswer(((StorPoolSetVolumeEncryptionAnswer) answer).getVolume()); + } + } } else { err = String.format("Could not create Storpool volume %s. Error: %s", name, resp.getError()); } @@ -775,6 +829,19 @@ public class StorPoolPrimaryDataStoreDriver implements PrimaryDataStoreDriver { callback.complete(res); } + private void updateVolumePoolType(VolumeInfo vinfo) { + VolumeVO volumeVO = volumeDao.findById(vinfo.getId()); + volumeVO.setPoolType(StoragePoolType.StorPool); + volumeDao.update(volumeVO.getId(), volumeVO); + } + + private VolumeObjectTO updateVolumeObjectTO(VolumeInfo vinfo, SpApiResponse resp) { + VolumeObjectTO to = (VolumeObjectTO) vinfo.getTO(); + to.setSize(vinfo.getSize()); + to.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); + return to; + } + /** * Live migrate/copy volume from one StorPool storage to another * @param srcData The source volume data diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java index 1608680e41b..a735b0fe918 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java @@ -292,6 +292,9 @@ public class StorPoolDataMotionStrategy implements DataMotionStrategy { for (Map.Entry entry : volumeDataStoreMap.entrySet()) { VolumeInfo srcVolumeInfo = entry.getKey(); + if (srcVolumeInfo.getPassphraseId() != null) { + throw new CloudRuntimeException(String.format("Cannot live migrate encrypted volume [%s] to StorPool", srcVolumeInfo.getName())); + } DataStore destDataStore = entry.getValue(); VolumeVO srcVolume = _volumeDao.findById(srcVolumeInfo.getId()); diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index fd234b24794..6b1d170be9c 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -160,6 +160,7 @@ import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.service.dao.ServiceOfferingDetailsDao; import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.SnapshotDao; import com.cloud.storage.dao.StoragePoolTagsDao; @@ -2983,6 +2984,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } boolean liveMigrateVolume = false; + boolean srcAndDestOnStorPool = false; Long instanceId = vol.getInstanceId(); Long srcClusterId = null; VMInstanceVO vm = null; @@ -3026,6 +3028,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic "Therefore, to live migrate a volume between storage pools, one must migrate the VM to a different host as well to force the VM XML domain update. " + "Use 'migrateVirtualMachineWithVolumes' instead."); } + srcAndDestOnStorPool = isSourceAndDestOnStorPool(storagePoolVO, destinationStoragePoolVo); } } @@ -3039,6 +3042,10 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } } + if (vol.getPassphraseId() != null && !srcAndDestOnStorPool) { + throw new InvalidParameterValueException("Migration of encrypted volumes is unsupported"); + } + if (vm != null && HypervisorType.VMware.equals(vm.getHypervisorType()) && State.Stopped.equals(vm.getState())) { @@ -3177,6 +3184,11 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic || destinationStoragePoolVo.getPoolType() != Storage.StoragePoolType.StorPool; } + private boolean isSourceAndDestOnStorPool(StoragePoolVO storagePoolVO, StoragePoolVO destinationStoragePoolVo) { + return storagePoolVO.getPoolType() == Storage.StoragePoolType.StorPool + && destinationStoragePoolVo.getPoolType() == Storage.StoragePoolType.StorPool; + } + /** * Retrieves the new disk offering UUID that might be sent to replace the current one in the volume being migrated. * If no disk offering UUID is provided we return null. Otherwise, we perform the following checks. @@ -3476,7 +3488,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic throw new InvalidParameterValueException("VolumeId: " + volumeId + " is not in " + Volume.State.Ready + " state but " + volume.getState() + ". Cannot take snapshot."); } - if (volume.getEncryptFormat() != null && volume.getAttachedVM() != null && volume.getAttachedVM().getState() != State.Stopped) { + boolean isSnapshotOnStorPoolOnly = volume.getStoragePoolType() == StoragePoolType.StorPool && BooleanUtils.toBoolean(_configDao.getValue("sp.bypass.secondary.storage")); + if (volume.getEncryptFormat() != null && volume.getAttachedVM() != null && volume.getAttachedVM().getState() != State.Stopped && !isSnapshotOnStorPoolOnly) { s_logger.debug(String.format("Refusing to take snapshot of encrypted volume (%s) on running VM (%s)", volume, volume.getAttachedVM())); throw new UnsupportedOperationException("Volume snapshots for encrypted volumes are not supported if VM is running"); } diff --git a/test/integration/plugins/storpool/TestEncryptedVolumes.py b/test/integration/plugins/storpool/TestEncryptedVolumes.py new file mode 100644 index 00000000000..fcf72cf98bc --- /dev/null +++ b/test/integration/plugins/storpool/TestEncryptedVolumes.py @@ -0,0 +1,681 @@ +# 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. + +# Import Local Modules +from marvin.codes import FAILED, KVM, PASS, XEN_SERVER, RUNNING +from nose.plugins.attrib import attr +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.utils import random_gen, cleanup_resources, validateList, is_snapshot_on_nfs, isAlmostEqual +from marvin.lib.base import (Account, + Cluster, + Configurations, + ServiceOffering, + Snapshot, + StoragePool, + Template, + VirtualMachine, + VmSnapshot, + Volume, + SecurityGroup, + Role, + DiskOffering, + ) +from marvin.lib.common import (get_zone, + get_domain, + get_template, + list_disk_offering, + list_hosts, + list_snapshots, + list_storage_pools, + list_volumes, + list_virtual_machines, + list_configurations, + list_service_offering, + list_clusters, + list_zones) +from marvin.cloudstackAPI import (listOsTypes, + listTemplates, + listHosts, + createTemplate, + createVolume, + getVolumeSnapshotDetails, + resizeVolume, + listZones, + migrateVirtualMachine, + findHostsForMigration, + revertSnapshot, + deleteSnapshot) +from marvin.sshClient import SshClient + +import time +import pprint +import random +import subprocess +from storpool import spapi +from storpool import sptypes +import unittest + +import uuid +from sp_util import (TestData, StorPoolHelper) + +class TestEncryptedVolumes(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + super(TestEncryptedVolumes, cls).setUpClass() + try: + cls.setUpCloudStack() + except Exception: + cls.cleanUpCloudStack() + raise + + @classmethod + def setUpCloudStack(cls): + testClient = super(TestEncryptedVolumes, cls).getClsTestClient() + + cls._cleanup = [] + + config = cls.getClsConfig() + StorPoolHelper.logger = cls + cls.logger = StorPoolHelper.logger + + cls.apiclient = testClient.getApiClient() + + zone = config.zones[0] + assert zone is not None + cls.zone = list_zones(cls.apiclient, name=zone.name)[0] + + cls.hostConfig = cls.config.__dict__["zones"][0].__dict__["pods"][0].__dict__["clusters"][0].__dict__["hosts"][0].__dict__ + + cls.spapi = spapi.Api(host=zone.spEndpoint, port=zone.spEndpointPort, auth=zone.spAuthToken, multiCluster=True) + cls.helper = StorPoolHelper() + + cls.unsupportedHypervisor = False + cls.hypervisor = testClient.getHypervisorInfo() + if cls.hypervisor.lower() in ("hyperv", "lxc"): + cls.unsupportedHypervisor = True + return + + cls.services = testClient.getParsedTestDataConfig() + + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + + td = TestData() + cls.testdata = td.testdata + + cls.sp_template_1 = "ssd" + storpool_primary_storage = { + "name": cls.sp_template_1, + "zoneid": cls.zone.id, + "url": "SP_API_HTTP=%s:%s;SP_AUTH_TOKEN=%s;SP_TEMPLATE=%s" % (zone.spEndpoint, zone.spEndpointPort, zone.spAuthToken, cls.sp_template_1), + "scope": "zone", + "capacitybytes": 564325555333, + "capacityiops": 155466, + "hypervisor": "kvm", + "provider": "StorPool", + "tags": cls.sp_template_1 + } + + cls.storpool_primary_storage = storpool_primary_storage + + storage_pool = list_storage_pools( + cls.apiclient, + name=storpool_primary_storage["name"] + ) + + if storage_pool is None: + newTemplate = sptypes.VolumeTemplateCreateDesc(name=storpool_primary_storage["name"], placeAll="virtual", + placeTail="virtual", placeHead="virtual", replication=1) + template_on_local = cls.spapi.volumeTemplateCreate(newTemplate) + + storage_pool = StoragePool.create(cls.apiclient, storpool_primary_storage) + else: + storage_pool = storage_pool[0] + cls.primary_storage = storage_pool + + storpool_service_offerings_ssd = { + "name": "ssd-encrypted", + "displaytext": "SP_CO_2 (Min IOPS = 10,000; Max IOPS = 15,000)", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + "customizediops": False, + "hypervisorsnapshotreserve": 200, + "encryptroot": True, + "tags": cls.sp_template_1 + } + + service_offerings_ssd = list_service_offering( + cls.apiclient, + name=storpool_service_offerings_ssd["name"] + ) + + if service_offerings_ssd is None: + service_offerings_ssd = ServiceOffering.create(cls.apiclient, storpool_service_offerings_ssd, encryptroot=True) + else: + service_offerings_ssd = service_offerings_ssd[0] + + cls.service_offering = service_offerings_ssd + cls.debug(pprint.pformat(cls.service_offering)) + + cls.sp_template_2 = "ssd2" + + storpool_primary_storage2 = { + "name": cls.sp_template_2, + "zoneid": cls.zone.id, + "url": "SP_API_HTTP=%s:%s;SP_AUTH_TOKEN=%s;SP_TEMPLATE=%s" % (zone.spEndpoint, zone.spEndpointPort, zone.spAuthToken, cls.sp_template_2), + "scope": "zone", + "capacitybytes": 564325555333, + "capacityiops": 1554, + "hypervisor": "kvm", + "provider": "StorPool", + "tags": cls.sp_template_2 + } + + cls.storpool_primary_storage2 = storpool_primary_storage2 + storage_pool = list_storage_pools( + cls.apiclient, + name=storpool_primary_storage2["name"] + ) + + if storage_pool is None: + newTemplate = sptypes.VolumeTemplateCreateDesc(name=storpool_primary_storage2["name"], placeAll="virtual", + placeTail="virtual", placeHead="virtual", replication=1) + template_on_local = cls.spapi.volumeTemplateCreate(newTemplate) + storage_pool = StoragePool.create(cls.apiclient, storpool_primary_storage2) + + else: + storage_pool = storage_pool[0] + cls.primary_storage2 = storage_pool + + storpool_service_offerings_ssd2 = { + "name": "ssd2-encrypted", + "displaytext": "SP_CO_2", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + "customizediops": False, + "encryptroot": True, + "tags": cls.sp_template_2 + } + + service_offerings_ssd2 = list_service_offering( + cls.apiclient, + name=storpool_service_offerings_ssd2["name"] + ) + + if service_offerings_ssd2 is None: + service_offerings_ssd2 = ServiceOffering.create(cls.apiclient, storpool_service_offerings_ssd2, encryptroot=True) + else: + service_offerings_ssd2 = service_offerings_ssd2[0] + + cls.service_offering2 = service_offerings_ssd2 + + cls.disk_offerings_ssd2_encrypted = list_disk_offering( + cls.apiclient, + name=cls.testdata[TestData.diskOfferingEncrypted2]["name"] + ) + if cls.disk_offerings_ssd2_encrypted is None: + cls.disk_offerings_ssd2_encrypted = DiskOffering.create(cls.apiclient, cls.testdata[TestData.diskOfferingEncrypted2], encrypt=True) + else: + cls.disk_offerings_ssd2_encrypted = cls.disk_offerings_ssd2_encrypted[0] + + cls.disk_offering_ssd_encrypted = list_disk_offering( + cls.apiclient, + name=cls.testdata[TestData.diskOfferingEncrypted]["name"] + ) + + if cls.disk_offering_ssd_encrypted is None: + cls.disk_offering_ssd_encrypted = DiskOffering.create(cls.apiclient, cls.testdata[TestData.diskOfferingEncrypted], encrypt=True) + else: + cls.disk_offering_ssd_encrypted = cls.disk_offering_ssd_encrypted[0] + + template = get_template( + cls.apiclient, + cls.zone.id, + account="system" + ) + + if template == FAILED: + assert False, "get_template() failed to return template\ + with description %s" % cls.services["ostype"] + + cls.services["domainid"] = cls.domain.id + cls.services["small"]["zoneid"] = cls.zone.id + cls.services["templates"]["ostypeid"] = template.ostypeid + cls.services["zoneid"] = cls.zone.id + + role = Role.list(cls.apiclient, name='Root Admin') + + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + domainid=cls.domain.id, + roleid= 1 + ) + + securitygroup = SecurityGroup.list(cls.apiclient, account=cls.account.name, domainid=cls.account.domainid)[0] + cls.helper.set_securityGroups(cls.apiclient, account=cls.account.name, domainid=cls.account.domainid, + id=securitygroup.id) + cls._cleanup.append(cls.account) + + cls.volume_1 = Volume.create( + cls.apiclient, + {"diskname": "StorPoolEncryptedDiskLiveMigrate"}, + zoneid=cls.zone.id, + diskofferingid=cls.disk_offering_ssd_encrypted.id, + account=cls.account.name, + domainid=cls.account.domainid, + ) + + cls.volume_2 = Volume.create( + cls.apiclient, + {"diskname": "StorPoolEncryptedDiskVMSnapshot"}, + zoneid=cls.zone.id, + diskofferingid=cls.disk_offering_ssd_encrypted.id, + account=cls.account.name, + domainid=cls.account.domainid, + ) + + cls.virtual_machine = VirtualMachine.create( + cls.apiclient, + {"name": "StorPool-LiveMigrate-VM%s" % uuid.uuid4()}, + accountid=cls.account.name, + domainid=cls.account.domainid, + zoneid=cls.zone.id, + templateid=template.id, + serviceofferingid=cls.service_offering.id, + hypervisor=cls.hypervisor, + rootdisksize=10 + ) + + cls.virtual_machine2 = VirtualMachine.create( + cls.apiclient, + {"name": "StorPool-VMSnapshots%s" % uuid.uuid4()}, + accountid=cls.account.name, + domainid=cls.account.domainid, + zoneid=cls.zone.id, + templateid=template.id, + serviceofferingid=cls.service_offering.id, + hypervisor=cls.hypervisor, + rootdisksize=10 + ) + + cls.virtual_machine3 = VirtualMachine.create( + cls.apiclient, + {"name": "StorPool-VolumeSnapshots%s" % uuid.uuid4()}, + accountid=cls.account.name, + domainid=cls.account.domainid, + zoneid=cls.zone.id, + templateid=template.id, + serviceofferingid=cls.service_offering.id, + hypervisor=cls.hypervisor, + rootdisksize=10 + ) + + cls.template = template + cls.hostid = cls.virtual_machine.hostid + cls.random_data_0 = random_gen(size=100) + cls.test_dir = "/tmp" + cls.random_data = "random.data" + return + + @classmethod + def tearDownClass(cls): + cls.cleanUpCloudStack() + + @classmethod + def cleanUpCloudStack(cls): + try: + cleanup_resources(cls.apiclient, cls._cleanup) + + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + + if self.unsupportedHypervisor: + self.skipTest("Skipping test because unsupported hypervisor\ + %s" % self.hypervisor) + return + + def tearDown(self): + return + + +# live migrate VM with encrypted volumes to another host + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_01_live_migrate_vm(self): + ''' + Live Migrate VM to another host with encrypted volumes + ''' + self.virtual_machine.attach_volume( + self.apiclient, + self.volume_1 + ) + + volumes = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + ) + + vm_host = list_hosts(self.apiclient, id=self.virtual_machine.hostid)[0] + self.logger.debug(vm_host) + # sshc = SshClient( + # host=vm_host.name, + # port=22, + # user=None, + # passwd=None) + # + # for volume in volumes: + # cmd = 'blkid %s' % volume.path + # result = sshc.execute(cmd) + # if "LUKS" not in result: + # self.fail("The volume isn't encrypted %s" % volume) + + + dest_host_cmd = findHostsForMigration.findHostsForMigrationCmd() + dest_host_cmd.virtualmachineid = self.virtual_machine.id + host = self.apiclient.findHostsForMigration(dest_host_cmd)[0] + + cmd = migrateVirtualMachine.migrateVirtualMachineCmd() + cmd.virtualmachineid = self.virtual_machine.id + cmd.hostid = host.id + self.apiclient.migrateVirtualMachine(cmd) + +# VM snapshot + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_02_vm_snapshot(self): + self.virtual_machine2.attach_volume( + self.apiclient, + self.volume_2 + ) + + try: + ssh_client = self.virtual_machine2.get_ssh_client(reconnect=True) + + cmds = [ + "echo %s > %s/%s" % + (self.random_data_0, self.test_dir, self.random_data), + "sync", + "sleep 1", + "sync", + "sleep 1", + "cat %s/%s" % + (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine2.ipaddress) + self.assertEqual( + self.random_data_0, + result[0], + "Check the random data has be write into temp file!" + ) + + time.sleep(30) + MemorySnapshot = False + vm_snapshot = VmSnapshot.create( + self.apiclient, + self.virtual_machine2.id, + MemorySnapshot, + "TestSnapshot", + "Display Text" + ) + self.assertEqual( + vm_snapshot.state, + "Ready", + "Check the snapshot of vm is ready!" + ) + +# Revert VM snapshot + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_03_revert_vm_snapshots(self): + """Test to revert VM snapshots + """ + + try: + ssh_client = self.virtual_machine2.get_ssh_client(reconnect=True) + + cmds = [ + "rm -rf %s/%s" % (self.test_dir, self.random_data), + "ls %s/%s" % (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine2.ipaddress) + + if str(result[0]).index("No such file or directory") == -1: + self.fail("Check the random data has be delete from temp file!") + + time.sleep(30) + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + virtualmachineid=self.virtual_machine2.id, + listall=True) + + self.assertEqual( + isinstance(list_snapshot_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + list_snapshot_response, + None, + "Check if snapshot exists in ListSnapshot" + ) + + self.assertEqual( + list_snapshot_response[0].state, + "Ready", + "Check the snapshot of vm is ready!" + ) + + self.virtual_machine2.stop(self.apiclient, forced=True) + + VmSnapshot.revertToSnapshot( + self.apiclient, + list_snapshot_response[0].id + ) + + self.virtual_machine2.start(self.apiclient) + + try: + ssh_client = self.virtual_machine2.get_ssh_client(reconnect=True) + + cmds = [ + "cat %s/%s" % (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine2.ipaddress) + + self.assertEqual( + self.random_data_0, + result[0], + "Check the random data is equal with the ramdom file!" + ) + + # Delete VM snapshot + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_04_delete_vm_snapshots(self): + """Test to delete vm snapshots + """ + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + virtualmachineid=self.virtual_machine2.id, + listall=True) + + self.assertEqual( + isinstance(list_snapshot_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + list_snapshot_response, + None, + "Check if snapshot exists in ListSnapshot" + ) + VmSnapshot.deleteVMSnapshot( + self.apiclient, + list_snapshot_response[0].id) + + time.sleep(30) + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + #vmid=self.virtual_machine.id, + virtualmachineid=self.virtual_machine2.id, + listall=False) + self.debug('list_snapshot_response -------------------- %s' % list_snapshot_response) + + self.assertIsNone(list_snapshot_response, "snapshot is already deleted") + +# Take volume snapshot + @unittest.expectedFailure + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_05_snapshot_volume_with_secondary(self): + ''' + Test Create snapshot and backup to secondary + ''' + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "false") + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine3.id, + type = "ROOT", + listall = True, + ) + + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_06_snapshot_volume_on_primary(self): + ''' + Test Create snapshot and backup to secondary + ''' + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "true") + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine3.id, + type = "ROOT", + listall = True, + ) + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + if flag == False: + raise Exception("Could not find snapshot in snapshot_details") + except spapi.ApiError as err: + raise Exception(err) + self.assertIsNotNone(snapshot, "Could not create snapshot") +# Rever Volume snapshot + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_07_revert_volume_on_primary(self): + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine3.id, + type = "ROOT", + listall = True, + )[0] + snapshot = list_snapshots( + self.apiclient, + volumeid = volume.id, + listall=True + )[0] + self.virtual_machine3.stop(self.apiclient, forced=True) + + cmd = revertSnapshot.revertSnapshotCmd() + cmd.id = snapshot.id + revertcmd = self.apiclient.revertSnapshot(cmd) + +# Delete volume snapshot + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_08_delete_volume_on_primary(self): + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine3.id, + type = "ROOT", + listall = True, + )[0] + snapshot = list_snapshots( + self.apiclient, + volumeid = volume.id, + listall=True + )[0] + cmd = deleteSnapshot.deleteSnapshotCmd() + cmd.id = snapshot.id + self.apiclient.deleteSnapshot(cmd) + +# Live migrate encrypted volume + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_09_live_migrate_volume(self): + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + )[0] + + Volume.migrate(self.apiclient, volumeid=volume.id, storageid=self.primary_storage2.id, livemigrate=True) \ No newline at end of file diff --git a/test/integration/plugins/storpool/sp_util.py b/test/integration/plugins/storpool/sp_util.py index 76edba075ed..86a40ae22c7 100644 --- a/test/integration/plugins/storpool/sp_util.py +++ b/test/integration/plugins/storpool/sp_util.py @@ -75,6 +75,8 @@ class TestData(): diskName = "diskname" diskOffering = "diskoffering" diskOffering2 = "diskoffering2" + diskOfferingEncrypted = "diskOfferingEncrypted" + diskOfferingEncrypted2 = "diskOfferingEncrypted2" cephDiskOffering = "cephDiskOffering" nfsDiskOffering = "nfsDiskOffering" domainId = "domainId" @@ -236,6 +238,24 @@ class TestData(): TestData.tags: sp_template_2, "storagetype": "shared" }, + TestData.diskOfferingEncrypted: { + "name": "ssd-encrypted", + "displaytext": "ssd-encrypted", + "disksize": 5, + "hypervisorsnapshotreserve": 200, + "encrypt": True, + TestData.tags: sp_template_1, + "storagetype": "shared" + }, + TestData.diskOfferingEncrypted2: { + "name": "ssd2-encrypted", + "displaytext": "ssd2-encrypted", + "disksize": 5, + "hypervisorsnapshotreserve": 200, + "encrypt": True, + TestData.tags: sp_template_2, + "storagetype": "shared" + }, TestData.cephDiskOffering: { "name": "ceph", "displaytext": "Ceph fixed disk offering", From 985f0ecb533e5d8305ec34686840902f51963b9c Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 26 Jun 2023 13:36:36 +0200 Subject: [PATCH 4/6] Tungsten: change conserve_mode of default network offering to 0 (#7511) --- .../src/main/resources/META-INF/db/schema-41800to41810.sql | 3 +++ .../tungsten/api/command/ConfigTungstenFabricServiceCmd.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql b/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql index b3600522f99..4f27d0408d7 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql @@ -19,6 +19,9 @@ -- Schema upgrade from 4.18.0.0 to 4.18.1.0 --; +-- Update conserve_mode of the default network offering for Tungsten Fabric (this fixes issue #7241) +UPDATE `cloud`.`network_offerings` SET conserve_mode = 0 WHERE unique_name ='DefaultTungstenFarbicNetworkOffering'; + -- Add Windows Server 2022 guest OS and mappings CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2022 (64-bit)', 'KVM', 'default', 'Windows Server 2022 (64-bit)'); CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2022 (64-bit)', 'VMware', '7.0', 'windows2019srvNext_64Guest'); diff --git a/plugins/network-elements/tungsten/src/main/java/org/apache/cloudstack/network/tungsten/api/command/ConfigTungstenFabricServiceCmd.java b/plugins/network-elements/tungsten/src/main/java/org/apache/cloudstack/network/tungsten/api/command/ConfigTungstenFabricServiceCmd.java index 8544ec9bf97..305eb60abb2 100644 --- a/plugins/network-elements/tungsten/src/main/java/org/apache/cloudstack/network/tungsten/api/command/ConfigTungstenFabricServiceCmd.java +++ b/plugins/network-elements/tungsten/src/main/java/org/apache/cloudstack/network/tungsten/api/command/ConfigTungstenFabricServiceCmd.java @@ -166,7 +166,7 @@ public class ConfigTungstenFabricServiceCmd extends BaseCmd { networkOfferingVO = new NetworkOfferingVO(NETWORKOFFERING, "Default offering for Tungsten-Fabric Network", Networks.TrafficType.Guest, false, false, null, null, true, NetworkOffering.Availability.Optional, null, Network.GuestType.Isolated, - true, false, false, false, true, false); + false, false, false, false, true, false); networkOfferingVO.setForTungsten(true); networkOfferingVO.setState(NetworkOffering.State.Enabled); networkOfferingDao.persist(networkOfferingVO); From 973b0e28fda0c0b2600b7435ddbe5d029a733dd7 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 27 Jun 2023 10:47:25 +0200 Subject: [PATCH 5/6] test: fix Super-Linter Check error in storpool tests 2023-06-27 08:42:08 [INFO] File:[/github/workspace/test/integration/plugins/storpool/TestEncryptedVolumes.py] 2023-06-27 08:42:08 [ERROR] Found errors in [flake8] linter! 2023-06-27 08:42:08 [ERROR] Error code: 1. Command output: ------ /github/workspace/test/integration/plugins/storpool/TestEncryptedVolumes.py:681:113: W292 no newline at end of file ------ 2023-06-27 08:42:08 [INFO] --------------------------- 2023-06-27 08:42:08 [INFO] File:[/github/workspace/test/integration/plugins/storpool/sp_util.py] 2023-06-27 08:42:08 [ERROR] Found errors in [flake8] linter! 2023-06-27 08:42:08 [ERROR] Error code: 1. Command output: ------ /github/workspace/test/integration/plugins/storpool/sp_util.py:798:31: W292 no newline at end of file ------ --- test/integration/plugins/storpool/TestEncryptedVolumes.py | 2 +- test/integration/plugins/storpool/sp_util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/plugins/storpool/TestEncryptedVolumes.py b/test/integration/plugins/storpool/TestEncryptedVolumes.py index fcf72cf98bc..eed64950ef5 100644 --- a/test/integration/plugins/storpool/TestEncryptedVolumes.py +++ b/test/integration/plugins/storpool/TestEncryptedVolumes.py @@ -678,4 +678,4 @@ class TestEncryptedVolumes(cloudstackTestCase): listall = True, )[0] - Volume.migrate(self.apiclient, volumeid=volume.id, storageid=self.primary_storage2.id, livemigrate=True) \ No newline at end of file + Volume.migrate(self.apiclient, volumeid=volume.id, storageid=self.primary_storage2.id, livemigrate=True) diff --git a/test/integration/plugins/storpool/sp_util.py b/test/integration/plugins/storpool/sp_util.py index 86a40ae22c7..6517841354a 100644 --- a/test/integration/plugins/storpool/sp_util.py +++ b/test/integration/plugins/storpool/sp_util.py @@ -795,4 +795,4 @@ class StorPoolHelper(): destinationHost = host[0] break - return destinationHost \ No newline at end of file + return destinationHost From 83dca2bf51895332f56041a576ed29d5f9ccead5 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 27 Jun 2023 14:57:42 +0530 Subject: [PATCH 6/6] ui: fix vm import for L2 n/w in Setup state (#7628) L2 networks created using offerings that allow using custom VLAN can remain in Setup state. Allow importing of VMs with such networks when VLAN matches with unmanaged instance. Signed-off-by: Abhishek Kumar --- ui/src/views/compute/wizard/MultiNetworkSelection.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/views/compute/wizard/MultiNetworkSelection.vue b/ui/src/views/compute/wizard/MultiNetworkSelection.vue index 227ea34eb14..bdbd23edec1 100644 --- a/ui/src/views/compute/wizard/MultiNetworkSelection.vue +++ b/ui/src/views/compute/wizard/MultiNetworkSelection.vue @@ -193,7 +193,7 @@ export default { for (const item of this.items) { this.validNetworks[item.id] = this.networks if (this.filterUnimplementedNetworks) { - this.validNetworks[item.id] = this.validNetworks[item.id].filter(x => (x.state === 'Implemented' || (x.state === 'Setup' && x.type === 'Shared'))) + this.validNetworks[item.id] = this.validNetworks[item.id].filter(x => (x.state === 'Implemented' || (x.state === 'Setup' && ['Shared', 'L2'].includes(x.type)))) } if (this.filterMatchKey) { this.validNetworks[item.id] = this.validNetworks[item.id].filter(x => x[this.filterMatchKey] === item[this.filterMatchKey])