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.hbase.tool.coprocessor;
019
020import java.io.IOException;
021import java.lang.reflect.Method;
022import java.net.URL;
023import java.net.URLClassLoader;
024import java.nio.file.Files;
025import java.nio.file.Path;
026import java.nio.file.Paths;
027import java.security.AccessController;
028import java.security.PrivilegedAction;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.List;
034import java.util.Optional;
035import java.util.regex.Pattern;
036import java.util.stream.Collectors;
037import java.util.stream.Stream;
038import org.apache.hadoop.fs.FileSystem;
039import org.apache.hadoop.hbase.HBaseInterfaceAudience;
040import org.apache.hadoop.hbase.client.Admin;
041import org.apache.hadoop.hbase.client.Connection;
042import org.apache.hadoop.hbase.client.ConnectionFactory;
043import org.apache.hadoop.hbase.client.CoprocessorDescriptor;
044import org.apache.hadoop.hbase.client.TableDescriptor;
045import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
046import org.apache.hadoop.hbase.tool.PreUpgradeValidator;
047import org.apache.hadoop.hbase.tool.coprocessor.CoprocessorViolation.Severity;
048import org.apache.hadoop.hbase.util.AbstractHBaseTool;
049import org.apache.yetus.audience.InterfaceAudience;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
054
055@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.TOOLS)
056public class CoprocessorValidator extends AbstractHBaseTool {
057  private static final Logger LOG = LoggerFactory.getLogger(CoprocessorValidator.class);
058
059  private CoprocessorMethods branch1;
060  private CoprocessorMethods current;
061
062  private final List<String> jars;
063  private final List<Pattern> tablePatterns;
064  private final List<String> classes;
065  private boolean config;
066
067  private boolean dieOnWarnings;
068
069  public CoprocessorValidator() {
070    branch1 = new Branch1CoprocessorMethods();
071    current = new CurrentCoprocessorMethods();
072
073    jars = new ArrayList<>();
074    tablePatterns = new ArrayList<>();
075    classes = new ArrayList<>();
076  }
077
078  /**
079   * This classloader implementation calls {@link #resolveClass(Class)} method for every loaded
080   * class. It means that some extra validation will take place
081   * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.3"> according to
082   * JLS</a>.
083   */
084  private static final class ResolverUrlClassLoader extends URLClassLoader {
085    private ResolverUrlClassLoader(URL[] urls, ClassLoader parent) {
086      super(urls, parent);
087    }
088
089    @Override
090    public Class<?> loadClass(String name) throws ClassNotFoundException {
091      return loadClass(name, true);
092    }
093  }
094
095  private ResolverUrlClassLoader createClassLoader(URL[] urls) {
096    return createClassLoader(urls, getClass().getClassLoader());
097  }
098
099  private ResolverUrlClassLoader createClassLoader(URL[] urls, ClassLoader parent) {
100    return AccessController.doPrivileged(new PrivilegedAction<ResolverUrlClassLoader>() {
101      @Override
102      public ResolverUrlClassLoader run() {
103        return new ResolverUrlClassLoader(urls, parent);
104      }
105    });
106  }
107
108  private ResolverUrlClassLoader createClassLoader(ClassLoader parent,
109    org.apache.hadoop.fs.Path path) throws IOException {
110    Path tempPath = Files.createTempFile("hbase-coprocessor-", ".jar");
111    org.apache.hadoop.fs.Path destination = new org.apache.hadoop.fs.Path(tempPath.toString());
112
113    LOG.debug("Copying coprocessor jar '{}' to '{}'.", path, tempPath);
114
115    FileSystem fileSystem = FileSystem.get(getConf());
116    fileSystem.copyToLocalFile(path, destination);
117
118    URL url = tempPath.toUri().toURL();
119
120    return createClassLoader(new URL[] { url }, parent);
121  }
122
123  private void validate(ClassLoader classLoader, String className,
124    List<CoprocessorViolation> violations) {
125    LOG.debug("Validating class '{}'.", className);
126
127    try {
128      Class<?> clazz = classLoader.loadClass(className);
129
130      for (Method method : clazz.getDeclaredMethods()) {
131        LOG.trace("Validating method '{}'.", method);
132
133        if (branch1.hasMethod(method) && !current.hasMethod(method)) {
134          CoprocessorViolation violation =
135            new CoprocessorViolation(className, Severity.WARNING, "method '" + method
136              + "' was removed from new coprocessor API, so it won't be called by HBase");
137          violations.add(violation);
138        }
139      }
140    } catch (ClassNotFoundException e) {
141      CoprocessorViolation violation =
142        new CoprocessorViolation(className, Severity.ERROR, "no such class", e);
143      violations.add(violation);
144    } catch (RuntimeException | Error e) {
145      CoprocessorViolation violation =
146        new CoprocessorViolation(className, Severity.ERROR, "could not validate class", e);
147      violations.add(violation);
148    }
149  }
150
151  public void validateClasses(ClassLoader classLoader, List<String> classNames,
152    List<CoprocessorViolation> violations) {
153    for (String className : classNames) {
154      validate(classLoader, className, violations);
155    }
156  }
157
158  public void validateClasses(ClassLoader classLoader, String[] classNames,
159    List<CoprocessorViolation> violations) {
160    validateClasses(classLoader, Arrays.asList(classNames), violations);
161  }
162
163  @InterfaceAudience.Private
164  protected void validateTables(ClassLoader classLoader, Admin admin, Pattern pattern,
165    List<CoprocessorViolation> violations) throws IOException {
166    List<TableDescriptor> tableDescriptors = admin.listTableDescriptors(pattern);
167
168    for (TableDescriptor tableDescriptor : tableDescriptors) {
169      LOG.debug("Validating table {}", tableDescriptor.getTableName());
170
171      Collection<CoprocessorDescriptor> coprocessorDescriptors =
172        tableDescriptor.getCoprocessorDescriptors();
173
174      for (CoprocessorDescriptor coprocessorDescriptor : coprocessorDescriptors) {
175        String className = coprocessorDescriptor.getClassName();
176        Optional<String> jarPath = coprocessorDescriptor.getJarPath();
177
178        if (jarPath.isPresent()) {
179          org.apache.hadoop.fs.Path path = new org.apache.hadoop.fs.Path(jarPath.get());
180          try (ResolverUrlClassLoader cpClassLoader = createClassLoader(classLoader, path)) {
181            validate(cpClassLoader, className, violations);
182          } catch (IOException e) {
183            CoprocessorViolation violation = new CoprocessorViolation(className, Severity.ERROR,
184              "could not validate jar file '" + path + "'", e);
185            violations.add(violation);
186          }
187        } else {
188          validate(classLoader, className, violations);
189        }
190      }
191    }
192  }
193
194  private void validateTables(ClassLoader classLoader, Pattern pattern,
195    List<CoprocessorViolation> violations) throws IOException {
196    try (Connection connection = ConnectionFactory.createConnection(getConf());
197      Admin admin = connection.getAdmin()) {
198      validateTables(classLoader, admin, pattern, violations);
199    }
200  }
201
202  @Override
203  protected void printUsage() {
204    String header = "hbase " + PreUpgradeValidator.TOOL_NAME + " "
205      + PreUpgradeValidator.VALIDATE_CP_NAME + " [-jar ...] [-class ... | -table ... | -config]";
206    printUsage(header, "Options:", "");
207  }
208
209  @Override
210  protected void addOptions() {
211    addOptNoArg("e", "Treat warnings as errors.");
212    addOptWithArg("jar", "Jar file/directory of the coprocessor.");
213    addOptWithArg("table", "Table coprocessor(s) to check.");
214    addOptWithArg("class", "Coprocessor class(es) to check.");
215    addOptNoArg("config", "Obtain coprocessor class(es) from configuration.");
216  }
217
218  @Override
219  protected void processOptions(CommandLine cmd) {
220    String[] jars = cmd.getOptionValues("jar");
221    if (jars != null) {
222      Collections.addAll(this.jars, jars);
223    }
224
225    String[] tables = cmd.getOptionValues("table");
226    if (tables != null) {
227      Arrays.stream(tables).map(Pattern::compile).forEach(tablePatterns::add);
228    }
229
230    String[] classes = cmd.getOptionValues("class");
231    if (classes != null) {
232      Collections.addAll(this.classes, classes);
233    }
234
235    config = cmd.hasOption("config");
236    dieOnWarnings = cmd.hasOption("e");
237  }
238
239  private List<URL> buildClasspath(List<String> jars) throws IOException {
240    List<URL> urls = new ArrayList<>();
241
242    for (String jar : jars) {
243      Path jarPath = Paths.get(jar);
244      if (Files.isDirectory(jarPath)) {
245        try (Stream<Path> stream = Files.list(jarPath)) {
246          List<Path> files =
247            stream.filter((path) -> Files.isRegularFile(path)).collect(Collectors.toList());
248
249          for (Path file : files) {
250            URL url = file.toUri().toURL();
251            urls.add(url);
252          }
253        }
254      } else {
255        URL url = jarPath.toUri().toURL();
256        urls.add(url);
257      }
258    }
259
260    return urls;
261  }
262
263  @Override
264  protected int doWork() throws Exception {
265    if (tablePatterns.isEmpty() && classes.isEmpty() && !config) {
266      LOG.error("Please give at least one -table, -class or -config parameter.");
267      printUsage();
268      return EXIT_FAILURE;
269    }
270
271    List<URL> urlList = buildClasspath(jars);
272    URL[] urls = urlList.toArray(new URL[urlList.size()]);
273
274    LOG.debug("Classpath: {}", urlList);
275
276    List<CoprocessorViolation> violations = new ArrayList<>();
277
278    try (ResolverUrlClassLoader classLoader = createClassLoader(urls)) {
279      for (Pattern tablePattern : tablePatterns) {
280        validateTables(classLoader, tablePattern, violations);
281      }
282
283      validateClasses(classLoader, classes, violations);
284
285      if (config) {
286        String[] masterCoprocessors =
287          getConf().getStrings(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY);
288        if (masterCoprocessors != null) {
289          validateClasses(classLoader, masterCoprocessors, violations);
290        }
291
292        String[] regionCoprocessors =
293          getConf().getStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY);
294        if (regionCoprocessors != null) {
295          validateClasses(classLoader, regionCoprocessors, violations);
296        }
297      }
298    }
299
300    boolean error = false;
301
302    for (CoprocessorViolation violation : violations) {
303      String className = violation.getClassName();
304      String message = violation.getMessage();
305      Throwable throwable = violation.getThrowable();
306
307      switch (violation.getSeverity()) {
308        case WARNING:
309          if (throwable == null) {
310            LOG.warn("Warning in class '{}': {}.", className, message);
311          } else {
312            LOG.warn("Warning in class '{}': {}.", className, message, throwable);
313          }
314
315          if (dieOnWarnings) {
316            error = true;
317          }
318
319          break;
320        case ERROR:
321          if (throwable == null) {
322            LOG.error("Error in class '{}': {}.", className, message);
323          } else {
324            LOG.error("Error in class '{}': {}.", className, message, throwable);
325          }
326
327          error = true;
328
329          break;
330      }
331    }
332
333    return (error) ? EXIT_FAILURE : EXIT_SUCCESS;
334  }
335}