gobblin.yarn.YarnAppSecurityManager.java Source code

Java tutorial

Introduction

Here is the source code for gobblin.yarn.YarnAppSecurityManager.java

Source

/*
 * 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));
    }
}