com.hazelcast.aws.AwsEc2Api Maven / Gradle / Ivy
/*
* Copyright 2020 Hazelcast Inc.
*
* Licensed under the Hazelcast Community License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at
*
* http://hazelcast.com/hazelcast-community-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.hazelcast.aws;
import com.hazelcast.logging.ILogger;
import com.hazelcast.logging.Logger;
import org.w3c.dom.Node;
import java.time.Clock;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.hazelcast.aws.AwsRequestUtils.canonicalQueryString;
import static com.hazelcast.aws.AwsRequestUtils.createRestClient;
import static com.hazelcast.aws.AwsRequestUtils.currentTimestamp;
import static com.hazelcast.aws.StringUtils.isNotEmpty;
/**
* Responsible for connecting to AWS EC2 API.
*
* @see AWS EC2 API
*/
class AwsEc2Api {
private static final ILogger LOGGER = Logger.getLogger(AwsEc2Api.class);
private final String endpoint;
private final AwsConfig awsConfig;
private final AwsRequestSigner requestSigner;
private final Clock clock;
AwsEc2Api(String endpoint, AwsConfig awsConfig, AwsRequestSigner requestSigner, Clock clock) {
this.endpoint = endpoint;
this.awsConfig = awsConfig;
this.requestSigner = requestSigner;
this.clock = clock;
}
/**
* Calls AWS EC2 Describe Instances API, parses the response, and returns mapping from private to public IPs.
*
* Note that if EC2 Instance does not have a public IP, then an entry (private-ip, null) is returned.
*
* @return map from private to public IP
* @see EC2 Describe Instances
*/
Map describeInstances(AwsCredentials credentials) {
Map attributes = createAttributesDescribeInstances();
Map headers = createHeaders(attributes, credentials);
String response = callAwsService(attributes, headers);
return parseDescribeInstances(response);
}
private Map createAttributesDescribeInstances() {
Map attributes = createSharedAttributes();
attributes.put("Action", "DescribeInstances");
attributes.putAll(filterAttributesDescribeInstances());
return attributes;
}
private Map filterAttributesDescribeInstances() {
Filter filter = new Filter();
if (isNotEmpty(awsConfig.getTagKey())) {
if (isNotEmpty(awsConfig.getTagValue())) {
filter.add("tag:" + awsConfig.getTagKey(), awsConfig.getTagValue());
} else {
filter.add("tag-key", awsConfig.getTagKey());
}
} else if (isNotEmpty(awsConfig.getTagValue())) {
filter.add("tag-value", awsConfig.getTagValue());
}
if (isNotEmpty(awsConfig.getSecurityGroupName())) {
filter.add("instance.group-name", awsConfig.getSecurityGroupName());
}
filter.add("instance-state-name", "running");
return filter.getFilterAttributes();
}
private static Map parseDescribeInstances(String xmlResponse) {
Map result = new HashMap<>();
XmlNode.create(xmlResponse)
.getSubNodes("reservationset").stream()
.flatMap(e -> e.getSubNodes("item").stream())
.flatMap(e -> e.getSubNodes("instancesset").stream())
.flatMap(e -> e.getSubNodes("item").stream())
.filter(e -> e.getValue("privateipaddress") != null)
.peek(AwsEc2Api::logInstanceName)
.forEach(e -> result.put(e.getValue("privateipaddress"), e.getValue("ipaddress")));
return result;
}
private static void logInstanceName(XmlNode item) {
LOGGER.fine(String.format("Accepting EC2 instance [%s][%s]",
parseInstanceName(item).orElse(""),
item.getValue("privateipaddress")));
}
private static Optional parseInstanceName(XmlNode nodeHolder) {
return nodeHolder.getSubNodes("tagset").stream()
.flatMap(e -> e.getSubNodes("item").stream())
.filter(AwsEc2Api::isNameField)
.flatMap(e -> e.getSubNodes("value").stream())
.map(XmlNode::getNode)
.map(Node::getFirstChild)
.map(Node::getNodeValue)
.findFirst();
}
private static boolean isNameField(XmlNode item) {
return item.getSubNodes("key").stream()
.map(XmlNode::getNode)
.map(Node::getFirstChild)
.map(Node::getNodeValue)
.map("Name"::equals)
.findFirst()
.orElse(false);
}
/**
* Calls AWS EC2 Describe Network Interfaces API, parses the response, and returns mapping from private to public
* IPs.
*
* Note that if the given private IP does not have a public IP association, then an entry (private-ip, null)
* is returned.
*
* @return map from private to public IP
* @see
* EC2 Describe Network Interfaces
*/
Map describeNetworkInterfaces(List privateAddresses, AwsCredentials credentials) {
Map attributes = createAttributesDescribeNetworkInterfaces(privateAddresses);
Map headers = createHeaders(attributes, credentials);
String response = callAwsService(attributes, headers);
return parseDescribeNetworkInterfaces(response);
}
private Map createAttributesDescribeNetworkInterfaces(List privateAddresses) {
Map attributes = createSharedAttributes();
attributes.put("Action", "DescribeNetworkInterfaces");
attributes.putAll(filterAttributesDescribeNetworkInterfaces(privateAddresses));
return attributes;
}
private Map filterAttributesDescribeNetworkInterfaces(List privateAddresses) {
Filter filter = new Filter();
filter.addMulti("addresses.private-ip-address", privateAddresses);
return filter.getFilterAttributes();
}
private static Map parseDescribeNetworkInterfaces(String xmlResponse) {
Map result = new HashMap<>();
XmlNode.create(xmlResponse)
.getSubNodes("networkinterfaceset").stream()
.flatMap(e -> e.getSubNodes("item").stream())
.filter(e -> e.getValue("privateipaddress") != null)
.forEach(e -> result.put(
e.getValue("privateipaddress"),
e.getSubNodes("association").stream()
.map(a -> a.getValue("publicip"))
.findFirst()
.orElse(null)
));
return result;
}
private static Map createSharedAttributes() {
Map attributes = new HashMap<>();
attributes.put("Version", "2016-11-15");
return attributes;
}
private Map createHeaders(Map attributes, AwsCredentials credentials) {
Map headers = new HashMap<>();
if (isNotEmpty(credentials.getToken())) {
headers.put("X-Amz-Security-Token", credentials.getToken());
}
headers.put("Host", endpoint);
String timestamp = currentTimestamp(clock);
headers.put("X-Amz-Date", timestamp);
headers.put("Authorization", requestSigner.authHeader(attributes, headers, "", credentials, timestamp, "GET"));
return headers;
}
private String callAwsService(Map attributes, Map headers) {
String query = canonicalQueryString(attributes);
return createRestClient(urlFor(endpoint, query), awsConfig)
.withHeaders(headers)
.get();
}
private static String urlFor(String endpoint, String query) {
return AwsRequestUtils.urlFor(endpoint) + "/?" + query;
}
}