api/server: create dummy KVM VM without volume and network is optional

This commit is contained in:
Wei Zhou 2026-01-27 08:32:30 +01:00 committed by Abhishek Kumar
parent c36cd2c26c
commit ca4112e7d0
7 changed files with 86 additions and 14 deletions

View File

@ -216,6 +216,7 @@ public class ApiConstants {
public static final String DOMAIN_PATH = "domainpath";
public static final String DOMAIN_ID = "domainid";
public static final String DOMAIN__ID = "domainId";
public static final String DUMMY = "dummy";
public static final String DURATION = "duration";
public static final String ELIGIBLE = "eligible";
public static final String EMAIL = "email";

View File

@ -64,6 +64,10 @@ public class DeployVMCmd extends BaseDeployVMCmd {
@Parameter(name = ApiConstants.SNAPSHOT_ID, type = CommandType.UUID, entityType = SnapshotResponse.class, since = "4.21")
private Long snapshotId;
@Parameter(name = ApiConstants.DUMMY, type = CommandType.BOOLEAN, since = "4.23", description = "Deploy a dummy VM without any disk. False by default. This supports KVM only.")
private Boolean dummy;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -84,6 +88,10 @@ public class DeployVMCmd extends BaseDeployVMCmd {
return snapshotId;
}
public boolean getDummy() {
return dummy != null && dummy;
}
public boolean isVolumeOrSnapshotProvided() {
return volumeId != null || snapshotId != null;
}
@ -132,7 +140,7 @@ public class DeployVMCmd extends BaseDeployVMCmd {
@Override
public void create() throws ResourceAllocationException {
if (Stream.of(templateId, snapshotId, volumeId).filter(Objects::nonNull).count() != 1) {
if (!getDummy() && Stream.of(templateId, snapshotId, volumeId).filter(Objects::nonNull).count() != 1) {
throw new CloudRuntimeException("Please provide only one of the following parameters - template ID, volume ID or snapshot ID");
}

View File

@ -577,7 +577,13 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
logger.debug("Allocating disks for {}", persistedVm);
allocateRootVolume(persistedVm, template, rootDiskOfferingInfo, owner, rootDiskSizeFinal, volume, snapshot);
if (_userVmMgr.isDummyTemplate(hyperType, template.getId())) {
logger.debug("Template is a dummy template for hypervisor {}, skipping volume allocation", hyperType);
return;
} else {
allocateRootVolume(persistedVm, template, rootDiskOfferingInfo, owner, rootDiskSizeFinal, volume, snapshot);
}
// Create new Volume context and inject event resource type, id and details to generate VOLUME.CREATE event for the ROOT disk.
CallContext volumeContext = CallContext.register(CallContext.current(), ApiCommandResourceType.Volume);

View File

@ -106,4 +106,6 @@ public interface VMTemplateDao extends GenericDao<VMTemplateVO, Long>, StateDao<
VMTemplateVO findActiveSystemTemplateByHypervisorArchAndUrlPath(HypervisorType hypervisorType,
CPU.CPUArch arch, String urlPathSuffix);
VMTemplateVO findByAccountAndName(Long accountId, String templateName);
}

View File

@ -945,4 +945,12 @@ public class VMTemplateDaoImpl extends GenericDaoBase<VMTemplateVO, Long> implem
}
return rows > 0;
}
@Override
public VMTemplateVO findByAccountAndName(Long accountId, String templateName) {
SearchCriteria<VMTemplateVO> sc = NameAccountIdSearch.create();
sc.setParameters("name", templateName);
sc.setParameters("accountId", accountId);
return findOneBy(sc);
}
}

View File

@ -16,13 +16,14 @@
// under the License.
package com.cloud.vm;
import static com.cloud.user.ResourceLimitService.ResourceLimitHostTags;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.cloud.utils.StringUtils;
import org.apache.cloudstack.api.BaseCmd.HTTPMethod;
import org.apache.cloudstack.framework.config.ConfigKey;
@ -40,8 +41,7 @@ import com.cloud.storage.Storage.StoragePoolType;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.uservm.UserVm;
import com.cloud.utils.Pair;
import static com.cloud.user.ResourceLimitService.ResourceLimitHostTags;
import com.cloud.utils.StringUtils;
/**
*
@ -204,4 +204,5 @@ public interface UserVmManager extends UserVmService {
*/
boolean isVMPartOfAnyCKSCluster(VMInstanceVO vm);
boolean isDummyTemplate(HypervisorType hypervisorType, Long templateId);
}

View File

