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