// 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.hypervisor.guru;

import static com.cloud.utils.NumbersUtil.toHumanReadableSize;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.inject.Inject;

import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.backup.Backup;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.storage.command.CopyCommand;
import org.apache.cloudstack.storage.command.DeleteCommand;
import org.apache.cloudstack.storage.command.DownloadCommand;
import org.apache.cloudstack.storage.command.StorageSubSystemCommand;
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.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import com.cloud.agent.api.BackupSnapshotCommand;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.CreatePrivateTemplateFromSnapshotCommand;
import com.cloud.agent.api.CreatePrivateTemplateFromVolumeCommand;
import com.cloud.agent.api.CreateVolumeFromSnapshotCommand;
import com.cloud.agent.api.MigrateVmToPoolCommand;
import com.cloud.agent.api.UnregisterNicCommand;
import com.cloud.agent.api.UnregisterVMCommand;
import com.cloud.agent.api.storage.CopyVolumeCommand;
import com.cloud.agent.api.storage.CreateEntityDownloadURLCommand;
import com.cloud.agent.api.storage.CreateVolumeOVACommand;
import com.cloud.agent.api.storage.PrepareOVAPackingCommand;
import com.cloud.agent.api.to.DataObjectType;
import com.cloud.agent.api.to.DataStoreTO;
import com.cloud.agent.api.to.DataTO;
import com.cloud.agent.api.to.DiskTO;
import com.cloud.agent.api.to.StorageFilerTO;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.agent.api.to.VolumeTO;
import com.cloud.cluster.ClusterManager;
import com.cloud.configuration.Resource;
import com.cloud.dc.ClusterDetailsDao;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDetailsDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.hypervisor.HypervisorGuru;
import com.cloud.hypervisor.HypervisorGuruBase;
import com.cloud.hypervisor.vmware.VmwareDatacenterVO;
import com.cloud.hypervisor.vmware.VmwareDatacenterZoneMapVO;
import com.cloud.hypervisor.vmware.dao.VmwareDatacenterDao;
import com.cloud.hypervisor.vmware.dao.VmwareDatacenterZoneMapDao;
import com.cloud.hypervisor.vmware.manager.VmwareManager;
import com.cloud.hypervisor.vmware.mo.DatacenterMO;
import com.cloud.hypervisor.vmware.mo.NetworkMO;
import com.cloud.hypervisor.vmware.mo.VirtualDiskManagerMO;
import com.cloud.hypervisor.vmware.mo.VirtualMachineDiskInfoBuilder;
import com.cloud.hypervisor.vmware.mo.VirtualMachineMO;
import com.cloud.hypervisor.vmware.resource.VmwareContextFactory;
import com.cloud.hypervisor.vmware.util.VmwareContext;
import com.cloud.network.Network;
import com.cloud.network.Networks;
import com.cloud.network.Networks.BroadcastDomainType;
import com.cloud.network.Networks.TrafficType;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkTrafficTypeDao;
import com.cloud.network.dao.PhysicalNetworkTrafficTypeVO;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.secstorage.CommandExecLogDao;
import com.cloud.secstorage.CommandExecLogVO;
import com.cloud.serializer.GsonHelper;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.GuestOSVO;
import com.cloud.storage.Storage;
import com.cloud.storage.StorageManager;
import com.cloud.storage.StoragePool;
import com.cloud.storage.StoragePoolHostVO;
import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.VMTemplateStorageResourceAssoc;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.GuestOSDao;
import com.cloud.storage.dao.StoragePoolHostDao;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.storage.dao.VMTemplatePoolDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.secondary.SecondaryStorageVmManager;
import com.cloud.user.ResourceLimitService;
import com.cloud.utils.Pair;
import com.cloud.utils.UuidUtils;
import com.cloud.utils.db.DB;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.NicProfile;
import com.cloud.vm.NicVO;
import com.cloud.vm.SecondaryStorageVmVO;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachine.Type;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
import com.google.gson.Gson;
import com.vmware.vim25.ManagedObjectReference;
import com.vmware.vim25.VirtualDevice;
import com.vmware.vim25.VirtualDeviceBackingInfo;
import com.vmware.vim25.VirtualDeviceConnectInfo;
import com.vmware.vim25.VirtualDisk;
import com.vmware.vim25.VirtualDiskFlatVer2BackingInfo;
import com.vmware.vim25.VirtualEthernetCard;
import com.vmware.vim25.VirtualEthernetCardNetworkBackingInfo;
import com.vmware.vim25.VirtualMachineConfigSummary;
import com.vmware.vim25.VirtualMachineRuntimeInfo;

