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     */
018    package org.apache.hadoop.ha;
019    
020    import java.io.IOException;
021    import java.lang.reflect.Field;
022    import java.util.Map;
023    
024    import org.apache.commons.logging.Log;
025    import org.apache.commons.logging.LogFactory;
026    import org.apache.hadoop.conf.Configured;
027    
028    import com.google.common.annotations.VisibleForTesting;
029    import 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     */
054    public 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    }