/*
 * *
 *  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.hadoop.yarn.server.nodemanager.containermanager.linux.privileged;

import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.util.Shell.ShellCommandExecutor;
import org.apache.hadoop.util.Shell.ExitCodeException;
import org.apache.hadoop.yarn.api.ApplicationConstants;
import org.apache.hadoop.yarn.conf.YarnConfiguration;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * provides mechanisms to execute PrivilegedContainerOperations *
 */

@InterfaceAudience.Private
@InterfaceStability.Unstable
public class PrivilegedOperationExecutor {
  private static final Logger LOG =
       LoggerFactory.getLogger(PrivilegedOperationExecutor
      .class);
  private volatile static PrivilegedOperationExecutor instance;

  private String containerExecutorExe;

  public static String getContainerExecutorExecutablePath(Configuration conf) {
    String yarnHomeEnvVar =
        System.getenv(ApplicationConstants.Environment.HADOOP_YARN_HOME.key());
    File hadoopBin = new File(yarnHomeEnvVar, "bin");
    String defaultPath =
        new File(hadoopBin, "container-executor").getAbsolutePath();
    return null == conf
        ? defaultPath
        : conf.get(YarnConfiguration.NM_LINUX_CONTAINER_EXECUTOR_PATH,
        defaultPath);
  }

  private void init(Configuration conf) {
    containerExecutorExe = getContainerExecutorExecutablePath(conf);
  }

  private PrivilegedOperationExecutor(Configuration conf) {
    init(conf);
  }

  public static PrivilegedOperationExecutor getInstance(Configuration conf) {
    if (instance == null) {
      synchronized (PrivilegedOperationExecutor.class) {
        if (instance == null) {
          instance = new PrivilegedOperationExecutor(conf);
        }
      }
    }

    return instance;
  }

  /**
   * @param prefixCommands in some cases ( e.g priorities using nice ),
   *                       prefix commands are necessary
   * @param operation      the type and arguments for the operation to be
   *                       executed
   * @return execution string array for priviledged operation
   */

  public String[] getPrivilegedOperationExecutionCommand(List<String>
      prefixCommands,
      PrivilegedOperation operation) {
    List<String> fullCommand = new ArrayList<String>();

    if (prefixCommands != null && !prefixCommands.isEmpty()) {
      fullCommand.addAll(prefixCommands);
    }

    fullCommand.add(containerExecutorExe);

    String cliSwitch = operation.getOperationType().getOption();

    if (!cliSwitch.isEmpty()) {
      fullCommand.add(cliSwitch);
    }

    fullCommand.addAll(operation.getArguments());

    String[] fullCommandArray =
        fullCommand.toArray(new String[fullCommand.size()]);

    if (LOG.isDebugEnabled()) {
      LOG.debug("Privileged Execution Command Array: " +
          Arrays.toString(fullCommandArray));
    }

    return fullCommandArray;
  }

  /**
   * Executes a privileged operation. It is up to the callers to ensure that
   * each privileged operation's parameters are constructed correctly. The
   * parameters are passed verbatim to the container-executor binary.
   *
   * @param prefixCommands in some cases ( e.g priorities using nice ),
   *                       prefix commands are necessary
   * @param operation      the type and arguments for the operation to be executed
   * @param workingDir     (optional) working directory for execution
   * @param env            (optional) env of the command will include specified vars
   * @param grabOutput     return (possibly large) shell command output
   * @param inheritParentEnv inherit the env vars from the parent process
   * @return stdout contents from shell executor - useful for some privileged
   * operations - e.g --tc_read
   * @throws org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.privileged.PrivilegedOperationException
   */
  public String executePrivilegedOperation(List<String> prefixCommands,
      PrivilegedOperation operation, File workingDir,
      Map<String, String> env, boolean grabOutput, boolean inheritParentEnv)
      throws PrivilegedOperationException {
    String[] fullCommandArray = getPrivilegedOperationExecutionCommand
        (prefixCommands, operation);
    ShellCommandExecutor exec = new ShellCommandExecutor(fullCommandArray,
        workingDir, env, 0L, inheritParentEnv);

    try {
      exec.execute();
      if (LOG.isDebugEnabled()) {
        LOG.debug("command array:");
        LOG.debug(Arrays.toString(fullCommandArray));
        LOG.debug("Privileged Execution Operation Output:");
        LOG.debug(exec.getOutput());
      }
    } catch (ExitCodeException e) {
      if (operation.isFailureLoggingEnabled()) {
        StringBuilder logBuilder = new StringBuilder("Shell execution returned "
            + "exit code: ")
            .append(exec.getExitCode())
            .append(". Privileged Execution Operation Stderr: ")
            .append(System.lineSeparator())
            .append(e.getMessage())
            .append(System.lineSeparator())
            .append("Stdout: " + exec.getOutput())
            .append(System.lineSeparator());
        logBuilder.append("Full command array for failed execution: ")
            .append(System.lineSeparator());
        logBuilder.append(Arrays.toString(fullCommandArray));

        LOG.warn(logBuilder.toString());
      }

      //stderr from shell executor seems to be stuffed into the exception
      //'message' - so, we have to extract it and set it as the error out
      throw new PrivilegedOperationException(e, e.getExitCode(),
          exec.getOutput(), e.getMessage());
    } catch (IOException e) {
      LOG.warn("IOException executing command: ", e);
      throw new PrivilegedOperationException(e);
    }

    if (grabOutput) {
      return exec.getOutput();
    }

    return null;
  }

