This is an automated email from the ASF dual-hosted git repository.
vishesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push:
new 18972caf5f1 api,server: allow cleaning up vm extraconfig (#11974)
18972caf5f1 is described below
commit 18972caf5f1cfc90bdad59669dc7f8cb031ba64d
Author: Abhishek Kumar <[email protected]>
AuthorDate: Fri Jan 30 13:54:01 2026 +0530
api,server: allow cleaning up vm extraconfig (#11974)
---
.../org/apache/cloudstack/api/ApiConstants.java | 1 +
.../api/command/user/vm/UpdateVMCmd.java | 18 ++++-
.../com/cloud/vm/dao/VMInstanceDetailsDao.java | 1 +
.../com/cloud/vm/dao/VMInstanceDetailsDaoImpl.java | 17 +++++
.../cloud/vm/dao/VMInstanceDetailsDaoImplTest.java | 86 ++++++++++++++++++++++
.../main/java/com/cloud/vm/UserVmManagerImpl.java | 28 ++++---
.../java/com/cloud/vm/UserVmManagerImplTest.java | 75 ++++++++++++++++++-
ui/src/views/compute/EditVM.vue | 2 +
8 files changed, 212 insertions(+), 16 deletions(-)
diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index 790070e5244..1ab6fba6081 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -1168,6 +1168,7 @@ public class ApiConstants {
public static final String OVM3_VIP = "ovm3vip";
public static final String CLEAN_UP_DETAILS = "cleanupdetails";
public static final String CLEAN_UP_EXTERNAL_DETAILS =
"cleanupexternaldetails";
+ public static final String CLEAN_UP_EXTRA_CONFIG = "cleanupextraconfig";
public static final String CLEAN_UP_PARAMETERS = "cleanupparameters";
public static final String VIRTUAL_SIZE = "virtualsize";
public static final String NETSCALER_CONTROLCENTER_ID =
"netscalercontrolcenterid";
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java
index ddc2c06eb09..e0da954d381 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java
@@ -163,6 +163,12 @@ public class UpdateVMCmd extends BaseCustomIdCmd
implements SecurityGroupAction,
description = "Lease expiry action, valid values are STOP and
DESTROY")
private String leaseExpiryAction;
+ @Parameter(name = ApiConstants.CLEAN_UP_EXTRA_CONFIG, type =
CommandType.BOOLEAN, since = "4.23.0",
+ description = "Optional boolean field, which indicates if
extraconfig for the instance should be " +
+ "cleaned up or not (If set to true, extraconfig removed
for this instance, extraconfig field " +
+ "ignored; if false or not set, no action)")
+ private Boolean cleanupExtraConfig;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -271,14 +277,18 @@ public class UpdateVMCmd extends BaseCustomIdCmd
implements SecurityGroupAction,
return extraConfig;
}
- /////////////////////////////////////////////////////
- /////////////// API Implementation///////////////////
- /////////////////////////////////////////////////////
-
public Long getOsTypeId() {
return osTypeId;
}
+ public boolean isCleanupExtraConfig() {
+ return Boolean.TRUE.equals(cleanupExtraConfig);
+ }
+
+ /////////////////////////////////////////////////////
+ /////////////// API Implementation///////////////////
+ /////////////////////////////////////////////////////
+
@Override
public String getCommandName() {
return s_name;
diff --git
a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDao.java
b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDao.java
index ea9ac5afba6..4cbdc516ba0 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDao.java
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDao.java
@@ -22,4 +22,5 @@ import com.cloud.utils.db.GenericDao;
import com.cloud.vm.VMInstanceDetailVO;
public interface VMInstanceDetailsDao extends GenericDao<VMInstanceDetailVO,
Long>, ResourceDetailsDao<VMInstanceDetailVO> {
+ int removeDetailsWithPrefix(long vmId, String prefix);
}
diff --git
a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDaoImpl.java
b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDaoImpl.java
index ca11b005fb2..4c2fdd6f8d4 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDaoImpl.java
@@ -17,10 +17,13 @@
package com.cloud.vm.dao;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
import com.cloud.vm.VMInstanceDetailVO;
@Component
@@ -31,4 +34,18 @@ public class VMInstanceDetailsDaoImpl extends
ResourceDetailsDaoBase<VMInstanceD
super.addDetail(new VMInstanceDetailVO(resourceId, key, value,
display));
}
+ @Override
+ public int removeDetailsWithPrefix(long vmId, String prefix) {
+ if (StringUtils.isBlank(prefix)) {
+ return 0;
+ }
+ SearchBuilder<VMInstanceDetailVO> sb = createSearchBuilder();
+ sb.and("vmId", sb.entity().getResourceId(), SearchCriteria.Op.EQ);
+ sb.and("prefix", sb.entity().getName(), SearchCriteria.Op.LIKE);
+ sb.done();
+ SearchCriteria<VMInstanceDetailVO> sc = sb.create();
+ sc.setParameters("vmId", vmId);
+ sc.setParameters("prefix", prefix + "%");
+ return super.remove(sc);
+ }
}
diff --git
a/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDetailsDaoImplTest.java
b/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDetailsDaoImplTest.java
new file mode 100644
index 00000000000..06238e386d5
--- /dev/null
+++
b/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDetailsDaoImplTest.java
@@ -0,0 +1,86 @@
+// 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.vm.dao;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.vm.VMInstanceDetailVO;
+
+@RunWith(MockitoJUnitRunner.class)
+public class VMInstanceDetailsDaoImplTest {
+ @Spy
+ @InjectMocks
+ private VMInstanceDetailsDaoImpl vmInstanceDetailsDaoImpl;
+
+ @Test
+ public void removeDetailsWithPrefixReturnsZeroWhenPrefixIsBlank() {
+ Assert.assertEquals(0,
vmInstanceDetailsDaoImpl.removeDetailsWithPrefix(1L, ""));
+ Assert.assertEquals(0,
vmInstanceDetailsDaoImpl.removeDetailsWithPrefix(1L, " "));
+ Assert.assertEquals(0,
vmInstanceDetailsDaoImpl.removeDetailsWithPrefix(1L, null));
+ }
+
+ @Test
+ public void removeDetailsWithPrefixRemovesMatchingDetails() {
+ SearchBuilder<VMInstanceDetailVO> sb = mock(SearchBuilder.class);
+ VMInstanceDetailVO entity = mock(VMInstanceDetailVO.class);
+ when(sb.entity()).thenReturn(entity);
+ when(sb.and(anyString(), any(),
any(SearchCriteria.Op.class))).thenReturn(sb);
+ SearchCriteria<VMInstanceDetailVO> sc = mock(SearchCriteria.class);
+ when(sb.create()).thenReturn(sc);
+ when(vmInstanceDetailsDaoImpl.createSearchBuilder()).thenReturn(sb);
+ doReturn(3).when(vmInstanceDetailsDaoImpl).remove(sc);
+ int removedCount =
vmInstanceDetailsDaoImpl.removeDetailsWithPrefix(1L, "testPrefix");
+ Assert.assertEquals(3, removedCount);
+ Mockito.verify(sc).setParameters("vmId", 1L);
+ Mockito.verify(sc).setParameters("prefix", "testPrefix%");
+ Mockito.verify(vmInstanceDetailsDaoImpl, Mockito.times(1)).remove(sc);
+ }
+
+ @Test
+ public void removeDetailsWithPrefixDoesNotRemoveWhenNoMatch() {
+ SearchBuilder<VMInstanceDetailVO> sb = mock(SearchBuilder.class);
+ VMInstanceDetailVO entity = mock(VMInstanceDetailVO.class);
+ when(sb.entity()).thenReturn(entity);
+ when(sb.and(anyString(), any(),
any(SearchCriteria.Op.class))).thenReturn(sb);
+ SearchCriteria<VMInstanceDetailVO> sc = mock(SearchCriteria.class);
+ when(sb.create()).thenReturn(sc);
+ when(vmInstanceDetailsDaoImpl.createSearchBuilder()).thenReturn(sb);
+ doReturn(0).when(vmInstanceDetailsDaoImpl).remove(sc);
+
+ int removedCount =
vmInstanceDetailsDaoImpl.removeDetailsWithPrefix(1L, "nonExistentPrefix");
+
+ Assert.assertEquals(0, removedCount);
+ Mockito.verify(sc).setParameters("vmId", 1L);
+ Mockito.verify(sc).setParameters("prefix", "nonExistentPrefix%");
+ Mockito.verify(vmInstanceDetailsDaoImpl, Mockito.times(1)).remove(sc);
+ }
+}
diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
index eccea944fe6..b9b6e5a6839 100644
--- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
+++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
@@ -2866,6 +2866,22 @@ public class UserVmManagerImpl extends ManagerBase
implements UserVmManager, Vir
}
}
+ protected void updateVmExtraConfig(UserVmVO userVm, String extraConfig,
boolean cleanupExtraConfig) {
+ if (cleanupExtraConfig) {
+ logger.info("Cleaning up extraconfig from user vm: {}",
userVm.getUuid());
+ vmInstanceDetailsDao.removeDetailsWithPrefix(userVm.getId(),
ApiConstants.EXTRA_CONFIG);
+ return;
+ }
+ if (StringUtils.isNotBlank(extraConfig)) {
+ if (EnableAdditionalVmConfig.valueIn(userVm.getAccountId())) {
+ logger.info("Adding extra configuration to user vm: {}",
userVm.getUuid());
+ addExtraConfig(userVm, extraConfig);
+ } else {
+ throw new InvalidParameterValueException("attempted setting
extraconfig but enable.additional.vm.configuration is disabled");
+ }
+ }
+ }
+
@Override
@ActionEvent(eventType = EventTypes.EVENT_VM_UPDATE, eventDescription =
"updating Vm")
public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws
ResourceUnavailableException, InsufficientCapacityException {
@@ -2883,6 +2899,7 @@ public class UserVmManagerImpl extends ManagerBase
implements UserVmManager, Vir
List<Long> securityGroupIdList = getSecurityGroupIdList(cmd);
boolean cleanupDetails = cmd.isCleanupDetails();
String extraConfig = cmd.getExtraConfig();
+ boolean cleanupExtraConfig = cmd.isCleanupExtraConfig();
UserVmVO vmInstance = _vmDao.findById(cmd.getId());
VMTemplateVO template =
_templateDao.findById(vmInstance.getTemplateId());
@@ -2919,7 +2936,7 @@ public class UserVmManagerImpl extends ManagerBase
implements UserVmManager, Vir
.map(item -> (item).trim())
.collect(Collectors.toList());
List<VMInstanceDetailVO> existingDetails =
vmInstanceDetailsDao.listDetails(id);
- if (cleanupDetails){
+ if (cleanupDetails) {
if (caller != null && caller.getType() == Account.Type.ADMIN) {
for (final VMInstanceDetailVO detail : existingDetails) {
if (detail != null && detail.isDisplay() &&
!isExtraConfig(detail.getName())) {
@@ -2982,15 +2999,8 @@ public class UserVmManagerImpl extends ManagerBase
implements UserVmManager, Vir
vmInstance.setDetails(details);
_vmDao.saveDetails(vmInstance);
}
- if (StringUtils.isNotBlank(extraConfig)) {
- if (EnableAdditionalVmConfig.valueIn(accountId)) {
- logger.info("Adding extra configuration to user vm: " +
vmInstance.getUuid());
- addExtraConfig(vmInstance, extraConfig);
- } else {
- throw new InvalidParameterValueException("attempted
setting extraconfig but enable.additional.vm.configuration is disabled");
- }
- }
}
+ updateVmExtraConfig(userVm, extraConfig, cleanupExtraConfig);
if (VMLeaseManager.InstanceLeaseEnabled.value() &&
cmd.getLeaseDuration() != null) {
applyLeaseOnUpdateInstance(vmInstance, cmd.getLeaseDuration(),
cmd.getLeaseExpiryAction());
diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
index 4ec9caab5aa..4edafb3a05a 100644
--- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
+++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
@@ -40,8 +40,10 @@ import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
@@ -59,8 +61,6 @@ import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
-import com.cloud.domain.Domain;
-import com.cloud.storage.dao.SnapshotPolicyDao;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.api.ApiCommandResourceType;
@@ -88,6 +88,7 @@ import
org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.Scope;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
+import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.template.VnfTemplateManager;
@@ -117,6 +118,7 @@ import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeployDestination;
import com.cloud.deploy.DeploymentPlanner;
import com.cloud.deploy.DeploymentPlanningManager;
+import com.cloud.domain.Domain;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEventUtils;
@@ -142,9 +144,9 @@ import com.cloud.network.dao.LoadBalancerVMMapDao;
import com.cloud.network.dao.LoadBalancerVMMapVO;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
-import com.cloud.network.element.UserDataServiceProvider;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkVO;
+import com.cloud.network.element.UserDataServiceProvider;
import com.cloud.network.guru.NetworkGuru;
import com.cloud.network.rules.FirewallRuleVO;
import com.cloud.network.rules.PortForwardingRule;
@@ -172,6 +174,7 @@ import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.GuestOSDao;
import com.cloud.storage.dao.SnapshotDao;
+import com.cloud.storage.dao.SnapshotPolicyDao;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.template.VirtualMachineTemplate;
@@ -467,6 +470,24 @@ public class UserVmManagerImplTest {
Class<InvalidParameterValueException>
expectedInvalidParameterValueException = InvalidParameterValueException.class;
Class<CloudRuntimeException> expectedCloudRuntimeException =
CloudRuntimeException.class;
+ private Map<ConfigKey, Object> originalConfigValues = new HashMap<>();
+
+
+ private void updateDefaultConfigValue(final ConfigKey configKey, final
Object o, boolean revert) {
+ try {
+ final String name = "_defaultValue";
+ Field f = ConfigKey.class.getDeclaredField(name);
+ f.setAccessible(true);
+ String stringVal = String.valueOf(o);
+ if (!revert) {
+ originalConfigValues.put(configKey, f.get(configKey));
+ }
+ f.set(configKey, stringVal);
+ } catch (IllegalAccessException | NoSuchFieldException e) {
+ Assert.fail("Failed to mock config " + configKey.key() + " value
due to " + e.getMessage());
+ }
+ }
+
@Before
public void beforeTest() {
userVmManagerImpl.resourceLimitService = resourceLimitMgr;
@@ -496,6 +517,9 @@ public class UserVmManagerImplTest {
public void afterTest() {
CallContext.unregister();
unmanagedVMsManagerMockedStatic.close();
+ for (Map.Entry<ConfigKey, Object> entry :
originalConfigValues.entrySet()) {
+ updateDefaultConfigValue(entry.getKey(), entry.getValue(), true);
+ }
}
@Test
@@ -4178,4 +4202,49 @@ public class UserVmManagerImplTest {
verify(userVmDao, times(1)).releaseFromLockTable(vmId);
}
+ @Test
+ public void updateVmExtraConfigCleansUpWhenCleanupFlagIsTrue() {
+ UserVmVO userVm = mock(UserVmVO.class);
+ when(userVm.getUuid()).thenReturn("test-uuid");
+ when(userVm.getId()).thenReturn(1L);
+
+ userVmManagerImpl.updateVmExtraConfig(userVm, "someConfig", true);
+
+ verify(vmInstanceDetailsDao, times(1)).removeDetailsWithPrefix(1L,
ApiConstants.EXTRA_CONFIG);
+ verifyNoMoreInteractions(vmInstanceDetailsDao);
+ }
+
+ @Test
+ public void updateVmExtraConfigAddsConfigWhenValidAndEnabled() {
+ UserVmVO userVm = mock(UserVmVO.class);
+ when(userVm.getUuid()).thenReturn("test-uuid");
+ when(userVm.getAccountId()).thenReturn(1L);
+
when(userVm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
+ doNothing().when(userVmManagerImpl).persistExtraConfigKvm(anyString(),
eq(userVm));
+ updateDefaultConfigValue(UserVmManagerImpl.EnableAdditionalVmConfig,
true, false);
+
+ userVmManagerImpl.updateVmExtraConfig(userVm, "validConfig", false);
+
+ verify(vmInstanceDetailsDao,
never()).removeDetailsWithPrefix(anyLong(), anyString());
+ verify(userVmManagerImpl, times(1)).addExtraConfig(userVm,
"validConfig");
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void updateVmExtraConfigThrowsExceptionWhenConfigDisabled() {
+ UserVmVO userVm = mock(UserVmVO.class);
+ when(userVm.getAccountId()).thenReturn(1L);
+ updateDefaultConfigValue(UserVmManagerImpl.EnableAdditionalVmConfig,
false, false);
+
+ userVmManagerImpl.updateVmExtraConfig(userVm, "validConfig", false);
+ }
+
+ @Test
+ public void updateVmExtraConfigDoesNothingWhenExtraConfigIsBlank() {
+ UserVmVO userVm = mock(UserVmVO.class);
+
+ userVmManagerImpl.updateVmExtraConfig(userVm, "", false);
+
+ verify(vmInstanceDetailsDao,
never()).removeDetailsWithPrefix(anyLong(), anyString());
+ verify(userVmManagerImpl, never()).addExtraConfig(any(UserVmVO.class),
anyString());
+ }
}
diff --git a/ui/src/views/compute/EditVM.vue b/ui/src/views/compute/EditVM.vue
index 19055afde97..9f08a3368a2 100644
--- a/ui/src/views/compute/EditVM.vue
+++ b/ui/src/views/compute/EditVM.vue
@@ -425,6 +425,8 @@ export default {
}
if (values.extraconfig && values.extraconfig.length > 0) {
params.extraconfig = encodeURIComponent(values.extraconfig)
+ } else if (this.combinedExtraConfig) {
+ params.cleanupextraconfig = true
}
this.loading = true