From 8c1d749360657a4909c558c3df5dec57ca66c977 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Fri, 26 Jun 2020 08:31:43 -0300 Subject: [PATCH] [VMware] Enable unmanaging guest VMs (#4103) * Enable unmanaging guest VMs * Minor fixes * Fix stop usage event only if VM is not stopped when unmanaging * Rename unmanaged VMs manager * Generate netofferingremove usage event if VM is not stopped * Generate usage event VM snapshot primary off when unmanaging --- .../main/java/com/cloud/event/EventTypes.java | 2 + .../main/java/com/cloud/vm/UserVmService.java | 5 + .../com/cloud/vm/VirtualMachineProfile.java | 1 + .../cloud/vm/snapshot/VMSnapshotService.java | 2 +- .../admin/vm/ImportUnmanagedInstanceCmd.java | 10 + .../admin/vm/UnmanageVMInstanceCmd.java | 136 ++++++++++++++ .../response/UnmanageVMInstanceResponse.java | 58 ++++++ .../cloudstack/vm/UnmanageVMService.java | 27 +++ .../cloudstack/vm/UnmanagedVMsManager.java | 29 +++ .../apache/cloudstack/vm/VmImportService.java | 4 +- .../api/PrepareUnmanageVMInstanceAnswer.java | 27 +++ .../api/PrepareUnmanageVMInstanceCommand.java | 39 ++++ .../java/com/cloud/vm/VirtualMachineGuru.java | 2 + .../com/cloud/vm/VirtualMachineManager.java | 7 + .../service/NetworkOrchestrationService.java | 4 +- .../service/VolumeOrchestrationService.java | 5 + .../api/storage/VMSnapshotStrategy.java | 2 +- .../subsystem/api/storage/VolumeService.java | 2 + .../cloud/vm/VirtualMachineManagerImpl.java | 87 ++++++++- .../orchestration/NetworkOrchestrator.java | 47 ++++- .../orchestration/VolumeOrchestrator.java | 23 +++ .../NetworkOrchestratorTest.java | 20 ++ .../vmsnapshot/DefaultVMSnapshotStrategy.java | 7 +- .../storage/volume/VolumeServiceImpl.java | 17 ++ .../vmware/resource/VmwareResource.java | 26 +++ .../lb/ElasticLoadBalancerManagerImpl.java | 4 + .../lb/InternalLoadBalancerVMManagerImpl.java | 4 + .../network/vm/NetScalerVMManagerImpl.java | 4 + .../consoleproxy/ConsoleProxyManagerImpl.java | 4 + .../VirtualNetworkApplianceManagerImpl.java | 4 + .../java/com/cloud/vm/UserVmManagerImpl.java | 146 ++++++++++++++- .../vm/snapshot/VMSnapshotManagerImpl.java | 4 +- ...Impl.java => UnmanagedVMsManagerImpl.java} | 173 +++++++++++++++++- .../spring-server-compute-context.xml | 2 +- .../com/cloud/vpc/MockNetworkManagerImpl.java | 6 +- ....java => UnmanagedVMsManagerImplTest.java} | 95 +++++++++- .../SecondaryStorageManagerImpl.java | 4 + test/integration/smoke/test_vm_life_cycle.py | 161 +++++++++++++++- tools/marvin/marvin/lib/base.py | 14 ++ 39 files changed, 1174 insertions(+), 40 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/vm/UnmanageVMService.java create mode 100644 api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java create mode 100644 core/src/main/java/com/cloud/agent/api/PrepareUnmanageVMInstanceAnswer.java create mode 100644 core/src/main/java/com/cloud/agent/api/PrepareUnmanageVMInstanceCommand.java rename server/src/main/java/org/apache/cloudstack/vm/{VmImportManagerImpl.java => UnmanagedVMsManagerImpl.java} (90%) rename server/src/test/java/org/apache/cloudstack/vm/{VmImportManagerImplTest.java => UnmanagedVMsManagerImplTest.java} (81%) diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index ec8089078c9..679c06a6c85 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -102,6 +102,7 @@ public class EventTypes { public static final String EVENT_VM_RESTORE = "VM.RESTORE"; public static final String EVENT_VM_EXPUNGE = "VM.EXPUNGE"; public static final String EVENT_VM_IMPORT = "VM.IMPORT"; + public static final String EVENT_VM_UNMANAGE = "VM.UNMANAGE"; // Domain Router public static final String EVENT_ROUTER_CREATE = "ROUTER.CREATE"; @@ -624,6 +625,7 @@ public class EventTypes { entityEventDetails.put(EVENT_VM_RESTORE, VirtualMachine.class); entityEventDetails.put(EVENT_VM_EXPUNGE, VirtualMachine.class); entityEventDetails.put(EVENT_VM_IMPORT, VirtualMachine.class); + entityEventDetails.put(EVENT_VM_UNMANAGE, VirtualMachine.class); entityEventDetails.put(EVENT_ROUTER_CREATE, VirtualRouter.class); entityEventDetails.put(EVENT_ROUTER_DESTROY, VirtualRouter.class); diff --git a/api/src/main/java/com/cloud/vm/UserVmService.java b/api/src/main/java/com/cloud/vm/UserVmService.java index 50786d2e493..56455a320bd 100644 --- a/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/api/src/main/java/com/cloud/vm/UserVmService.java @@ -517,4 +517,9 @@ public interface UserVmService { final long accountId, final long userId, final ServiceOffering serviceOffering, final String sshPublicKey, final String hostName, final HypervisorType hypervisorType, final Map customParameters, final VirtualMachine.PowerState powerState) throws InsufficientCapacityException; + /** + * Unmanage a guest VM from CloudStack + * @return true if the VM is successfully unmanaged, false if not. + */ + boolean unmanageUserVM(Long vmId); } diff --git a/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java b/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java index 4354a2c69ed..e6c196b1142 100644 --- a/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java +++ b/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java @@ -64,6 +64,7 @@ public interface VirtualMachineProfile { public static final Param BootMode = new Param("BootMode"); public static final Param BootType = new Param("BootType"); public static final Param BootIntoSetup = new Param("enterHardwareSetup"); + public static final Param PreserveNics = new Param("PreserveNics"); private String name; diff --git a/api/src/main/java/com/cloud/vm/snapshot/VMSnapshotService.java b/api/src/main/java/com/cloud/vm/snapshot/VMSnapshotService.java index e376265acfa..84a56aaedd3 100644 --- a/api/src/main/java/com/cloud/vm/snapshot/VMSnapshotService.java +++ b/api/src/main/java/com/cloud/vm/snapshot/VMSnapshotService.java @@ -52,5 +52,5 @@ public interface VMSnapshotService { * the vm gets deleted on hypervisor (no need to delete each vm snapshot before deleting vm, just mark them as deleted on DB) * @param id vm id */ - boolean deleteVMSnapshotsFromDB(Long vmId); + boolean deleteVMSnapshotsFromDB(Long vmId, boolean unmanage); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java index 4b367f8f08c..10321cc035d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java @@ -39,6 +39,7 @@ import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.vm.VmImportService; import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang.BooleanUtils; import org.apache.log4j.Logger; import com.cloud.event.EventTypes; @@ -152,6 +153,11 @@ public class ImportUnmanagedInstanceCmd extends BaseAsyncCmd { description = "vm and its volumes are allowed to migrate to different host/pool when offerings passed are incompatible with current host/pool") private Boolean migrateAllowed; + @Parameter(name = ApiConstants.FORCED, + type = CommandType.BOOLEAN, + description = "VM is imported despite some of its NIC's MAC addresses are already present") + private Boolean forced; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -268,6 +274,10 @@ public class ImportUnmanagedInstanceCmd extends BaseAsyncCmd { return "Importing unmanaged VM"; } + public boolean isForced() { + return BooleanUtils.isTrue(forced); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java new file mode 100644 index 00000000000..f49577ebd59 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java @@ -0,0 +1,136 @@ +// +// 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.api.command.admin.vm; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandJobType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.UnmanageVMInstanceResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.UnmanagedVMsManager; +import org.apache.log4j.Logger; + +import javax.inject.Inject; + +@APICommand(name = UnmanageVMInstanceCmd.API_NAME, + description = "Unmanage a guest virtual machine.", + entityType = {VirtualMachine.class}, + responseObject = UnmanageVMInstanceResponse.class, + requestHasSensitiveInfo = false, + authorized = {RoleType.Admin}, + since = "4.15.0") +public class UnmanageVMInstanceCmd extends BaseAsyncCmd { + + public static final Logger LOGGER = Logger.getLogger(UnmanageVMInstanceCmd.class); + public static final String API_NAME = "unmanageVirtualMachine"; + + @Inject + private UnmanagedVMsManager unmanagedVMsManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, + entityType = UserVmResponse.class, required = true, + description = "The ID of the virtual machine to unmanage") + private Long vmId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getVmId() { + return vmId; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_UNMANAGE; + } + + @Override + public String getEventDescription() { + return "unmanaging VM. VM ID = " + vmId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, + ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + UnmanageVMInstanceResponse response = new UnmanageVMInstanceResponse(); + try { + CallContext.current().setEventDetails("VM ID = " + vmId); + boolean result = unmanagedVMsManager.unmanageVMInstance(vmId); + response.setSuccess(result); + if (result) { + response.setDetails("VM unmanaged successfully"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + response.setResponseName(getCommandName()); + response.setObjectName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public String getCommandName() { + return API_NAME.toLowerCase() + BaseAsyncCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + UserVm vm = _responseGenerator.findUserVmById(vmId); + if (vm != null) { + return vm.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public ApiCommandJobType getInstanceType() { + return ApiCommandJobType.VirtualMachine; + } + + @Override + public Long getInstanceId() { + return vmId; + } + +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java new file mode 100644 index 00000000000..2c96f935d18 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java @@ -0,0 +1,58 @@ +// 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.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +public class UnmanageVMInstanceResponse extends BaseResponse { + + @SerializedName(ApiConstants.RESULT) + @Param(description = "result of the unmanage VM operation") + private boolean success; + + @SerializedName(ApiConstants.DETAILS) + @Param(description = "details of the unmanage VM operation") + private String details; + + public UnmanageVMInstanceResponse() { + } + + public UnmanageVMInstanceResponse(boolean success, String details) { + this.success = success; + this.details = details; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getDetails() { + return details; + } + + public void setDetails(String details) { + this.details = details; + } +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/vm/UnmanageVMService.java b/api/src/main/java/org/apache/cloudstack/vm/UnmanageVMService.java new file mode 100644 index 00000000000..23c006bf02c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/UnmanageVMService.java @@ -0,0 +1,27 @@ +// 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.vm; + +public interface UnmanageVMService { + + /** + * Unmanage a guest VM from CloudStack + * @return true if the VM is successfully unmanaged, false if not. + */ + boolean unmanageVMInstance(long vmId); +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java new file mode 100644 index 00000000000..c43c1c3f9a1 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java @@ -0,0 +1,29 @@ +// 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.vm; + +import com.cloud.utils.component.PluggableService; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +public interface UnmanagedVMsManager extends VmImportService, UnmanageVMService, PluggableService, Configurable { + + ConfigKey UnmanageVMPreserveNic = new ConfigKey<>("Advanced", Boolean.class, "unmanage.vm.preserve.nics", "false", + "If set to true, do not remove VM nics (and its MAC addresses) when unmanaging a VM, leaving them allocated but not reserved. " + + "If set to false, nics are removed and MAC addresses can be reassigned", true, ConfigKey.Scope.Zone); +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java b/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java index 783a5d295ee..cce28474541 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java +++ b/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java @@ -23,9 +23,7 @@ import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.UnmanagedInstanceResponse; import org.apache.cloudstack.api.response.UserVmResponse; -import com.cloud.utils.component.PluggableService; - -public interface VmImportService extends PluggableService { +public interface VmImportService { ListResponse listUnmanagedInstances(ListUnmanagedInstancesCmd cmd); UserVmResponse importUnmanagedInstance(ImportUnmanagedInstanceCmd cmd); } diff --git a/core/src/main/java/com/cloud/agent/api/PrepareUnmanageVMInstanceAnswer.java b/core/src/main/java/com/cloud/agent/api/PrepareUnmanageVMInstanceAnswer.java new file mode 100644 index 00000000000..2fd515a2551 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PrepareUnmanageVMInstanceAnswer.java @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.agent.api; + +public class PrepareUnmanageVMInstanceAnswer extends Answer { + + public PrepareUnmanageVMInstanceAnswer() { + } + + public PrepareUnmanageVMInstanceAnswer(PrepareUnmanageVMInstanceCommand cmd, boolean result, String details) { + super(cmd, result, details); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/cloud/agent/api/PrepareUnmanageVMInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/PrepareUnmanageVMInstanceCommand.java new file mode 100644 index 00000000000..8a788859069 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PrepareUnmanageVMInstanceCommand.java @@ -0,0 +1,39 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.agent.api; + +public class PrepareUnmanageVMInstanceCommand extends Command { + + private String instanceName; + + public PrepareUnmanageVMInstanceCommand() { + } + + public String getInstanceName() { + return instanceName; + } + + public void setInstanceName(String instanceName) { + this.instanceName = instanceName; + } + + @Override + public boolean executeInSequence() { + return false; + } +} \ No newline at end of file diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java index 9c3f421dbfb..d6d123cd243 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java @@ -58,4 +58,6 @@ public interface VirtualMachineGuru { * @return */ void prepareStop(VirtualMachineProfile profile); + + void finalizeUnmanage(VirtualMachine vm); } diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index e3afbf40a00..ea2fa760c81 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -213,4 +213,11 @@ public interface VirtualMachineManager extends Manager { void migrateForScale(String vmUuid, long srcHostId, DeployDestination dest, Long newSvcOfferingId) throws ResourceUnavailableException, ConcurrentOperationException; boolean getExecuteInSequence(HypervisorType hypervisorType); + + /** + * Unmanage a VM from CloudStack: + * - Remove the references of the VM and its volumes, nics, IPs from database + * - Keep the VM as it is on the hypervisor + */ + boolean unmanage(String vmUuid); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 1e8135460ab..682660252b3 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -317,5 +317,7 @@ public interface NetworkOrchestrationService { */ void cleanupNicDhcpDnsEntry(Network network, VirtualMachineProfile vmProfile, NicProfile nicProfile); - Pair importNic(final String macAddress, int deviceId, final Network network, final Boolean isDefaultNic, final VirtualMachine vm, final Network.IpAddresses ipAddresses) throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException; + Pair importNic(final String macAddress, int deviceId, final Network network, final Boolean isDefaultNic, final VirtualMachine vm, final Network.IpAddresses ipAddresses, boolean forced) throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException; + + void unmanageNics(VirtualMachineProfile vm); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java index a769a3428b5..9458de76353 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java @@ -149,4 +149,9 @@ public interface VolumeOrchestrationService { */ DiskProfile importVolume(Type type, String name, DiskOffering offering, Long size, Long minIops, Long maxIops, VirtualMachine vm, VirtualMachineTemplate template, Account owner, Long deviceId, Long poolId, String path, String chainInfo); + + /** + * Unmanage VM volumes + */ + void unmanageVolumes(long vmId); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VMSnapshotStrategy.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VMSnapshotStrategy.java index 38f633aefb3..a009a940a74 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VMSnapshotStrategy.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VMSnapshotStrategy.java @@ -35,5 +35,5 @@ public interface VMSnapshotStrategy { * @param vmSnapshot vm snapshot to be marked as deleted. * @return true if vm snapshot removed from DB, false if not. */ - boolean deleteVMSnapshotFromDB(VMSnapshot vmSnapshot); + boolean deleteVMSnapshotFromDB(VMSnapshot vmSnapshot, boolean unmanage); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java index c56fef70b75..e8b533db0fd 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java @@ -93,4 +93,6 @@ public interface VolumeService { SnapshotInfo takeSnapshot(VolumeInfo volume); VolumeInfo updateHypervisorSnapshotReserveForVolume(DiskOffering diskOffering, long volumeId, HypervisorType hyperType); + + void unmanageVolume(long volumeId); } \ No newline at end of file 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 32ea5a7495b..e74e6fffa4d 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -74,8 +74,10 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.vm.UnmanagedVMsManager; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang.BooleanUtils; import org.apache.log4j.Logger; import com.cloud.agent.AgentManager; @@ -212,6 +214,7 @@ import com.cloud.utils.db.DB; import com.cloud.utils.db.EntityManager; import com.cloud.utils.db.GlobalLock; import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; import com.cloud.utils.db.TransactionCallbackWithException; import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn; import com.cloud.utils.db.TransactionLegacy; @@ -1488,6 +1491,88 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } } + @Override + public boolean unmanage(String vmUuid) { + VMInstanceVO vm = _vmDao.findByUuid(vmUuid); + if (vm == null || vm.getRemoved() != null) { + throw new CloudRuntimeException("Could not find VM with id = " + vmUuid); + } + + final List pendingWorkJobs = _workJobDao.listPendingWorkJobs(VirtualMachine.Type.Instance, vm.getId()); + if (CollectionUtils.isNotEmpty(pendingWorkJobs) || _haMgr.hasPendingHaWork(vm.getId())) { + String msg = "There are pending jobs or HA tasks working on the VM with id: " + vm.getId() + ", can't unmanage the VM."; + s_logger.info(msg); + throw new ConcurrentOperationException(msg); + } + + Boolean result = Transaction.execute(new TransactionCallback() { + @Override + public Boolean doInTransaction(TransactionStatus status) { + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Unmanaging vm " + vm); + } + + final VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm); + final HypervisorGuru hvGuru = _hvGuruMgr.getGuru(vm.getHypervisorType()); + final VirtualMachineGuru guru = getVmGuru(vm); + + try { + unmanageVMSnapshots(vm); + unmanageVMNics(profile, vm); + unmanageVMVolumes(vm); + + guru.finalizeUnmanage(vm); + } catch (Exception e) { + s_logger.error("Error while unmanaging VM " + vm, e); + return false; + } + + return true; + } + }); + + return BooleanUtils.isTrue(result); + } + + /** + * Clean up VM snapshots (if any) from DB + */ + private void unmanageVMSnapshots(VMInstanceVO vm) { + _vmSnapshotMgr.deleteVMSnapshotsFromDB(vm.getId(), true); + } + + /** + * Clean up volumes for a VM to be unmanaged from CloudStack + */ + private void unmanageVMVolumes(VMInstanceVO vm) { + final Long hostId = vm.getHostId() != null ? vm.getHostId() : vm.getLastHostId(); + if (hostId != null) { + volumeMgr.revokeAccess(vm.getId(), hostId); + } + volumeMgr.unmanageVolumes(vm.getId()); + + List> targets = getTargets(hostId, vm.getId()); + if (hostId != null && CollectionUtils.isNotEmpty(targets)) { + removeDynamicTargets(hostId, targets); + } + } + + /** + * Clean up NICs for a VM to be unmanaged from CloudStack: + * - If 'unmanage.vm.preserve.nics' = true: then the NICs are not removed but still Allocated, to preserve MAC addresses + * - If 'unmanage.vm.preserve.nics' = false: then the NICs are removed while unmanaging + */ + private void unmanageVMNics(VirtualMachineProfile profile, VMInstanceVO vm) { + s_logger.debug("Cleaning up NICs"); + Boolean preserveNics = UnmanagedVMsManager.UnmanageVMPreserveNic.valueIn(vm.getDataCenterId()); + if (BooleanUtils.isTrue(preserveNics)) { + s_logger.debug("Preserve NICs configuration enabled"); + profile.setParameter(VirtualMachineProfile.Param.PreserveNics, true); + } + _networkMgr.unmanageNics(profile); + } + private List> getVolumesToDisconnect(VirtualMachine vm) { List> volumesToDisconnect = new ArrayList<>(); @@ -1978,7 +2063,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } else { if (expunge) { - _vmSnapshotMgr.deleteVMSnapshotsFromDB(vm.getId()); + _vmSnapshotMgr.deleteVMSnapshotsFromDB(vm.getId(), false); } } } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 4c6d2603e1e..078cba26893 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -38,6 +39,8 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.event.EventTypes; +import com.cloud.event.UsageEventUtils; import com.cloud.network.dao.NetworkDetailVO; import com.cloud.network.dao.NetworkDetailsDao; import org.apache.cloudstack.acl.ControlledEntity.ACLType; @@ -53,6 +56,7 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.PublishScope; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.commons.lang.BooleanUtils; import org.apache.log4j.Logger; import com.cloud.agent.AgentManager; @@ -2057,8 +2061,12 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra } } - nic.setState(Nic.State.Deallocating); - _nicDao.update(nic.getId(), nic); + Boolean preserveNics = (Boolean) vm.getParameter(VirtualMachineProfile.Param.PreserveNics); + if (BooleanUtils.isNotTrue(preserveNics)) { + nic.setState(Nic.State.Deallocating); + _nicDao.update(nic.getId(), nic); + } + final NicProfile profile = new NicProfile(nic, network, null, null, null, _networkModel.isSecurityGroupSupportedInNetwork(network), _networkModel.getNetworkTag( vm.getHypervisorType(), network)); @@ -2113,7 +2121,9 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra final NetworkGuru guru = AdapterBase.getAdapterByName(networkGurus, network.getGuruName()); guru.deallocate(network, profile, vm); - _nicDao.remove(nic.getId()); + if (BooleanUtils.isNotTrue(preserveNics)) { + _nicDao.remove(nic.getId()); + } s_logger.debug("Removed nic id=" + nic.getId()); //remove the secondary ip addresses corresponding to to this nic @@ -4002,7 +4012,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra @DB @Override - public Pair importNic(final String macAddress, int deviceId, final Network network, final Boolean isDefaultNic, final VirtualMachine vm, final Network.IpAddresses ipAddresses) + public Pair importNic(final String macAddress, int deviceId, final Network network, final Boolean isDefaultNic, final VirtualMachine vm, final Network.IpAddresses ipAddresses, final boolean forced) throws ConcurrentOperationException, InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException { s_logger.debug("Allocating nic for vm " + vm.getUuid() + " in network " + network + " during import"); String guestIp = null; @@ -4024,6 +4034,17 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra final NicVO vo = Transaction.execute(new TransactionCallback() { @Override public NicVO doInTransaction(TransactionStatus status) { + NicVO existingNic = _nicDao.findByNetworkIdAndMacAddress(network.getId(), macAddress); + if (existingNic != null) { + if (!forced) { + throw new CloudRuntimeException("NIC with MAC address = " + macAddress + " exists on network with ID = " + network.getId() + + " and forced flag is disabled"); + } + s_logger.debug("Removing existing NIC with MAC address = " + macAddress + " on network with ID = " + network.getId()); + existingNic.setState(Nic.State.Deallocating); + existingNic.setRemoved(new Date()); + _nicDao.update(existingNic.getId(), existingNic); + } NicVO vo = new NicVO(network.getGuruName(), vm.getId(), network.getId(), vm.getType()); vo.setMacAddress(macAddress); vo.setAddressFormat(Networks.AddressFormat.Ip4); @@ -4065,6 +4086,24 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra return new Pair(vmNic, Integer.valueOf(deviceId)); } + @Override + public void unmanageNics(VirtualMachineProfile vm) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Unmanaging NICs for VM: " + vm.getId()); + } + + VirtualMachine virtualMachine = vm.getVirtualMachine(); + final List nics = _nicDao.listByVmId(vm.getId()); + for (final NicVO nic : nics) { + removeNic(vm, nic); + NetworkVO network = _networksDao.findById(nic.getNetworkId()); + if (virtualMachine.getState() != VirtualMachine.State.Stopped) { + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, virtualMachine.getAccountId(), virtualMachine.getDataCenterId(), virtualMachine.getId(), + Long.toString(nic.getId()), network.getNetworkOfferingId(), null, 0L, virtualMachine.getClass().getName(), virtualMachine.getUuid(), virtualMachine.isDisplay()); + } + } + } + @Override public String getConfigComponentName() { return NetworkOrchestrationService.class.getSimpleName(); 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 9e2168e0bfd..c1816750937 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 @@ -1680,4 +1680,27 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati vol = _volsDao.persist(vol); return toDiskProfile(vol, offering); } + + @Override + public void unmanageVolumes(long vmId) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Unmanaging storage for vm: " + vmId); + } + final List volumesForVm = _volsDao.findByInstance(vmId); + + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + for (VolumeVO vol : volumesForVm) { + boolean volumeAlreadyDestroyed = (vol.getState() == Volume.State.Destroy || vol.getState() == Volume.State.Expunged + || vol.getState() == Volume.State.Expunging); + if (volumeAlreadyDestroyed) { + s_logger.debug("Skipping destroy for the volume " + vol + " as its in state " + vol.getState().toString()); + } else { + volService.unmanageVolume(vol.getId()); + } + } + } + }); + } } diff --git a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java index 3450c09b263..e68ac5cde42 100644 --- a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java +++ b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java @@ -463,4 +463,24 @@ public class NetworkOrchestratorTest extends TestCase { testOrchastrator.validateLockedRequestedIp(ipVoSpy, lockedIp); } + @Test + public void testDontReleaseNicWhenPreserveNicsSettingEnabled() { + VirtualMachineProfile vm = mock(VirtualMachineProfile.class); + NicVO nic = mock(NicVO.class); + NetworkVO network = mock(NetworkVO.class); + + when(vm.getType()).thenReturn(Type.User); + when(network.getGuruName()).thenReturn(guruName); + when(testOrchastrator._networksDao.findById(nic.getNetworkId())).thenReturn(network); + + Long nicId = 1L; + when(nic.getId()).thenReturn(nicId); + when(vm.getParameter(VirtualMachineProfile.Param.PreserveNics)).thenReturn(true); + + testOrchastrator.removeNic(vm, nic); + + verify(nic, never()).setState(Nic.State.Deallocating); + verify(testOrchastrator._nicDao, never()).remove(nicId); + } + } diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java index 6b83dabe24c..84236249cd2 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java @@ -426,7 +426,7 @@ public class DefaultVMSnapshotStrategy extends ManagerBase implements VMSnapshot } @Override - public boolean deleteVMSnapshotFromDB(VMSnapshot vmSnapshot) { + public boolean deleteVMSnapshotFromDB(VMSnapshot vmSnapshot, boolean unmanage) { try { vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.ExpungeRequested); } catch (NoTransitionException e) { @@ -435,9 +435,14 @@ public class DefaultVMSnapshotStrategy extends ManagerBase implements VMSnapshot } UserVm userVm = userVmDao.findById(vmSnapshot.getVmId()); List volumeTOs = vmSnapshotHelper.getVolumeTOList(userVm.getId()); + long full_chain_size = 0; for (VolumeObjectTO volumeTo: volumeTOs) { volumeTo.setSize(0); publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_DELETE, vmSnapshot, userVm, volumeTo); + full_chain_size += volumeTo.getSize(); + } + if (unmanage) { + publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_OFF_PRIMARY, vmSnapshot, userVm, full_chain_size, 0L); } return vmSnapshotDao.remove(vmSnapshot.getId()); } diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index 92c8a93f515..77413ad6c2b 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -2108,4 +2108,21 @@ public class VolumeServiceImpl implements VolumeService { return volFactory.getVolume(volumeId); } + + @DB + @Override + /** + * The volume must be marked as expunged on DB to exclude it from the storage cleanup task + */ + public void unmanageVolume(long volumeId) { + VolumeInfo vol = volFactory.getVolume(volumeId); + if (vol != null) { + vol.stateTransit(Volume.Event.DestroyRequested); + snapshotMgr.deletePoliciesForVolume(volumeId); + vol.stateTransit(Volume.Event.OperationSucceeded); + vol.stateTransit(Volume.Event.ExpungingRequested); + vol.stateTransit(Volume.Event.OperationSucceeded); + volDao.remove(vol.getId()); + } + } } diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java index 6168441d1db..aebda554a93 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java @@ -129,6 +129,8 @@ import com.cloud.agent.api.PlugNicAnswer; import com.cloud.agent.api.PlugNicCommand; import com.cloud.agent.api.PrepareForMigrationAnswer; import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.PrepareUnmanageVMInstanceAnswer; +import com.cloud.agent.api.PrepareUnmanageVMInstanceCommand; import com.cloud.agent.api.PvlanSetupCommand; import com.cloud.agent.api.ReadyAnswer; import com.cloud.agent.api.ReadyCommand; @@ -561,6 +563,8 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa answer = execute((UnregisterNicCommand) cmd); } else if (clz == GetUnmanagedInstancesCommand.class) { answer = execute((GetUnmanagedInstancesCommand) cmd); + } else if (clz == PrepareUnmanageVMInstanceCommand.class) { + answer = execute((PrepareUnmanageVMInstanceCommand) cmd); } else { answer = Answer.createUnsupportedCommandAnswer(cmd); } @@ -7148,4 +7152,26 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa } return new GetUnmanagedInstancesAnswer(cmd, "", unmanagedInstances); } + + private Answer execute(PrepareUnmanageVMInstanceCommand cmd) { + s_logger.debug("Verify VMware instance: " + cmd.getInstanceName() + " is available before unmanaging VM"); + VmwareContext context = getServiceContext(); + VmwareHypervisorHost hyperHost = getHyperHost(context); + String instanceName = cmd.getInstanceName(); + + try { + ManagedObjectReference dcMor = hyperHost.getHyperHostDatacenter(); + DatacenterMO dataCenterMo = new DatacenterMO(getServiceContext(), dcMor); + VirtualMachineMO vm = dataCenterMo.findVm(instanceName); + if (vm == null) { + return new PrepareUnmanageVMInstanceAnswer(cmd, false, "Cannot find VM with name " + instanceName + + " in datacenter " + dataCenterMo.getName()); + } + } catch (Exception e) { + s_logger.error("Error trying to verify if VM to unmanage exists", e); + return new PrepareUnmanageVMInstanceAnswer(cmd, false, "Error: " + e.getMessage()); + } + + return new PrepareUnmanageVMInstanceAnswer(cmd, true, "OK"); + } } diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java index 78e04b5dc53..bc35b34ea46 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java @@ -596,4 +596,8 @@ public class ElasticLoadBalancerManagerImpl extends ManagerBase implements Elast public void prepareStop(VirtualMachineProfile profile) { } + @Override + public void finalizeUnmanage(VirtualMachine vm) { + } + } diff --git a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java index 52ecebc08bc..7232c1a032a 100644 --- a/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java +++ b/plugins/network-elements/internal-loadbalancer/src/main/java/org/apache/cloudstack/network/lb/InternalLoadBalancerVMManagerImpl.java @@ -356,6 +356,10 @@ public class InternalLoadBalancerVMManagerImpl extends ManagerBase implements In public void prepareStop(final VirtualMachineProfile profile) { } + @Override + public void finalizeUnmanage(VirtualMachine vm) { + } + @Override public boolean configure(final String name, final Map params) throws ConfigurationException { final Map configs = _configDao.getConfiguration("AgentManager", params); diff --git a/plugins/network-elements/netscaler/src/main/java/com/cloud/network/vm/NetScalerVMManagerImpl.java b/plugins/network-elements/netscaler/src/main/java/com/cloud/network/vm/NetScalerVMManagerImpl.java index d869f0bd12f..277c7747227 100644 --- a/plugins/network-elements/netscaler/src/main/java/com/cloud/network/vm/NetScalerVMManagerImpl.java +++ b/plugins/network-elements/netscaler/src/main/java/com/cloud/network/vm/NetScalerVMManagerImpl.java @@ -189,6 +189,10 @@ public class NetScalerVMManagerImpl extends ManagerBase implements NetScalerVMMa public void prepareStop(VirtualMachineProfile profile) { } + @Override + public void finalizeUnmanage(VirtualMachine vm) { + } + @Override public boolean configure(String name, Map params) throws ConfigurationException { _itMgr.registerGuru(VirtualMachine.Type.NetScalerVm, this); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index 8638fb59222..b0eac2bcf44 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -1735,6 +1735,10 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy public void prepareStop(VirtualMachineProfile profile) { } + @Override + public void finalizeUnmanage(VirtualMachine vm) { + } + public List getConsoleProxyAllocators() { return _consoleProxyAllocators; } diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index 076925854e1..0c2277ecebf 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -3148,6 +3148,10 @@ Configurable, StateListener volumes = _volsDao.findByInstance(vm.getId()); + checkUnmanagingVMOngoingVolumeSnapshots(vm); + checkUnmanagingVMVolumes(vm, volumes); + + result = _itMgr.unmanage(vm.getUuid()); + if (result) { + cleanupUnmanageVMResources(vm.getId()); + unmanageVMFromDB(vm.getId()); + publishUnmanageVMUsageEvents(vm, volumes); + } else { + throw new CloudRuntimeException("Error while unmanaging VM: " + vm.getUuid()); + } + } catch (Exception e) { + s_logger.error("Could not unmanage VM " + vm.getUuid(), e); + throw new CloudRuntimeException(e); + } finally { + _vmDao.releaseFromLockTable(vm.getId()); + } + + return true; + } + + /* + Generate usage events related to unmanaging a VM + */ + private void publishUnmanageVMUsageEvents(UserVmVO vm, List volumes) { + postProcessingUnmanageVMVolumes(volumes, vm); + postProcessingUnmanageVM(vm); + } + + /* + Cleanup the VM from resources and groups + */ + private void cleanupUnmanageVMResources(long vmId) { + cleanupVmResources(vmId); + removeVMFromAffinityGroups(vmId); + } + + private void unmanageVMFromDB(long vmId) { + VMInstanceVO vm = _vmInstanceDao.findById(vmId); + userVmDetailsDao.removeDetails(vmId); + vm.setState(State.Expunging); + vm.setRemoved(new Date()); + _vmInstanceDao.update(vm.getId(), vm); + } + + /* + Remove VM from affinity groups after unmanaging + */ + private void removeVMFromAffinityGroups(long vmId) { + List affinityGroups = _affinityGroupVMMapDao.listByInstanceId(vmId); + if (affinityGroups.size() > 0) { + s_logger.debug("Cleaning up VM from affinity groups after unmanaging"); + for (AffinityGroupVMMapVO map : affinityGroups) { + _affinityGroupVMMapDao.expunge(map.getId()); + } + } + } + + /* + Decrement VM resources and generate usage events after unmanaging VM + */ + private void postProcessingUnmanageVM(UserVmVO vm) { + ServiceOfferingVO offering = _serviceOfferingDao.findById(vm.getServiceOfferingId()); + + // First generate a VM stop event if the VM was not stopped already + if (vm.getState() != State.Stopped) { + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_STOP, vm.getAccountId(), vm.getDataCenterId(), + vm.getId(), vm.getHostName(), vm.getServiceOfferingId(), vm.getTemplateId(), + vm.getHypervisorType().toString(), VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplayVm()); + resourceCountDecrement(vm.getAccountId(), vm.isDisplayVm(), new Long(offering.getCpu()), new Long(offering.getRamSize())); + } + + // VM destroy usage event + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_DESTROY, vm.getAccountId(), vm.getDataCenterId(), + vm.getId(), vm.getHostName(), vm.getServiceOfferingId(), vm.getTemplateId(), + vm.getHypervisorType().toString(), VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplayVm()); + resourceCountDecrement(vm.getAccountId(), vm.isDisplayVm(), new Long(offering.getCpu()), new Long(offering.getRamSize())); + } + + /* + Decrement resources for volumes and generate usage event for ROOT volume after unmanaging VM. + Usage events for DATA disks are published by the transition listener: @see VolumeStateListener#postStateTransitionEvent + */ + private void postProcessingUnmanageVMVolumes(List volumes, UserVmVO vm) { + for (VolumeVO volume : volumes) { + if (volume.getVolumeType() == Volume.Type.ROOT) { + // + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_DELETE, volume.getAccountId(), volume.getDataCenterId(), volume.getId(), volume.getName(), + Volume.class.getName(), volume.getUuid(), volume.isDisplayVolume()); + } + _resourceLimitMgr.decrementResourceCount(vm.getAccountId(), ResourceType.volume); + _resourceLimitMgr.decrementResourceCount(vm.getAccountId(), ResourceType.primary_storage, new Long(volume.getSize())); + } + } + + private void checkUnmanagingVMOngoingVolumeSnapshots(UserVmVO vm) { + s_logger.debug("Checking if there are any ongoing snapshots on the ROOT volumes associated with VM with ID " + vm.getId()); + if (checkStatusOfVolumeSnapshots(vm.getId(), Volume.Type.ROOT)) { + throw new CloudRuntimeException("There is/are unbacked up snapshot(s) on ROOT volume, vm unmanage is not permitted, please try again later."); + } + s_logger.debug("Found no ongoing snapshots on volume of type ROOT, for the vm with id " + vm.getId()); + } + + private void checkUnmanagingVMVolumes(UserVmVO vm, List volumes) { + for (VolumeVO volume : volumes) { + if (volume.getInstanceId() == null || !volume.getInstanceId().equals(vm.getId())) { + throw new CloudRuntimeException("Invalid state for volume with ID " + volume.getId() + " of VM " + + vm.getId() +": it is not attached to VM"); + } else if (volume.getVolumeType() != Volume.Type.ROOT && volume.getVolumeType() != Volume.Type.DATADISK) { + throw new CloudRuntimeException("Invalid type for volume with ID " + volume.getId() + + ": ROOT or DATADISK expected but got " + volume.getVolumeType()); + } + } + } +} \ No newline at end of file diff --git a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java index 86357f7700f..2c0ca8b6703 100644 --- a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java @@ -1282,7 +1282,7 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme } @Override - public boolean deleteVMSnapshotsFromDB(Long vmId) { + public boolean deleteVMSnapshotsFromDB(Long vmId, boolean unmanage) { List listVmSnapshots = _vmSnapshotDao.findByVm(vmId); if (listVmSnapshots == null || listVmSnapshots.isEmpty()) { return true; @@ -1290,7 +1290,7 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme for (VMSnapshotVO snapshot : listVmSnapshots) { try { VMSnapshotStrategy strategy = findVMSnapshotStrategy(snapshot); - if (! strategy.deleteVMSnapshotFromDB(snapshot)) { + if (! strategy.deleteVMSnapshotFromDB(snapshot, unmanage)) { s_logger.error("Couldn't delete vm snapshot with id " + snapshot.getId()); return false; } diff --git a/server/src/main/java/org/apache/cloudstack/vm/VmImportManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java similarity index 90% rename from server/src/main/java/org/apache/cloudstack/vm/VmImportManagerImpl.java rename to server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 7560d74506b..26f8675f226 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/VmImportManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -25,6 +25,17 @@ import java.util.Set; import javax.inject.Inject; +import com.cloud.agent.api.PrepareUnmanageVMInstanceAnswer; +import com.cloud.agent.api.PrepareUnmanageVMInstanceCommand; +import com.cloud.event.ActionEvent; +import com.cloud.exception.UnsupportedServiceException; +import com.cloud.storage.Snapshot; +import com.cloud.storage.SnapshotVO; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.vm.NicVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.ResponseGenerator; @@ -32,6 +43,7 @@ import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.admin.vm.ImportUnmanagedInstanceCmd; import org.apache.cloudstack.api.command.admin.vm.ListUnmanagedInstancesCmd; +import org.apache.cloudstack.api.command.admin.vm.UnmanageVMInstanceCmd; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.NicResponse; import org.apache.cloudstack.api.response.UnmanagedInstanceDiskResponse; @@ -40,6 +52,7 @@ import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; @@ -127,9 +140,9 @@ import com.cloud.vm.dao.VMInstanceDao; import com.google.common.base.Strings; import com.google.gson.Gson; -public class VmImportManagerImpl implements VmImportService { +public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { public static final String VM_IMPORT_DEFAULT_TEMPLATE_NAME = "system-default-vm-import-dummy-template.iso"; - private static final Logger LOGGER = Logger.getLogger(VmImportManagerImpl.class); + private static final Logger LOGGER = Logger.getLogger(UnmanagedVMsManagerImpl.class); @Inject private AgentManager agentManager; @@ -191,10 +204,16 @@ public class VmImportManagerImpl implements VmImportService { private GuestOSDao guestOSDao; @Inject private GuestOSHypervisorDao guestOSHypervisorDao; + @Inject + private VMSnapshotDao vmSnapshotDao; + @Inject + private SnapshotDao snapshotDao; + @Inject + private UserVmDao userVmDao; protected Gson gson; - public VmImportManagerImpl() { + public UnmanagedVMsManagerImpl() { gson = GsonHelper.getGsonLogger(); } @@ -680,8 +699,8 @@ public class VmImportManagerImpl implements VmImportService { return new Pair(profile, storagePool); } - private NicProfile importNic(UnmanagedInstanceTO.Nic nic, VirtualMachine vm, Network network, Network.IpAddresses ipAddresses, boolean isDefaultNic) throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException { - Pair result = networkOrchestrationService.importNic(nic.getMacAddress(), 0, network, isDefaultNic, vm, ipAddresses); + private NicProfile importNic(UnmanagedInstanceTO.Nic nic, VirtualMachine vm, Network network, Network.IpAddresses ipAddresses, boolean isDefaultNic, boolean forced) throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException { + Pair result = networkOrchestrationService.importNic(nic.getMacAddress(), 0, network, isDefaultNic, vm, ipAddresses, forced); if (result == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("NIC ID: %s import failed", nic.getNicId())); } @@ -850,12 +869,16 @@ public class VmImportManagerImpl implements VmImportService { } try { if (!serviceOfferingVO.isDynamic()) { - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_IMPORT, userVm.getAccountId(), userVm.getDataCenterId(), userVm.getId(), userVm.getHostName(), serviceOfferingVO.getId(), userVm.getTemplateId(), + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_CREATE, userVm.getAccountId(), userVm.getDataCenterId(), userVm.getId(), userVm.getHostName(), serviceOfferingVO.getId(), userVm.getTemplateId(), userVm.getHypervisorType().toString(), VirtualMachine.class.getName(), userVm.getUuid(), userVm.isDisplayVm()); } else { - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_IMPORT, userVm.getAccountId(), userVm.getAccountId(), userVm.getDataCenterId(), userVm.getHostName(), serviceOfferingVO.getId(), userVm.getTemplateId(), + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_CREATE, userVm.getAccountId(), userVm.getAccountId(), userVm.getDataCenterId(), userVm.getHostName(), serviceOfferingVO.getId(), userVm.getTemplateId(), userVm.getHypervisorType().toString(), VirtualMachine.class.getName(), userVm.getUuid(), userVm.getDetails(), userVm.isDisplayVm()); } + if (userVm.getState() == VirtualMachine.State.Running) { + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_START, userVm.getAccountId(), userVm.getDataCenterId(), userVm.getId(), userVm.getHostName(), serviceOfferingVO.getId(), userVm.getTemplateId(), + userVm.getHypervisorType().toString(), VirtualMachine.class.getName(), userVm.getUuid(), userVm.isDisplayVm()); + } } catch (Exception e) { LOGGER.error(String.format("Failed to publish usage records during VM import for unmanaged vm %s", userVm.getInstanceName()), e); cleanupFailedImportVM(userVm); @@ -876,13 +899,24 @@ public class VmImportManagerImpl implements VmImportService { resourceLimitService.incrementResourceCount(userVm.getAccountId(), Resource.ResourceType.volume, volume.isDisplayVolume()); resourceLimitService.incrementResourceCount(userVm.getAccountId(), Resource.ResourceType.primary_storage, volume.isDisplayVolume(), volume.getSize()); } + + List nics = nicDao.listByVmId(userVm.getId()); + for (NicVO nic : nics) { + try { + NetworkVO network = networkDao.findById(nic.getNetworkId()); + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, userVm.getAccountId(), userVm.getDataCenterId(), userVm.getId(), + Long.toString(nic.getId()), network.getNetworkOfferingId(), null, 1L, VirtualMachine.class.getName(), userVm.getUuid(), userVm.isDisplay()); + } catch (Exception e) { + LOGGER.error(String.format("Failed to publish network usage records during VM import. %s", Strings.nullToEmpty(e.getMessage()))); + } + } } private UserVm importVirtualMachineInternal(final UnmanagedInstanceTO unmanagedInstance, final String instanceName, final DataCenter zone, final Cluster cluster, final HostVO host, final VirtualMachineTemplate template, final String displayName, final String hostName, final Account caller, final Account owner, final Long userId, final ServiceOfferingVO serviceOffering, final Map dataDiskOfferingMap, final Map nicNetworkMap, final Map callerNicIpAddressMap, - final Map details, final boolean migrateAllowed) { + final Map details, final boolean migrateAllowed, final boolean forced) { UserVm userVm = null; ServiceOfferingVO validatedServiceOffering = null; @@ -986,7 +1020,7 @@ public class VmImportManagerImpl implements VmImportService { for (UnmanagedInstanceTO.Nic nic : unmanagedInstance.getNics()) { Network network = networkDao.findById(allNicNetworkMap.get(nic.getNicId())); Network.IpAddresses ipAddresses = nicIpAddressMap.get(nic.getNicId()); - importNic(nic, userVm, network, ipAddresses, firstNic); + importNic(nic, userVm, network, ipAddresses, firstNic, forced); firstNic = false; } } catch (Exception e) { @@ -1139,6 +1173,7 @@ public class VmImportManagerImpl implements VmImportService { final Map nicIpAddressMap = cmd.getNicIpAddressList(); final Map dataDiskOfferingMap = cmd.getDataDiskToDiskOfferingList(); final Map details = cmd.getDetails(); + final boolean forced = cmd.isForced(); List hosts = resourceManager.listHostsInClusterByStatus(clusterId, Status.Up); UserVm userVm = null; List additionalNameFilters = getAdditionalNameFilters(cluster); @@ -1192,7 +1227,7 @@ public class VmImportManagerImpl implements VmImportService { template, displayName, hostName, caller, owner, userId, serviceOffering, dataDiskOfferingMap, nicNetworkMap, nicIpAddressMap, - details, cmd.getMigrateAllowed()); + details, cmd.getMigrateAllowed(), forced); break; } } @@ -1211,6 +1246,124 @@ public class VmImportManagerImpl implements VmImportService { final List> cmdList = new ArrayList>(); cmdList.add(ListUnmanagedInstancesCmd.class); cmdList.add(ImportUnmanagedInstanceCmd.class); + cmdList.add(UnmanageVMInstanceCmd.class); return cmdList; } + + /** + * Perform validations before attempting to unmanage a VM from CloudStack: + * - VM must not have any associated volume snapshot + * - VM must not have an attached ISO + */ + private void performUnmanageVMInstancePrechecks(VMInstanceVO vmVO) { + if (hasVolumeSnapshotsPriorToUnmanageVM(vmVO)) { + throw new UnsupportedServiceException("Cannot unmanage VM with id = " + vmVO.getUuid() + + " as there are volume snapshots for its volume(s). Please remove snapshots before unmanaging."); + } + + if (hasISOAttached(vmVO)) { + throw new UnsupportedServiceException("Cannot unmanage VM with id = " + vmVO.getUuid() + + " as there is an ISO attached. Please detach ISO before unmanaging."); + } + } + + private boolean hasVolumeSnapshotsPriorToUnmanageVM(VMInstanceVO vmVO) { + List volumes = volumeDao.findByInstance(vmVO.getId()); + for (VolumeVO volume : volumes) { + List snaps = snapshotDao.listByVolumeId(volume.getId()); + if (CollectionUtils.isNotEmpty(snaps)) { + for (SnapshotVO snap : snaps) { + if (snap.getState() != Snapshot.State.Destroyed && snap.getRemoved() == null) { + return true; + } + } + } + } + return false; + } + + private boolean hasISOAttached(VMInstanceVO vmVO) { + UserVmVO userVM = userVmDao.findById(vmVO.getId()); + if (userVM == null) { + throw new InvalidParameterValueException("Could not find user VM with ID = " + vmVO.getUuid()); + } + return userVM.getIsoId() != null; + } + + /** + * Find a suitable host within the scope of the VM to unmanage to verify the VM exists + */ + private Long findSuitableHostId(VMInstanceVO vmVO) { + Long hostId = vmVO.getHostId(); + if (hostId == null) { + long zoneId = vmVO.getDataCenterId(); + List hosts = hostDao.listAllHostsUpByZoneAndHypervisor(zoneId, vmVO.getHypervisorType()); + for (HostVO host : hosts) { + if (host.isInMaintenanceStates() || host.getState() != Status.Up || host.getStatus() != Status.Up) { + continue; + } + hostId = host.getId(); + break; + } + } + + if (hostId == null) { + throw new CloudRuntimeException("Cannot find a host to verify if the VM to unmanage " + + "with id = " + vmVO.getUuid() + " exists."); + } + return hostId; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_UNMANAGE, eventDescription = "unmanaging VM", async = true) + public boolean unmanageVMInstance(long vmId) { + VMInstanceVO vmVO = vmDao.findById(vmId); + if (vmVO == null || vmVO.getRemoved() != null) { + throw new InvalidParameterValueException("Could not find VM to unmanage, it is either removed or not existing VM"); + } else if (vmVO.getState() != VirtualMachine.State.Running && vmVO.getState() != VirtualMachine.State.Stopped) { + throw new InvalidParameterValueException("VM with id = " + vmVO.getUuid() + " must be running or stopped to be unmanaged"); + } else if (vmVO.getHypervisorType() != Hypervisor.HypervisorType.VMware) { + throw new UnsupportedServiceException("Unmanage VM is currently allowed for VMware VMs only"); + } else if (vmVO.getType() != VirtualMachine.Type.User) { + throw new UnsupportedServiceException("Unmanage VM is currently allowed for guest VMs only"); + } + + performUnmanageVMInstancePrechecks(vmVO); + + Long hostId = findSuitableHostId(vmVO); + String instanceName = vmVO.getInstanceName(); + + if (!existsVMToUnmanage(instanceName, hostId)) { + throw new CloudRuntimeException("VM with id = " + vmVO.getUuid() + " is not found in the hypervisor"); + } + + return userVmManager.unmanageUserVM(vmId); + } + + /** + * Verify the VM to unmanage exists on the hypervisor + */ + private boolean existsVMToUnmanage(String instanceName, Long hostId) { + PrepareUnmanageVMInstanceCommand command = new PrepareUnmanageVMInstanceCommand(); + command.setInstanceName(instanceName); + Answer ans = agentManager.easySend(hostId, command); + if (!(ans instanceof PrepareUnmanageVMInstanceAnswer)) { + throw new CloudRuntimeException("Error communicating with host " + hostId); + } + PrepareUnmanageVMInstanceAnswer answer = (PrepareUnmanageVMInstanceAnswer) ans; + if (!answer.getResult()) { + LOGGER.error("Error verifying VM " + instanceName + " exists on host with ID = " + hostId + ": " + answer.getDetails()); + } + return answer.getResult(); + } + + @Override + public String getConfigComponentName() { + return UnmanagedVMsManagerImpl.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] { UnmanageVMPreserveNic }; + } } diff --git a/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml b/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml index ca707a03ba6..0215abf44b4 100644 --- a/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml @@ -35,6 +35,6 @@ - + diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java index c962d90d2dd..b96b58e5ef8 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java @@ -985,7 +985,11 @@ public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrches } @Override - public Pair importNic(String macAddress, int deviceId, Network network, Boolean isDefaultNic, VirtualMachine vm, IpAddresses ipAddresses) { + public Pair importNic(String macAddress, int deviceId, Network network, Boolean isDefaultNic, VirtualMachine vm, IpAddresses ipAddresses, boolean forced) { return null; } + + @Override + public void unmanageNics(VirtualMachineProfile vm) { + } } diff --git a/server/src/test/java/org/apache/cloudstack/vm/VmImportManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java similarity index 81% rename from server/src/test/java/org/apache/cloudstack/vm/VmImportManagerImplTest.java rename to server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java index ad2bf086d66..32b0e433534 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/VmImportManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -19,17 +19,29 @@ package org.apache.cloudstack.vm; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyLong; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import com.cloud.exception.UnsupportedServiceException; +import com.cloud.storage.SnapshotVO; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.vm.NicVO; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; import org.apache.cloudstack.api.ResponseGenerator; import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.ServerApiException; @@ -111,10 +123,10 @@ import com.cloud.vm.dao.VMInstanceDao; @RunWith(PowerMockRunner.class) @PrepareForTest(UsageEventUtils.class) -public class VmImportManagerImplTest { +public class UnmanagedVMsManagerImplTest { @InjectMocks - private VmImportService vmIngestionService = new VmImportManagerImpl(); + private UnmanagedVMsManager unmanagedVMsManager = new UnmanagedVMsManagerImpl(); @Mock private UserVmManager userVmManager; @@ -160,6 +172,21 @@ public class VmImportManagerImplTest { private NetworkModel networkModel; @Mock private ConfigurationDao configurationDao; + @Mock + private VMSnapshotDao vmSnapshotDao; + @Mock + private SnapshotDao snapshotDao; + @Mock + private UserVmDao userVmDao; + @Mock + private NicDao nicDao; + + @Mock + private VMInstanceVO virtualMachine; + @Mock + private NicVO nicVO; + + private static final long virtualMachineId = 1L; @Before public void setUp() throws Exception { @@ -281,7 +308,7 @@ public class VmImportManagerImplTest { NicProfile profile = Mockito.mock(NicProfile.class); Integer deviceId = 100; Pair pair = new Pair(profile, deviceId); - when(networkOrchestrationService.importNic(nullable(String.class), nullable(Integer.class), nullable(Network.class), nullable(Boolean.class), nullable(VirtualMachine.class), nullable(Network.IpAddresses.class))).thenReturn(pair); + when(networkOrchestrationService.importNic(nullable(String.class), nullable(Integer.class), nullable(Network.class), nullable(Boolean.class), nullable(VirtualMachine.class), nullable(Network.IpAddresses.class), anyBoolean())).thenReturn(pair); when(volumeManager.importVolume(Mockito.any(Volume.Type.class), Mockito.anyString(), Mockito.any(DiskOffering.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.any(VirtualMachine.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any(Account.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString())).thenReturn(Mockito.mock(DiskProfile.class)); @@ -291,6 +318,18 @@ public class VmImportManagerImplTest { userVmResponse.setInstanceName(instance.getName()); userVmResponses.add(userVmResponse); when(responseGenerator.createUserVmResponse(Mockito.any(ResponseObject.ResponseView.class), Mockito.anyString(), Mockito.any(UserVm.class))).thenReturn(userVmResponses); + + when(vmDao.findById(virtualMachineId)).thenReturn(virtualMachine); + when(virtualMachine.getState()).thenReturn(VirtualMachine.State.Running); + when(virtualMachine.getInstanceName()).thenReturn("i-2-7-VM"); + when(virtualMachine.getId()).thenReturn(virtualMachineId); + VolumeVO volumeVO = mock(VolumeVO.class); + when(volumeDao.findByInstance(virtualMachineId)).thenReturn(Collections.singletonList(volumeVO)); + when(volumeVO.getInstanceId()).thenReturn(virtualMachineId); + when(volumeVO.getId()).thenReturn(virtualMachineId); + when(nicDao.listByVmId(virtualMachineId)).thenReturn(Collections.singletonList(nicVO)); + when(nicVO.getNetworkId()).thenReturn(1L); + when(networkDao.findById(1L)).thenReturn(networkVO); } @After @@ -301,7 +340,7 @@ public class VmImportManagerImplTest { @Test public void listUnmanagedInstancesTest() { ListUnmanagedInstancesCmd cmd = Mockito.mock(ListUnmanagedInstancesCmd.class); - vmIngestionService.listUnmanagedInstances(cmd); + unmanagedVMsManager.listUnmanagedInstances(cmd); } @Test(expected = InvalidParameterValueException.class) @@ -310,7 +349,7 @@ public class VmImportManagerImplTest { ClusterVO cluster = new ClusterVO(1, 1, "Cluster"); cluster.setHypervisorType(Hypervisor.HypervisorType.KVM.toString()); when(clusterDao.findById(Mockito.anyLong())).thenReturn(cluster); - vmIngestionService.listUnmanagedInstances(cmd); + unmanagedVMsManager.listUnmanagedInstances(cmd); } @Test(expected = PermissionDeniedException.class) @@ -320,7 +359,7 @@ public class VmImportManagerImplTest { UserVO user = new UserVO(1, "testuser", "password", "firstname", "lastName", "email", "timezone", UUID.randomUUID().toString(), User.Source.UNKNOWN); CallContext.register(user, account); ListUnmanagedInstancesCmd cmd = Mockito.mock(ListUnmanagedInstancesCmd.class); - vmIngestionService.listUnmanagedInstances(cmd); + unmanagedVMsManager.listUnmanagedInstances(cmd); } @Test @@ -330,7 +369,7 @@ public class VmImportManagerImplTest { when(importUnmanageInstanceCmd.getAccountName()).thenReturn(null); when(importUnmanageInstanceCmd.getDomainId()).thenReturn(null); PowerMockito.mockStatic(UsageEventUtils.class); - vmIngestionService.importUnmanagedInstance(importUnmanageInstanceCmd); + unmanagedVMsManager.importUnmanagedInstance(importUnmanageInstanceCmd); } @Test(expected = InvalidParameterValueException.class) @@ -339,7 +378,7 @@ public class VmImportManagerImplTest { when(importUnmanageInstanceCmd.getName()).thenReturn("TestInstance"); when(importUnmanageInstanceCmd.getName()).thenReturn("some name"); when(importUnmanageInstanceCmd.getMigrateAllowed()).thenReturn(false); - vmIngestionService.importUnmanagedInstance(importUnmanageInstanceCmd); + unmanagedVMsManager.importUnmanagedInstance(importUnmanageInstanceCmd); } @Test(expected = ServerApiException.class) @@ -348,6 +387,44 @@ public class VmImportManagerImplTest { when(importUnmanageInstanceCmd.getName()).thenReturn("SomeInstance"); when(importUnmanageInstanceCmd.getAccountName()).thenReturn(null); when(importUnmanageInstanceCmd.getDomainId()).thenReturn(null); - vmIngestionService.importUnmanagedInstance(importUnmanageInstanceCmd); + unmanagedVMsManager.importUnmanagedInstance(importUnmanageInstanceCmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void unmanageVMInstanceMissingInstanceTest() { + long notExistingId = 10L; + unmanagedVMsManager.unmanageVMInstance(notExistingId); + } + + @Test(expected = InvalidParameterValueException.class) + public void unmanageVMInstanceDestroyedInstanceTest() { + when(virtualMachine.getState()).thenReturn(VirtualMachine.State.Destroyed); + unmanagedVMsManager.unmanageVMInstance(virtualMachineId); + } + + @Test(expected = InvalidParameterValueException.class) + public void unmanageVMInstanceExpungedInstanceTest() { + when(virtualMachine.getState()).thenReturn(VirtualMachine.State.Expunging); + unmanagedVMsManager.unmanageVMInstance(virtualMachineId); + } + + @Test(expected = UnsupportedServiceException.class) + public void unmanageVMInstanceExistingVMSnapshotsTest() { + when(vmSnapshotDao.findByVm(virtualMachineId)).thenReturn(Arrays.asList(new VMSnapshotVO(), new VMSnapshotVO())); + unmanagedVMsManager.unmanageVMInstance(virtualMachineId); + } + + @Test(expected = UnsupportedServiceException.class) + public void unmanageVMInstanceExistingVolumeSnapshotsTest() { + when(snapshotDao.listByVolumeId(virtualMachineId)).thenReturn(Arrays.asList(new SnapshotVO(), new SnapshotVO())); + unmanagedVMsManager.unmanageVMInstance(virtualMachineId); + } + + @Test(expected = UnsupportedServiceException.class) + public void unmanageVMInstanceExistingISOAttachedTest() { + UserVmVO userVmVO = mock(UserVmVO.class); + when(userVmDao.findById(virtualMachineId)).thenReturn(userVmVO); + when(userVmVO.getIsoId()).thenReturn(3L); + unmanagedVMsManager.unmanageVMInstance(virtualMachineId); } } \ No newline at end of file diff --git a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index a1a3873bf88..568aa1b2e1f 100644 --- a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -1496,6 +1496,10 @@ public class SecondaryStorageManagerImpl extends ManagerBase implements Secondar } + @Override + public void finalizeUnmanage(VirtualMachine vm) { + } + public List getSecondaryStorageVmAllocators() { return _ssVmAllocators; } diff --git a/test/integration/smoke/test_vm_life_cycle.py b/test/integration/smoke/test_vm_life_cycle.py index 3def05346a4..01bf60b981f 100644 --- a/test/integration/smoke/test_vm_life_cycle.py +++ b/test/integration/smoke/test_vm_life_cycle.py @@ -25,7 +25,9 @@ from marvin.cloudstackAPI import (recoverVirtualMachine, provisionCertificate, updateConfiguration, migrateVirtualMachine, - migrateVirtualMachineWithVolume) + migrateVirtualMachineWithVolume, + unmanageVirtualMachine, + listUnmanagedInstances) from marvin.lib.utils import * from marvin.lib.base import (Account, @@ -37,13 +39,17 @@ from marvin.lib.base import (Account, Configurations, StoragePool, Volume, - DiskOffering) + DiskOffering, + NetworkOffering, + Network) from marvin.lib.common import (get_domain, get_zone, get_template, - list_hosts) + list_hosts, + list_virtual_machines) from marvin.codes import FAILED, PASS from nose.plugins.attrib import attr +from marvin.lib.decoratorGenerators import skipTestIf # Import System modules import time @@ -1512,3 +1518,152 @@ class TestKVMLiveMigration(cloudstackTestCase): target_host.id, "HostID not as expected") + +class TestUnmanageVM(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + testClient = super(TestUnmanageVM, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls.services = testClient.getParsedTestDataConfig() + cls.hypervisor = testClient.getHypervisorInfo() + cls._cleanup = [] + + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.services['mode'] = cls.zone.networktype + + cls.template = get_template( + cls.apiclient, + cls.zone.id, + cls.services["ostype"], + hypervisor=cls.hypervisor.lower() + ) + if cls.template == FAILED: + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] + + cls.hypervisorNotSupported = cls.hypervisor.lower() != "vmware" + + cls.services["small"]["zoneid"] = cls.zone.id + cls.services["small"]["template"] = cls.template.id + + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + domainid=cls.domain.id + ) + + cls.small_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["small"] + ) + + cls.network_offering = NetworkOffering.create( + cls.apiclient, + cls.services["l2-network_offering"], + ) + cls.network_offering.update(cls.apiclient, state='Enabled') + + cls._cleanup = [ + cls.small_offering, + cls.network_offering, + cls.account + ] + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.services["network"]["networkoffering"] = self.network_offering.id + + self.network = Network.create( + self.apiclient, + self.services["l2-network"], + zoneid=self.zone.id, + networkofferingid=self.network_offering.id + ) + + self.cleanup = [ + self.network + ] + + @attr(tags=["advanced", "advancedns", "smoke", "sg"], required_hardware="false") + @skipTestIf("hypervisorNotSupported") + def test_01_unmanage_vm_cycle(self): + """ + Test the following: + 1. Deploy VM + 2. Unmanage VM + 3. Verify VM is not listed in CloudStack + 4. Verify VM is listed as part of the unmanaged instances + 5. Import VM + 6. Destroy VM + """ + + # 1 - Deploy VM + self.virtual_machine = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + templateid=self.template.id, + serviceofferingid=self.small_offering.id, + networkids=self.network.id, + zoneid=self.zone.id + ) + vm_id = self.virtual_machine.id + vm_instance_name = self.virtual_machine.instancename + hostid = self.virtual_machine.hostid + hosts = Host.list( + self.apiclient, + id=hostid + ) + host = hosts[0] + clusterid = host.clusterid + + list_vm = list_virtual_machines( + self.apiclient, + id=vm_id + ) + self.assertEqual( + isinstance(list_vm, list), + True, + "Check if virtual machine is present" + ) + vm_response = list_vm[0] + + self.assertEqual( + vm_response.state, + "Running", + "VM state should be running after deployment" + ) + + # 2 - Unmanage VM from CloudStack + self.virtual_machine.unmanage(self.apiclient) + + list_vm = list_virtual_machines( + self.apiclient, + id=vm_id + ) + + self.assertEqual( + list_vm, + None, + "VM should not be listed" + ) + + unmanaged_vms = VirtualMachine.listUnmanagedInstances( + self.apiclient, + clusterid=clusterid, + name=vm_instance_name + ) + + self.assertEqual( + len(unmanaged_vms), + 1, + "Unmanaged VMs matching instance name list size is 1" + ) + + unmanaged_vm = unmanaged_vms[0] + self.assertEqual( + unmanaged_vm.powerstate, + "PowerOn", + "Unmanaged VM is still running" + ) \ No newline at end of file diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index df38bb54a2b..7ed52dfd43d 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -966,6 +966,20 @@ class VirtualMachine: cmd.details[0]["memory"] = custommemory return apiclient.scaleVirtualMachine(cmd) + def unmanage(self, apiclient): + """Unmanage a VM from CloudStack (currently VMware only)""" + cmd = unmanageVirtualMachine.unmanageVirtualMachineCmd() + cmd.id = self.id + return apiclient.unmanageVirtualMachine(cmd) + + @classmethod + def listUnmanagedInstances(cls, apiclient, clusterid, name = None): + """List unmanaged VMs (currently VMware only)""" + cmd = listUnmanagedInstances.listUnmanagedInstancesCmd() + cmd.clusterid = clusterid + cmd.name = name + return apiclient.listUnmanagedInstances(cmd) + class Volume: """Manage Volume Life cycle