Java tutorial
/* * Copyright 2012-2014, Continuuity, Inc. * * Licensed 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.continuuity.loom.layout; import com.continuuity.loom.admin.ClusterTemplate; import com.continuuity.loom.admin.Compatibilities; import com.continuuity.loom.admin.HardwareType; import com.continuuity.loom.admin.ImageType; import com.continuuity.loom.admin.Provider; import com.continuuity.loom.admin.ProviderType; import com.continuuity.loom.admin.Service; import com.continuuity.loom.cluster.Cluster; import com.continuuity.loom.cluster.Node; import com.continuuity.loom.cluster.NodeProperties; import com.continuuity.loom.layout.change.ClusterLayoutChange; import com.continuuity.loom.layout.change.ClusterLayoutTracker; import com.continuuity.loom.scheduler.task.NodeService; import com.continuuity.loom.store.entity.EntityStoreService; import com.continuuity.loom.store.entity.EntityStoreView; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; /** * The solver takes a cluster template, a number of machines, and figures out what services to put on what hardware * and images in order to satisfy the cluster constraints. * * TODO: add ability to grow/shrink, making sure services dont move. * TODO: add ability to add/remove services from an existing cluster * TODO: refactor into cleaner pieces with pluggable pieces for constraints */ public class Solver { private static final Logger LOG = LoggerFactory.getLogger(Solver.class); private final EntityStoreService entityStoreService; private final ClusterLayoutUpdater updater; @Inject private Solver(EntityStoreService entityStoreService, ClusterLayoutUpdater updater) { this.entityStoreService = entityStoreService; this.updater = updater; } /** * Add services to a cluster, returning which nodes were affected by the change or null if there was no way to * add the services to the cluster. * * @param cluster Cluster to add the services to. * @param clusterNodes Nodes in the cluster. * @param servicesToAdd Services to add to the cluster. * @return Nodes that need to have services added to them. * @throws Exception */ public Set<Node> addServicesToCluster(Cluster cluster, Set<Node> clusterNodes, Set<String> servicesToAdd) throws Exception { EntityStoreView entityStore = entityStoreService.getView(cluster.getAccount()); Map<String, Service> serviceMap = getServiceMap(Sets.union(cluster.getServices(), servicesToAdd), entityStore); validateServiceCompatibilities(cluster.getClusterTemplate().getCompatibilities(), servicesToAdd); validateServiceDependencies(serviceMap); ClusterLayoutTracker tracker = updater.addServicesToCluster(cluster, clusterNodes, servicesToAdd); if (tracker == null) { return null; } Set<Node> changedNodes = Sets.newHashSet(); for (ClusterLayoutChange change : tracker.getChanges()) { changedNodes.addAll(change.applyChange(cluster, clusterNodes, serviceMap)); } return changedNodes; } /** * Validate whether or not a set of services are allowed to be added to a cluster. * * @param cluster Cluster to check addition of services to. * @param servicesToAdd Services to add to the cluster * @throws IOException */ public void validateServicesToAdd(Cluster cluster, Set<String> servicesToAdd) throws IOException { EntityStoreView entityStore = entityStoreService.getView(cluster.getAccount()); Map<String, Service> serviceMap = getServiceMap(Sets.union(cluster.getServices(), servicesToAdd), entityStore); validateServicesToAdd(cluster, servicesToAdd, serviceMap); } private void validateServicesToAdd(Cluster cluster, Set<String> servicesToAdd, Map<String, Service> serviceMap) { validateServiceCompatibilities(cluster.getClusterTemplate().getCompatibilities(), servicesToAdd); validateServiceDependencies(serviceMap); } /** * Given a {@link Cluster} and {@link ClusterCreateRequest}, return a mapping of node id to {@link Node} describing * how the cluster should be laid out. If multiple possible cluster layouts are possible, one will be chosen * deterministically. * * @param cluster Cluster to solve a layout for. * @param request Request to create a cluster containing cluster settings to use. * @return Mapping of node id to node for all nodes in the cluster. * @throws Exception */ public Map<String, Node> solveClusterNodes(Cluster cluster, ClusterCreateRequest request) throws Exception { EntityStoreView entityStore = entityStoreService.getView(cluster.getAccount()); // TODO: these checks should happen at request time, not at solve time // make sure the template exists String clusterTemplateName = request.getClusterTemplate(); ClusterTemplate template = entityStore.getClusterTemplate(clusterTemplateName); if (template == null) { throw new IllegalArgumentException("cluster template " + clusterTemplateName + " does not exist."); } cluster.setClusterTemplate(template); if (request.getConfig() == null) { cluster.setConfig(template.getClusterDefaults().getConfig()); } // make sure the provider exists String providerName = request.getProvider(); if (providerName == null || providerName.isEmpty()) { providerName = template.getClusterDefaults().getProvider(); } Provider provider = entityStore.getProvider(providerName); if (provider == null) { throw new IllegalArgumentException("provider " + providerName + " does not exist."); } ProviderType providerType = entityStore.getProviderType(provider.getProviderType()); if (providerType == null) { throw new IllegalArgumentException("provider type " + providerType + " does not exist."); } // add user given provider fields to the provider object provider.addUserFields(request.getProviderFields(), providerType); cluster.setProvider(provider); // make sure there are hardware types that can be used String requiredHardwareType = request.getHardwareType(); if (requiredHardwareType == null || requiredHardwareType.isEmpty()) { // this can be null too, which means no cluster wide required type requiredHardwareType = template.getClusterDefaults().getHardwaretype(); } if (requiredHardwareType != null && requiredHardwareType.isEmpty()) { requiredHardwareType = null; } Map<String, String> hardwareTypeFlavors = getHardwareTypeMap(providerName, template, requiredHardwareType, entityStore); if (hardwareTypeFlavors.isEmpty()) { throw new IllegalArgumentException("no hardware types are available to use with template " + template.getName() + " and provider " + providerName); } // TODO: horribly ugly... just get the ImageType object instead of treating flavor/image specially // make sure there are image types that can be used String requiredImageType = request.getImageType(); if (requiredImageType == null || requiredImageType.isEmpty()) { // this can be null too, which means no cluster wide required type requiredImageType = template.getClusterDefaults().getImagetype(); } if (requiredImageType != null && requiredImageType.isEmpty()) { requiredImageType = null; } Map<String, Map<String, String>> imageTypeMap = getImageTypeMap(providerName, template, requiredImageType, entityStore); if (imageTypeMap.isEmpty()) { throw new IllegalArgumentException("no image types are available to use with template " + template.getName() + " and provider " + providerName); } // Determine valid lease duration for the cluster. It has to be less than the initial lease duration set in // template. long initialLeaseDuration = template.getAdministration().getLeaseDuration().getInitial(); long effectiveRequestLeaseDuration = request.getInitialLeaseDuration() == 0 ? Long.MAX_VALUE : request.getInitialLeaseDuration(); long leaseDuration; if (request.getInitialLeaseDuration() == -1) { leaseDuration = initialLeaseDuration; } else if (initialLeaseDuration == 0 || initialLeaseDuration >= effectiveRequestLeaseDuration) { leaseDuration = request.getInitialLeaseDuration(); } else { throw new IllegalArgumentException("lease duration cannot be greater than specified in template"); } if (leaseDuration < 0) { throw new IllegalArgumentException("invalid lease duration: " + leaseDuration); } // Lease duration of 0 is forever. cluster.setExpireTime(leaseDuration == 0 ? 0 : cluster.getCreateTime() + leaseDuration); // make sure the services to place on the cluster are all valid Set<String> serviceNames = request.getServices(); if (serviceNames == null || serviceNames.isEmpty()) { serviceNames = template.getClusterDefaults().getServices(); } validateServiceCompatibilities(template.getCompatibilities(), serviceNames); Map<String, Service> serviceMap = getServiceMap(serviceNames, entityStore); validateServiceDependencies(serviceMap); cluster.setServices(serviceNames); // TODO: move building of node properties to NodeService or Node or some place more sensible String dnsSuffix = request.getDnsSuffix(); if (dnsSuffix == null || dnsSuffix.isEmpty()) { dnsSuffix = template.getClusterDefaults().getDnsSuffix(); } Map<String, Node> nodes = solveConstraints(cluster.getId(), template, request.getName(), request.getNumMachines(), hardwareTypeFlavors, imageTypeMap, serviceNames, serviceMap, dnsSuffix); // Update cluster object // TODO: this should happen outside Solver. cluster.setNodes(nodes == null ? ImmutableSet.<String>of() : nodes.keySet()); return nodes; } // get a mapping of service name to service object for fast lookup later. Also check that each service actually // exists. private Map<String, Service> getServiceMap(Set<String> serviceNames, EntityStoreView entityStore) throws IOException { Map<String, Service> map = Maps.newHashMap(); for (String serviceName : serviceNames) { Service service = entityStore.getService(serviceName); if (service == null) { throw new IllegalArgumentException("service " + serviceName + " does not exist"); } map.put(serviceName, entityStore.getService(serviceName)); } return map; } // get a mapping of hardware type name to flavor that can be used with the given provider and cluster template. private Map<String, String> getHardwareTypeMap(String providerName, ClusterTemplate template, String requiredHardwareType, EntityStoreView entityStore) throws Exception { Map<String, String> flavorMap = Maps.newHashMap(); Compatibilities compatibilities = template.getCompatibilities(); if (requiredHardwareType != null) { addProviderFlavor(flavorMap, providerName, compatibilities, entityStore.getHardwareType(requiredHardwareType)); return flavorMap; } for (HardwareType hardwareType : entityStore.getAllHardwareTypes()) { addProviderFlavor(flavorMap, providerName, compatibilities, hardwareType); } return flavorMap; } // Adds an hardwareType name -> flavor entry to the map given a hardware type, the name of the provider, and // a whitelist of allowed hardware types. If the flavor does not exist for the provider and whitelisted // hardware type, nothing is added. private void addProviderFlavor(Map<String, String> map, String providerName, Compatibilities compatibilities, HardwareType hardwareType) { if (hardwareType != null) { Map<String, Map<String, String>> providerMap = hardwareType.getProviderMap(); String name = hardwareType.getName(); // empty allowed types means all types are allowed if (compatibilities.compatibleWithHardwareType(name) && providerMap.containsKey(providerName)) { String flavor = providerMap.get(providerName).get("flavor"); if (flavor != null) { map.put(name, flavor); } } } } // get a mapping of image type name to provider properties for that image type private Map<String, Map<String, String>> getImageTypeMap(String providerName, ClusterTemplate template, String requiredImageType, EntityStoreView entityStore) throws Exception { Map<String, Map<String, String>> imageMap = Maps.newHashMap(); Compatibilities compatibilities = template.getCompatibilities(); if (requiredImageType != null) { addProviderImage(imageMap, providerName, compatibilities, entityStore.getImageType(requiredImageType)); return imageMap; } for (ImageType imageType : entityStore.getAllImageTypes()) { addProviderImage(imageMap, providerName, compatibilities, imageType); } return imageMap; } private void addProviderImage(Map<String, Map<String, String>> map, String providerName, Compatibilities compatibilities, ImageType imageType) { if (imageType != null) { Map<String, Map<String, String>> providerMap = imageType.getProviderMap(); String name = imageType.getName(); // empty allowed types means all types are allowed if (compatibilities.compatibleWithImageType(name) && providerMap.containsKey(providerName)) { Map<String, String> providerProperties = providerMap.get(providerName); String image = providerProperties.get("image"); if (image != null) { map.put(name, providerProperties); } } } } private void validateServiceCompatibilities(Compatibilities compatibilities, Set<String> services) { if (!compatibilities.compatibleWithServices(services)) { Set<String> incompatibleServices = Sets.difference(services, compatibilities.getServices()); if (!incompatibleServices.isEmpty()) { String incompatibleStr = Joiner.on(',').join(incompatibleServices); throw new IllegalArgumentException(incompatibleStr + " are incompatible with the cluster"); } } } /** * Given a map of service name to {@link Service}, validate that the service dependency requirements for all services * in the map are satisfied by some other service in the map, throwing an {@link IllegalArgumentException} if they * are not. * * @param serviceMap Map of service name to {@link Service} to check all dependency requirements for. */ private void validateServiceDependencies(Map<String, Service> serviceMap) { // gather all services that will be provided on the cluster. This is every service plus any service they provide. Set<String> providedServices = Sets.newHashSet(); for (Service service : serviceMap.values()) { providedServices.addAll(service.getDependencies().getProvides()); } providedServices = Sets.union(serviceMap.keySet(), providedServices); // check dependencies boolean dependenciesSatisfied = true; StringBuilder errMsg = new StringBuilder(); for (Service service : serviceMap.values()) { for (String serviceDependency : service.getDependencies().getRequiredServices()) { if (!providedServices.contains(serviceDependency)) { if (!dependenciesSatisfied) { errMsg.append("\n"); } errMsg.append(service.getName()); errMsg.append(" requires "); errMsg.append(serviceDependency); errMsg.append(", which is not on the cluster or in the list of services to add."); dependenciesSatisfied = false; } } } if (!dependenciesSatisfied) { throw new IllegalArgumentException(errMsg.toString()); } boolean hasConflicts = false; errMsg = new StringBuilder(); for (Service service : serviceMap.values()) { for (String conflictingService : service.getDependencies().getConflicts()) { if (serviceMap.keySet().contains(conflictingService)) { if (hasConflicts) { errMsg.append("\n"); } errMsg.append(service.getName()); errMsg.append(" conflicts with "); errMsg.append(conflictingService); errMsg.append("."); hasConflicts = true; } } } if (hasConflicts) { throw new IllegalArgumentException(errMsg.toString()); } } // solves for a valid cluster layout based on the constraints. First finds all possible node layouts that can be // used in the cluster based on the services that need to be on the cluster and constraints. Then searches for a // valid number of each node layout based on the constraints. static Map<String, Node> solveConstraints(String clusterId, ClusterTemplate clusterTemplate, String clusterName, int numMachines, Map<String, String> hardwareTypeMap, Map<String, Map<String, String>> imageTypeMap, Set<String> serviceNames, Map<String, Service> serviceMap, String dnsSuffix) { NodeLayoutGenerator nodeLayoutGenerator = new NodeLayoutGenerator(clusterTemplate, serviceNames, hardwareTypeMap.keySet(), imageTypeMap.keySet()); // We need to deterministically choose the same cluster. Nodelayouts earlier in the traversal order are // preferred. List<NodeLayout> traversalOrder = nodeLayoutGenerator.generateNodeLayoutPreferences(); long start = System.nanoTime(); ClusterLayoutFinder layoutFinder = new ClusterLayoutFinder(traversalOrder, clusterTemplate, serviceNames, numMachines); int[] clusterlayout = layoutFinder.findValidNodeCounts(); long dur = (System.nanoTime() - start) / 1000000; LOG.debug("took {} ms to find cluster layout", dur); if (clusterlayout == null) { return null; } Map<String, Node> clusterNodes = Maps.newHashMap(); int nodeNum = 1000; for (int i = 0; i < clusterlayout.length; i++) { NodeLayout nodeLayout = traversalOrder.get(i); for (int j = 0; j < clusterlayout[i]; j++) { String nodeId = UUID.randomUUID().toString(); Set<Service> nodeServices = Sets.newHashSet(); for (String serviceName : nodeLayout.getServiceNames()) { nodeServices.add(serviceMap.get(serviceName)); } String hardwaretype = nodeLayout.getHardwareTypeName(); String imagetype = nodeLayout.getImageTypeName(); String imageId = imageTypeMap.get(imagetype).get("image"); // TODO: temporary workaround, need to refactor the task json String sshUser = imageTypeMap.get(imagetype).get("ssh-user"); String hostname = NodeService.createHostname(clusterName, clusterId, nodeNum, dnsSuffix); String flavor = hardwareTypeMap.get(hardwaretype); // TODO: these should be proper fields and logic for populating node properties should not be in the solver. NodeProperties nodeProperties = NodeProperties.builder().setHostname(hostname).setNodenum(nodeNum) .setHardwaretype(hardwaretype).setImagetype(imagetype).setFlavor(flavor).setImage(imageId) .setSSHUser(sshUser).setServices(nodeServices).build(); nodeNum++; clusterNodes.put(nodeId, new Node(nodeId, clusterId, nodeServices, nodeProperties)); } } return clusterNodes; } }