@ -60,9 +60,6 @@ import javax.naming.ConfigurationException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import com.cloud.serializer.GsonHelper;
import com.cloud.storage.SnapshotPolicyVO;
import com.cloud.storage.dao.SnapshotPolicyDao;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.ControlledEntity.ACLType;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
@ -315,6 +312,7 @@ import com.cloud.org.Grouping;
import com.cloud.resource.ResourceManager;
import com.cloud.resource.ResourceState;
import com.cloud.resourcelimit.CheckedReservation;
import com.cloud.serializer.GsonHelper;
import com.cloud.server.ManagementService;
import com.cloud.server.ResourceTag;
import com.cloud.service.ServiceOfferingVO;
@ -324,8 +322,10 @@ import com.cloud.storage.DataStoreRole;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.GuestOSCategoryVO;
import com.cloud.storage.GuestOSVO;
import com.cloud.storage.LaunchPermissionVO;
import com.cloud.storage.ScopeType;
import com.cloud.storage.Snapshot;
import com.cloud.storage.SnapshotPolicyVO;
import com.cloud.storage.SnapshotVO;
import com.cloud.storage.Storage;
import com.cloud.storage.Storage.ImageFormat;
@ -343,7 +343,9 @@ import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.GuestOSCategoryDao;
import com.cloud.storage.dao.GuestOSDao;
import com.cloud.storage.dao.LaunchPermissionDao;
import com.cloud.storage.dao.SnapshotDao;
import com.cloud.storage.dao.SnapshotPolicyDao;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.storage.dao.VMTemplateZoneDao;
import com.cloud.storage.dao.VolumeDao;
@ -421,6 +423,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
private static final long GiB_TO_BYTES = 1024 * 1024 * 1024;
public static final String KVM_VM_DUMMY_TEMPLATE_NAME = "kvm-vm-dummy-template";
@Inject
private EntityManager _entityMgr;
@Inject
@ -617,6 +622,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@Inject
BackupScheduleDao backupScheduleDao;
@Inject
LaunchPermissionDao launchPermissionDao;
@Inject
private UserDataDao userDataDao;
@Inject
protected SnapshotHelper snapshotHelper;
@ -651,6 +658,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
private boolean _instanceNameFlag;
private int _scaleRetry;
private Map<Long, VmAndCountDetails> vmIdCountMap = new ConcurrentHashMap<>();
private static VMTemplateVO KVM_VM_DUMMY_TEMPLATE;
protected static long ROOT_DEVICE_ID = 0;
@ -2498,6 +2506,16 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
_vmIpFetchThreadExecutor = Executors.newFixedThreadPool(VmIpFetchThreadPoolMax.value(), new NamedThreadFactory("vmIpFetchThread"));
KVM_VM_DUMMY_TEMPLATE = _templateDao.findByAccountAndName(Account.ACCOUNT_ID_SYSTEM, KVM_VM_DUMMY_TEMPLATE_NAME);
if (KVM_VM_DUMMY_TEMPLATE == null) {
KVM_VM_DUMMY_TEMPLATE = VMTemplateVO.createSystemIso(_templateDao.getNextInSequence(Long.class, "id"), KVM_VM_DUMMY_TEMPLATE_NAME, KVM_VM_DUMMY_TEMPLATE_NAME, true,
"", true, 64, Account.ACCOUNT_ID_SYSTEM, "",
"Dummy Template for KVM VM", false, 1);
KVM_VM_DUMMY_TEMPLATE.setState(VirtualMachineTemplate.State.Active);
KVM_VM_DUMMY_TEMPLATE.setFormat(ImageFormat.QCOW2);
KVM_VM_DUMMY_TEMPLATE = _templateDao.persist(KVM_VM_DUMMY_TEMPLATE);
}
logger.info("User VM Manager is configured.");
return true;
@ -3927,7 +3945,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
_accountMgr.checkAccess(owner, _diskOfferingDao.findById(diskOfferingId), zone);
// If no network is specified, find system security group enabled network
if (networkIdList == null || networkIdList.isEmpty()) {
if (isDummyTemplate(hypervisor, template.getId())) {
logger.debug("Template is a dummy template for hypervisor {}, skipping network allocation in an advanced security group enabled zone", hypervisor);
} else if (networkIdList == null || networkIdList.isEmpty()) {
Network networkWithSecurityGroup = _networkModel.getNetworkWithSGWithFreeIPs(owner, zone.getId());
if (networkWithSecurityGroup == null) {
throw new InvalidParameterValueException("No network with security enabled is found in zone id=" + zone.getUuid());
@ -4040,7 +4060,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
_accountMgr.checkAccess(owner, diskOffering, zone);
List<HypervisorType> vpcSupportedHTypes = _vpcMgr.getSupportedVpcHypervisors();
if (networkIdList == null || networkIdList.isEmpty()) {
if (isDummyTemplate(hypervisor, template.getId())) {
logger.debug("Template is a dummy template for hypervisor {}, skipping network allocation in an advanced zone", hypervisor);
} else if (networkIdList == null || networkIdList.isEmpty()) {
NetworkVO defaultNetwork = getDefaultNetwork(zone, owner, false);
if (defaultNetwork != null) {
networkList.add(defaultNetwork);
@ -4475,7 +4497,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
}
if (TemplateType.SYSTEM.equals(template.getTemplateType()) && !CKS_NODE.equals(vmType) && !SHAREDFSVM.equals(vmType)) {
if (TemplateType.SYSTEM.equals(template.getTemplateType()) && !CKS_NODE.equals(vmType) && !SHAREDFSVM.equals(vmType) && !isDummyTemplate(hypervisorType, template.getId())) {
throw new InvalidParameterValueException(String.format("Unable to use system template %s to deploy a user vm", template));
}
@ -4488,7 +4510,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
if (CollectionUtils.isEmpty(snapshotsOnZone)) {
throw new InvalidParameterValueException("The snapshot does not exist on zone " + zone.getId());
}
} else {
} else if (!isDummyTemplate(hypervisorType, template.getId())) {
List<VMTemplateZoneVO> listZoneTemplate = _templateZoneDao.listByZoneTemplate(zone.getId(), template.getId());
if (listZoneTemplate == null || listZoneTemplate.isEmpty()) {
throw new InvalidParameterValueException("The template " + template.getId() + " is not available for use");
@ -4603,7 +4625,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
// by Agent Manager in order to configure default
// gateway for the vm
if (defaultNetworkNumber == 0) {
throw new InvalidParameterValueException("At least 1 default network has to be specified for the vm");
if (isDummyTemplate(hypervisorType, template.getId())) {
logger.debug("Template is a dummy template for hypervisor {}, vm can be created without a default network", hypervisorType);
} else {
throw new InvalidParameterValueException("At least 1 default network has to be specified for the vm");
}
} else if (defaultNetworkNumber > 1) {
throw new InvalidParameterValueException("Only 1 default network per vm is supported");
}
@ -5321,7 +5347,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "deploying Vm", async = true)
public UserVm startVirtualMachine(DeployVMCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException, ConcurrentOperationException, ResourceAllocationException {
long vmId = cmd.getEntityId();
if (!cmd.getStartVm()) {
if (!cmd.getStartVm() || cmd.getDummy()) {
return getUserVm(vmId);
}
Long podId = null;
@ -6469,6 +6495,12 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
(!(HypervisorType.KVM.equals(template.getHypervisorType()) || HypervisorType.KVM.equals(cmd.getHypervisor())))) {
throw new InvalidParameterValueException("Deploying a virtual machine with existing volume/snapshot is supported only from KVM hypervisors");
}
if (template == null && HypervisorType.KVM.equals(cmd.getHypervisor()) && cmd.getDummy()) {
template = KVM_VM_DUMMY_TEMPLATE;
logger.info("Creating launch permission for Dummy template");
LaunchPermissionVO launchPermission = new LaunchPermissionVO(KVM_VM_DUMMY_TEMPLATE.getId(), owner.getId());
launchPermissionDao.persist(launchPermission);
}
// Make sure a valid template ID was specified
if (template == null) {
throw new InvalidParameterValueException("Unable to use template " + templateId);
@ -6627,6 +6659,12 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
if (isLeaseFeatureEnabled) {
applyLeaseOnCreateInstance(vm, cmd.getLeaseDuration(), cmd.getLeaseExpiryAction(), svcOffering);
}
if (KVM_VM_DUMMY_TEMPLATE != null && template.getId() == KVM_VM_DUMMY_TEMPLATE.getId() && cmd instanceof DeployVMCmd && ((DeployVMCmd) cmd).getDummy()) {
logger.info("Revoking launch permission for Dummy template");
launchPermissionDao.removePermissions(KVM_VM_DUMMY_TEMPLATE.getId(), Collections.singletonList(owner.getId()));
}
return vm;
}
@ -10061,4 +10099,12 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
vm.setVncPassword(customParameters.get(VmDetailConstants.KVM_VNC_PASSWORD));
}
}
@Override
public boolean isDummyTemplate(HypervisorType hypervisorType, Long templateId) {
if (HypervisorType.KVM.equals(hypervisorType) && KVM_VM_DUMMY_TEMPLATE != null && KVM_VM_DUMMY_TEMPLATE.getId() == templateId) {
return true;
}
return false;
}
}