From 903857b4ef01b577db2cbcf3ea9a9b194dede21c Mon Sep 17 00:00:00 2001 From: Stefan Miklosovic Date: Thu, 29 Jun 2023 17:30:02 +0200 Subject: [PATCH] Deprecate CloudstackSnitch and remove duplicate code in snitches The patch also refactors existing cloud snitches to get rid of the duplicate code, this is the logical follow-up of CASSANDRA-16555 where AbstractCloudMetadataServiceConnector was introduced. patch by Stefan Miklosovic; reviewed by Jacek Lewandowski, Jackson Fleming and Maxwell Guo for CASSANDRA-18438 --- CHANGES.txt | 1 + NEWS.txt | 2 + conf/cassandra-rackdc.properties | 15 +- conf/cassandra.yaml | 15 ++ ...AbstractCloudMetadataServiceConnector.java | 71 ++++++++- .../AbstractCloudMetadataServiceSnitch.java | 108 ++++++++++++++ .../cassandra/locator/AlibabaCloudSnitch.java | 137 +++-------------- .../cassandra/locator/CloudstackSnitch.java | 139 +++++------------- .../locator/Ec2MetadataServiceConnector.java | 6 +- .../locator/Ec2MultiRegionSnitch.java | 4 +- .../apache/cassandra/locator/Ec2Snitch.java | 83 +++-------- .../cassandra/locator/GoogleCloudSnitch.java | 109 +++----------- .../cassandra/locator/SnitchProperties.java | 48 ++++++ .../apache/cassandra/locator/SnitchUtils.java | 50 +++++++ .../locator/AlibabaCloudSnitchTest.java | 38 +++-- .../locator/CloudstackSnitchTest.java | 35 ++--- ...aultCloudMetadataServiceConnectorTest.java | 63 ++++++++ .../locator/GoogleCloudSnitchTest.java | 36 ++--- .../cassandra/locator/SnitchUtilsTest.java | 45 ++++++ 19 files changed, 564 insertions(+), 441 deletions(-) create mode 100644 src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceSnitch.java create mode 100644 src/java/org/apache/cassandra/locator/SnitchUtils.java create mode 100644 test/unit/org/apache/cassandra/locator/DefaultCloudMetadataServiceConnectorTest.java create mode 100644 test/unit/org/apache/cassandra/locator/SnitchUtilsTest.java diff --git a/CHANGES.txt b/CHANGES.txt index 371026ce5840..8198ece74cab 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.0 + * Deprecate CloudstackSnitch and remove duplicate code in snitches (CASSANDRA-18438) * Add support for vectors in UDFs (CASSANDRA-18613) * Improve vector value validation errors (CASSANDRA-18652) * Upgrade Guava to 32.0.1 (CASSANDRA-18645) diff --git a/NEWS.txt b/NEWS.txt index 0faea57dfa2e..db77d36eba6a 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -223,6 +223,8 @@ Deprecation for partition tombstones. That guardrail is based on the properties `partition_tombstones_warn_threshold` and `partition_tombstones_fail_threshold`. The warn threshold has a very similar behaviour to the old config property. The old property is still supported for backward compatibility, but now it is disabled by default. + - CloudstackSnitch is marked as deprecated and it is not actively maintained anymore. It is scheduled to be removed + in the next major version of Cassandra. 4.1 === diff --git a/conf/cassandra-rackdc.properties b/conf/cassandra-rackdc.properties index 2d17808d30f1..588d6a70dc6d 100644 --- a/conf/cassandra-rackdc.properties +++ b/conf/cassandra-rackdc.properties @@ -19,8 +19,7 @@ dc=dc1 rack=rack1 -# Add a suffix to a datacenter name. Used by the Ec2Snitch and Ec2MultiRegionSnitch -# to append a string to the EC2 region name. +# Add a suffix to a datacenter name. Used by all cloud-based snitches. #dc_suffix= # Uncomment the following line to make this snitch prefer the internal ip when possible, as the Ec2MultiRegionSnitch does. @@ -43,3 +42,15 @@ rack=rack1 # 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 + +# For all cloud-based snitches, there are following options available: +# +# Property to change metadata service url for a cloud-based snitch. The endpoint of a particular +# snitch will be appended to this property. A user is not normally using this property, it is here +# for tweaking the url of a service itself, e.g. for testing purposes. +# metadata_url=http://some/service +# +# Sets a specified timeout value, in duration format, to be used when opening a communications link to metadata service, +# referenced by an URLConnection. The timeout of zero (0s) is interpreted as an infinite timeout. +# Defaults to 30 seconds. +# metadata_request_timeout=30s diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index b8aa59e2cab7..942812a7e1e1 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -1298,6 +1298,17 @@ slow_query_log_timeout: 500ms # Proximity is determined by rack and data center, which are # explicitly configured in cassandra-topology.properties. # +# AlibabaCloudSnitch: +# Snitch for getting dc and rack of a node from metadata service of Alibaba cloud. +# This snitch that assumes an ECS region is a DC and an ECS availability_zone is a rack. +# +# CloudstackSnitch: +# A snitch that assumes a Cloudstack Zone follows the typical convention +# country-location-az and uses a country/location tuple as a datacenter +# and the availability zone as a rack. +# WARNING: This snitch is deprecated and it is scheduled to be removed +# in the next major version of Cassandra. +# # Ec2Snitch: # Appropriate for EC2 deployments in a single Region. Loads Region # and Availability Zone information from the EC2 API. The Region is @@ -1313,6 +1324,10 @@ slow_query_log_timeout: 500ms # traffic, Cassandra will switch to the private IP after # establishing a connection.) # +# GoogleCloudSnitch: +# Snitch for getting dc and rack of a node from metadata service of Google cloud. +# This snitch that assumes an GCE region is a DC and an GCE availability_zone is a rack. +# # RackInferringSnitch: # Proximity is determined by rack and data center, which are # assumed to correspond to the 3rd and 2nd octet of each node's IP diff --git a/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceConnector.java b/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceConnector.java index 6210ce40b5ae..026ab4f2b782 100644 --- a/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceConnector.java +++ b/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceConnector.java @@ -22,30 +22,72 @@ import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.util.Map; import com.google.common.collect.ImmutableMap; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DurationSpec; +import org.apache.cassandra.exceptions.ConfigurationException; + +import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; abstract class AbstractCloudMetadataServiceConnector { + static final String METADATA_URL_PROPERTY = "metadata_url"; + static final String METADATA_REQUEST_TIMEOUT_PROPERTY = "metadata_request_timeout"; + static final String DEFAULT_METADATA_REQUEST_TIMEOUT = "30s"; + protected final String metadataServiceUrl; + protected final int requestTimeoutMs; - protected AbstractCloudMetadataServiceConnector(String metadataServiceUrl) + public AbstractCloudMetadataServiceConnector(SnitchProperties properties) { - this.metadataServiceUrl = metadataServiceUrl; + String parsedMetadataServiceUrl = properties.get(METADATA_URL_PROPERTY, null); + + try + { + URL url = new URL(parsedMetadataServiceUrl); + url.toURI(); + + this.metadataServiceUrl = parsedMetadataServiceUrl; + } + catch (MalformedURLException | IllegalArgumentException | URISyntaxException ex) + { + throw new ConfigurationException(format("Snitch metadata service URL '%s' is invalid. Please review snitch properties " + + "defined in the configured '%s' configuration file.", + parsedMetadataServiceUrl, + CassandraRelevantProperties.CASSANDRA_RACKDC_PROPERTIES.getKey()), + ex); + } + + + String metadataRequestTimeout = properties.get(METADATA_REQUEST_TIMEOUT_PROPERTY, DEFAULT_METADATA_REQUEST_TIMEOUT); + + try + { + this.requestTimeoutMs = new DurationSpec.IntMillisecondsBound(metadataRequestTimeout).toMilliseconds(); + } + catch (IllegalArgumentException ex) + { + throw new ConfigurationException(format("%s as value of %s is invalid duration! " + ex.getMessage(), + metadataRequestTimeout, + METADATA_REQUEST_TIMEOUT_PROPERTY)); + } } - public String apiCall(String query) throws IOException + public final String apiCall(String query) throws IOException { - return apiCall(metadataServiceUrl, query, 200); + return apiCall(metadataServiceUrl, query, "GET", ImmutableMap.of(), 200); } - public String apiCall(String url, String query, int expectedResponseCode) throws IOException + public final String apiCall(String query, Map extraHeaders) throws IOException { - return apiCall(url, query, "GET", ImmutableMap.of(), expectedResponseCode); + return apiCall(metadataServiceUrl, query, "GET", extraHeaders, 200); } public String apiCall(String url, @@ -61,6 +103,7 @@ public String apiCall(String url, conn = (HttpURLConnection) new URL(url + query).openConnection(); extraHeaders.forEach(conn::setRequestProperty); conn.setRequestMethod(method); + conn.setConnectTimeout(requestTimeoutMs); if (conn.getResponseCode() != expectedResponseCode) throw new HttpException(conn.getResponseCode(), conn.getResponseMessage()); @@ -84,6 +127,14 @@ public String apiCall(String url, } } + @Override + public String toString() + { + return format("%s{%s=%s,%s=%s}", getClass().getName(), + METADATA_URL_PROPERTY, metadataServiceUrl, + METADATA_REQUEST_TIMEOUT_PROPERTY, requestTimeoutMs); + } + public static final class HttpException extends IOException { public final int responseCode; @@ -96,4 +147,12 @@ public HttpException(int responseCode, String responseMessage) this.responseMessage = responseMessage; } } + + public static class DefaultCloudMetadataServiceConnector extends AbstractCloudMetadataServiceConnector + { + public DefaultCloudMetadataServiceConnector(SnitchProperties properties) + { + super(properties); + } + } } diff --git a/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceSnitch.java b/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceSnitch.java new file mode 100644 index 000000000000..2e34f7839313 --- /dev/null +++ b/src/java/org/apache/cassandra/locator/AbstractCloudMetadataServiceSnitch.java @@ -0,0 +1,108 @@ +/* + * 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.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; + +import static java.lang.String.format; + +abstract class AbstractCloudMetadataServiceSnitch extends AbstractNetworkTopologySnitch +{ + static final Logger logger = LoggerFactory.getLogger(AbstractCloudMetadataServiceSnitch.class); + + static final String DEFAULT_DC = "UNKNOWN-DC"; + static final String DEFAULT_RACK = "UNKNOWN-RACK"; + + protected final AbstractCloudMetadataServiceConnector connector; + protected final SnitchProperties snitchProperties; + + private final String localRack; + private final String localDc; + + private Map> savedEndpoints; + + public AbstractCloudMetadataServiceSnitch(AbstractCloudMetadataServiceConnector connector, + SnitchProperties snitchProperties, + Pair dcAndRack) + { + this.connector = connector; + this.snitchProperties = snitchProperties; + this.localDc = dcAndRack.left; + this.localRack = dcAndRack.right; + + logger.info(format("%s using datacenter: %s, rack: %s, connector: %s, properties: %s", + getClass().getName(), getLocalDatacenter(), getLocalRack(), connector, snitchProperties)); + } + + @Override + public final String getLocalRack() + { + return localRack; + } + + @Override + public final String getLocalDatacenter() + { + return localDc; + } + + @Override + public final String getRack(InetAddressAndPort endpoint) + { + if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) + return getLocalRack(); + EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); + if (state == null || state.getApplicationState(ApplicationState.RACK) == null) + { + if (savedEndpoints == null) + savedEndpoints = SystemKeyspace.loadDcRackInfo(); + if (savedEndpoints.containsKey(endpoint)) + return savedEndpoints.get(endpoint).get("rack"); + return DEFAULT_RACK; + } + return state.getApplicationState(ApplicationState.RACK).value; + } + + @Override + public final String getDatacenter(InetAddressAndPort endpoint) + { + if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) + return getLocalDatacenter(); + EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); + if (state == null || state.getApplicationState(ApplicationState.DC) == null) + { + if (savedEndpoints == null) + savedEndpoints = SystemKeyspace.loadDcRackInfo(); + if (savedEndpoints.containsKey(endpoint)) + return savedEndpoints.get(endpoint).get("data_center"); + return DEFAULT_DC; + } + return state.getApplicationState(ApplicationState.DC).value; + } +} diff --git a/src/java/org/apache/cassandra/locator/AlibabaCloudSnitch.java b/src/java/org/apache/cassandra/locator/AlibabaCloudSnitch.java index 729e1b376393..2c76bf514382 100644 --- a/src/java/org/apache/cassandra/locator/AlibabaCloudSnitch.java +++ b/src/java/org/apache/cassandra/locator/AlibabaCloudSnitch.java @@ -17,130 +17,41 @@ */ package org.apache.cassandra.locator; -import java.io.DataInputStream; -import java.io.FilterInputStream; import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import org.apache.cassandra.db.SystemKeyspace; -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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableMap; + +import org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.DefaultCloudMetadataServiceConnector; + +import static org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.METADATA_URL_PROPERTY; /** - * A snitch that assumes an ECS region is a DC and an ECS availability_zone - * is a rack. This information is available in the config for the node. the - * format of the zone-id is like :cn-hangzhou-a where cn means china, hangzhou - * means the hangzhou region, a means the az id. We use cn-hangzhou as the dc, - * and f as the zone-id. + * A snitch that assumes an ECS region is a DC and an ECS availability_zone + * is a rack. This information is available in the config for the node. the + * format of the zone-id is like 'cn-hangzhou-a' where cn means china, hangzhou + * means the hangzhou region, a means the az id. We use 'cn-hangzhou' as the dc, + * and 'a' as the zone-id. */ -public class AlibabaCloudSnitch extends AbstractNetworkTopologySnitch +public class AlibabaCloudSnitch extends AbstractCloudMetadataServiceSnitch { - protected static final Logger logger = LoggerFactory.getLogger(AlibabaCloudSnitch.class); - protected static final String ZONE_NAME_QUERY_URL = "http://100.100.100.200/latest/meta-data/zone-id"; - private static final String DEFAULT_DC = "UNKNOWN-DC"; - private static final String DEFAULT_RACK = "UNKNOWN-RACK"; - private Map> savedEndpoints; - protected String ecsZone; - protected String ecsRegion; - - private static final int HTTP_CONNECT_TIMEOUT = 30000; - - - public AlibabaCloudSnitch() throws MalformedURLException, IOException - { - String response = alibabaApiCall(ZONE_NAME_QUERY_URL); - String[] splits = response.split("/"); - String az = splits[splits.length - 1]; - - // Split "us-central1-a" or "asia-east1-a" into "us-central1"/"a" and "asia-east1"/"a". - splits = az.split("-"); - ecsZone = splits[splits.length - 1]; + static final String DEFAULT_METADATA_SERVICE_URL = "http://100.100.100.200"; + static final String ZONE_NAME_QUERY_URL = "/latest/meta-data/zone-id"; - int lastRegionIndex = az.lastIndexOf("-"); - ecsRegion = az.substring(0, lastRegionIndex); - - String datacenterSuffix = (new SnitchProperties()).get("dc_suffix", ""); - ecsRegion = ecsRegion.concat(datacenterSuffix); - logger.info("AlibabaSnitch using region: {}, zone: {}.", ecsRegion, ecsZone); - - } - - String alibabaApiCall(String url) throws ConfigurationException, IOException, SocketTimeoutException + public AlibabaCloudSnitch() throws IOException { - // Populate the region and zone by introspection, fail if 404 on metadata - HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); - DataInputStream d = null; - try - { - conn.setConnectTimeout(HTTP_CONNECT_TIMEOUT); - conn.setRequestMethod("GET"); - - int code = conn.getResponseCode(); - if (code != HttpURLConnection.HTTP_OK) - throw new ConfigurationException("AlibabaSnitch was unable to execute the API call. Not an ecs node? and the returun code is " + code); - - // 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); - } - catch (SocketTimeoutException e) - { - throw new SocketTimeoutException("Timeout occurred reading a response from the Alibaba ECS metadata"); - } - finally - { - FileUtils.close(d); - conn.disconnect(); - } + this(new SnitchProperties()); } - - @Override - public String getRack(InetAddressAndPort endpoint) + + public AlibabaCloudSnitch(SnitchProperties properties) throws IOException { - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - return ecsZone; - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null || state.getApplicationState(ApplicationState.RACK) == null) - { - if (savedEndpoints == null) - savedEndpoints = SystemKeyspace.loadDcRackInfo(); - if (savedEndpoints.containsKey(endpoint)) - return savedEndpoints.get(endpoint).get("rack"); - return DEFAULT_RACK; - } - return state.getApplicationState(ApplicationState.RACK).value; - + this(properties, new DefaultCloudMetadataServiceConnector(properties.putIfAbsent(METADATA_URL_PROPERTY, + DEFAULT_METADATA_SERVICE_URL))); } - @Override - public String getDatacenter(InetAddressAndPort endpoint) + public AlibabaCloudSnitch(SnitchProperties properties, AbstractCloudMetadataServiceConnector connector) throws IOException { - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - return ecsRegion; - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null || state.getApplicationState(ApplicationState.DC) == null) - { - if (savedEndpoints == null) - savedEndpoints = SystemKeyspace.loadDcRackInfo(); - if (savedEndpoints.containsKey(endpoint)) - return savedEndpoints.get(endpoint).get("data_center"); - return DEFAULT_DC; - } - return state.getApplicationState(ApplicationState.DC).value; - + super(connector, properties, SnitchUtils.parseDcAndRack(connector.apiCall(ZONE_NAME_QUERY_URL, + ImmutableMap.of()), + properties.getDcSuffix())); } - } diff --git a/src/java/org/apache/cassandra/locator/CloudstackSnitch.java b/src/java/org/apache/cassandra/locator/CloudstackSnitch.java index d8579534fb58..a0c4947baebf 100644 --- a/src/java/org/apache/cassandra/locator/CloudstackSnitch.java +++ b/src/java/org/apache/cassandra/locator/CloudstackSnitch.java @@ -17,140 +17,79 @@ */ package org.apache.cassandra.locator; -import java.io.DataInputStream; -import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.regex.Pattern; +import java.util.Properties; import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileReader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.SystemKeyspace; -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; import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.Pair; + +import static org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.METADATA_URL_PROPERTY; +import static org.apache.cassandra.locator.CloudstackSnitch.CloudstackConnector.ZONE_NAME_QUERY_URI; /** - * {@code * A snitch that assumes a Cloudstack Zone follows the typical convention - * -- and uses the country/location + * {@code country-location-availability zone} and uses the country/location * tuple as a datacenter and the availability zone as a rack - * } + * + * This snitch is deprecated, and it is eligible for the removal in the next major release of Cassandra. */ - -public class CloudstackSnitch extends AbstractNetworkTopologySnitch +@Deprecated +public class CloudstackSnitch extends AbstractCloudMetadataServiceSnitch { - protected static final Logger logger = LoggerFactory.getLogger(CloudstackSnitch.class); - protected static final String ZONE_NAME_QUERY_URI = "/latest/meta-data/availability-zone"; - - private Map> savedEndpoints; - - private static final String DEFAULT_DC = "UNKNOWN-DC"; - private static final String DEFAULT_RACK = "UNKNOWN-RACK"; private static final String[] LEASE_FILES = { - "file:///var/lib/dhcp/dhclient.eth0.leases", - "file:///var/lib/dhclient/dhclient.eth0.leases" + "file:///var/lib/dhcp/dhclient.eth0.leases", + "file:///var/lib/dhclient/dhclient.eth0.leases" }; - protected String csZoneDc; - protected String csZoneRack; - - public CloudstackSnitch() throws IOException, ConfigurationException + public CloudstackSnitch() throws IOException { - String endpoint = csMetadataEndpoint(); - String zone = csQueryMetadata(endpoint + ZONE_NAME_QUERY_URI); - String zone_parts[] = zone.split("-"); + this(new SnitchProperties(new Properties())); + } - if (zone_parts.length != 3) - { - throw new ConfigurationException("CloudstackSnitch cannot handle invalid zone format: " + zone); - } - csZoneDc = zone_parts[0] + "-" + zone_parts[1]; - csZoneRack = zone_parts[2]; + public CloudstackSnitch(SnitchProperties snitchProperties) throws IOException + { + this(snitchProperties, new CloudstackConnector(snitchProperties.putIfAbsent(METADATA_URL_PROPERTY, csMetadataEndpoint()))); } - public String getRack(InetAddressAndPort endpoint) + public CloudstackSnitch(SnitchProperties properties, AbstractCloudMetadataServiceConnector connector) throws IOException { - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - return csZoneRack; - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null || state.getApplicationState(ApplicationState.RACK) == null) - { - if (savedEndpoints == null) - savedEndpoints = SystemKeyspace.loadDcRackInfo(); - if (savedEndpoints.containsKey(endpoint)) - return savedEndpoints.get(endpoint).get("rack"); - return DEFAULT_RACK; - } - return state.getApplicationState(ApplicationState.RACK).value; + super(connector, properties, resolveDcAndRack(connector)); + logger.warn("{} is deprecated and not actively maintained. It will be removed in the next " + + "major version of Cassandra.", CloudstackSnitch.class.getName()); } - public String getDatacenter(InetAddressAndPort endpoint) + static class CloudstackConnector extends AbstractCloudMetadataServiceConnector { - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - return csZoneDc; - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null || state.getApplicationState(ApplicationState.DC) == null) + static final String ZONE_NAME_QUERY_URI = "/latest/meta-data/availability-zone"; + + protected CloudstackConnector(SnitchProperties properties) { - if (savedEndpoints == null) - savedEndpoints = SystemKeyspace.loadDcRackInfo(); - if (savedEndpoints.containsKey(endpoint)) - return savedEndpoints.get(endpoint).get("data_center"); - return DEFAULT_DC; + super(properties); } - return state.getApplicationState(ApplicationState.DC).value; } - String csQueryMetadata(String url) throws ConfigurationException, IOException + private static Pair resolveDcAndRack(AbstractCloudMetadataServiceConnector connector) throws IOException { - HttpURLConnection conn = null; - DataInputStream is = null; + String zone = connector.apiCall(ZONE_NAME_QUERY_URI); + String[] zoneParts = zone.split("-"); - try - { - conn = (HttpURLConnection) new URL(url).openConnection(); - } - catch (Exception e) - { - throw new ConfigurationException("CloudstackSnitch cannot query wrong metadata URL: " + url); - } - try - { - conn.setRequestMethod("GET"); - if (conn.getResponseCode() != 200) - { - throw new ConfigurationException("CloudstackSnitch was unable to query metadata."); - } + if (zoneParts.length != 3) + throw new ConfigurationException("CloudstackSnitch cannot handle invalid zone format: " + zone); - int cl = conn.getContentLength(); - byte[] b = new byte[cl]; - is = new DataInputStream(new BufferedInputStream(conn.getInputStream())); - is.readFully(b); - return new String(b, StandardCharsets.UTF_8); - } - finally - { - FileUtils.close(is); - conn.disconnect(); - } + return Pair.create(zoneParts[0] + '-' + zoneParts[1], zoneParts[2]); } - String csMetadataEndpoint() throws ConfigurationException + private static String csMetadataEndpoint() throws ConfigurationException { - for (String lease_uri: LEASE_FILES) + for (String lease_uri : LEASE_FILES) { try { @@ -163,15 +102,13 @@ String csMetadataEndpoint() throws ConfigurationException catch (Exception e) { JVMStabilityInspector.inspectThrowable(e); - continue; } - } throw new ConfigurationException("No valid DHCP lease file could be found."); } - String csEndpointFromLease(File lease) throws ConfigurationException + private static String csEndpointFromLease(File lease) throws ConfigurationException { String line; String endpoint = null; diff --git a/src/java/org/apache/cassandra/locator/Ec2MetadataServiceConnector.java b/src/java/org/apache/cassandra/locator/Ec2MetadataServiceConnector.java index b5ab2b75d782..f975ca95d6f0 100644 --- a/src/java/org/apache/cassandra/locator/Ec2MetadataServiceConnector.java +++ b/src/java/org/apache/cassandra/locator/Ec2MetadataServiceConnector.java @@ -45,7 +45,7 @@ abstract class Ec2MetadataServiceConnector extends AbstractCloudMetadataServiceC Ec2MetadataServiceConnector(SnitchProperties properties) { - super(properties.get(EC2_METADATA_URL_PROPERTY, DEFAULT_EC2_METADATA_URL)); + super(properties.putIfAbsent(METADATA_URL_PROPERTY, properties.get(EC2_METADATA_URL_PROPERTY, DEFAULT_EC2_METADATA_URL))); } enum EC2MetadataType @@ -94,7 +94,7 @@ static V1Connector create(SnitchProperties props) @Override public String toString() { - return String.format("%s{%s=%s}", V1Connector.class.getName(), EC2_METADATA_URL_PROPERTY, metadataServiceUrl); + return String.format("%s{%s=%s}", V1Connector.class.getName(), METADATA_URL_PROPERTY, metadataServiceUrl); } } @@ -184,7 +184,7 @@ public String toString() { return String.format("%s{%s=%s,%s=%s}", V2Connector.class.getName(), - EC2_METADATA_URL_PROPERTY, metadataServiceUrl, + METADATA_URL_PROPERTY, metadataServiceUrl, AWS_EC2_METADATA_TOKEN_TTL_SECONDS_HEADER_PROPERTY, tokenTTL.getSeconds()); } diff --git a/src/java/org/apache/cassandra/locator/Ec2MultiRegionSnitch.java b/src/java/org/apache/cassandra/locator/Ec2MultiRegionSnitch.java index 1ddb2b896211..b4dcfcae6510 100644 --- a/src/java/org/apache/cassandra/locator/Ec2MultiRegionSnitch.java +++ b/src/java/org/apache/cassandra/locator/Ec2MultiRegionSnitch.java @@ -58,7 +58,7 @@ public Ec2MultiRegionSnitch(SnitchProperties props) throws IOException, Configur this(props, Ec2MetadataServiceConnector.create(props)); } - Ec2MultiRegionSnitch(SnitchProperties props, Ec2MetadataServiceConnector connector) throws IOException + Ec2MultiRegionSnitch(SnitchProperties props, AbstractCloudMetadataServiceConnector connector) throws IOException { super(props, connector); InetAddress localPublicAddress = InetAddress.getByName(connector.apiCall(PUBLIC_IP_QUERY)); @@ -88,6 +88,6 @@ public void gossiperStarting() } Gossiper.instance.addLocalApplicationState(ApplicationState.INTERNAL_ADDRESS_AND_PORT, StorageService.instance.valueFactory.internalAddressAndPort(address)); Gossiper.instance.addLocalApplicationState(ApplicationState.INTERNAL_IP, StorageService.instance.valueFactory.internalIP(address.getAddress())); - Gossiper.instance.register(new ReconnectableSnitchHelper(this, ec2region, true)); + Gossiper.instance.register(new ReconnectableSnitchHelper(this, getLocalDatacenter(), true)); } } diff --git a/src/java/org/apache/cassandra/locator/Ec2Snitch.java b/src/java/org/apache/cassandra/locator/Ec2Snitch.java index bf72a5f75dba..056a265ada14 100644 --- a/src/java/org/apache/cassandra/locator/Ec2Snitch.java +++ b/src/java/org/apache/cassandra/locator/Ec2Snitch.java @@ -18,19 +18,12 @@ package org.apache.cassandra.locator; import java.io.IOException; -import java.util.Map; import java.util.Set; import com.google.common.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.SystemKeyspace; 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.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; /** * A snitch that assumes an EC2 region is a DC and an EC2 availability_zone @@ -52,10 +45,8 @@ * 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 +public class Ec2Snitch extends AbstractCloudMetadataServiceSnitch { - protected static final Logger logger = LoggerFactory.getLogger(Ec2Snitch.class); - private static final String SNITCH_PROP_NAMING_SCHEME = "ec2_naming_scheme"; static final String EC2_NAMING_LEGACY = "legacy"; private static final String EC2_NAMING_STANDARD = "standard"; @@ -63,17 +54,8 @@ public class Ec2Snitch extends AbstractNetworkTopologySnitch @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"; - - final String ec2region; - private final String ec2zone; private final boolean usingLegacyNaming; - private Map> savedEndpoints; - - protected final Ec2MetadataServiceConnector connector; - public Ec2Snitch() throws IOException, ConfigurationException { this(new SnitchProperties()); @@ -84,20 +66,27 @@ public Ec2Snitch(SnitchProperties props) throws IOException, ConfigurationExcept this(props, Ec2MetadataServiceConnector.create(props)); } - Ec2Snitch(SnitchProperties props, Ec2MetadataServiceConnector connector) throws IOException + Ec2Snitch(SnitchProperties props, AbstractCloudMetadataServiceConnector connector) throws IOException + { + super(connector, props, getDcAndRack(props, connector)); + usingLegacyNaming = isUsingLegacyNaming(props); + } + + private static Pair getDcAndRack(SnitchProperties props, AbstractCloudMetadataServiceConnector connector) throws IOException { - this.connector = connector; String az = connector.apiCall(ZONE_NAME_QUERY); // if using the full naming scheme, region name is created by removing letters from the // end of the availability zone and zone is the full zone name - usingLegacyNaming = isUsingLegacyNaming(props); + boolean usingLegacyNaming = isUsingLegacyNaming(props); String region; + String localDc; + String localRack; if (usingLegacyNaming) { // Split "us-east-1a" or "asia-1a" into "us-east"/"1a" and "asia"/"1a". String[] splits = az.split("-"); - ec2zone = splits[splits.length - 1]; + localRack = splits[splits.length - 1]; // hack for CASSANDRA-4026 region = az.substring(0, az.length() - 1); @@ -109,12 +98,12 @@ public Ec2Snitch(SnitchProperties props) throws IOException, ConfigurationExcept // grab the region name, which is embedded in the availability zone name. // thus an AZ of "us-east-1a" yields the region name "us-east-1" region = az.replaceFirst("[a-z]+$",""); - ec2zone = az; + localRack = az; } - String datacenterSuffix = props.get("dc_suffix", ""); - ec2region = region.concat(datacenterSuffix); - logger.info("EC2Snitch using region: {}, zone: {}, properties: {}", ec2region, ec2zone, connector); + localDc = region.concat(props.getDcSuffix()); + + return Pair.create(localDc, localRack); } private static boolean isUsingLegacyNaming(SnitchProperties props) @@ -122,38 +111,6 @@ private static boolean isUsingLegacyNaming(SnitchProperties props) return props.get(SNITCH_PROP_NAMING_SCHEME, EC2_NAMING_STANDARD).equalsIgnoreCase(EC2_NAMING_LEGACY); } - public String getRack(InetAddressAndPort endpoint) - { - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - return ec2zone; - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null || state.getApplicationState(ApplicationState.RACK) == null) - { - if (savedEndpoints == null) - savedEndpoints = SystemKeyspace.loadDcRackInfo(); - if (savedEndpoints.containsKey(endpoint)) - return savedEndpoints.get(endpoint).get("rack"); - return DEFAULT_RACK; - } - return state.getApplicationState(ApplicationState.RACK).value; - } - - public String getDatacenter(InetAddressAndPort endpoint) - { - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - return ec2region; - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null || state.getApplicationState(ApplicationState.DC) == null) - { - if (savedEndpoints == null) - savedEndpoints = SystemKeyspace.loadDcRackInfo(); - if (savedEndpoints.containsKey(endpoint)) - return savedEndpoints.get(endpoint).get("data_center"); - return DEFAULT_DC; - } - return state.getApplicationState(ApplicationState.DC).value; - } - @Override public boolean validate(Set datacenters, Set racks) { @@ -179,7 +136,10 @@ static boolean validate(Set datacenters, Set racks, boolean usin // We can still identify as legacy the dc names without a number as a suffix like us-east" boolean dcUsesLegacyFormat = dc.matches("^[a-z]+-[a-z]+$"); if (dcUsesLegacyFormat && !usingLegacyNaming) + { valid = false; + break; + } } for (String rack : racks) @@ -190,7 +150,10 @@ static boolean validate(Set datacenters, Set racks, boolean usin // NOTE: the allowed custom suffix only applies to datacenter (region) names, not availability zones. boolean rackUsesLegacyFormat = rack.matches("[\\d][a-z]"); if (rackUsesLegacyFormat != usingLegacyNaming) + { valid = false; + break; + } } if (!valid) diff --git a/src/java/org/apache/cassandra/locator/GoogleCloudSnitch.java b/src/java/org/apache/cassandra/locator/GoogleCloudSnitch.java index 1e1c5007918d..4472631e2c75 100644 --- a/src/java/org/apache/cassandra/locator/GoogleCloudSnitch.java +++ b/src/java/org/apache/cassandra/locator/GoogleCloudSnitch.java @@ -17,111 +17,38 @@ */ package org.apache.cassandra.locator; -import java.io.DataInputStream; -import java.io.FilterInputStream; import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.SystemKeyspace; -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; +import com.google.common.collect.ImmutableMap; + +import org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.DefaultCloudMetadataServiceConnector; + +import static org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.METADATA_URL_PROPERTY; /** * A snitch that assumes an GCE region is a DC and an GCE 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. */ -public class GoogleCloudSnitch extends AbstractNetworkTopologySnitch +public class GoogleCloudSnitch extends AbstractCloudMetadataServiceSnitch { - protected static final Logger logger = LoggerFactory.getLogger(GoogleCloudSnitch.class); - protected static final String ZONE_NAME_QUERY_URL = "http://metadata.google.internal/computeMetadata/v1/instance/zone"; - private static final String DEFAULT_DC = "UNKNOWN-DC"; - private static final String DEFAULT_RACK = "UNKNOWN-RACK"; - private Map> savedEndpoints; - protected String gceZone; - protected String gceRegion; + static final String DEFAULT_METADATA_SERVICE_URL = "http://metadata.google.internal"; + static final String ZONE_NAME_QUERY_URL = "/computeMetadata/v1/instance/zone"; - public GoogleCloudSnitch() throws IOException, ConfigurationException + public GoogleCloudSnitch() throws IOException { - String response = gceApiCall(ZONE_NAME_QUERY_URL); - String[] splits = response.split("/"); - String az = splits[splits.length - 1]; - - // Split "us-central1-a" or "asia-east1-a" into "us-central1"/"a" and "asia-east1"/"a". - splits = az.split("-"); - gceZone = splits[splits.length - 1]; - - int lastRegionIndex = az.lastIndexOf("-"); - gceRegion = az.substring(0, lastRegionIndex); - - String datacenterSuffix = (new SnitchProperties()).get("dc_suffix", ""); - gceRegion = gceRegion.concat(datacenterSuffix); - logger.info("GCESnitch using region: {}, zone: {}.", gceRegion, gceZone); - } - - String gceApiCall(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"); - conn.setRequestProperty("Metadata-Flavor", "Google"); - if (conn.getResponseCode() != 200) - throw new ConfigurationException("GoogleCloudSnitch was unable to execute the API call. Not a gce node?"); - - // Read the information. - 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(); - } + this(new SnitchProperties()); } - public String getRack(InetAddressAndPort endpoint) + public GoogleCloudSnitch(SnitchProperties properties) throws IOException { - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - return gceZone; - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null || state.getApplicationState(ApplicationState.RACK) == null) - { - if (savedEndpoints == null) - savedEndpoints = SystemKeyspace.loadDcRackInfo(); - if (savedEndpoints.containsKey(endpoint)) - return savedEndpoints.get(endpoint).get("rack"); - return DEFAULT_RACK; - } - return state.getApplicationState(ApplicationState.RACK).value; + this(properties, new DefaultCloudMetadataServiceConnector(properties.putIfAbsent(METADATA_URL_PROPERTY, + DEFAULT_METADATA_SERVICE_URL))); } - public String getDatacenter(InetAddressAndPort endpoint) + public GoogleCloudSnitch(SnitchProperties properties, AbstractCloudMetadataServiceConnector connector) throws IOException { - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - return gceRegion; - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null || state.getApplicationState(ApplicationState.DC) == null) - { - if (savedEndpoints == null) - savedEndpoints = SystemKeyspace.loadDcRackInfo(); - if (savedEndpoints.containsKey(endpoint)) - return savedEndpoints.get(endpoint).get("data_center"); - return DEFAULT_DC; - } - return state.getApplicationState(ApplicationState.DC).value; + super(connector, properties, SnitchUtils.parseDcAndRack(connector.apiCall(ZONE_NAME_QUERY_URL, + ImmutableMap.of("Metadata-Flavor", "Google")), + properties.getDcSuffix())); } } diff --git a/src/java/org/apache/cassandra/locator/SnitchProperties.java b/src/java/org/apache/cassandra/locator/SnitchProperties.java index efd29300aca0..1745b796506d 100644 --- a/src/java/org/apache/cassandra/locator/SnitchProperties.java +++ b/src/java/org/apache/cassandra/locator/SnitchProperties.java @@ -24,6 +24,8 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.Pair; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,6 +71,14 @@ public SnitchProperties(Properties properties) this.properties = properties; } + @SafeVarargs + public SnitchProperties(Pair... pairs) + { + properties = new Properties(); + for (Pair pair : pairs) + properties.setProperty(pair.left, pair.right); + } + /** * Get a snitch property value or return defaultValue if not defined. */ @@ -77,8 +87,46 @@ public String get(String propertyName, String defaultValue) return properties.getProperty(propertyName, defaultValue); } + public SnitchProperties add(String key, String value) + { + properties.put(key, value); + return this; + } + + /** + * Returns this instance of snitch properties if key is present + * otherwise create new instance of properties and put key with a give value into it + * + * @param key key to add + * @param value value to add + * @return same properties if key is present or new object with added key and value if not + */ + public SnitchProperties putIfAbsent(String key, String value) + { + if (contains(key)) + return this; + + Properties p = new Properties(); + p.putAll(this.properties); + p.put(key, value); + return new SnitchProperties(p); + } + public boolean contains(String propertyName) { return properties.containsKey(propertyName); } + + public String getDcSuffix() + { + return properties.getProperty("dc_suffix", ""); + } + + @Override + public String toString() + { + return "SnitchProperties{" + + "properties=" + (properties != null ? properties.toString() : "null") + + '}'; + } } diff --git a/src/java/org/apache/cassandra/locator/SnitchUtils.java b/src/java/org/apache/cassandra/locator/SnitchUtils.java new file mode 100644 index 000000000000..65c1f28c0641 --- /dev/null +++ b/src/java/org/apache/cassandra/locator/SnitchUtils.java @@ -0,0 +1,50 @@ +/* + * 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 org.apache.cassandra.utils.Pair; + +import static java.lang.String.format; + +public class SnitchUtils +{ + private SnitchUtils() {} + + static Pair parseDcAndRack(String response, String dcSuffix) + { + String[] splits = response.split("/"); + String az = splits[splits.length - 1]; + + splits = az.split("-"); + String localRack = splits[splits.length - 1]; + + int lastRegionIndex = az.lastIndexOf('-'); + + // we would hit StringIndexOutOfBoundsException on the az.substring method if we did not do this + if (lastRegionIndex == -1) + throw new IllegalStateException(format("%s does not contain at least one '-' to differentiate " + + "between datacenter and rack", response)); + + String localDc = az.substring(0, lastRegionIndex); + + localDc = localDc.concat(dcSuffix); + + return Pair.create(localDc, localRack); + } +} diff --git a/test/unit/org/apache/cassandra/locator/AlibabaCloudSnitchTest.java b/test/unit/org/apache/cassandra/locator/AlibabaCloudSnitchTest.java index 82108f29c65e..f7330098304c 100644 --- a/test/unit/org/apache/cassandra/locator/AlibabaCloudSnitchTest.java +++ b/test/unit/org/apache/cassandra/locator/AlibabaCloudSnitchTest.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.EnumMap; import java.util.Map; +import java.util.Properties; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -38,10 +39,14 @@ import static org.apache.cassandra.ServerTestUtils.mkdirs; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -public class AlibabaCloudSnitchTest +public class AlibabaCloudSnitchTest { - private static String az; + static String az; @BeforeClass public static void setup() throws Exception @@ -56,25 +61,15 @@ public static void setup() throws Exception StorageService.instance.initServer(0); } - private class TestAlibabaCloudSnitch extends AlibabaCloudSnitch - { - public TestAlibabaCloudSnitch() throws IOException, ConfigurationException - { - super(); - } - - @Override - String alibabaApiCall(String url) throws IOException, ConfigurationException - { - return az; - } - } - @Test public void testRac() throws IOException, ConfigurationException { az = "cn-hangzhou-f"; - AlibabaCloudSnitch snitch = new TestAlibabaCloudSnitch(); + + AbstractCloudMetadataServiceConnector mock = mock(AbstractCloudMetadataServiceConnector.class); + when(mock.apiCall(any(), anyMap())).thenReturn(az); + + AlibabaCloudSnitch snitch = new AlibabaCloudSnitch(new SnitchProperties(new Properties()), mock); InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1"); InetAddressAndPort nonlocal = InetAddressAndPort.getByName("127.0.0.7"); @@ -90,12 +85,16 @@ public void testRac() throws IOException, ConfigurationException assertEquals("cn-hangzhou", snitch.getDatacenter(local)); assertEquals("f", snitch.getRack(local)); } - + @Test public void testNewRegions() throws IOException, ConfigurationException { az = "us-east-1a"; - AlibabaCloudSnitch snitch = new TestAlibabaCloudSnitch(); + + AbstractCloudMetadataServiceConnector mock = mock(AbstractCloudMetadataServiceConnector.class); + when(mock.apiCall(any(), anyMap())).thenReturn(az); + + AlibabaCloudSnitch snitch = new AlibabaCloudSnitch(new SnitchProperties(new Properties()), mock); InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1"); assertEquals("us-east", snitch.getDatacenter(local)); assertEquals("1a", snitch.getRack(local)); @@ -106,5 +105,4 @@ public static void tearDown() { StorageService.instance.stopClient(); } - } diff --git a/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java b/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java index e167e99ce11d..5b5b56955464 100644 --- a/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java +++ b/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.EnumMap; import java.util.Map; +import java.util.Properties; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -39,6 +40,9 @@ import static org.apache.cassandra.ServerTestUtils.mkdirs; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class CloudstackSnitchTest { @@ -57,31 +61,14 @@ public static void setup() throws Exception StorageService.instance.initServer(0); } - private class TestCloudstackSnitch extends CloudstackSnitch - { - public TestCloudstackSnitch() throws IOException, ConfigurationException - { - super(); - } - - @Override - String csMetadataEndpoint() throws ConfigurationException - { - return ""; - } - - @Override - String csQueryMetadata(String endpoint) throws IOException, ConfigurationException - { - return az; - } - } - @Test public void testRacks() throws IOException, ConfigurationException { az = "ch-gva-1"; - CloudstackSnitch snitch = new TestCloudstackSnitch(); + CloudstackSnitch.CloudstackConnector mock = mock(CloudstackSnitch.CloudstackConnector.class); + when(mock.apiCall(any())).thenReturn(az); + + CloudstackSnitch snitch = new CloudstackSnitch(new SnitchProperties(new Properties()), mock); InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1"); InetAddressAndPort nonlocal = InetAddressAndPort.getByName("127.0.0.7"); @@ -96,14 +83,16 @@ public void testRacks() throws IOException, ConfigurationException assertEquals("ch-gva", snitch.getDatacenter(local)); assertEquals("1", snitch.getRack(local)); - } @Test public void testNewRegions() throws IOException, ConfigurationException { az = "ch-gva-1"; - CloudstackSnitch snitch = new TestCloudstackSnitch(); + CloudstackSnitch.CloudstackConnector mock = mock(CloudstackSnitch.CloudstackConnector.class); + when(mock.apiCall(any())).thenReturn(az); + CloudstackSnitch snitch = new CloudstackSnitch(new SnitchProperties(new Properties()), mock); + InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1"); assertEquals("ch-gva", snitch.getDatacenter(local)); diff --git a/test/unit/org/apache/cassandra/locator/DefaultCloudMetadataServiceConnectorTest.java b/test/unit/org/apache/cassandra/locator/DefaultCloudMetadataServiceConnectorTest.java new file mode 100644 index 000000000000..9f450f919c86 --- /dev/null +++ b/test/unit/org/apache/cassandra/locator/DefaultCloudMetadataServiceConnectorTest.java @@ -0,0 +1,63 @@ +/* + * 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 org.junit.Test; + +import org.apache.cassandra.config.DurationSpec; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.DefaultCloudMetadataServiceConnector; +import org.apache.cassandra.utils.Pair; + +import static org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.DEFAULT_METADATA_REQUEST_TIMEOUT; +import static org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.METADATA_REQUEST_TIMEOUT_PROPERTY; +import static org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.METADATA_URL_PROPERTY; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.Assert.assertEquals; + +public class DefaultCloudMetadataServiceConnectorTest +{ + @Test + public void testDefaultConnector() + { + assertEquals(new DurationSpec.IntMillisecondsBound(DEFAULT_METADATA_REQUEST_TIMEOUT).toMilliseconds(), + new DefaultCloudMetadataServiceConnector(new SnitchProperties(Pair.create(METADATA_REQUEST_TIMEOUT_PROPERTY, DEFAULT_METADATA_REQUEST_TIMEOUT), + Pair.create(METADATA_URL_PROPERTY, "http://127.0.0.1/abc"))).requestTimeoutMs); + + assertEquals(0, new DefaultCloudMetadataServiceConnector(new SnitchProperties(Pair.create(METADATA_REQUEST_TIMEOUT_PROPERTY, "0s"), Pair.create(METADATA_URL_PROPERTY, "http://127.0.0.1/abc"))).requestTimeoutMs); + assertEquals(30000, new DefaultCloudMetadataServiceConnector(new SnitchProperties(Pair.create(METADATA_URL_PROPERTY, "http://127.0.0.1/abc"))).requestTimeoutMs); + } + + @Test + public void testInvalidMetadataURL() + { + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(() -> new DefaultCloudMetadataServiceConnector(new SnitchProperties(Pair.create(METADATA_URL_PROPERTY, "http:")))) + .withMessage("Snitch metadata service URL 'http:' is invalid. Please review snitch properties defined in the configured 'cassandra-rackdc.properties' configuration file."); + } + + @Test + public void testInvalidTimeout() + { + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(() -> new DefaultCloudMetadataServiceConnector(new SnitchProperties(Pair.create(METADATA_REQUEST_TIMEOUT_PROPERTY, "1x"), + Pair.create(METADATA_URL_PROPERTY, "http://127.0.0.1/abc")))) + .withMessage("1x as value of metadata_request_timeout is invalid duration! Invalid duration: 1x Accepted units:[MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS] where case matters and only non-negative values."); + } +} diff --git a/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java b/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java index 1fdfd1cb6221..63270f4cb074 100644 --- a/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java +++ b/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.EnumMap; import java.util.Map; +import java.util.Properties; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -40,11 +41,13 @@ import static org.apache.cassandra.ServerTestUtils.mkdirs; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class GoogleCloudSnitchTest { - private static String az; - @BeforeClass public static void setup() throws Exception { @@ -58,25 +61,15 @@ public static void setup() throws Exception StorageService.instance.initServer(0); } - private class TestGoogleCloudSnitch extends GoogleCloudSnitch - { - public TestGoogleCloudSnitch() throws IOException, ConfigurationException - { - super(); - } - - @Override - String gceApiCall(String url) throws IOException, ConfigurationException - { - return az; - } - } - @Test public void testRac() throws IOException, ConfigurationException { - az = "us-central1-a"; - GoogleCloudSnitch snitch = new TestGoogleCloudSnitch(); + String az = "us-central1-a"; + + AbstractCloudMetadataServiceConnector mock = mock(AbstractCloudMetadataServiceConnector.class); + when(mock.apiCall(any(), anyMap())).thenReturn(az); + + GoogleCloudSnitch snitch = new GoogleCloudSnitch(new SnitchProperties(new Properties()), mock); InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1"); InetAddressAndPort nonlocal = InetAddressAndPort.getByName("127.0.0.7"); @@ -96,8 +89,11 @@ public void testRac() throws IOException, ConfigurationException @Test public void testNewRegions() throws IOException, ConfigurationException { - az = "asia-east1-a"; - GoogleCloudSnitch snitch = new TestGoogleCloudSnitch(); + String az = "asia-east1-a"; + AbstractCloudMetadataServiceConnector mock = mock(AbstractCloudMetadataServiceConnector.class); + when(mock.apiCall(any(), anyMap())).thenReturn(az); + + GoogleCloudSnitch snitch = new GoogleCloudSnitch(new SnitchProperties(new Properties()), mock); InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1"); assertEquals("asia-east1", snitch.getDatacenter(local)); assertEquals("a", snitch.getRack(local)); diff --git a/test/unit/org/apache/cassandra/locator/SnitchUtilsTest.java b/test/unit/org/apache/cassandra/locator/SnitchUtilsTest.java new file mode 100644 index 000000000000..b5b140098832 --- /dev/null +++ b/test/unit/org/apache/cassandra/locator/SnitchUtilsTest.java @@ -0,0 +1,45 @@ +/* + * 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 org.junit.Test; + +import org.apache.cassandra.utils.Pair; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.Assert.assertEquals; + +public class SnitchUtilsTest +{ + @Test + public void testSnitchUtils() + { + Pair result = SnitchUtils.parseDcAndRack("my-dc-rack1", ""); + assertEquals("my-dc", result.left); + assertEquals("rack1", result.right); + + result = SnitchUtils.parseDcAndRack("my-rack", ""); + assertEquals("my", result.left); + assertEquals("rack", result.right); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> SnitchUtils.parseDcAndRack("myresponse", "")) + .withMessage("myresponse does not contain at least one '-' to differentiate between datacenter and rack"); + } +}