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