/* * 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 gobblin.yarn; import gobblin.cluster.GobblinHelixMessagingService; import java.io.File; import java.io.IOException; import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.permission.FsAction; import org.apache.hadoop.fs.permission.FsPermission; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.token.Token; import org.apache.hadoop.security.token.TokenIdentifier; import org.apache.helix.Criteria; import org.apache.helix.HelixManager; import org.apache.helix.InstanceType; import org.apache.helix.model.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.util.concurrent.AbstractIdleService; import com.typesafe.config.Config; import gobblin.util.ExecutorsUtils; /** * A class for managing Kerberos login and token renewing on the client side that has access to * the keytab file. * * <p> * This class works with {@link YarnContainerSecurityManager} to manage renewing of delegation * tokens across the application. This class is responsible for login through a Kerberos keytab, * renewing the delegation token, and storing the token to a token file on HDFS. It sends a * Helix message to the controller and all the participants upon writing the token to the token * file, which rely on the {@link YarnContainerSecurityManager} to read the token in the file * upon receiving the message. * </p> * * <p> * This class uses a scheduled task to do Kerberos re-login to renew the Kerberos ticket on a * configurable schedule if login is from a keytab file. It also uses a second scheduled task * to renew the delegation token after each login. Both the re-login interval and the token * renewing interval are configurable. * </p> * * @author Yinan Li */ public class YarnAppSecurityManager extends AbstractIdleService { private static final Logger LOGGER = LoggerFactory.getLogger(YarnAppSecurityManager.class); private final Config config; private final HelixManager helixManager; private final FileSystem fs; private final Path tokenFilePath; private UserGroupInformation loginUser; private Token<? extends TokenIdentifier> token; private final long loginIntervalInMinutes; private final long tokenRenewIntervalInMinutes; private final ScheduledExecutorService loginExecutor; private final ScheduledExecutorService tokenRenewExecutor; private Optional<ScheduledFuture<?>> scheduledTokenRenewTask = Optional.absent(); // This flag is used to tell if this is the first login. If yes, no token updated message will be // sent to the controller and the participants as they may not be up running yet. The first login // happens after this class starts up so the token gets regularly refreshed before the next login. private volatile boolean firstLogin = true; public YarnAppSecurityManager(Config config, HelixManager helixManager, FileSystem fs, Path tokenFilePath) throws IOException { this.config = config; this.helixManager = helixManager; this.fs = fs; this.tokenFilePath = tokenFilePath; this.fs.makeQualified(tokenFilePath); this.loginUser = UserGroupInformation.getLoginUser(); this.loginIntervalInMinutes = config.getLong(GobblinYarnConfigurationKeys.LOGIN_INTERVAL_IN_MINUTES); this.tokenRenewIntervalInMinutes = config.getLong(GobblinYarnConfigurationKeys.TOKEN_RENEW_INTERVAL_IN_MINUTES); this.loginExecutor = Executors.newSingleThreadScheduledExecutor( ExecutorsUtils.newThreadFactory(Optional.of(LOGGER), Optional.of("KeytabReLoginExecutor"))); this.tokenRenewExecutor = Executors.newSingleThreadScheduledExecutor( ExecutorsUtils.newThreadFactory(Optional.of(LOGGER), Optional.of("TokenRenewExecutor"))); } @Override protected void startUp() throws Exception { LOGGER.info("Starting the " + YarnAppSecurityManager.class.getSimpleName()); LOGGER.info( String.format("Scheduling the login task with an interval of %d minute(s)", this.loginIntervalInMinutes)); // Schedule the Kerberos re-login task this.loginExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { try { // Cancel the currently scheduled token renew task if (scheduledTokenRenewTask.isPresent() && scheduledTokenRenewTask.get().cancel(true)) { LOGGER.info("Cancelled the token renew task"); } loginFromKeytab(); if (firstLogin) { firstLogin = false; } // Re-schedule the token renew task after re-login scheduleTokenRenewTask(); } catch (IOException ioe) { LOGGER.error("Failed to login from keytab", ioe); throw Throwables.propagate(ioe); } } }, 0, this.loginIntervalInMinutes, TimeUnit.MINUTES); } @Override protected void shutDown() throws Exception { LOGGER.info("Stopping the " + YarnAppSecurityManager.class.getSimpleName()); if (this.scheduledTokenRenewTask.isPresent()) { this.scheduledTokenRenewTask.get().cancel(true); } ExecutorsUtils.shutdownExecutorService(this.loginExecutor, Optional.of(LOGGER)); ExecutorsUtils.shutdownExecutorService(this.tokenRenewExecutor, Optional.of(LOGGER)); } private void scheduleTokenRenewTask() { LOGGER.info(String.format("Scheduling the token renew task with an interval of %d minute(s)", this.tokenRenewIntervalInMinutes)); this.scheduledTokenRenewTask = Optional.<ScheduledFuture<?>>of( this.tokenRenewExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { try { renewDelegationToken(); } catch (IOException ioe) { LOGGER.error("Failed to renew delegation token", ioe); throw Throwables.propagate(ioe); } catch (InterruptedException ie) { LOGGER.error("Token renew task has been interrupted"); Thread.currentThread().interrupt(); } } }, this.tokenRenewIntervalInMinutes, this.tokenRenewIntervalInMinutes, TimeUnit.MINUTES)); } /** * Renew the existing delegation token. */ private synchronized void renewDelegationToken() throws IOException, InterruptedException { this.token.renew(this.fs.getConf()); writeDelegationTokenToFile(); if (!this.firstLogin) { // Send a message to the controller and all the participants if this is not the first login sendTokenFileUpdatedMessage(InstanceType.CONTROLLER); sendTokenFileUpdatedMessage(InstanceType.PARTICIPANT); } } /** * Get a new delegation token for the current logged-in user. */ @VisibleForTesting synchronized void getNewDelegationTokenForLoginUser() throws IOException { this.token = this.fs.getDelegationToken(this.loginUser.getShortUserName()); } /** * Login the user from a given keytab file. */ private void loginFromKeytab() throws IOException { String keyTabFilePath = this.config.getString(GobblinYarnConfigurationKeys.KEYTAB_FILE_PATH); if (Strings.isNullOrEmpty(keyTabFilePath)) { throw new IOException("Keytab file path is not defined for Kerberos login"); } if (!new File(keyTabFilePath).exists()) { throw new IOException("Keytab file not found at: " + keyTabFilePath); } String principal = this.config.getString(GobblinYarnConfigurationKeys.KEYTAB_PRINCIPAL_NAME); if (Strings.isNullOrEmpty(principal)) { principal = this.loginUser.getShortUserName() + "/localhost@LOCALHOST"; } Configuration conf = new Configuration(); conf.set("hadoop.security.authentication", UserGroupInformation.AuthenticationMethod.KERBEROS.toString().toLowerCase()); UserGroupInformation.setConfiguration(conf); UserGroupInformation.loginUserFromKeytab(principal, keyTabFilePath); LOGGER.info(String.format("Logged in from keytab file %s using principal %s", keyTabFilePath, principal)); this.loginUser = UserGroupInformation.getLoginUser(); getNewDelegationTokenForLoginUser(); writeDelegationTokenToFile(); if (!this.firstLogin) { // Send a message to the controller and all the participants sendTokenFileUpdatedMessage(InstanceType.CONTROLLER); sendTokenFileUpdatedMessage(InstanceType.PARTICIPANT); } } /** * Write the current delegation token to the token file. */ @VisibleForTesting synchronized void writeDelegationTokenToFile() throws IOException { if (this.fs.exists(this.tokenFilePath)) { LOGGER.info("Deleting existing token file " + this.tokenFilePath); this.fs.delete(this.tokenFilePath, false); } LOGGER.info("Writing new or renewed token to token file " + this.tokenFilePath); YarnHelixUtils.writeTokenToFile(this.token, this.tokenFilePath, this.fs.getConf()); // Only grand access to the token file to the login user this.fs.setPermission(this.tokenFilePath, new FsPermission(FsAction.READ_WRITE, FsAction.NONE, FsAction.NONE)); } @VisibleForTesting void sendTokenFileUpdatedMessage(InstanceType instanceType) { Criteria criteria = new Criteria(); criteria.setInstanceName("%"); criteria.setResource("%"); criteria.setPartition("%"); criteria.setPartitionState("%"); criteria.setRecipientInstanceType(instanceType); /** * #HELIX-0.6.7-WORKAROUND * Add back when LIVESTANCES messaging is ported to 0.6 branch if (instanceType == InstanceType.PARTICIPANT) { criteria.setDataSource(Criteria.DataSource.LIVEINSTANCES); } **/ criteria.setSessionSpecific(true); Message tokenFileUpdatedMessage = new Message(Message.MessageType.USER_DEFINE_MSG, HelixMessageSubTypes.TOKEN_FILE_UPDATED.toString().toLowerCase() + UUID.randomUUID().toString()); tokenFileUpdatedMessage.setMsgSubType(HelixMessageSubTypes.TOKEN_FILE_UPDATED.toString()); tokenFileUpdatedMessage.setMsgState(Message.MessageState.NEW); if (instanceType == InstanceType.CONTROLLER) { tokenFileUpdatedMessage.setTgtSessionId("*"); } // #HELIX-0.6.7-WORKAROUND // Temporarily bypass the default messaging service to allow upgrade to 0.6.7 which is missing support // for messaging to instances //int messagesSent = this.helixManager.getMessagingService().send(criteria, tokenFileUpdatedMessage); GobblinHelixMessagingService messagingService = new GobblinHelixMessagingService(this.helixManager); int messagesSent = messagingService.send(criteria, tokenFileUpdatedMessage); LOGGER.info(String.format("Sent %d token file updated message(s) to the %s", messagesSent, instanceType)); } }