/* * 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.gce; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; import com.google.api.client.googleapis.json.GoogleJsonResponseException; import com.google.api.client.http.InputStreamContent; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.services.storage.Storage; import com.google.api.services.storage.StorageScopes; import com.google.api.services.storage.model.Bucket; import com.google.api.services.storage.model.StorageObject; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; 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.typedef.F; import org.apache.ignite.internal.util.typedef.internal.S; 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; /** * Google Cloud Storage based IP finder. * <p> * For information about Cloud Storage visit <a href="https://cloud.google.com/storage/">cloud.google.com</a>. * <h1 class="header">Configuration</h1> * <h2 class="header">Mandatory</h2> * <ul> * <li>Service Account Id (see {@link #setServiceAccountId(String)})</li> * <li>Service Account P12 key file path (see {@link #setServiceAccountP12FilePath(String)})</li> * <li>Google Platform project name (see {@link #setProjectName(String)})</li> * <li>Google Storage bucket name (see {@link #setBucketName(String)})</li> * </ul> * <h2 class="header">Optional</h2> * <ul> * <li>Shared flag (see {@link #setShared(boolean)})</li> * </ul> * <p> * The finder will create a bucket with the provided name. The bucket will contain entries named * like the following: {@code 192.168.1.136#1001}. * <p> * Note that storing data in Google Cloud Storage service will result in charges to your Google Cloud Platform 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 TcpDiscoveryGoogleStorageIpFinder extends TcpDiscoveryIpFinderAdapter { /** Default object's content. */ private final static ByteArrayInputStream OBJECT_CONTENT = new ByteArrayInputStream(new byte[0]); /** Grid logger. */ @LoggerResource private IgniteLogger log; /** Google Cloud Platform's project name.*/ private String projectName; /** Google Storage bucket name. */ private String bucketName; /** Service account p12 private key file name. */ private String srvcAccountP12FilePath; /** Service account id. */ private String srvcAccountId; /** Google storage. */ private Storage storage; /** Init routine guard. */ private final AtomicBoolean initGuard = new AtomicBoolean(); /** Init routine latch. */ private final CountDownLatch initLatch = new CountDownLatch(1); /** * */ public TcpDiscoveryGoogleStorageIpFinder() { setShared(true); } /** {@inheritDoc} */ @Override public Collection<InetSocketAddress> getRegisteredAddresses() throws IgniteSpiException { init(); Collection<InetSocketAddress> addrs = new ArrayList<>(); try { Storage.Objects.List listObjects = storage.objects().list(bucketName); com.google.api.services.storage.model.Objects objects; do { objects = listObjects.execute(); if (objects == null || objects.getItems() == null) break; for (StorageObject object : objects.getItems()) addrs.add(addrFromString(object.getName())); listObjects.setPageToken(objects.getNextPageToken()); } while (null != objects.getNextPageToken()); } catch (Exception e) { throw new IgniteSpiException("Failed to get content from the bucket: " + bucketName, e); } return addrs; } /** {@inheritDoc} */ @Override public void registerAddresses(Collection<InetSocketAddress> addrs) throws IgniteSpiException { assert !F.isEmpty(addrs); init(); for (InetSocketAddress addr : addrs) { String key = keyFromAddr(addr); StorageObject object = new StorageObject(); object.setBucket(bucketName); object.setName(key); InputStreamContent content = new InputStreamContent("application/octet-stream", OBJECT_CONTENT); content.setLength(OBJECT_CONTENT.available()); try { Storage.Objects.Insert insertObject = storage.objects().insert(bucketName, object, content); insertObject.execute(); } catch (Exception 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); init(); for (InetSocketAddress addr : addrs) { String key = keyFromAddr(addr); try { Storage.Objects.Delete deleteObject = storage.objects().delete(bucketName, key); deleteObject.execute(); } catch (Exception e) { throw new IgniteSpiException("Failed to delete entry [bucketName=" + bucketName + ", entry=" + key + ']', e); } } } /** * Sets Google Cloud Platforms project name. * Usually this is an auto generated project number (ex. 208709979073) that can be found in "Overview" section * of Google Developer Console. * <p> * For details refer to Google Cloud Platform API reference. * * @param projectName Project name. * @return {@code this} for chaining. */ @IgniteSpiConfiguration(optional = false) public TcpDiscoveryGoogleStorageIpFinder setProjectName(String projectName) { this.projectName = projectName; return this; } /** * Sets Google Cloud Storage bucket name. * If the bucket doesn't exist Ignite will automatically create it. However the name must be unique across whole * Google Cloud Storage and Service Account Id (see {@link #setServiceAccountId(String)}) must be authorized to * perform this operation. * * @param bucketName Bucket name. * @return {@code this} for chaining. */ @IgniteSpiConfiguration(optional = false) public TcpDiscoveryGoogleStorageIpFinder setBucketName(String bucketName) { this.bucketName = bucketName; return this; } /** * Sets a full path to the private key in PKCS12 format of the Service Account. * <p> * For more information please refer to * <a href="https://cloud.google.com/storage/docs/authentication#service_accounts"> * Service Account Authentication</a>. * * @param p12FileName Private key file full path. * @return {@code this} for chaining. */ @IgniteSpiConfiguration(optional = false) public TcpDiscoveryGoogleStorageIpFinder setServiceAccountP12FilePath(String p12FileName) { this.srvcAccountP12FilePath = p12FileName; return this; } /** * Sets the service account ID (typically an e-mail address). * <p> * For more information please refer to * <a href="https://cloud.google.com/storage/docs/authentication#service_accounts"> * Service Account Authentication</a>. * * @param id Service account ID. * @return {@code this} for chaining. */ @IgniteSpiConfiguration(optional = false) public TcpDiscoveryGoogleStorageIpFinder setServiceAccountId(String id) { this.srvcAccountId = id; return this; } /** * Google Cloud Storage initialization. * * @throws IgniteSpiException In case of error. */ private void init() throws IgniteSpiException { if (initGuard.compareAndSet(false, true)) { if (srvcAccountId == null || srvcAccountP12FilePath == null || projectName == null || bucketName == null) { throw new IgniteSpiException( "One or more of the required parameters is not set [serviceAccountId=" + srvcAccountId + ", serviceAccountP12FilePath=" + srvcAccountP12FilePath + ", projectName=" + projectName + ", bucketName=" + bucketName + "]"); } try { NetHttpTransport httpTransport; try { httpTransport = GoogleNetHttpTransport.newTrustedTransport(); } catch (GeneralSecurityException | IOException e) { throw new IgniteSpiException(e); } GoogleCredential cred; try { cred = new GoogleCredential.Builder().setTransport(httpTransport) .setJsonFactory(JacksonFactory.getDefaultInstance()).setServiceAccountId(srvcAccountId) .setServiceAccountPrivateKeyFromP12File(new File(srvcAccountP12FilePath)) .setServiceAccountScopes(Collections.singleton(StorageScopes.DEVSTORAGE_FULL_CONTROL)).build(); } catch (Exception e) { throw new IgniteSpiException("Failed to authenticate on Google Cloud Platform", e); } try { storage = new Storage.Builder(httpTransport, JacksonFactory.getDefaultInstance(), cred) .setApplicationName(projectName).build(); } catch (Exception e) { throw new IgniteSpiException("Failed to open a storage for given project name: " + projectName, e); } boolean createBucket = false; try { Storage.Buckets.Get getBucket = storage.buckets().get(bucketName); getBucket.setProjection("full"); getBucket.execute(); } catch (GoogleJsonResponseException e) { if (e.getStatusCode() == 404) { U.warn(log, "Bucket doesn't exist, will create it [bucketName=" + bucketName + "]"); createBucket = true; } else throw new IgniteSpiException("Failed to open the bucket: " + bucketName, e); } catch (Exception e) { throw new IgniteSpiException("Failed to open the bucket: " + bucketName, e); } if (createBucket) { Bucket newBucket = new Bucket(); newBucket.setName(bucketName); try { Storage.Buckets.Insert insertBucket = storage.buckets().insert(projectName, newBucket); insertBucket.setProjection("full"); insertBucket.setPredefinedDefaultObjectAcl("projectPrivate"); insertBucket.execute(); } catch (Exception e) { throw new IgniteSpiException("Failed to create the bucket: " + bucketName, e); } } } finally { initLatch.countDown(); } } else { try { U.await(initLatch); } catch (IgniteInterruptedCheckedException e) { throw new IgniteSpiException("Thread has been interrupted.", e); } if (storage == null) throw new IgniteSpiException("IpFinder has not been initialized properly"); } } /** * Constructs bucket's key from an address. * * @param addr Node address. * @return Bucket key. */ private String keyFromAddr(InetSocketAddress addr) { return addr.getAddress().getHostAddress() + "#" + addr.getPort(); } /** * Constructs a node address from bucket's key. * * @param key Bucket key. * @return Node address. * @throws IgniteSpiException In case of error. */ private InetSocketAddress addrFromString(String key) throws IgniteSpiException { String[] res = key.split("#"); if (res.length != 2) throw new IgniteSpiException("Invalid address string: " + key); int port; try { port = Integer.parseInt(res[1]); } catch (NumberFormatException ignored) { throw new IgniteSpiException("Invalid port number: " + res[1]); } return new InetSocketAddress(res[0], port); } /** * Used by TEST SUITES only. Called through reflection. * * @param bucketName Bucket to delete. */ private void removeBucket(String bucketName) { init(); try { Storage.Buckets.Delete deleteBucket = storage.buckets().delete(bucketName); deleteBucket.execute(); } catch (Exception e) { throw new IgniteSpiException("Failed to remove the bucket: " + bucketName, e); } } /** {@inheritDoc} */ @Override public TcpDiscoveryGoogleStorageIpFinder setShared(boolean shared) { super.setShared(shared); return this; } /** {@inheritDoc} */ @Override public String toString() { return S.toString(TcpDiscoveryGoogleStorageIpFinder.class, this); } }