diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a55596d419b..0848a124794 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -19,7 +19,7 @@ default_stages: [commit, push]
default_language_version:
# force all unspecified Python hooks to run python3
python: python3
-minimum_pre_commit_version: "2.18.0"
+minimum_pre_commit_version: "2.17.0"
repos:
- repo: meta
hooks:
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java
index 1ba182d4d61..3873b4415a8 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java
@@ -87,7 +87,8 @@ public class CreateNetworkOfferingCmd extends BaseCmd {
@Parameter(name = ApiConstants.SPECIFY_VLAN, type = CommandType.BOOLEAN, description = "true if network offering supports vlans")
private Boolean specifyVlan;
- @Parameter(name = ApiConstants.AVAILABILITY, type = CommandType.STRING, description = "the availability of network offering. Default value is Optional")
+ @Parameter(name = ApiConstants.AVAILABILITY, type = CommandType.STRING, description = "the availability of network offering. The default value is Optional. "
+ + " Another value is Required, which will make it as the default network offering for new networks ")
private String availability;
@Parameter(name = ApiConstants.NETWORKRATE, type = CommandType.INTEGER, description = "data transfer rate in megabits per second allowed")
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java
index 57c34822759..e8f9e5f8cfe 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java
@@ -55,7 +55,7 @@ public class UpdateNetworkOfferingCmd extends BaseCmd {
private String displayText;
@Parameter(name = ApiConstants.AVAILABILITY, type = CommandType.STRING, description = "the availability of network offering."
- + " Default value is Required for Guest Virtual network offering; Optional for Guest Direct network offering")
+ + " The value is Required makes this network offering default for Guest Virtual Networks. Only one network offering can have the value Required ")
private String availability;
@Parameter(name = ApiConstants.SORT_KEY, type = CommandType.INTEGER, description = "sort key of the network offering, integer")
diff --git a/client/conf/db.properties.in b/client/conf/db.properties.in
index 572cfbc1ff2..8f31aff17e6 100644
--- a/client/conf/db.properties.in
+++ b/client/conf/db.properties.in
@@ -29,6 +29,10 @@ db.cloud.driver=@DBDRIVER@
db.cloud.port=3306
db.cloud.name=cloud
+# Connection URI to the database "cloud". When this property is set, only the following properties will be used along with it: db.cloud.maxActive, db.cloud.maxIdle, db.cloud.maxWait, db.cloud.username, db.cloud.password, db.cloud.driver, db.cloud.validationQuery, db.cloud.isolation.level. Other properties will be ignored.
+db.cloud.uri=
+
+
# CloudStack database tuning parameters
db.cloud.maxActive=250
db.cloud.maxIdle=30
@@ -61,6 +65,10 @@ db.usage.driver=@DBDRIVER@
db.usage.port=3306
db.usage.name=cloud_usage
+# Connection URI to the database "usage". When this property is set, only the following properties will be used along with it: db.usage.maxActive, db.cloud.maxIdle, db.cloud.maxWait, db.usage.username, db.usage.password, db.usage.driver, db.usage.validationQuery, db.usage.isolation.level. Other properties will be ignored.
+db.usage.uri=
+
+
# usage database tuning parameters
db.usage.maxActive=100
db.usage.maxIdle=30
@@ -79,6 +87,9 @@ db.simulator.maxIdle=30
db.simulator.maxWait=10000
db.simulator.autoReconnect=true
+# Connection URI to the database "simulator". When this property is set, only the following properties will be used along with it: db.simulator.host, db.simulator.port, db.simulator.name, db.simulator.autoReconnect. Other properties will be ignored.
+db.simulator.uri=
+
# High Availability And Cluster Properties
db.ha.enabled=false
diff --git a/client/pom.xml b/client/pom.xml
index 68112f29e4b..05aa7f8b53e 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -842,6 +842,12 @@
false
${project.build.directory}/lib
+
+ com.linbit.linstor.api
+ java-linstor
+ false
+ ${project.build.directory}/lib
+
org.bouncycastle
bctls-jdk15on
@@ -885,6 +891,7 @@
mysql:mysql-connector-java
org.apache.cloudstack:cloud-plugin-storage-volume-storpool
org.apache.cloudstack:cloud-plugin-storage-volume-linstor
+ com.linbit.linstor.api:java-linstor
diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
index 423dc4fdca8..78b94912a0c 100755
--- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
+++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
@@ -581,7 +581,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
allocate(vmInstanceName, template, serviceOffering, new DiskOfferingInfo(diskOffering), new ArrayList<>(), networks, plan, hyperType, null, null);
}
- private VirtualMachineGuru getVmGuru(final VirtualMachine vm) {
+ VirtualMachineGuru getVmGuru(final VirtualMachine vm) {
if(vm != null) {
return _vmGurus.get(vm.getType());
}
@@ -1457,6 +1457,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
}
if (canRetry) {
try {
+ conditionallySetPodToDeployIn(vm);
changeState(vm, Event.OperationFailed, null, work, Step.Done);
} catch (final NoTransitionException e) {
throw new ConcurrentOperationException(e.getMessage());
@@ -1512,7 +1513,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
}
String networkName = String.format("D%s-A%s-Z%s", domain.getId(), acc.getId(), zone.getId());
if (Objects.isNull(networkVO.getVpcId())) {
- networkName += "-S"+networkVO.getId();
+ networkName += "-S" + networkVO.getId();
} else {
VpcVO vpc = vpcDao.findById(networkVO.getVpcId());
if (Objects.isNull(vpc)) {
@@ -1523,6 +1524,24 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
networkToNetworkNameMap.put(networkVO.getId(), networkName);
}
+ /**
+ * Setting pod id to null can result in migration of Volumes across pods. This is not desirable for VMs which
+ * have a volume in Ready state (happens when a VM is shutdown and started again).
+ * So, we set it to null only when
+ * migration of VM across cluster is enabled
+ * Or, volumes are still in allocated state for that VM (happens when VM is Starting/deployed for the first time)
+ */
+ private void conditionallySetPodToDeployIn(VMInstanceVO vm) {
+ if (MIGRATE_VM_ACROSS_CLUSTERS.valueIn(vm.getDataCenterId()) || areAllVolumesAllocated(vm.getId())) {
+ vm.setPodIdToDeployIn(null);
+ }
+ }
+
+ boolean areAllVolumesAllocated(long vmId) {
+ final List vols = _volsDao.findByInstance(vmId);
+ return CollectionUtils.isEmpty(vols) || vols.stream().allMatch(v -> Volume.State.Allocated.equals(v.getState()));
+ }
+
private void logBootModeParameters(Map params) {
if (params == null) {
return;
diff --git a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java
index 1e7cc124643..452f6cd1f4b 100644
--- a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java
+++ b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java
@@ -19,9 +19,12 @@ package com.cloud.vm;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyLong;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
@@ -47,6 +50,23 @@ import com.cloud.network.vpc.VpcVO;
import com.cloud.network.vpc.dao.VpcDao;
import com.cloud.user.AccountVO;
import com.cloud.user.dao.AccountDao;
+import com.cloud.dc.ClusterDetailsDao;
+import com.cloud.dc.ClusterDetailsVO;
+import com.cloud.dc.Pod;
+import com.cloud.deploy.DeployDestination;
+import com.cloud.deploy.DeploymentPlanningManager;
+import com.cloud.hypervisor.HypervisorGuruManager;
+import com.cloud.org.Cluster;
+import com.cloud.template.VirtualMachineTemplate;
+import com.cloud.user.Account;
+import com.cloud.user.User;
+import com.cloud.utils.Journal;
+import com.cloud.utils.Pair;
+import com.cloud.utils.Ternary;
+import com.cloud.utils.db.EntityManager;
+import com.cloud.utils.fsm.StateMachine2;
+import com.cloud.vm.dao.UserVmDetailsDao;
+import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
@@ -58,10 +78,11 @@ import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
-import org.mockito.runners.MockitoJUnitRunner;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Command;
@@ -100,13 +121,14 @@ import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VirtualMachine.State;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
+import org.springframework.test.util.ReflectionTestUtils;
@RunWith(MockitoJUnitRunner.class)
public class VirtualMachineManagerImplTest {
@Spy
@InjectMocks
- private VirtualMachineManagerImpl virtualMachineManagerImpl;
+ private VirtualMachineManagerImpl virtualMachineManagerImpl = new VirtualMachineManagerImpl();
@Mock
private AgentManager agentManagerMock;
@Mock
@@ -177,6 +199,20 @@ public class VirtualMachineManagerImplTest {
private DataCenterDao dcDao;
@Mock
private VpcDao vpcDao;
+ @Mock
+ private EntityManager _entityMgr;
+ @Mock
+ private DeploymentPlanningManager _dpMgr;
+ @Mock
+ private HypervisorGuruManager _hvGuruMgr;
+ @Mock
+ private ClusterDetailsDao _clusterDetailsDao;
+ @Mock
+ private UserVmDetailsDao userVmDetailsDao;
+ @Mock
+ private ItWorkDao _workDao;
+ @Mock
+ protected StateMachine2 _stateMachine;
@Before
public void setup() {
@@ -455,7 +491,7 @@ public class VirtualMachineManagerImplTest {
HashMap userDefinedVolumeToStoragePoolMap = new HashMap<>();
userDefinedVolumeToStoragePoolMap.put(volumeMockId, storagePoolVoMockId);
- Mockito.doNothing().when(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolProvided(Mockito.any(StoragePoolVO.class), Mockito.any(VolumeVO.class), Mockito.any(StoragePoolVO.class));
+ Mockito.doNothing().when(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolProvided(any(StoragePoolVO.class), any(VolumeVO.class), any(StoragePoolVO.class));
Mockito.doReturn(null).when(storagePoolHostDaoMock).findByPoolHost(storagePoolVoMockId, hostMockId);
virtualMachineManagerImpl.buildMapUsingUserInformation(virtualMachineProfileMock, hostMock, userDefinedVolumeToStoragePoolMap);
@@ -467,8 +503,8 @@ public class VirtualMachineManagerImplTest {
HashMap userDefinedVolumeToStoragePoolMap = Mockito.spy(new HashMap<>());
userDefinedVolumeToStoragePoolMap.put(volumeMockId, storagePoolVoMockId);
- Mockito.doNothing().when(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolProvided(Mockito.any(StoragePoolVO.class), Mockito.any(VolumeVO.class),
- Mockito.any(StoragePoolVO.class));
+ Mockito.doNothing().when(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolProvided(any(StoragePoolVO.class), any(VolumeVO.class),
+ any(StoragePoolVO.class));
Mockito.doReturn(Mockito.mock(StoragePoolHostVO.class)).when(storagePoolHostDaoMock).findByPoolHost(storagePoolVoMockId, hostMockId);
Map volumeToPoolObjectMap = virtualMachineManagerImpl.buildMapUsingUserInformation(virtualMachineProfileMock, hostMock, userDefinedVolumeToStoragePoolMap);
@@ -504,7 +540,7 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.executeManagedStorageChecksWhenTargetStoragePoolNotProvided(hostMock, storagePoolVoMock, volumeVoMock);
Mockito.verify(storagePoolVoMock).isManaged();
- Mockito.verify(storagePoolHostDaoMock, Mockito.times(0)).findByPoolHost(Mockito.anyLong(), Mockito.anyLong());
+ Mockito.verify(storagePoolHostDaoMock, Mockito.times(0)).findByPoolHost(anyLong(), anyLong());
}
@Test
@@ -528,15 +564,15 @@ public class VirtualMachineManagerImplTest {
@Test
public void getCandidateStoragePoolsToMigrateLocalVolumeTestLocalVolume() {
- Mockito.doReturn(Mockito.mock(DiskOfferingVO.class)).when(diskOfferingDaoMock).findById(Mockito.anyLong());
+ Mockito.doReturn(Mockito.mock(DiskOfferingVO.class)).when(diskOfferingDaoMock).findById(anyLong());
Mockito.doReturn(true).when(storagePoolVoMock).isLocal();
List poolListMock = new ArrayList<>();
poolListMock.add(storagePoolVoMock);
- Mockito.doReturn(poolListMock).when(storagePoolAllocatorMock).allocateToPool(Mockito.any(DiskProfile.class), Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class),
- Mockito.any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
+ Mockito.doReturn(poolListMock).when(storagePoolAllocatorMock).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
+ any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
List poolList = virtualMachineManagerImpl.getCandidateStoragePoolsToMigrateLocalVolume(virtualMachineProfileMock, dataCenterDeploymentMock, volumeVoMock);
@@ -546,15 +582,15 @@ public class VirtualMachineManagerImplTest {
@Test
public void getCandidateStoragePoolsToMigrateLocalVolumeTestCrossClusterMigration() {
- Mockito.doReturn(Mockito.mock(DiskOfferingVO.class)).when(diskOfferingDaoMock).findById(Mockito.anyLong());
+ Mockito.doReturn(Mockito.mock(DiskOfferingVO.class)).when(diskOfferingDaoMock).findById(anyLong());
Mockito.doReturn(false).when(storagePoolVoMock).isLocal();
List poolListMock = new ArrayList<>();
poolListMock.add(storagePoolVoMock);
- Mockito.doReturn(poolListMock).when(storagePoolAllocatorMock).allocateToPool(Mockito.any(DiskProfile.class), Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class),
- Mockito.any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
+ Mockito.doReturn(poolListMock).when(storagePoolAllocatorMock).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
+ any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
Mockito.doReturn(true).when(virtualMachineManagerImpl).isStorageCrossClusterMigration(clusterMockId, storagePoolVoMock);
List poolList = virtualMachineManagerImpl.getCandidateStoragePoolsToMigrateLocalVolume(virtualMachineProfileMock, dataCenterDeploymentMock, volumeVoMock);
@@ -565,15 +601,15 @@ public class VirtualMachineManagerImplTest {
@Test
public void getCandidateStoragePoolsToMigrateLocalVolumeTestWithinClusterMigration() {
- Mockito.doReturn(Mockito.mock(DiskOfferingVO.class)).when(diskOfferingDaoMock).findById(Mockito.anyLong());
+ Mockito.doReturn(Mockito.mock(DiskOfferingVO.class)).when(diskOfferingDaoMock).findById(anyLong());
Mockito.doReturn(false).when(storagePoolVoMock).isLocal();
List poolListMock = new ArrayList<>();
poolListMock.add(storagePoolVoMock);
- Mockito.doReturn(poolListMock).when(storagePoolAllocatorMock).allocateToPool(Mockito.any(DiskProfile.class), Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class),
- Mockito.any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
+ Mockito.doReturn(poolListMock).when(storagePoolAllocatorMock).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
+ any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
Mockito.doReturn(false).when(virtualMachineManagerImpl).isStorageCrossClusterMigration(clusterMockId, storagePoolVoMock);
List poolList = virtualMachineManagerImpl.getCandidateStoragePoolsToMigrateLocalVolume(virtualMachineProfileMock, dataCenterDeploymentMock, volumeVoMock);
@@ -593,33 +629,33 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.setStoragePoolAllocators(storagePoolAllocatorsMock);
- Mockito.doReturn(Mockito.mock(DiskOfferingVO.class)).when(diskOfferingDaoMock).findById(Mockito.anyLong());
+ Mockito.doReturn(Mockito.mock(DiskOfferingVO.class)).when(diskOfferingDaoMock).findById(anyLong());
Mockito.doReturn(false).when(storagePoolVoMock).isLocal();
List poolListMock = new ArrayList<>();
poolListMock.add(storagePoolVoMock);
- Mockito.doReturn(poolListMock).when(storagePoolAllocatorMock).allocateToPool(Mockito.any(DiskProfile.class), Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class),
- Mockito.any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
+ Mockito.doReturn(poolListMock).when(storagePoolAllocatorMock).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
+ any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
- Mockito.doReturn(null).when(storagePoolAllocatorMock2).allocateToPool(Mockito.any(DiskProfile.class), Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class),
- Mockito.any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
+ Mockito.doReturn(null).when(storagePoolAllocatorMock2).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
+ any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
- Mockito.doReturn(new ArrayList<>()).when(storagePoolAllocatorMock3).allocateToPool(Mockito.any(DiskProfile.class), Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class),
- Mockito.any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
+ Mockito.doReturn(new ArrayList<>()).when(storagePoolAllocatorMock3).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
+ any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
Mockito.doReturn(false).when(virtualMachineManagerImpl).isStorageCrossClusterMigration(clusterMockId, storagePoolVoMock);
List poolList = virtualMachineManagerImpl.getCandidateStoragePoolsToMigrateLocalVolume(virtualMachineProfileMock, dataCenterDeploymentMock, volumeVoMock);
Assert.assertTrue(poolList.isEmpty());
- Mockito.verify(storagePoolAllocatorMock).allocateToPool(Mockito.any(DiskProfile.class), Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class),
- Mockito.any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
- Mockito.verify(storagePoolAllocatorMock2).allocateToPool(Mockito.any(DiskProfile.class), Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class),
- Mockito.any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
- Mockito.verify(storagePoolAllocatorMock3).allocateToPool(Mockito.any(DiskProfile.class), Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class),
- Mockito.any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
+ Mockito.verify(storagePoolAllocatorMock).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
+ any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
+ Mockito.verify(storagePoolAllocatorMock2).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
+ any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
+ Mockito.verify(storagePoolAllocatorMock3).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
+ any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
}
@Test(expected = CloudRuntimeException.class)
@@ -708,8 +744,8 @@ public class VirtualMachineManagerImplTest {
HashMap volumeToPoolObjectMap = new HashMap<>();
Mockito.doReturn(ScopeType.CLUSTER).when(storagePoolVoMock).getScope();
- Mockito.doNothing().when(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolNotProvided(Mockito.any(), Mockito.any(), Mockito.any());
- Mockito.doReturn(false).when(virtualMachineManagerImpl).isStorageCrossClusterMigration(Mockito.anyLong(), Mockito.any());
+ Mockito.doNothing().when(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolNotProvided(any(), any(), any());
+ Mockito.doReturn(false).when(virtualMachineManagerImpl).isStorageCrossClusterMigration(anyLong(), any());
virtualMachineManagerImpl.createStoragePoolMappingsForVolumes(virtualMachineProfileMock, dataCenterDeploymentMock, volumeToPoolObjectMap, allVolumes);
@@ -732,7 +768,7 @@ public class VirtualMachineManagerImplTest {
Mockito.doReturn(volumesNotMapped).when(virtualMachineManagerImpl).findVolumesThatWereNotMappedByTheUser(virtualMachineProfileMock, volumeToPoolObjectMap);
Mockito.doNothing().when(virtualMachineManagerImpl).createStoragePoolMappingsForVolumes(Mockito.eq(virtualMachineProfileMock),
- Mockito.any(DataCenterDeployment.class), Mockito.eq(volumeToPoolObjectMap), Mockito.eq(volumesNotMapped));
+ any(DataCenterDeployment.class), Mockito.eq(volumeToPoolObjectMap), Mockito.eq(volumesNotMapped));
Map mappingVolumeAndStoragePool = virtualMachineManagerImpl.createMappingVolumeAndStoragePool(virtualMachineProfileMock, hostMock, new HashMap<>());
@@ -742,7 +778,7 @@ public class VirtualMachineManagerImplTest {
inOrder.verify(virtualMachineManagerImpl).buildMapUsingUserInformation(Mockito.eq(virtualMachineProfileMock), Mockito.eq(hostMock), Mockito.anyMapOf(Long.class, Long.class));
inOrder.verify(virtualMachineManagerImpl).findVolumesThatWereNotMappedByTheUser(virtualMachineProfileMock, volumeToPoolObjectMap);
inOrder.verify(virtualMachineManagerImpl).createStoragePoolMappingsForVolumes(Mockito.eq(virtualMachineProfileMock),
- Mockito.any(DataCenterDeployment.class), Mockito.eq(volumeToPoolObjectMap), Mockito.eq(volumesNotMapped));
+ any(DataCenterDeployment.class), Mockito.eq(volumeToPoolObjectMap), Mockito.eq(volumesNotMapped));
}
@Test
@@ -796,11 +832,11 @@ public class VirtualMachineManagerImplTest {
private void prepareAndTestIsRootVolumeOnLocalStorage(ScopeType scope, boolean expected) {
StoragePoolVO storagePoolVoMock = Mockito.mock(StoragePoolVO.class);
- Mockito.doReturn(storagePoolVoMock).when(storagePoolDaoMock).findById(Mockito.anyLong());
+ Mockito.doReturn(storagePoolVoMock).when(storagePoolDaoMock).findById(anyLong());
Mockito.doReturn(scope).when(storagePoolVoMock).getScope();
List mockedVolumes = new ArrayList<>();
mockedVolumes.add(volumeVoMock);
- Mockito.doReturn(mockedVolumes).when(volumeDaoMock).findByInstanceAndType(Mockito.anyLong(), Mockito.any());
+ Mockito.doReturn(mockedVolumes).when(volumeDaoMock).findByInstanceAndType(anyLong(), any());
boolean result = virtualMachineManagerImpl.isRootVolumeOnLocalStorage(0l);
@@ -828,7 +864,7 @@ public class VirtualMachineManagerImplTest {
}
private void prepareAndRunCheckIfNewOfferingStorageScopeMatchesStoragePool(boolean isRootOnLocal, boolean isOfferingUsingLocal) {
- Mockito.doReturn(isRootOnLocal).when(virtualMachineManagerImpl).isRootVolumeOnLocalStorage(Mockito.anyLong());
+ Mockito.doReturn(isRootOnLocal).when(virtualMachineManagerImpl).isRootVolumeOnLocalStorage(anyLong());
Mockito.doReturn("vmInstanceMockedToString").when(vmInstanceMock).toString();
Mockito.doReturn(isOfferingUsingLocal).when(diskOfferingMock).isUseLocalStorage();
virtualMachineManagerImpl.checkIfNewOfferingStorageScopeMatchesStoragePool(vmInstanceMock, diskOfferingMock);
@@ -924,7 +960,8 @@ public class VirtualMachineManagerImplTest {
VirtualMachine.Type.User, 1L, HypervisorType.KVM, 1L, 1L, 1L,
1L, false, false);
- VirtualMachineTO vmTO = new VirtualMachineTO() {};
+ VirtualMachineTO vmTO = new VirtualMachineTO() {
+ };
UserVmJoinVO userVm = new UserVmJoinVO();
NetworkVO networkVO = mock(NetworkVO.class);
AccountVO accountVO = mock(AccountVO.class);
@@ -958,4 +995,189 @@ public class VirtualMachineManagerImplTest {
assertEquals(vmTO.getNetworkIdToNetworkNameMap().size(), 1);
assertEquals(vmTO.getNetworkIdToNetworkNameMap().get(5L), "D3-A2-Z1-V4-S5");
}
+
+ public void testOrchestrateStartNonNullPodId() throws Exception {
+ VMInstanceVO vmInstance = new VMInstanceVO();
+ ReflectionTestUtils.setField(vmInstance, "id", 1L);
+ ReflectionTestUtils.setField(vmInstance, "uuid", "vm-uuid");
+ ReflectionTestUtils.setField(vmInstance, "serviceOfferingId", 2L);
+ ReflectionTestUtils.setField(vmInstance, "instanceName", "myVm");
+ ReflectionTestUtils.setField(vmInstance, "hostId", 2L);
+ ReflectionTestUtils.setField(vmInstance, "type", VirtualMachine.Type.User);
+ ReflectionTestUtils.setField(vmInstance, "dataCenterId", 1L);
+ ReflectionTestUtils.setField(vmInstance, "hypervisorType", HypervisorType.KVM);
+
+ VirtualMachineGuru vmGuru = mock(VirtualMachineGuru.class);
+
+ User user = mock(User.class);
+
+ Account account = mock(Account.class);
+
+ ReservationContext ctx = mock(ReservationContext.class);
+
+ ItWorkVO work = mock(ItWorkVO.class);
+
+ ServiceOfferingVO serviceOffering = mock(ServiceOfferingVO.class);
+
+ VirtualMachineTemplate template = mock(VirtualMachineTemplate.class);
+ when(template.isDeployAsIs()).thenReturn(false);
+
+ DataCenterDeployment plan = mock(DataCenterDeployment.class);
+ when(plan.getDataCenterId()).thenReturn(1L);
+ when(plan.getPodId()).thenReturn(1L);
+
+ Map params = new HashMap<>();
+
+ DeploymentPlanner planner = mock(DeploymentPlanner.class);
+
+ when(vmInstanceDaoMock.findByUuid("vm-uuid")).thenReturn(vmInstance);
+
+ doReturn(vmGuru).when(virtualMachineManagerImpl).getVmGuru(vmInstance);
+
+ Ternary start = new Ternary<>(vmInstance, ctx, work);
+ Mockito.doReturn(start).when(virtualMachineManagerImpl).changeToStartState(vmGuru, vmInstance, user, account);
+
+ when(ctx.getJournal()).thenReturn(Mockito.mock(Journal.class));
+
+ when(serviceOfferingDaoMock.findById(vmInstance.getId(), vmInstance.getServiceOfferingId())).thenReturn(serviceOffering);
+
+ when(_entityMgr.findByIdIncludingRemoved(VirtualMachineTemplate.class, vmInstance.getTemplateId())).thenReturn(template);
+
+ Host destHost = mock(Host.class);
+ Pod destPod = mock(Pod.class);
+ DeployDestination dest = mock(DeployDestination.class);
+ when(dest.getHost()).thenReturn(destHost);
+ when(dest.getPod()).thenReturn(destPod);
+ when(dest.getCluster()).thenReturn(mock(Cluster.class));
+ when(destHost.getId()).thenReturn(1L);
+ when(destPod.getId()).thenReturn(2L);
+ when(_dpMgr.planDeployment(any(VirtualMachineProfileImpl.class), any(DataCenterDeployment.class), any(ExcludeList.class), any(DeploymentPlanner.class))).thenReturn(dest);
+
+ doNothing().when(virtualMachineManagerImpl).checkIfTemplateNeededForCreatingVmVolumes(vmInstance);
+
+ when(_workDao.updateStep(any(), any())).thenReturn(true);
+ when(_stateMachine.transitTo(vmInstance, VirtualMachine.Event.OperationRetry, new Pair(vmInstance.getHostId(), 1L), vmInstanceDaoMock)).thenThrow(new CloudRuntimeException("Error while transitioning"));
+ when(_stateMachine.transitTo(vmInstance, VirtualMachine.Event.OperationFailed, new Pair(vmInstance.getHostId(), null), vmInstanceDaoMock)).thenReturn(true);
+
+
+ Cluster cluster = mock(Cluster.class);
+ when(dest.getCluster()).thenReturn(cluster);
+ ClusterDetailsVO cluster_detail_cpu = mock(ClusterDetailsVO.class);
+ ClusterDetailsVO cluster_detail_ram = mock(ClusterDetailsVO.class);
+ when(cluster.getId()).thenReturn(1L);
+ when(_clusterDetailsDao.findDetail(1L, VmDetailConstants.CPU_OVER_COMMIT_RATIO)).thenReturn(cluster_detail_cpu);
+ when(_clusterDetailsDao.findDetail(1L, VmDetailConstants.MEMORY_OVER_COMMIT_RATIO)).thenReturn(cluster_detail_ram);
+ when(userVmDetailsDao.findDetail(anyLong(), Mockito.anyString())).thenReturn(null);
+ when(cluster_detail_cpu.getValue()).thenReturn("1.0");
+ when(cluster_detail_ram.getValue()).thenReturn("1.0");
+ doReturn(false).when(virtualMachineManagerImpl).areAllVolumesAllocated(Mockito.anyLong());
+
+ CallContext callContext = mock(CallContext.class);
+ when(callContext.getCallingAccount()).thenReturn(account);
+ when(callContext.getCallingUser()).thenReturn(user);
+ try (MockedStatic ignored = Mockito.mockStatic(CallContext.class)) {
+ when(CallContext.current()).thenReturn(callContext);
+
+ try {
+ virtualMachineManagerImpl.orchestrateStart("vm-uuid", params, plan, planner);
+ } catch (CloudRuntimeException e) {
+ assertEquals(e.getMessage(), "Error while transitioning");
+ }
+ }
+
+ assertEquals(vmInstance.getPodIdToDeployIn(), (Long) destPod.getId());
+ }
+
+ @Test
+ public void testOrchestrateStartNullPodId() throws Exception {
+ VMInstanceVO vmInstance = new VMInstanceVO();
+ ReflectionTestUtils.setField(vmInstance, "id", 1L);
+ ReflectionTestUtils.setField(vmInstance, "uuid", "vm-uuid");
+ ReflectionTestUtils.setField(vmInstance, "serviceOfferingId", 2L);
+ ReflectionTestUtils.setField(vmInstance, "instanceName", "myVm");
+ ReflectionTestUtils.setField(vmInstance, "hostId", 2L);
+ ReflectionTestUtils.setField(vmInstance, "type", VirtualMachine.Type.User);
+ ReflectionTestUtils.setField(vmInstance, "dataCenterId", 1L);
+ ReflectionTestUtils.setField(vmInstance, "hypervisorType", HypervisorType.KVM);
+
+ VirtualMachineGuru vmGuru = mock(VirtualMachineGuru.class);
+
+ User user = mock(User.class);
+
+ Account account = mock(Account.class);
+
+ ReservationContext ctx = mock(ReservationContext.class);
+
+ ItWorkVO work = mock(ItWorkVO.class);
+
+ ServiceOfferingVO serviceOffering = mock(ServiceOfferingVO.class);
+
+ VirtualMachineTemplate template = mock(VirtualMachineTemplate.class);
+ when(template.isDeployAsIs()).thenReturn(false);
+
+ DataCenterDeployment plan = mock(DataCenterDeployment.class);
+ when(plan.getDataCenterId()).thenReturn(1L);
+ when(plan.getPodId()).thenReturn(1L);
+
+ Map params = new HashMap<>();
+
+ DeploymentPlanner planner = mock(DeploymentPlanner.class);
+
+ when(vmInstanceDaoMock.findByUuid("vm-uuid")).thenReturn(vmInstance);
+
+ doReturn(vmGuru).when(virtualMachineManagerImpl).getVmGuru(vmInstance);
+
+ Ternary start = new Ternary<>(vmInstance, ctx, work);
+ Mockito.doReturn(start).when(virtualMachineManagerImpl).changeToStartState(vmGuru, vmInstance, user, account);
+
+ when(ctx.getJournal()).thenReturn(Mockito.mock(Journal.class));
+
+ when(serviceOfferingDaoMock.findById(vmInstance.getId(), vmInstance.getServiceOfferingId())).thenReturn(serviceOffering);
+
+ when(_entityMgr.findByIdIncludingRemoved(VirtualMachineTemplate.class, vmInstance.getTemplateId())).thenReturn(template);
+
+ Host destHost = mock(Host.class);
+ Pod destPod = mock(Pod.class);
+ DeployDestination dest = mock(DeployDestination.class);
+ when(dest.getHost()).thenReturn(destHost);
+ when(dest.getPod()).thenReturn(destPod);
+ when(dest.getCluster()).thenReturn(mock(Cluster.class));
+ when(destHost.getId()).thenReturn(1L);
+ when(destPod.getId()).thenReturn(2L);
+ when(_dpMgr.planDeployment(any(VirtualMachineProfileImpl.class), any(DataCenterDeployment.class), any(ExcludeList.class), any(DeploymentPlanner.class))).thenReturn(dest);
+
+ doNothing().when(virtualMachineManagerImpl).checkIfTemplateNeededForCreatingVmVolumes(vmInstance);
+
+ when(_workDao.updateStep(any(), any())).thenReturn(true);
+ when(_stateMachine.transitTo(vmInstance, VirtualMachine.Event.OperationRetry, new Pair(vmInstance.getHostId(), 1L), vmInstanceDaoMock)).thenThrow(new CloudRuntimeException("Error while transitioning"));
+ when(_stateMachine.transitTo(vmInstance, VirtualMachine.Event.OperationFailed, new Pair(vmInstance.getHostId(), null), vmInstanceDaoMock)).thenReturn(true);
+
+
+ Cluster cluster = mock(Cluster.class);
+ when(dest.getCluster()).thenReturn(cluster);
+ ClusterDetailsVO cluster_detail_cpu = mock(ClusterDetailsVO.class);
+ ClusterDetailsVO cluster_detail_ram = mock(ClusterDetailsVO.class);
+ when(cluster.getId()).thenReturn(1L);
+ when(_clusterDetailsDao.findDetail(1L, VmDetailConstants.CPU_OVER_COMMIT_RATIO)).thenReturn(cluster_detail_cpu);
+ when(_clusterDetailsDao.findDetail(1L, VmDetailConstants.MEMORY_OVER_COMMIT_RATIO)).thenReturn(cluster_detail_ram);
+ when(userVmDetailsDao.findDetail(anyLong(), Mockito.anyString())).thenReturn(null);
+ when(cluster_detail_cpu.getValue()).thenReturn("1.0");
+ when(cluster_detail_ram.getValue()).thenReturn("1.0");
+ doReturn(true).when(virtualMachineManagerImpl).areAllVolumesAllocated(Mockito.anyLong());
+
+ CallContext callContext = mock(CallContext.class);
+ when(callContext.getCallingAccount()).thenReturn(account);
+ when(callContext.getCallingUser()).thenReturn(user);
+ try (MockedStatic ignored = Mockito.mockStatic(CallContext.class)) {
+ when(CallContext.current()).thenReturn(callContext);
+
+ try {
+ virtualMachineManagerImpl.orchestrateStart("vm-uuid", params, plan, planner);
+ } catch (CloudRuntimeException e) {
+ assertEquals(e.getMessage(), "Error while transitioning");
+ }
+ }
+
+ assertNull(vmInstance.getPodIdToDeployIn());
+ }
}
diff --git a/engine/orchestration/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/engine/orchestration/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000000..1f0955d450f
--- /dev/null
+++ b/engine/orchestration/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/framework/db/pom.xml b/framework/db/pom.xml
index c3f99ae61c0..846c9007708 100644
--- a/framework/db/pom.xml
+++ b/framework/db/pom.xml
@@ -53,6 +53,11 @@
cloud-utils
${project.version}
+
+ org.mariadb.jdbc
+ mariadb-java-client
+ 3.1.4
+
diff --git a/framework/db/src/main/java/com/cloud/utils/db/DriverLoader.java b/framework/db/src/main/java/com/cloud/utils/db/DriverLoader.java
index 34af85fbcbd..55fc1dbb6ed 100644
--- a/framework/db/src/main/java/com/cloud/utils/db/DriverLoader.java
+++ b/framework/db/src/main/java/com/cloud/utils/db/DriverLoader.java
@@ -38,6 +38,7 @@ public class DriverLoader {
DRIVERS.put("jdbc:mysql", "com.mysql.cj.jdbc.Driver");
DRIVERS.put("jdbc:postgresql", "org.postgresql.Driver");
DRIVERS.put("jdbc:h2", "org.h2.Driver");
+ DRIVERS.put("jdbc:mariadb", "org.mariadb.jdbc.Driver");
LOADED_DRIVERS = new ArrayList();
}
diff --git a/framework/db/src/main/java/com/cloud/utils/db/TransactionLegacy.java b/framework/db/src/main/java/com/cloud/utils/db/TransactionLegacy.java
index eb6b09c31f3..df0df60f519 100644
--- a/framework/db/src/main/java/com/cloud/utils/db/TransactionLegacy.java
+++ b/framework/db/src/main/java/com/cloud/utils/db/TransactionLegacy.java
@@ -38,6 +38,7 @@ import org.apache.commons.dbcp2.DriverManagerConnectionFactory;
import org.apache.commons.dbcp2.PoolableConnection;
import org.apache.commons.dbcp2.PoolableConnectionFactory;
import org.apache.commons.dbcp2.PoolingDataSource;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
@@ -1001,7 +1002,7 @@ public class TransactionLegacy implements Closeable {
private static DataSource s_ds;
private static DataSource s_usageDS;
private static DataSource s_simulatorDS;
- private static boolean s_dbHAEnabled;
+ protected static boolean s_dbHAEnabled;
static {
// Initialize with assumed db.properties file
@@ -1032,11 +1033,6 @@ public class TransactionLegacy implements Closeable {
final long cloudMaxWait = Long.parseLong(dbProps.getProperty("db.cloud.maxWait"));
final String cloudUsername = dbProps.getProperty("db.cloud.username");
final String cloudPassword = dbProps.getProperty("db.cloud.password");
- final String cloudHost = dbProps.getProperty("db.cloud.host");
- final String cloudDriver = dbProps.getProperty("db.cloud.driver");
- final int cloudPort = Integer.parseInt(dbProps.getProperty("db.cloud.port"));
- final String cloudDbName = dbProps.getProperty("db.cloud.name");
- final boolean cloudAutoReconnect = Boolean.parseBoolean(dbProps.getProperty("db.cloud.autoReconnect"));
final String cloudValidationQuery = dbProps.getProperty("db.cloud.validationQuery");
final String cloudIsolationLevel = dbProps.getProperty("db.cloud.isolation.level");
@@ -1059,16 +1055,6 @@ public class TransactionLegacy implements Closeable {
final boolean cloudTestWhileIdle = Boolean.parseBoolean(dbProps.getProperty("db.cloud.testWhileIdle"));
final long cloudTimeBtwEvictionRunsMillis = Long.parseLong(dbProps.getProperty("db.cloud.timeBetweenEvictionRunsMillis"));
final long cloudMinEvcitableIdleTimeMillis = Long.parseLong(dbProps.getProperty("db.cloud.minEvictableIdleTimeMillis"));
- final boolean cloudPoolPreparedStatements = Boolean.parseBoolean(dbProps.getProperty("db.cloud.poolPreparedStatements"));
- final String url = dbProps.getProperty("db.cloud.url.params");
-
- String cloudDbHAParams = null;
- String cloudReplicas = null;
- if (s_dbHAEnabled) {
- cloudDbHAParams = getDBHAParams("cloud", dbProps);
- cloudReplicas = dbProps.getProperty("db.cloud.replicas");
- s_logger.info("The replicas configured for Cloud Data base is/are : " + cloudReplicas);
- }
final boolean useSSL = Boolean.parseBoolean(dbProps.getProperty("db.cloud.useSSL"));
if (useSSL) {
@@ -1078,13 +1064,12 @@ public class TransactionLegacy implements Closeable {
System.setProperty("javax.net.ssl.trustStorePassword", dbProps.getProperty("db.cloud.trustStorePassword"));
}
- final String cloudConnectionUri = cloudDriver + "://" + cloudHost + (s_dbHAEnabled ? "," + cloudReplicas : "") + ":" + cloudPort + "/" + cloudDbName +
- "?autoReconnect=" + cloudAutoReconnect + (url != null ? "&" + url : "") + (useSSL ? "&useSSL=true" : "") +
- (s_dbHAEnabled ? "&" + cloudDbHAParams : "") + (s_dbHAEnabled ? "&loadBalanceStrategy=" + loadBalanceStrategy : "");
- DriverLoader.loadDriver(cloudDriver);
+ Pair cloudUriAndDriver = getConnectionUriAndDriver(dbProps, loadBalanceStrategy, useSSL, "cloud");
+
+ DriverLoader.loadDriver(cloudUriAndDriver.second());
// Default Data Source for CloudStack
- s_ds = createDataSource(cloudConnectionUri, cloudUsername, cloudPassword, cloudMaxActive, cloudMaxIdle, cloudMaxWait,
+ s_ds = createDataSource(cloudUriAndDriver.first(), cloudUsername, cloudPassword, cloudMaxActive, cloudMaxIdle, cloudMaxWait,
cloudTimeBtwEvictionRunsMillis, cloudMinEvcitableIdleTimeMillis, cloudTestWhileIdle, cloudTestOnBorrow,
cloudValidationQuery, isolationLevel);
@@ -1094,20 +1079,13 @@ public class TransactionLegacy implements Closeable {
final long usageMaxWait = Long.parseLong(dbProps.getProperty("db.usage.maxWait"));
final String usageUsername = dbProps.getProperty("db.usage.username");
final String usagePassword = dbProps.getProperty("db.usage.password");
- final String usageHost = dbProps.getProperty("db.usage.host");
- final String usageDriver = dbProps.getProperty("db.usage.driver");
- final int usagePort = Integer.parseInt(dbProps.getProperty("db.usage.port"));
- final String usageDbName = dbProps.getProperty("db.usage.name");
- final boolean usageAutoReconnect = Boolean.parseBoolean(dbProps.getProperty("db.usage.autoReconnect"));
- final String usageUrl = dbProps.getProperty("db.usage.url.params");
- final String usageConnectionUri = usageDriver + "://" + usageHost + (s_dbHAEnabled ? "," + dbProps.getProperty("db.cloud.replicas") : "") + ":" + usagePort +
- "/" + usageDbName + "?autoReconnect=" + usageAutoReconnect + (usageUrl != null ? "&" + usageUrl : "") +
- (s_dbHAEnabled ? "&" + getDBHAParams("usage", dbProps) : "") + (s_dbHAEnabled ? "&loadBalanceStrategy=" + loadBalanceStrategy : "");
- DriverLoader.loadDriver(usageDriver);
+ Pair usageUriAndDriver = getConnectionUriAndDriver(dbProps, loadBalanceStrategy, useSSL, "usage");
+
+ DriverLoader.loadDriver(usageUriAndDriver.second());
// Data Source for usage server
- s_usageDS = createDataSource(usageConnectionUri, usageUsername, usagePassword,
+ s_usageDS = createDataSource(usageUriAndDriver.first(), usageUsername, usagePassword,
usageMaxActive, usageMaxIdle, usageMaxWait, null, null, null, null,
null, isolationLevel);
@@ -1118,14 +1096,28 @@ public class TransactionLegacy implements Closeable {
final long simulatorMaxWait = Long.parseLong(dbProps.getProperty("db.simulator.maxWait"));
final String simulatorUsername = dbProps.getProperty("db.simulator.username");
final String simulatorPassword = dbProps.getProperty("db.simulator.password");
- final String simulatorHost = dbProps.getProperty("db.simulator.host");
- final String simulatorDriver = dbProps.getProperty("db.simulator.driver");
- final int simulatorPort = Integer.parseInt(dbProps.getProperty("db.simulator.port"));
- final String simulatorDbName = dbProps.getProperty("db.simulator.name");
- final boolean simulatorAutoReconnect = Boolean.parseBoolean(dbProps.getProperty("db.simulator.autoReconnect"));
- final String simulatorConnectionUri = simulatorDriver + "://" + simulatorHost + ":" + simulatorPort + "/" + simulatorDbName + "?autoReconnect=" +
- simulatorAutoReconnect;
+ String simulatorDriver;
+ String simulatorConnectionUri;
+ String simulatorUri = dbProps.getProperty("db.simulator.uri");
+
+ if (StringUtils.isEmpty(simulatorUri)) {
+ simulatorDriver = dbProps.getProperty("db.simulator.driver");
+ final int simulatorPort = Integer.parseInt(dbProps.getProperty("db.simulator.port"));
+ final String simulatorDbName = dbProps.getProperty("db.simulator.name");
+ final boolean simulatorAutoReconnect = Boolean.parseBoolean(dbProps.getProperty("db.simulator.autoReconnect"));
+ final String simulatorHost = dbProps.getProperty("db.simulator.host");
+
+ simulatorConnectionUri = simulatorDriver + "://" + simulatorHost + ":" + simulatorPort + "/" + simulatorDbName + "?autoReconnect=" +
+ simulatorAutoReconnect;
+ } else {
+ s_logger.warn("db.simulator.uri was set, ignoring the following properties on db.properties: [db.simulator.driver, db.simulator.host, db.simulator.port, "
+ + "db.simulator.name, db.simulator.autoReconnect].");
+ String[] splitUri = simulatorUri.split(":");
+ simulatorDriver = String.format("%s:%s", splitUri[0], splitUri[1]);
+ simulatorConnectionUri = simulatorUri;
+ }
+
DriverLoader.loadDriver(simulatorDriver);
s_simulatorDS = createDataSource(simulatorConnectionUri, simulatorUsername, simulatorPassword,
@@ -1143,6 +1135,85 @@ public class TransactionLegacy implements Closeable {
}
}
+ protected static Pair getConnectionUriAndDriver(Properties dbProps, String loadBalanceStrategy, boolean useSSL, String schema) {
+ String connectionUri;
+ String driver;
+ String propertyUri = dbProps.getProperty(String.format("db.%s.uri", schema));
+
+ if (StringUtils.isEmpty(propertyUri)) {
+ driver = dbProps.getProperty(String.format("db.%s.driver", schema));
+ connectionUri = getPropertiesAndBuildConnectionUri(dbProps, loadBalanceStrategy, driver, useSSL, schema);
+ } else {
+ s_logger.warn(String.format("db.%s.uri was set, ignoring the following properties for schema %s of db.properties: [host, port, name, driver, autoReconnect, url.params,"
+ + " replicas, ha.loadBalanceStrategy, ha.enable, failOverReadOnly, reconnectAtTxEnd, autoReconnectForPools, secondsBeforeRetrySource, queriesBeforeRetrySource, "
+ + "initialTimeout].", schema, schema));
+
+ String[] splitUri = propertyUri.split(":");
+ driver = String.format("%s:%s", splitUri[0], splitUri[1]);
+
+ connectionUri = propertyUri;
+ }
+ s_logger.info(String.format("Using the following URI to connect to %s database [%s].", schema, connectionUri));
+ return new Pair<>(connectionUri, driver);
+ }
+
+ protected static String getPropertiesAndBuildConnectionUri(Properties dbProps, String loadBalanceStrategy, String driver, boolean useSSL, String schema) {
+ String host = dbProps.getProperty(String.format("db.%s.host", schema));
+ int port = Integer.parseInt(dbProps.getProperty(String.format("db.%s.port", schema)));
+ String dbName = dbProps.getProperty(String.format("db.%s.name", schema));
+ boolean autoReconnect = Boolean.parseBoolean(dbProps.getProperty(String.format("db.%s.autoReconnect", schema)));
+ String urlParams = dbProps.getProperty(String.format("db.%s.url.params", schema));
+
+ String replicas = null;
+ String dbHaParams = null;
+ if (s_dbHAEnabled) {
+ dbHaParams = getDBHAParams(schema, dbProps);
+ replicas = dbProps.getProperty(String.format("db.%s.replicas", schema));
+ s_logger.info(String.format("The replicas configured for %s data base are %s.", schema, replicas));
+ }
+
+ return buildConnectionUri(loadBalanceStrategy, driver, useSSL, host, replicas, port, dbName, autoReconnect, urlParams, dbHaParams);
+ }
+
+ protected static String buildConnectionUri(String loadBalanceStrategy, String driver, boolean useSSL, String host, String replicas, int port, String dbName, boolean autoReconnect,
+ String urlParams, String dbHaParams) {
+
+ StringBuilder connectionUri = new StringBuilder();
+ connectionUri.append(driver);
+ connectionUri.append("://");
+ connectionUri.append(host);
+
+ if (s_dbHAEnabled) {
+ connectionUri.append(",");
+ connectionUri.append(replicas);
+ }
+
+ connectionUri.append(":");
+ connectionUri.append(port);
+ connectionUri.append("/");
+ connectionUri.append(dbName);
+ connectionUri.append("?autoReconnect=");
+ connectionUri.append(autoReconnect);
+
+ if (urlParams != null) {
+ connectionUri.append("&");
+ connectionUri.append(urlParams);
+ }
+
+ if (useSSL) {
+ connectionUri.append("&useSSL=true");
+ }
+
+ if (s_dbHAEnabled) {
+ connectionUri.append("&");
+ connectionUri.append(dbHaParams);
+ connectionUri.append("&loadBalanceStrategy=");
+ connectionUri.append(loadBalanceStrategy);
+ }
+
+ return connectionUri.toString();
+ }
+
/**
* Creates a data source
*/
diff --git a/framework/db/src/test/java/com/cloud/utils/db/TransactionLegacyTest.java b/framework/db/src/test/java/com/cloud/utils/db/TransactionLegacyTest.java
new file mode 100644
index 00000000000..2e0af6fa186
--- /dev/null
+++ b/framework/db/src/test/java/com/cloud/utils/db/TransactionLegacyTest.java
@@ -0,0 +1,117 @@
+// 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.utils.db;
+
+import com.cloud.utils.Pair;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Properties;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TransactionLegacyTest {
+
+ Properties properties;
+
+ @Before
+ public void setup(){
+ properties = new Properties();
+ properties.setProperty("db.cloud.host", "host");
+ properties.setProperty("db.cloud.port", "5555");
+ properties.setProperty("db.cloud.name", "name");
+ properties.setProperty("db.cloud.autoReconnect", "false");
+ properties.setProperty("db.cloud.url.params", "someParams");
+ TransactionLegacy.s_dbHAEnabled = false;
+ }
+ @Test
+ public void getConnectionUriAndDriverTestWithoutUri() {
+ properties.setProperty("db.cloud.uri", "");
+ properties.setProperty("db.cloud.driver", "driver");
+
+ Pair result = TransactionLegacy.getConnectionUriAndDriver(properties, null, false, "cloud");
+
+ Assert.assertEquals("driver://host:5555/name?autoReconnect=false&someParams", result.first());
+ Assert.assertEquals("driver", result.second());
+ }
+
+ @Test
+ public void getConnectionUriAndDriverTestWithUri() {
+ properties.setProperty("db.cloud.uri", "jdbc:driver:myFavoriteUri");
+
+ Pair result = TransactionLegacy.getConnectionUriAndDriver(properties, null, false, "cloud");
+
+ Assert.assertEquals("jdbc:driver:myFavoriteUri", result.first());
+ Assert.assertEquals("jdbc:driver", result.second());
+ }
+
+ @Test
+ public void getPropertiesAndBuildConnectionUriTestDbHaDisabled() {
+ String result = TransactionLegacy.getPropertiesAndBuildConnectionUri(properties, "strat", "driver", true, "cloud");
+
+ Assert.assertEquals("driver://host:5555/name?autoReconnect=false&someParams&useSSL=true", result);
+ }
+
+ @Test
+ public void getPropertiesAndBuildConnectionUriTestDbHaEnabled() {
+ TransactionLegacy.s_dbHAEnabled = true;
+ properties.setProperty("db.cloud.failOverReadOnly", "true");
+ properties.setProperty("db.cloud.reconnectAtTxEnd", "false");
+ properties.setProperty("db.cloud.autoReconnectForPools", "true");
+ properties.setProperty("db.cloud.secondsBeforeRetrySource", "25");
+ properties.setProperty("db.cloud.queriesBeforeRetrySource", "105");
+ properties.setProperty("db.cloud.initialTimeout", "1000");
+ properties.setProperty("db.cloud.replicas", "second_host");
+
+ String result = TransactionLegacy.getPropertiesAndBuildConnectionUri(properties, "strat", "driver", true, "cloud");
+
+ Assert.assertEquals("driver://host,second_host:5555/name?autoReconnect=false&someParams&useSSL=true&failOverReadOnly=true&reconnectAtTxEnd=false&autoReconnectFor"
+ + "Pools=true&secondsBeforeRetrySource=25&queriesBeforeRetrySource=105&initialTimeout=1000&loadBalanceStrategy=strat", result);
+ }
+
+ @Test
+ public void buildConnectionUriTestDbHaDisabled() {
+ String result = TransactionLegacy.buildConnectionUri(null, "driver", false, "host", null, 5555, "cloud", false, null, null);
+
+ Assert.assertEquals("driver://host:5555/cloud?autoReconnect=false", result);
+ }
+
+ @Test
+ public void buildConnectionUriTestDbHaEnabled() {
+ TransactionLegacy.s_dbHAEnabled = true;
+
+ String result = TransactionLegacy.buildConnectionUri("strat", "driver", false, "host", "second_host", 5555, "cloud", false, null, "dbHaParams");
+
+ Assert.assertEquals("driver://host,second_host:5555/cloud?autoReconnect=false&dbHaParams&loadBalanceStrategy=strat", result);
+ }
+
+ @Test
+ public void buildConnectionUriTestUrlParamsNotNull() {
+ String result = TransactionLegacy.buildConnectionUri(null, "driver", false, "host", null, 5555, "cloud", false, "urlParams", null);
+
+ Assert.assertEquals("driver://host:5555/cloud?autoReconnect=false&urlParams", result);
+ }
+
+ @Test
+ public void buildConnectionUriTestUseSslTrue() {
+ String result = TransactionLegacy.buildConnectionUri(null, "driver", true, "host", null, 5555, "cloud", false, null, null);
+
+ Assert.assertEquals("driver://host:5555/cloud?autoReconnect=false&useSSL=true", result);
+ }
+}
diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java
index aa53a0573f9..12a50c9e88f 100644
--- a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java
+++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java
@@ -145,7 +145,7 @@ public class CreateKubernetesClusterCmd extends BaseAsyncCreateCmd {
description = "root disk size in GB for each node")
private Long nodeRootDiskSize;
- @Parameter(name = ApiConstants.CLUSTER_TYPE, type = CommandType.STRING, required = true, description = "type of the cluster: CloudManaged, ExternalManaged", since="4.19.0")
+ @Parameter(name = ApiConstants.CLUSTER_TYPE, type = CommandType.STRING, description = "type of the cluster: CloudManaged, ExternalManaged. The default value is CloudManaged.", since="4.19.0")
private String clusterType;
/////////////////////////////////////////////////////
diff --git a/plugins/storage/volume/linstor/pom.xml b/plugins/storage/volume/linstor/pom.xml
index 69de4ff3d67..44e79dbce51 100644
--- a/plugins/storage/volume/linstor/pom.xml
+++ b/plugins/storage/volume/linstor/pom.xml
@@ -43,6 +43,12 @@
cloud-plugin-hypervisor-kvm
${project.version}
+
+ org.apache.cloudstack
+ cloud-engine-storage-snapshot
+ ${project.version}
+ compile
+
diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorBackupSnapshotCommand.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorBackupSnapshotCommand.java
new file mode 100644
index 00000000000..8d887dbba21
--- /dev/null
+++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorBackupSnapshotCommand.java
@@ -0,0 +1,28 @@
+// 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.api.storage;
+
+import com.cloud.agent.api.to.DataTO;
+import org.apache.cloudstack.storage.command.CopyCommand;
+
+public class LinstorBackupSnapshotCommand extends CopyCommand
+{
+ public LinstorBackupSnapshotCommand(DataTO srcData, DataTO destData, int timeout, boolean executeInSequence)
+ {
+ super(srcData, destData, timeout, executeInSequence);
+ }
+}
diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorRevertBackupSnapshotCommand.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorRevertBackupSnapshotCommand.java
new file mode 100644
index 00000000000..1f1880d5da5
--- /dev/null
+++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorRevertBackupSnapshotCommand.java
@@ -0,0 +1,28 @@
+// 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.api.storage;
+
+import com.cloud.agent.api.to.DataTO;
+import org.apache.cloudstack.storage.command.CopyCommand;
+
+public class LinstorRevertBackupSnapshotCommand extends CopyCommand
+{
+ public LinstorRevertBackupSnapshotCommand(DataTO srcData, DataTO destData, int timeout, boolean executeInSequence)
+ {
+ super(srcData, destData, timeout, executeInSequence);
+ }
+}
diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java
new file mode 100644
index 00000000000..a210d53d7e7
--- /dev/null
+++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java
@@ -0,0 +1,167 @@
+// 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.File;
+import java.io.IOException;
+
+import com.cloud.agent.api.to.DataStoreTO;
+import com.cloud.agent.api.to.NfsTO;
+import com.cloud.api.storage.LinstorBackupSnapshotCommand;
+import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.hypervisor.kvm.storage.KVMStoragePool;
+import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
+import com.cloud.resource.CommandWrapper;
+import com.cloud.resource.ResourceWrapper;
+import com.cloud.storage.Storage;
+import com.cloud.utils.script.Script;
+import org.apache.cloudstack.storage.command.CopyCmdAnswer;
+import org.apache.cloudstack.storage.to.SnapshotObjectTO;
+import org.apache.cloudstack.utils.qemu.QemuImg;
+import org.apache.cloudstack.utils.qemu.QemuImgException;
+import org.apache.cloudstack.utils.qemu.QemuImgFile;
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Logger;
+import org.joda.time.Duration;
+import org.libvirt.LibvirtException;
+
+@ResourceWrapper(handles = LinstorBackupSnapshotCommand.class)
+public final class LinstorBackupSnapshotCommandWrapper
+ extends CommandWrapper
+{
+ private static final Logger s_logger = Logger.getLogger(LinstorBackupSnapshotCommandWrapper.class);
+
+ private String zfsSnapdev(boolean hide, String zfsUrl) {
+ Script script = new Script("/usr/bin/zfs", Duration.millis(5000));
+ script.add("set");
+ script.add("snapdev=" + (hide ? "hidden" : "visible"));
+ script.add(zfsUrl.substring(6)); // cutting zfs://
+ return script.execute();
+ }
+
+ private String qemuShrink(String path, long sizeByte, long timeout) {
+ Script qemuImg = new Script("qemu-img", Duration.millis(timeout));
+ qemuImg.add("resize");
+ qemuImg.add("--shrink");
+ qemuImg.add(path);
+ qemuImg.add("" + sizeByte);
+ return qemuImg.execute();
+ }
+
+ static void cleanupSecondaryPool(final KVMStoragePool secondaryPool) {
+ if (secondaryPool != null) {
+ try {
+ secondaryPool.delete();
+ } catch (final Exception e) {
+ s_logger.debug("Failed to delete secondary storage", e);
+ }
+ }
+ }
+
+ private String convertImageToQCow2(
+ final String srcPath,
+ final SnapshotObjectTO dst,
+ final KVMStoragePool secondaryPool,
+ int waitMilliSeconds
+ )
+ throws LibvirtException, QemuImgException, IOException
+ {
+ final String dstDir = secondaryPool.getLocalPath() + File.separator + dst.getPath();
+ FileUtils.forceMkdir(new File(dstDir));
+
+ final String dstPath = dstDir + File.separator + dst.getName();
+ final QemuImgFile srcFile = new QemuImgFile(srcPath, QemuImg.PhysicalDiskFormat.RAW);
+ final QemuImgFile dstFile = new QemuImgFile(dstPath, QemuImg.PhysicalDiskFormat.QCOW2);
+
+ // NOTE: the qemu img will also contain the drbd metadata at the end
+ final QemuImg qemu = new QemuImg(waitMilliSeconds);
+ qemu.convert(srcFile, dstFile);
+ s_logger.info("Backup snapshot " + srcFile + " to " + dstPath);
+ return dstPath;
+ }
+
+ private SnapshotObjectTO setCorrectSnapshotSize(final SnapshotObjectTO dst, final String dstPath) {
+ final File snapFile = new File(dstPath);
+ final long size = snapFile.exists() ? snapFile.length() : 0;
+
+ final SnapshotObjectTO snapshot = new SnapshotObjectTO();
+ snapshot.setPath(dst.getPath() + File.separator + dst.getName());
+ snapshot.setPhysicalSize(size);
+ return snapshot;
+ }
+
+ @Override
+ public CopyCmdAnswer execute(LinstorBackupSnapshotCommand cmd, LibvirtComputingResource serverResource)
+ {
+ s_logger.debug("LinstorBackupSnapshotCommandWrapper: " + cmd.getSrcTO().getPath() + " -> " + cmd.getDestTO().getPath());
+ final SnapshotObjectTO src = (SnapshotObjectTO) cmd.getSrcTO();
+ final SnapshotObjectTO dst = (SnapshotObjectTO) cmd.getDestTO();
+ KVMStoragePool secondaryPool = null;
+ final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr();
+ KVMStoragePool linstorPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.Linstor, src.getDataStore().getUuid());
+ boolean zfsHidden = false;
+ String srcPath = src.getPath();
+
+ if (linstorPool == null) {
+ return new CopyCmdAnswer("Unable to get linstor storage pool from destination volume.");
+ }
+
+ final DataStoreTO dstDataStore = dst.getDataStore();
+ if (!(dstDataStore instanceof NfsTO)) {
+ return new CopyCmdAnswer("Backup Linstor snapshot: Only NFS secondary supported at present!");
+ }
+
+ try
+ {
+ // provide the linstor snapshot block device
+ // on lvm thin this should already be there in /dev/mapper/vg-snapshotname
+ // on zfs we need to unhide the snapshot block device
+ s_logger.info("Src: " + srcPath + " | " + src.getName());
+ if (srcPath.startsWith("zfs://")) {
+ zfsHidden = true;
+ if (zfsSnapdev(false, srcPath) != null) {
+ return new CopyCmdAnswer("Unable to unhide zfs snapshot device.");
+ }
+ srcPath = "/dev/" + srcPath.substring(6);
+ }
+
+ secondaryPool = storagePoolMgr.getStoragePoolByURI(dstDataStore.getUrl());
+
+ String dstPath = convertImageToQCow2(srcPath, dst, secondaryPool, cmd.getWaitInMillSeconds());
+
+ // resize to real volume size, cutting of drbd metadata
+ String result = qemuShrink(dstPath, src.getVolume().getSize(), cmd.getWaitInMillSeconds());
+ if (result != null) {
+ return new CopyCmdAnswer("qemu-img shrink failed: " + result);
+ }
+ s_logger.info("Backup shrunk " + dstPath + " to actual size " + src.getVolume().getSize());
+
+ SnapshotObjectTO snapshot = setCorrectSnapshotSize(dst, dstPath);
+ return new CopyCmdAnswer(snapshot);
+ } catch (final Exception e) {
+ final String error = String.format("Failed to backup snapshot with id [%s] with a pool %s, due to %s",
+ cmd.getSrcTO().getId(), cmd.getSrcTO().getDataStore().getUuid(), e.getMessage());
+ s_logger.error(error);
+ return new CopyCmdAnswer(cmd, e);
+ } finally {
+ cleanupSecondaryPool(secondaryPool);
+ if (zfsHidden) {
+ zfsSnapdev(true, src.getPath());
+ }
+ }
+ }
+}
diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java
new file mode 100644
index 00000000000..511b5a40ca8
--- /dev/null
+++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java
@@ -0,0 +1,92 @@
+// 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.File;
+
+import com.cloud.agent.api.to.DataStoreTO;
+import com.cloud.api.storage.LinstorRevertBackupSnapshotCommand;
+import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.hypervisor.kvm.storage.KVMStoragePool;
+import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
+import com.cloud.resource.CommandWrapper;
+import com.cloud.resource.ResourceWrapper;
+import com.cloud.storage.Storage;
+import org.apache.cloudstack.storage.command.CopyCmdAnswer;
+import org.apache.cloudstack.storage.to.SnapshotObjectTO;
+import org.apache.cloudstack.storage.to.VolumeObjectTO;
+import org.apache.cloudstack.utils.qemu.QemuImg;
+import org.apache.cloudstack.utils.qemu.QemuImgException;
+import org.apache.cloudstack.utils.qemu.QemuImgFile;
+import org.apache.log4j.Logger;
+import org.libvirt.LibvirtException;
+
+@ResourceWrapper(handles = LinstorRevertBackupSnapshotCommand.class)
+public final class LinstorRevertBackupSnapshotCommandWrapper
+ extends CommandWrapper
+{
+ private static final Logger s_logger = Logger.getLogger(LinstorRevertBackupSnapshotCommandWrapper.class);
+
+ private void convertQCow2ToRAW(final String srcPath, final String dstPath, int waitMilliSeconds)
+ throws LibvirtException, QemuImgException
+ {
+ final QemuImgFile srcQemuFile = new QemuImgFile(
+ srcPath, QemuImg.PhysicalDiskFormat.QCOW2);
+ final QemuImg qemu = new QemuImg(waitMilliSeconds);
+ final QemuImgFile dstFile = new QemuImgFile(dstPath, QemuImg.PhysicalDiskFormat.RAW);
+ qemu.convert(srcQemuFile, dstFile);
+ }
+
+ @Override
+ public CopyCmdAnswer execute(LinstorRevertBackupSnapshotCommand cmd, LibvirtComputingResource serverResource)
+ {
+ s_logger.debug("LinstorRevertBackupSnapshotCommandWrapper: " + cmd.getSrcTO().getPath() + " -> " + cmd.getDestTO().getPath());
+ final SnapshotObjectTO src = (SnapshotObjectTO) cmd.getSrcTO();
+ final VolumeObjectTO dst = (VolumeObjectTO) cmd.getDestTO();
+ KVMStoragePool secondaryPool = null;
+ final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr();
+ KVMStoragePool linstorPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.Linstor, dst.getDataStore().getUuid());
+
+ if (linstorPool == null) {
+ return new CopyCmdAnswer("Unable to get linstor storage pool from destination volume.");
+ }
+
+ try
+ {
+ final DataStoreTO srcDataStore = src.getDataStore();
+ File srcFile = new File(src.getPath());
+ secondaryPool = storagePoolMgr.getStoragePoolByURI(
+ srcDataStore.getUrl() + File.separator + srcFile.getParent());
+
+ convertQCow2ToRAW(
+ secondaryPool.getLocalPath() + File.separator + srcFile.getName(),
+ linstorPool.getPhysicalDisk(dst.getPath()).getPath(),
+ cmd.getWaitInMillSeconds());
+
+ final VolumeObjectTO dstVolume = new VolumeObjectTO();
+ dstVolume.setPath(dst.getPath());
+ return new CopyCmdAnswer(dstVolume);
+ } catch (final Exception e) {
+ final String error = String.format("Failed to revert snapshot with id [%s] with a pool %s, due to %s",
+ cmd.getSrcTO().getId(), cmd.getSrcTO().getDataStore().getUuid(), e.getMessage());
+ s_logger.error(error);
+ return new CopyCmdAnswer(cmd, e);
+ } finally {
+ LinstorBackupSnapshotCommandWrapper.cleanupSecondaryPool(secondaryPool);
+ }
+ }
+}
diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java
index 2847484e30e..92b617c7128 100644
--- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java
+++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java
@@ -28,6 +28,7 @@ import java.util.StringJoiner;
import javax.annotation.Nonnull;
+import org.apache.cloudstack.storage.datastore.util.LinstorUtil;
import org.apache.cloudstack.utils.qemu.QemuImg;
import org.apache.cloudstack.utils.qemu.QemuImgException;
import org.apache.cloudstack.utils.qemu.QemuImgFile;
@@ -65,8 +66,8 @@ public class LinstorStorageAdaptor implements StorageAdaptor {
return new DevelopersApi(client);
}
- private String getLinstorRscName(String name) {
- return "cs-" + name;
+ private static String getLinstorRscName(String name) {
+ return LinstorUtil.RSC_PREFIX + name;
}
private String getHostname() {
@@ -214,6 +215,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor {
public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, QemuImg.PhysicalDiskFormat format,
Storage.ProvisioningType provisioningType, long size, byte[] passphrase)
{
+ s_logger.debug(String.format("Linstor.createPhysicalDisk: %s;%s", name, format));
final String rscName = getLinstorRscName(name);
LinstorStoragePool lpool = (LinstorStoragePool) pool;
final DevelopersApi api = getLinstorAPI(pool);
@@ -254,6 +256,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor {
throw new CloudRuntimeException("Linstor: viewResources didn't return resources or volumes.");
}
} catch (ApiException apiEx) {
+ s_logger.error(String.format("Linstor.createPhysicalDisk: ApiException: %s", apiEx.getBestMessage()));
throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx);
}
}
@@ -424,7 +427,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor {
@Override
public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPools, int timeout, byte[] srcPassphrase, byte[] destPassphrase, Storage.ProvisioningType provisioningType)
{
- s_logger.debug("Linstor: copyPhysicalDisk");
+ s_logger.debug(String.format("Linstor.copyPhysicalDisk: %s -> %s", disk.getPath(), name));
final QemuImg.PhysicalDiskFormat sourceFormat = disk.getFormat();
final String sourcePath = disk.getPath();
@@ -433,6 +436,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor {
final KVMPhysicalDisk dstDisk = destPools.createPhysicalDisk(
name, QemuImg.PhysicalDiskFormat.RAW, provisioningType, disk.getVirtualSize(), null);
+ s_logger.debug(String.format("Linstor.copyPhysicalDisk: dstPath: %s", dstDisk.getPath()));
final QemuImgFile destFile = new QemuImgFile(dstDisk.getPath());
destFile.setFormat(dstDisk.getFormat());
destFile.setSize(disk.getVirtualSize());
diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java
index d67b8ab7d5b..c0f3cb4b459 100644
--- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java
+++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java
@@ -28,6 +28,7 @@ import com.linbit.linstor.api.model.ResourceDefinitionCloneStarted;
import com.linbit.linstor.api.model.ResourceDefinitionCreate;
import com.linbit.linstor.api.model.ResourceDefinitionModify;
import com.linbit.linstor.api.model.ResourceGroupSpawn;
+import com.linbit.linstor.api.model.ResourceMakeAvailable;
import com.linbit.linstor.api.model.ResourceWithVolumes;
import com.linbit.linstor.api.model.Snapshot;
import com.linbit.linstor.api.model.SnapshotRestore;
@@ -43,18 +44,26 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.storage.ResizeVolumeAnswer;
import com.cloud.agent.api.storage.ResizeVolumeCommand;
+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.DiskTO;
import com.cloud.agent.api.to.StorageFilerTO;
+import com.cloud.api.storage.LinstorBackupSnapshotCommand;
+import com.cloud.api.storage.LinstorRevertBackupSnapshotCommand;
+import com.cloud.configuration.Config;
import com.cloud.host.Host;
+import com.cloud.host.dao.HostDao;
+import com.cloud.storage.DataStoreRole;
import com.cloud.storage.ResizeVolumePayload;
import com.cloud.storage.SnapshotVO;
import com.cloud.storage.Storage.StoragePoolType;
+import com.cloud.storage.Storage;
import com.cloud.storage.StorageManager;
import com.cloud.storage.StoragePool;
import com.cloud.storage.VMTemplateStoragePoolVO;
@@ -67,8 +76,10 @@ import com.cloud.storage.dao.SnapshotDetailsVO;
import com.cloud.storage.dao.VMTemplatePoolDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.dao.VolumeDetailsDao;
+import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.vm.VirtualMachineManager;
import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
@@ -80,10 +91,14 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
+import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
+import org.apache.cloudstack.storage.RemoteHostEndPoint;
import org.apache.cloudstack.storage.command.CommandResult;
+import org.apache.cloudstack.storage.command.CopyCommand;
import org.apache.cloudstack.storage.command.CreateObjectAnswer;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
+import org.apache.cloudstack.storage.datastore.util.LinstorConfigurationManager;
import org.apache.cloudstack.storage.datastore.util.LinstorUtil;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.volume.VolumeObject;
@@ -98,6 +113,10 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
@Inject private SnapshotDao _snapshotDao;
@Inject private SnapshotDetailsDao _snapshotDetailsDao;
@Inject private StorageManager _storageMgr;
+ @Inject
+ ConfigurationDao _configDao;
+ @Inject
+ private HostDao _hostDao;
public LinstorPrimaryDataStoreDriverImpl()
{
@@ -109,10 +128,12 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
Map mapCapabilities = new HashMap<>();
// Linstor will be restricted to only run on LVM-THIN and ZFS storage pools with ACS
+ // This enables template caching on our primary storage
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_VOLUME.toString(), Boolean.TRUE.toString());
// fetch if lvm-thin or ZFS
- mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.TRUE.toString());
+ boolean system_snapshot = !LinstorConfigurationManager.BackupSnapshots.value();
+ mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.toString(system_snapshot));
// CAN_CREATE_VOLUME_FROM_SNAPSHOT see note from CAN_CREATE_VOLUME_FROM_VOLUME
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString());
@@ -216,6 +237,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
}
throw new CloudRuntimeException("Linstor: Unable to delete snapshot: " + rscDefName);
}
+ s_logger.info("Linstor: Deleted snapshot " + snapshotName + " for resource " + rscDefName);
} catch (ApiException apiEx)
{
s_logger.error("Linstor: ApiEx - " + apiEx.getMessage());
@@ -402,27 +424,46 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
}
}
- private String createResource(VolumeInfo vol, StoragePoolVO storagePoolVO)
- {
- DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress());
- final String rscGrp = storagePoolVO.getUserInfo() != null && !storagePoolVO.getUserInfo().isEmpty() ?
+ private String getRscGrp(StoragePoolVO storagePoolVO) {
+ return storagePoolVO.getUserInfo() != null && !storagePoolVO.getUserInfo().isEmpty() ?
storagePoolVO.getUserInfo() : "DfltRscGrp";
+ }
+ private String createResourceBase(
+ String rscName, long sizeInBytes, String volName, String vmName, DevelopersApi api, String rscGrp) {
ResourceGroupSpawn rscGrpSpawn = new ResourceGroupSpawn();
- final String rscName = LinstorUtil.RSC_PREFIX + vol.getUuid();
rscGrpSpawn.setResourceDefinitionName(rscName);
- rscGrpSpawn.addVolumeSizesItem(vol.getSize() / 1024);
+ rscGrpSpawn.addVolumeSizesItem(sizeInBytes / 1024);
try
{
s_logger.info("Linstor: Spawn resource " + rscName);
- ApiCallRcList answers = linstorApi.resourceGroupSpawn(rscGrp, rscGrpSpawn);
+ ApiCallRcList answers = api.resourceGroupSpawn(rscGrp, rscGrpSpawn);
checkLinstorAnswersThrow(answers);
- applyAuxProps(linstorApi, rscName, vol.getName(), vol.getAttachedVmName());
+ applyAuxProps(api, rscName, volName, vmName);
+
+ return getDeviceName(api, rscName);
+ } catch (ApiException apiEx)
+ {
+ s_logger.error("Linstor: ApiEx - " + apiEx.getMessage());
+ throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx);
+ }
+ }
+
+ private String createResource(VolumeInfo vol, StoragePoolVO storagePoolVO) {
+ DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress());
+ final String rscGrp = getRscGrp(storagePoolVO);
+
+ final String rscName = LinstorUtil.RSC_PREFIX + vol.getUuid();
+ String deviceName = createResourceBase(
+ rscName, vol.getSize(), vol.getName(), vol.getAttachedVmName(), linstorApi, rscGrp);
+
+ try
+ {
applyQoSSettings(storagePoolVO, linstorApi, rscName, vol.getMaxIops());
- return getDeviceName(linstorApi, rscName);
+ return deviceName;
} catch (ApiException apiEx)
{
s_logger.error("Linstor: ApiEx - " + apiEx.getMessage());
@@ -487,8 +528,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
}
private String createResourceFromSnapshot(long csSnapshotId, String rscName, StoragePoolVO storagePoolVO) {
- final String rscGrp = storagePoolVO.getUserInfo() != null && !storagePoolVO.getUserInfo().isEmpty() ?
- storagePoolVO.getUserInfo() : "DfltRscGrp";
+ final String rscGrp = getRscGrp(storagePoolVO);
final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress());
SnapshotVO snapshotVO = _snapshotDao.findById(csSnapshotId);
@@ -654,6 +694,59 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
}
}
+ private String revertSnapshotFromImageStore(
+ final SnapshotInfo snapshot,
+ final VolumeInfo volumeInfo,
+ final DevelopersApi linstorApi,
+ final String rscName)
+ throws ApiException {
+ String resultMsg = null;
+ String value = _configDao.getValue(Config.BackupSnapshotWait.toString());
+ int _backupsnapshotwait = NumbersUtil.parseInt(
+ value, Integer.parseInt(Config.BackupSnapshotWait.getDefaultValue()));
+
+ LinstorRevertBackupSnapshotCommand cmd = new LinstorRevertBackupSnapshotCommand(
+ snapshot.getTO(),
+ volumeInfo.getTO(),
+ _backupsnapshotwait,
+ VirtualMachineManager.ExecuteInSequence.value());
+
+ Optional optEP = getDiskfullEP(linstorApi, rscName);
+ if (optEP.isPresent()) {
+ Answer answer = optEP.get().sendMessage(cmd);
+ if (!answer.getResult()) {
+ resultMsg = answer.getDetails();
+ }
+ } else {
+ resultMsg = "Unable to get matching Linstor endpoint.";
+ }
+ return resultMsg;
+ }
+
+ private String doRevertSnapshot(final SnapshotInfo snapshot, final VolumeInfo volumeInfo) {
+ final StoragePool pool = (StoragePool) volumeInfo.getDataStore();
+ final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(pool.getHostAddress());
+ final String rscName = LinstorUtil.RSC_PREFIX + volumeInfo.getUuid();
+ String resultMsg;
+ try {
+ if (snapshot.getDataStore().getRole() == DataStoreRole.Primary) {
+ final String snapName = LinstorUtil.RSC_PREFIX + snapshot.getUuid();
+
+ ApiCallRcList answers = linstorApi.resourceSnapshotRollback(rscName, snapName);
+ resultMsg = checkLinstorAnswers(answers);
+ } else if (snapshot.getDataStore().getRole() == DataStoreRole.Image) {
+ resultMsg = revertSnapshotFromImageStore(snapshot, volumeInfo, linstorApi, rscName);
+ } else {
+ resultMsg = "Linstor: Snapshot revert datastore not supported";
+ }
+ } catch (ApiException apiEx) {
+ s_logger.error("Linstor: ApiEx - " + apiEx.getMessage());
+ resultMsg = apiEx.getBestMessage();
+ }
+
+ return resultMsg;
+ }
+
@Override
public void revertSnapshot(
SnapshotInfo snapshot,
@@ -670,19 +763,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
return;
}
- String resultMsg;
- try {
- final StoragePool pool = (StoragePool) snapshot.getDataStore();
- final String rscName = LinstorUtil.RSC_PREFIX + volumeInfo.getUuid();
- final String snapName = LinstorUtil.RSC_PREFIX + snapshot.getUuid();
- final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(pool.getHostAddress());
-
- ApiCallRcList answers = linstorApi.resourceSnapshotRollback(rscName, snapName);
- resultMsg = checkLinstorAnswers(answers);
- } catch (ApiException apiEx) {
- s_logger.error("Linstor: ApiEx - " + apiEx.getMessage());
- resultMsg = apiEx.getBestMessage();
- }
+ String resultMsg = doRevertSnapshot(snapshot, volumeInfo);
if (callback != null)
{
@@ -692,24 +773,211 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
}
}
+ private static boolean canCopySnapshotCond(DataObject srcData, DataObject dstData) {
+ return srcData.getType() == DataObjectType.SNAPSHOT && dstData.getType() == DataObjectType.SNAPSHOT
+ && (dstData.getDataStore().getRole() == DataStoreRole.Image
+ || dstData.getDataStore().getRole() == DataStoreRole.ImageCache);
+ }
+
+ private static boolean canCopyTemplateCond(DataObject srcData, DataObject dstData) {
+ return srcData.getType() == DataObjectType.TEMPLATE && dstData.getType() == DataObjectType.TEMPLATE
+ && dstData.getDataStore().getRole() == DataStoreRole.Primary
+ && (srcData.getDataStore().getRole() == DataStoreRole.Image
+ || srcData.getDataStore().getRole() == DataStoreRole.ImageCache);
+ }
+
@Override
- public boolean canCopy(DataObject srcData, DataObject destData)
+ public boolean canCopy(DataObject srcData, DataObject dstData)
{
+ s_logger.debug("LinstorPrimaryDataStoreDriverImpl.canCopy: " + srcData.getType() + " -> " + dstData.getType());
+
+ if (canCopySnapshotCond(srcData, dstData)) {
+ SnapshotInfo sinfo = (SnapshotInfo) srcData;
+ VolumeInfo volume = sinfo.getBaseVolume();
+ StoragePoolVO storagePool = _storagePoolDao.findById(volume.getPoolId());
+ return storagePool.getStorageProviderName().equals(LinstorUtil.PROVIDER_NAME);
+ } else if (canCopyTemplateCond(srcData, dstData)) {
+ TemplateInfo tInfo = (TemplateInfo) dstData;
+ StoragePoolVO storagePoolVO = _storagePoolDao.findById(dstData.getDataStore().getId());
+ return storagePoolVO != null
+ && storagePoolVO.getPoolType() == Storage.StoragePoolType.Linstor
+ && tInfo.getSize() != null;
+ }
return false;
}
@Override
- public void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback)
+ public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCallback callback)
{
- // as long as canCopy is false, this isn't called
- s_logger.debug("Linstor: copyAsync with srcdata: " + srcData.getUuid());
+ s_logger.debug("LinstorPrimaryDataStoreDriverImpl.copyAsync: "
+ + srcData.getType() + " -> " + dstData.getType());
+
+ final CopyCommandResult res;
+ if (canCopySnapshotCond(srcData, dstData)) {
+ String errMsg = null;
+ Answer answer = copySnapshot(srcData, dstData);
+ if (answer != null && !answer.getResult()) {
+ errMsg = answer.getDetails();
+ } else {
+ // delete primary storage snapshot
+ SnapshotInfo sinfo = (SnapshotInfo) srcData;
+ VolumeInfo volume = sinfo.getBaseVolume();
+ deleteSnapshot(
+ srcData.getDataStore(),
+ LinstorUtil.RSC_PREFIX + volume.getUuid(),
+ LinstorUtil.RSC_PREFIX + sinfo.getUuid());
+ }
+ res = new CopyCommandResult(null, answer);
+ res.setResult(errMsg);
+ } else if (canCopyTemplateCond(srcData, dstData)) {
+ Answer answer = copyTemplate(srcData, dstData);
+ res = new CopyCommandResult(null, answer);
+ } else {
+ Answer answer = new Answer(null, false, "noimpl");
+ res = new CopyCommandResult(null, answer);
+ res.setResult("Not implemented yet");
+ }
+ callback.complete(res);
+ }
+
+ private Optional getLinstorEP(DevelopersApi api, String rscName) throws ApiException {
+ List linstorNodeNames = LinstorUtil.getLinstorNodeNames(api);
+ Collections.shuffle(linstorNodeNames); // do not always pick the first linstor node
+
+ Host host = null;
+ for (String nodeName : linstorNodeNames) {
+ host = _hostDao.findByName(nodeName);
+ if (host != null) {
+ s_logger.info(String.format("Linstor: Make resource %s available on node %s ...", rscName, nodeName));
+ ApiCallRcList answers = api.resourceMakeAvailableOnNode(rscName, nodeName, new ResourceMakeAvailable());
+ if (!answers.hasError()) {
+ break; // found working host
+ } else {
+ s_logger.error(
+ String.format("Linstor: Unable to make resource %s on node %s available: %s",
+ rscName,
+ nodeName,
+ LinstorUtil.getBestErrorMessage(answers)));
+ }
+ }
+ }
+
+ if (host == null)
+ {
+ s_logger.error("Linstor: Couldn't create a resource on any cloudstack host.");
+ return Optional.empty();
+ }
+ else
+ {
+ return Optional.of(RemoteHostEndPoint.getHypervisorHostEndPoint(host));
+ }
+ }
+
+ private Optional getDiskfullEP(DevelopersApi api, String rscName) throws ApiException {
+ com.linbit.linstor.api.model.StoragePool linSP =
+ LinstorUtil.getDiskfulStoragePool(api, rscName);
+ if (linSP != null)
+ {
+ Host host = _hostDao.findByName(linSP.getNodeName());
+ if (host == null)
+ {
+ s_logger.error("Linstor: Host '" + linSP.getNodeName() + "' not found.");
+ return Optional.empty();
+ }
+ else
+ {
+ return Optional.of(RemoteHostEndPoint.getHypervisorHostEndPoint(host));
+ }
+ }
+ return Optional.empty();
+ }
+
+ private Answer copyTemplate(DataObject srcData, DataObject dstData) {
+ TemplateInfo tInfo = (TemplateInfo) dstData;
+ final StoragePoolVO pool = _storagePoolDao.findById(dstData.getDataStore().getId());
+ final DevelopersApi api = LinstorUtil.getLinstorAPI(pool.getHostAddress());
+ final String rscName = LinstorUtil.RSC_PREFIX + dstData.getUuid();
+ createResourceBase(
+ LinstorUtil.RSC_PREFIX + dstData.getUuid(),
+ tInfo.getSize(),
+ tInfo.getName(),
+ "",
+ api,
+ getRscGrp(pool));
+
+ int nMaxExecutionMinutes = NumbersUtil.parseInt(
+ _configDao.getValue(Config.SecStorageCmdExecutionTimeMax.key()), 30);
+ CopyCommand cmd = new CopyCommand(
+ srcData.getTO(),
+ dstData.getTO(),
+ nMaxExecutionMinutes * 60 * 1000,
+ VirtualMachineManager.ExecuteInSequence.value());
+ Answer answer;
+
+ try {
+ Optional optEP = getLinstorEP(api, rscName);
+ if (optEP.isPresent()) {
+ answer = optEP.get().sendMessage(cmd);
+ }
+ else {
+ answer = new Answer(cmd, false, "Unable to get matching Linstor endpoint.");
+ }
+ } catch (ApiException exc) {
+ s_logger.error("copy template failed: ", exc);
+ throw new CloudRuntimeException(exc.getBestMessage());
+ }
+ return answer;
+ }
+
+ protected Answer copySnapshot(DataObject srcData, DataObject destData) {
+ String value = _configDao.getValue(Config.BackupSnapshotWait.toString());
+ int _backupsnapshotwait = NumbersUtil.parseInt(
+ value, Integer.parseInt(Config.BackupSnapshotWait.getDefaultValue()));
+
+ SnapshotInfo snapshotInfo = (SnapshotInfo)srcData;
+ Boolean snapshotFullBackup = snapshotInfo.getFullBackup();
+ final StoragePoolVO pool = _storagePoolDao.findById(srcData.getDataStore().getId());
+ final DevelopersApi api = LinstorUtil.getLinstorAPI(pool.getHostAddress());
+ boolean fullSnapshot = true;
+ if (snapshotFullBackup != null) {
+ fullSnapshot = snapshotFullBackup;
+ }
+ Map options = new HashMap<>();
+ options.put("fullSnapshot", fullSnapshot + "");
+ options.put(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.key(),
+ String.valueOf(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value()));
+ options.put("volumeSize", snapshotInfo.getBaseVolume().getSize() + "");
+
+ try {
+ CopyCommand cmd = new LinstorBackupSnapshotCommand(
+ srcData.getTO(),
+ destData.getTO(),
+ _backupsnapshotwait,
+ VirtualMachineManager.ExecuteInSequence.value());
+ cmd.setOptions(options);
+
+ Optional optEP = getDiskfullEP(
+ api, LinstorUtil.RSC_PREFIX + snapshotInfo.getBaseVolume().getUuid());
+ Answer answer;
+ if (optEP.isPresent()) {
+ answer = optEP.get().sendMessage(cmd);
+ } else {
+ answer = new Answer(cmd, false, "Unable to get matching Linstor endpoint.");
+ }
+ return answer;
+ } catch (Exception e) {
+ s_logger.debug("copy snapshot failed: ", e);
+ throw new CloudRuntimeException(e.toString());
+ }
+
}
@Override
public void copyAsync(DataObject srcData, DataObject destData, Host destHost, AsyncCompletionCallback callback)
{
// as long as canCopy is false, this isn't called
- s_logger.debug("Linstor: copyAsync with srcdata: " + srcData.getUuid());
+ s_logger.debug("Linstor: copyAsync with host");
+ copyAsync(srcData, destData, callback);
}
private CreateCmdResult notifyResize(
@@ -794,6 +1062,23 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
s_logger.debug("Linstor: handleQualityOfServiceForVolumeMigration");
}
+ private Answer createAnswerAndPerstistDetails(DevelopersApi api, SnapshotInfo snapshotInfo, String rscName)
+ throws ApiException {
+ SnapshotObjectTO snapshotTO = (SnapshotObjectTO)snapshotInfo.getTO();
+ com.linbit.linstor.api.model.StoragePool linStoragePool = LinstorUtil.getDiskfulStoragePool(api, rscName);
+ if (linStoragePool == null) {
+ throw new CloudRuntimeException("Linstor: Unable to find storage pool for resource " + rscName);
+ }
+
+ final String path = LinstorUtil.getSnapshotPath(linStoragePool, rscName, LinstorUtil.RSC_PREFIX + snapshotInfo.getUuid());
+ snapshotTO.setPath(path);
+ SnapshotDetailsVO details = new SnapshotDetailsVO(
+ snapshotInfo.getId(), snapshotInfo.getUuid(), path, false);
+ _snapshotDetailsDao.persist(details);
+
+ return new CreateObjectAnswer(snapshotTO);
+ }
+
@Override
public void takeSnapshot(SnapshotInfo snapshotInfo, AsyncCompletionCallback callback)
{
@@ -823,12 +1108,11 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
result.setResult(errMsg);
} else
{
- s_logger.info(String.format("Successfully took snapshot from %s", rscName));
+ s_logger.info(String.format("Successfully took snapshot %s from %s", snapshot.getName(), rscName));
- SnapshotObjectTO snapshotObjectTo = (SnapshotObjectTO)snapshotInfo.getTO();
- snapshotObjectTo.setPath(rscName + "-" + snapshotInfo.getName());
+ Answer answer = createAnswerAndPerstistDetails(api, snapshotInfo, rscName);
- result = new CreateCmdResult(null, new CreateObjectAnswer(snapshotObjectTo));
+ result = new CreateCmdResult(null, answer);
result.setResult(null);
}
} catch (ApiException apiExc)
diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java
index de0a16986c5..563f542db37 100644
--- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java
+++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java
@@ -27,16 +27,16 @@ import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreProvider;
import org.apache.cloudstack.storage.datastore.driver.LinstorPrimaryDataStoreDriverImpl;
import org.apache.cloudstack.storage.datastore.lifecycle.LinstorPrimaryDataStoreLifeCycleImpl;
+import org.apache.cloudstack.storage.datastore.util.LinstorUtil;
public class LinstorPrimaryDatastoreProviderImpl implements PrimaryDataStoreProvider {
- private final static String PROVIDER_NAME = "Linstor";
protected PrimaryDataStoreDriver driver;
protected HypervisorHostListener listener;
protected DataStoreLifeCycle lifecycle;
@Override
public String getName() {
- return PROVIDER_NAME;
+ return LinstorUtil.PROVIDER_NAME;
}
@Override
diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java
new file mode 100644
index 00000000000..16cc24a78d4
--- /dev/null
+++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java
@@ -0,0 +1,40 @@
+// 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 org.apache.cloudstack.storage.datastore.util;
+
+import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.cloudstack.framework.config.Configurable;
+
+public class LinstorConfigurationManager implements Configurable
+{
+ public static final ConfigKey BackupSnapshots = new ConfigKey<>(Boolean.class, "lin.backup.snapshots", "Advanced", "false",
+ "Backup Linstor primary storage snapshots to secondary storage (deleting ps snapshot), only works on hyperconverged setups.", true, ConfigKey.Scope.Global, null);
+
+ public static final ConfigKey>[] CONFIG_KEYS = new ConfigKey>[] { BackupSnapshots };
+
+ @Override
+ public String getConfigComponentName()
+ {
+ return LinstorConfigurationManager.class.getSimpleName();
+ }
+
+ @Override
+ public ConfigKey>[] getConfigKeys()
+ {
+ return CONFIG_KEYS;
+ }
+}
diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java
index f1760a003ab..cd2191a6ef1 100644
--- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java
+++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java
@@ -20,12 +20,20 @@ import com.linbit.linstor.api.ApiClient;
import com.linbit.linstor.api.ApiException;
import com.linbit.linstor.api.Configuration;
import com.linbit.linstor.api.DevelopersApi;
+import com.linbit.linstor.api.model.ApiCallRc;
+import com.linbit.linstor.api.model.ApiCallRcList;
+import com.linbit.linstor.api.model.Node;
import com.linbit.linstor.api.model.ProviderKind;
import com.linbit.linstor.api.model.ResourceGroup;
+import com.linbit.linstor.api.model.ResourceWithVolumes;
import com.linbit.linstor.api.model.StoragePool;
+import com.linbit.linstor.api.model.Volume;
+
+import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.List;
+import java.util.stream.Collectors;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.log4j.Logger;
@@ -33,6 +41,7 @@ import org.apache.log4j.Logger;
public class LinstorUtil {
private static final Logger s_logger = Logger.getLogger(LinstorUtil.class);
+ public final static String PROVIDER_NAME = "Linstor";
public static final String RSC_PREFIX = "cs-";
public static final String RSC_GROUP = "resourceGroup";
@@ -47,6 +56,86 @@ public class LinstorUtil {
return new DevelopersApi(client);
}
+ public static String getBestErrorMessage(ApiCallRcList answers) {
+ return answers != null && !answers.isEmpty() ?
+ answers.stream()
+ .filter(ApiCallRc::isError)
+ .findFirst()
+ .map(ApiCallRc::getMessage)
+ .orElse((answers.get(0)).getMessage()) : null;
+ }
+
+ public static List getLinstorNodeNames(@Nonnull DevelopersApi api) throws ApiException
+ {
+ List nodes = api.nodeList(
+ Collections.emptyList(),
+ Collections.emptyList(),
+ null,
+ null
+ );
+
+ return nodes.stream().map(Node::getName).collect(Collectors.toList());
+ }
+
+ public static com.linbit.linstor.api.model.StoragePool
+ getDiskfulStoragePool(@Nonnull DevelopersApi api, @Nonnull String rscName) throws ApiException
+ {
+ List resources = api.viewResources(
+ Collections.emptyList(),
+ Collections.singletonList(rscName),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ null,
+ null);
+
+ String nodeName = null;
+ String storagePoolName = null;
+ for (ResourceWithVolumes rwv : resources) {
+ if (rwv.getVolumes().isEmpty()) {
+ continue;
+ }
+ Volume vol = rwv.getVolumes().get(0);
+ if (vol.getProviderKind() != ProviderKind.DISKLESS) {
+ nodeName = rwv.getNodeName();
+ storagePoolName = vol.getStoragePoolName();
+ break;
+ }
+ }
+
+ if (nodeName == null) {
+ return null;
+ }
+
+ List sps = api.viewStoragePools(
+ Collections.singletonList(nodeName),
+ Collections.singletonList(storagePoolName),
+ Collections.emptyList(),
+ null,
+ null
+ );
+ return !sps.isEmpty() ? sps.get(0) : null;
+ }
+
+ public static String getSnapshotPath(com.linbit.linstor.api.model.StoragePool sp, String rscName, String snapshotName) {
+ final String suffix = "00000";
+ final String backingPool = sp.getProps().get("StorDriver/StorPoolName");
+ final String path;
+ switch (sp.getProviderKind()) {
+ case LVM_THIN:
+ path = String.format("/dev/mapper/%s-%s_%s_%s",
+ backingPool.split("/")[0], rscName.replace("-", "--"), suffix, snapshotName.replace("-", "--"));
+ break;
+ case ZFS:
+ case ZFS_THIN:
+ path = String.format("zfs://%s/%s_%s@%s", backingPool.split("/")[0], rscName, suffix, snapshotName);
+ break;
+ default:
+ throw new CloudRuntimeException(
+ String.format("Linstor: storage pool type %s doesn't support snapshots.", sp.getProviderKind()));
+ }
+ return path;
+ }
+
public static long getCapacityBytes(String linstorUrl, String rscGroupName) {
DevelopersApi linstorApi = getLinstorAPI(linstorUrl);
try {
diff --git a/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml b/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml
index e4fcb456263..48913cdb3f2 100644
--- a/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml
+++ b/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml
@@ -29,4 +29,6 @@
+
diff --git a/pom.xml b/pom.xml
index 365b13a3fbf..6200b8cabe4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -699,6 +699,11 @@
xml-apis
2.0.2
+
+ com.linbit.linstor.api
+ java-linstor
+ ${cs.java-linstor.version}
+
diff --git a/requirements.txt b/requirements.txt
index 187beba2883..96f8c9c1e70 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,10 +15,9 @@
# specific language governing permissions and limitations
# under the License.
-# Install the latest version of cloudmonkey
-cloudmonkey
-
# Marvin dependencies are installed via its bundle
+pre-commit
+
# Install the SolidFire SDK for Python
solidfire-sdk-python
diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java
index e891d932437..3c422659853 100644
--- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java
+++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java
@@ -4004,12 +4004,8 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
throw new InvalidParameterException("Only one isolationMethod can be specified for a physical network at this time");
}
- if (vnetRange != null) {
- // Verify zone type
- if (zoneType == NetworkType.Basic || (zoneType == NetworkType.Advanced && zone.isSecurityGroupEnabled())) {
- throw new InvalidParameterValueException(
- "Can't add vnet range to the physical network in the zone that supports " + zoneType + " network, Security Group enabled: " + zone.isSecurityGroupEnabled());
- }
+ if (vnetRange != null && zoneType == NetworkType.Basic) {
+ throw new InvalidParameterValueException("Can't add vnet range to the physical network in the Basic zone");
}
BroadcastDomainRange broadcastDomainRange = null;
@@ -4132,11 +4128,9 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
if (zone == null) {
throwInvalidIdException("Zone with id=" + network.getDataCenterId() + " doesn't exist in the system", String.valueOf(network.getDataCenterId()), "dataCenterId");
}
- if (newVnetRange != null) {
- if (zone.getNetworkType() == NetworkType.Basic || (zone.getNetworkType() == NetworkType.Advanced && zone.isSecurityGroupEnabled())) {
- throw new InvalidParameterValueException(
- "Can't add vnet range to the physical network in the zone that supports " + zone.getNetworkType() + " network, Security Group enabled: " + zone.isSecurityGroupEnabled());
- }
+
+ if (newVnetRange != null && zone.getNetworkType() == NetworkType.Basic) {
+ throw new InvalidParameterValueException("Can't add vnet range to the physical network in the Basic zone");
}
if (tags != null && tags.size() > 1) {
diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
index 1f8332d6f02..824dac0d567 100644
--- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
+++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
@@ -3665,8 +3665,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
isSecurityGroupEnabledNetworkUsed = true;
}
- if (!(network.getTrafficType() == TrafficType.Guest && network.getGuestType() == Network.GuestType.Shared)) {
- throw new InvalidParameterValueException("Can specify only Shared Guest networks when" + " deploy vm in Advance Security Group enabled zone");
+ if (network.getTrafficType() != TrafficType.Guest || !Arrays.asList(GuestType.Shared, GuestType.L2).contains(network.getGuestType())) {
+ throw new InvalidParameterValueException("Can specify only Shared or L2 Guest networks when deploy vm in Advance Security Group enabled zone");
}
_accountMgr.checkAccess(owner, AccessType.UseEntry, false, network);
diff --git a/ui/src/views/image/RegisterOrUploadTemplate.vue b/ui/src/views/image/RegisterOrUploadTemplate.vue
index 77eb73dc969..ca43a8399c3 100644
--- a/ui/src/views/image/RegisterOrUploadTemplate.vue
+++ b/ui/src/views/image/RegisterOrUploadTemplate.vue
@@ -269,7 +269,7 @@
-
+
0 ? 'osdefault' : ''
@@ -1016,10 +1016,10 @@ export default {
delete this.form.zoneids
}
const formRaw = toRaw(this.form)
- const values = this.handleRemoveFields(formRaw)
+ const formvalues = this.handleRemoveFields(formRaw)
let params = {}
- for (const key in values) {
- const input = values[key]
+ for (const key in formvalues) {
+ const input = formvalues[key]
if (input === undefined) {
continue