// 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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.inject.Inject;

import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ResponseGenerator;
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;
import org.apache.cloudstack.api.response.UnmanagedInstanceResponse;
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;
import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetUnmanagedInstancesAnswer;
import com.cloud.agent.api.GetUnmanagedInstancesCommand;
import com.cloud.agent.api.PrepareUnmanageVMInstanceAnswer;
import com.cloud.agent.api.PrepareUnmanageVMInstanceCommand;
import com.cloud.capacity.CapacityManager;
import com.cloud.configuration.Config;
import com.cloud.configuration.Resource;
import com.cloud.dc.DataCenter;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeployDestination;
import com.cloud.deploy.DeploymentPlanner;
import com.cloud.deploy.DeploymentPlanningManager;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InsufficientVirtualNetworkCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.UnsupportedServiceException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.Networks;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.offering.DiskOffering;
import com.cloud.offering.ServiceOffering;
import com.cloud.org.Cluster;
import com.cloud.resource.ResourceManager;
import com.cloud.serializer.GsonHelper;
import com.cloud.server.ManagementService;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.GuestOS;
import com.cloud.storage.GuestOSHypervisor;
import com.cloud.storage.Snapshot;
import com.cloud.storage.SnapshotVO;
import com.cloud.storage.StoragePool;
import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeApiService;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.GuestOSDao;
import com.cloud.storage.dao.GuestOSHypervisorDao;
import com.cloud.storage.dao.SnapshotDao;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.storage.dao.VMTemplatePoolDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.ResourceLimitService;
import com.cloud.user.UserVO;
import com.cloud.user.dao.UserDao;
import com.cloud.uservm.UserVm;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.DiskProfile;
import com.cloud.vm.NicProfile;
import com.cloud.vm.NicVO;
import com.cloud.vm.UserVmManager;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.VirtualMachineProfileImpl;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.NicDao;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
import com.cloud.vm.snapshot.dao.VMSnapshotDao;
import com.google.gson.Gson;

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(UnmanagedVMsManagerImpl.class);

    @Inject
    private AgentManager agentManager;
    @Inject
    private DataCenterDao dataCenterDao;
    @Inject
    private ClusterDao clusterDao;
    @Inject
    private HostDao hostDao;
    @Inject
    private AccountService accountService;
    @Inject
    private UserDao userDao;
    @Inject
    private VMTemplateDao templateDao;
    @Inject
    private VMTemplatePoolDao templatePoolDao;
    @Inject
    private ServiceOfferingDao serviceOfferingDao;
    @Inject
    private DiskOfferingDao diskOfferingDao;
    @Inject
    private ResourceManager resourceManager;
    @Inject
    private ResourceLimitService resourceLimitService;
    @Inject
    private UserVmManager userVmManager;
    @Inject
    private ResponseGenerator responseGenerator;
    @Inject
    private VolumeOrchestrationService volumeManager;
    @Inject
    private VolumeDao volumeDao;
    @Inject
    private PrimaryDataStoreDao primaryDataStoreDao;
    @Inject
    private NetworkDao networkDao;
    @Inject
    private NetworkOrchestrationService networkOrchestrationService;
    @Inject
    private VMInstanceDao vmDao;
    @Inject
    private CapacityManager capacityManager;
    @Inject
    private VolumeApiService volumeApiService;
    @Inject
    private DeploymentPlanningManager deploymentPlanningManager;
    @Inject
    private VirtualMachineManager virtualMachineManager;
    @Inject
    private ManagementService managementService;
    @Inject
    private NicDao nicDao;
    @Inject
    private NetworkModel networkModel;
    @Inject
    private ConfigurationDao configurationDao;
    @Inject
    private GuestOSDao guestOSDao;
    @Inject
    private GuestOSHypervisorDao guestOSHypervisorDao;
    @Inject
    private VMSnapshotDao vmSnapshotDao;
    @Inject
    private SnapshotDao snapshotDao;
    @Inject
    private UserVmDao userVmDao;

    protected Gson gson;

    public UnmanagedVMsManagerImpl() {
        gson = GsonHelper.getGsonLogger();
    }

    private VMTemplateVO createDefaultDummyVmImportTemplate() {
        VMTemplateVO template = null;
        try {
            template = VMTemplateVO.createSystemIso(templateDao.getNextInSequence(Long.class, "id"), VM_IMPORT_DEFAULT_TEMPLATE_NAME, VM_IMPORT_DEFAULT_TEMPLATE_NAME, true,
                    "", true, 64, Account.ACCOUNT_ID_SYSTEM, "",
                    "VM Import Default Template", false, 1);
            template.setState(VirtualMachineTemplate.State.Inactive);
            template = templateDao.persist(template);
            if (template == null) {
                return null;
            }
            templateDao.remove(template.getId());
            template = templateDao.findByName(VM_IMPORT_DEFAULT_TEMPLATE_NAME);
        } catch (Exception e) {
            LOGGER.error("Unable to create default dummy template for VM import", e);
        }
        return template;
    }

    private UnmanagedInstanceResponse createUnmanagedInstanceResponse(UnmanagedInstanceTO instance, Cluster cluster, Host host) {
        UnmanagedInstanceResponse response = new UnmanagedInstanceResponse();
        response.setName(instance.getName());
        if (cluster != null) {
            response.setClusterId(cluster.getUuid());
        }
        if (host != null) {
            response.setHostId(host.getUuid());
            response.setHostName(host.getName());
        }
        response.setPowerState(instance.getPowerState().toString());
        response.setCpuCores(instance.getCpuCores());
        response.setCpuSpeed(instance.getCpuSpeed());
        response.setCpuCoresPerSocket(instance.getCpuCoresPerSocket());
        response.setMemory(instance.getMemory());
        response.setOperatingSystemId(instance.getOperatingSystemId());
        response.setOperatingSystem(instance.getOperatingSystem());
        response.setObjectName("unmanagedinstance");

        if (instance.getDisks() != null) {
            for (UnmanagedInstanceTO.Disk disk : instance.getDisks()) {
                UnmanagedInstanceDiskResponse diskResponse = new UnmanagedInstanceDiskResponse();
                diskResponse.setDiskId(disk.getDiskId());
                if (StringUtils.isNotEmpty(disk.getLabel())) {
                    diskResponse.setLabel(disk.getLabel());
                }
                diskResponse.setCapacity(disk.getCapacity());
                diskResponse.setController(disk.getController());
                diskResponse.setControllerUnit(disk.getControllerUnit());
                diskResponse.setPosition(disk.getPosition());
                diskResponse.setImagePath(disk.getImagePath());
                diskResponse.setDatastoreName(disk.getDatastoreName());
                diskResponse.setDatastoreHost(disk.getDatastoreHost());
                diskResponse.setDatastorePath(disk.getDatastorePath());
                diskResponse.setDatastoreType(disk.getDatastoreType());
                response.addDisk(diskResponse);
            }
        }

        if (instance.getNics() != null) {
            for (UnmanagedInstanceTO.Nic nic : instance.getNics()) {
                NicResponse nicResponse = new NicResponse();
                nicResponse.setId(nic.getNicId());
                nicResponse.setNetworkName(nic.getNetwork());
                nicResponse.setMacAddress(nic.getMacAddress());
                if (StringUtils.isNotEmpty(nic.getAdapterType())) {
                    nicResponse.setAdapterType(nic.getAdapterType());
                }
                if (!CollectionUtils.isEmpty(nic.getIpAddress())) {
                    nicResponse.setIpAddresses(nic.getIpAddress());
                }
                nicResponse.setVlanId(nic.getVlan());
                nicResponse.setIsolatedPvlanId(nic.getPvlan());
                nicResponse.setIsolatedPvlanType(nic.getPvlanType());
                response.addNic(nicResponse);
            }
        }
        return response;
    }

    private List<String> getAdditionalNameFilters(Cluster cluster) {
        List<String> additionalNameFilter = new ArrayList<>();
        if (cluster == null) {
            return additionalNameFilter;
        }
        if (cluster.getHypervisorType() == Hypervisor.HypervisorType.VMware) {
            // VMWare considers some templates as VM and they are not filtered by VirtualMachineMO.isTemplate()
            List<VMTemplateStoragePoolVO> templates = templatePoolDao.listAll();
            for (VMTemplateStoragePoolVO template : templates) {
                additionalNameFilter.add(template.getInstallPath());
            }

            // VMWare considers some removed volumes as VM
            List<VolumeVO> volumes = volumeDao.findIncludingRemovedByZone(cluster.getDataCenterId());
            for (VolumeVO volumeVO : volumes) {
                if (volumeVO.getRemoved() == null) {
                    continue;
                }
                if (StringUtils.isEmpty(volumeVO.getChainInfo())) {
                    continue;
                }
                List<String> volumeFileNames = new ArrayList<>();
                try {
                    VirtualMachineDiskInfo diskInfo = gson.fromJson(volumeVO.getChainInfo(), VirtualMachineDiskInfo.class);
                    String[] files = diskInfo.getDiskChain();
                    if (files.length == 1) {
                        continue;
                    }
                    boolean firstFile = true;
                    for (final String file : files) {
                        if (firstFile) {
                            firstFile = false;
                            continue;
                        }
                        String path = file;
                        String[] split = path.split(" ");
                        path = split[split.length - 1];
                        split = path.split("/");
                        ;
                        path = split[split.length - 1];
                        split = path.split("\\.");
                        path = split[0];
                        if (StringUtils.isNotEmpty(path)) {
                            if (!additionalNameFilter.contains(path)) {
                                volumeFileNames.add(path);
                            }
                            if (path.contains("-")) {
                                split = path.split("-");
                                path = split[0];
                                if (StringUtils.isNotEmpty(path) && !path.equals("ROOT") && !additionalNameFilter.contains(path)) {
                                    volumeFileNames.add(path);
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    LOGGER.warn(String.format("Unable to find volume file name for volume ID: %s while adding filters unmanaged VMs", volumeVO.getUuid()), e);
                }
                if (!volumeFileNames.isEmpty()) {
                    additionalNameFilter.addAll(volumeFileNames);
                }
            }
        }
        return additionalNameFilter;
    }

    private List<String> getHostsManagedVms(List<HostVO> hosts) {
        if (CollectionUtils.isEmpty(hosts)) {
            return new ArrayList<>();
        }
        List<VMInstanceVO> instances = vmDao.listByHostOrLastHostOrHostPod(hosts.stream().map(HostVO::getId).collect(Collectors.toList()), hosts.get(0).getPodId());
        List<String> managedVms = instances.stream().map(VMInstanceVO::getInstanceName).collect(Collectors.toList());
        return managedVms;
    }

    private boolean hostSupportsServiceOffering(HostVO host, ServiceOffering serviceOffering) {
        hostDao.loadHostTags(host);
        return host.checkHostServiceOfferingTags(serviceOffering);
    }

    private boolean storagePoolSupportsDiskOffering(StoragePool pool, DiskOffering diskOffering) {
        if (pool == null) {
            return false;
        }
        if (diskOffering == null) {
            return false;
        }
        return volumeApiService.doesTargetStorageSupportDiskOffering(pool, diskOffering.getTags());
    }

    private ServiceOfferingVO getUnmanagedInstanceServiceOffering(final UnmanagedInstanceTO instance, ServiceOfferingVO serviceOffering, final Account owner, final DataCenter zone, final Map<String, String> details)
            throws ServerApiException, PermissionDeniedException, ResourceAllocationException {
        if (instance == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM is not valid"));
        }
        if (serviceOffering == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Service offering is not valid"));
        }
        accountService.checkAccess(owner, serviceOffering, zone);
        final Integer cpu = instance.getCpuCores();
        final Integer memory = instance.getMemory();
        Integer cpuSpeed = instance.getCpuSpeed() == null ? 0 : instance.getCpuSpeed();
        if (cpu == null || cpu == 0) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("CPU cores for VM (%s) not valid", instance.getName()));
        }
        if (memory == null || memory == 0) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Memory for VM (%s) not valid", instance.getName()));
        }
        if (serviceOffering.isDynamic()) {
            if (details.containsKey(VmDetailConstants.CPU_SPEED)) {
                try {
                    cpuSpeed = Integer.parseInt(details.get(VmDetailConstants.CPU_SPEED));
                } catch (Exception e) {
                }
            }
            Map<String, String> parameters = new HashMap<>();
            parameters.put(VmDetailConstants.CPU_NUMBER, String.valueOf(cpu));
            parameters.put(VmDetailConstants.MEMORY, String.valueOf(memory));
            if (serviceOffering.getSpeed() == null && cpuSpeed > 0) {
                parameters.put(VmDetailConstants.CPU_SPEED, String.valueOf(cpuSpeed));
            }
            serviceOffering.setDynamicFlag(true);
            userVmManager.validateCustomParameters(serviceOffering, parameters);
            serviceOffering = serviceOfferingDao.getComputeOffering(serviceOffering, parameters);
        } else {
            if (!cpu.equals(serviceOffering.getCpu()) && !instance.getPowerState().equals(UnmanagedInstanceTO.PowerState.PowerOff)) {
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Service offering (%s) %d CPU cores do not match VM CPU cores %d and VM is not in powered off state (Power state: %s)", serviceOffering.getUuid(), serviceOffering.getCpu(), cpu, instance.getPowerState()));
            }
            if (!memory.equals(serviceOffering.getRamSize()) && !instance.getPowerState().equals(UnmanagedInstanceTO.PowerState.PowerOff)) {
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Service offering (%s) %dMB memory does not match VM memory %dMB and VM is not in powered off state (Power state: %s)", serviceOffering.getUuid(), serviceOffering.getRamSize(), memory, instance.getPowerState()));
            }
            if (cpuSpeed != null && cpuSpeed > 0 && !cpuSpeed.equals(serviceOffering.getSpeed()) && !instance.getPowerState().equals(UnmanagedInstanceTO.PowerState.PowerOff)) {
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Service offering (%s) %dMHz CPU speed does not match VM CPU speed %dMHz and VM is not in powered off state (Power state: %s)", serviceOffering.getUuid(), serviceOffering.getSpeed(), cpuSpeed, instance.getPowerState()));
            }
        }
        resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.cpu, new Long(serviceOffering.getCpu()));
        resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.memory, new Long(serviceOffering.getRamSize()));
        return serviceOffering;
    }

    private Map<String, Network.IpAddresses> getNicIpAddresses(final List<UnmanagedInstanceTO.Nic> nics, final Map<String, Network.IpAddresses> callerNicIpAddressMap) {
        Map<String, Network.IpAddresses> nicIpAddresses = new HashMap<>();
        for (UnmanagedInstanceTO.Nic nic : nics) {
            Network.IpAddresses ipAddresses = null;
            if (MapUtils.isNotEmpty(callerNicIpAddressMap) && callerNicIpAddressMap.containsKey(nic.getNicId())) {
                ipAddresses = callerNicIpAddressMap.get(nic.getNicId());
            }
            // If IP is set to auto-assign, check NIC doesn't have more that one IP from SDK
            if (ipAddresses != null && ipAddresses.getIp4Address() != null && ipAddresses.getIp4Address().equals("auto") && !CollectionUtils.isEmpty(nic.getIpAddress())) {
                if (nic.getIpAddress().size() > 1) {
                    throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Multiple IP addresses (%s, %s) present for nic ID: %s. IP address cannot be assigned automatically, only single IP address auto-assigning supported", nic.getIpAddress().get(0), nic.getIpAddress().get(1), nic.getNicId()));
                }
                String address = nic.getIpAddress().get(0);
                if (NetUtils.isValidIp4(address)) {
                    ipAddresses.setIp4Address(address);
                }
            }
            if (ipAddresses != null) {
                nicIpAddresses.put(nic.getNicId(), ipAddresses);
            }
        }
        return nicIpAddresses;
    }

    private StoragePool getStoragePool(final UnmanagedInstanceTO.Disk disk, final DataCenter zone, final Cluster cluster) {
        StoragePool storagePool = null;
        final String dsHost = disk.getDatastoreHost();
        final String dsPath = disk.getDatastorePath();
        final String dsType = disk.getDatastoreType();
        final String dsName = disk.getDatastoreName();
        if (dsType != null) {
            List<StoragePoolVO> pools = primaryDataStoreDao.listPoolByHostPath(dsHost, dsPath);
            for (StoragePool pool : pools) {
                if (pool.getDataCenterId() == zone.getId() &&
                        (pool.getClusterId() == null || pool.getClusterId().equals(cluster.getId()))) {
                    storagePool = pool;
                    break;
                }
            }
        }

        if (storagePool == null) {
            List<StoragePoolVO> pools = primaryDataStoreDao.listPoolsByCluster(cluster.getId());
            pools.addAll(primaryDataStoreDao.listByDataCenterId(zone.getId()));
            for (StoragePool pool : pools) {
                if (pool.getPath().endsWith(dsName)) {
                    storagePool = pool;
                    break;
                }
            }
        }
        if (storagePool == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Storage pool for disk %s(%s) with datastore: %s not found in zone ID: %s", disk.getLabel(), disk.getDiskId(), disk.getDatastoreName(), zone.getUuid()));
        }
        return storagePool;
    }

    private Pair<UnmanagedInstanceTO.Disk, List<UnmanagedInstanceTO.Disk>> getRootAndDataDisks(List<UnmanagedInstanceTO.Disk> disks, final Map<String, Long> dataDiskOfferingMap) {
        UnmanagedInstanceTO.Disk rootDisk = null;
        List<UnmanagedInstanceTO.Disk> dataDisks = new ArrayList<>();
        if (disks.size() == 1) {
            rootDisk = disks.get(0);
            return new Pair<>(rootDisk, dataDisks);
        }
        Set<String> callerDiskIds = dataDiskOfferingMap.keySet();
        if (callerDiskIds.size() != disks.size() - 1) {
            String msg = String.format("VM has total %d disks for which %d disk offering mappings provided. %d disks need a disk offering for import", disks.size(), callerDiskIds.size(), disks.size()-1);
            LOGGER.error(String.format("%s. %s parameter can be used to provide disk offerings for the disks", msg, ApiConstants.DATADISK_OFFERING_LIST));
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, msg);
        }
        List<String> diskIdsWithoutOffering = new ArrayList<>();
        for (UnmanagedInstanceTO.Disk disk : disks) {
            String diskId = disk.getDiskId();
            if (!callerDiskIds.contains(diskId)) {
                diskIdsWithoutOffering.add(diskId);
                rootDisk = disk;
            } else {
                dataDisks.add(disk);
            }
        }
        if (diskIdsWithoutOffering.size() > 1) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM has total %d disks, disk offering mapping not provided for %d disks. Disk IDs that may need a disk offering - %s", disks.size(), diskIdsWithoutOffering.size()-1, String.join(", ", diskIdsWithoutOffering)));
        }
        return new Pair<>(rootDisk, dataDisks);
    }

    private void checkUnmanagedDiskAndOfferingForImport(UnmanagedInstanceTO.Disk disk, DiskOffering diskOffering, ServiceOffering serviceOffering, final Account owner, final DataCenter zone, final Cluster cluster, final boolean migrateAllowed)
            throws ServerApiException, PermissionDeniedException, ResourceAllocationException {
        if (serviceOffering == null && diskOffering == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Disk offering for disk ID: %s not found during VM import", disk.getDiskId()));
        }
        if (diskOffering != null) {
            accountService.checkAccess(owner, diskOffering, zone);
        }
        resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.volume);
        if (disk.getCapacity() == null || disk.getCapacity() == 0) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Size of disk(ID: %s) is found invalid during VM import", disk.getDiskId()));
        }
        if (diskOffering != null && !diskOffering.isCustomized() && diskOffering.getDiskSize() == 0) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Size of fixed disk offering(ID: %s) is found invalid during VM import", diskOffering.getUuid()));
        }
        if (diskOffering != null && !diskOffering.isCustomized() && diskOffering.getDiskSize() < disk.getCapacity()) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Size of disk offering(ID: %s) %dGB is found less than the size of disk(ID: %s) %dGB during VM import", diskOffering.getUuid(), (diskOffering.getDiskSize() / Resource.ResourceType.bytesToGiB), disk.getDiskId(), (disk.getCapacity() / (Resource.ResourceType.bytesToGiB))));
        }
        StoragePool storagePool = getStoragePool(disk, zone, cluster);
        if (diskOffering != null && !migrateAllowed && !storagePoolSupportsDiskOffering(storagePool, diskOffering)) {
            throw new InvalidParameterValueException(String.format("Disk offering: %s is not compatible with storage pool: %s of unmanaged disk: %s", diskOffering.getUuid(), storagePool.getUuid(), disk.getDiskId()));
        }
    }

    private void checkUnmanagedDiskAndOfferingForImport(List<UnmanagedInstanceTO.Disk> disks, final Map<String, Long> diskOfferingMap, final Account owner, final DataCenter zone, final Cluster cluster, final boolean migrateAllowed)
            throws ServerApiException, PermissionDeniedException, ResourceAllocationException {
        String diskController = null;
        for (UnmanagedInstanceTO.Disk disk : disks) {
            if (disk == null) {
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Unable to retrieve disk details for VM"));
            }
            if (!diskOfferingMap.containsKey(disk.getDiskId())) {
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Disk offering for disk ID: %s not found during VM import", disk.getDiskId()));
            }
            if (StringUtils.isEmpty(diskController)) {
                diskController = disk.getController();
            } else {
                if (!diskController.equals(disk.getController())) {
                    throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Multiple data disk controllers of different type (%s, %s) are not supported for import. Please make sure that all data disk controllers are of the same type", diskController, disk.getController()));
                }
            }
            checkUnmanagedDiskAndOfferingForImport(disk, diskOfferingDao.findById(diskOfferingMap.get(disk.getDiskId())), null, owner, zone, cluster, migrateAllowed);
        }
    }

    private void checkUnmanagedNicAndNetworkForImport(UnmanagedInstanceTO.Nic nic, Network network, final DataCenter zone, final Account owner, final boolean autoAssign) throws ServerApiException {
        if (nic == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Unable to retrieve NIC details during VM import"));
        }
        if (network == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Network for nic ID: %s not found during VM import", nic.getNicId()));
        }
        if (network.getDataCenterId() != zone.getId()) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Network(ID: %s) for nic(ID: %s) belongs to a different zone than VM to be imported", network.getUuid(), nic.getNicId()));
        }
        networkModel.checkNetworkPermissions(owner, network);
        if (!autoAssign && network.getGuestType().equals(Network.GuestType.Isolated)) {
            return;
        }

        String networkBroadcastUri = network.getBroadcastUri() == null ? null : network.getBroadcastUri().toString();
        if (nic.getVlan() != null && nic.getVlan() != 0 && nic.getPvlan() == null &&
                (StringUtils.isEmpty(networkBroadcastUri) ||
                        !networkBroadcastUri.equals(String.format("vlan://%d", nic.getVlan())))) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VLAN of network(ID: %s) %s is found different from the VLAN of nic(ID: %s) vlan://%d during VM import", network.getUuid(), networkBroadcastUri, nic.getNicId(), nic.getVlan()));
        }
        String pvLanType = nic.getPvlanType() == null ? "" : nic.getPvlanType().toLowerCase().substring(0, 1);
        if (nic.getVlan() != null && nic.getVlan() != 0 && nic.getPvlan() != null && nic.getPvlan() != 0 &&
                (StringUtils.isEmpty(network.getBroadcastUri().toString()) ||
                        !networkBroadcastUri.equals(String.format("pvlan://%d-%s%d", nic.getVlan(), pvLanType, nic.getPvlan())))) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("PVLAN of network(ID: %s) %s is found different from the VLAN of nic(ID: %s) pvlan://%d-%s%d during VM import", network.getUuid(), networkBroadcastUri, nic.getNicId(), nic.getVlan(), pvLanType, nic.getPvlan()));
        }
    }

    private void checkUnmanagedNicAndNetworkHostnameForImport(UnmanagedInstanceTO.Nic nic, Network network, final String hostName) throws ServerApiException {
        if (nic == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Unable to retrieve NIC details during VM import"));
        }
        if (network == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Network for nic ID: %s not found during VM import", nic.getNicId()));
        }
        // Check for duplicate hostname in network, get all vms hostNames in the network
        List<String> hostNames = vmDao.listDistinctHostNames(network.getId());
        if (CollectionUtils.isNotEmpty(hostNames) && hostNames.contains(hostName)) {
            throw new InvalidParameterValueException("The vm with hostName " + hostName + " already exists in the network domain: " + network.getNetworkDomain() + "; network="
                    + network);
        }
    }

    private void checkUnmanagedNicIpAndNetworkForImport(UnmanagedInstanceTO.Nic nic, Network network, final Network.IpAddresses ipAddresses) throws ServerApiException {
        if (nic == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Unable to retrieve NIC details during VM import"));
        }
        if (network == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Network for nic ID: %s not found during VM import", nic.getNicId()));
        }
        // Check IP is assigned for non L2 networks
        if (!network.getGuestType().equals(Network.GuestType.L2) && (ipAddresses == null || StringUtils.isEmpty(ipAddresses.getIp4Address()))) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("NIC(ID: %s) needs a valid IP address for it to be associated with network(ID: %s). %s parameter of API can be used for this", nic.getNicId(), network.getUuid(), ApiConstants.NIC_IP_ADDRESS_LIST));
        }
        // If network is non L2, IP v4 is assigned and not set to auto-assign, check it is available for network
        if (!network.getGuestType().equals(Network.GuestType.L2) && ipAddresses != null && StringUtils.isNotEmpty(ipAddresses.getIp4Address()) && !ipAddresses.getIp4Address().equals("auto")) {
            Set<Long> ips = networkModel.getAvailableIps(network, ipAddresses.getIp4Address());
            if (CollectionUtils.isEmpty(ips) || !ips.contains(NetUtils.ip2Long(ipAddresses.getIp4Address()))) {
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("IP address %s for NIC(ID: %s) is not available in network(ID: %s)", ipAddresses.getIp4Address(), nic.getNicId(), network.getUuid()));
            }
        }
    }

    private Map<String, Long> getUnmanagedNicNetworkMap(List<UnmanagedInstanceTO.Nic> nics, final Map<String, Long> callerNicNetworkMap, final Map<String, Network.IpAddresses> callerNicIpAddressMap, final DataCenter zone, final String hostName, final Account owner) throws ServerApiException {
        Map<String, Long> nicNetworkMap = new HashMap<>();
        String nicAdapter = null;
        for (UnmanagedInstanceTO.Nic nic : nics) {
            if (StringUtils.isEmpty(nicAdapter)) {
                nicAdapter = nic.getAdapterType();
            } else {
                if (!nicAdapter.equals(nic.getAdapterType())) {
                    throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Multiple network adapter of different type (%s, %s) are not supported for import. Please make sure that all network adapters are of the same type", nicAdapter, nic.getAdapterType()));
                }
            }
            Network network = null;
            Network.IpAddresses ipAddresses = null;
            if (MapUtils.isNotEmpty(callerNicIpAddressMap) && callerNicIpAddressMap.containsKey(nic.getNicId())) {
                ipAddresses = callerNicIpAddressMap.get(nic.getNicId());
            }
            if (!callerNicNetworkMap.containsKey(nic.getNicId())) {
                if (nic.getVlan() != null && nic.getVlan() != 0) {
                    // Find a suitable network
                    List<NetworkVO> networks = networkDao.listByZone(zone.getId());
                    for (NetworkVO networkVO : networks) {
                        if (networkVO.getTrafficType() == Networks.TrafficType.None || Networks.TrafficType.isSystemNetwork(networkVO.getTrafficType())) {
                            continue;
                        }
                        try {
                            checkUnmanagedNicAndNetworkForImport(nic, networkVO, zone, owner, true);
                            network = networkVO;
                        } catch (Exception e) {
                        }
                        if (network != null) {
                            checkUnmanagedNicAndNetworkHostnameForImport(nic, network, hostName);
                            checkUnmanagedNicIpAndNetworkForImport(nic, network, ipAddresses);
                            break;
                        }
                    }
                }
            } else {
                network = networkDao.findById(callerNicNetworkMap.get(nic.getNicId()));
                checkUnmanagedNicAndNetworkForImport(nic, network, zone, owner, false);
                checkUnmanagedNicAndNetworkHostnameForImport(nic, network, hostName);
                checkUnmanagedNicIpAndNetworkForImport(nic, network, ipAddresses);
            }
            if (network == null) {
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Suitable network for nic(ID: %s) not found during VM import", nic.getNicId()));
            }
            nicNetworkMap.put(nic.getNicId(), network.getId());
        }
        return nicNetworkMap;
    }

    private Pair<DiskProfile, StoragePool> importDisk(UnmanagedInstanceTO.Disk disk, VirtualMachine vm, Cluster cluster, DiskOffering diskOffering,
                                                      Volume.Type type, String name, Long diskSize, Long minIops, Long maxIops, VirtualMachineTemplate template,
                                                      Account owner, Long deviceId) {
        final DataCenter zone = dataCenterDao.findById(vm.getDataCenterId());
        final String path = StringUtils.isEmpty(disk.getFileBaseName()) ? disk.getImagePath() : disk.getFileBaseName();
        String chainInfo = disk.getChainInfo();
        if (StringUtils.isEmpty(chainInfo)) {
            VirtualMachineDiskInfo diskInfo = new VirtualMachineDiskInfo();
            diskInfo.setDiskDeviceBusName(String.format("%s%d:%d", disk.getController(), disk.getControllerUnit(), disk.getPosition()));
            diskInfo.setDiskChain(new String[]{disk.getImagePath()});
            chainInfo = gson.toJson(diskInfo);
        }
        StoragePool storagePool = getStoragePool(disk, zone, cluster);
        DiskProfile profile = volumeManager.importVolume(type, name, diskOffering, diskSize,
                minIops, maxIops, vm, template, owner, deviceId, storagePool.getId(), path, chainInfo);

        return new Pair<DiskProfile, StoragePool>(profile, storagePool);
    }

    private NicProfile importNic(UnmanagedInstanceTO.Nic nic, VirtualMachine vm, Network network, Network.IpAddresses ipAddresses, int deviceId, boolean isDefaultNic, boolean forced) throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException {
        Pair<NicProfile, Integer> result = networkOrchestrationService.importNic(nic.getMacAddress(), deviceId, network, isDefaultNic, vm, ipAddresses, forced);
        if (result == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("NIC ID: %s import failed", nic.getNicId()));
        }
        return result.first();
    }

    private void cleanupFailedImportVM(final UserVm userVm) {
        if (userVm == null) {
            return;
        }
        VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVm);
        // Remove all volumes
        volumeDao.deleteVolumesByInstance(userVm.getId());
        // Remove all nics
        try {
            networkOrchestrationService.release(profile, true);
        } catch (Exception e) {
            LOGGER.error(String.format("Unable to release NICs for unsuccessful import unmanaged VM: %s", userVm.getInstanceName()), e);
            nicDao.removeNicsForInstance(userVm.getId());
        }
        // Remove vm
        vmDao.remove(userVm.getId());
    }

    private UserVm migrateImportedVM(HostVO sourceHost, VirtualMachineTemplate template, ServiceOfferingVO serviceOffering, UserVm userVm, final Account owner, List<Pair<DiskProfile, StoragePool>> diskProfileStoragePoolList) {
        UserVm vm = userVm;
        if (vm == null) {
            LOGGER.error(String.format("Failed to check migrations need during VM import"));
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to check migrations need during VM import"));
        }
        if (sourceHost == null || serviceOffering == null || diskProfileStoragePoolList == null) {
            LOGGER.error(String.format("Failed to check migrations need during import, VM: %s", userVm.getInstanceName()));
            cleanupFailedImportVM(vm);
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to check migrations need during import, VM: %s", userVm.getInstanceName()));
        }
        if (!hostSupportsServiceOffering(sourceHost, serviceOffering)) {
            LOGGER.debug(String.format("VM %s needs to be migrated", vm.getUuid()));
            final VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm, template, serviceOffering, owner, null);
            DeploymentPlanner.ExcludeList excludeList = new DeploymentPlanner.ExcludeList();
            excludeList.addHost(sourceHost.getId());
            final DataCenterDeployment plan = new DataCenterDeployment(sourceHost.getDataCenterId(), sourceHost.getPodId(), sourceHost.getClusterId(), null, null, null);
            DeployDestination dest = null;
            try {
                dest = deploymentPlanningManager.planDeployment(profile, plan, excludeList, null);
            } catch (Exception e) {
                LOGGER.warn(String.format("VM import failed for unmanaged vm: %s during vm migration, finding deployment destination", vm.getInstanceName()), e);
                cleanupFailedImportVM(vm);
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed for unmanaged vm: %s during vm migration, finding deployment destination", vm.getInstanceName()));
            }
            if (dest != null) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug(" Found " + dest + " for migrating the vm to");
                }
            }
            if (dest == null) {
                cleanupFailedImportVM(vm);
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed for unmanaged vm: %s during vm migration, no deployment destination found", vm.getInstanceName()));
            }
            try {
                if (vm.getState().equals(VirtualMachine.State.Stopped)) {
                    VMInstanceVO vmInstanceVO = vmDao.findById(userVm.getId());
                    vmInstanceVO.setHostId(dest.getHost().getId());
                    vmInstanceVO.setLastHostId(dest.getHost().getId());
                    vmDao.update(vmInstanceVO.getId(), vmInstanceVO);
                } else {
                    virtualMachineManager.migrate(vm.getUuid(), sourceHost.getId(), dest);
                }
                vm = userVmManager.getUserVm(vm.getId());
            } catch (Exception e) {
                LOGGER.error(String.format("VM import failed for unmanaged vm: %s during vm migration", vm.getInstanceName()), e);
                cleanupFailedImportVM(vm);
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed for unmanaged vm: %s during vm migration. %s", userVm.getInstanceName(), e.getMessage()));
            }
        }
        for (Pair<DiskProfile, StoragePool> diskProfileStoragePool : diskProfileStoragePoolList) {
            if (diskProfileStoragePool == null ||
                    diskProfileStoragePool.first() == null ||
                    diskProfileStoragePool.second() == null) {
                continue;
            }
            DiskProfile profile = diskProfileStoragePool.first();
            DiskOffering dOffering = diskOfferingDao.findById(profile.getDiskOfferingId());
            if (dOffering == null) {
                continue;
            }
            VolumeVO volumeVO = volumeDao.findById(profile.getVolumeId());
            if (volumeVO == null) {
                continue;
            }
            boolean poolSupportsOfferings = storagePoolSupportsDiskOffering(diskProfileStoragePool.second(), dOffering);
            if (poolSupportsOfferings) {
                continue;
            }
            LOGGER.debug(String.format("Volume %s needs to be migrated", volumeVO.getUuid()));
            Pair<List<? extends StoragePool>, List<? extends StoragePool>> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(profile.getVolumeId(), null, null, null, null, false, true);
            if (CollectionUtils.isEmpty(poolsPair.first()) && CollectionUtils.isEmpty(poolsPair.second())) {
                cleanupFailedImportVM(vm);
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed for unmanaged vm: %s during volume ID: %s migration as no suitable pool(s) found", userVm.getInstanceName(), volumeVO.getUuid()));
            }
            List<? extends StoragePool> storagePools = poolsPair.second();
            StoragePool storagePool = null;
            if (CollectionUtils.isNotEmpty(storagePools)) {
                for (StoragePool pool : storagePools) {
                    if (diskProfileStoragePool.second().getId() != pool.getId() &&
                            storagePoolSupportsDiskOffering(pool, dOffering)
                            ) {
                        storagePool = pool;
                        break;
                    }
                }
            }
            // For zone-wide pools, at times, suitable storage pools are not returned therefore consider all pools.
            if (storagePool == null && CollectionUtils.isNotEmpty(poolsPair.first())) {
                storagePools = poolsPair.first();
                for (StoragePool pool : storagePools) {
                    if (diskProfileStoragePool.second().getId() != pool.getId() &&
                            storagePoolSupportsDiskOffering(pool, dOffering)
                            ) {
                        storagePool = pool;
                        break;
                    }
                }
            }
            if (storagePool == null) {
                cleanupFailedImportVM(vm);
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed for unmanaged vm: %s during volume ID: %s migration as no suitable pool found", userVm.getInstanceName(), volumeVO.getUuid()));
            } else {
                LOGGER.debug(String.format("Found storage pool %s(%s) for migrating the volume %s to", storagePool.getName(), storagePool.getUuid(), volumeVO.getUuid()));
            }
            try {
                Volume volume = null;
                if (vm.getState().equals(VirtualMachine.State.Running)) {
                    volume = volumeManager.liveMigrateVolume(volumeVO, storagePool);
                } else {
                    volume = volumeManager.migrateVolume(volumeVO, storagePool);
                }
                if (volume == null) {
                    String msg = "";
                    if (vm.getState().equals(VirtualMachine.State.Running)) {
                        msg = String.format("Live migration for volume ID: %s to destination pool ID: %s failed", volumeVO.getUuid(), storagePool.getUuid());
                    } else {
                        msg = String.format("Migration for volume ID: %s to destination pool ID: %s failed", volumeVO.getUuid(), storagePool.getUuid());
                    }
                    LOGGER.error(msg);
                    throw new CloudRuntimeException(msg);
                }
            } catch (Exception e) {
                LOGGER.error(String.format("VM import failed for unmanaged vm: %s during volume migration", vm.getInstanceName()), e);
                cleanupFailedImportVM(vm);
                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed for unmanaged vm: %s during volume migration. %s", userVm.getInstanceName(), StringUtils.defaultString(e.getMessage())));
            }
        }
        return userVm;
    }

    private void publishVMUsageUpdateResourceCount(final UserVm userVm, ServiceOfferingVO serviceOfferingVO) {
        if (userVm == null || serviceOfferingVO == null) {
            LOGGER.error("Failed to publish usage records during VM import");
            cleanupFailedImportVM(userVm);
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed for unmanaged vm during publishing usage records"));
        }
        try {
            if (!serviceOfferingVO.isDynamic()) {
                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_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);
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed for unmanaged vm %s during publishing usage records", userVm.getInstanceName()));
        }
        resourceLimitService.incrementResourceCount(userVm.getAccountId(), Resource.ResourceType.user_vm, userVm.isDisplayVm());
        resourceLimitService.incrementResourceCount(userVm.getAccountId(), Resource.ResourceType.cpu, userVm.isDisplayVm(), new Long(serviceOfferingVO.getCpu()));
        resourceLimitService.incrementResourceCount(userVm.getAccountId(), Resource.ResourceType.memory, userVm.isDisplayVm(), new Long(serviceOfferingVO.getRamSize()));
        // Save usage event and update resource count for user vm volumes
        List<VolumeVO> volumes = volumeDao.findByInstance(userVm.getId());
        for (VolumeVO volume : volumes) {
            try {
                UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_CREATE, volume.getAccountId(), volume.getDataCenterId(), volume.getId(), volume.getName(), volume.getDiskOfferingId(), null, volume.getSize(),
                        Volume.class.getName(), volume.getUuid(), volume.isDisplayVolume());
            } catch (Exception e) {
                LOGGER.error(String.format("Failed to publish volume ID: %s usage records during VM import", volume.getUuid()), e);
            }
            resourceLimitService.incrementResourceCount(userVm.getAccountId(), Resource.ResourceType.volume, volume.isDisplayVolume());
            resourceLimitService.incrementResourceCount(userVm.getAccountId(), Resource.ResourceType.primary_storage, volume.isDisplayVolume(), volume.getSize());
        }

        List<NicVO> 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", StringUtils.defaultString(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<String, Long> dataDiskOfferingMap,
                                                final Map<String, Long> nicNetworkMap, final Map<String, Network.IpAddresses> callerNicIpAddressMap,
                                                final Map<String, String> details, final boolean migrateAllowed, final boolean forced) {
        UserVm userVm = null;

        ServiceOfferingVO validatedServiceOffering = null;
        try {
            validatedServiceOffering = getUnmanagedInstanceServiceOffering(unmanagedInstance, serviceOffering, owner, zone, details);
        } catch (Exception e) {
            LOGGER.error("Service offering for VM import not compatible", e);
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import VM: %s. %s", unmanagedInstance.getName(), StringUtils.defaultString(e.getMessage())));
        }

        String internalCSName = unmanagedInstance.getInternalCSName();
        if(StringUtils.isEmpty(internalCSName)){
            internalCSName = instanceName;
        }
        Map<String, String> allDetails = new HashMap<>(details);
        if (validatedServiceOffering.isDynamic()) {
            allDetails.put(VmDetailConstants.CPU_NUMBER, String.valueOf(validatedServiceOffering.getCpu()));
            allDetails.put(VmDetailConstants.MEMORY, String.valueOf(validatedServiceOffering.getRamSize()));
            if (serviceOffering.getSpeed() == null) {
                allDetails.put(VmDetailConstants.CPU_SPEED, String.valueOf(validatedServiceOffering.getSpeed()));
            }
        }

        if (!migrateAllowed && !hostSupportsServiceOffering(host, validatedServiceOffering)) {
            throw new InvalidParameterValueException(String.format("Service offering: %s is not compatible with host: %s of unmanaged VM: %s", serviceOffering.getUuid(), host.getUuid(), instanceName));
        }
        // Check disks and supplied disk offerings
        List<UnmanagedInstanceTO.Disk> unmanagedInstanceDisks = unmanagedInstance.getDisks();
        if (CollectionUtils.isEmpty(unmanagedInstanceDisks)) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("No attached disks found for the unmanaged VM: %s", instanceName));
        }
        Pair<UnmanagedInstanceTO.Disk, List<UnmanagedInstanceTO.Disk>> rootAndDataDisksPair = getRootAndDataDisks(unmanagedInstanceDisks, dataDiskOfferingMap);
        final UnmanagedInstanceTO.Disk rootDisk = rootAndDataDisksPair.first();
        final List<UnmanagedInstanceTO.Disk> dataDisks = rootAndDataDisksPair.second();
        if (rootDisk == null || StringUtils.isEmpty(rootDisk.getController())) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed. Unable to retrieve root disk details for VM: %s ", instanceName));
        }
        allDetails.put(VmDetailConstants.ROOT_DISK_CONTROLLER, rootDisk.getController());
        try {
            checkUnmanagedDiskAndOfferingForImport(rootDisk, null, validatedServiceOffering, owner, zone, cluster, migrateAllowed);
            if (CollectionUtils.isNotEmpty(dataDisks)) { // Data disk(s) present
                checkUnmanagedDiskAndOfferingForImport(dataDisks, dataDiskOfferingMap, owner, zone, cluster, migrateAllowed);
                allDetails.put(VmDetailConstants.DATA_DISK_CONTROLLER, dataDisks.get(0).getController());
            }
            resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.volume, unmanagedInstanceDisks.size());
        } catch (ResourceAllocationException e) {
            LOGGER.error(String.format("Volume resource allocation error for owner: %s", owner.getUuid()), e);
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume resource allocation error for owner: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage())));
        }
        // Check NICs and supplied networks
        Map<String, Network.IpAddresses> nicIpAddressMap = getNicIpAddresses(unmanagedInstance.getNics(), callerNicIpAddressMap);
        Map<String, Long> allNicNetworkMap = getUnmanagedNicNetworkMap(unmanagedInstance.getNics(), nicNetworkMap, nicIpAddressMap, zone, hostName, owner);
        if (!CollectionUtils.isEmpty(unmanagedInstance.getNics())) {
            allDetails.put(VmDetailConstants.NIC_ADAPTER, unmanagedInstance.getNics().get(0).getAdapterType());
        }
        VirtualMachine.PowerState powerState = VirtualMachine.PowerState.PowerOff;
        if (unmanagedInstance.getPowerState().equals(UnmanagedInstanceTO.PowerState.PowerOn)) {
            powerState = VirtualMachine.PowerState.PowerOn;
        }
        try {
            userVm = userVmManager.importVM(zone, host, template, internalCSName, displayName, owner,
                    null, caller, true, null, owner.getAccountId(), userId,
                    validatedServiceOffering, null, hostName,
                    cluster.getHypervisorType(), allDetails, powerState);
        } catch (InsufficientCapacityException ice) {
            LOGGER.error(String.format("Failed to import vm name: %s", instanceName), ice);
            throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, ice.getMessage());
        }
        if (userVm == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName));
        }
        List<Pair<DiskProfile, StoragePool>> diskProfileStoragePoolList = new ArrayList<>();
        try {
            if (rootDisk.getCapacity() == null || rootDisk.getCapacity() == 0) {
                throw new InvalidParameterValueException(String.format("Root disk ID: %s size is invalid", rootDisk.getDiskId()));
            }
            Long minIops = null;
            if (details.containsKey("minIops")) {
                minIops = Long.parseLong(details.get("minIops"));
            }
            Long maxIops = null;
            if (details.containsKey("maxIops")) {
                maxIops = Long.parseLong(details.get("maxIops"));
            }
            DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId());
            diskProfileStoragePoolList.add(importDisk(rootDisk, userVm, cluster, diskOffering, Volume.Type.ROOT, String.format("ROOT-%d", userVm.getId()),
                    (rootDisk.getCapacity() / Resource.ResourceType.bytesToGiB), minIops, maxIops,
                    template, owner, null));
            long deviceId = 1L;
            for (UnmanagedInstanceTO.Disk disk : dataDisks) {
                if (disk.getCapacity() == null || disk.getCapacity() == 0) {
                    throw new InvalidParameterValueException(String.format("Disk ID: %s size is invalid", rootDisk.getDiskId()));
                }
                DiskOffering offering = diskOfferingDao.findById(dataDiskOfferingMap.get(disk.getDiskId()));
                diskProfileStoragePoolList.add(importDisk(disk, userVm, cluster, offering, Volume.Type.DATADISK, String.format("DATA-%d-%s", userVm.getId(), disk.getDiskId()),
                        (disk.getCapacity() / Resource.ResourceType.bytesToGiB), offering.getMinIops(), offering.getMaxIops(),
                        template, owner, deviceId));
                deviceId++;
            }
        } catch (Exception e) {
            LOGGER.error(String.format("Failed to import volumes while importing vm: %s", instanceName), e);
            cleanupFailedImportVM(userVm);
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import volumes while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage())));
        }
        try {
            int nicIndex = 0;
            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, nicIndex, nicIndex==0, forced);
                nicIndex++;
            }
        } catch (Exception e) {
            LOGGER.error(String.format("Failed to import NICs while importing vm: %s", instanceName), e);
            cleanupFailedImportVM(userVm);
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import NICs while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage())));
        }
        if (migrateAllowed) {
            userVm = migrateImportedVM(host, template, validatedServiceOffering, userVm, owner, diskProfileStoragePoolList);
        }
        publishVMUsageUpdateResourceCount(userVm, validatedServiceOffering);
        return userVm;
    }

    private HashMap<String, UnmanagedInstanceTO> getUnmanagedInstancesForHost(HostVO host, String instanceName, List<String> managedVms) {
        HashMap<String, UnmanagedInstanceTO> unmanagedInstances = new HashMap<>();
        if (host.isInMaintenanceStates()) {
            return unmanagedInstances;
        }

        GetUnmanagedInstancesCommand command = new GetUnmanagedInstancesCommand();
        command.setInstanceName(instanceName);
        command.setManagedInstancesNames(managedVms);
        Answer answer = agentManager.easySend(host.getId(), command);
        if (!(answer instanceof GetUnmanagedInstancesAnswer)) {
            return unmanagedInstances;
        }
        GetUnmanagedInstancesAnswer unmanagedInstancesAnswer = (GetUnmanagedInstancesAnswer) answer;
        unmanagedInstances = unmanagedInstancesAnswer.getUnmanagedInstances();
        return unmanagedInstances;
    }

    @Override
    public ListResponse<UnmanagedInstanceResponse> listUnmanagedInstances(ListUnmanagedInstancesCmd cmd) {
        final Account caller = CallContext.current().getCallingAccount();
        if (caller.getType() != Account.Type.ADMIN) {
            throw new PermissionDeniedException(String.format("Cannot perform this operation, Calling account is not root admin: %s", caller.getUuid()));
        }
        final Long clusterId = cmd.getClusterId();
        if (clusterId == null) {
            throw new InvalidParameterValueException(String.format("Cluster ID cannot be null"));
        }
        final Cluster cluster = clusterDao.findById(clusterId);
        if (cluster == null) {
            throw new InvalidParameterValueException(String.format("Cluster ID: %d cannot be found", clusterId));
        }
        if (cluster.getHypervisorType() != Hypervisor.HypervisorType.VMware) {
            throw new InvalidParameterValueException(String.format("VM ingestion is currently not supported for hypervisor: %s", cluster.getHypervisorType().toString()));
        }
        String keyword = cmd.getKeyword();
        if (StringUtils.isNotEmpty(keyword)) {
            keyword = keyword.toLowerCase();
        }
        List<HostVO> hosts = resourceManager.listHostsInClusterByStatus(clusterId, Status.Up);
        List<String> additionalNameFilters = getAdditionalNameFilters(cluster);
        List<String> managedVms = new ArrayList<>(additionalNameFilters);
        managedVms.addAll(getHostsManagedVms(hosts));
        List<UnmanagedInstanceResponse> responses = new ArrayList<>();
        for (HostVO host : hosts) {
            HashMap<String, UnmanagedInstanceTO> unmanagedInstances = getUnmanagedInstancesForHost(host, cmd.getName(), managedVms);
            Set<String> keys = unmanagedInstances.keySet();
            for (String key : keys) {
                UnmanagedInstanceTO instance = unmanagedInstances.get(key);
                if (StringUtils.isNotEmpty(keyword) &&
                        !instance.getName().toLowerCase().contains(keyword)) {
                    continue;
                }
                responses.add(createUnmanagedInstanceResponse(instance, cluster, host));
            }
        }
        ListResponse<UnmanagedInstanceResponse> listResponses = new ListResponse<>();
        listResponses.setResponses(responses, responses.size());
        return listResponses;
    }

    @Override
    public UserVmResponse importUnmanagedInstance(ImportUnmanagedInstanceCmd cmd) {
        final Account caller = CallContext.current().getCallingAccount();
        if (caller.getType() != Account.Type.ADMIN) {
            throw new PermissionDeniedException(String.format("Cannot perform this operation, Calling account is not root admin: %s", caller.getUuid()));
        }
        final Long clusterId = cmd.getClusterId();
        final Cluster cluster = clusterDao.findById(clusterId);
        if (cluster == null) {
            throw new InvalidParameterValueException(String.format("Cluster ID: %d cannot be found", clusterId));
        }
        if (cluster.getHypervisorType() != Hypervisor.HypervisorType.VMware) {
            throw new InvalidParameterValueException(String.format("VM import is currently not supported for hypervisor: %s", cluster.getHypervisorType().toString()));
        }
        final DataCenter zone = dataCenterDao.findById(cluster.getDataCenterId());
        final String instanceName = cmd.getName();
        if (StringUtils.isEmpty(instanceName)) {
            throw new InvalidParameterValueException("Instance name cannot be empty");
        }
        if (cmd.getDomainId() != null && StringUtils.isEmpty(cmd.getAccountName())) {
            throw new InvalidParameterValueException(String.format("%s parameter must be specified with %s parameter", ApiConstants.DOMAIN_ID, ApiConstants.ACCOUNT));
        }
        final Account owner = accountService.getActiveAccountById(cmd.getEntityOwnerId());
        long userId = CallContext.current().getCallingUserId();
        List<UserVO> userVOs = userDao.listByAccount(owner.getAccountId());
        if (CollectionUtils.isNotEmpty(userVOs)) {
            userId = userVOs.get(0).getId();
        }
        VMTemplateVO template;
        final Long templateId = cmd.getTemplateId();
        if (templateId == null) {
            template = templateDao.findByName(VM_IMPORT_DEFAULT_TEMPLATE_NAME);
            if (template == null) {
                template = createDefaultDummyVmImportTemplate();
                if (template == null) {
                    throw new InvalidParameterValueException(String.format("Default VM import template with unique name: %s for hypervisor: %s cannot be created. Please use templateid parameter for import", VM_IMPORT_DEFAULT_TEMPLATE_NAME, cluster.getHypervisorType().toString()));
                }
            }
        } else {
            template = templateDao.findById(templateId);
        }
        if (template == null) {
            throw new InvalidParameterValueException(String.format("Template ID: %d cannot be found", templateId));
        }
        final Long serviceOfferingId = cmd.getServiceOfferingId();
        final ServiceOfferingVO serviceOffering = serviceOfferingDao.findById(serviceOfferingId);
        if (serviceOffering == null) {
            throw new InvalidParameterValueException(String.format("Service offering ID: %d cannot be found", serviceOfferingId));
        }
        accountService.checkAccess(owner, serviceOffering, zone);
        try {
            resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.user_vm, 1);
        } catch (ResourceAllocationException e) {
            LOGGER.error(String.format("VM resource allocation error for account: %s", owner.getUuid()), e);
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM resource allocation error for account: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage())));
        }
        String displayName = cmd.getDisplayName();
        if (StringUtils.isEmpty(displayName)) {
            displayName = instanceName;
        }
        String hostName = cmd.getHostName();
        if (StringUtils.isEmpty(hostName)) {
            if (!NetUtils.verifyDomainNameLabel(instanceName, true)) {
                throw new InvalidParameterValueException("Please provide hostname for the VM. VM name contains unsupported characters for it to be used as hostname");
            }
            hostName = instanceName;
        }
        if (!NetUtils.verifyDomainNameLabel(hostName, true)) {
            throw new InvalidParameterValueException("Invalid VM hostname. VM hostname can contain ASCII letters 'a' through 'z', the digits '0' through '9', "
                    + "and the hyphen ('-'), must be between 1 and 63 characters long, and can't start or end with \"-\" and can't start with digit");
        }
        if (cluster.getHypervisorType().equals(Hypervisor.HypervisorType.VMware) &&
                Boolean.parseBoolean(configurationDao.getValue(Config.SetVmInternalNameUsingDisplayName.key()))) {
            // If global config vm.instancename.flag is set to true, then CS will set guest VM's name as it appears on the hypervisor, to its hostname.
            // In case of VMware since VM name must be unique within a DC, check if VM with the same hostname already exists in the zone.
            VMInstanceVO vmByHostName = vmDao.findVMByHostNameInZone(hostName, zone.getId());
            if (vmByHostName != null && vmByHostName.getState() != VirtualMachine.State.Expunging) {
                throw new InvalidParameterValueException(String.format("Failed to import VM: %s. There already exists a VM by the hostname: %s in zone: %s", instanceName, hostName, zone.getUuid()));
            }
        }
        final Map<String, Long> nicNetworkMap = cmd.getNicNetworkList();
        final Map<String, Network.IpAddresses> nicIpAddressMap = cmd.getNicIpAddressList();
        final Map<String, Long> dataDiskOfferingMap = cmd.getDataDiskToDiskOfferingList();
        final Map<String, String> details = cmd.getDetails();
        final boolean forced = cmd.isForced();
        List<HostVO> hosts = resourceManager.listHostsInClusterByStatus(clusterId, Status.Up);
        UserVm userVm = null;
        List<String> additionalNameFilters = getAdditionalNameFilters(cluster);
        List<String> managedVms = new ArrayList<>(additionalNameFilters);
        managedVms.addAll(getHostsManagedVms(hosts));
        for (HostVO host : hosts) {
            HashMap<String, UnmanagedInstanceTO> unmanagedInstances = getUnmanagedInstancesForHost(host, cmd.getName(), managedVms);
            if (MapUtils.isEmpty(unmanagedInstances)) {
                continue;
            }
            Set<String> names = unmanagedInstances.keySet();
            for (String name : names) {
                if (!instanceName.equals(name)) {
                    continue;
                }
                UnmanagedInstanceTO unmanagedInstance = unmanagedInstances.get(name);
                if (unmanagedInstance == null) {
                    throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Unable to retrieve details for unmanaged VM: %s", name));
                }
                if (template.getName().equals(VM_IMPORT_DEFAULT_TEMPLATE_NAME)) {
                    String osName = unmanagedInstance.getOperatingSystem();
                    GuestOS guestOS = null;
                    if (StringUtils.isNotEmpty(osName)) {
                        guestOS = guestOSDao.findOneByDisplayName(osName);
                    }
                    GuestOSHypervisor guestOSHypervisor = null;
                    if (guestOS != null) {
                        guestOSHypervisor = guestOSHypervisorDao.findByOsIdAndHypervisor(guestOS.getId(), host.getHypervisorType().toString(), host.getHypervisorVersion());
                    }
                    if (guestOSHypervisor == null && StringUtils.isNotEmpty(unmanagedInstance.getOperatingSystemId())) {
                        guestOSHypervisor = guestOSHypervisorDao.findByOsNameAndHypervisor(unmanagedInstance.getOperatingSystemId(), host.getHypervisorType().toString(), host.getHypervisorVersion());
                    }
                    if (guestOSHypervisor == null) {
                        if (guestOS != null) {
                            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Unable to find hypervisor guest OS ID: %s details for unmanaged VM: %s for hypervisor: %s version: %s. templateid parameter can be used to assign template for VM", guestOS.getUuid(), name, host.getHypervisorType().toString(), host.getHypervisorVersion()));
                        }
                        throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Unable to retrieve guest OS details for unmanaged VM: %s with OS name: %s, OS ID: %s for hypervisor: %s version: %s. templateid parameter can be used to assign template for VM", name, osName, unmanagedInstance.getOperatingSystemId(), host.getHypervisorType().toString(), host.getHypervisorVersion()));
                    }
                    template.setGuestOSId(guestOSHypervisor.getGuestOsId());
                }
                userVm = importVirtualMachineInternal(unmanagedInstance, instanceName, zone, cluster, host,
                        template, displayName, hostName, caller, owner, userId,
                        serviceOffering, dataDiskOfferingMap,
                        nicNetworkMap, nicIpAddressMap,
                        details, cmd.getMigrateAllowed(), forced);
                break;
            }
            if (userVm != null) {
                break;
            }
        }
        if (userVm == null) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to find unmanaged vm with name: %s in cluster: %s", instanceName, cluster.getUuid()));
        }
        return responseGenerator.createUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVm).get(0);
    }

    @Override
    public List<Class<?>> getCommands() {
        final List<Class<?>> cmdList = new ArrayList<Class<?>>();
        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<VolumeVO> volumes = volumeDao.findByInstance(vmVO.getId());
        for (VolumeVO volume : volumes) {
            List<SnapshotVO> 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<HostVO> 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 };
    }
}
