EC2PropertiesProvider Class Included in NuoDB

The EC2PropertiesProvider class is a PropertiesProvider class shipped with NuoDB. This class is intended for developing on Amazon EC2. It is used only by the broker and is contained in NUODB_HOME/jar/nuoagent.jar. EC2PropertiesProvider uses the EC2 Instance Metadata REST interface to find standard properties as well as user-defined properties. In particular, it does the following:

  1. It will set the altAddr property to the public IPV4 address for the EC2 instance and it will set the region property to the AWS region name.
  2. It loads the AWS instance User Data as if it were a Java properties file. When you launch an instance in Amazon EC2, you have the option of passing User Data to the instance. In this way, you can provide property settings in the same format as they would appear in default.properties.

To specify the use of EC2PropertiesProvider for the broker, you must specify a Java JVM property definition and this can be done in the file NUODB_CFGDIR/jvm-options. The jvm-options file contains JVM property definitions for NuoDB Java applications started as services on Linux. See Java JVM Options (jvm-options) for more information. Modify the jvm-options file to set the following:

NUODB_AGENT_JAVA_OPTS="-DpropertyProvider=EC2PropertiesProvider"

For more information, see Deploying NuoDB with Amazon Web Services.

Following is the source code for EC2PropertiesProvider.java:

/* Copyright 2015 NuoDB, Inc. All rights reserved */
 
package com.nuodb.agent.plugin;
 
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
 
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
 
import com.nuodb.agent.PropertiesProvider;
 
/**
 * An implementation of {@code PropertiesProvider} intended for developing on EC2. It uses the standard Instance
 * Metadata REST interface to find specific, standard properties as well as user-defined properties.
 * <p>
 * In addition to the standard keys, if the user-data provided to an instance is formatted as a valid {@code Properties}
 * file then it will also be loaded and made available through this provider.
 * <p>
 * Note that this is currently intended just for our own development and experimentation, and should be treated as such.
 * It is still a work in progress until someone has the cycles to test in a running environment.
 *
 * @see http://docs.amazonwebservices.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html
 */
public class EC2PropertiesProvider implements PropertiesProvider {
 
    /** The base of any metadata query. */
    public static final String BASE_URL = "http://169.254.169.254/latest/";
 
    // logger for this component
    private static final Logger logger = Logger.getLogger(EC2PropertiesProvider.class.getName());
 
    // mapping for the well-known properties that have keys in the amazon API
    private final Map<String, String> apiKeyMap = new HashMap<String, String>();
 
    // local cache of the user-data properties, if any, provided at startup
    private final Properties userDataProperties = new Properties();
 
    // backing provider that uses the local properties
    private final PropertiesProvider backingProperties;
 
    /**
     * Creates an instance of {@code EC2PropertiesProvider}.
     *
     * @param systemProperties the {@code Properties} for the local system
     *
     * @throws IOException if user-data is specified but cannot be loaded
     */
    public EC2PropertiesProvider(Properties systemProperties) throws IOException {
        apiKeyMap.put("altAddr", BASE_URL + "meta-data/public-ipv4");
        apiKeyMap.put("region", BASE_URL + "dynamic/instance-identity/document");
 
        PropertiesProvider p = null;
        try {
            p = new URLPropertiesProvider(systemProperties);
        } catch (IllegalArgumentException iae) {
            logger.log(Level.WARNING, "no local, backing properties", iae);
        }
        this.backingProperties = p;
 
        // TODO: define one of the standard key locations for the public
        // key to use in licensing? Or is this completely separate? This would
        // have to be resolved based on provided properties so that we know
        // *which* key to use
 
        String userData = null;
 
        try {
            userData = getData(BASE_URL + "user-data");
        } catch (IOException ioe) {
            logger.warning("no user-data was configured for this instance");
            return;
        }
 
        if (userData.trim().isEmpty()) {
            logger.config("no user-data was provided");
        } else {
            userDataProperties.load(new StringReader(userData));
 
            logger.config("using provided user-data");
        }
    }
 
    /* Implement PropertiesProvider */
 
    @Override
    public String getProperty(String key) {
        try {
            if (apiKeyMap.containsKey(key)) {
                String value = getData(apiKeyMap.get(key));
 
                if (value.trim().isEmpty()) {
                    return null;
                }
 
                if (value.trim().startsWith("{")) {
                    // the URL resolved a JSON document, so try to find a
                    // field in the doc with the same key
 
                    JSONObject jObj = JSONObject.fromObject(value);
                    try {
                        return jObj.getString(key);
                    } catch (JSONException jsone) {
                        throw new IOException("Failed to resolve property from JSON document", jsone);
                    }
                } else {
                    return value;
                }
            } else {
                String value = userDataProperties.getProperty(key);
                if (value != null) {
                    return value;
                }
 
                if (backingProperties != null) {
                    return backingProperties.getProperty(key);
                } else {
                    return null;
                }
            }
        } catch (IOException ioe) {
            if (logger.isLoggable(Level.FINE)) {
                logger.log(Level.FINE, "could not resolve key: " + key, ioe);
            }
 
            return null;
        }
    }
 
    /* Private utility routines */
 
    private String getData(String urlString) throws IOException {
        URL url = new URL(urlString);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 
        try {
            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                throw new IOException("Failed to get url: " + urlString);
            }
 
            InputStream input = connection.getInputStream();
            byte[] bytes = new byte[connection.getContentLength()];
 
            try {
                int i = 0;
                int offset = 0;
                while ((i = input.read(bytes, offset, bytes.length - offset)) > 0) {
                    offset += i;
                }
 
 
                return URLDecoder.decode(new String(bytes), "UTF-8");
            } finally {
                input.close();
            }
        } finally {
            connection.disconnect();
        }
    }
 
}