/*
* 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.ignite.spi.discovery.tcp.ipfinder.s3;
import com.amazonaws.AmazonClientException;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import java.io.ByteArrayInputStream;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.LinkedList;
import java.util.StringTokenizer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.internal.IgniteInterruptedCheckedException;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.SB;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.resources.LoggerResource;
import org.apache.ignite.spi.IgniteSpiConfiguration;
import org.apache.ignite.spi.IgniteSpiException;
import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinderAdapter;
/**
* AWS S3-based IP finder.
* <p>
* For information about Amazon S3 visit <a href="http://aws.amazon.com">aws.amazon.com</a>.
* <h1 class="header">Configuration</h1>
* <h2 class="header">Mandatory</h2>
* <ul>
* <li>AWS credentials (see {@link #setAwsCredentials(AWSCredentials)} and
* {@link #setAwsCredentialsProvider(AWSCredentialsProvider)}</li>
* <li>Bucket name (see {@link #setBucketName(String)})</li>
* </ul>
* <h2 class="header">Optional</h2>
* <ul>
* <li>Client configuration (see {@link #setClientConfiguration(ClientConfiguration)})</li>
* <li>Shared flag (see {@link #setShared(boolean)})</li>
* </ul>
* <p>
* The finder will create S3 bucket with configured name. The bucket will contain entries named
* like the following: {@code 192.168.1.136#1001}.
* <p>
* Note that storing data in AWS S3 service will result in charges to your AWS account.
* Choose another implementation of {@link org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder} for local
* or home network tests.
* <p>
* Note that this finder is shared by default (see {@link org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder#isShared()}.
*/
public class TcpDiscoveryS3IpFinder extends TcpDiscoveryIpFinderAdapter {
/** Delimiter to use in S3 entries name. */
public static final String DELIM = "#";
/** Entry content. */
private static final byte[] ENTRY_CONTENT = new byte[] {1};
/** Entry metadata with content length set. */
private static final ObjectMetadata ENTRY_METADATA;
static {
ENTRY_METADATA = new ObjectMetadata();
ENTRY_METADATA.setContentLength(ENTRY_CONTENT.length);
}
/** Grid logger. */
@LoggerResource
private IgniteLogger log;
/** Client to interact with S3 storage. */
@GridToStringExclude
private AmazonS3 s3;
/** Bucket name. */
private String bucketName;
/** Init guard. */
@GridToStringExclude
private final AtomicBoolean initGuard = new AtomicBoolean();
/** Init latch. */
@GridToStringExclude
private final CountDownLatch initLatch = new CountDownLatch(1);
/** Amazon client configuration. */
private ClientConfiguration cfg;
/** AWS Credentials. */
@GridToStringExclude
private AWSCredentials cred;
/** AWS Credentials. */
@GridToStringExclude
private AWSCredentialsProvider credProvider;
/**
* Constructor.
*/
public TcpDiscoveryS3IpFinder() {
setShared(true);
}
/** {@inheritDoc} */
@Override public Collection<InetSocketAddress> getRegisteredAddresses() throws IgniteSpiException {
initClient();
Collection<InetSocketAddress> addrs = new LinkedList<>();
try {
ObjectListing list = s3.listObjects(bucketName);
while (true) {
for (S3ObjectSummary sum : list.getObjectSummaries()) {
String key = sum.getKey();
StringTokenizer st = new StringTokenizer(key, DELIM);
if (st.countTokens() != 2)
U.error(log, "Failed to parse S3 entry due to invalid format: " + key);
else {
String addrStr = st.nextToken();
String portStr = st.nextToken();
int port = -1;
try {
port = Integer.parseInt(portStr);
}
catch (NumberFormatException e) {
U.error(log, "Failed to parse port for S3 entry: " + key, e);
}
if (port != -1)
try {
addrs.add(new InetSocketAddress(addrStr, port));
}
catch (IllegalArgumentException e) {
U.error(log, "Failed to parse port for S3 entry: " + key, e);
}
}
}
if (list.isTruncated())
list = s3.listNextBatchOfObjects(list);
else
break;
}
}
catch (AmazonClientException e) {
throw new IgniteSpiException("Failed to list objects in the bucket: " + bucketName, e);
}
return addrs;
}
/** {@inheritDoc} */
@Override public void registerAddresses(Collection<InetSocketAddress> addrs) throws IgniteSpiException {
assert !F.isEmpty(addrs);
initClient();
for (InetSocketAddress addr : addrs) {
String key = key(addr);
try {
s3.putObject(bucketName, key, new ByteArrayInputStream(ENTRY_CONTENT), ENTRY_METADATA);
}
catch (AmazonClientException e) {
throw new IgniteSpiException("Failed to put entry [bucketName=" + bucketName +
", entry=" + key + ']', e);
}
}
}
/** {@inheritDoc} */
@Override public void unregisterAddresses(Collection<InetSocketAddress> addrs) throws IgniteSpiException {
assert !F.isEmpty(addrs);
initClient();
for (InetSocketAddress addr : addrs) {
String key = key(addr);
try {
s3.deleteObject(bucketName, key);
}
catch (AmazonClientException e) {
throw new IgniteSpiException("Failed to delete entry [bucketName=" + bucketName +
", entry=" + key + ']', e);
}
}
}
/**
* Gets S3 key for provided address.
*
* @param addr Node address.
* @return Key.
*/
private String key(InetSocketAddress addr) {
assert addr != null;
SB sb = new SB();
sb.a(addr.getAddress().getHostAddress())
.a(DELIM)
.a(addr.getPort());
return sb.toString();
}
/**
* Amazon s3 client initialization.
*
* @throws org.apache.ignite.spi.IgniteSpiException In case of error.
*/
@SuppressWarnings({"BusyWait"})
private void initClient() throws IgniteSpiException {
if (initGuard.compareAndSet(false, true))
try {
if (cred == null && credProvider == null)
throw new IgniteSpiException("AWS credentials are not set.");
if (cfg == null)
U.warn(log, "Amazon client configuration is not set (will use default).");
if (F.isEmpty(bucketName))
throw new IgniteSpiException("Bucket name is null or empty (provide bucket name and restart).");
s3 = createAmazonS3Client();
if (!s3.doesBucketExist(bucketName)) {
try {
s3.createBucket(bucketName);
if (log.isDebugEnabled())
log.debug("Created S3 bucket: " + bucketName);
while (!s3.doesBucketExist(bucketName))
try {
U.sleep(200);
}
catch (IgniteInterruptedCheckedException e) {
throw new IgniteSpiException("Thread has been interrupted.", e);
}
}
catch (AmazonClientException e) {
if (!s3.doesBucketExist(bucketName)) {
s3 = null;
throw new IgniteSpiException("Failed to create bucket: " + bucketName, e);
}
}
}
}
finally {
initLatch.countDown();
}
else {
try {
U.await(initLatch);
}
catch (IgniteInterruptedCheckedException e) {
throw new IgniteSpiException("Thread has been interrupted.", e);
}
if (s3 == null)
throw new IgniteSpiException("Ip finder has not been initialized properly.");
}
}
/**
* Instantiates {@code AmazonS3Client} instance.
*
* @return Client instance to use to connect to AWS.
*/
private AmazonS3Client createAmazonS3Client() {
return cfg != null
? (cred != null ? new AmazonS3Client(cred, cfg) : new AmazonS3Client(credProvider, cfg))
: (cred != null ? new AmazonS3Client(cred) : new AmazonS3Client(credProvider));
}
/**
* Sets bucket name for IP finder.
*
* @param bucketName Bucket name.
* @return {@code this} for chaining.
*/
@IgniteSpiConfiguration(optional = false)
public TcpDiscoveryS3IpFinder setBucketName(String bucketName) {
this.bucketName = bucketName;
return this;
}
/**
* Sets Amazon client configuration.
* <p>
* For details refer to Amazon S3 API reference.
*
* @param cfg Amazon client configuration.
* @return {@code this} for chaining.
*/
@IgniteSpiConfiguration(optional = true)
public TcpDiscoveryS3IpFinder setClientConfiguration(ClientConfiguration cfg) {
this.cfg = cfg;
return this;
}
/**
* Sets AWS credentials. Either use {@link #setAwsCredentialsProvider(AWSCredentialsProvider)} or this one.
* <p>
* For details refer to Amazon S3 API reference.
*
* @param cred AWS credentials.
* @return {@code this} for chaining.
*/
@IgniteSpiConfiguration(optional = false)
public TcpDiscoveryS3IpFinder setAwsCredentials(AWSCredentials cred) {
this.cred = cred;
return this;
}
/**
* Sets AWS credentials provider. Either use {@link #setAwsCredentials(AWSCredentials)} or this one.
* <p>
* For details refer to Amazon S3 API reference.
*
* @param credProvider AWS credentials provider.
* @return {@code this} for chaining.
*/
@IgniteSpiConfiguration(optional = false)
public TcpDiscoveryS3IpFinder setAwsCredentialsProvider(AWSCredentialsProvider credProvider) {
this.credProvider = credProvider;
return this;
}
/** {@inheritDoc} */
@Override public TcpDiscoveryS3IpFinder setShared(boolean shared) {
super.setShared(shared);
return this;
}
/** {@inheritDoc} */
@Override public String toString() {
return S.toString(TcpDiscoveryS3IpFinder.class, this, "super", super.toString());
}
}