/* * Copyright 2012 Netflix, 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.netflix.appinfo; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.netflix.discovery.converters.jackson.builder.StringInterningAmazonInfoBuilder; import com.netflix.discovery.internal.util.AmazonInfoUtils; import com.thoughtworks.xstream.annotations.XStreamOmitField; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An AWS specific {@link DataCenterInfo} implementation. * * <p> * Gets AWS specific information for registration with eureka by making a HTTP * call to an AWS service as recommended by AWS. * </p> * * @author Karthik Ranganathan, Greg Kim * */ @JsonDeserialize(builder = StringInterningAmazonInfoBuilder.class) public class AmazonInfo implements DataCenterInfo, UniqueIdentifier { private Map<String, String> metadata = new HashMap<String, String>(); private static final String AWS_API_VERSION = "latest"; private static final String AWS_METADATA_URL = "http://169.254.169.254/" + AWS_API_VERSION + "/meta-data/"; public enum MetaDataKey { instanceId("instance-id"), // always have this first as we use it as a fail fast mechanism amiId("ami-id"), instanceType("instance-type"), localIpv4("local-ipv4"), localHostname("local-hostname"), availabilityZone("availability-zone", "placement/"), publicHostname("public-hostname"), publicIpv4("public-ipv4"), mac("mac"), // mac is declared above vpcId so will be found before vpcId (where it is needed) vpcId("vpc-id", "network/interfaces/macs/") { @Override public URL getURL(String prepend, String mac) throws MalformedURLException { return new URL(AWS_METADATA_URL + this.path + mac + "/" + this.name); } }, accountId("accountId") { private Pattern pattern = Pattern.compile("\"accountId\"\\s?:\\s?\\\"([A-Za-z0-9]*)\\\""); @Override public URL getURL(String prepend, String append) throws MalformedURLException { return new URL("http://169.254.169.254/" + AWS_API_VERSION + "/dynamic/instance-identity/document"); } // no need to use a json deserializer, do a custom regex parse @Override public String read(InputStream inputStream) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); try { String toReturn = null; String inputLine; while ((inputLine = br.readLine()) != null) { Matcher matcher = pattern.matcher(inputLine); if (toReturn == null && matcher.find()) { toReturn = matcher.group(1); // don't break here as we want to read the full buffer for a clean connection close } } return toReturn; } finally { br.close(); } } }; protected String name; protected String path; MetaDataKey(String name) { this(name, ""); } MetaDataKey(String name, String path) { this.name = name; this.path = path; } public String getName() { return name; } // override to apply prepend and append public URL getURL(String prepend, String append) throws MalformedURLException { return new URL(AWS_METADATA_URL + path + name); } public String read(InputStream inputStream) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); String toReturn; try { String line = br.readLine(); toReturn = line; while (line != null) { // need to read all the buffer for a clean connection close line = br.readLine(); } return toReturn; } finally { br.close(); } } public String toString() { return getName(); } } public static final class Builder { private static final Logger logger = LoggerFactory.getLogger(Builder.class); private static final int SLEEP_TIME_MS = 100; @XStreamOmitField private AmazonInfo result; @XStreamOmitField private AmazonInfoConfig config; private Builder() { result = new AmazonInfo(); } public static Builder newBuilder() { return new Builder(); } public Builder addMetadata(MetaDataKey key, String value) { result.metadata.put(key.getName(), value); return this; } public Builder withAmazonInfoConfig(AmazonInfoConfig config) { this.config = config; return this; } /** * Build the {@link InstanceInfo} information. * * @return AWS specific instance information. */ public AmazonInfo build() { return result; } /** * Build the {@link AmazonInfo} automatically via HTTP calls to instance * metadata API. * * @param namespace the namespace to look for configuration properties. * @return the instance information specific to AWS. */ public AmazonInfo autoBuild(String namespace) { if (config == null) { config = new Archaius1AmazonInfoConfig(namespace); } for (MetaDataKey key : MetaDataKey.values()) { int numOfRetries = config.getNumRetries(); while (numOfRetries-- > 0) { try { String mac = null; if (key == MetaDataKey.vpcId) { mac = result.metadata.get(MetaDataKey.mac.getName()); // mac should be read before vpcId due to declaration order } URL url = key.getURL(null, mac); String value = AmazonInfoUtils.readEc2MetadataUrl(key, url, config.getConnectTimeout(), config.getReadTimeout()); if (value != null) { result.metadata.put(key.getName(), value); } break; } catch (Throwable e) { if (config.shouldLogAmazonMetadataErrors()) { logger.warn("Cannot get the value for the metadata key :" + key + " Reason :", e); } if (numOfRetries >= 0) { try { Thread.sleep(SLEEP_TIME_MS); } catch (InterruptedException e1) { } continue; } } } if (key == MetaDataKey.instanceId && config.shouldFailFastOnFirstLoad() && !result.metadata.containsKey(MetaDataKey.instanceId.getName())) { logger.warn("Skipping the rest of AmazonInfo init as we were not able to load instanceId after " + "the configured number of retries: {}, per fail fast configuration: {}", config.getNumRetries(), config.shouldFailFastOnFirstLoad()); break; // break out of loop and return whatever we have thus far } } return result; } } public AmazonInfo() { } /** * Constructor provided for deserialization framework. It is expected that {@link AmazonInfo} will be built * programmatically using {@link AmazonInfo.Builder}. * * @param name this value is ignored, as it is always set to "Amazon" */ @JsonCreator public AmazonInfo( @JsonProperty("name") String name, @JsonProperty("metadata") HashMap<String, String> metadata) { this.metadata = metadata; } @Override public Name getName() { return Name.Amazon; } /** * Get the metadata information specific to AWS. * * @return the map of AWS metadata as specified by {@link MetaDataKey}. */ @JsonProperty("metadata") public Map<String, String> getMetadata() { return metadata; } /** * Set AWS metadata. * * @param metadataMap * the map containing AWS metadata. */ public void setMetadata(Map<String, String> metadataMap) { this.metadata = metadataMap; } /** * Gets the AWS metadata specified in {@link MetaDataKey}. * * @param key * the metadata key. * @return String returning the value. */ public String get(MetaDataKey key) { return metadata.get(key.getName()); } @Override @JsonIgnore public String getId() { return get(MetaDataKey.instanceId); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AmazonInfo)) return false; AmazonInfo that = (AmazonInfo) o; if (metadata != null ? !metadata.equals(that.metadata) : that.metadata != null) return false; return true; } @Override public int hashCode() { return metadata != null ? metadata.hashCode() : 0; } @Override public String toString() { return "AmazonInfo{" + "metadata=" + metadata + '}'; } }