/** * Abiquo community edition * cloud management application for hybrid clouds * Copyright (C) 2008-2010 - Abiquo Holdings S.L. * * This application is free software; you can redistribute it and/or * modify it under the terms of the GNU LESSER GENERAL PUBLIC * LICENSE as published by the Free Software Foundation under * version 3 of the License * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * LESSER GENERAL PUBLIC LICENSE v.3 for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ package com.abiquo.scheduler.workload; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import javax.jms.ResourceAllocationException; import javax.persistence.EntityManager; import javax.persistence.PersistenceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.abiquo.model.enumerator.FitPolicy; import com.abiquo.scheduler.fit.AllocationFitMax; import com.abiquo.scheduler.fit.AllocationFitMin; import com.abiquo.scheduler.fit.IAllocationFit; import com.abiquo.server.core.appslibrary.VirtualMachineTemplate; import com.abiquo.server.core.cloud.VirtualAppliance; import com.abiquo.server.core.cloud.VirtualApplianceDAO; import com.abiquo.server.core.cloud.VirtualDatacenter; import com.abiquo.server.core.enterprise.Enterprise; import com.abiquo.server.core.infrastructure.Datastore; import com.abiquo.server.core.infrastructure.InfrastructureRep; import com.abiquo.server.core.infrastructure.Machine; import com.abiquo.server.core.infrastructure.Rack; import com.abiquo.server.core.infrastructure.network.NetworkAssignment; import com.abiquo.server.core.infrastructure.network.NetworkAssignmentDAO; import com.abiquo.server.core.scheduler.MachineLoadRule; import com.abiquo.server.core.scheduler.VirtualMachineRequirements; import com.abiquo.tracer.ComponentType; import com.abiquo.tracer.EventType; import com.abiquo.tracer.SeverityType; import com.abiquo.tracer.client.TracerFactory; /** * Provides a way to find out which is the best target to which to assign a resource. * <p> * This component provides a simple algorith to choose a target: * <ul> * <li><em>First pass</em>: get a consistent list of candidate targets. * <p> * This list must contain only potentially valid candidates, and must leave none of the potential * valid candidates out. But it can provide candidates that might be valid but will be pruned by the * second pass. * <p> * Typically, this first pass is going to be implemented via JPA-QL queries, to minimize the number * of loaded objects. * <p> * Implemented via <em>findFirstPassCandidates</em>. * <li><em>Second pass</em>: selects just one of the candidates obtained during the firt pass, by * applying several rules. * <p> * Why this second pass? While the ideal would be to get this done in the first pass, in reality you * have scenarios in which you might want to do a first cut via SQL, and then perform very * complicated checks that can be hellish in SQL via Java code. * <p> * Implemented via <em>findSecondPassCandidates</em>.</li> * </ul> * <p> * We can have different policies to choose one among a list of candidates: these policies are * defined via FitPolicy. * * @author pedro.agullo * @param RESOURCE: the resource to be assigned to a target (for example, a VirtualMachineTemplate) * @param TARGET: the place where we want to assign a resource (for example, a Physicalmachine) * @param CONTEXT_DATA: optative additional data that might be convenient or needed to perform * processing. */ /** * Notes for the current (single) implementation {@link VirtualimageAllocationService} * <p> * This class has implemented rule checking using some rules in the second pass (MachineLoadRuleHB), * and others in the fist pass (EnterpriseExclusionRuleHB). * <p> * Why is this so? In practice, we find that some rules are almost self contained, and hence will * not force unexpected load of objects (i.e., many hidden queries executed under the covers): these * are good candidates for being implemented as Rule<...>, as this provides us with the full power * of Java to implement complex logic. * <p> * Other rules can involve many entities, and it will be a good idea to make them part of the first * pass, which cuts down the number of entities to deal with and can perform fetching of those * entities that will need to be traversed to check the rules -via the appropriate JPA-QL. * <p> * In the future, it might make sense to create rules that exhibit both behaviors, making them * candidates to implement Rule<...> as well as participate in the first pass via some query or just * some JPA-QL fragment to be added to other queries in the first pass. * <p> * WARNING: while this sounds good and nice, some serious analysis of the generated SQL should be * performed once we get the final implementation, to minimize the number of queries and maximize * their speed. * <p> * NOTE: the current implementation only fetch once the physical machine rules. */ @Service public class VirtualimageAllocationService { private final static Logger log = LoggerFactory.getLogger(VirtualimageAllocationService.class); @Autowired private InfrastructureRep datacenterRepo; @Autowired private VirtualApplianceDAO virtualApplianceDao; @Autowired private NetworkAssignmentDAO networkAssignmentDao; @Autowired private SecondPassRuleFinder<VirtualMachineTemplate, Machine, Integer> ruleFinder; public VirtualimageAllocationService() { } public VirtualimageAllocationService(final EntityManager em) { this.datacenterRepo = new InfrastructureRep(em); this.virtualApplianceDao = new VirtualApplianceDAO(em); this.networkAssignmentDao = new NetworkAssignmentDAO(em); this.ruleFinder = new PhysicalmachineRuleFinder(em); } /** Starting best fit. (none ''computeRank'' can be higher) */ private static final long NO_FIT = Long.MIN_VALUE; /** * Finds the targets that best fits a given resource. If there is no target that can accept the * resource, then null will be returned. * * @throws ResourceAllocationException, it there isn't enough resources to fulfilling the * target. */ @Transactional(readOnly = true, propagation = Propagation.REQUIRED) public Machine findBestTarget(final VirtualMachineRequirements requirements, final FitPolicy fitPolicy, final VirtualAppliance virtualAppliance) { final List<Integer> rackCandidates = getCandidateRacks(virtualAppliance); if (rackCandidates.isEmpty()) { final String msg = "Any rack can be selected: all exceed the max VLAN allowed."; throw new NotEnoughResourcesException(msg); } StringBuilder sbErrorRacks = new StringBuilder("Caused by:"); for (Integer idRack : rackCandidates) { final Rack rack = datacenterRepo.findRackById(idRack); try { final Collection<Machine> firstPassCandidates = findFirstPassCandidates(requirements, virtualAppliance, rack); log.debug(String.format( "All the virtual machines of the current virtual datacenter " + "will be deployed on the rack id : %d", idRack)); return findSecondPassCandidates(firstPassCandidates, requirements, virtualAppliance, fitPolicy); } catch (Exception e) // NotEnoughResourcesException or PersistenceException { final String error = String.format("Rack [%s] can't be used : %s", // rack.getName(), e.getMessage()); sbErrorRacks.append("\n").append(error); log.error(error); } }// racks final String msg = "Any rack can be selected: There is no physical machine capacity to instantiate the required virtual appliance." + sbErrorRacks.toString(); throw new NotEnoughResourcesException(msg); } /** * Finds the targets that best fits a given resource. If there is no target that can accept the * resource, then null will be returned. * * @param requirements, specify the hardware needs of the desired virtual machine * @param datastoreUuid, the selected machine should have this datastore enabled. * @param originalHypervisorId, the selected machine IS NOT this provided hypervisor. * @param rackId, the rack is already defined. * @throws ResourceAllocationException, it there isn't enough resources to fulfilling the * target. */ @Transactional(readOnly = true, propagation = Propagation.REQUIRED) public Machine findBestTarget(final VirtualMachineRequirements requirements, final FitPolicy fitPolicy, final VirtualAppliance vapp, final String datastoreUuid, final Integer originalHypervisorId, final Integer rackId) throws ResourceAllocationException { final Integer virtualDatacenterId = vapp.getVirtualDatacenter().getId(); final Collection<Machine> firstPassCandidates = datacenterRepo.findCandidateMachines(rackId, virtualDatacenterId, vapp.getEnterprise(), datastoreUuid, originalHypervisorId); return findSecondPassCandidates(firstPassCandidates, requirements, vapp, fitPolicy); } /** * Return a sorted list of racks (sorted by rack goodness based on network params). If some * network assigment on the datacenter then the rack is already defined. */ protected List<Integer> getCandidateRacks(final VirtualAppliance vapp) { final VirtualDatacenter virtualDatacenter = vapp.getVirtualDatacenter(); // Gets the network assignment to the virtualmachine final List<NetworkAssignment> networksAssignedList = networkAssignmentDao.findByVirtualDatacenter(virtualDatacenter); if (networksAssignedList.isEmpty()) { log.debug("First virtual machine of the current virtual appliance " + "(no rack assigned to the network attached). " + "Selecting the rack to be used on the hole virtual appliance."); // Gets the rack candidates that would fit a new virtual datacenter return datacenterRepo .getRackIdByMinVLANCount(virtualDatacenter.getDatacenter().getId()); } else // the rack is already selected { // As all the networks of the same virtualdata center are assigned to the same rack, we // get the first rack - vlan assignment final NetworkAssignment na = networksAssignedList.get(0); final Integer idRack = na.getRack().getId(); return Collections.singletonList(idRack); } } protected Collection<Machine> findFirstPassCandidates( final VirtualMachineRequirements requirements, final VirtualAppliance vapp, final Rack rack) throws NotEnoughResourcesException { Collection<Machine> candidateMachines; final Enterprise enterprise = vapp.getEnterprise(); final VirtualDatacenter virtualDatacenter = vapp.getVirtualDatacenter(); final Integer idRack = rack.getId(); final Long numberOfDeployedVLAN = datacenterRepo.getNumberOfDeployedVlanNetworksByRack(idRack); final Integer vlanPerSwitch = rack.getVlanIdMax() - rack.getVlanIdMin() + 1; log.debug("The number of deployed VLAN for the rack: {}, is: {}", idRack, numberOfDeployedVLAN); final int second_operator = Math.round(vlanPerSwitch * rack.getNrsq() / 100); final int vlan_soft_limit = vlanPerSwitch - second_operator; if (numberOfDeployedVLAN.intValue() >= vlan_soft_limit) { String warning = "The number of deployed VLAN has exceeded the networking resource security quotient"; log.warn(warning); TracerFactory.getTracer().log(SeverityType.WARNING, ComponentType.NETWORK, EventType.RACK_VLAN_POOL, warning); } if (numberOfDeployedVLAN.compareTo(new Long(vlanPerSwitch)) >= 0) { throw new NotEnoughResourcesException(String.format( "Not enough VLAN resource on rack [%s] to instantiate the required virtual appliance.", rack.getName())); } // log.debug("The network assigned to the VM, VLAN network ID: {}, " // + "has already been assigned to rack : {}.", na.getVlanNetwork().getId(), idRack); try { candidateMachines = datacenterRepo.findCandidateMachines(idRack, virtualDatacenter.getId(), requirements.getHd(), enterprise); } catch (PersistenceException e) { throw new NotEnoughResourcesException(e.getMessage()); } return candidateMachines; } /** * Default rule check the actual utilization (load factor = 100%) for CPU, RAM and HD. */ class DefaultLoadRule extends MachineLoadRule { @Override public boolean pass(final VirtualMachineRequirements requirements, final Machine machine, final Integer contextData) { final boolean passCPU = pass(Long.valueOf(machine.getVirtualCpusUsed()), requirements.getCpu(), Long.valueOf(machine.getVirtualCpuCores()), 100); final boolean passRAM = pass(Long.valueOf(machine.getVirtualRamUsedInMb()), requirements.getRam(), Long.valueOf(machine.getVirtualRamInMb()), 100); // BYTE to MB Long templateRequiredMb = requirements.getHd() / (1024 * 1024); Long machineAllowedMb = 0L; Long machineUsedMb = 0L; if (machine.getDatastores() != null && !machine.getDatastores().isEmpty()) { for (Datastore d : machine.getDatastores()) { machineAllowedMb += d.getSize() / (1024 * 1024); machineUsedMb += d.getUsedSize() / (1024 * 1024); } } // machine.getVirtualHardDiskInBytes() / (1024 * 1024); // machine.getVirtualHardDiskUsedInBytes() / (1024 * 1024); final boolean passHD = pass(machineUsedMb, templateRequiredMb, machineAllowedMb, 100); return passCPU && passRAM && passHD; } } private final MachineLoadRule DEFAULT_RULE = new DefaultLoadRule(); /** * TODO TBD * * @param requirements, specify the hardware needs of the desired virtual machine * @throws ResourceAllocationException, it there isn't enough resources to fulfilling the * target. */ protected final Machine findSecondPassCandidates(final Collection<Machine> firstPassCandidates, final VirtualMachineRequirements requirements, final VirtualAppliance virtualAppliance, final FitPolicy fitPolicy) throws NotEnoughResourcesException { IAllocationFit physicalMachineFit; // get all the rules of the candiate machines Map<Machine, List<MachineLoadRule>> machineRulesMap = ruleFinder.initializeMachineLoadRuleCache(firstPassCandidates); physicalMachineFit = fitPolicy == FitPolicy.PROGRESSIVE ? new AllocationFitMax() : new AllocationFitMin(); Machine bestTarget = null; long bestFitTarget = NO_FIT; for (final Machine target : firstPassCandidates) { boolean pass = true; if (machineRulesMap != null) // community impl --> rules == null (so always pass) { List<MachineLoadRule> rules = machineRulesMap.get(target); if (rules == null || rules.isEmpty()) { // XXX unused vappid pass = DEFAULT_RULE.pass(requirements, target, virtualAppliance.getId()); } else { for (final MachineLoadRule rule : rules) { // XXX unused vappid if (!rule.pass(requirements, target, virtualAppliance.getId())) { pass = false; break; } } } } else // default rule is to check the actual resource utilization (load = 100%) { // XXX unused vappid pass = DEFAULT_RULE.pass(requirements, target, virtualAppliance.getId()); } if (pass) { final long fitTarget = physicalMachineFit.computeRanking(target); if (isGoodEnough(fitTarget)) { return target; } if (fitTarget > bestFitTarget) { bestFitTarget = fitTarget; bestTarget = target; } } else { log.error(String.format("Machine %s rejected by some load rule.", target.getName())); } } if (bestTarget == null) { final String cause = String.format("There are %d candidate machines but all are discarded by the " + "current workload rules (RAM and CPU oversubscription " + "or suitable Datastore with enought free size).\n" + "Please check the workload rules or the physical machine resources " + "available on the datacenter from the infrastructure view.\n" + "Virtual machine requires %d Cpu -- %d Ram \n" + "Candidate machines : %s", firstPassCandidates.size(), requirements.getCpu(), requirements.getRam(), candidateNames(firstPassCandidates)); throw new NotEnoughResourcesException(cause); } return bestTarget; } private String candidateNames(final Collection<Machine> firstPassCandidates) { StringBuilder sb = new StringBuilder(); for (Machine candidate : firstPassCandidates) { sb.append(candidate.getName()); sb.append(" ip - "); sb.append(candidate.getHypervisor().getIp()); sb.append("\n"); } return sb.toString(); } protected boolean isGoodEnough(final long fitTarget) { // TODO never is good enough return false; } /** * When editing a virtual machine this method checks if the increases resources (setted at * vmtemplate) are allowed by the workload rules. */ public boolean checkVirtualMachineResourceIncrease(final Machine machine, final VirtualMachineRequirements increaseRequirements, final Integer virtualApplianceId) { // get all the rules of the candiate machines Map<Machine, List<MachineLoadRule>> machineRulesMap = ruleFinder.initializeMachineLoadRuleCache(Collections.singletonList(machine)); boolean pass = true; if (machineRulesMap != null) // community impl --> rules == null (so always pass) { List<MachineLoadRule> rules = machineRulesMap.get(machine); if (rules == null || rules.isEmpty()) { pass = DEFAULT_RULE.pass(increaseRequirements, machine, virtualApplianceId); } else { for (final MachineLoadRule rule : rules) { if (!rule.pass(increaseRequirements, machine, virtualApplianceId)) { pass = false; break; } } } } else // default rule is to check the actual resource utilization (load = 100%) { pass = DEFAULT_RULE.pass(increaseRequirements, machine, virtualApplianceId); } return pass; } /** * Return all machines in a rack that are empty of VM. * * @param rackId rack. * @return Integer */ public Integer getEmptyOffMachines(final Integer rackId) { return datacenterRepo.getEmptyOffMachines(rackId); } /** * Return all machines in a rack that are empty of VM. * * @param rackId rack. * @return Integer */ public Integer getEmptyOnMachines(final Integer rackId) { return datacenterRepo.getEmptyOnMachines(rackId); } /** * Return all machines in a rack that are empty of VM. * * @param rackId rack. * @return Integer */ public List<Machine> getAllEmptyOnMachines(final Integer rackId) { return datacenterRepo.getAllMachinesToShutDownFromRack(rackId); } /** * Returns any machine that is in the rack in HALTED_FOR_SAVE. * * @param rackId rack. * @return Machine */ public List<Machine> getRandomMachinesToStartFromRack(final Integer rackId, final Integer howMany) { return datacenterRepo.getRandomMachinesToStartFromRack(rackId, howMany); } /** * Returns any machine that is in the rack in MANAGED. * * @param rackId rack. * @return Machine */ public List<Machine> getRandomMachinesToShutDownFromRack(final Integer rackId, final Integer howMany) { return datacenterRepo.getRandomMachinesToShutDownFromRack(rackId, howMany); } }