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}