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
019package org.apache.hadoop.net;
020
021import java.util.*;
022import java.io.*;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.apache.hadoop.util.Shell.ShellCommandExecutor;
027import org.apache.hadoop.classification.InterfaceAudience;
028import org.apache.hadoop.classification.InterfaceStability;
029import org.apache.hadoop.conf.Configuration;
030import org.apache.hadoop.fs.CommonConfigurationKeys;
031
032/**
033 * This class implements the {@link DNSToSwitchMapping} interface using a 
034 * script configured via the
035 * {@link CommonConfigurationKeys#NET_TOPOLOGY_SCRIPT_FILE_NAME_KEY} option.
036 * <p/>
037 * It contains a static class <code>RawScriptBasedMapping</code> that performs
038 * the work: reading the configuration parameters, executing any defined
039 * script, handling errors and such like. The outer
040 * class extends {@link CachedDNSToSwitchMapping} to cache the delegated
041 * queries.
042 * <p/>
043 * This DNS mapper's {@link #isSingleSwitch()} predicate returns
044 * true if and only if a script is defined.
045 */
046@InterfaceAudience.Public
047@InterfaceStability.Evolving
048public class ScriptBasedMapping extends CachedDNSToSwitchMapping {
049
050  /**
051   * Minimum number of arguments: {@value}
052   */
053  static final int MIN_ALLOWABLE_ARGS = 1;
054
055  /**
056   * Default number of arguments: {@value}
057   */
058  static final int DEFAULT_ARG_COUNT = 
059                     CommonConfigurationKeys.NET_TOPOLOGY_SCRIPT_NUMBER_ARGS_DEFAULT;
060
061  /**
062   * key to the script filename {@value}
063   */
064  static final String SCRIPT_FILENAME_KEY = 
065                     CommonConfigurationKeys.NET_TOPOLOGY_SCRIPT_FILE_NAME_KEY ;
066
067  /**
068   * key to the argument count that the script supports
069   * {@value}
070   */
071  static final String SCRIPT_ARG_COUNT_KEY =
072                     CommonConfigurationKeys.NET_TOPOLOGY_SCRIPT_NUMBER_ARGS_KEY ;
073  /**
074   * Text used in the {@link #toString()} method if there is no string
075   * {@value}
076   */
077  public static final String NO_SCRIPT = "no script";
078
079  /**
080   * Create an instance with the default configuration.
081   * </p>
082   * Calling {@link #setConf(Configuration)} will trigger a
083   * re-evaluation of the configuration settings and so be used to
084   * set up the mapping script.
085   *
086   */
087  public ScriptBasedMapping() {
088    this(new RawScriptBasedMapping());
089  }
090
091  /**
092   * Create an instance from the given raw mapping
093   * @param rawMap raw DNSTOSwithMapping
094   */
095  public ScriptBasedMapping(DNSToSwitchMapping rawMap) {
096    super(rawMap);
097  }
098
099  /**
100   * Create an instance from the given configuration
101   * @param conf configuration
102   */
103  public ScriptBasedMapping(Configuration conf) {
104    this();
105    setConf(conf);
106  }
107
108  /**
109   * Get the cached mapping and convert it to its real type
110   * @return the inner raw script mapping.
111   */
112  private RawScriptBasedMapping getRawMapping() {
113    return (RawScriptBasedMapping)rawMapping;
114  }
115
116  @Override
117  public Configuration getConf() {
118    return getRawMapping().getConf();
119  }
120
121  @Override
122  public String toString() {
123    return "script-based mapping with " + getRawMapping().toString();
124  }
125
126  /**
127   * {@inheritDoc}
128   * <p/>
129   * This will get called in the superclass constructor, so a check is needed
130   * to ensure that the raw mapping is defined before trying to relaying a null
131   * configuration.
132   * @param conf
133   */
134  @Override
135  public void setConf(Configuration conf) {
136    super.setConf(conf);
137    getRawMapping().setConf(conf);
138  }
139
140  /**
141   * This is the uncached script mapping that is fed into the cache managed
142   * by the superclass {@link CachedDNSToSwitchMapping}
143   */
144  protected static class RawScriptBasedMapping
145      extends AbstractDNSToSwitchMapping {
146    private String scriptName;
147    private int maxArgs; //max hostnames per call of the script
148    private static final Log LOG =
149        LogFactory.getLog(ScriptBasedMapping.class);
150
151    /**
152     * Set the configuration and extract the configuration parameters of interest
153     * @param conf the new configuration
154     */
155    @Override
156    public void setConf (Configuration conf) {
157      super.setConf(conf);
158      if (conf != null) {
159        scriptName = conf.get(SCRIPT_FILENAME_KEY);
160        maxArgs = conf.getInt(SCRIPT_ARG_COUNT_KEY, DEFAULT_ARG_COUNT);
161      } else {
162        scriptName = null;
163        maxArgs = 0;
164      }
165    }
166
167    /**
168     * Constructor. The mapping is not ready to use until
169     * {@link #setConf(Configuration)} has been called
170     */
171    public RawScriptBasedMapping() {}
172
173    @Override
174    public List<String> resolve(List<String> names) {
175      List<String> m = new ArrayList<String>(names.size());
176
177      if (names.isEmpty()) {
178        return m;
179      }
180
181      if (scriptName == null) {
182        for (String name : names) {
183          m.add(NetworkTopology.DEFAULT_RACK);
184        }
185        return m;
186      }
187
188      String output = runResolveCommand(names, scriptName);
189      if (output != null) {
190        StringTokenizer allSwitchInfo = new StringTokenizer(output);
191        while (allSwitchInfo.hasMoreTokens()) {
192          String switchInfo = allSwitchInfo.nextToken();
193          m.add(switchInfo);
194        }
195
196        if (m.size() != names.size()) {
197          // invalid number of entries returned by the script
198          LOG.error("Script " + scriptName + " returned "
199              + Integer.toString(m.size()) + " values when "
200              + Integer.toString(names.size()) + " were expected.");
201          return null;
202        }
203      } else {
204        // an error occurred. return null to signify this.
205        // (exn was already logged in runResolveCommand)
206        return null;
207      }
208
209      return m;
210    }
211
212    /**
213     * Build and execute the resolution command. The command is
214     * executed in the directory specified by the system property
215     * "user.dir" if set; otherwise the current working directory is used
216     * @param args a list of arguments
217     * @return null if the number of arguments is out of range,
218     * or the output of the command.
219     */
220    protected String runResolveCommand(List<String> args, 
221        String commandScriptName) {
222      int loopCount = 0;
223      if (args.size() == 0) {
224        return null;
225      }
226      StringBuilder allOutput = new StringBuilder();
227      int numProcessed = 0;
228      if (maxArgs < MIN_ALLOWABLE_ARGS) {
229        LOG.warn("Invalid value " + Integer.toString(maxArgs)
230            + " for " + SCRIPT_ARG_COUNT_KEY + "; must be >= "
231            + Integer.toString(MIN_ALLOWABLE_ARGS));
232        return null;
233      }
234
235      while (numProcessed != args.size()) {
236        int start = maxArgs * loopCount;
237        List<String> cmdList = new ArrayList<String>();
238        cmdList.add(commandScriptName);
239        for (numProcessed = start; numProcessed < (start + maxArgs) &&
240            numProcessed < args.size(); numProcessed++) {
241          cmdList.add(args.get(numProcessed));
242        }
243        File dir = null;
244        String userDir;
245        if ((userDir = System.getProperty("user.dir")) != null) {
246          dir = new File(userDir);
247        }
248        ShellCommandExecutor s = new ShellCommandExecutor(
249            cmdList.toArray(new String[cmdList.size()]), dir);
250        try {
251          s.execute();
252          allOutput.append(s.getOutput()).append(" ");
253        } catch (Exception e) {
254          LOG.warn("Exception running " + s, e);
255          return null;
256        }
257        loopCount++;
258      }
259      return allOutput.toString();
260    }
261
262    /**
263     * Declare that the mapper is single-switched if a script was not named
264     * in the configuration.
265     * @return true iff there is no script
266     */
267    @Override
268    public boolean isSingleSwitch() {
269      return scriptName == null;
270    }
271
272    @Override
273    public String toString() {
274      return scriptName != null ? ("script " + scriptName) : NO_SCRIPT;
275    }
276
277    @Override
278    public void reloadCachedMappings() {
279      // Nothing to do here, since RawScriptBasedMapping has no cache, and
280      // does not inherit from CachedDNSToSwitchMapping
281    }
282
283    @Override
284    public void reloadCachedMappings(List<String> names) {
285      // Nothing to do here, since RawScriptBasedMapping has no cache, and
286      // does not inherit from CachedDNSToSwitchMapping
287    }
288  }
289}