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 */
018package org.apache.hadoop.ha;
019
020import java.io.IOException;
021import java.lang.reflect.Field;
022import java.util.Map;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.apache.hadoop.conf.Configured;
027
028import com.google.common.annotations.VisibleForTesting;
029
030/**
031 * Fencing method that runs a shell command. It should be specified
032 * in the fencing configuration like:<br>
033 * <code>
034 *   shell(/path/to/my/script.sh arg1 arg2 ...)
035 * </code><br>
036 * The string between '(' and ')' is passed directly to a bash shell and
037 * may not include any closing parentheses.<p>
038 * 
039 * The shell command will be run with an environment set up to contain
040 * all of the current Hadoop configuration variables, with the '_' character 
041 * replacing any '.' characters in the configuration keys.<p>
042 * 
043 * If the shell command returns an exit code of 0, the fencing is
044 * determined to be successful. If it returns any other exit code, the
045 * fencing was not successful and the next fencing method in the list
046 * will be attempted.<p>
047 * 
048 * <em>Note:</em> this fencing method does not implement any timeout.
049 * If timeouts are necessary, they should be implemented in the shell
050 * script itself (eg by forking a subshell to kill its parent in
051 * some number of seconds).
052 */
053public class ShellCommandFencer
054  extends Configured implements FenceMethod {
055
056  /** Length at which to abbreviate command in long messages */
057  private static final int ABBREV_LENGTH = 20;
058
059  /** Prefix for target parameters added to the environment */
060  private static final String TARGET_PREFIX = "target_";
061  
062  @VisibleForTesting
063  static Log LOG = LogFactory.getLog(
064      ShellCommandFencer.class);
065  
066  @Override
067  public void checkArgs(String args) throws BadFencingConfigurationException {
068    if (args == null || args.isEmpty()) {
069      throw new BadFencingConfigurationException(
070          "No argument passed to 'shell' fencing method");
071    }
072    // Nothing else we can really check without actually running the command
073  }
074
075  @Override
076  public boolean tryFence(HAServiceTarget target, String cmd) {
077    ProcessBuilder builder = new ProcessBuilder(
078        "bash", "-e", "-c", cmd);
079    setConfAsEnvVars(builder.environment());
080    addTargetInfoAsEnvVars(target, builder.environment());
081
082    Process p;
083    try {
084      p = builder.start();
085      p.getOutputStream().close();
086    } catch (IOException e) {
087      LOG.warn("Unable to execute " + cmd, e);
088      return false;
089    }
090    
091    String pid = tryGetPid(p);
092    LOG.info("Launched fencing command '" + cmd + "' with "
093        + ((pid != null) ? ("pid " + pid) : "unknown pid"));
094    
095    String logPrefix = abbreviate(cmd, ABBREV_LENGTH);
096    if (pid != null) {
097      logPrefix = "[PID " + pid + "] " + logPrefix;
098    }
099    
100    // Pump logs to stderr
101    StreamPumper errPumper = new StreamPumper(
102        LOG, logPrefix, p.getErrorStream(),
103        StreamPumper.StreamType.STDERR);
104    errPumper.start();
105    
106    StreamPumper outPumper = new StreamPumper(
107        LOG, logPrefix, p.getInputStream(),
108        StreamPumper.StreamType.STDOUT);
109    outPumper.start();
110    
111    int rc;
112    try {
113      rc = p.waitFor();
114      errPumper.join();
115      outPumper.join();
116    } catch (InterruptedException ie) {
117      LOG.warn("Interrupted while waiting for fencing command: " + cmd);
118      return false;
119    }
120    
121    return rc == 0;
122  }
123
124  /**
125   * Abbreviate a string by putting '...' in the middle of it,
126   * in an attempt to keep logs from getting too messy.
127   * @param cmd the string to abbreviate
128   * @param len maximum length to abbreviate to
129   * @return abbreviated string
130   */
131  static String abbreviate(String cmd, int len) {
132    if (cmd.length() > len && len >= 5) {
133      int firstHalf = (len - 3) / 2;
134      int rem = len - firstHalf - 3;
135      
136      return cmd.substring(0, firstHalf) + 
137        "..." + cmd.substring(cmd.length() - rem);
138    } else {
139      return cmd;
140    }
141  }
142  
143  /**
144   * Attempt to use evil reflection tricks to determine the
145   * pid of a launched process. This is helpful to ops
146   * if debugging a fencing process that might have gone
147   * wrong. If running on a system or JVM where this doesn't
148   * work, it will simply return null.
149   */
150  private static String tryGetPid(Process p) {
151    try {
152      Class<? extends Process> clazz = p.getClass();
153      if (clazz.getName().equals("java.lang.UNIXProcess")) {
154        Field f = clazz.getDeclaredField("pid");
155        f.setAccessible(true);
156        return String.valueOf(f.getInt(p));
157      } else {
158        LOG.trace("Unable to determine pid for " + p
159            + " since it is not a UNIXProcess");
160        return null;
161      }
162    } catch (Throwable t) {
163      LOG.trace("Unable to determine pid for " + p, t);
164      return null;
165    }
166  }
167
168  /**
169   * Set the environment of the subprocess to be the Configuration,
170   * with '.'s replaced by '_'s.
171   */
172  private void setConfAsEnvVars(Map<String, String> env) {
173    for (Map.Entry<String, String> pair : getConf()) {
174      env.put(pair.getKey().replace('.', '_'), pair.getValue());
175    }
176  }
177
178  /**
179   * Add information about the target to the the environment of the
180   * subprocess.
181   * 
182   * @param target
183   * @param environment
184   */
185  private void addTargetInfoAsEnvVars(HAServiceTarget target,
186      Map<String, String> environment) {
187    for (Map.Entry<String, String> e :
188         target.getFencingParameters().entrySet()) {
189      String key = TARGET_PREFIX + e.getKey();
190      key = key.replace('.', '_');
191      environment.put(key, e.getValue());
192    }
193  }
194}