001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.crypto.key.kms;
020
021import java.io.IOException;
022import java.security.GeneralSecurityException;
023import java.security.NoSuchAlgorithmException;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.List;
027import java.util.concurrent.atomic.AtomicInteger;
028
029import org.apache.hadoop.conf.Configuration;
030import org.apache.hadoop.crypto.key.KeyProvider;
031import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.CryptoExtension;
032import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.EncryptedKeyVersion;
033import org.apache.hadoop.crypto.key.KeyProviderDelegationTokenExtension;
034import org.apache.hadoop.security.Credentials;
035import org.apache.hadoop.security.token.Token;
036import org.apache.hadoop.util.Time;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040import com.google.common.annotations.VisibleForTesting;
041
042/**
043 * A simple LoadBalancing KMSClientProvider that round-robins requests
044 * across a provided array of KMSClientProviders. It also retries failed
045 * requests on the next available provider in the load balancer group. It
046 * only retries failed requests that result in an IOException, sending back
047 * all other Exceptions to the caller without retry.
048 */
049public class LoadBalancingKMSClientProvider extends KeyProvider implements
050    CryptoExtension,
051    KeyProviderDelegationTokenExtension.DelegationTokenExtension {
052
053  public static Logger LOG =
054      LoggerFactory.getLogger(LoadBalancingKMSClientProvider.class);
055
056  static interface ProviderCallable<T> {
057    public T call(KMSClientProvider provider) throws IOException, Exception;
058  }
059
060  @SuppressWarnings("serial")
061  static class WrapperException extends RuntimeException {
062    public WrapperException(Throwable cause) {
063      super(cause);
064    }
065  }
066
067  private final KMSClientProvider[] providers;
068  private final AtomicInteger currentIdx;
069
070  public LoadBalancingKMSClientProvider(KMSClientProvider[] providers,
071      Configuration conf) {
072    this(shuffle(providers), Time.monotonicNow(), conf);
073  }
074
075  @VisibleForTesting
076  LoadBalancingKMSClientProvider(KMSClientProvider[] providers, long seed,
077      Configuration conf) {
078    super(conf);
079    this.providers = providers;
080    this.currentIdx = new AtomicInteger((int)(seed % providers.length));
081  }
082
083  @VisibleForTesting
084  KMSClientProvider[] getProviders() {
085    return providers;
086  }
087
088  private <T> T doOp(ProviderCallable<T> op, int currPos)
089      throws IOException {
090    IOException ex = null;
091    for (int i = 0; i < providers.length; i++) {
092      KMSClientProvider provider = providers[(currPos + i) % providers.length];
093      try {
094        return op.call(provider);
095      } catch (IOException ioe) {
096        LOG.warn("KMS provider at [{}] threw an IOException [{}]!!",
097            provider.getKMSUrl(), ioe.getMessage());
098        ex = ioe;
099      } catch (Exception e) {
100        if (e instanceof RuntimeException) {
101          throw (RuntimeException)e;
102        } else {
103          throw new WrapperException(e);
104        }
105      }
106    }
107    if (ex != null) {
108      LOG.warn("Aborting since the Request has failed with all KMS"
109          + " providers in the group. !!");
110      throw ex;
111    }
112    throw new IOException("No providers configured !!");
113  }
114
115  private int nextIdx() {
116    while (true) {
117      int current = currentIdx.get();
118      int next = (current + 1) % providers.length;
119      if (currentIdx.compareAndSet(current, next)) {
120        return current;
121      }
122    }
123  }
124
125  @Override
126  public Token<?>[]
127      addDelegationTokens(final String renewer, final Credentials credentials)
128          throws IOException {
129    return doOp(new ProviderCallable<Token<?>[]>() {
130      @Override
131      public Token<?>[] call(KMSClientProvider provider) throws IOException {
132        return provider.addDelegationTokens(renewer, credentials);
133      }
134    }, nextIdx());
135  }
136
137  // This request is sent to all providers in the load-balancing group
138  @Override
139  public void warmUpEncryptedKeys(String... keyNames) throws IOException {
140    for (KMSClientProvider provider : providers) {
141      try {
142        provider.warmUpEncryptedKeys(keyNames);
143      } catch (IOException ioe) {
144        LOG.error(
145            "Error warming up keys for provider with url"
146            + "[" + provider.getKMSUrl() + "]");
147      }
148    }
149  }
150
151  // This request is sent to all providers in the load-balancing group
152  @Override
153  public void drain(String keyName) {
154    for (KMSClientProvider provider : providers) {
155      provider.drain(keyName);
156    }
157  }
158
159  @Override
160  public EncryptedKeyVersion
161      generateEncryptedKey(final String encryptionKeyName)
162          throws IOException, GeneralSecurityException {
163    try {
164      return doOp(new ProviderCallable<EncryptedKeyVersion>() {
165        @Override
166        public EncryptedKeyVersion call(KMSClientProvider provider)
167            throws IOException, GeneralSecurityException {
168          return provider.generateEncryptedKey(encryptionKeyName);
169        }
170      }, nextIdx());
171    } catch (WrapperException we) {
172      throw (GeneralSecurityException) we.getCause();
173    }
174  }
175
176  @Override
177  public KeyVersion
178      decryptEncryptedKey(final EncryptedKeyVersion encryptedKeyVersion)
179          throws IOException, GeneralSecurityException {
180    try {
181      return doOp(new ProviderCallable<KeyVersion>() {
182        @Override
183        public KeyVersion call(KMSClientProvider provider)
184            throws IOException, GeneralSecurityException {
185          return provider.decryptEncryptedKey(encryptedKeyVersion);
186        }
187      }, nextIdx());
188    } catch (WrapperException we) {
189      throw (GeneralSecurityException)we.getCause();
190    }
191  }
192
193  @Override
194  public KeyVersion getKeyVersion(final String versionName) throws IOException {
195    return doOp(new ProviderCallable<KeyVersion>() {
196      @Override
197      public KeyVersion call(KMSClientProvider provider) throws IOException {
198        return provider.getKeyVersion(versionName);
199      }
200    }, nextIdx());
201  }
202
203  @Override
204  public List<String> getKeys() throws IOException {
205    return doOp(new ProviderCallable<List<String>>() {
206      @Override
207      public List<String> call(KMSClientProvider provider) throws IOException {
208        return provider.getKeys();
209      }
210    }, nextIdx());
211  }
212
213  @Override
214  public Metadata[] getKeysMetadata(final String... names) throws IOException {
215    return doOp(new ProviderCallable<Metadata[]>() {
216      @Override
217      public Metadata[] call(KMSClientProvider provider) throws IOException {
218        return provider.getKeysMetadata(names);
219      }
220    }, nextIdx());
221  }
222
223  @Override
224  public List<KeyVersion> getKeyVersions(final String name) throws IOException {
225    return doOp(new ProviderCallable<List<KeyVersion>>() {
226      @Override
227      public List<KeyVersion> call(KMSClientProvider provider)
228          throws IOException {
229        return provider.getKeyVersions(name);
230      }
231    }, nextIdx());
232  }
233
234  @Override
235  public KeyVersion getCurrentKey(final String name) throws IOException {
236    return doOp(new ProviderCallable<KeyVersion>() {
237      @Override
238      public KeyVersion call(KMSClientProvider provider) throws IOException {
239        return provider.getCurrentKey(name);
240      }
241    }, nextIdx());
242  }
243  @Override
244  public Metadata getMetadata(final String name) throws IOException {
245    return doOp(new ProviderCallable<Metadata>() {
246      @Override
247      public Metadata call(KMSClientProvider provider) throws IOException {
248        return provider.getMetadata(name);
249      }
250    }, nextIdx());
251  }
252
253  @Override
254  public KeyVersion createKey(final String name, final byte[] material,
255      final Options options) throws IOException {
256    return doOp(new ProviderCallable<KeyVersion>() {
257      @Override
258      public KeyVersion call(KMSClientProvider provider) throws IOException {
259        return provider.createKey(name, material, options);
260      }
261    }, nextIdx());
262  }
263
264  @Override
265  public KeyVersion createKey(final String name, final Options options)
266      throws NoSuchAlgorithmException, IOException {
267    try {
268      return doOp(new ProviderCallable<KeyVersion>() {
269        @Override
270        public KeyVersion call(KMSClientProvider provider) throws IOException,
271            NoSuchAlgorithmException {
272          return provider.createKey(name, options);
273        }
274      }, nextIdx());
275    } catch (WrapperException e) {
276      throw (NoSuchAlgorithmException)e.getCause();
277    }
278  }
279  @Override
280  public void deleteKey(final String name) throws IOException {
281    doOp(new ProviderCallable<Void>() {
282      @Override
283      public Void call(KMSClientProvider provider) throws IOException {
284        provider.deleteKey(name);
285        return null;
286      }
287    }, nextIdx());
288  }
289  @Override
290  public KeyVersion rollNewVersion(final String name, final byte[] material)
291      throws IOException {
292    return doOp(new ProviderCallable<KeyVersion>() {
293      @Override
294      public KeyVersion call(KMSClientProvider provider) throws IOException {
295        return provider.rollNewVersion(name, material);
296      }
297    }, nextIdx());
298  }
299
300  @Override
301  public KeyVersion rollNewVersion(final String name)
302      throws NoSuchAlgorithmException, IOException {
303    try {
304      return doOp(new ProviderCallable<KeyVersion>() {
305        @Override
306        public KeyVersion call(KMSClientProvider provider) throws IOException,
307        NoSuchAlgorithmException {
308          return provider.rollNewVersion(name);
309        }
310      }, nextIdx());
311    } catch (WrapperException e) {
312      throw (NoSuchAlgorithmException)e.getCause();
313    }
314  }
315
316  // Close all providers in the LB group
317  @Override
318  public void close() throws IOException {
319    for (KMSClientProvider provider : providers) {
320      try {
321        provider.close();
322      } catch (IOException ioe) {
323        LOG.error("Error closing provider with url"
324            + "[" + provider.getKMSUrl() + "]");
325      }
326    }
327  }
328
329
330  @Override
331  public void flush() throws IOException {
332    for (KMSClientProvider provider : providers) {
333      try {
334        provider.flush();
335      } catch (IOException ioe) {
336        LOG.error("Error flushing provider with url"
337            + "[" + provider.getKMSUrl() + "]");
338      }
339    }
340  }
341
342  private static KMSClientProvider[] shuffle(KMSClientProvider[] providers) {
343    List<KMSClientProvider> list = Arrays.asList(providers);
344    Collections.shuffle(list);
345    return list.toArray(providers);
346  }
347}