mirror of https://github.com/apache/cassandra
Add support for AWS Ec2 IMDSv2
patch by Stefan Miklosovic; reviewed by Jacek Lewandowski and Brandon Williams for CASSANDRA-16555 Co-authored-by: Jacek Lewandowski <lewandowski.jacek@gmail.com> Co-authored-by: Paul Rütter <paul@blueconic.com>
This commit is contained in:
parent
de7b1584f8
commit
4ea7bb25b4
|
|
@ -1,4 +1,5 @@
|
|||
3.0.30
|
||||
* Add support for AWS Ec2 IMDSv2 (CASSANDRA-16555)
|
||||
* Suppress CVE-2023-35116 (CASSANDRA-18630)
|
||||
* Pass taskId from CompactionTask to system.compaction_history (CASSANDRA-12183)
|
||||
* Suppress CVE-2023-34455, CVE-2023-34454, CVE-2023-34453 (CASSANDRA-18608)
|
||||
|
|
|
|||
10
NEWS.txt
10
NEWS.txt
|
|
@ -60,6 +60,16 @@ restore snapshots created with the previous major version using the
|
|||
'sstableloader' tool. You can upgrade the file format of your snapshots
|
||||
using the provided 'sstableupgrade' tool.
|
||||
|
||||
3.0.30
|
||||
======
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
- Please beware that if you use Ec2Snitch or Ec2MultiRegionSnitch, by default it will
|
||||
communicate with AWS IMDS of version 2. This change is transparent, there does not need
|
||||
to be done anything upon upgrade. Furthermore, IMDS of version 2 can be configured to be required in AWS EC2 console.
|
||||
Consult cassandra-rackdc.properties for more details. (CASSANDRA-16555)
|
||||
|
||||
3.0.26
|
||||
======
|
||||
|
||||
|
|
|
|||
|
|
@ -25,3 +25,9 @@ rack=rack1
|
|||
|
||||
# Uncomment the following line to make this snitch prefer the internal ip when possible, as the Ec2MultiRegionSnitch does.
|
||||
# prefer_local=true
|
||||
|
||||
# Type of AWS IMDS for Ec2Snitch and Ec2MultiRegionSnitch, value might be either "v1" or "v2". Defaults to "v2".
|
||||
# ec2_metadata_type=v2
|
||||
# If AWS IMDS of v2 is configured, ec2_metadata_token_ttl_seconds says how many seconds a token will be valid until
|
||||
# it is refreshed. Defaults to 21600. Can not be smaller than 30 and bigger than 21600. Has to be an integer.
|
||||
# ec2_metadata_token_ttl_seconds=21600
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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.apache.cassandra.locator;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
abstract class AbstractCloudMetadataServiceConnector
|
||||
{
|
||||
protected final String metadataServiceUrl;
|
||||
|
||||
protected AbstractCloudMetadataServiceConnector(String metadataServiceUrl)
|
||||
{
|
||||
this.metadataServiceUrl = metadataServiceUrl;
|
||||
}
|
||||
|
||||
public String apiCall(String query) throws IOException
|
||||
{
|
||||
return apiCall(metadataServiceUrl, query, 200);
|
||||
}
|
||||
|
||||
public String apiCall(String url, String query, int expectedResponseCode) throws IOException
|
||||
{
|
||||
return apiCall(url, query, "GET", ImmutableMap.of(), expectedResponseCode);
|
||||
}
|
||||
|
||||
public String apiCall(String url,
|
||||
String query,
|
||||
String method,
|
||||
Map<String, String> extraHeaders,
|
||||
int expectedResponseCode) throws IOException
|
||||
{
|
||||
HttpURLConnection conn = null;
|
||||
try
|
||||
{
|
||||
// Populate the region and zone by introspection, fail if 404 on metadata
|
||||
conn = (HttpURLConnection) new URL(url + query).openConnection();
|
||||
extraHeaders.forEach(conn::setRequestProperty);
|
||||
conn.setRequestMethod(method);
|
||||
if (conn.getResponseCode() != expectedResponseCode)
|
||||
throw new HttpException(conn.getResponseCode(), conn.getResponseMessage());
|
||||
|
||||
// Read the information. I wish I could say (String) conn.getContent() here...
|
||||
int cl = conn.getContentLength();
|
||||
|
||||
if (cl == -1)
|
||||
return null;
|
||||
|
||||
byte[] b = new byte[cl];
|
||||
try (DataInputStream d = new DataInputStream((InputStream) conn.getContent()))
|
||||
{
|
||||
d.readFully(b);
|
||||
}
|
||||
return new String(b, UTF_8);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (conn != null)
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HttpException extends IOException
|
||||
{
|
||||
public final int responseCode;
|
||||
public final String responseMessage;
|
||||
|
||||
public HttpException(int responseCode, String responseMessage)
|
||||
{
|
||||
super("HTTP response code: " + responseCode + " (" + responseMessage + ')');
|
||||
this.responseCode = responseCode;
|
||||
this.responseMessage = responseMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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.apache.cassandra.locator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.utils.Pair;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static org.apache.cassandra.locator.Ec2MetadataServiceConnector.EC2MetadataType.v2;
|
||||
|
||||
abstract class Ec2MetadataServiceConnector extends AbstractCloudMetadataServiceConnector
|
||||
{
|
||||
static final String EC2_METADATA_TYPE_PROPERTY = "ec2_metadata_type";
|
||||
static final String EC2_METADATA_URL_PROPERTY = "ec2_metadata_url";
|
||||
static final String DEFAULT_EC2_METADATA_URL = "http://169.254.169.254";
|
||||
|
||||
Ec2MetadataServiceConnector(SnitchProperties properties)
|
||||
{
|
||||
super(properties.get(EC2_METADATA_URL_PROPERTY, DEFAULT_EC2_METADATA_URL));
|
||||
}
|
||||
|
||||
enum EC2MetadataType
|
||||
{
|
||||
v1(props -> Ec2MetadataServiceConnector.V1Connector.create(props)),
|
||||
v2(props -> Ec2MetadataServiceConnector.V2Connector.create(props));
|
||||
|
||||
private final Function<SnitchProperties, Ec2MetadataServiceConnector> provider;
|
||||
|
||||
EC2MetadataType(Function<SnitchProperties, Ec2MetadataServiceConnector> provider)
|
||||
{
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
Ec2MetadataServiceConnector create(SnitchProperties properties)
|
||||
{
|
||||
return provider.apply(properties);
|
||||
}
|
||||
}
|
||||
|
||||
static Ec2MetadataServiceConnector create(SnitchProperties props)
|
||||
{
|
||||
try
|
||||
{
|
||||
return EC2MetadataType.valueOf(props.get(EC2_METADATA_TYPE_PROPERTY, v2.name())).create(props);
|
||||
}
|
||||
catch (IllegalArgumentException ex)
|
||||
{
|
||||
throw new ConfigurationException(format("%s must be one of %s", EC2_METADATA_TYPE_PROPERTY,
|
||||
stream(EC2MetadataType.values()).map(Enum::name).collect(joining(", "))));
|
||||
}
|
||||
}
|
||||
|
||||
static class V1Connector extends Ec2MetadataServiceConnector
|
||||
{
|
||||
static V1Connector create(SnitchProperties props)
|
||||
{
|
||||
return new V1Connector(props);
|
||||
}
|
||||
|
||||
V1Connector(SnitchProperties props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s{%s=%s}", V1Connector.class.getName(), EC2_METADATA_URL_PROPERTY, metadataServiceUrl);
|
||||
}
|
||||
}
|
||||
|
||||
static class V2Connector extends Ec2MetadataServiceConnector
|
||||
{
|
||||
static final int MAX_TOKEN_TIME_IN_SECONDS = 21600;
|
||||
static final int MIN_TOKEN_TIME_IN_SECONDS = 30;
|
||||
|
||||
static final String AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY = "ec2_metadata_token_ttl_seconds";
|
||||
static final String AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER = "X-aws-ec2-metadata-token-ttl-seconds";
|
||||
static final String AWS_EC2_METADATA_TOKEN_HEADER = "X-aws-ec2-metadata-token";
|
||||
static final String TOKEN_QUERY = "/latest/api/token";
|
||||
|
||||
@VisibleForTesting
|
||||
static int HTTP_REQUEST_RETRIES = 1;
|
||||
|
||||
private Pair<String, Long> token;
|
||||
@VisibleForTesting
|
||||
final Duration tokenTTL;
|
||||
|
||||
static V2Connector create(SnitchProperties props)
|
||||
{
|
||||
String tokenTTLString = props.get(AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY,
|
||||
Integer.toString(MAX_TOKEN_TIME_IN_SECONDS));
|
||||
|
||||
Duration tokenTTL;
|
||||
try
|
||||
{
|
||||
tokenTTL = Duration.ofSeconds(Integer.parseInt(tokenTTLString));
|
||||
|
||||
if (tokenTTL.getSeconds() < MIN_TOKEN_TIME_IN_SECONDS || tokenTTL.getSeconds() > MAX_TOKEN_TIME_IN_SECONDS)
|
||||
{
|
||||
throw new ConfigurationException(format("property %s was set to %s seconds which is not in allowed range of [%s..%s]",
|
||||
AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY,
|
||||
tokenTTL.getSeconds(),
|
||||
MIN_TOKEN_TIME_IN_SECONDS,
|
||||
MAX_TOKEN_TIME_IN_SECONDS));
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException ex)
|
||||
{
|
||||
throw new ConfigurationException(format("Unable to parse integer from property %s, value to parse: %s",
|
||||
AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY, tokenTTLString));
|
||||
}
|
||||
|
||||
return new V2Connector(props, tokenTTL);
|
||||
}
|
||||
|
||||
V2Connector(SnitchProperties properties, Duration tokenTTL)
|
||||
{
|
||||
super(properties);
|
||||
this.tokenTTL = tokenTTL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String apiCall(String url, String query, String method, Map<String, String> extraHeaders, int expectedResponseCode) throws IOException
|
||||
{
|
||||
Map<String, String> headers = new HashMap<>(extraHeaders);
|
||||
for (int retry = 0; retry <= HTTP_REQUEST_RETRIES; retry++)
|
||||
{
|
||||
String resolvedToken;
|
||||
if (token != null && token.right > System.currentTimeMillis())
|
||||
resolvedToken = token.left;
|
||||
else
|
||||
resolvedToken = getToken();
|
||||
|
||||
try
|
||||
{
|
||||
headers.put(AWS_EC2_METADATA_TOKEN_HEADER, resolvedToken);
|
||||
return super.apiCall(url, query, method, headers, expectedResponseCode);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (retry == HTTP_REQUEST_RETRIES)
|
||||
throw ex;
|
||||
|
||||
if (ex.responseCode == 401) // invalidate token if it is 401
|
||||
this.token = null;
|
||||
}
|
||||
}
|
||||
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s{%s=%s,%s=%s}",
|
||||
V2Connector.class.getName(),
|
||||
EC2_METADATA_URL_PROPERTY, metadataServiceUrl,
|
||||
AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY,
|
||||
tokenTTL.getSeconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a session token to enable requests to the meta data service.
|
||||
* <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html">IMDSv2</a>
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public synchronized String getToken()
|
||||
{
|
||||
try
|
||||
{
|
||||
token = Pair.create(super.apiCall(metadataServiceUrl,
|
||||
TOKEN_QUERY,
|
||||
"PUT",
|
||||
ImmutableMap.of(AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER, String.valueOf(tokenTTL.getSeconds())),
|
||||
200),
|
||||
System.currentTimeMillis() + tokenTTL.toMillis() - TimeUnit.SECONDS.toMillis(5));
|
||||
return token.left;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,10 @@ package org.apache.cassandra.locator;
|
|||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.gms.ApplicationState;
|
||||
import org.apache.cassandra.gms.Gossiper;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
|
|
@ -39,16 +41,29 @@ import org.apache.cassandra.service.StorageService;
|
|||
*/
|
||||
public class Ec2MultiRegionSnitch extends Ec2Snitch
|
||||
{
|
||||
private static final String PUBLIC_IP_QUERY_URL = "http://169.254.169.254/latest/meta-data/public-ipv4";
|
||||
private static final String PRIVATE_IP_QUERY_URL = "http://169.254.169.254/latest/meta-data/local-ipv4";
|
||||
@VisibleForTesting
|
||||
static final String PUBLIC_IP_QUERY = "/latest/meta-data/public-ipv4";
|
||||
@VisibleForTesting
|
||||
static final String PRIVATE_IP_QUERY = "/latest/meta-data/local-ipv4";
|
||||
private final String localPrivateAddress;
|
||||
|
||||
public Ec2MultiRegionSnitch() throws IOException, ConfigurationException
|
||||
{
|
||||
super();
|
||||
InetAddress localPublicAddress = InetAddress.getByName(awsApiCall(PUBLIC_IP_QUERY_URL));
|
||||
this(new SnitchProperties());
|
||||
}
|
||||
|
||||
public Ec2MultiRegionSnitch(SnitchProperties props) throws IOException, ConfigurationException
|
||||
{
|
||||
this(props, Ec2MetadataServiceConnector.create(props));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Ec2MultiRegionSnitch(SnitchProperties props, Ec2MetadataServiceConnector connector) throws IOException
|
||||
{
|
||||
super(props, connector);
|
||||
InetAddress localPublicAddress = InetAddress.getByName(connector.apiCall(PUBLIC_IP_QUERY));
|
||||
logger.info("EC2Snitch using publicIP as identifier: {}", localPublicAddress);
|
||||
localPrivateAddress = awsApiCall(PRIVATE_IP_QUERY_URL);
|
||||
localPrivateAddress = connector.apiCall(PRIVATE_IP_QUERY);
|
||||
// use the Public IP to broadcast Address to other nodes.
|
||||
DatabaseDescriptor.setBroadcastAddress(localPublicAddress);
|
||||
if (DatabaseDescriptor.getBroadcastRpcAddress() == null)
|
||||
|
|
|
|||
|
|
@ -17,15 +17,11 @@
|
|||
*/
|
||||
package org.apache.cassandra.locator;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.apache.cassandra.db.SystemKeyspace;
|
||||
|
|
@ -33,26 +29,56 @@ import org.apache.cassandra.exceptions.ConfigurationException;
|
|||
import org.apache.cassandra.gms.ApplicationState;
|
||||
import org.apache.cassandra.gms.EndpointState;
|
||||
import org.apache.cassandra.gms.Gossiper;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
/**
|
||||
* A snitch that assumes an EC2 region is a DC and an EC2 availability_zone
|
||||
* is a rack. This information is available in the config for the node.
|
||||
* is a rack. This information is available in the config for the node.
|
||||
|
||||
* Since CASSANDRA-16555, it is possible to choose version of AWS IMDS.
|
||||
*
|
||||
* By default, since CASSANDRA-16555, IMDSv2 is used.
|
||||
*
|
||||
* The version of IMDS is driven by property {@link Ec2MetadataServiceConnector#EC2_METADATA_TYPE_PROPERTY} and
|
||||
* can be of value either 'v1' or 'v2'.
|
||||
*
|
||||
* It is possible to specify custom URL of IMDS by {@link Ec2MetadataServiceConnector#EC2_METADATA_URL_PROPERTY}.
|
||||
* A user is not meant to change this under normal circumstances, it is suitable for testing only.
|
||||
*
|
||||
* IMDSv2 is secured by a token which needs to be fetched from IDMSv2 first, and it has to be passed in a header
|
||||
* for the actual queries to IDMSv2. Ec2Snitch is doing this automatically. The only configuration parameter exposed
|
||||
* to a user is {@link Ec2MetadataServiceConnector.V2Connector#AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY}
|
||||
* which is by default set to {@link Ec2MetadataServiceConnector.V2Connector#MAX_TOKEN_TIME_IN_SECONDS}. TTL has
|
||||
* to be an integer from the range [30, 21600].
|
||||
*/
|
||||
public class Ec2Snitch extends AbstractNetworkTopologySnitch
|
||||
{
|
||||
protected static final Logger logger = LoggerFactory.getLogger(Ec2Snitch.class);
|
||||
protected static final String ZONE_NAME_QUERY_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone";
|
||||
@VisibleForTesting
|
||||
public static final String ZONE_NAME_QUERY = "/latest/meta-data/placement/availability-zone";
|
||||
private static final String DEFAULT_DC = "UNKNOWN-DC";
|
||||
private static final String DEFAULT_RACK = "UNKNOWN-RACK";
|
||||
private Map<InetAddress, Map<String, String>> savedEndpoints;
|
||||
protected String ec2zone;
|
||||
protected String ec2region;
|
||||
|
||||
protected final Ec2MetadataServiceConnector connector;
|
||||
|
||||
public Ec2Snitch() throws IOException, ConfigurationException
|
||||
{
|
||||
String az = awsApiCall(ZONE_NAME_QUERY_URL);
|
||||
this(new SnitchProperties());
|
||||
}
|
||||
|
||||
public Ec2Snitch(SnitchProperties props) throws IOException, ConfigurationException
|
||||
{
|
||||
this(props, Ec2MetadataServiceConnector.create(props));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Ec2Snitch(SnitchProperties props, Ec2MetadataServiceConnector connector) throws IOException
|
||||
{
|
||||
this.connector = connector;
|
||||
String az = connector.apiCall(ZONE_NAME_QUERY);
|
||||
// Split "us-east-1a" or "asia-1a" into "us-east"/"1a" and "asia"/"1a".
|
||||
String[] splits = az.split("-");
|
||||
ec2zone = splits[splits.length - 1];
|
||||
|
|
@ -67,31 +93,6 @@ public class Ec2Snitch extends AbstractNetworkTopologySnitch
|
|||
logger.info("EC2Snitch using region: {}, zone: {}.", ec2region, ec2zone);
|
||||
}
|
||||
|
||||
String awsApiCall(String url) throws IOException, ConfigurationException
|
||||
{
|
||||
// Populate the region and zone by introspection, fail if 404 on metadata
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
DataInputStream d = null;
|
||||
try
|
||||
{
|
||||
conn.setRequestMethod("GET");
|
||||
if (conn.getResponseCode() != 200)
|
||||
throw new ConfigurationException("Ec2Snitch was unable to execute the API call. Not an ec2 node?");
|
||||
|
||||
// Read the information. I wish I could say (String) conn.getContent() here...
|
||||
int cl = conn.getContentLength();
|
||||
byte[] b = new byte[cl];
|
||||
d = new DataInputStream((FilterInputStream) conn.getContent());
|
||||
d.readFully(b);
|
||||
return new String(b, StandardCharsets.UTF_8);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FileUtils.close(d);
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public String getRack(InetAddress endpoint)
|
||||
{
|
||||
if (endpoint.equals(FBUtilities.getBroadcastAddress()))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import java.io.InputStream;
|
|||
import java.net.URL;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
|
@ -59,6 +61,12 @@ public class SnitchProperties
|
|||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public SnitchProperties(Properties properties)
|
||||
{
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a snitch property value or return defaultValue if not defined.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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.apache.cassandra.locator;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.locator.Ec2MetadataServiceConnector.EC2MetadataType;
|
||||
import org.apache.cassandra.locator.Ec2MetadataServiceConnector.V1Connector;
|
||||
import org.apache.cassandra.locator.Ec2MetadataServiceConnector.V2Connector;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static org.apache.cassandra.locator.Ec2MetadataServiceConnector.DEFAULT_EC2_METADATA_URL;
|
||||
import static org.apache.cassandra.locator.Ec2MetadataServiceConnector.EC2_METADATA_TYPE_PROPERTY;
|
||||
import static org.apache.cassandra.locator.Ec2MetadataServiceConnector.V2Connector.AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY;
|
||||
import static org.apache.cassandra.locator.Ec2MetadataServiceConnector.V2Connector.MAX_TOKEN_TIME_IN_SECONDS;
|
||||
import static org.apache.cassandra.locator.Ec2MetadataServiceConnector.V2Connector.MIN_TOKEN_TIME_IN_SECONDS;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class Ec2ConnectorTest
|
||||
{
|
||||
@Test
|
||||
public void testV1Configuration()
|
||||
{
|
||||
Properties p = new Properties();
|
||||
p.setProperty(EC2_METADATA_TYPE_PROPERTY, EC2MetadataType.v1.name());
|
||||
Ec2MetadataServiceConnector ec2Connector = Ec2MetadataServiceConnector.create(new SnitchProperties(p));
|
||||
|
||||
assertTrue(ec2Connector instanceof V1Connector);
|
||||
|
||||
assertEquals(DEFAULT_EC2_METADATA_URL, ec2Connector.metadataServiceUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testV2Configuration()
|
||||
{
|
||||
Ec2MetadataServiceConnector ec2Connector = Ec2MetadataServiceConnector.create(new SnitchProperties(new Properties()));
|
||||
|
||||
// v2 connector by default
|
||||
assertTrue(ec2Connector instanceof V2Connector);
|
||||
assertEquals(DEFAULT_EC2_METADATA_URL, ec2Connector.metadataServiceUrl);
|
||||
assertEquals(Duration.ofSeconds(MAX_TOKEN_TIME_IN_SECONDS), ((V2Connector) ec2Connector).tokenTTL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidConfiguration()
|
||||
{
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.describedAs("it should not be possible create a connector of type non-existent")
|
||||
.isThrownBy(() -> {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(EC2_METADATA_TYPE_PROPERTY, "non-existent");
|
||||
Ec2MetadataServiceConnector.create(new SnitchProperties(p));
|
||||
}).withMessage(format("%s must be one of %s", EC2_METADATA_TYPE_PROPERTY,
|
||||
stream(EC2MetadataType.values()).map(Enum::name).collect(joining(", "))));
|
||||
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.describedAs(format("it should not be possible to set %s to more than %s",
|
||||
AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY,
|
||||
MAX_TOKEN_TIME_IN_SECONDS))
|
||||
.isThrownBy(() -> {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(EC2_METADATA_TYPE_PROPERTY, EC2MetadataType.v2.name());
|
||||
p.setProperty(AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY, "50000");
|
||||
|
||||
Ec2MetadataServiceConnector.create(new SnitchProperties(p));
|
||||
}).withMessage(format("property %s was set to 50000 seconds which is not in allowed range of [%s..%s]",
|
||||
AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY,
|
||||
MIN_TOKEN_TIME_IN_SECONDS,
|
||||
MAX_TOKEN_TIME_IN_SECONDS));
|
||||
|
||||
assertThatExceptionOfType(ConfigurationException.class)
|
||||
.describedAs("ttl value should be integer")
|
||||
.isThrownBy(() -> {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(EC2_METADATA_TYPE_PROPERTY, EC2MetadataType.v2.name());
|
||||
p.setProperty(AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY, "abc");
|
||||
|
||||
Ec2MetadataServiceConnector.create(new SnitchProperties(p));
|
||||
}).withMessage(format("Unable to parse integer from property %s, value to parse: abc",
|
||||
AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY));
|
||||
}
|
||||
}
|
||||
|
|
@ -18,15 +18,14 @@
|
|||
|
||||
package org.apache.cassandra.locator;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
|
|
@ -42,11 +41,12 @@ import org.apache.cassandra.net.OutboundTcpConnectionPool;
|
|||
import org.apache.cassandra.service.StorageService;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class EC2SnitchTest
|
||||
public class Ec2SnitchTest
|
||||
{
|
||||
private static String az;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() throws Exception
|
||||
{
|
||||
|
|
@ -58,25 +58,13 @@ public class EC2SnitchTest
|
|||
StorageService.instance.initServer(0);
|
||||
}
|
||||
|
||||
private class TestEC2Snitch extends Ec2Snitch
|
||||
{
|
||||
public TestEC2Snitch() throws IOException, ConfigurationException
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
String awsApiCall(String url) throws IOException, ConfigurationException
|
||||
{
|
||||
return az;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRac() throws IOException, ConfigurationException
|
||||
{
|
||||
az = "us-east-1d";
|
||||
Ec2Snitch snitch = new TestEC2Snitch();
|
||||
Ec2MetadataServiceConnector connectorMock = mock(Ec2MetadataServiceConnector.class);
|
||||
when(connectorMock.apiCall(anyString())).thenReturn("us-east-1d");
|
||||
Ec2Snitch snitch = new Ec2Snitch(new SnitchProperties(new Properties()), connectorMock);
|
||||
|
||||
InetAddress local = InetAddress.getByName("127.0.0.1");
|
||||
InetAddress nonlocal = InetAddress.getByName("127.0.0.7");
|
||||
|
||||
|
|
@ -92,12 +80,13 @@ public class EC2SnitchTest
|
|||
assertEquals("us-east", snitch.getDatacenter(local));
|
||||
assertEquals("1d", snitch.getRack(local));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testNewRegions() throws IOException, ConfigurationException
|
||||
{
|
||||
az = "us-east-2d";
|
||||
Ec2Snitch snitch = new TestEC2Snitch();
|
||||
Ec2MetadataServiceConnector connectorMock = mock(Ec2MetadataServiceConnector.class);
|
||||
when(connectorMock.apiCall(anyString())).thenReturn( "us-east-2d");
|
||||
Ec2Snitch snitch = new Ec2Snitch(new SnitchProperties(new Properties()), connectorMock);
|
||||
InetAddress local = InetAddress.getByName("127.0.0.1");
|
||||
assertEquals("us-east-2", snitch.getDatacenter(local));
|
||||
assertEquals("2d", snitch.getRack(local));
|
||||
|
|
@ -110,13 +99,13 @@ public class EC2SnitchTest
|
|||
InetAddress com_ip = InetAddress.getByName("127.0.0.3");
|
||||
|
||||
OutboundTcpConnectionPool pool = MessagingService.instance().getConnectionPool(me);
|
||||
Assert.assertEquals(me, pool.endPoint());
|
||||
assertEquals(me, pool.endPoint());
|
||||
pool.reset(com_ip);
|
||||
Assert.assertEquals(com_ip, pool.endPoint());
|
||||
assertEquals(com_ip, pool.endPoint());
|
||||
|
||||
MessagingService.instance().destroyConnectionPool(me);
|
||||
pool = MessagingService.instance().getConnectionPool(me);
|
||||
Assert.assertEquals(com_ip, pool.endPoint());
|
||||
assertEquals(com_ip, pool.endPoint());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
Loading…
Reference in New Issue