  /**
   * Executes a privileged operation. It is up to the callers to ensure that
   * each privileged operation's parameters are constructed correctly. The
   * parameters are passed verbatim to the container-executor binary.
   *
   * @param operation  the type and arguments for the operation to be executed
   * @param grabOutput return (possibly large) shell command output
   * @return stdout contents from shell executor - useful for some privileged
   * operations - e.g --tc_read
   * @throws org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.privileged.PrivilegedOperationException
   */
  public String executePrivilegedOperation(PrivilegedOperation operation,
      boolean grabOutput) throws PrivilegedOperationException {
    return executePrivilegedOperation(null, operation, null, null, grabOutput,
        false);
  }

  //Utility functions for squashing together operations in supported ways
  //At some point, we need to create a generalized mechanism that uses a set
  //of squashing 'rules' to squash an set of PrivilegedOperations of varying
  //types - e.g Launch Container + Add Pid to CGroup(s) + TC rules

  /**
   * Squash operations for cgroups - e.g mount, add pid to cgroup etc .,
   * For now, we only implement squashing for 'add pid to cgroup' since this
   * is the only optimization relevant to launching containers
   *
   * @return single squashed cgroup operation. Null on failure.
   */

  public static PrivilegedOperation squashCGroupOperations
  (List<PrivilegedOperation> ops) throws PrivilegedOperationException {
    if (ops.size() == 0) {
      return null;
    }

    StringBuffer finalOpArg = new StringBuffer(PrivilegedOperation
        .CGROUP_ARG_PREFIX);
    boolean noTasks = true;

    for (PrivilegedOperation op : ops) {
      if (!op.getOperationType()
          .equals(PrivilegedOperation.OperationType.ADD_PID_TO_CGROUP)) {
        LOG.warn("Unsupported operation type: " + op.getOperationType());
        throw new PrivilegedOperationException("Unsupported operation type:"
            + op.getOperationType());
      }

      List<String> args = op.getArguments();
      if (args.size() != 1) {
        LOG.warn("Invalid number of args: " + args.size());
        throw new PrivilegedOperationException("Invalid number of args: "
            + args.size());
      }

      String arg = args.get(0);
      String tasksFile = StringUtils.substringAfter(arg,
          PrivilegedOperation.CGROUP_ARG_PREFIX);
      if (tasksFile == null || tasksFile.isEmpty()) {
        LOG.warn("Invalid argument: " + arg);
        throw new PrivilegedOperationException("Invalid argument: " + arg);
      }

      if (tasksFile.equals(PrivilegedOperation.CGROUP_ARG_NO_TASKS)) {
        //Don't append to finalOpArg
        continue;
      }

      if (noTasks == false) {
        //We have already appended at least one tasks file.
        finalOpArg.append(PrivilegedOperation.LINUX_FILE_PATH_SEPARATOR);
        finalOpArg.append(tasksFile);
      } else {
        finalOpArg.append(tasksFile);
        noTasks = false;
      }
    }

    if (noTasks) {
      finalOpArg.append(PrivilegedOperation.CGROUP_ARG_NO_TASKS); //there
      // were no tasks file to append
    }

    PrivilegedOperation finalOp = new PrivilegedOperation(
        PrivilegedOperation.OperationType.ADD_PID_TO_CGROUP, finalOpArg
        .toString());

    return finalOp;
  }
}