public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Configurable {
    private static final Logger s_logger = Logger.getLogger(VMwareGuru.class);
    private static final Gson GSON = GsonHelper.getGson();


    @Inject VmwareVmImplementer vmwareVmImplementer;
    @Inject GuestOSDao _guestOsDao;
    @Inject HostDetailsDao _hostDetailsDao;
    @Inject ClusterDetailsDao _clusterDetailsDao;
    @Inject CommandExecLogDao _cmdExecLogDao;
    @Inject VmwareManager _vmwareMgr;
    @Inject VMInstanceDao vmDao;
    @Inject SecondaryStorageVmManager _secStorageMgr;
    @Inject PhysicalNetworkTrafficTypeDao _physicalNetworkTrafficTypeDao;
    @Inject VirtualMachineManager vmManager;
    @Inject ClusterManager _clusterMgr;
    @Inject VolumeDao _volumeDao;
    @Inject ResourceLimitService _resourceLimitService;
    @Inject PrimaryDataStoreDao _storagePoolDao;
    @Inject VolumeDataFactory _volFactory;
    @Inject VmwareDatacenterDao vmwareDatacenterDao;
    @Inject VmwareDatacenterZoneMapDao vmwareDatacenterZoneMapDao;
    @Inject VMTemplatePoolDao templateStoragePoolDao;
    @Inject VMTemplateDao vmTemplateDao;
    @Inject UserVmDao userVmDao;
    @Inject DiskOfferingDao diskOfferingDao;
    @Inject PhysicalNetworkDao physicalNetworkDao;
    @Inject StoragePoolHostDao storagePoolHostDao;

    protected VMwareGuru() {
        super();
    }

    public static final ConfigKey<Boolean> VmwareReserveCpu = new ConfigKey<Boolean>(Boolean.class, "vmware.reserve.cpu", "Advanced", "false",
            "Specify whether or not to reserve CPU when deploying an instance.", true, ConfigKey.Scope.Cluster, null);

    public static final ConfigKey<Boolean> VmwareReserveMemory = new ConfigKey<Boolean>(Boolean.class, "vmware.reserve.mem", "Advanced", "false",
            "Specify whether or not to reserve memory when deploying an instance.", true, ConfigKey.Scope.Cluster, null);

    public static final ConfigKey<Boolean> VmwareEnableNestedVirtualization = new ConfigKey<Boolean>(Boolean.class, "vmware.nested.virtualization", "Advanced", "false",
            "When set to true this will enable nested virtualization when this is supported by the hypervisor", true, ConfigKey.Scope.Global, null);

    public static final ConfigKey<Boolean> VmwareEnableNestedVirtualizationPerVM = new ConfigKey<Boolean>(Boolean.class, "vmware.nested.virtualization.perVM", "Advanced", "false",
            "When set to true this will enable nested virtualization per vm", true, ConfigKey.Scope.Global, null);

    @Override public HypervisorType getHypervisorType() {
        return HypervisorType.VMware;
    }

    @Override public VirtualMachineTO implement(VirtualMachineProfile vm) {
        vmwareVmImplementer.setGlobalNestedVirtualisationEnabled(VmwareEnableNestedVirtualization.value());
        vmwareVmImplementer.setGlobalNestedVPerVMEnabled(VmwareEnableNestedVirtualizationPerVM.value());
        return vmwareVmImplementer.implement(vm, toVirtualMachineTO(vm), vmManager.findClusterAndHostIdForVm(vm.getId()).first());
    }

    @Override @DB public Pair<Boolean, Long> getCommandHostDelegation(long hostId, Command cmd) {
        if (s_logger.isTraceEnabled()) {
            s_logger.trace(String.format("Finding delegation for command of type %s to host %d.", cmd.getClass(), hostId));
        }

        boolean needDelegation = false;
        if (cmd instanceof StorageSubSystemCommand) {
            StorageSubSystemCommand c = (StorageSubSystemCommand)cmd;
            c.setExecuteInSequence(StorageManager.shouldExecuteInSequenceOnVmware());
        }
        if (cmd instanceof DownloadCommand) {
            cmd.setContextParam(VmwareManager.s_vmwareOVAPackageTimeout.key(), String.valueOf(VmwareManager.s_vmwareOVAPackageTimeout.value()));
        }
        //NOTE: the hostid can be a hypervisor host, or a ssvm agent. For copycommand, if it's for volume upload, the hypervisor
        //type is empty, so we need to check the format of volume at first.
        if (cmd instanceof CopyCommand) {
            CopyCommand cpyCommand = (CopyCommand)cmd;
            DataTO srcData = cpyCommand.getSrcTO();
            DataStoreTO srcStoreTO = srcData.getDataStore();
            DataTO destData = cpyCommand.getDestTO();
            DataStoreTO destStoreTO = destData.getDataStore();

            boolean inSeq = true;
            if (parallelExecutionAllowed(srcData, destData, srcStoreTO, destStoreTO)) {
                inSeq = false;
            }
            cpyCommand.setExecuteInSequence(inSeq);

            if (srcData.getObjectType() == DataObjectType.VOLUME) {
                VolumeObjectTO volumeObjectTO = (VolumeObjectTO)srcData;
                if (Storage.ImageFormat.OVA == volumeObjectTO.getFormat()) {
                    needDelegation = true;
                }
            }

            if (!needDelegation && !(HypervisorType.VMware == srcData.getHypervisorType() || HypervisorType.VMware == destData.getHypervisorType())) {
                return new Pair<Boolean, Long>(Boolean.FALSE, new Long(hostId));
            }

            if (destData.getObjectType() == DataObjectType.VOLUME && destStoreTO.getRole() == DataStoreRole.Primary && srcData.getObjectType() == DataObjectType.TEMPLATE
                    && srcStoreTO.getRole() == DataStoreRole.Primary) {
                needDelegation = false;
            } else {
                needDelegation = true;
            }
        } else if (cmd instanceof CreateEntityDownloadURLCommand) {
            DataTO srcData = ((CreateEntityDownloadURLCommand)cmd).getData();
            if ((HypervisorType.VMware == srcData.getHypervisorType())) {
                needDelegation = true;
            }
            if (srcData.getObjectType() == DataObjectType.VOLUME) {
                VolumeObjectTO volumeObjectTO = (VolumeObjectTO)srcData;
                if (Storage.ImageFormat.OVA == volumeObjectTO.getFormat()) {
                    needDelegation = true;
                }
            }
        }

        if (s_logger.isTraceEnabled()) {
            s_logger.trace(String.format("Command of type %s is going to be executed in sequence? %b", cmd.getClass(), cmd.executeInSequence()));
            s_logger.trace(String.format("Command of type %s is going to need delegation? %b", cmd.getClass(), needDelegation));
        }

        if (!needDelegation) {
            return new Pair<Boolean, Long>(Boolean.FALSE, new Long(hostId));
        }
        HostVO host = hostDao.findById(hostId);
        long dcId = host.getDataCenterId();
        Pair<HostVO, SecondaryStorageVmVO> cmdTarget = _secStorageMgr.assignSecStorageVm(dcId, cmd);
        if (cmdTarget != null) {
            // TODO, we need to make sure agent is actually connected too

            cmd.setContextParam("hypervisor", HypervisorType.VMware.toString());
            if (host.getType() == Host.Type.Routing) {
                Map<String, String> hostDetails = _hostDetailsDao.findDetails(hostId);
                cmd.setContextParam("guid", resolveNameInGuid(hostDetails.get("guid")));
                cmd.setContextParam("username", hostDetails.get("username"));
                cmd.setContextParam("password", hostDetails.get("password"));
                cmd.setContextParam("serviceconsole", _vmwareMgr.getServiceConsolePortGroupName());
                cmd.setContextParam("manageportgroup", _vmwareMgr.getManagementPortGroupName());
            }

            CommandExecLogVO execLog = new CommandExecLogVO(cmdTarget.first().getId(), cmdTarget.second().getId(), cmd.getClass().getSimpleName(), 1);
            _cmdExecLogDao.persist(execLog);
            cmd.setContextParam("execid", String.valueOf(execLog.getId()));
            cmd.setContextParam("noderuninfo", String.format("%d-%d", _clusterMgr.getManagementNodeId(), _clusterMgr.getCurrentRunId()));
            cmd.setContextParam("vCenterSessionTimeout", String.valueOf(_vmwareMgr.getVcenterSessionTimeout()));
            cmd.setContextParam(VmwareManager.s_vmwareOVAPackageTimeout.key(), String.valueOf(VmwareManager.s_vmwareOVAPackageTimeout.value()));

            if (cmd instanceof BackupSnapshotCommand || cmd instanceof CreatePrivateTemplateFromVolumeCommand || cmd instanceof CreatePrivateTemplateFromSnapshotCommand || cmd instanceof CopyVolumeCommand || cmd instanceof CopyCommand || cmd instanceof CreateVolumeOVACommand || cmd instanceof PrepareOVAPackingCommand
                    || cmd instanceof CreateVolumeFromSnapshotCommand) {
                String workerName = _vmwareMgr.composeWorkerName();
                long checkPointId = 1;
                // FIXME: Fix                    long checkPointId = _checkPointMgr.pushCheckPoint(new VmwareCleanupMaid(hostDetails.get("guid"), workerName));
                cmd.setContextParam("worker", workerName);
                cmd.setContextParam("checkpoint", String.valueOf(checkPointId));

                // some commands use 2 workers
                String workerName2 = _vmwareMgr.composeWorkerName();
                long checkPointId2 = 1;
                // FIXME: Fix                    long checkPointId2 = _checkPointMgr.pushCheckPoint(new VmwareCleanupMaid(hostDetails.get("guid"), workerName2));
                cmd.setContextParam("worker2", workerName2);
                cmd.setContextParam("checkpoint2", String.valueOf(checkPointId2));
                cmd.setContextParam("searchexludefolders", _vmwareMgr.s_vmwareSearchExcludeFolder.value());
            }

            return new Pair<Boolean, Long>(Boolean.TRUE, cmdTarget.first().getId());

        }
        return new Pair<Boolean, Long>(Boolean.FALSE, new Long(hostId));
    }

    private boolean parallelExecutionAllowed(DataTO srcData, DataTO destData, DataStoreTO srcStoreTO, DataStoreTO destStoreTO) {
        Long srcId = srcStoreTO == null || srcStoreTO.getUuid() == null ? null : _storagePoolDao.findByUuid(srcStoreTO.getUuid()).getId();
        Long dstId = destStoreTO == null || destStoreTO.getUuid() == null ? null : _storagePoolDao.findByUuid(destStoreTO.getUuid()).getId();
        return (srcData.getObjectType() == DataObjectType.SNAPSHOT)
                || (destData.getObjectType() == DataObjectType.SNAPSHOT)
                || (destStoreTO != null && destStoreTO.getRole() == DataStoreRole.Image)
                || (destStoreTO != null && destStoreTO.getRole() == DataStoreRole.ImageCache)
                || !StorageManager.shouldExecuteInSequenceOnVmware(srcId, dstId);
    }

    @Override
    public boolean trackVmHostChange() {
        return true;
    }

    private static String resolveNameInGuid(String guid) {
        String tokens[] = guid.split("@");
        assert (tokens.length == 2);

        String vCenterIp = NetUtils.resolveToIp(tokens[1]);
        if (vCenterIp == null) {
            s_logger.error("Fatal : unable to resolve vCenter address " + tokens[1] + ", please check your DNS configuration");
            return guid;
        }

        if (vCenterIp.equals(tokens[1]))
            return guid;

        return tokens[0] + "@" + vCenterIp;
    }

    @Override public List<Command> finalizeExpungeNics(VirtualMachine vm, List<NicProfile> nics) {
        List<Command> commands = new ArrayList<Command>();
        List<NicVO> nicVOs = nicDao.listByVmId(vm.getId());
        for (NicVO nic : nicVOs) {
            NetworkVO network = networkDao.findById(nic.getNetworkId());
            if (network.getBroadcastDomainType() == BroadcastDomainType.Lswitch) {
                s_logger.debug("Nic " + nic.toString() + " is connected to an lswitch, cleanup required");
                NetworkVO networkVO = networkDao.findById(nic.getNetworkId());
                // We need the traffic label to figure out which vSwitch has the
                // portgroup
                PhysicalNetworkTrafficTypeVO trafficTypeVO = _physicalNetworkTrafficTypeDao.findBy(networkVO.getPhysicalNetworkId(), networkVO.getTrafficType());
                UnregisterNicCommand unregisterNicCommand = new UnregisterNicCommand(vm.getInstanceName(), trafficTypeVO.getVmwareNetworkLabel(), UUID.fromString(nic.getUuid()));
                commands.add(unregisterNicCommand);
            }
        }
        return commands;
    }

    @Override public String getConfigComponentName() {
        return VMwareGuru.class.getSimpleName();
    }

    @Override public ConfigKey<?>[] getConfigKeys() {
        return new ConfigKey<?>[] {VmwareReserveCpu, VmwareReserveMemory, VmwareEnableNestedVirtualization, VmwareEnableNestedVirtualizationPerVM};
    }

    @Override public List<Command> finalizeExpungeVolumes(VirtualMachine vm) {
        List<Command> commands = new ArrayList<Command>();

        List<VolumeVO> volumes = _volumeDao.findByInstance(vm.getId());

        if (volumes != null) {
            for (VolumeVO volume : volumes) {
                StoragePoolVO storagePool = _storagePoolDao.findById(volume.getPoolId());

                // storagePool should be null if we are expunging a volume that was never
                // attached to a VM that was started (the "trick" for storagePool to be null
                // is that none of the VMs this volume may have been attached to were ever started,
                // so the volume was never assigned to a storage pool)
                if (storagePool != null && storagePool.isManaged() && volume.getVolumeType() == Volume.Type.ROOT) {
                    VolumeInfo volumeInfo = _volFactory.getVolume(volume.getId());
                    PrimaryDataStore primaryDataStore = (PrimaryDataStore)volumeInfo.getDataStore();
                    Map<String, String> details = primaryDataStore.getDetails();

                    if (details == null) {
                        details = new HashMap<String, String>();

                        primaryDataStore.setDetails(details);
                    }

                    details.put(DiskTO.MANAGED, Boolean.TRUE.toString());

                    DeleteCommand cmd = new DeleteCommand(volumeInfo.getTO());

                    commands.add(cmd);

                    break;
                }
            }
        }

        return commands;
    }

    @Override public Map<String, String> getClusterSettings(long vmId) {
        Map<String, String> details = new HashMap<String, String>();
        Long clusterId = vmManager.findClusterAndHostIdForVm(vmId).first();
        if (clusterId != null) {
            details.put(VmwareReserveCpu.key(), VmwareReserveCpu.valueIn(clusterId).toString());
            details.put(VmwareReserveMemory.key(), VmwareReserveMemory.valueIn(clusterId).toString());
        }
        return details;
    }

    /**
     * Get vmware datacenter mapped to the zoneId
     */
    private VmwareDatacenterVO getVmwareDatacenter(long zoneId) {
        VmwareDatacenterZoneMapVO zoneMap = vmwareDatacenterZoneMapDao.findByZoneId(zoneId);
        long vmwareDcId = zoneMap.getVmwareDcId();
        return vmwareDatacenterDao.findById(vmwareDcId);
    }

    /**
     * Get Vmware datacenter MO
     */
    private DatacenterMO getDatacenterMO(long zoneId) throws Exception {
        VmwareDatacenterVO vmwareDatacenter = getVmwareDatacenter(zoneId);
        VmwareContext context = VmwareContextFactory.getContext(vmwareDatacenter.getVcenterHost(), vmwareDatacenter.getUser(), vmwareDatacenter.getPassword());
        DatacenterMO dcMo = new DatacenterMO(context, vmwareDatacenter.getVmwareDatacenterName());
        ManagedObjectReference dcMor = dcMo.getMor();
        if (dcMor == null) {
            String msg = "Error while getting Vmware datacenter " + vmwareDatacenter.getVmwareDatacenterName();
            s_logger.error(msg);
            throw new InvalidParameterValueException(msg);
        }
        return dcMo;
    }

    /**
     * Get guest OS ID for VM being imported.
     * If it cannot be found it is mapped to: "Other (64-bit)" ID
     */
    private Long getImportingVMGuestOs(VirtualMachineConfigSummary configSummary) {
        String guestFullName = configSummary.getGuestFullName();
        GuestOSVO os = _guestOsDao.findOneByDisplayName(guestFullName);
        return os != null ? os.getId() : _guestOsDao.findOneByDisplayName("Other (64-bit)").getId();
    }

    /**
     * Create and persist service offering
     */
    private ServiceOfferingVO createServiceOfferingForVMImporting(Integer cpus, Integer memory, Integer maxCpuUsage) {
        String name = "Imported-" + cpus + "-" + memory;

        DiskOfferingVO diskOfferingVO = new DiskOfferingVO(name, name, Storage.ProvisioningType.THIN, false, null, false, false, true);
        diskOfferingVO = diskOfferingDao.persistDefaultDiskOffering(diskOfferingVO);

        ServiceOfferingVO vo = new ServiceOfferingVO(name, cpus, memory, maxCpuUsage, null, null, false, name, false, Type.User,
                false);
        vo.setDiskOfferingId(diskOfferingVO.getId());
        return serviceOfferingDao.persist(vo);
    }

    /**
     * Get service offering ID for VM being imported.
     * If it cannot be found it creates one and returns its ID
     */
    private Long getImportingVMServiceOffering(VirtualMachineConfigSummary configSummary, VirtualMachineRuntimeInfo runtimeInfo) {
        Integer numCpu = configSummary.getNumCpu();
        Integer memorySizeMB = configSummary.getMemorySizeMB();
        Integer maxCpuUsage = runtimeInfo.getMaxCpuUsage();
        List<ServiceOfferingVO> offerings = serviceOfferingDao.listPublicByCpuAndMemory(numCpu, memorySizeMB);
        return CollectionUtils.isEmpty(offerings) ? createServiceOfferingForVMImporting(numCpu, memorySizeMB, maxCpuUsage).getId() : offerings.get(0).getId();
    }

    /**
     * Check if disk is ROOT disk
     */
    private boolean isRootDisk(VirtualDisk disk, Map<VirtualDisk, VolumeVO> disksMapping, Backup backup) {
        if (!disksMapping.containsKey(disk)) {
            return false;
        }
        VolumeVO volumeVO = disksMapping.get(disk);
        if (volumeVO == null) {
            final VMInstanceVO vm = virtualMachineDao.findByIdIncludingRemoved(backup.getVmId());
            if (vm == null) {
                throw new CloudRuntimeException("Failed to find the volumes details from the VM backup");
            }
            List<Backup.VolumeInfo> backedUpVolumes = vm.getBackupVolumeList();
            for (Backup.VolumeInfo backedUpVolume : backedUpVolumes) {
                if (backedUpVolume.getSize().equals(disk.getCapacityInBytes())) {
                    return backedUpVolume.getType().equals(Volume.Type.ROOT);
                }
            }
        } else {
            return volumeVO.getVolumeType().equals(Volume.Type.ROOT);
        }
        throw new CloudRuntimeException("Could not determinate ROOT disk for VM to import");
    }

    /**
     * Check backing info
     */
    private void checkBackingInfo(VirtualDeviceBackingInfo backingInfo) {
        if (!(backingInfo instanceof VirtualDiskFlatVer2BackingInfo)) {
            String errorMessage = String.format("Unsupported backing info. Expected: [%s], but received: [%s].", VirtualDiskFlatVer2BackingInfo.class.getSimpleName(), backingInfo.getClass().getSimpleName());
            s_logger.error(errorMessage);
            throw new CloudRuntimeException(errorMessage);
        }
    }

    /**
     * Get pool ID from datastore UUID
     */
    private Long getPoolIdFromDatastoreUuid(String datastoreUuid) {
        String poolUuid = UuidUtils.normalize(datastoreUuid);
        StoragePoolVO pool = _storagePoolDao.findByUuid(poolUuid);
        if (pool == null) {
            throw new CloudRuntimeException("Couldn't find storage pool " + poolUuid);
        }
        return pool.getId();
    }

    /**
     * Get pool ID for disk
     */
    private Long getPoolId(VirtualDisk disk) {
        VirtualDeviceBackingInfo backing = disk.getBacking();
        checkBackingInfo(backing);
        VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo)backing;
        String[] fileNameParts = info.getFileName().split(" ");
        String datastoreUuid = StringUtils.substringBetween(fileNameParts[0], "[", "]");
        return getPoolIdFromDatastoreUuid(datastoreUuid);
    }

    /**
     * Get volume name from filename
     */
    private String getVolumeNameFromFileName(String fileName) {
        String[] fileNameParts = fileName.split(" ");
        String volumePath = fileNameParts[1];
        return volumePath.split("/")[1].replaceFirst(".vmdk", "");
    }

    /**
     * Get root disk template path
     */
    private String getRootDiskTemplatePath(VirtualDisk rootDisk) {
        VirtualDeviceBackingInfo backing = rootDisk.getBacking();
        checkBackingInfo(backing);
        VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo)backing;
        VirtualDiskFlatVer2BackingInfo parent = info.getParent();
        return (parent != null) ? getVolumeNameFromFileName(parent.getFileName()) : getVolumeNameFromFileName(info.getFileName());
    }

    /**
     * Get template MO
     */
    private VirtualMachineMO getTemplate(DatacenterMO dcMo, String templatePath) throws Exception {
        VirtualMachineMO template = dcMo.findVm(templatePath);
        if (!template.isTemplate()) {
            throw new CloudRuntimeException(templatePath + " is not a template");
        }
        return template;
    }

    /**
     * Get template pool ID
     */
    private Long getTemplatePoolId(VirtualMachineMO template) throws Exception {
        VirtualMachineConfigSummary configSummary = template.getConfigSummary();
        String vmPathName = configSummary.getVmPathName();
        String[] pathParts = vmPathName.split(" ");
        String dataStoreUuid = pathParts[0].replace("[", "").replace("]", "");
        return getPoolIdFromDatastoreUuid(dataStoreUuid);
    }

    /**
     * Get template size
     */
    private Long getTemplateSize(VirtualMachineMO template, String vmInternalName, Map<VirtualDisk, VolumeVO> disksMapping, Backup backup) throws Exception {
        List<VirtualDisk> disks = template.getVirtualDisks();
        if (CollectionUtils.isEmpty(disks)) {
            throw new CloudRuntimeException("Couldn't find VM template size");
        }
        return disks.get(0).getCapacityInBytes();
    }

    /**
     * Create a VM Template record on DB
     */
    private VMTemplateVO createVMTemplateRecord(String vmInternalName, long guestOsId, long accountId) {
        Long nextTemplateId = vmTemplateDao.getNextInSequence(Long.class, "id");
        VMTemplateVO templateVO = new VMTemplateVO(nextTemplateId, "Imported-from-" + vmInternalName, Storage.ImageFormat.OVA, false, false, false, Storage.TemplateType.USER, null,
                false, 64, accountId, null, "Template imported from VM " + vmInternalName, false, guestOsId, false, HypervisorType.VMware, null, null, false, false, false, false);
        return vmTemplateDao.persist(templateVO);
    }

    /**
     * Retrieve the template ID matching the template on templatePath. There are 2 cases:
     * - There are no references on DB for primary storage -> create a template DB record and return its ID
     * - There are references on DB for primary storage -> return template ID for any of those references
     */
    private long getTemplateId(String templatePath, String vmInternalName, Long guestOsId, long accountId) {
        List<VMTemplateStoragePoolVO> poolRefs = templateStoragePoolDao.listByTemplatePath(templatePath);
        return CollectionUtils.isNotEmpty(poolRefs) ? poolRefs.get(0).getTemplateId() : createVMTemplateRecord(vmInternalName, guestOsId, accountId).getId();
    }

    /**
     * Update template reference on primary storage, if needed
     */
    private void updateTemplateRef(long templateId, Long poolId, String templatePath, Long templateSize) {
        VMTemplateStoragePoolVO templateRef = templateStoragePoolDao.findByPoolPath(poolId, templatePath);
        if (templateRef == null) {
            templateRef = new VMTemplateStoragePoolVO(poolId, templateId, null, 100, VMTemplateStorageResourceAssoc.Status.DOWNLOADED, templatePath, null, null, templatePath,
                    templateSize, null);
            templateRef.setState(ObjectInDataStoreStateMachine.State.Ready);
            templateStoragePoolDao.persist(templateRef);
        }
    }

    /**
     * Get template ID for VM being imported. If it is not found, it is created
     */
    private Long getImportingVMTemplate(List<VirtualDisk> virtualDisks, DatacenterMO dcMo, String vmInternalName, Long guestOsId, long accountId, Map<VirtualDisk, VolumeVO> disksMapping, Backup backup) throws Exception {
        for (VirtualDisk disk : virtualDisks) {
            if (isRootDisk(disk, disksMapping, backup)) {
                VolumeVO volumeVO = disksMapping.get(disk);
                if (volumeVO == null) {
                    String templatePath = getRootDiskTemplatePath(disk);
                    VirtualMachineMO template = getTemplate(dcMo, templatePath);
                    Long poolId = getTemplatePoolId(template);
                    Long templateSize = getTemplateSize(template, vmInternalName, disksMapping, backup);
                    long templateId = getTemplateId(templatePath, vmInternalName, guestOsId, accountId);
                    updateTemplateRef(templateId, poolId, templatePath, templateSize);
                    return templateId;
                } else {
                    return volumeVO.getTemplateId();
                }
            }
        }
        throw new CloudRuntimeException("Could not find ROOT disk for VM " + vmInternalName);
    }

    /**
     * If VM does not exist: create and persist VM
     * If VM exists: update VM
     */
    private VMInstanceVO getVM(String vmInternalName, long templateId, long guestOsId, long serviceOfferingId, long zoneId, long accountId, long userId, long domainId) {
        s_logger.debug(String.format("Trying to get VM with specs: [vmInternalName: %s, templateId: %s, guestOsId: %s, serviceOfferingId: %s].", vmInternalName,
                templateId, guestOsId, serviceOfferingId));
        VMInstanceVO vm = virtualMachineDao.findVMByInstanceNameIncludingRemoved(vmInternalName);
        if (vm != null) {
            s_logger.debug(String.format("Found an existing VM [id: %s, removed: %s] with internalName: [%s].", vm.getUuid(), vm.getRemoved() != null ? "yes" : "no", vmInternalName));
            vm.setState(VirtualMachine.State.Stopped);
            vm.setPowerState(VirtualMachine.PowerState.PowerOff);
            virtualMachineDao.update(vm.getId(), vm);
            if (vm.getRemoved() != null) {
                virtualMachineDao.unremove(vm.getId());
                UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_CREATE, accountId, vm.getDataCenterId(), vm.getId(), vm.getHostName(), vm.getServiceOfferingId(), vm.getTemplateId(),
                        vm.getHypervisorType().toString(), VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplayVm());
            }
            return virtualMachineDao.findById(vm.getId());
        } else {
            long id = userVmDao.getNextInSequence(Long.class, "id");
            s_logger.debug(String.format("Can't find an existing VM with internalName: [%s]. Creating a new VM with: [id: %s, name: %s, templateId: %s, guestOsId: %s, serviceOfferingId: %s].",
                    vmInternalName, id, vmInternalName, templateId, guestOsId, serviceOfferingId));

            UserVmVO vmInstanceVO = new UserVmVO(id, vmInternalName, vmInternalName, templateId, HypervisorType.VMware, guestOsId, false, false, domainId, accountId, userId,
                    serviceOfferingId, null, null, null, vmInternalName);
            vmInstanceVO.setDataCenterId(zoneId);
            return userVmDao.persist(vmInstanceVO);
        }
    }

    /**
     * Create and persist volume
     */
    private VolumeVO createVolumeRecord(Volume.Type type, String volumeName, long zoneId, long domainId, long accountId, long diskOfferingId, Storage.ProvisioningType provisioningType,
            Long size, long instanceId, Long poolId, long templateId, Integer unitNumber, VirtualMachineDiskInfo diskInfo) {
        VolumeVO volumeVO = new VolumeVO(type, volumeName, zoneId, domainId, accountId, diskOfferingId, provisioningType, size, null, null, null);
        volumeVO.setFormat(Storage.ImageFormat.OVA);
        volumeVO.setPath(volumeName);
        volumeVO.setState(Volume.State.Ready);
        volumeVO.setInstanceId(instanceId);
        volumeVO.setPoolId(poolId);
        volumeVO.setTemplateId(templateId);
        volumeVO.setAttached(new Date());
        volumeVO.setRemoved(null);
        volumeVO.setChainInfo(GSON.toJson(diskInfo));
        if (unitNumber != null) {
            volumeVO.setDeviceId(unitNumber.longValue());
        }
        return _volumeDao.persist(volumeVO);
    }

    /**
     * Get volume name from VM disk
     */
    private String getVolumeName(VirtualDisk disk, VirtualMachineMO vmToImport) throws Exception {
        return vmToImport.getVmdkFileBaseName(disk);
    }

    /**
     * Get provisioning type for VM disk info
     */
    private Storage.ProvisioningType getProvisioningType(VirtualDiskFlatVer2BackingInfo backing) {
        Boolean thinProvisioned = backing.isThinProvisioned();
        if (BooleanUtils.isTrue(thinProvisioned)) {
            return Storage.ProvisioningType.THIN;
        }
        return Storage.ProvisioningType.SPARSE;
    }

    /**
     * Get disk offering ID for volume being imported. If it is not found it is mapped to "Custom" ID
     */
    private long getDiskOfferingId(long size, Storage.ProvisioningType provisioningType) {
        List<DiskOfferingVO> offerings = diskOfferingDao.listAllBySizeAndProvisioningType(size, provisioningType);
        return CollectionUtils.isNotEmpty(offerings) ? offerings.get(0).getId() : diskOfferingDao.findByUniqueName("Cloud.Com-Custom").getId();
    }

    protected VolumeVO updateVolume(VirtualDisk disk, Map<VirtualDisk, VolumeVO> disksMapping, VirtualMachineMO vmToImport, Long poolId, VirtualMachine vm) throws Exception {
        VolumeVO volume = disksMapping.get(disk);
        String volumeName = getVolumeName(disk, vmToImport);
        volume.setPath(volumeName);
        volume.setPoolId(poolId);
        VirtualMachineDiskInfo diskInfo = getDiskInfo(vmToImport, poolId, volumeName);
        volume.setChainInfo(GSON.toJson(diskInfo));
        volume.setInstanceId(vm.getId());
        volume.setState(Volume.State.Ready);
        volume.setAttached(new Date());
        _volumeDao.update(volume.getId(), volume);
        if (volume.getRemoved() != null) {
            s_logger.debug(String.format("Marking volume [uuid: %s] of restored VM [%s] as non removed.", volume.getUuid(),
                    ReflectionToStringBuilderUtils.reflectOnlySelectedFields(vm, "uuid", "instanceName")));
            _volumeDao.unremove(volume.getId());
            if (vm.getType() == Type.User) {
                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());
                _resourceLimitService.incrementResourceCount(vm.getAccountId(), Resource.ResourceType.volume, volume.isDisplayVolume());
                _resourceLimitService.incrementResourceCount(vm.getAccountId(), Resource.ResourceType.primary_storage, volume.isDisplayVolume(), volume.getSize());
            }
        }
        return volume;
    }

    /**
     * Get volumes for VM being imported
     */
    private void syncVMVolumes(VMInstanceVO vmInstanceVO, List<VirtualDisk> virtualDisks, Map<VirtualDisk, VolumeVO> disksMapping, VirtualMachineMO vmToImport, Backup backup)
            throws Exception {
        long zoneId = vmInstanceVO.getDataCenterId();
        long accountId = vmInstanceVO.getAccountId();
        long domainId = vmInstanceVO.getDomainId();
        long templateId = vmInstanceVO.getTemplateId();
        long instanceId = vmInstanceVO.getId();

        String operation = "";
        for (VirtualDisk disk : virtualDisks) {
            Long poolId = getPoolId(disk);
            Volume volume = null;
            if (disksMapping.containsKey(disk) && disksMapping.get(disk) != null) {
                volume = updateVolume(disk, disksMapping, vmToImport, poolId, vmInstanceVO);
                operation = "updated";
            } else {
                volume = createVolume(disk, vmToImport, domainId, zoneId, accountId, instanceId, poolId, templateId, backup, true);
                operation = "created";
            }
            s_logger.debug(String.format("Sync volumes to %s in backup restore operation: %s volume [id: %s].", vmInstanceVO, operation, volume.getUuid()));
        }
    }

    private VirtualMachineDiskInfo getDiskInfo(VirtualMachineMO vmMo, Long poolId, String volumeName) throws Exception {
        VirtualMachineDiskInfoBuilder diskInfoBuilder = vmMo.getDiskInfoBuilder();
        String poolName = _storagePoolDao.findById(poolId).getUuid().replace("-", "");
        return diskInfoBuilder.getDiskInfoByBackingFileBaseName(volumeName, poolName);
    }

    private VolumeVO createVolume(VirtualDisk disk, VirtualMachineMO vmToImport, long domainId, long zoneId, long accountId, long instanceId, Long poolId, long templateId, Backup backup, boolean isImport) throws Exception {
        VMInstanceVO vm = virtualMachineDao.findByIdIncludingRemoved(backup.getVmId());
        if (vm == null) {
            throw new CloudRuntimeException("Failed to find the backup volume information from the VM backup");
        }
        List<Backup.VolumeInfo> backedUpVolumes = vm.getBackupVolumeList();
        Volume.Type type = Volume.Type.DATADISK;
        Long size = disk.getCapacityInBytes();
        if (isImport) {
            for (Backup.VolumeInfo volumeInfo : backedUpVolumes) {
                if (volumeInfo.getSize().equals(disk.getCapacityInBytes())) {
                    type = volumeInfo.getType();
                }
            }
        }
        VirtualDeviceBackingInfo backing = disk.getBacking();
        checkBackingInfo(backing);
        VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo)backing;
        String volumeName = getVolumeName(disk, vmToImport);
        Storage.ProvisioningType provisioningType = getProvisioningType(info);
        long diskOfferingId = getDiskOfferingId(size, provisioningType);
        Integer unitNumber = disk.getUnitNumber();
        VirtualMachineDiskInfo diskInfo = getDiskInfo(vmToImport, poolId, volumeName);
        return createVolumeRecord(type, volumeName, zoneId, domainId, accountId, diskOfferingId, provisioningType, size, instanceId, poolId, templateId, unitNumber, diskInfo);
    }

    protected String createVolumeInfoFromVolumes(List<VolumeVO> vmVolumes) {
        try {
            List<Backup.VolumeInfo> list = new ArrayList<>();
            for (VolumeVO vol : vmVolumes) {
                list.add(new Backup.VolumeInfo(vol.getUuid(), vol.getPath(), vol.getVolumeType(), vol.getSize()));
            }
            return GSON.toJson(list.toArray(), Backup.VolumeInfo[].class);
        } catch (Exception e) {
            if (CollectionUtils.isEmpty(vmVolumes) || vmVolumes.get(0).getInstanceId() == null) {
                s_logger.error(String.format("Failed to create VolumeInfo of VM [id: null] volumes due to: [%s].", e.getMessage()), e);
            } else {
                s_logger.error(String.format("Failed to create VolumeInfo of VM [id: %s] volumes due to: [%s].", vmVolumes.get(0).getInstanceId(), e.getMessage()), e);
            }
            throw e;
        }
    }

    /**
     * Get physical network ID from zoneId and Vmware label
     */
    private long getPhysicalNetworkId(Long zoneId, String tag) {
        List<PhysicalNetworkVO> physicalNetworks = physicalNetworkDao.listByZone(zoneId);
        for (PhysicalNetworkVO physicalNetwork : physicalNetworks) {
            PhysicalNetworkTrafficTypeVO vo = _physicalNetworkTrafficTypeDao.findBy(physicalNetwork.getId(), TrafficType.Guest);
            if (vo == null) {
                continue;
            }
            String vmwareNetworkLabel = vo.getVmwareNetworkLabel();
            if (!vmwareNetworkLabel.startsWith(tag)) {
                throw new CloudRuntimeException("Vmware network label does not start with: " + tag);
            }
            return physicalNetwork.getId();
        }
        throw new CloudRuntimeException("Could not find guest physical network matching tag: " + tag + " on zone " + zoneId);
    }

    /**
     * Create and persist network
     */
    private NetworkVO createNetworkRecord(Long zoneId, String tag, String vlan, long accountId, long domainId) {
        Long physicalNetworkId = getPhysicalNetworkId(zoneId, tag);
        final long id = networkDao.getNextInSequence(Long.class, "id");
        NetworkVO networkVO = new NetworkVO(id, TrafficType.Guest, Networks.Mode.Dhcp, BroadcastDomainType.Vlan, 9L, domainId, accountId, id, "Imported-network-" + id,
                "Imported-network-" + id, null, Network.GuestType.Isolated, zoneId, physicalNetworkId, ControlledEntity.ACLType.Account, false, null, false);
        networkVO.setBroadcastUri(BroadcastDomainType.Vlan.toUri(vlan));
        networkVO.setGuruName("ExternalGuestNetworkGuru");
        networkVO.setState(Network.State.Implemented);
        return networkDao.persist(networkVO);
    }

    /**
     * Get network from VM network name
     */
    private NetworkVO getGuestNetworkFromNetworkMorName(String name, long accountId, Long zoneId, long domainId) {
        String prefix = "cloud.guest.";
        String nameWithoutPrefix = name.replace(prefix, "");
        String[] parts = nameWithoutPrefix.split("\\.");
        String vlan = parts[0];
        String tag = parts[parts.length - 1];
        String[] tagSplit = tag.split("-");
        tag = tagSplit[tagSplit.length - 1];

        s_logger.debug(String.format("Trying to find network with vlan: [%s].", vlan));
        NetworkVO networkVO = networkDao.findByVlan(vlan);
        if (networkVO == null) {
            networkVO = createNetworkRecord(zoneId, tag, vlan, accountId, domainId);
            s_logger.debug(String.format("Created new network record [id: %s] with details [zoneId: %s, tag: %s, vlan: %s, accountId: %s and domainId: %s].",
                    networkVO.getUuid(), zoneId, tag, vlan, accountId, domainId));
        }
        return networkVO;
    }

    /**
     * Get map between VM networks and its IDs on CloudStack
     */
    private Map<String, NetworkVO> getNetworksMapping(String[] vmNetworkNames, long accountId, long zoneId, long domainId) {
        Map<String, NetworkVO> mapping = new HashMap<>();
        for (String networkName : vmNetworkNames) {
            NetworkVO networkVO = getGuestNetworkFromNetworkMorName(networkName, accountId, zoneId, domainId);
            s_logger.debug(String.format("Mapping network name [%s] to networkVO [id: %s].", networkName, networkVO.getUuid()));
            mapping.put(networkName, networkVO);
        }
        return mapping;
    }

    /**
     * Get network MO from VM NIC
     */
    private NetworkMO getNetworkMO(VirtualEthernetCard nic, VmwareContext context) {
        VirtualDeviceConnectInfo connectable = nic.getConnectable();
        VirtualEthernetCardNetworkBackingInfo info = (VirtualEthernetCardNetworkBackingInfo)nic.getBacking();
        ManagedObjectReference networkMor = info.getNetwork();
        if (networkMor == null) {
            throw new CloudRuntimeException("Could not find network for NIC on: " + nic.getMacAddress());
        }
        return new NetworkMO(context, networkMor);
    }

    private Pair<String, String> getNicMacAddressAndNetworkName(VirtualDevice nicDevice, VmwareContext context) throws Exception {
        VirtualEthernetCard nic = (VirtualEthernetCard)nicDevice;
        String macAddress = nic.getMacAddress();
        NetworkMO networkMO = getNetworkMO(nic, context);
        String networkName = networkMO.getName();
        return new Pair<>(macAddress, networkName);
    }

    private void syncVMNics(VirtualDevice[] nicDevices, DatacenterMO dcMo, Map<String, NetworkVO> networksMapping, VMInstanceVO vm) throws Exception {
        VmwareContext context = dcMo.getContext();
        List<NicVO> allNics = nicDao.listByVmId(vm.getId());
        for (VirtualDevice nicDevice : nicDevices) {
            Pair<String, String> pair = getNicMacAddressAndNetworkName(nicDevice, context);
            String macAddress = pair.first();
            String networkName = pair.second();
            NetworkVO networkVO = networksMapping.get(networkName);
            NicVO nicVO = nicDao.findByNetworkIdAndMacAddressIncludingRemoved(networkVO.getId(), macAddress);
            if (nicVO != null) {
                s_logger.warn(String.format("Find NIC in DB with networkId [%s] and MAC Address [%s], so this NIC will be removed from list of unmapped NICs of VM [id: %s, name: %s].",
                        networkVO.getId(), macAddress, vm.getUuid(), vm.getInstanceName()));
                allNics.remove(nicVO);

                if (nicVO.getRemoved() != null) {
                    nicDao.unremove(nicVO.getId());
                }
            }
        }
        for (final NicVO unMappedNic : allNics) {
            s_logger.debug(String.format("Removing NIC [%s] from backup restored %s.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(unMappedNic, "uuid", "macAddress"), vm));
            vmManager.removeNicFromVm(vm, unMappedNic);
        }
    }

    private Map<VirtualDisk, VolumeVO> getDisksMapping(Backup backup, List<VirtualDisk> virtualDisks) {
        final VMInstanceVO vm = virtualMachineDao.findByIdIncludingRemoved(backup.getVmId());
        if (vm == null) {
            throw new CloudRuntimeException("Failed to find the volumes details from the VM backup");
        }

        List<Backup.VolumeInfo> backedUpVolumes = vm.getBackupVolumeList();
        Map<String, Boolean> usedVols = new HashMap<>();
        Map<VirtualDisk, VolumeVO> map = new HashMap<>();

        for (Backup.VolumeInfo backedUpVol : backedUpVolumes) {
            VolumeVO volumeExtra = _volumeDao.findByUuid(backedUpVol.getUuid());
            if (volumeExtra != null) {
                s_logger.debug(String.format("Marking volume [id: %s] of VM [%s] as removed for the backup process.", backedUpVol.getUuid(), ReflectionToStringBuilderUtils.reflectOnlySelectedFields(vm, "uuid", "instanceName")));
                _volumeDao.remove(volumeExtra.getId());

                if (vm.getType() == Type.User) {
                    UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_DETACH, volumeExtra.getAccountId(), volumeExtra.getDataCenterId(), volumeExtra.getId(),
                            volumeExtra.getName(), volumeExtra.getDiskOfferingId(), null, volumeExtra.getSize(), Volume.class.getName(),
                            volumeExtra.getUuid(), volumeExtra.isDisplayVolume());
                    _resourceLimitService.decrementResourceCount(vm.getAccountId(), Resource.ResourceType.volume, volumeExtra.isDisplayVolume());
                    _resourceLimitService.decrementResourceCount(vm.getAccountId(), Resource.ResourceType.primary_storage, volumeExtra.isDisplayVolume(), volumeExtra.getSize());
                }
            }
            for (VirtualDisk disk : virtualDisks) {
                if (!map.containsKey(disk) && backedUpVol.getSize().equals(disk.getCapacityInBytes()) && !usedVols.containsKey(backedUpVol.getUuid())) {
                    String volId = backedUpVol.getUuid();
                    VolumeVO vol = _volumeDao.findByUuidIncludingRemoved(volId);
                    usedVols.put(backedUpVol.getUuid(), true);
                    map.put(disk, vol);
                    s_logger.debug("VM restore mapping for disk " + disk.getBacking() + " (capacity: " + toHumanReadableSize(disk.getCapacityInBytes()) + ") with volume ID" + vol.getId());
                }
            }
        }
        return map;
    }

    /**
     * Find VM on datacenter
     */
    private VirtualMachineMO findVM(DatacenterMO dcMo, String path) throws Exception {
        VirtualMachineMO vm = dcMo.findVm(path);
        if (vm == null) {
            throw new CloudRuntimeException("Error finding VM: " + path);
        }
        return vm;
    }

    /**
     * Find restored volume based on volume info
     */
    private VirtualDisk findRestoredVolume(Backup.VolumeInfo volumeInfo, VirtualMachineMO vm) throws Exception {
        List<VirtualDisk> virtualDisks = vm.getVirtualDisks();
        for (VirtualDisk disk : virtualDisks) {
            if (disk.getCapacityInBytes().equals(volumeInfo.getSize())) {
                return disk;
            }
        }
        throw new CloudRuntimeException("Volume to restore could not be found");
    }

    /**
     * Get volume full path
     */
    private String getVolumeFullPath(VirtualDisk disk) {
        VirtualDeviceBackingInfo backing = disk.getBacking();
        checkBackingInfo(backing);
        VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo)backing;
        return info.getFileName();
    }

    /**
     * Get dest volume full path
     */
    private String getDestVolumeFullPath(VirtualMachineMO vmMo) throws Exception {
        VirtualDisk vmDisk = vmMo.getVirtualDisks().get(0);
        String vmDiskPath = vmMo.getVmdkFileBaseName(vmDisk);
        String vmDiskFullPath = getVolumeFullPath(vmMo.getVirtualDisks().get(0));
        String uuid = UUID.randomUUID().toString().replace("-", "");
        return vmDiskFullPath.replace(vmDiskPath, uuid);
    }

    /**
     * Get dest datastore mor
     */
    private ManagedObjectReference getDestStoreMor(VirtualMachineMO vmMo) throws Exception {
        VirtualDisk vmDisk = vmMo.getVirtualDisks().get(0);
        VirtualDeviceBackingInfo backing = vmDisk.getBacking();
        checkBackingInfo(backing);
        VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo)backing;
        return info.getDatastore();
    }

    @Override
    public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, String vmInternalName, Backup backup) throws Exception {
        s_logger.debug(String.format("Trying to import VM [vmInternalName: %s] from Backup [%s].", vmInternalName,
                ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "backupType")));
        DatacenterMO dcMo = getDatacenterMO(zoneId);
        VirtualMachineMO vmToImport = dcMo.findVm(vmInternalName);
        if (vmToImport == null) {
            throw new CloudRuntimeException("Error finding VM: " + vmInternalName);
        }
        VirtualMachineConfigSummary configSummary = vmToImport.getConfigSummary();
        VirtualMachineRuntimeInfo runtimeInfo = vmToImport.getRuntimeInfo();
        List<VirtualDisk> virtualDisks = vmToImport.getVirtualDisks();
        String[] vmNetworkNames = vmToImport.getNetworks();
        VirtualDevice[] nicDevices = vmToImport.getNicDevices();

        Map<VirtualDisk, VolumeVO> disksMapping = getDisksMapping(backup, virtualDisks);
        Map<String, NetworkVO> networksMapping = getNetworksMapping(vmNetworkNames, accountId, zoneId, domainId);

        long guestOsId = getImportingVMGuestOs(configSummary);
        long serviceOfferingId = getImportingVMServiceOffering(configSummary, runtimeInfo);
        long templateId = getImportingVMTemplate(virtualDisks, dcMo, vmInternalName, guestOsId, accountId, disksMapping, backup);

        VMInstanceVO vm = getVM(vmInternalName, templateId, guestOsId, serviceOfferingId, zoneId, accountId, userId, domainId);
        syncVMVolumes(vm, virtualDisks, disksMapping, vmToImport, backup);
        syncVMNics(nicDevices, dcMo, networksMapping, vm);

        return vm;
    }

    @Override
    public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, VirtualMachine vm, long poolId, Backup backup)
            throws Exception {
        DatacenterMO dcMo = getDatacenterMO(zoneId);
        VirtualMachineMO vmRestored = findVM(dcMo, location);
        VirtualMachineMO vmMo = findVM(dcMo, vm.getInstanceName());
        VirtualDisk restoredDisk = findRestoredVolume(volumeInfo, vmRestored);
        String diskPath = vmRestored.getVmdkFileBaseName(restoredDisk);

        s_logger.debug("Restored disk size=" + toHumanReadableSize(restoredDisk.getCapacityInKB() * Resource.ResourceType.bytesToKiB) + " path=" + diskPath);

        // Detach restored VM disks
        vmRestored.detachDisk(String.format("%s/%s.vmdk", location, diskPath), false);

        String srcPath = getVolumeFullPath(restoredDisk);
        String destPath = getDestVolumeFullPath(vmMo);

        VirtualDiskManagerMO virtualDiskManagerMO = new VirtualDiskManagerMO(dcMo.getContext());

        // Copy volume to the VM folder
        s_logger.debug(String.format("Moving volume from %s to %s", srcPath, destPath));
        virtualDiskManagerMO.moveVirtualDisk(srcPath, dcMo.getMor(), destPath, dcMo.getMor(), true);

        try {
            // Attach volume to VM
            vmMo.attachDisk(new String[] {destPath}, getDestStoreMor(vmMo));
        } catch (Exception e) {
            s_logger.error("Failed to attach the restored volume: " + diskPath, e);
            return false;
        } finally {
            // Destroy restored VM
            vmRestored.destroy();
        }

        s_logger.debug(String.format("Attaching disk %s to vm %s", destPath, vm.getId()));
        VirtualDisk attachedDisk = getAttachedDisk(vmMo, destPath);
        if (attachedDisk == null) {
            s_logger.error("Failed to get the attached the (restored) volume " + destPath);
            return false;
        }
        s_logger.debug(String.format("Creating volume entry for disk %s attached to vm %s", destPath, vm.getId()));
        createVolume(attachedDisk, vmMo, vm.getDomainId(), vm.getDataCenterId(), vm.getAccountId(), vm.getId(), poolId, vm.getTemplateId(), backup, false);

        if (vm.getBackupOfferingId() == null) {
            return true;
        }
        VMInstanceVO vmVO = (VMInstanceVO)vm;
        vmVO.setBackupVolumes(createVolumeInfoFromVolumes(_volumeDao.findByInstance(vm.getId())));
        vmDao.update(vmVO.getId(), vmVO);
        return true;
    }

    private VirtualDisk getAttachedDisk(VirtualMachineMO vmMo, String diskFullPath) throws Exception {
        for (VirtualDisk disk : vmMo.getVirtualDisks()) {
            if (getVolumeFullPath(disk).equals(diskFullPath)) {
                return disk;
            }
        }
        return null;
    }

    private boolean isInterClusterMigration(Long srcClusterId, Long destClusterId) {
        return srcClusterId != null && destClusterId != null && ! srcClusterId.equals(destClusterId);
    }

    private String getHostGuidForLocalStorage(StoragePool pool) {
        List<StoragePoolHostVO> storagePoolHostVOs = storagePoolHostDao.listByPoolId(pool.getId());
        StoragePoolHostVO storagePoolHostVO = storagePoolHostVOs.get(0);
        HostVO hostVO = hostDao.findById(storagePoolHostVO.getHostId());
        return hostVO.getGuid();
    }

    private String getHostGuidInTargetCluster(boolean isInterClusterMigration, Long destClusterId) {
        String hostGuidInTargetCluster = null;
        if (isInterClusterMigration) {
            Host hostInTargetCluster = null;
            // Without host vMotion might fail between non-shared storages with error similar to,
            // https://kb.vmware.com/s/article/1003795
            // As this is offline migration VM won't be started on this host
            List<HostVO> hosts = hostDao.findHypervisorHostInCluster(destClusterId);
            if (CollectionUtils.isNotEmpty(hosts)) {
                hostInTargetCluster = hosts.get(0);
            }
            if (hostInTargetCluster == null) {
                throw new CloudRuntimeException("Migration failed, unable to find suitable target host for VM placement while migrating between storage pools of different clusters without shared storages");
            }
            hostGuidInTargetCluster = hostInTargetCluster.getGuid();
        }
        return hostGuidInTargetCluster;
    }

    @Override
    public List<Command> finalizeMigrate(VirtualMachine vm, Map<Volume, StoragePool> volumeToPool) {
        List<Command> commands = new ArrayList<>();

        // OfflineVmwareMigration: specialised migration command
        List<Pair<VolumeTO, StorageFilerTO>> volumeToFilerTo = new ArrayList<>();
        Long poolClusterId = null;
        StoragePool targetLocalPoolForVM = null;
        for (Map.Entry<Volume, StoragePool> entry : volumeToPool.entrySet()) {
            Volume volume = entry.getKey();
            StoragePool pool = entry.getValue();
            VolumeTO volumeTo = new VolumeTO(volume, _storagePoolDao.findById(pool.getId()));
            StorageFilerTO filerTo = new StorageFilerTO(pool);
            if (pool.getClusterId() != null) {
                poolClusterId = pool.getClusterId();
            }
            if (volume.getVolumeType().equals(Volume.Type.ROOT) && pool.isLocal()) {
                targetLocalPoolForVM = pool;
            }
            volumeToFilerTo.add(new Pair<>(volumeTo, filerTo));
        }
        final Long destClusterId = poolClusterId;
        final Long srcClusterId = vmManager.findClusterAndHostIdForVm(vm, true).first();
        final boolean isInterClusterMigration = isInterClusterMigration(destClusterId, srcClusterId);
        String targetHostGuid = getTargetHostGuid(targetLocalPoolForVM, destClusterId, isInterClusterMigration);

        MigrateVmToPoolCommand migrateVmToPoolCommand = new MigrateVmToPoolCommand(vm.getInstanceName(),
                volumeToFilerTo, targetHostGuid, true);
        commands.add(migrateVmToPoolCommand);

        // OfflineVmwareMigration: cleanup if needed
        if (isInterClusterMigration) {
            final String srcDcName = _clusterDetailsDao.getVmwareDcName(srcClusterId);
            final String destDcName = _clusterDetailsDao.getVmwareDcName(destClusterId);
            if (srcDcName != null && destDcName != null && !srcDcName.equals(destDcName)) {
                final UnregisterVMCommand unregisterVMCommand = new UnregisterVMCommand(vm.getInstanceName(), true);
                unregisterVMCommand.setCleanupVmFiles(true);

                commands.add(unregisterVMCommand);
            }
        }
        return commands;
    }

    private String getTargetHostGuid(StoragePool targetLocalPoolForVM, Long destClusterId, boolean isInterClusterMigration) {
        String targetHostGuid = null;
        if (targetLocalPoolForVM != null) {
            // Get the target host for local storage migration
            targetHostGuid = getHostGuidForLocalStorage(targetLocalPoolForVM);
        } else {
            targetHostGuid = getHostGuidInTargetCluster(isInterClusterMigration, destClusterId);
        }
        return targetHostGuid;
    }

    @Override
    protected VirtualMachineTO toVirtualMachineTO(VirtualMachineProfile vmProfile) {
        return super.toVirtualMachineTO(vmProfile);
    }
}
