/*
* Copyright 2011 DTO Solutions, Inc. (http://dtosolutions.com)
*
* 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.
*/
/*
* NodeGenerator.java
*
* User: Greg Schueler <a href="mailto:greg@dtosolutions.com">greg@dtosolutions.com</a>
* Created: Oct 18, 2010 7:03:37 PM
*
*/
package com.dtolabs.rundeck.plugin.resources.ec2;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.services.ec2.AmazonEC2AsyncClient;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.*;
import com.dtolabs.rundeck.core.common.INodeEntry;
import com.dtolabs.rundeck.core.common.INodeSet;
import com.dtolabs.rundeck.core.common.NodeEntryImpl;
import com.dtolabs.rundeck.core.common.NodeSetImpl;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.log4j.Logger;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* InstanceToNodeMapper produces Rundeck node definitions from EC2 Instances
*
* @author Greg Schueler <a href="mailto:greg@dtosolutions.com">greg@dtosolutions.com</a>
*/
class InstanceToNodeMapper {
static final Logger logger = Logger.getLogger(InstanceToNodeMapper.class);
final AWSCredentials credentials;
private ArrayList<String> filterParams;
private String endpoint;
private boolean runningStateOnly = true;
private Properties mapping;
/**
* Create with the credentials and mapping definition
*/
InstanceToNodeMapper(final AWSCredentials credentials, final Properties mapping) {
this.credentials = credentials;
this.mapping = mapping;
}
/**
* Perform the query and return the set of instances
*
* @param nodeSet
*/
public INodeSet performQuery() {
final NodeSetImpl nodeSet = new NodeSetImpl();
final AmazonEC2Client ec2 = new AmazonEC2Client(credentials);
if (null != getEndpoint()) {
ec2.setEndpoint(getEndpoint());
}
final ArrayList<Filter> filters = buildFilters();
final Set<Instance> instances = query(ec2, new DescribeInstancesRequest().withFilters(filters));
mapInstances(nodeSet, instances);
return nodeSet;
}
/**
* Perform the query asynchronously and return the set of instances
*
* @param nodeSet
*/
public Future<INodeSet> performQueryAsync() {
final AmazonEC2AsyncClient ec2 = new AmazonEC2AsyncClient(credentials);
if (null != getEndpoint()) {
ec2.setEndpoint(getEndpoint());
}
final ArrayList<Filter> filters = buildFilters();
final Future<DescribeInstancesResult> describeInstancesRequest = ec2.describeInstancesAsync(
new DescribeInstancesRequest().withFilters(filters));
return new Future<INodeSet>() {
public boolean cancel(boolean b) {
return describeInstancesRequest.cancel(b);
}
public boolean isCancelled() {
return describeInstancesRequest.isCancelled();
}
public boolean isDone() {
return describeInstancesRequest.isDone();
}
public INodeSet get() throws InterruptedException, ExecutionException {
DescribeInstancesResult describeInstancesResult = describeInstancesRequest.get();
final NodeSetImpl nodeSet = new NodeSetImpl();
final Set<Instance> instances = examineResult(describeInstancesResult);
mapInstances(nodeSet, instances);
return nodeSet;
}
public INodeSet get(final long l, final TimeUnit timeUnit) throws InterruptedException, ExecutionException,
TimeoutException {
DescribeInstancesResult describeInstancesResult = describeInstancesRequest.get(l, timeUnit);
final NodeSetImpl nodeSet = new NodeSetImpl();
final Set<Instance> instances = examineResult(describeInstancesResult);
mapInstances(nodeSet, instances);
return nodeSet;
}
};
}
private Set<Instance> query(final AmazonEC2Client ec2, final DescribeInstancesRequest request) {
//create "running" filter
final DescribeInstancesResult describeInstancesRequest = ec2.describeInstances(request);
return examineResult(describeInstancesRequest);
}
private Set<Instance> examineResult(DescribeInstancesResult describeInstancesRequest) {
final List<Reservation> reservations = describeInstancesRequest.getReservations();
final Set<Instance> instances = new HashSet<Instance>();
for (final Reservation reservation : reservations) {
instances.addAll(reservation.getInstances());
}
return instances;
}
private ArrayList<Filter> buildFilters() {
final ArrayList<Filter> filters = new ArrayList<Filter>();
if (isRunningStateOnly()) {
final Filter filter = new Filter("instance-state-name").withValues(InstanceStateName.Running.toString());
filters.add(filter);
}
if (null != getFilterParams()) {
for (final String filterParam : getFilterParams()) {
final String[] x = filterParam.split("=", 2);
if (!"".equals(x[0]) && !"".equals(x[1])) {
filters.add(new Filter(x[0]).withValues(x[1]));
}
}
}
return filters;
}
private void mapInstances(final NodeSetImpl nodeSet, final Set<Instance> instances) {
for (final Instance inst : instances) {
final INodeEntry iNodeEntry;
try {
iNodeEntry = InstanceToNodeMapper.instanceToNode(inst, mapping);
if (null != iNodeEntry) {
nodeSet.putNode(iNodeEntry);
}
} catch (GeneratorException e) {
logger.error(e);
}
}
}
/**
* Convert an AWS EC2 Instance to a RunDeck INodeEntry based on the mapping input
*/
@SuppressWarnings("unchecked")
static INodeEntry instanceToNode(final Instance inst, final Properties mapping) throws GeneratorException {
final NodeEntryImpl node = new NodeEntryImpl();
//evaluate single settings.selector=tags/* mapping
if ("tags/*".equals(mapping.getProperty("attributes.selector"))) {
//iterate through instance tags and generate settings
for (final Tag tag : inst.getTags()) {
if (null == node.getAttributes()) {
node.setAttributes(new HashMap<String, String>());
}
node.getAttributes().put(tag.getKey(), tag.getValue());
}
}
if (null != mapping.getProperty("tags.selector")) {
final String selector = mapping.getProperty("tags.selector");
final String value = applySelector(inst, selector, mapping.getProperty("tags.default"), true);
if (null != value) {
final String[] values = value.split(",");
final HashSet<String> tagset = new HashSet<String>();
for (final String s : values) {
tagset.add(s.trim());
}
if (null == node.getTags()) {
node.setTags(tagset);
} else {
final HashSet orig = new HashSet(node.getTags());
orig.addAll(tagset);
node.setTags(orig);
}
}
}
if (null == node.getTags()) {
node.setTags(new HashSet());
}
final HashSet orig = new HashSet(node.getTags());
//apply specific tag selectors
final Pattern tagPat = Pattern.compile("^tag\\.(.+?)\\.selector$");
//evaluate tag selectors
for (final Object o : mapping.keySet()) {
final String key = (String) o;
final String selector = mapping.getProperty(key);
//split selector by = if present
final String[] selparts = selector.split("=");
final Matcher m = tagPat.matcher(key);
if (m.matches()) {
final String tagName = m.group(1);
if (null == node.getAttributes()) {
node.setAttributes(new HashMap<String, String>());
}
final String value = applySelector(inst, selparts[0], null);
if (null != value) {
if (selparts.length > 1 && !value.equals(selparts[1])) {
continue;
}
//use add the tag if the value is not null
orig.add(tagName);
}
}
}
node.setTags(orig);
//apply default values which do not have corresponding selector
final Pattern attribDefPat = Pattern.compile("^([^.]+?)\\.default$");
//evaluate selectors
for (final Object o : mapping.keySet()) {
final String key = (String) o;
final String value = mapping.getProperty(key);
final Matcher m = attribDefPat.matcher(key);
if (m.matches() && (!mapping.containsKey(key + ".selector") || "".equals(mapping.getProperty(
key + ".selector")))) {
final String attrName = m.group(1);
if (null == node.getAttributes()) {
node.setAttributes(new HashMap<String, String>());
}
if (null != value) {
node.getAttributes().put(attrName, value);
}
}
}
final Pattern attribPat = Pattern.compile("^([^.]+?)\\.selector$");
//evaluate selectors
for (final Object o : mapping.keySet()) {
final String key = (String) o;
final String selector = mapping.getProperty(key);
final Matcher m = attribPat.matcher(key);
if (m.matches()) {
final String attrName = m.group(1);
if (null == node.getAttributes()) {
node.setAttributes(new HashMap<String, String>());
}
final String value = applySelector(inst, selector, mapping.getProperty(attrName + ".default"));
if (null != value) {
//use nodename-settingname to make the setting unique to the node
node.getAttributes().put(attrName, value);
}
}
}
// String hostSel = mapping.getProperty("hostname.selector");
// String host = applySelector(inst, hostSel, mapping.getProperty("hostname.default"));
// if (null == node.getHostname()) {
// System.err.println("Unable to determine hostname for instance: " + inst.getInstanceId());
// return null;
// }
String name = node.getNodename();
if (null == name || "".equals(name)) {
name = node.getHostname();
}
if (null == name || "".equals(name)) {
name = inst.getInstanceId();
}
node.setNodename(name);
return node;
}
/**
* Return the result of the selector applied to the instance, otherwise return the defaultValue. The selector can be
* a comma-separated list of selectors
*/
public static String applySelector(final Instance inst, final String selector, final String defaultValue) throws
GeneratorException {
return applySelector(inst, selector, defaultValue, false);
}
/**
* Return the result of the selector applied to the instance, otherwise return the defaultValue. The selector can be
* a comma-separated list of selectors.
* @param inst the instance
* @param selector the selector string
* @param defaultValue a default value to return if there is no result from the selector
* @param tagMerge if true, allow | separator to merge multiple values
*/
public static String applySelector(final Instance inst, final String selector, final String defaultValue,
final boolean tagMerge) throws
GeneratorException {
if (null != selector) {
for (final String selPart : selector.split(",")) {
if (tagMerge) {
final StringBuilder sb = new StringBuilder();
for (final String subPart : selPart.split("\\|")) {
final String val = applySingleSelector(inst, subPart);
if (null != val) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(val);
}
}
if (sb.length() > 0) {
return sb.toString();
}
} else {
final String val = applySingleSelector(inst, selPart);
if (null != val) {
return val;
}
}
}
}
return defaultValue;
}
private static String applySingleSelector(final Instance inst, final String selector) throws
GeneratorException {
if (null != selector && !"".equals(selector) && selector.startsWith("tags/")) {
final String tag = selector.substring("tags/".length());
final List<Tag> tags = inst.getTags();
for (final Tag tag1 : tags) {
if (tag.equals(tag1.getKey())) {
return tag1.getValue();
}
}
} else if (null != selector && !"".equals(selector)) {
try {
final String value = BeanUtils.getProperty(inst, selector);
if (null != value) {
return value;
}
} catch (Exception e) {
throw new GeneratorException(e);
}
}
return null;
}
/**
* Return the list of "filter=value" filters
*/
public ArrayList<String> getFilterParams() {
return filterParams;
}
/**
* Return the endpoint
*/
public String getEndpoint() {
return endpoint;
}
/**
* Return true if runningStateOnly
*/
public boolean isRunningStateOnly() {
return runningStateOnly;
}
/**
* If true, the an automatic "running" state filter will be applied
*/
public void setRunningStateOnly(final boolean runningStateOnly) {
this.runningStateOnly = runningStateOnly;
}
/**
* Set the list of "filter=value" filters
*/
public void setFilterParams(final ArrayList<String> filterParams) {
this.filterParams = filterParams;
}
/**
* Set the region endpoint to use.
*/
public void setEndpoint(final String endpoint) {
this.endpoint = endpoint;
}
public Properties getMapping() {
return mapping;
}
public void setMapping(Properties mapping) {
this.mapping = mapping;
}
public static class GeneratorException extends Exception {
public GeneratorException() {
}
public GeneratorException(final String message) {
super(message);
}
public GeneratorException(final String message, final Throwable cause) {
super(message, cause);
}
public GeneratorException(final Throwable cause) {
super(cause);
}
}
}