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 }