/*******************************************************************************
* Copyright (c) 2012 GigaSpaces Technologies Ltd. All rights reserved
*
* 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 org.cloudifysource.esc.byon;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import org.apache.commons.lang.StringUtils;
import org.cloudifysource.domain.cloud.RemoteExecutionModes;
import org.cloudifysource.domain.cloud.compute.ComputeTemplate;
import org.cloudifysource.dsl.internal.CloudifyConstants;
import org.cloudifysource.dsl.utils.IPUtils;
import org.cloudifysource.esc.driver.provisioning.CloudProvisioningException;
import org.cloudifysource.esc.driver.provisioning.CustomNode;
import org.cloudifysource.esc.driver.provisioning.byon.CustomNodeImpl;
/**
* @author noak
* @since 2.0.1
*
* Implements a cloud-simulator, using private machines as a pool of nodes on which the application is deployed.
* The list of available nodes and matching credentials are configured through the cloud Groovy file.
*/
public class ByonDeployer {
private static final String NODES_LIST_FREE = "FREE";
private static final String NODES_LIST_ALLOCATED = "ALLOCATED";
private static final String NODES_LIST_INVALID = "INVALID";
private static final String NODES_LIST_TERMINATED = "TERMINATED";
protected static final java.util.logging.Logger logger = java.util.logging.Logger
.getLogger(ByonDeployer.class.getName());
private final Map<String, Map<String, List<CustomNode>>> nodesListsByTemplates =
new Hashtable<String, Map<String, List<CustomNode>>>();
/**
* Constructor.
*/
public ByonDeployer() {
}
/**
* Adds a list of nodes related to a specific template.
*
* @param templateName
* The name of the template this nodes-list belongs to
* @param template
* The cloud template for this node list.
* @param nodesList
* A list of maps, each map representing a cloud node
* @throws Exception
* Indicates the node parsing failed
*/
public synchronized void addNodesList(final String templateName,
final ComputeTemplate template,
final List<Map<String, String>> nodesList) throws CloudProvisioningException {
final List<CustomNode> resolvedNodes = new ArrayList<CustomNode>();
final List<CustomNode> unresolvedNodes = new ArrayList<CustomNode>();
// parse the given nodes list
List<CustomNode> parsedNodes = ByonUtils.parseCloudNodes(nodesList);
parsedNodes = removeDuplicates(parsedNodes);
// avoid duplicate machines in different templates (compare by IP)
logger.fine("Attempting to set template " + templateName + " with the following pool: "
+ getNodesListForPrint(parsedNodes));
// the infrastructure is based on machine IPs, they need to be unique.
// we set the resolved IP address on each node for an easy machine
// comparison from this point on
for (CustomNode node : parsedNodes) {
try {
node.resolve();
if (template.getRemoteExecution() == RemoteExecutionModes.WINRM) {
node.setLoginPort(RemoteExecutionModes.WINRM.getDefaultPort());
}
IPUtils.validateConnection(node.getPrivateIP(), node.getLoginPort());
resolvedNodes.add(node);
} catch (final Exception ex) {
// this node is not reachable - add it to the invalid nodes pool
logger.log(Level.WARNING, "Failed to resolve node: " + node.toShortString() + ", exception: "
+ ex.getMessage(), ex);
unresolvedNodes.add(node);
}
}
final Set<String> duplicateNodes = getDuplicateIPs(getAllNodes(), parsedNodes);
if (duplicateNodes.size() > 0) {
throw new CloudProvisioningException(
"Failed to add nodes for template \""
+ templateName
+ "\","
+ " some IP addresses were already defined by a different template: "
+ Arrays.toString(duplicateNodes.toArray()));
}
setInitialPoolsForTemplate(templateName, resolvedNodes, unresolvedNodes);
}
/**
* Creates a server (AKA a machine or a node) with the assigned logical name. The server is taken from the list of
* free nodes, unless this list is exhausted. If all there are no free nodes available, the invalid nodes are
* checked for SSH connection, and if a connection can be established - the node is used.
*
* @param templateName
* The name of the nodes-list' template this server belongs to
* @param serverName
* A logical name used to uniquely identify this node (does not have to match the host name)
* @return A node available for use
* @throws CloudProvisioningException
* Indicated a new machine could not be allocated, either because the name is empty or because the nodes
* pool is exhausted
*/
public synchronized CustomNode createServer(final String templateName,
final String serverName) throws CloudProvisioningException {
if (org.apache.commons.lang.StringUtils.isBlank(serverName)) {
throw new CloudProvisioningException(
"Failed to create new cloud node, server name is missing");
}
final Map<String, List<CustomNode>> templateLists = nodesListsByTemplates
.get(templateName);
if (templateLists == null || templateLists.isEmpty()) {
throw new CloudProvisioningException(
"Failed to create new cloud node. \"" + templateName
+ "\" is not a known template.");
}
final List<CustomNode> freeNodesPool = templateLists
.get(NODES_LIST_FREE);
final List<CustomNode> allocatedNodesPool = templateLists
.get(NODES_LIST_ALLOCATED);
final List<CustomNode> invalidNodesPool = templateLists
.get(NODES_LIST_INVALID);
if (freeNodesPool.isEmpty() && invalidNodesPool.isEmpty()) {
throw new CloudProvisioningException(
"Failed to create a new cloud node for template \""
+ templateName
+ "\", all available nodes are currently used."
+ " Free nodes: " + getNodesListForPrint(freeNodesPool)
+ ", Invalid nodes: " + getNodesListForPrint(invalidNodesPool)
+ ", Allocated nodes: " + getNodesListForPrint(allocatedNodesPool));
}
CustomNode node = null;
if (!freeNodesPool.isEmpty()) {
node = freeNodesPool.iterator().next();
// if this node is indeed not allocated, test the connectivity to it.
// if we can't connect to it - move it to the invalid pool,
// otherwise - allocate it.
if (!allocatedNodesPool.contains(node)) {
try {
node.resolve();
IPUtils.validateConnection(node.getPrivateIP(),
node.getLoginPort());
allocatedNodesPool.add(node);
freeNodesPool.remove(node);
} catch (final Exception e) {
// catch any exception - to prevent a machine leak. Add the
// machine to the invalids pool
logger.log(
Level.INFO,
"Failed to create server on " + node.getPrivateIP()
+ ", connection failed on port "
+ node.getLoginPort(), e);
try {
invalidateServer(templateName, node);
} catch (final CloudProvisioningException ie) {
logger.log(Level.INFO,
"Failed to mark machine " + node.getPrivateIP()
+ " as Invalid.", ie);
}
throw new CloudProvisioningException(e);
}
}
} else {
for (CustomNode currentNode : invalidNodesPool) {
try {
currentNode.resolve();
IPUtils.validateConnection(currentNode.getPrivateIP(), currentNode.getLoginPort());
if (!allocatedNodesPool.contains(currentNode)) {
allocatedNodesPool.add(currentNode);
}
invalidNodesPool.remove(currentNode);
node = currentNode;
break;
} catch (final Exception ex) {
// ignore and continue
}
}
}
if (node == null) {
throw new CloudProvisioningException(
"Failed to create a new cloud node for template \""
+ templateName
+ "\", all available nodes are currently used."
+ " Free nodes: " + getNodesListForPrint(freeNodesPool)
+ ", Invalid nodes: " + getNodesListForPrint(invalidNodesPool)
+ ", Allocated nodes: " + getNodesListForPrint(allocatedNodesPool));
}
node.setNodeName(serverName);
return node;
}
/**
* Sets the servers with the specified IPs as allocated, so they would not be re-allocated on future calls.
*
* @param templateName
* The name of the nodes-list' template the IPs belongs to
* @param ipAddresses
* A set of IP addresses (decimal dotted format)
*
* @throws CloudProvisioningException
* Indicates the IPs could not be marked as allocated with the specified template
*/
public synchronized void setAllocated(final String templateName,
final Set<String> ipAddresses) throws CloudProvisioningException {
final Map<String, List<CustomNode>> templateLists = nodesListsByTemplates
.get(templateName);
if (templateLists == null || templateLists.isEmpty()) {
throw new CloudProvisioningException(
"Failed to set allocated servers. \"" + templateName
+ "\" is not a known template.");
}
final List<CustomNode> freeNodesPool = templateLists.get(NODES_LIST_FREE);
final List<CustomNode> allocatedNodesPool = templateLists.get(NODES_LIST_ALLOCATED);
for (final String ipAddress : ipAddresses) {
logger.log(Level.INFO, "Looking for " + ipAddress + " in the pool of \"free\" machines");
for (final CustomNode node : freeNodesPool) {
if (StringUtils.isNotBlank(node.getPrivateIP())) {
if (IPUtils.isSameIpAddress(node.getPrivateIP(), ipAddress)) {
freeNodesPool.remove(node);
if (!allocatedNodesPool.contains(node)) {
allocatedNodesPool.add(node);
}
logger.log(Level.INFO, "Marking " + node.getPrivateIP() + " (" + ipAddress + ")"
+ " as \"allocated\"");
break;
}
} else {
logger.log(Level.INFO, "Host " + node.getPrivateIP() + " is not resolved to an IP address");
}
}
}
}
/**
* Shuts down a given node, (moves the node back to the free nodes list, to be used again).
*
* @param templateName
* The name of the nodes-list' template this server belongs to
* @param serverName
* A server to shutdown
* @throws CloudProvisioningException
* Indicates the server could not be shutdown with the specified template
*/
public synchronized void shutdownServer(final String templateName,
final CustomNode serverName) throws CloudProvisioningException {
if (serverName == null) {
return;
}
final Map<String, List<CustomNode>> templateLists = nodesListsByTemplates
.get(templateName);
if (templateLists == null || templateLists.isEmpty()) {
throw new CloudProvisioningException("Failed to shutdown server \""
+ serverName + "\". \"" + templateName
+ "\" is not a known template.");
}
final List<CustomNode> freeNodesPool = templateLists
.get(NODES_LIST_FREE);
final List<CustomNode> allocatedNodesPool = templateLists
.get(NODES_LIST_ALLOCATED);
((CustomNodeImpl) serverName).setGroup(null);
allocatedNodesPool.remove(serverName);
if (!freeNodesPool.contains(serverName)) {
freeNodesPool.add(serverName);
}
}
/**
* Shuts down the server with the given ID.
*
* @param templateName
* The name of the nodes-list' template this server belongs to
* @param serverId
* The ID of the server to shutdown
* @throws CloudProvisioningException
* Indicates the server could not be shutdown with the specified template
*/
public void shutdownServerById(final String templateName,
final String serverId) throws CloudProvisioningException {
shutdownServer(templateName, getServerByID(templateName, serverId));
}
/**
* Shuts down the server with the given IP address.
*
* @param templateName
* The name of the nodes-list' template this server belongs to
* @param serverIp
* The IP of the server to shutdown (dotted decimal format)
* @throws CloudProvisioningException
* Indicates the server could not be shutdown with the specified template
*/
public void shutdownServerByIp(final String templateName,
final String serverIp) throws CloudProvisioningException {
shutdownServer(templateName, getServerByIP(templateName, serverIp));
}
/**
* Retrieves the server with the given name (not a host name).
*
* @param templateName
* The name of the nodes-list' template this server belongs to
* @param serverName
* The name of the server to retrieve
* @return A node matching the given name, if found
* @throws CloudProvisioningException
* Indicates the server could not be obtained with the specified template
*/
public CustomNode getServerByName(final String templateName,
final String serverName) throws CloudProvisioningException {
CustomNode selectedNode = null;
for (final CustomNode node : getAllNodesByTemplateName(templateName)) {
if (node.getNodeName().equalsIgnoreCase(serverName)) {
selectedNode = node;
break;
}
}
return selectedNode;
}
/**
* Retrieves the server with the given Id (not a host name).
*
* @param templateName
* The name of the nodes-list' template this server belongs to
* @param id
* The id of the server to retrieve
* @return A node matching the given id, if found
* @throws CloudProvisioningException
* Indicates the server could not be obtained with the specified template
*/
public CustomNode getServerByID(final String templateName, final String id)
throws CloudProvisioningException {
CustomNode selectedNode = null;
for (final CustomNode node : getAllNodesByTemplateName(templateName)) {
if (node.getId().equalsIgnoreCase(id)) {
selectedNode = node;
break;
}
}
return selectedNode;
}
/**
* Retrieves the server with the given IP (not a host name).
*
* @param templateName
* The name of the nodes-list' template this IP belongs to
* @param ipAddress
* The IP address of the server to retrieve
* @return A node with the given IP, if found
* @throws CloudProvisioningException
* Indicates the server could not be obtained with the specified template
*/
public CustomNode getServerByIP(final String templateName,
final String ipAddress) throws CloudProvisioningException {
CustomNode selectedNode = null;
for (final CustomNode node : getAllNodesByTemplateName(templateName)) {
if (IPUtils.isSameIpAddress(node.getPrivateIP(), ipAddress)) {
selectedNode = node;
break;
}
}
return selectedNode;
}
/**
* Retrieves all nodes (i.e. in all states - free, allocated and invalid).
*
* @param templateName
* The name of the nodes-list' template to use
* @return A collection of all the managed nodes of the specified template
* @throws CloudProvisioningException
* Indicates the servers list could not be obtained for the given template name
*/
public synchronized Set<CustomNode> getAllNodesByTemplateName(final String templateName)
throws CloudProvisioningException {
final Set<CustomNode> allNodes = new HashSet<CustomNode>();
final Map<String, List<CustomNode>> templateLists = nodesListsByTemplates
.get(templateName);
if (templateLists == null || templateLists.isEmpty()) {
throw new CloudProvisioningException(
"Failed to get servers list. \"" + templateName
+ "\" is not a known template.");
}
final List<CustomNode> freeNodesPool = templateLists
.get(NODES_LIST_FREE);
final List<CustomNode> allocatedNodesPool = templateLists
.get(NODES_LIST_ALLOCATED);
final List<CustomNode> invalidNodesPool = templateLists
.get(NODES_LIST_INVALID);
allNodes.addAll(freeNodesPool);
allNodes.addAll(allocatedNodesPool);
allNodes.addAll(invalidNodesPool);
return allNodes;
}
/**
* Retrieves all free nodes.
*
* @param templateName
* The name of the nodes-list' template to use
* @return A collection of all the free nodes of the specified template
* @throws CloudProvisioningException
* Indicates the servers list could not be obtained for the given template name
*/
public synchronized Set<CustomNode> getFreeNodesByTemplateName(final String templateName)
throws CloudProvisioningException {
final Set<CustomNode> allNodes = new HashSet<CustomNode>();
final Map<String, List<CustomNode>> templateLists = nodesListsByTemplates
.get(templateName);
if (templateLists == null || templateLists.isEmpty()) {
throw new CloudProvisioningException(
"Failed to get servers list. \"" + templateName
+ "\" is not a known template.");
}
allNodes.addAll(templateLists.get(NODES_LIST_FREE));
return allNodes;
}
/**
* Retrieves all allocated nodes.
*
* @param templateName
* The name of the nodes-list' template to use
* @return A collection of all the allocated (active, in use) nodes of the specified template
* @throws CloudProvisioningException
* Indicates the servers list could not be obtained for the given template name
*/
public synchronized Set<CustomNode> getAllocatedNodesByTemplateName(
final String templateName) throws CloudProvisioningException {
final Set<CustomNode> allNodes = new HashSet<CustomNode>();
final Map<String, List<CustomNode>> templateLists = nodesListsByTemplates
.get(templateName);
if (templateLists == null || templateLists.isEmpty()) {
throw new CloudProvisioningException(
"Failed to get servers list. \"" + templateName
+ "\" is not a known template.");
}
allNodes.addAll(templateLists.get(NODES_LIST_ALLOCATED));
return allNodes;
}
/**
* Retrieves all invalid nodes.
*
* @param templateName
* The name of the nodes-list' template to use
* @return A collection of all the invalid nodes of the specified template
* @throws CloudProvisioningException
* Indicates the servers list could not be obtained for the given template name
*/
public synchronized Set<CustomNode> getInvalidNodesByTemplateName(
final String templateName) throws CloudProvisioningException {
final Set<CustomNode> allNodes = new HashSet<CustomNode>();
final Map<String, List<CustomNode>> templateLists = nodesListsByTemplates
.get(templateName);
if (templateLists == null || templateLists.isEmpty()) {
throw new CloudProvisioningException(
"Failed to get servers list. \"" + templateName
+ "\" is not a known template.");
}
allNodes.addAll(templateLists.get(NODES_LIST_INVALID));
return allNodes;
}
/**
* Invalidates the given node (i.e. moves it from the free pool to the invalid pool), so it will not be
* allocated unless all the free nodes are in use.
*
* @param templateName
* The template this server belongs to
* @param serverName
* The name of the server to invalidate
* @throws CloudProvisioningException
* Indicates the server could not be marked as Invalid for the specified template
*/
public synchronized void invalidateServer(final String templateName,
final CustomNode serverName) throws CloudProvisioningException {
logger.warning("Invalidaing node: " + serverName + " from template: " + templateName);
// attempting to remove the invalid node from the active lists so it
// will not be used anymore, just to
// be sure.
final Map<String, List<CustomNode>> templateLists = nodesListsByTemplates
.get(templateName);
if (templateLists == null || templateLists.isEmpty()) {
// todo illegal argument exception
throw new CloudProvisioningException(
"Failed to invalidate server. \"" + templateName
+ "\" is not a known template.");
}
final List<CustomNode> freeNodesPool = templateLists
.get(NODES_LIST_FREE);
final List<CustomNode> allocatedNodesPool = templateLists
.get(NODES_LIST_ALLOCATED);
final List<CustomNode> invalidNodesPool = templateLists
.get(NODES_LIST_INVALID);
freeNodesPool.remove(serverName);
allocatedNodesPool.remove(serverName);
if (!invalidNodesPool.contains(serverName)) {
invalidNodesPool.add(serverName);
}
}
/**
* closes the deployer, currently not doing anything.
*/
public void close() {
// Do nothing
}
private synchronized List<CustomNode> getAllNodes() throws CloudProvisioningException {
final List<CustomNode> allNodes = new ArrayList<CustomNode>();
for (final String templateName : nodesListsByTemplates.keySet()) {
final Map<String, List<CustomNode>> templateLists = nodesListsByTemplates
.get(templateName);
if (templateLists == null || templateLists.isEmpty()) {
throw new CloudProvisioningException(
"Failed to get servers list. \"" + templateName
+ "\" is not a known template.");
}
final List<CustomNode> freeNodesPool = templateLists
.get(NODES_LIST_FREE);
final List<CustomNode> allocatedNodesPool = templateLists
.get(NODES_LIST_ALLOCATED);
final List<CustomNode> invalidNodesPool = templateLists
.get(NODES_LIST_INVALID);
allNodes.addAll(freeNodesPool);
allNodes.addAll(allocatedNodesPool);
allNodes.addAll(invalidNodesPool);
}
return allNodes;
}
/**
* Sets the initial nodes pools (free nodes, allocated, invalid and terminated) for each template. The initial
* allocated-nodes pool is always empty. The terminated nodes pool is currently not supported and remains empty all
* along.
*
* @param templateName
* The name of the template
* @param resolvedNodes
* The resolved nodes (will be set as free nodes ready for use)
* @param unresolvedNodes
* The unresolved nodes (will be set as invalid as they aren't reachable now)
*/
private void setInitialPoolsForTemplate(final String templateName,
final List<CustomNode> resolvedNodes,
final List<CustomNode> unresolvedNodes) {
// todo use hashmap instead
// use pojo instead of a complex map
final Map<String, List<CustomNode>> templateLists = new Hashtable<String, List<CustomNode>>();
final List<CustomNode> freeNodesPool = new ArrayList<CustomNode>();
final List<CustomNode> invalidNodesPool = new ArrayList<CustomNode>();
freeNodesPool.addAll(aggregateWithoutDuplicates(freeNodesPool,
resolvedNodes));
invalidNodesPool.addAll(aggregateWithoutDuplicates(invalidNodesPool,
unresolvedNodes));
logger.info("Setting initial pools for template: " + templateName + ". "
+ CloudifyConstants.NEW_LINE + "Free nodes: " + getNodesListForPrint(freeNodesPool)
+ CloudifyConstants.NEW_LINE + "Invalid nodes: " + getNodesListForPrint(invalidNodesPool));
templateLists.put(NODES_LIST_FREE, freeNodesPool);
templateLists.put(NODES_LIST_ALLOCATED, new ArrayList<CustomNode>());
templateLists.put(NODES_LIST_INVALID, invalidNodesPool);
templateLists.put(NODES_LIST_TERMINATED, new ArrayList<CustomNode>());
nodesListsByTemplates.put(templateName, templateLists);
}
private static List<CustomNode> removeDuplicates(
final List<CustomNode> customNodesList) {
final List<CustomNode> totalList = new ArrayList<CustomNode>();
for (final CustomNode node : customNodesList) {
if (!totalList.contains(node)) {
totalList.add(node);
}
}
return totalList;
}
private static Set<String> getDuplicateIPs(final List<CustomNode> oldNodes,
final List<CustomNode> newNodes) {
final Set<String> existingIPs = new HashSet<String>();
for (final CustomNode newNode : newNodes) {
for (final CustomNode oldNode : oldNodes) {
if (IPUtils.isSameIpAddress(oldNode.getPrivateIP(), newNode.getPrivateIP())) {
existingIPs.add(oldNode.getPrivateIP());
break;
}
}
}
return existingIPs;
}
private static List<CustomNode> aggregateWithoutDuplicates(
final List<CustomNode> basicList, final List<CustomNode> newItems) {
final List<CustomNode> totalList = new ArrayList<CustomNode>(basicList);
for (final CustomNode node : newItems) {
if (!totalList.contains(node)) {
totalList.add(node);
}
}
return totalList;
}
/**
* Gets a list of the templates being used.
* @return a list of the templates being used
*/
public synchronized List<String> getTemplatesList() {
List<String> templatesList = new LinkedList<String>();
templatesList.addAll(nodesListsByTemplates.keySet());
return templatesList;
}
/**
* Removes templates that should not be used, as long as they are not already being used
* (i.e. template's nodes are allocated)
* @param redundantTemplates The names of the template to be removed
* @throws CloudProvisioningException Indicates one or more of the template's nodes are allocated,
* and so the template cannot be removed
*/
public synchronized void removeTemplates(final List<String> redundantTemplates) throws CloudProvisioningException {
for (String templateName : redundantTemplates) {
Map<String, List<CustomNode>> nodesMap = nodesListsByTemplates.get(templateName);
List<CustomNode> allocatedNodesList = nodesMap.get(NODES_LIST_ALLOCATED);
if (allocatedNodesList != null && !allocatedNodesList.isEmpty()) {
String errMsg = "Failed to remove template [" + templateName
+ "] from deployer, some nodes are still allocated: " + allocatedNodesList;
logger.log(Level.WARNING, errMsg);
throw new CloudProvisioningException(errMsg);
}
nodesListsByTemplates.remove(templateName);
}
}
/**
* Builds a string representing the given list of nodes, including only their main details
* (i.e. node ID, private IP, host name, node name).
* @param nodesToPrint The list of nodes to print
* @return The nodes' main details, as a string
*/
public String getNodesListForPrint(final List<CustomNode> nodesToPrint) {
StringBuilder nodesStr = new StringBuilder();
boolean first = true;
for (CustomNode node : nodesToPrint) {
if (first) {
nodesStr.append("CustomNode[" + node.toShortString() + "]");
first = false;
} else {
nodesStr.append(", CustomNode[" + node.toShortString() + "]");
}
}
return nodesStr.toString();
}
}