/*
* Copyright 1999-2008 University of Chicago
*
* 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.nimbustools.metadataserver.defaults;
import org.nimbustools.api.services.rm.Manager;
import org.nimbustools.api.services.metadata.MetadataServer;
import org.nimbustools.api.services.metadata.MetadataServerException;
import org.nimbustools.api.services.metadata.MetadataServerUnauthorizedException;
import org.nimbustools.api.repr.vm.VM;
import org.nimbustools.api.repr.vm.VMFile;
import org.nimbustools.api.repr.vm.NIC;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.safehaus.uuid.UUIDGenerator;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URI;
import java.util.*;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
/**
* See: http://docs.amazonwebservices.com/AWSEC2/2008-08-08/DeveloperGuide/index.html?AESDG-chapter-instancedata.html
*/
@SuppressWarnings("unchecked")
public class DefaultMetadataServer implements MetadataServer {
// -------------------------------------------------------------------------
// STATIC VARIABLES
// -------------------------------------------------------------------------
private static final Log logger =
LogFactory.getLog(MetadataRequestHandler.class.getName());
protected static final String CONTACT_SOCKET_PREFIX = "contact.socket";
// -------------------------------------------------------------------------
// INSTANCE VARIABLES
// -------------------------------------------------------------------------
protected Manager manager;
protected String customizationPath;
protected String listenSocket = null;
protected boolean enabled;
protected boolean listening;
protected HTTPListener listener;
protected String[] localNets;
protected String[] publicNets;
protected final Cache cache;
protected Properties properties;
private final UUIDGenerator uuidGen;
private Set<URL> listenSockets;
private Map<String, URL> networkContacts;
private URL defaultContact;
// -------------------------------------------------------------------------
// CONSTRUCTOR
// -------------------------------------------------------------------------
public DefaultMetadataServer(CacheManager cacheManager) {
if (cacheManager == null) {
throw new IllegalArgumentException("cacheManager may not be null");
}
this.cache = cacheManager.getCache("metadataServerCache");
if (this.cache == null) {
throw new IllegalArgumentException(
"cacheManager does not provide 'metadataServerCache'");
}
this.uuidGen = UUIDGenerator.getInstance();
}
// -------------------------------------------------------------------------
// PROPERTIES
// -------------------------------------------------------------------------
public void setCustomizationPath(String path) {
this.customizationPath = path;
}
public void setListenSocket(String listenSocket) {
this.listenSocket = listenSocket;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setManager(Manager manager) {
this.manager = manager;
}
public void setLocalNets(String localNetsStr) {
if (localNetsStr == null || localNetsStr.trim().length() == 0) {
this.localNets = null;
return;
}
final String[] parts = localNetsStr.split(",");
final ArrayList partsArray = new ArrayList(parts.length);
for (String part : parts) {
if (part != null && part.trim().length() > 0) {
partsArray.add(part.trim());
}
}
this.localNets = (String[]) partsArray.toArray(new String[0]);
}
public void setPublicNets(String publicNetsStr) {
if (publicNetsStr == null || publicNetsStr.trim().length() == 0) {
this.publicNets = null;
return;
}
final String[] parts = publicNetsStr.split(",");
final ArrayList partsArray = new ArrayList(parts.length);
for (String part : parts) {
if (part != null && part.trim().length() > 0) {
partsArray.add(part.trim());
}
}
this.publicNets = (String[]) partsArray.toArray(new String[0]);
}
// -------------------------------------------------------------------------
// implements MetadataServer
// -------------------------------------------------------------------------
public String getResponse(String target, String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
// If developers try to access directly , these checks could trigger.
// Possible use cases in future? If so, the intialize method will
// need to be changed to bring the server up even if the HTTP server
// fails to start up for whatever reason.
if (!this.enabled) {
throw new MetadataServerException("metadata server not enabled");
}
if (!this.listening) {
throw new MetadataServerException(
"metadata server did not initialize correctly, sorry.");
}
logger.debug("considering target: '" + target + "', client: " +
remoteAddress);
try {
return this.dispatch(target, remoteAddress);
} catch (MetadataServerUnauthorizedException e) {
logger.error("UNAUTHORIZED call to metadata server, message: " +
e.getMessage());
throw e;
} catch (MetadataServerException e) {
logger.error("Problem with metadata server, message: " +
e.getMessage() + " ||| Client visible message: " +
e.getClientVisibleMessage());
throw e;
} catch (Throwable t) {
final String err = "Unhandled problem dispatching metadata " +
"request: " + t.getMessage();
if (logger.isDebugEnabled()) {
logger.error(err, t);
} else {
logger.error(err);
}
throw new MetadataServerException(err, t);
}
}
public String getCustomizationPath() {
return this.customizationPath;
}
public synchronized String getContactURL(NIC[] nics) {
if (!this.listening) {
logger.warn("contact URL requested but not listening?");
return null;
}
if (this.listener == null) {
logger.warn("listening but no listener??");
return null;
}
// search for the first NIC with a matching contact address
if (!this.networkContacts.isEmpty() && nics != null) {
for (NIC nic : nics) {
final String network = nic.getNetworkName();
if (network == null) {
continue;
}
final URL url = this.networkContacts.get(network);
if (url != null) {
return url.toString();
}
}
}
if (defaultContact != null) {
return defaultContact.toString();
}
return null;
}
public synchronized boolean isListening() {
return this.listening;
}
public boolean isEnabled() {
return this.enabled;
}
public synchronized void initServerAndListen() throws Exception {
if (!this.enabled) {
logger.info("metadata server not enabled");
return;
}
initialize();
start();
}
public synchronized void initialize() throws Exception {
if (!this.enabled) {
logger.info("metadata server not enabled");
return;
}
if (this.listening) {
throw new Exception("already listening");
}
this.intakeProperties();
final MetadataRequestHandler handler = new MetadataRequestHandler(this);
this.listener = new HTTPListener(this.listenSockets);
this.listener.initServer(handler);
}
public synchronized void start() throws Exception {
this.listener.start();
this.listening = true;
}
public synchronized void stop() throws Exception {
if (!this.listening) {
return;
}
this.listener.stop();
this.listening = false;
}
private void intakeProperties() throws Exception {
if (this.properties == null) {
throw new Exception("properties not provided, don't know where to listen");
}
URL defaultContact = null;
final Map<String, URL> networkContacts = new HashMap();
for (Object propNameObject : properties.keySet()) {
final String propName = (String) propNameObject;
if (propName.startsWith(CONTACT_SOCKET_PREFIX)) {
String value = this.properties.getProperty(propName);
if (value == null) {
continue;
}
value = value.trim();
if (value.length() == 0) {
continue;
}
// hardcoding http here on purpose, to make it obvious that https is
// not supported if someone tries to put more than a host+port in the
// configuration
final URL url;
try {
url = new URL("http://" + value);
} catch (MalformedURLException e) {
throw new Exception("Invalid host:port value in "+ propName +
" property: " + e.getMessage());
}
if (propName.length() == CONTACT_SOCKET_PREFIX.length()) {
defaultContact = url;
} else if (propName.charAt(CONTACT_SOCKET_PREFIX.length()) == '.') {
final String network = propName.substring(CONTACT_SOCKET_PREFIX.length()+1);
if (network.length() == 0) {
throw new Exception("Missing network name in property: " + propName);
}
networkContacts.put(network, url);
}
}
}
final Set<URL> listenSockets;
if (defaultContact != null && defaultContact.getHost().equals("0.0.0.0")) {
if (networkContacts.isEmpty()) {
throw new Exception("if the metadata server listens on all interfaces (0.0.0.0), " +
"you must specify contact addresses to be given to VMs. For example, \""+
CONTACT_SOCKET_PREFIX + ".public\"");
}
listenSockets = java.util.Collections.singleton(defaultContact);
} else {
listenSockets = new HashSet<URL>(networkContacts.values());
if (defaultContact != null) {
listenSockets.add(defaultContact);
}
}
this.listenSockets = listenSockets;
this.defaultContact = defaultContact;
this.networkContacts = networkContacts;
}
// -------------------------------------------------------------------------
// DISPATCH
// -------------------------------------------------------------------------
protected final String[] API_VERSIONS =
{"latest", "1.0", "2007-01-19", "2007-03-01", "2008-08-08"};
protected String dispatch(String target, String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
if (target.equals("/")) {
return this.topIndex();
}
String subtarget = null;
for (String version : API_VERSIONS) {
version = "/" + version;
if (target.startsWith(version)) {
if (target.length() == version.length()) {
subtarget = "";
break;
} else if (target.charAt(version.length()) == '/') {
subtarget = target.substring(version.length() + 1);
break;
}
}
}
if (subtarget == null) {
final StringBuilder sb = new StringBuilder();
sb.append("Unrecognized path: '").append(target).
append("'. Expected first subdirectory in path to be one of: ");
for (int i = 0; i < API_VERSIONS.length; i++) {
String version = API_VERSIONS[i];
if (i == API_VERSIONS.length-1 && API_VERSIONS.length > 1) {
sb.append(", or ");
} else if (i > 0) {
sb.append(", ");
}
sb.append("'").append(version).append("'");
}
final String err = sb.toString();
throw new MetadataServerException(err, err);
}
return this.dispatch2(target, subtarget, remoteAddress);
}
protected String dispatch2(String target,
String subtarget,
String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
if (subtarget.equals("") || subtarget.equals("/")) {
return "meta-data\nuser-data\n";
} else if (subtarget.startsWith("meta-data/")) {
return dispatchMetaData(target,
subtarget.substring(10),
remoteAddress);
} else if (subtarget.startsWith("user-data")) {
return dispatchUserData(target,
subtarget.substring(9),
remoteAddress);
} else {
final String err = "Unrecognized URL: '" + target + "'. " +
"Expected second subdirectory in path to be either " +
"'meta-data/' or 'user-data'.";
throw new MetadataServerException(err, err);
}
}
protected String dispatchUserData(String target,
String subsubtarget,
String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
return this.userData(remoteAddress);
}
protected String dispatchMetaData(String target,
String subsubtarget,
String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
if (subsubtarget.equals("") || subsubtarget.equals("/")) {
return "ami-id\nami-launch-index\nlocal-hostname\nlocal-ipv4\n" +
"public-hostname\npublic-ipv4\n";
} else if (subsubtarget.startsWith("ami-id")) {
return this.amiID(remoteAddress);
} else if (subsubtarget.startsWith("ami-launch-index")) {
return this.amiLaunchIndex(remoteAddress);
} else if (subsubtarget.startsWith("local-hostname")) {
return this.localHostname(remoteAddress);
} else if (subsubtarget.startsWith("local-ipv4")) {
return this.localIPV4(remoteAddress);
} else if (subsubtarget.startsWith("public-hostname")) {
return this.publicHostname(remoteAddress);
} else if (subsubtarget.startsWith("public-ipv4")) {
return this.publicIPV4(remoteAddress);
} else {
throw unimplemented(target, remoteAddress);
}
}
private static String getUserErr(String method) {
final String USER_UNIMPL_METHOD = "You tried to use ";
final String USER_UNIMPL_METHOD2 = ", but that is not " +
"implemented yet. Usually these can be implemented quickly, " +
"inquire on the mailing list.";
return USER_UNIMPL_METHOD + method + USER_UNIMPL_METHOD2;
}
private static String getLogErr(String method, String remoteAddress) {
return "USER (@ ip: '" + remoteAddress +
"') TRIED UNIMPLEMENTED: " + method;
}
private static MetadataServerException unimplemented(String method, String remoteAddress) {
return new MetadataServerException(getLogErr(method, remoteAddress),
getUserErr(method));
}
// -------------------------------------------------------------------------
// ASSOCIATE IP ADDRESS
// -------------------------------------------------------------------------
/**
* The usual pattern for access is a burst of request at each VM's boot.
*
* In the future this lookup should lock on IP address and not whole
* metadata server instance.
*
* @param ip remote client's IP
* @return VM instance, never null
* @throws MetadataServerException problem
* @throws MetadataServerUnauthorizedException could not associate VM
*/
public synchronized VM getCachedAndValidatedVM(String ip)
throws MetadataServerException,
MetadataServerUnauthorizedException {
final Element el = this.cache.get(ip);
final VM vm;
if (el == null) {
vm = this.getVM(ip);
this.validateVM(vm, ip);
this.cache.put(new Element(ip, vm));
} else {
vm = (VM) el.getObjectValue();
}
return vm;
}
/**
* @param ipAddress remote client's IP
* @return VM instance, never null
* @throws MetadataServerException problem
* @throws MetadataServerUnauthorizedException could not associate VM
*/
public VM getValidatedVM(String ipAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
final VM vm = this.getVM(ipAddress);
this.validateVM(vm, ipAddress);
return vm;
}
/**
* @param ipAddress remote client's IP
* @return VM instance, never null
* @throws MetadataServerException problem
* @throws MetadataServerUnauthorizedException could not associate VM
*/
public VM getVM(String ipAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
final VM[] vms;
try {
vms = this.manager.getAllByIPAddress(ipAddress);
} catch (Throwable t) {
final String uuid =
this.uuidGen.generateRandomBasedUUID().toString();
String userError = "There has been an internal server error. " +
"Contact the administrator with this lookup key for more " +
"information: '" + uuid + "'";
String fullError = "Problem querying manager for IP '" +
ipAddress + "', ERROR UUID='" + uuid + "': " + t.getMessage();
throw new MetadataServerException(fullError, userError, t);
}
if (vms == null || vms.length == 0) {
final String err =
"Could not associate IP with a VM: " + ipAddress;
throw new MetadataServerUnauthorizedException(err);
} else if (vms.length > 1) {
final String err =
"IP is associated with more than one VM! IP: " + ipAddress;
throw new MetadataServerException(err, err);
}
return vms[0];
}
protected void validateVM(VM vm,
String ip) throws MetadataServerException {
if (vm == null) {
throw this.validationProblem("manager implementation is invalid, " +
"returned a null VM", ip);
}
VMFile[] vmFiles = vm.getVMFiles();
if (vmFiles == null || vmFiles.length == 0) {
throw this.validationProblem("manager implementation is invalid, " +
"returned a VM with null or zero vmFiles", ip);
}
boolean foundRootfile = false;
for (VMFile file: vmFiles) {
if (file == null) {
throw this.validationProblem("manager implementation is " +
"invalid, returned a VM with a null value in " +
"vmFiles array", ip);
}
final URI uri = file.getURI();
if (uri == null && file.getBlankSpaceSize() < 1) {
throw this.validationProblem("manager implementation is " +
"invalid, returned a VM with a null URI value in " +
"vmFiles array", ip);
}
if (file.isRootFile()) {
if (uri == null) {
throw this.validationProblem("manager implementation is " +
"invalid, returned a VM with a null URI value " +
"for root disk in vmFiles array", ip);
}
foundRootfile = true;
}
}
if (!foundRootfile) {
throw this.validationProblem("manager implementation is " +
"invalid, returned a VM with no " +
"root disk file in vmFiles array", ip);
}
}
private MetadataServerException validationProblem(String issue,
String ipAddress) {
final String uuid =
this.uuidGen.generateRandomBasedUUID().toString();
String userError = "There has been an internal server error. " +
"Contact the administrator with this lookup key for more " +
"information: '" + uuid + "'";
String fullError = "Problem querying manager for IP '" +
ipAddress + "', ERROR UUID='" + uuid + "': " + issue;
return new MetadataServerException(fullError, userError);
}
// -------------------------------------------------------------------------
// 1.0 API (FLAT)
// -------------------------------------------------------------------------
/**
* "/"
*
* @return APIs supported. Sending back lies so that recent tooling works.
*/
protected String topIndex() {
/* NOT actually supporting these later protocols but providing them
as a passthrough to 1.0 */
StringBuilder sb = new StringBuilder();
for (String version : API_VERSIONS) {
sb.append(version).append("\n");
}
return sb.toString();
}
/*
* "/user-data"
*/
protected String userData(String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
VM vm = this.getCachedAndValidatedVM(remoteAddress);
final String data = vm.getMdUserData();
if (data == null) {
return "";
} else {
return data;
}
}
/*
* "/meta-data/ami-id"
*/
protected String amiID(String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
VM vm = this.getCachedAndValidatedVM(remoteAddress);
VMFile[] files = vm.getVMFiles();
for (VMFile file: files) {
if (file.isRootFile()) {
return this.justFilename(file.getURI().toASCIIString());
}
}
throw this.validationProblem("Should be impossible because we used " +
"get*ValidatedVM", remoteAddress);
}
protected String justFilename(String imageURI) {
if (imageURI == null) {
throw new IllegalArgumentException("imageURI may not be null");
}
final int idx = imageURI.lastIndexOf('/');
if (idx < 0) {
return imageURI;
} else {
return imageURI.substring(idx+1);
}
}
/*
* "/meta-data/ami-launch-index"
*/
protected String amiLaunchIndex(String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
final VM vm = this.getCachedAndValidatedVM(remoteAddress);
return Integer.toString(vm.getLaunchIndex());
}
/*
* "/meta-data/local-ipv4"
*/
protected String localIPV4(String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
final VM vm = this.getCachedAndValidatedVM(remoteAddress);
final NIC nic = this.getLocalNIC(vm);
if (nic != null) {
final String ip = nic.getIpAddress();
if (ip != null) {
return ip.trim();
}
}
return "";
}
/*
* "/meta-data/public-ipv4"
*/
protected String publicIPV4(String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
final VM vm = this.getCachedAndValidatedVM(remoteAddress);
final NIC nic = this.getPublicNIC(vm);
if (nic != null) {
final String ip = nic.getIpAddress();
if (ip != null) {
return ip.trim();
}
}
return "";
}
/*
* "/meta-data/local-hostname"
*/
protected String localHostname(String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
final VM vm = this.getCachedAndValidatedVM(remoteAddress);
final NIC nic = this.getLocalNIC(vm);
if (nic != null) {
final String hostname = nic.getHostname();
if (hostname != null) {
return hostname.trim();
}
}
return "";
}
/*
* "/meta-data/public-hostname"
*/
protected String publicHostname(String remoteAddress)
throws MetadataServerException,
MetadataServerUnauthorizedException {
final VM vm = this.getCachedAndValidatedVM(remoteAddress);
final NIC nic = this.getPublicNIC(vm);
if (nic != null) {
final String hostname = nic.getHostname();
if (hostname != null) {
return hostname.trim();
}
}
return "";
}
private NIC getLocalNIC(VM vm) {
return findNIC(this.localNets, vm);
}
private NIC getPublicNIC(VM vm) {
return findNIC(this.publicNets, vm);
}
private static NIC findNIC(String[] networkNames, VM vm) {
if (networkNames == null || networkNames.length == 0) {
return null;
}
final NIC[] nics = vm.getNics();
if (nics == null || nics.length == 0) {
return null;
}
for (String netname : networkNames) {
final NIC nic = getParticularNetworkNIC(netname, nics);
if (nic != null) {
return nic;
}
}
return null;
}
private static NIC getParticularNetworkNIC(String netname, NIC[] nics) {
if (netname == null || netname.trim().length() == 0) {
return null;
}
for (NIC nic : nics) {
if (nic != null) {
final String oneName = nic.getNetworkName();
if (oneName != null
&& oneName.trim().equals(netname.trim())) {
return nic;
}
}
}
return null;
}
}