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}