// 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.network.lb; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.naming.ConfigurationException; import org.apache.cloudstack.api.command.user.loadbalancer.CreateLoadBalancerRuleCmd; import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; import com.cloud.agent.api.check.CheckSshAnswer; import com.cloud.agent.api.check.CheckSshCommand; import com.cloud.agent.api.routing.LoadBalancerConfigCommand; import com.cloud.agent.api.routing.NetworkElementCommand; import com.cloud.agent.api.to.LoadBalancerTO; import com.cloud.agent.manager.Commands; import com.cloud.configuration.Config; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenter.NetworkType; import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.DataCenterDao; import com.cloud.dc.dao.HostPodDao; import com.cloud.deploy.DeployDestination; import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientAddressCapacityException; import com.cloud.exception.NetworkRuleConflictException; import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.ElasticLbVmMapVO; import com.cloud.network.Network; import com.cloud.network.NetworkModel; import com.cloud.network.Networks.TrafficType; import com.cloud.network.dao.LoadBalancerDao; import com.cloud.network.dao.LoadBalancerVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; import com.cloud.network.lb.LoadBalancingRule.LbDestination; import com.cloud.network.lb.LoadBalancingRule.LbHealthCheckPolicy; import com.cloud.network.lb.LoadBalancingRule.LbSslCert; import com.cloud.network.lb.LoadBalancingRule.LbStickinessPolicy; import com.cloud.network.lb.dao.ElasticLbVmMapDao; import com.cloud.network.router.VirtualRouter; import com.cloud.network.rules.FirewallRule; import com.cloud.network.rules.LoadBalancer; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.Storage; import com.cloud.user.Account; import com.cloud.user.AccountService; import com.cloud.utils.NumbersUtil; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.Ip; import com.cloud.vm.DomainRouterVO; import com.cloud.vm.NicProfile; import com.cloud.vm.ReservationContext; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.VirtualMachineGuru; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; @Component public class ElasticLoadBalancerManagerImpl extends ManagerBase implements ElasticLoadBalancerManager, VirtualMachineGuru { private static final Logger s_logger = Logger.getLogger(ElasticLoadBalancerManagerImpl.class); @Inject private AgentManager _agentMgr; @Inject private NetworkModel _networkModel; @Inject private LoadBalancingRulesManager _lbMgr; @Inject private final DomainRouterDao _routerDao = null; @Inject protected HostPodDao _podDao = null; @Inject protected ClusterDao _clusterDao; @Inject private final DataCenterDao _dcDao = null; @Inject protected NetworkDao _networkDao; @Inject protected NetworkOfferingDao _networkOfferingDao; @Inject private VirtualMachineManager _itMgr; @Inject private ConfigurationDao _configDao; @Inject private final ServiceOfferingDao _serviceOfferingDao = null; @Inject private AccountService _accountService; @Inject private LoadBalancerDao _lbDao; @Inject private ElasticLbVmMapDao _elbVmMapDao; @Inject private NicDao _nicDao; String _instance; static final private String SystemVmType = "elbvm"; boolean _enabled; TrafficType _frontendTrafficType = TrafficType.Guest; Account _systemAcct; ScheduledExecutorService _gcThreadPool; String _mgmtCidr; Set<Long> _gcCandidateElbVmIds = Collections.newSetFromMap(new ConcurrentHashMap<Long, Boolean>()); int _elasticLbVmRamSize; int _elasticLbvmCpuMHz; int _elasticLbvmNumCpu; private LoadBalanceRuleHandler loadBalanceRuleHandler; private boolean sendCommandsToRouter(final DomainRouterVO elbVm, Commands cmds) throws AgentUnavailableException { Answer[] answers = null; try { answers = _agentMgr.send(elbVm.getHostId(), cmds); } catch (OperationTimedoutException e) { s_logger.warn("ELB: Timed Out", e); throw new AgentUnavailableException("Unable to send commands to virtual elbVm ", elbVm.getHostId(), e); } if (answers == null) { return false; } if (answers.length != cmds.size()) { return false; } // FIXME: Have to return state for individual command in the future if (answers.length > 0) { Answer ans = answers[0]; return ans.getResult(); } return true; } private void createApplyLoadBalancingRulesCommands(List<LoadBalancingRule> rules, DomainRouterVO elbVm, Commands cmds, long guestNetworkId) { /* XXX: cert */ LoadBalancerTO[] lbs = new LoadBalancerTO[rules.size()]; int i = 0; for (LoadBalancingRule rule : rules) { boolean revoked = (rule.getState().equals(FirewallRule.State.Revoke)); String protocol = rule.getProtocol(); String algorithm = rule.getAlgorithm(); String elbIp = rule.getSourceIp().addr(); int srcPort = rule.getSourcePortStart(); String uuid = rule.getUuid(); List<LbDestination> destinations = rule.getDestinations(); LoadBalancerTO lb = new LoadBalancerTO(uuid, elbIp, srcPort, protocol, algorithm, revoked, false, false, destinations); lbs[i++] = lb; } NetworkOffering offering = _networkOfferingDao.findById(guestNetworkId); String maxconn = null; if (offering.getConcurrentConnections() == null) { maxconn = _configDao.getValue(Config.NetworkLBHaproxyMaxConn.key()); } else { maxconn = offering.getConcurrentConnections().toString(); } LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lbs, elbVm.getPublicIpAddress(), _nicDao.getIpAddress(guestNetworkId, elbVm.getId()), elbVm.getPrivateIpAddress(), null, null, maxconn, offering.isKeepAliveEnabled()); cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, elbVm.getPrivateIpAddress()); cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, elbVm.getInstanceName()); //FIXME: why are we setting attributes directly? Ick!! There should be accessors and //the constructor should set defaults. cmd.lbStatsVisibility = _configDao.getValue(Config.NetworkLBHaproxyStatsVisbility.key()); cmd.lbStatsUri = _configDao.getValue(Config.NetworkLBHaproxyStatsUri.key()); cmd.lbStatsAuth = _configDao.getValue(Config.NetworkLBHaproxyStatsAuth.key()); cmd.lbStatsPort = _configDao.getValue(Config.NetworkLBHaproxyStatsPort.key()); cmds.addCommand(cmd); } protected boolean applyLBRules(DomainRouterVO elbVm, List<LoadBalancingRule> rules, long guestNetworkId) throws ResourceUnavailableException { Commands cmds = new Commands(Command.OnError.Continue); createApplyLoadBalancingRulesCommands(rules, elbVm, cmds, guestNetworkId); // Send commands to elbVm return sendCommandsToRouter(elbVm, cmds); } protected DomainRouterVO findElbVmForLb(LoadBalancingRule lb) {//TODO: use a table to lookup Network ntwk = _networkModel.getNetwork(lb.getNetworkId()); long sourceIpId = _networkModel.getPublicIpAddress(lb.getSourceIp().addr(), ntwk.getDataCenterId()).getId(); ElasticLbVmMapVO map = _elbVmMapDao.findOneByIp(sourceIpId); if (map == null) { return null; } DomainRouterVO elbVm = _routerDao.findById(map.getElbVmId()); return elbVm; } @Override public boolean applyLoadBalancerRules(Network network, List<LoadBalancingRule> rules) throws ResourceUnavailableException { if (rules == null || rules.isEmpty()) { return true; } DomainRouterVO elbVm = findElbVmForLb(rules.get(0)); if (elbVm == null) { s_logger.warn("Unable to apply lb rules, ELB vm doesn't exist in the network " + network.getId()); throw new ResourceUnavailableException("Unable to apply lb rules", DataCenter.class, network.getDataCenterId()); } if (elbVm.getState() == State.Running) { //resend all rules for the public ip long sourceIpId = _networkModel.getPublicIpAddress(rules.get(0).getSourceIp().addr(), network.getDataCenterId()).getId(); List<LoadBalancerVO> lbs = _lbDao.listByIpAddress(sourceIpId); List<LoadBalancingRule> lbRules = new ArrayList<LoadBalancingRule>(); for (LoadBalancerVO lb : lbs) { List<LbDestination> dstList = _lbMgr.getExistingDestinations(lb.getId()); List<LbStickinessPolicy> policyList = _lbMgr.getStickinessPolicies(lb.getId()); List<LbHealthCheckPolicy> hcPolicyList = _lbMgr.getHealthCheckPolicies(lb.getId()); Ip sourceIp = _networkModel.getPublicIpAddress(lb.getSourceIpAddressId()).getAddress(); LbSslCert sslCert = _lbMgr.getLbSslCert(lb.getId()); LoadBalancingRule loadBalancing = new LoadBalancingRule(lb, dstList, policyList, hcPolicyList, sourceIp, sslCert, lb.getLbProtocol()); lbRules.add(loadBalancing); } return applyLBRules(elbVm, lbRules, network.getId()); } else if (elbVm.getState() == State.Stopped || elbVm.getState() == State.Stopping) { s_logger.debug("ELB VM is in " + elbVm.getState() + ", so not sending apply LoadBalancing rules commands to the backend"); return true; } else { s_logger.warn("Unable to apply loadbalancing rules, ELB VM is not in the right state " + elbVm.getState()); throw new ResourceUnavailableException("Unable to apply loadbalancing rules, ELB VM is not in the right state", VirtualRouter.class, elbVm.getId()); } } @Override public boolean configure(String name, Map<String, Object> params) throws ConfigurationException { final Map<String, String> configs = _configDao.getConfiguration("AgentManager", params); _systemAcct = _accountService.getSystemAccount(); _instance = configs.get("instance.name"); if (_instance == null) { _instance = "VM"; } _mgmtCidr = _configDao.getValue(Config.ManagementNetwork.key()); _elasticLbVmRamSize = NumbersUtil.parseInt(configs.get(Config.ElasticLoadBalancerVmMemory.key()), DEFAULT_ELB_VM_RAMSIZE); _elasticLbvmCpuMHz = NumbersUtil.parseInt(configs.get(Config.ElasticLoadBalancerVmCpuMhz.key()), DEFAULT_ELB_VM_CPU_MHZ); _elasticLbvmNumCpu = NumbersUtil.parseInt(configs.get(Config.ElasticLoadBalancerVmNumVcpu.key()), 1); List<ServiceOfferingVO> offerings = _serviceOfferingDao.createSystemServiceOfferings("System Offering For Elastic LB VM", ServiceOffering.elbVmDefaultOffUniqueName, _elasticLbvmNumCpu, _elasticLbVmRamSize, _elasticLbvmCpuMHz, 0, 0, true, null, Storage.ProvisioningType.THIN, true, null, true, VirtualMachine.Type.ElasticLoadBalancerVm, true); // this can sometimes happen, if DB is manually or programmatically manipulated if (offerings == null || offerings.size() < 2) { String msg = "Data integrity problem : System Offering For Elastic LB VM has been removed?"; s_logger.error(msg); throw new ConfigurationException(msg); } String enabled = _configDao.getValue(Config.ElasticLoadBalancerEnabled.key()); _enabled = (enabled == null) ? false : Boolean.parseBoolean(enabled); s_logger.info("Elastic Load balancer enabled: " + _enabled); if (_enabled) { String traffType = _configDao.getValue(Config.ElasticLoadBalancerNetwork.key()); if ("guest".equalsIgnoreCase(traffType)) { _frontendTrafficType = TrafficType.Guest; } else if ("public".equalsIgnoreCase(traffType)) { _frontendTrafficType = TrafficType.Public; } else throw new ConfigurationException("ELB: Traffic type for front end of load balancer has to be guest or public; found : " + traffType); s_logger.info("ELB: Elastic Load Balancer: will balance on " + traffType); int gcIntervalMinutes = NumbersUtil.parseInt(configs.get(Config.ElasticLoadBalancerVmGcInterval.key()), 5); if (gcIntervalMinutes < 5) gcIntervalMinutes = 5; s_logger.info("ELB: Elastic Load Balancer: scheduling GC to run every " + gcIntervalMinutes + " minutes"); _gcThreadPool = Executors.newScheduledThreadPool(1, new NamedThreadFactory("ELBVM-GC")); _gcThreadPool.scheduleAtFixedRate(new CleanupThread(), gcIntervalMinutes, gcIntervalMinutes, TimeUnit.MINUTES); _itMgr.registerGuru(VirtualMachine.Type.ElasticLoadBalancerVm, this); } loadBalanceRuleHandler = new LoadBalanceRuleHandler(_instance, _systemAcct); return true; } private DomainRouterVO stop(DomainRouterVO elbVm, boolean forced) throws ConcurrentOperationException, ResourceUnavailableException { s_logger.debug("Stopping ELB vm " + elbVm); try { _itMgr.advanceStop(elbVm.getUuid(), forced); return _routerDao.findById(elbVm.getId()); } catch (OperationTimedoutException e) { throw new CloudRuntimeException("Unable to stop " + elbVm, e); } } @Override public LoadBalancer handleCreateLoadBalancerRule(CreateLoadBalancerRuleCmd lb, Account account, long networkId) throws InsufficientAddressCapacityException, NetworkRuleConflictException { return loadBalanceRuleHandler.handleCreateLoadBalancerRule(lb, account, networkId); } void garbageCollectUnusedElbVms() { List<DomainRouterVO> unusedElbVms = _elbVmMapDao.listUnusedElbVms(); if (unusedElbVms != null) { if (unusedElbVms.size() > 0) { s_logger.info("Found " + unusedElbVms.size() + " unused ELB vms"); } Set<Long> currentGcCandidates = new HashSet<Long>(); for (DomainRouterVO elbVm : unusedElbVms) { currentGcCandidates.add(elbVm.getId()); } _gcCandidateElbVmIds.retainAll(currentGcCandidates); currentGcCandidates.removeAll(_gcCandidateElbVmIds); for (Long elbVmId : _gcCandidateElbVmIds) { DomainRouterVO elbVm = _routerDao.findById(elbVmId); boolean gceed = false; try { s_logger.info("Attempting to stop ELB VM: " + elbVm); stop(elbVm, true); gceed = true; } catch (ConcurrentOperationException e) { s_logger.warn("Unable to stop unused ELB vm " + elbVm + " due to ", e); } catch (ResourceUnavailableException e) { s_logger.warn("Unable to stop unused ELB vm " + elbVm + " due to ", e); continue; } if (gceed) { try { s_logger.info("Attempting to destroy ELB VM: " + elbVm); _itMgr.expunge(elbVm.getUuid()); _routerDao.remove(elbVm.getId()); } catch (ResourceUnavailableException e) { s_logger.warn("Unable to destroy unused ELB vm " + elbVm + " due to ", e); gceed = false; } } if (!gceed) { currentGcCandidates.add(elbVm.getId()); } } _gcCandidateElbVmIds = currentGcCandidates; } } public class CleanupThread extends ManagedContextRunnable { @Override protected void runInContext() { garbageCollectUnusedElbVms(); } CleanupThread() { } } @Override public void handleDeleteLoadBalancerRule(LoadBalancer lb, long userId, Account caller) { if (!_enabled) { return; } loadBalanceRuleHandler.handleDeleteLoadBalancerRule(lb, userId, caller); } @Override public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, DeployDestination dest, ReservationContext context) { List<NicProfile> elbNics = profile.getNics(); Long guestNtwkId = null; for (NicProfile routerNic : elbNics) { if (routerNic.getTrafficType() == TrafficType.Guest) { guestNtwkId = routerNic.getNetworkId(); break; } } NetworkVO guestNetwork = _networkDao.findById(guestNtwkId); DataCenter dc = dest.getDataCenter(); StringBuilder buf = profile.getBootArgsBuilder(); buf.append(" template=domP type=" + SystemVmType); buf.append(" name=").append(profile.getHostName()); NicProfile controlNic = null; String defaultDns1 = null; String defaultDns2 = null; for (NicProfile nic : profile.getNics()) { int deviceId = nic.getDeviceId(); buf.append(" eth").append(deviceId).append("ip=").append(nic.getIPv4Address()); buf.append(" eth").append(deviceId).append("mask=").append(nic.getIPv4Netmask()); if (nic.isDefaultNic()) { buf.append(" gateway=").append(nic.getIPv4Gateway()); defaultDns1 = nic.getIPv4Dns1(); defaultDns2 = nic.getIPv4Dns2(); } if (nic.getTrafficType() == TrafficType.Management) { buf.append(" localgw=").append(dest.getPod().getGateway()); } else if (nic.getTrafficType() == TrafficType.Control) { // control command is sent over management network in VMware if (dest.getHost().getHypervisorType() == HypervisorType.VMware) { if (s_logger.isInfoEnabled()) { s_logger.info("Check if we need to add management server explicit route to ELB vm. pod cidr: " + dest.getPod().getCidrAddress() + "/" + dest.getPod().getCidrSize() + ", pod gateway: " + dest.getPod().getGateway() + ", management host: " + ApiServiceConfiguration.ManagementHostIPAdr.value()); } if (s_logger.isDebugEnabled()) { s_logger.debug("Added management server explicit route to ELB vm."); } // always add management explicit route, for basic networking setup buf.append(" mgmtcidr=").append(_mgmtCidr); buf.append(" localgw=").append(dest.getPod().getGateway()); if (dc.getNetworkType() == NetworkType.Basic) { // ask elb vm to setup SSH on guest network buf.append(" sshonguest=true"); } } controlNic = nic; } } String domain = guestNetwork.getNetworkDomain(); if (domain != null) { buf.append(" domain=" + domain); } buf.append(" dns1=").append(defaultDns1); if (defaultDns2 != null) { buf.append(" dns2=").append(defaultDns2); } if (s_logger.isDebugEnabled()) { s_logger.debug("Boot Args for " + profile + ": " + buf.toString()); } if (controlNic == null) { throw new CloudRuntimeException("Didn't start a control port"); } return true; } @Override public boolean finalizeDeployment(Commands cmds, VirtualMachineProfile profile, DeployDestination dest, ReservationContext context) throws ResourceUnavailableException { DomainRouterVO elbVm = _routerDao.findById(profile.getVirtualMachine().getId()); List<NicProfile> nics = profile.getNics(); for (NicProfile nic : nics) { if (nic.getTrafficType() == TrafficType.Public) { elbVm.setPublicIpAddress(nic.getIPv4Address()); elbVm.setPublicNetmask(nic.getIPv4Netmask()); elbVm.setPublicMacAddress(nic.getMacAddress()); } else if (nic.getTrafficType() == TrafficType.Control) { elbVm.setPrivateIpAddress(nic.getIPv4Address()); elbVm.setPrivateMacAddress(nic.getMacAddress()); } } _routerDao.update(elbVm.getId(), elbVm); finalizeCommandsOnStart(cmds, profile); return true; } @Override public boolean finalizeStart(VirtualMachineProfile profile, long hostId, Commands cmds, ReservationContext context) { CheckSshAnswer answer = (CheckSshAnswer)cmds.getAnswer("checkSsh"); if (answer == null || !answer.getResult()) { s_logger.warn("Unable to ssh to the ELB VM: " + (answer != null ? answer.getDetails() : "No answer (answer for \"checkSsh\" was null)")); return false; } return true; } @Override public boolean finalizeCommandsOnStart(Commands cmds, VirtualMachineProfile profile) { DomainRouterVO elbVm = _routerDao.findById(profile.getVirtualMachine().getId()); DataCenterVO dcVo = _dcDao.findById(elbVm.getDataCenterId()); NicProfile controlNic = null; Long guestNetworkId = null; if (profile.getHypervisorType() == HypervisorType.VMware && dcVo.getNetworkType() == NetworkType.Basic) { // TODO this is a ugly to test hypervisor type here // for basic network mode, we will use the guest NIC for control NIC for (NicProfile nic : profile.getNics()) { if (nic.getTrafficType() == TrafficType.Guest && nic.getIPv4Address() != null) { controlNic = nic; guestNetworkId = nic.getNetworkId(); } } } else { for (NicProfile nic : profile.getNics()) { if (nic.getTrafficType() == TrafficType.Control && nic.getIPv4Address() != null) { controlNic = nic; } else if (nic.getTrafficType() == TrafficType.Guest) { guestNetworkId = nic.getNetworkId(); } } } if (controlNic == null) { s_logger.error("Control network doesn't exist for the ELB vm " + elbVm); return false; } cmds.addCommand("checkSsh", new CheckSshCommand(profile.getInstanceName(), controlNic.getIPv4Address(), 3922)); // Re-apply load balancing rules List<LoadBalancerVO> lbs = _elbVmMapDao.listLbsForElbVm(elbVm.getId()); List<LoadBalancingRule> lbRules = new ArrayList<LoadBalancingRule>(); for (LoadBalancerVO lb : lbs) { List<LbDestination> dstList = _lbMgr.getExistingDestinations(lb.getId()); List<LbStickinessPolicy> policyList = _lbMgr.getStickinessPolicies(lb.getId()); List<LbHealthCheckPolicy> hcPolicyList = _lbMgr.getHealthCheckPolicies(lb.getId()); Ip sourceIp = _networkModel.getPublicIpAddress(lb.getSourceIpAddressId()).getAddress(); LoadBalancingRule loadBalancing = new LoadBalancingRule(lb, dstList, policyList, hcPolicyList, sourceIp); lbRules.add(loadBalancing); } s_logger.debug("Found " + lbRules.size() + " load balancing rule(s) to apply as a part of ELB vm " + elbVm + " start."); if (!lbRules.isEmpty()) { createApplyLoadBalancingRulesCommands(lbRules, elbVm, cmds, guestNetworkId); } return true; } @Override public void finalizeStop(VirtualMachineProfile profile, Answer answer) { if (answer != null) { DomainRouterVO elbVm = _routerDao.findById(profile.getVirtualMachine().getId()); processStopOrRebootAnswer(elbVm, answer); } } @SuppressWarnings("unused") public void processStopOrRebootAnswer(final DomainRouterVO elbVm, Answer answer) { //TODO: process network usage stats } @Override public void finalizeExpunge(VirtualMachine vm) { // no-op } @Override public void prepareStop(VirtualMachineProfile profile) { } }