diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index b85195dafae..e5c938b1603 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -163,6 +163,8 @@ public interface VolumeApiService { Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge); + void destroyVolume(long volumeId); + Volume recoverVolume(long volumeId); void validateCustomDiskOfferingSizeRange(Long sizeInGB); diff --git a/api/src/main/java/com/cloud/vm/UserVmService.java b/api/src/main/java/com/cloud/vm/UserVmService.java index 258e87002da..b9fa2ee4a65 100644 --- a/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/api/src/main/java/com/cloud/vm/UserVmService.java @@ -432,6 +432,14 @@ public interface UserVmService { UserVm createVirtualMachine(DeployVMCmd cmd) throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException, StorageUnavailableException, ResourceAllocationException; + /** + * This API is mostly to trigger VM.CREATE event for deployVirtualMachine with startvm=false, because there is no code in "execute" part of VM creation. + * However, it can be used for additional VM customization in the future. + * @param vmId - Virtual Machine Id + * @return - Virtual Machine + */ + UserVm finalizeCreateVirtualMachine(long vmId); + UserVm getUserVm(long vmId); VirtualMachine getVm(long vmId); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java index 78d6155d012..7aa69bd11ee 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java @@ -753,7 +753,10 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG @Override public String getEventDescription() { - return "starting Vm. Vm Id: " + getEntityUuid(); + if(getStartVm()) { + return "starting Vm. Vm Id: " + getEntityUuid(); + } + return "deploying Vm. Vm Id: " + getEntityUuid(); } @Override @@ -765,28 +768,33 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG public void execute() { UserVm result; - try { - CallContext.current().setEventDetails("Vm Id: " + getEntityUuid()); - result = _userVmService.startVirtualMachine(this); - } catch (ResourceUnavailableException ex) { - s_logger.warn("Exception: ", ex); - throw new ServerApiException(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, ex.getMessage()); - } catch (ResourceAllocationException ex) { - s_logger.warn("Exception: ", ex); - throw new ServerApiException(ApiErrorCode.RESOURCE_ALLOCATION_ERROR, ex.getMessage()); - } catch (ConcurrentOperationException ex) { - s_logger.warn("Exception: ", ex); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); - } catch (InsufficientCapacityException ex) { - StringBuilder message = new StringBuilder(ex.getMessage()); - if (ex instanceof InsufficientServerCapacityException) { - if (((InsufficientServerCapacityException)ex).isAffinityApplied()) { - message.append(", Please check the affinity groups provided, there may not be sufficient capacity to follow them"); + CallContext.current().setEventDetails("Vm Id: " + getEntityUuid()); + if (getStartVm()) { + try { + result = _userVmService.startVirtualMachine(this); + } catch (ResourceUnavailableException ex) { + s_logger.warn("Exception: ", ex); + throw new ServerApiException(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, ex.getMessage()); + } catch (ResourceAllocationException ex) { + s_logger.warn("Exception: ", ex); + throw new ServerApiException(ApiErrorCode.RESOURCE_ALLOCATION_ERROR, ex.getMessage()); + } catch (ConcurrentOperationException ex) { + s_logger.warn("Exception: ", ex); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } catch (InsufficientCapacityException ex) { + StringBuilder message = new StringBuilder(ex.getMessage()); + if (ex instanceof InsufficientServerCapacityException) { + if (((InsufficientServerCapacityException)ex).isAffinityApplied()) { + message.append(", Please check the affinity groups provided, there may not be sufficient capacity to follow them"); + } } + s_logger.info(String.format("%s: %s", message.toString(), ex.getLocalizedMessage())); + s_logger.debug(message.toString(), ex); + throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, message.toString()); } - s_logger.info(ex); - s_logger.info(message.toString(), ex); - throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, message.toString()); + } else { + s_logger.info("VM " + getEntityUuid() + " already created, load UserVm from DB"); + result = _userVmService.finalizeCreateVirtualMachine(getEntityId()); } if (result != null) { 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 d3ba95061b6..cf188cbf58d 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -500,22 +500,29 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac allocateRootVolume(persistedVm, template, rootDiskOfferingInfo, owner, rootDiskSizeFinal); - if (dataDiskOfferings != null) { - for (final DiskOfferingInfo dataDiskOfferingInfo : dataDiskOfferings) { - volumeMgr.allocateRawVolume(Type.DATADISK, "DATA-" + persistedVm.getId(), dataDiskOfferingInfo.getDiskOffering(), dataDiskOfferingInfo.getSize(), - dataDiskOfferingInfo.getMinIops(), dataDiskOfferingInfo.getMaxIops(), persistedVm, template, owner, null); + // 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); + try { + if (dataDiskOfferings != null) { + for (final DiskOfferingInfo dataDiskOfferingInfo : dataDiskOfferings) { + volumeMgr.allocateRawVolume(Type.DATADISK, "DATA-" + persistedVm.getId(), dataDiskOfferingInfo.getDiskOffering(), dataDiskOfferingInfo.getSize(), + dataDiskOfferingInfo.getMinIops(), dataDiskOfferingInfo.getMaxIops(), persistedVm, template, owner, null); + } } - } - if (datadiskTemplateToDiskOfferingMap != null && !datadiskTemplateToDiskOfferingMap.isEmpty()) { - int diskNumber = 1; - for (Entry dataDiskTemplateToDiskOfferingMap : datadiskTemplateToDiskOfferingMap.entrySet()) { - DiskOffering diskOffering = dataDiskTemplateToDiskOfferingMap.getValue(); - long diskOfferingSize = diskOffering.getDiskSize() / (1024 * 1024 * 1024); - VMTemplateVO dataDiskTemplate = _templateDao.findById(dataDiskTemplateToDiskOfferingMap.getKey()); - volumeMgr.allocateRawVolume(Type.DATADISK, "DATA-" + persistedVm.getId() + "-" + String.valueOf(diskNumber), diskOffering, diskOfferingSize, null, null, - persistedVm, dataDiskTemplate, owner, Long.valueOf(diskNumber)); - diskNumber++; + if (datadiskTemplateToDiskOfferingMap != null && !datadiskTemplateToDiskOfferingMap.isEmpty()) { + int diskNumber = 1; + for (Entry dataDiskTemplateToDiskOfferingMap : datadiskTemplateToDiskOfferingMap.entrySet()) { + DiskOffering diskOffering = dataDiskTemplateToDiskOfferingMap.getValue(); + long diskOfferingSize = diskOffering.getDiskSize() / (1024 * 1024 * 1024); + VMTemplateVO dataDiskTemplate = _templateDao.findById(dataDiskTemplateToDiskOfferingMap.getKey()); + volumeMgr.allocateRawVolume(Type.DATADISK, "DATA-" + persistedVm.getId() + "-" + String.valueOf(diskNumber), diskOffering, diskOfferingSize, null, null, + persistedVm, dataDiskTemplate, owner, Long.valueOf(diskNumber)); + diskNumber++; + } } + } finally { + // Remove volumeContext and pop vmContext back + CallContext.unregister(); } if (s_logger.isDebugEnabled()) { diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index 2aa997e13aa..f7c8c9c70bf 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -819,7 +819,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati vol.getTemplateId()); } - @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating ROOT volume", create = true) + @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating volume", create = true) @Override public DiskProfile allocateRawVolume(Type type, String name, DiskOffering offering, Long size, Long minIops, Long maxIops, VirtualMachine vm, VirtualMachineTemplate template, Account owner, Long deviceId) { @@ -1035,13 +1035,13 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati private void updateRootDiskVolumeEventDetails(Type type, VirtualMachine vm, List diskProfiles) { CallContext callContext = CallContext.current(); // Update only for volume type ROOT and API command resource type Volume - if (type == Type.ROOT && callContext != null && callContext.getEventResourceType() == ApiCommandResourceType.Volume) { + if ((type == Type.ROOT || type == Type.DATADISK) && callContext != null && callContext.getEventResourceType() == ApiCommandResourceType.Volume) { List volumeIds = diskProfiles.stream().map(DiskProfile::getVolumeId).filter(volumeId -> volumeId != null).collect(Collectors.toList()); if (!volumeIds.isEmpty()) { callContext.setEventResourceId(volumeIds.get(0)); } String volumeUuids = volumeIds.stream().map(volumeId -> this._uuidMgr.getUuid(Volume.class, volumeId)).collect(Collectors.joining(", ")); - callContext.setEventDetails("Volume Id: " + volumeUuids + " Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, vm.getId())); + callContext.setEventDetails("Volume Type: " + type + "Volume Id: " + volumeUuids + " Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, vm.getId())); } } @@ -1245,7 +1245,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati // Destroy volume if not already destroyed boolean volumeAlreadyDestroyed = (vol.getState() == Volume.State.Destroy || vol.getState() == Volume.State.Expunged || vol.getState() == Volume.State.Expunging); if (!volumeAlreadyDestroyed) { - volService.destroyVolume(vol.getId()); + destroyVolumeInContext(vol); } else { s_logger.debug(String.format("Skipping destroy for the volume [%s] as it is in [%s] state.", volumeToString, vol.getState().toString())); } @@ -1277,6 +1277,21 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati } } + private void destroyVolumeInContext(Volume volume) { + // Create new context and inject correct event resource type, id and details, + // otherwise VOLUME.DESTROY event will be associated with VirtualMachine and contain VM id and other information. + CallContext volumeContext = CallContext.register(CallContext.current(), ApiCommandResourceType.Volume); + volumeContext.setEventDetails("Volume Type: " + volume.getVolumeType() + " Volume Id: " + volume.getUuid() + " Vm Id: " + _uuidMgr.getUuid(VirtualMachine.class, volume.getInstanceId())); + volumeContext.setEventResourceType(ApiCommandResourceType.Volume); + volumeContext.setEventResourceId(volume.getId()); + try { + _volumeApiService.destroyVolume(volume.getId()); + } finally { + // Remove volumeContext and pop vmContext back + CallContext.unregister(); + } + } + @Override public void revokeAccess(DataObject dataObject, Host host, DataStore dataStore) { DataStoreDriver dataStoreDriver = dataStore != null ? dataStore.getDriver() : null; @@ -2080,7 +2095,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati _resourceLimitMgr.decrementResourceCount(volume.getAccountId(), ResourceType.volume, volume.isDisplay()); _resourceLimitMgr.decrementResourceCount(volume.getAccountId(), ResourceType.primary_storage, volume.isDisplay(), new Long(volume.getSize())); } else { - volService.destroyVolume(volume.getId()); + destroyVolumeInContext(volume); } // FIXME - All this is boiler plate code and should be done as part of state transition. This shouldn't be part of orchestrator. // publish usage event for the volume diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 5aad6afcdbb..f1e370de33d 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -721,6 +721,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); Filter searchFilter = new Filter(EventJoinVO.class, "createDate", false, cmd.getStartIndex(), cmd.getPageSizeVal()); + // additional order by since createdDate does not have milliseconds + // and two events, created within one second can be incorrectly ordered (for example VM.CREATE Completed before Scheduled) + searchFilter.addOrderBy(EventJoinVO.class, "id", false); + SearchBuilder sb = _eventJoinDao.createSearchBuilder(); _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index e39fac147e7..e03942edc59 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -1728,6 +1728,12 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic return volume; } + @Override + @ActionEvent(eventType = EventTypes.EVENT_VOLUME_DESTROY, eventDescription = "destroying a volume") + public void destroyVolume(long volumeId) { + volService.destroyVolume(volumeId); + } + @Override @ActionEvent(eventType = EventTypes.EVENT_VOLUME_RECOVER, eventDescription = "recovering a volume in Destroy state") public Volume recoverVolume(long volumeId) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 7ec3450acb1..ecf60556db6 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -3272,7 +3272,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir autoScaleManager.removeVmFromVmGroup(vmId); - deleteVolumesFromVm(volumesToBeDeleted, expunge); + deleteVolumesFromVm(vm, volumesToBeDeleted, expunge); if (getDestroyRootVolumeOnVmDestruction(vm.getDomainId())) { VolumeVO rootVolume = _volsDao.getInstanceRootVolume(vm.getId()); @@ -3716,6 +3716,19 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir dataDiskTemplateToDiskOfferingMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, overrideDiskOfferingId); } + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "deploying Vm") + public UserVm finalizeCreateVirtualMachine(long vmId) { + s_logger.info("Loading UserVm " + vmId + " from DB"); + UserVm userVm = getUserVm(vmId); + if (userVm == null) { + s_logger.info("Loaded UserVm " + vmId + " (" + userVm.getUuid() + ") from DB"); + } else { + s_logger.warn("UserVm " + vmId + " does not exist in DB"); + } + return userVm; + } + private NetworkVO getNetworkToAddToNetworkList(VirtualMachineTemplate template, Account owner, HypervisorType hypervisor, List vpcSupportedHTypes, Long networkId) { NetworkVO network = _networkDao.findById(networkId); @@ -8002,7 +8015,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir // Create new context and inject correct event resource type, id and details, // otherwise VOLUME.DETACH event will be associated with VirtualMachine and contain VM id and other information. CallContext volumeContext = CallContext.register(CallContext.current(), ApiCommandResourceType.Volume); - volumeContext.setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, volume.getId()) + " Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, volume.getInstanceId())); + volumeContext.setEventDetails("Volume Type: " + volume.getVolumeType() + " Volume Id: " + this._uuidMgr.getUuid(Volume.class, volume.getId()) + " Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, volume.getInstanceId())); volumeContext.setEventResourceType(ApiCommandResourceType.Volume); volumeContext.setEventResourceId(volume.getId()); @@ -8020,15 +8033,29 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } - private void deleteVolumesFromVm(List volumes, boolean expunge) { + private void deleteVolumesFromVm(UserVmVO vm, List volumes, boolean expunge) { for (VolumeVO volume : volumes) { + destroyVolumeInContext(vm, expunge, volume); + } + } + private void destroyVolumeInContext(UserVmVO vm, boolean expunge, VolumeVO volume) { + // Create new context and inject correct event resource type, id and details, + // otherwise VOLUME.DESTROY event will be associated with VirtualMachine and contain VM id and other information. + CallContext volumeContext = CallContext.register(CallContext.current(), ApiCommandResourceType.Volume); + volumeContext.setEventDetails("Volume Type: " + volume.getVolumeType() + " Volume Id: " + this._uuidMgr.getUuid(Volume.class, volume.getId()) + " Vm Id: " + vm.getUuid()); + volumeContext.setEventResourceType(ApiCommandResourceType.Volume); + volumeContext.setEventResourceId(volume.getId()); + try { Volume result = _volumeService.destroyVolume(volume.getId(), CallContext.current().getCallingAccount(), expunge, false); if (result == null) { - s_logger.error("DestroyVM remove volume - failed to delete volume " + volume.getInstanceId() + " from instance " + volume.getId()); + s_logger.error(String.format("DestroyVM remove volume - failed to delete volume %s from instance %s", volume.getId(), volume.getInstanceId())); } + } finally { + // Remove volumeContext and pop vmContext back + CallContext.unregister(); } } diff --git a/test/integration/smoke/test_events_resource.py b/test/integration/smoke/test_events_resource.py index 4d6872af57e..660cbd37bce 100644 --- a/test/integration/smoke/test_events_resource.py +++ b/test/integration/smoke/test_events_resource.py @@ -16,6 +16,7 @@ # under the License. """ BVT tests for Events Resource """ +import json import os import tempfile import time @@ -184,7 +185,7 @@ class TestEventsResource(cloudstackTestCase): for event in events: if event.type.startswith("VM.") or (event.type.startswith("NETWORK.") and not event.type.startswith("NETWORK.ELEMENT")) or event.type.startswith("VOLUME.") or event.type.startswith("ACCOUNT.") or event.type.startswith("DOMAIN.") or event.type.startswith("TEMPLATE."): if event.resourceid is None or event.resourcetype is None: - self.debug("Failed event:: %" % event) + self.debug("Failed event:: %s" % json.dumps(event, indent=2)) self.fail("resourceid or resourcetype for the event not found!") else: self.debug("Event %s at %s:: Resource Type: %s, Resource ID: %s" % (event.type, event.created, event.resourcetype, event.resourceid))