blob: 766224e5d381da962bfd60399ec5e0b818d9de7e [file] [log] [blame]
/**
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.tool.coprocessor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.hbase.HBaseInterfaceAudience;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.CoprocessorDescriptor;
import org.apache.hadoop.hbase.client.TableDescriptor;
import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
import org.apache.hadoop.hbase.tool.PreUpgradeValidator;
import org.apache.hadoop.hbase.tool.coprocessor.CoprocessorViolation.Severity;
import org.apache.hadoop.hbase.util.AbstractHBaseTool;
import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.TOOLS)
public class CoprocessorValidator extends AbstractHBaseTool {
private static final Logger LOG = LoggerFactory
.getLogger(CoprocessorValidator.class);
private CoprocessorMethods branch1;
private CoprocessorMethods current;
private final List<String> jars;
private final List<Pattern> tablePatterns;
private final List<String> classes;
private boolean config;
private boolean dieOnWarnings;
public CoprocessorValidator() {
branch1 = new Branch1CoprocessorMethods();
current = new CurrentCoprocessorMethods();
jars = new ArrayList<>();
tablePatterns = new ArrayList<>();
classes = new ArrayList<>();
}
/**
* This classloader implementation calls {@link #resolveClass(Class)}
* method for every loaded class. It means that some extra validation will
* take place <a
* href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.3">
* according to JLS</a>.
*/
private static final class ResolverUrlClassLoader extends URLClassLoader {
private ResolverUrlClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, true);
}
}
private ResolverUrlClassLoader createClassLoader(URL[] urls) {
return createClassLoader(urls, getClass().getClassLoader());
}
private ResolverUrlClassLoader createClassLoader(URL[] urls, ClassLoader parent) {
return AccessController.doPrivileged(new PrivilegedAction<ResolverUrlClassLoader>() {
@Override
public ResolverUrlClassLoader run() {
return new ResolverUrlClassLoader(urls, parent);
}
});
}
private ResolverUrlClassLoader createClassLoader(ClassLoader parent,
org.apache.hadoop.fs.Path path) throws IOException {
Path tempPath = Files.createTempFile("hbase-coprocessor-", ".jar");
org.apache.hadoop.fs.Path destination = new org.apache.hadoop.fs.Path(tempPath.toString());
LOG.debug("Copying coprocessor jar '{}' to '{}'.", path, tempPath);
FileSystem fileSystem = FileSystem.get(getConf());
fileSystem.copyToLocalFile(path, destination);
URL url = tempPath.toUri().toURL();
return createClassLoader(new URL[] { url }, parent);
}
private void validate(ClassLoader classLoader, String className,
List<CoprocessorViolation> violations) {
LOG.debug("Validating class '{}'.", className);
try {
Class<?> clazz = classLoader.loadClass(className);
for (Method method : clazz.getDeclaredMethods()) {
LOG.trace("Validating method '{}'.", method);
if (branch1.hasMethod(method) && !current.hasMethod(method)) {
CoprocessorViolation violation = new CoprocessorViolation(
className, Severity.WARNING, "method '" + method +
"' was removed from new coprocessor API, so it won't be called by HBase");
violations.add(violation);
}
}
} catch (ClassNotFoundException e) {
CoprocessorViolation violation = new CoprocessorViolation(
className, Severity.ERROR, "no such class", e);
violations.add(violation);
} catch (RuntimeException | Error e) {
CoprocessorViolation violation = new CoprocessorViolation(
className, Severity.ERROR, "could not validate class", e);
violations.add(violation);
}
}
public void validateClasses(ClassLoader classLoader, List<String> classNames,
List<CoprocessorViolation> violations) {
for (String className : classNames) {
validate(classLoader, className, violations);
}
}
public void validateClasses(ClassLoader classLoader, String[] classNames,
List<CoprocessorViolation> violations) {
validateClasses(classLoader, Arrays.asList(classNames), violations);
}
@InterfaceAudience.Private
protected void validateTables(ClassLoader classLoader, Admin admin,
Pattern pattern, List<CoprocessorViolation> violations) throws IOException {
List<TableDescriptor> tableDescriptors = admin.listTableDescriptors(pattern);
for (TableDescriptor tableDescriptor : tableDescriptors) {
LOG.debug("Validating table {}", tableDescriptor.getTableName());
Collection<CoprocessorDescriptor> coprocessorDescriptors =
tableDescriptor.getCoprocessorDescriptors();
for (CoprocessorDescriptor coprocessorDescriptor : coprocessorDescriptors) {
String className = coprocessorDescriptor.getClassName();
Optional<String> jarPath = coprocessorDescriptor.getJarPath();
if (jarPath.isPresent()) {
org.apache.hadoop.fs.Path path = new org.apache.hadoop.fs.Path(jarPath.get());
try (ResolverUrlClassLoader cpClassLoader = createClassLoader(classLoader, path)) {
validate(cpClassLoader, className, violations);
} catch (IOException e) {
CoprocessorViolation violation = new CoprocessorViolation(
className, Severity.ERROR,
"could not validate jar file '" + path + "'", e);
violations.add(violation);
}
} else {
validate(classLoader, className, violations);
}
}
}
}
private void validateTables(ClassLoader classLoader, Pattern pattern,
List<CoprocessorViolation> violations) throws IOException {
try (Connection connection = ConnectionFactory.createConnection(getConf());
Admin admin = connection.getAdmin()) {
validateTables(classLoader, admin, pattern, violations);
}
}
@Override
protected void printUsage() {
String header = "hbase " + PreUpgradeValidator.TOOL_NAME + " " +
PreUpgradeValidator.VALIDATE_CP_NAME +
" [-jar ...] [-class ... | -table ... | -config]";
printUsage(header, "Options:", "");
}
@Override
protected void addOptions() {
addOptNoArg("e", "Treat warnings as errors.");
addOptWithArg("jar", "Jar file/directory of the coprocessor.");
addOptWithArg("table", "Table coprocessor(s) to check.");
addOptWithArg("class", "Coprocessor class(es) to check.");
addOptNoArg("config", "Obtain coprocessor class(es) from configuration.");
}
@Override
protected void processOptions(CommandLine cmd) {
String[] jars = cmd.getOptionValues("jar");
if (jars != null) {
Collections.addAll(this.jars, jars);
}
String[] tables = cmd.getOptionValues("table");
if (tables != null) {
Arrays.stream(tables).map(Pattern::compile).forEach(tablePatterns::add);
}
String[] classes = cmd.getOptionValues("class");
if (classes != null) {
Collections.addAll(this.classes, classes);
}
config = cmd.hasOption("config");
dieOnWarnings = cmd.hasOption("e");
}
private List<URL> buildClasspath(List<String> jars) throws IOException {
List<URL> urls = new ArrayList<>();
for (String jar : jars) {
Path jarPath = Paths.get(jar);
if (Files.isDirectory(jarPath)) {
try (Stream<Path> stream = Files.list(jarPath)) {
List<Path> files = stream
.filter((path) -> Files.isRegularFile(path))
.collect(Collectors.toList());
for (Path file : files) {
URL url = file.toUri().toURL();
urls.add(url);
}
}
} else {
URL url = jarPath.toUri().toURL();
urls.add(url);
}
}
return urls;
}
@Override
protected int doWork() throws Exception {
if (tablePatterns.isEmpty() && classes.isEmpty() && !config) {
LOG.error("Please give at least one -table, -class or -config parameter.");
printUsage();
return EXIT_FAILURE;
}
List<URL> urlList = buildClasspath(jars);
URL[] urls = urlList.toArray(new URL[urlList.size()]);
LOG.debug("Classpath: {}", urlList);
List<CoprocessorViolation> violations = new ArrayList<>();
try (ResolverUrlClassLoader classLoader = createClassLoader(urls)) {
for (Pattern tablePattern : tablePatterns) {
validateTables(classLoader, tablePattern, violations);
}
validateClasses(classLoader, classes, violations);
if (config) {
String[] masterCoprocessors =
getConf().getStrings(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY);
if (masterCoprocessors != null) {
validateClasses(classLoader, masterCoprocessors, violations);
}
String[] regionCoprocessors =
getConf().getStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY);
if (regionCoprocessors != null) {
validateClasses(classLoader, regionCoprocessors, violations);
}
}
}
boolean error = false;
for (CoprocessorViolation violation : violations) {
String className = violation.getClassName();
String message = violation.getMessage();
Throwable throwable = violation.getThrowable();
switch (violation.getSeverity()) {
case WARNING:
if (throwable == null) {
LOG.warn("Warning in class '{}': {}.", className, message);
} else {
LOG.warn("Warning in class '{}': {}.", className, message, throwable);
}
if (dieOnWarnings) {
error = true;
}
break;
case ERROR:
if (throwable == null) {
LOG.error("Error in class '{}': {}.", className, message);
} else {
LOG.error("Error in class '{}': {}.", className, message, throwable);
}
error = true;
break;
}
}
return (error) ? EXIT_FAILURE : EXIT_SUCCESS;
}
}