From 4ea7bb25b4079e951202762aeaabe1d23be5303c Mon Sep 17 00:00:00 2001 From: Stefan Miklosovic Date: Mon, 25 Apr 2022 15:02:03 +0200 Subject: [PATCH] Add support for AWS Ec2 IMDSv2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit patch by Stefan Miklosovic; reviewed by Jacek Lewandowski and Brandon Williams for CASSANDRA-16555 Co-authored-by: Jacek Lewandowski Co-authored-by: Paul Rütter --- CHANGES.txt | 1 + NEWS.txt | 10 + conf/cassandra-rackdc.properties | 6 + ...AbstractCloudMetadataServiceConnector.java | 99 ++++++++ .../locator/Ec2MetadataServiceConnector.java | 214 ++++++++++++++++++ .../locator/Ec2MultiRegionSnitch.java | 27 ++- .../apache/cassandra/locator/Ec2Snitch.java | 69 +++--- .../cassandra/locator/SnitchProperties.java | 8 + .../cassandra/locator/Ec2ConnectorTest.java | 106 +++++++++ ...{EC2SnitchTest.java => Ec2SnitchTest.java} | 43 ++-- 10 files changed, 516 insertions(+), 67 deletions(-) create mode 100644 src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceConnector.java create mode 100644 src/java/org/apache/cassandra/locator/Ec2MetadataServiceConnector.java create mode 100644 test/unit/org/apache/cassandra/locator/Ec2ConnectorTest.java rename test/unit/org/apache/cassandra/locator/{EC2SnitchTest.java => Ec2SnitchTest.java} (81%) diff --git a/CHANGES.txt b/CHANGES.txt index 5a1a609b06..069f446a80 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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) diff --git a/NEWS.txt b/NEWS.txt index f0a88ca7bf..d8210bd0a9 100644 --- a/NEWS.txt +++ b/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 ====== diff --git a/conf/cassandra-rackdc.properties b/conf/cassandra-rackdc.properties index 2ea604384b..96d52376b4 100644 --- a/conf/cassandra-rackdc.properties +++ b/conf/cassandra-rackdc.properties @@ -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 diff --git a/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceConnector.java b/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceConnector.java new file mode 100644 index 0000000000..6210ce40b5 --- /dev/null +++ b/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceConnector.java @@ -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 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; + } + } +} diff --git a/src/java/org/apache/cassandra/locator/Ec2MetadataServiceConnector.java b/src/java/org/apache/cassandra/locator/Ec2MetadataServiceConnector.java new file mode 100644 index 0000000000..f97fee3941 --- /dev/null +++ b/src/java/org/apache/cassandra/locator/Ec2MetadataServiceConnector.java @@ -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 provider; + + EC2MetadataType(Function 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 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 extraHeaders, int expectedResponseCode) throws IOException + { + Map 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. + * IMDSv2 + */ + @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); + } + } + } +} diff --git a/src/java/org/apache/cassandra/locator/Ec2MultiRegionSnitch.java b/src/java/org/apache/cassandra/locator/Ec2MultiRegionSnitch.java index b32ca84e96..9f9aee50e7 100644 --- a/src/java/org/apache/cassandra/locator/Ec2MultiRegionSnitch.java +++ b/src/java/org/apache/cassandra/locator/Ec2MultiRegionSnitch.java @@ -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) diff --git a/src/java/org/apache/cassandra/locator/Ec2Snitch.java b/src/java/org/apache/cassandra/locator/Ec2Snitch.java index 59eb27bb2b..7d691f1ee6 100644 --- a/src/java/org/apache/cassandra/locator/Ec2Snitch.java +++ b/src/java/org/apache/cassandra/locator/Ec2Snitch.java @@ -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> 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())) diff --git a/src/java/org/apache/cassandra/locator/SnitchProperties.java b/src/java/org/apache/cassandra/locator/SnitchProperties.java index 8fdda7a872..79ea301331 100644 --- a/src/java/org/apache/cassandra/locator/SnitchProperties.java +++ b/src/java/org/apache/cassandra/locator/SnitchProperties.java @@ -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. */ diff --git a/test/unit/org/apache/cassandra/locator/Ec2ConnectorTest.java b/test/unit/org/apache/cassandra/locator/Ec2ConnectorTest.java new file mode 100644 index 0000000000..756e13cef3 --- /dev/null +++ b/test/unit/org/apache/cassandra/locator/Ec2ConnectorTest.java @@ -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)); + } +} diff --git a/test/unit/org/apache/cassandra/locator/EC2SnitchTest.java b/test/unit/org/apache/cassandra/locator/Ec2SnitchTest.java similarity index 81% rename from test/unit/org/apache/cassandra/locator/EC2SnitchTest.java rename to test/unit/org/apache/cassandra/locator/Ec2SnitchTest.java index 9d078ce20c..addc9f727d 100644 --- a/test/unit/org/apache/cassandra/locator/EC2SnitchTest.java +++ b/test/unit/org/apache/cassandra/locator/Ec2SnitchTest.java @@ -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