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.snapshot;
019
020import java.io.BufferedInputStream;
021import java.io.DataInput;
022import java.io.DataOutput;
023import java.io.FileNotFoundException;
024import java.io.IOException;
025import java.io.InputStream;
026import java.util.ArrayList;
027import java.util.Collections;
028import java.util.Comparator;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.concurrent.ExecutionException;
032import java.util.concurrent.ExecutorService;
033import java.util.concurrent.Executors;
034import java.util.concurrent.Future;
035import java.util.function.BiConsumer;
036import org.apache.hadoop.conf.Configuration;
037import org.apache.hadoop.fs.FSDataInputStream;
038import org.apache.hadoop.fs.FSDataOutputStream;
039import org.apache.hadoop.fs.FileChecksum;
040import org.apache.hadoop.fs.FileStatus;
041import org.apache.hadoop.fs.FileSystem;
042import org.apache.hadoop.fs.Path;
043import org.apache.hadoop.fs.permission.FsPermission;
044import org.apache.hadoop.hbase.HBaseConfiguration;
045import org.apache.hadoop.hbase.HConstants;
046import org.apache.hadoop.hbase.TableName;
047import org.apache.hadoop.hbase.client.RegionInfo;
048import org.apache.hadoop.hbase.io.FileLink;
049import org.apache.hadoop.hbase.io.HFileLink;
050import org.apache.hadoop.hbase.io.WALLink;
051import org.apache.hadoop.hbase.io.hadoopbackport.ThrottledInputStream;
052import org.apache.hadoop.hbase.mapreduce.TableMapReduceUtil;
053import org.apache.hadoop.hbase.mob.MobUtils;
054import org.apache.hadoop.hbase.regionserver.StoreFileInfo;
055import org.apache.hadoop.hbase.util.AbstractHBaseTool;
056import org.apache.hadoop.hbase.util.CommonFSUtils;
057import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
058import org.apache.hadoop.hbase.util.FSUtils;
059import org.apache.hadoop.hbase.util.HFileArchiveUtil;
060import org.apache.hadoop.hbase.util.Pair;
061import org.apache.hadoop.io.BytesWritable;
062import org.apache.hadoop.io.NullWritable;
063import org.apache.hadoop.io.Writable;
064import org.apache.hadoop.mapreduce.InputFormat;
065import org.apache.hadoop.mapreduce.InputSplit;
066import org.apache.hadoop.mapreduce.Job;
067import org.apache.hadoop.mapreduce.JobContext;
068import org.apache.hadoop.mapreduce.Mapper;
069import org.apache.hadoop.mapreduce.RecordReader;
070import org.apache.hadoop.mapreduce.TaskAttemptContext;
071import org.apache.hadoop.mapreduce.lib.output.NullOutputFormat;
072import org.apache.hadoop.mapreduce.security.TokenCache;
073import org.apache.hadoop.util.StringUtils;
074import org.apache.hadoop.util.Tool;
075import org.apache.yetus.audience.InterfaceAudience;
076import org.slf4j.Logger;
077import org.slf4j.LoggerFactory;
078
079import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
080import org.apache.hbase.thirdparty.org.apache.commons.cli.Option;
081
082import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
083import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription;
084import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotFileInfo;
085import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotRegionManifest;
086
087/**
088 * Export the specified snapshot to a given FileSystem. The .snapshot/name folder is copied to the
089 * destination cluster and then all the hfiles/wals are copied using a Map-Reduce Job in the
090 * .archive/ location. When everything is done, the second cluster can restore the snapshot.
091 */
092@InterfaceAudience.Public
093public class ExportSnapshot extends AbstractHBaseTool implements Tool {
094  public static final String NAME = "exportsnapshot";
095  /** Configuration prefix for overrides for the source filesystem */
096  public static final String CONF_SOURCE_PREFIX = NAME + ".from.";
097  /** Configuration prefix for overrides for the destination filesystem */
098  public static final String CONF_DEST_PREFIX = NAME + ".to.";
099
100  private static final Logger LOG = LoggerFactory.getLogger(ExportSnapshot.class);
101
102  private static final String MR_NUM_MAPS = "mapreduce.job.maps";
103  private static final String CONF_NUM_SPLITS = "snapshot.export.format.splits";
104  private static final String CONF_SNAPSHOT_NAME = "snapshot.export.format.snapshot.name";
105  private static final String CONF_SNAPSHOT_DIR = "snapshot.export.format.snapshot.dir";
106  private static final String CONF_FILES_USER = "snapshot.export.files.attributes.user";
107  private static final String CONF_FILES_GROUP = "snapshot.export.files.attributes.group";
108  private static final String CONF_FILES_MODE = "snapshot.export.files.attributes.mode";
109  private static final String CONF_CHECKSUM_VERIFY = "snapshot.export.checksum.verify";
110  private static final String CONF_OUTPUT_ROOT = "snapshot.export.output.root";
111  private static final String CONF_INPUT_ROOT = "snapshot.export.input.root";
112  private static final String CONF_BUFFER_SIZE = "snapshot.export.buffer.size";
113  private static final String CONF_MAP_GROUP = "snapshot.export.default.map.group";
114  private static final String CONF_BANDWIDTH_MB = "snapshot.export.map.bandwidth.mb";
115  private static final String CONF_MR_JOB_NAME = "mapreduce.job.name";
116  protected static final String CONF_SKIP_TMP = "snapshot.export.skip.tmp";
117  private static final String CONF_COPY_MANIFEST_THREADS =
118    "snapshot.export.copy.references.threads";
119  private static final int DEFAULT_COPY_MANIFEST_THREADS =
120    Runtime.getRuntime().availableProcessors();
121
122  static class Testing {
123    static final String CONF_TEST_FAILURE = "test.snapshot.export.failure";
124    static final String CONF_TEST_FAILURE_COUNT = "test.snapshot.export.failure.count";
125    int failuresCountToInject = 0;
126    int injectedFailureCount = 0;
127  }
128
129  // Command line options and defaults.
130  static final class Options {
131    static final Option SNAPSHOT = new Option(null, "snapshot", true, "Snapshot to restore.");
132    static final Option TARGET_NAME =
133      new Option(null, "target", true, "Target name for the snapshot.");
134    static final Option COPY_TO =
135      new Option(null, "copy-to", true, "Remote " + "destination hdfs://");
136    static final Option COPY_FROM =
137      new Option(null, "copy-from", true, "Input folder hdfs:// (default hbase.rootdir)");
138    static final Option NO_CHECKSUM_VERIFY = new Option(null, "no-checksum-verify", false,
139      "Do not verify checksum, use name+length only.");
140    static final Option NO_TARGET_VERIFY = new Option(null, "no-target-verify", false,
141      "Do not verify the exported snapshot's expiration status and integrity.");
142    static final Option NO_SOURCE_VERIFY = new Option(null, "no-source-verify", false,
143      "Do not verify the source snapshot's expiration status and integrity.");
144    static final Option OVERWRITE =
145      new Option(null, "overwrite", false, "Rewrite the snapshot manifest if already exists.");
146    static final Option CHUSER =
147      new Option(null, "chuser", true, "Change the owner of the files to the specified one.");
148    static final Option CHGROUP =
149      new Option(null, "chgroup", true, "Change the group of the files to the specified one.");
150    static final Option CHMOD =
151      new Option(null, "chmod", true, "Change the permission of the files to the specified one.");
152    static final Option MAPPERS = new Option(null, "mappers", true,
153      "Number of mappers to use during the copy (mapreduce.job.maps).");
154    static final Option BANDWIDTH =
155      new Option(null, "bandwidth", true, "Limit bandwidth to this value in MB/second.");
156    static final Option RESET_TTL =
157      new Option(null, "reset-ttl", false, "Do not copy TTL for the snapshot");
158  }
159
160  // Export Map-Reduce Counters, to keep track of the progress
161  public enum Counter {
162    MISSING_FILES,
163    FILES_COPIED,
164    FILES_SKIPPED,
165    COPY_FAILED,
166    BYTES_EXPECTED,
167    BYTES_SKIPPED,
168    BYTES_COPIED
169  }
170
171  /**
172   * Indicates the checksum comparison result.
173   */
174  public enum ChecksumComparison {
175    TRUE, // checksum comparison is compatible and true.
176    FALSE, // checksum comparison is compatible and false.
177    INCOMPATIBLE, // checksum comparison is not compatible.
178  }
179
180  private static class ExportMapper
181    extends Mapper<BytesWritable, NullWritable, NullWritable, NullWritable> {
182    private static final Logger LOG = LoggerFactory.getLogger(ExportMapper.class);
183    final static int REPORT_SIZE = 1 * 1024 * 1024;
184    final static int BUFFER_SIZE = 64 * 1024;
185
186    private boolean verifyChecksum;
187    private String filesGroup;
188    private String filesUser;
189    private short filesMode;
190    private int bufferSize;
191
192    private FileSystem outputFs;
193    private Path outputArchive;
194    private Path outputRoot;
195
196    private FileSystem inputFs;
197    private Path inputArchive;
198    private Path inputRoot;
199
200    private static Testing testing = new Testing();
201
202    @Override
203    public void setup(Context context) throws IOException {
204      Configuration conf = context.getConfiguration();
205
206      Configuration srcConf = HBaseConfiguration.createClusterConf(conf, null, CONF_SOURCE_PREFIX);
207      Configuration destConf = HBaseConfiguration.createClusterConf(conf, null, CONF_DEST_PREFIX);
208
209      verifyChecksum = conf.getBoolean(CONF_CHECKSUM_VERIFY, true);
210
211      filesGroup = conf.get(CONF_FILES_GROUP);
212      filesUser = conf.get(CONF_FILES_USER);
213      filesMode = (short) conf.getInt(CONF_FILES_MODE, 0);
214      outputRoot = new Path(conf.get(CONF_OUTPUT_ROOT));
215      inputRoot = new Path(conf.get(CONF_INPUT_ROOT));
216
217      inputArchive = new Path(inputRoot, HConstants.HFILE_ARCHIVE_DIRECTORY);
218      outputArchive = new Path(outputRoot, HConstants.HFILE_ARCHIVE_DIRECTORY);
219
220      try {
221        inputFs = FileSystem.get(inputRoot.toUri(), srcConf);
222      } catch (IOException e) {
223        throw new IOException("Could not get the input FileSystem with root=" + inputRoot, e);
224      }
225
226      try {
227        outputFs = FileSystem.get(outputRoot.toUri(), destConf);
228      } catch (IOException e) {
229        throw new IOException("Could not get the output FileSystem with root=" + outputRoot, e);
230      }
231
232      // Use the default block size of the outputFs if bigger
233      int defaultBlockSize = Math.max((int) outputFs.getDefaultBlockSize(outputRoot), BUFFER_SIZE);
234      bufferSize = conf.getInt(CONF_BUFFER_SIZE, defaultBlockSize);
235      LOG.info("Using bufferSize=" + StringUtils.humanReadableInt(bufferSize));
236
237      for (Counter c : Counter.values()) {
238        context.getCounter(c).increment(0);
239      }
240      if (context.getConfiguration().getBoolean(Testing.CONF_TEST_FAILURE, false)) {
241        testing.failuresCountToInject = conf.getInt(Testing.CONF_TEST_FAILURE_COUNT, 0);
242        // Get number of times we have already injected failure based on attempt number of this
243        // task.
244        testing.injectedFailureCount = context.getTaskAttemptID().getId();
245      }
246    }
247
248    @Override
249    public void map(BytesWritable key, NullWritable value, Context context)
250      throws InterruptedException, IOException {
251      SnapshotFileInfo inputInfo = SnapshotFileInfo.parseFrom(key.copyBytes());
252      Path outputPath = getOutputPath(inputInfo);
253
254      copyFile(context, inputInfo, outputPath);
255    }
256
257    /**
258     * Returns the location where the inputPath will be copied.
259     */
260    private Path getOutputPath(final SnapshotFileInfo inputInfo) throws IOException {
261      Path path = null;
262      switch (inputInfo.getType()) {
263        case HFILE:
264          Path inputPath = new Path(inputInfo.getHfile());
265          String family = inputPath.getParent().getName();
266          TableName table = HFileLink.getReferencedTableName(inputPath.getName());
267          String region = HFileLink.getReferencedRegionName(inputPath.getName());
268          String hfile = HFileLink.getReferencedHFileName(inputPath.getName());
269          path = new Path(CommonFSUtils.getTableDir(new Path("./"), table),
270            new Path(region, new Path(family, hfile)));
271          break;
272        case WAL:
273          LOG.warn("snapshot does not keeps WALs: " + inputInfo);
274          break;
275        default:
276          throw new IOException("Invalid File Type: " + inputInfo.getType().toString());
277      }
278      return new Path(outputArchive, path);
279    }
280
281    @SuppressWarnings("checkstyle:linelength")
282    /**
283     * Used by TestExportSnapshot to test for retries when failures happen. Failure is injected in
284     * {@link #copyFile(Mapper.Context, org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotFileInfo, Path)}.
285     */
286    private void injectTestFailure(final Context context, final SnapshotFileInfo inputInfo)
287      throws IOException {
288      if (!context.getConfiguration().getBoolean(Testing.CONF_TEST_FAILURE, false)) return;
289      if (testing.injectedFailureCount >= testing.failuresCountToInject) return;
290      testing.injectedFailureCount++;
291      context.getCounter(Counter.COPY_FAILED).increment(1);
292      LOG.debug("Injecting failure. Count: " + testing.injectedFailureCount);
293      throw new IOException(String.format("TEST FAILURE (%d of max %d): Unable to copy input=%s",
294        testing.injectedFailureCount, testing.failuresCountToInject, inputInfo));
295    }
296
297    private void copyFile(final Context context, final SnapshotFileInfo inputInfo,
298      final Path outputPath) throws IOException {
299      // Get the file information
300      FileStatus inputStat = getSourceFileStatus(context, inputInfo);
301
302      // Verify if the output file exists and is the same that we want to copy
303      if (outputFs.exists(outputPath)) {
304        FileStatus outputStat = outputFs.getFileStatus(outputPath);
305        if (outputStat != null && sameFile(inputStat, outputStat)) {
306          LOG.info("Skip copy " + inputStat.getPath() + " to " + outputPath + ", same file.");
307          context.getCounter(Counter.FILES_SKIPPED).increment(1);
308          context.getCounter(Counter.BYTES_SKIPPED).increment(inputStat.getLen());
309          return;
310        }
311      }
312
313      InputStream in = openSourceFile(context, inputInfo);
314      int bandwidthMB = context.getConfiguration().getInt(CONF_BANDWIDTH_MB, 100);
315      if (Integer.MAX_VALUE != bandwidthMB) {
316        in = new ThrottledInputStream(new BufferedInputStream(in), bandwidthMB * 1024 * 1024L);
317      }
318
319      Path inputPath = inputStat.getPath();
320      try {
321        context.getCounter(Counter.BYTES_EXPECTED).increment(inputStat.getLen());
322
323        // Ensure that the output folder is there and copy the file
324        createOutputPath(outputPath.getParent());
325        FSDataOutputStream out = outputFs.create(outputPath, true);
326
327        long stime = EnvironmentEdgeManager.currentTime();
328        long totalBytesWritten =
329          copyData(context, inputPath, in, outputPath, out, inputStat.getLen());
330
331        // Verify the file length and checksum
332        verifyCopyResult(inputStat, outputFs.getFileStatus(outputPath));
333
334        long etime = EnvironmentEdgeManager.currentTime();
335        LOG.info("copy completed for input=" + inputPath + " output=" + outputPath);
336        LOG
337          .info("size=" + totalBytesWritten + " (" + StringUtils.humanReadableInt(totalBytesWritten)
338            + ")" + " time=" + StringUtils.formatTimeDiff(etime, stime) + String
339              .format(" %.3fM/sec", (totalBytesWritten / ((etime - stime) / 1000.0)) / 1048576.0));
340        context.getCounter(Counter.FILES_COPIED).increment(1);
341
342        // Try to Preserve attributes
343        if (!preserveAttributes(outputPath, inputStat)) {
344          LOG.warn("You may have to run manually chown on: " + outputPath);
345        }
346      } catch (IOException e) {
347        LOG.error("Error copying " + inputPath + " to " + outputPath, e);
348        context.getCounter(Counter.COPY_FAILED).increment(1);
349        throw e;
350      } finally {
351        injectTestFailure(context, inputInfo);
352      }
353    }
354
355    /**
356     * Create the output folder and optionally set ownership.
357     */
358    private void createOutputPath(final Path path) throws IOException {
359      if (filesUser == null && filesGroup == null) {
360        outputFs.mkdirs(path);
361      } else {
362        Path parent = path.getParent();
363        if (!outputFs.exists(parent) && !parent.isRoot()) {
364          createOutputPath(parent);
365        }
366        outputFs.mkdirs(path);
367        if (filesUser != null || filesGroup != null) {
368          // override the owner when non-null user/group is specified
369          outputFs.setOwner(path, filesUser, filesGroup);
370        }
371        if (filesMode > 0) {
372          outputFs.setPermission(path, new FsPermission(filesMode));
373        }
374      }
375    }
376
377    /**
378     * Try to Preserve the files attribute selected by the user copying them from the source file
379     * This is only required when you are exporting as a different user than "hbase" or on a system
380     * that doesn't have the "hbase" user. This is not considered a blocking failure since the user
381     * can force a chmod with the user that knows is available on the system.
382     */
383    private boolean preserveAttributes(final Path path, final FileStatus refStat) {
384      FileStatus stat;
385      try {
386        stat = outputFs.getFileStatus(path);
387      } catch (IOException e) {
388        LOG.warn("Unable to get the status for file=" + path);
389        return false;
390      }
391
392      try {
393        if (filesMode > 0 && stat.getPermission().toShort() != filesMode) {
394          outputFs.setPermission(path, new FsPermission(filesMode));
395        } else if (refStat != null && !stat.getPermission().equals(refStat.getPermission())) {
396          outputFs.setPermission(path, refStat.getPermission());
397        }
398      } catch (IOException e) {
399        LOG.warn("Unable to set the permission for file=" + stat.getPath() + ": " + e.getMessage());
400        return false;
401      }
402
403      boolean hasRefStat = (refStat != null);
404      String user = stringIsNotEmpty(filesUser) || !hasRefStat ? filesUser : refStat.getOwner();
405      String group = stringIsNotEmpty(filesGroup) || !hasRefStat ? filesGroup : refStat.getGroup();
406      if (stringIsNotEmpty(user) || stringIsNotEmpty(group)) {
407        try {
408          if (!(user.equals(stat.getOwner()) && group.equals(stat.getGroup()))) {
409            outputFs.setOwner(path, user, group);
410          }
411        } catch (IOException e) {
412          LOG.warn(
413            "Unable to set the owner/group for file=" + stat.getPath() + ": " + e.getMessage());
414          LOG.warn("The user/group may not exist on the destination cluster: user=" + user
415            + " group=" + group);
416          return false;
417        }
418      }
419
420      return true;
421    }
422
423    private boolean stringIsNotEmpty(final String str) {
424      return str != null && str.length() > 0;
425    }
426
427    private long copyData(final Context context, final Path inputPath, final InputStream in,
428      final Path outputPath, final FSDataOutputStream out, final long inputFileSize)
429      throws IOException {
430      final String statusMessage =
431        "copied %s/" + StringUtils.humanReadableInt(inputFileSize) + " (%.1f%%)";
432
433      try {
434        byte[] buffer = new byte[bufferSize];
435        long totalBytesWritten = 0;
436        int reportBytes = 0;
437        int bytesRead;
438
439        while ((bytesRead = in.read(buffer)) > 0) {
440          out.write(buffer, 0, bytesRead);
441          totalBytesWritten += bytesRead;
442          reportBytes += bytesRead;
443
444          if (reportBytes >= REPORT_SIZE) {
445            context.getCounter(Counter.BYTES_COPIED).increment(reportBytes);
446            context.setStatus(
447              String.format(statusMessage, StringUtils.humanReadableInt(totalBytesWritten),
448                (totalBytesWritten / (float) inputFileSize) * 100.0f) + " from " + inputPath
449                + " to " + outputPath);
450            reportBytes = 0;
451          }
452        }
453
454        context.getCounter(Counter.BYTES_COPIED).increment(reportBytes);
455        context
456          .setStatus(String.format(statusMessage, StringUtils.humanReadableInt(totalBytesWritten),
457            (totalBytesWritten / (float) inputFileSize) * 100.0f) + " from " + inputPath + " to "
458            + outputPath);
459
460        return totalBytesWritten;
461      } finally {
462        out.close();
463        in.close();
464      }
465    }
466
467    /**
468     * Try to open the "source" file. Throws an IOException if the communication with the inputFs
469     * fail or if the file is not found.
470     */
471    private FSDataInputStream openSourceFile(Context context, final SnapshotFileInfo fileInfo)
472      throws IOException {
473      try {
474        Configuration conf = context.getConfiguration();
475        FileLink link = null;
476        switch (fileInfo.getType()) {
477          case HFILE:
478            Path inputPath = new Path(fileInfo.getHfile());
479            link = getFileLink(inputPath, conf);
480            break;
481          case WAL:
482            String serverName = fileInfo.getWalServer();
483            String logName = fileInfo.getWalName();
484            link = new WALLink(inputRoot, serverName, logName);
485            break;
486          default:
487            throw new IOException("Invalid File Type: " + fileInfo.getType().toString());
488        }
489        return link.open(inputFs);
490      } catch (IOException e) {
491        context.getCounter(Counter.MISSING_FILES).increment(1);
492        LOG.error("Unable to open source file=" + fileInfo.toString(), e);
493        throw e;
494      }
495    }
496
497    private FileStatus getSourceFileStatus(Context context, final SnapshotFileInfo fileInfo)
498      throws IOException {
499      try {
500        Configuration conf = context.getConfiguration();
501        FileLink link = null;
502        switch (fileInfo.getType()) {
503          case HFILE:
504            Path inputPath = new Path(fileInfo.getHfile());
505            link = getFileLink(inputPath, conf);
506            break;
507          case WAL:
508            link = new WALLink(inputRoot, fileInfo.getWalServer(), fileInfo.getWalName());
509            break;
510          default:
511            throw new IOException("Invalid File Type: " + fileInfo.getType().toString());
512        }
513        return link.getFileStatus(inputFs);
514      } catch (FileNotFoundException e) {
515        context.getCounter(Counter.MISSING_FILES).increment(1);
516        LOG.error("Unable to get the status for source file=" + fileInfo.toString(), e);
517        throw e;
518      } catch (IOException e) {
519        LOG.error("Unable to get the status for source file=" + fileInfo.toString(), e);
520        throw e;
521      }
522    }
523
524    private FileLink getFileLink(Path path, Configuration conf) throws IOException {
525      String regionName = HFileLink.getReferencedRegionName(path.getName());
526      TableName tableName = HFileLink.getReferencedTableName(path.getName());
527      if (MobUtils.getMobRegionInfo(tableName).getEncodedName().equals(regionName)) {
528        return HFileLink.buildFromHFileLinkPattern(MobUtils.getQualifiedMobRootDir(conf),
529          HFileArchiveUtil.getArchivePath(conf), path);
530      }
531      return HFileLink.buildFromHFileLinkPattern(inputRoot, inputArchive, path);
532    }
533
534    private FileChecksum getFileChecksum(final FileSystem fs, final Path path) {
535      try {
536        return fs.getFileChecksum(path);
537      } catch (IOException e) {
538        LOG.warn("Unable to get checksum for file=" + path, e);
539        return null;
540      }
541    }
542
543    /**
544     * Utility to compare the file length and checksums for the paths specified.
545     */
546    private void verifyCopyResult(final FileStatus inputStat, final FileStatus outputStat)
547      throws IOException {
548      long inputLen = inputStat.getLen();
549      long outputLen = outputStat.getLen();
550      Path inputPath = inputStat.getPath();
551      Path outputPath = outputStat.getPath();
552
553      if (inputLen != outputLen) {
554        throw new IOException("Mismatch in length of input:" + inputPath + " (" + inputLen
555          + ") and output:" + outputPath + " (" + outputLen + ")");
556      }
557
558      // If length==0, we will skip checksum
559      if (inputLen != 0 && verifyChecksum) {
560        FileChecksum inChecksum = getFileChecksum(inputFs, inputStat.getPath());
561        FileChecksum outChecksum = getFileChecksum(outputFs, outputStat.getPath());
562
563        ChecksumComparison checksumComparison = verifyChecksum(inChecksum, outChecksum);
564        if (!checksumComparison.equals(ChecksumComparison.TRUE)) {
565          StringBuilder errMessage = new StringBuilder("Checksum mismatch between ")
566            .append(inputPath).append(" and ").append(outputPath).append(".");
567
568          boolean addSkipHint = false;
569          String inputScheme = inputFs.getScheme();
570          String outputScheme = outputFs.getScheme();
571          if (!inputScheme.equals(outputScheme)) {
572            errMessage.append(" Input and output filesystems are of different types.\n")
573              .append("Their checksum algorithms may be incompatible.");
574            addSkipHint = true;
575          } else if (inputStat.getBlockSize() != outputStat.getBlockSize()) {
576            errMessage.append(" Input and output differ in block-size.");
577            addSkipHint = true;
578          } else if (
579            inChecksum != null && outChecksum != null
580              && !inChecksum.getAlgorithmName().equals(outChecksum.getAlgorithmName())
581          ) {
582            errMessage.append(" Input and output checksum algorithms are of different types.");
583            addSkipHint = true;
584          }
585          if (addSkipHint) {
586            errMessage
587              .append(" You can choose file-level checksum validation via "
588                + "-Ddfs.checksum.combine.mode=COMPOSITE_CRC when block-sizes"
589                + " or filesystems are different.\n")
590              .append(" Or you can skip checksum-checks altogether with -no-checksum-verify,")
591              .append(
592                " for the table backup scenario, you should use -i option to skip checksum-checks.\n")
593              .append(" (NOTE: By skipping checksums, one runs the risk of "
594                + "masking data-corruption during file-transfer.)\n");
595          }
596          throw new IOException(errMessage.toString());
597        }
598      }
599    }
600
601    /**
602     * Utility to compare checksums
603     */
604    private ChecksumComparison verifyChecksum(final FileChecksum inChecksum,
605      final FileChecksum outChecksum) {
606      // If the input or output checksum is null, or the algorithms of input and output are not
607      // equal, that means there is no comparison
608      // and return not compatible. else if matched, return compatible with the matched result.
609      if (
610        inChecksum == null || outChecksum == null
611          || !inChecksum.getAlgorithmName().equals(outChecksum.getAlgorithmName())
612      ) {
613        return ChecksumComparison.INCOMPATIBLE;
614      } else if (inChecksum.equals(outChecksum)) {
615        return ChecksumComparison.TRUE;
616      }
617      return ChecksumComparison.FALSE;
618    }
619
620    /**
621     * Check if the two files are equal by looking at the file length, and at the checksum (if user
622     * has specified the verifyChecksum flag).
623     */
624    private boolean sameFile(final FileStatus inputStat, final FileStatus outputStat) {
625      // Not matching length
626      if (inputStat.getLen() != outputStat.getLen()) return false;
627
628      // Mark files as equals, since user asked for no checksum verification
629      if (!verifyChecksum) return true;
630
631      // If checksums are not available, files are not the same.
632      FileChecksum inChecksum = getFileChecksum(inputFs, inputStat.getPath());
633      if (inChecksum == null) return false;
634
635      FileChecksum outChecksum = getFileChecksum(outputFs, outputStat.getPath());
636      if (outChecksum == null) return false;
637
638      return inChecksum.equals(outChecksum);
639    }
640  }
641
642  // ==========================================================================
643  // Input Format
644  // ==========================================================================
645
646  /**
647   * Extract the list of files (HFiles/WALs) to copy using Map-Reduce.
648   * @return list of files referenced by the snapshot (pair of path and size)
649   */
650  private static List<Pair<SnapshotFileInfo, Long>> getSnapshotFiles(final Configuration conf,
651    final FileSystem fs, final Path snapshotDir) throws IOException {
652    SnapshotDescription snapshotDesc = SnapshotDescriptionUtils.readSnapshotInfo(fs, snapshotDir);
653
654    final List<Pair<SnapshotFileInfo, Long>> files = new ArrayList<>();
655    final TableName table = TableName.valueOf(snapshotDesc.getTable());
656
657    // Get snapshot files
658    LOG.info("Loading Snapshot '" + snapshotDesc.getName() + "' hfile list");
659    SnapshotReferenceUtil.visitReferencedFiles(conf, fs, snapshotDir, snapshotDesc,
660      new SnapshotReferenceUtil.SnapshotVisitor() {
661        @Override
662        public void storeFile(final RegionInfo regionInfo, final String family,
663          final SnapshotRegionManifest.StoreFile storeFile) throws IOException {
664          Pair<SnapshotFileInfo, Long> snapshotFileAndSize = null;
665          if (!storeFile.hasReference()) {
666            String region = regionInfo.getEncodedName();
667            String hfile = storeFile.getName();
668            snapshotFileAndSize = getSnapshotFileAndSize(fs, conf, table, region, family, hfile,
669              storeFile.hasFileSize() ? storeFile.getFileSize() : -1);
670          } else {
671            Pair<String, String> referredToRegionAndFile =
672              StoreFileInfo.getReferredToRegionAndFile(storeFile.getName());
673            String referencedRegion = referredToRegionAndFile.getFirst();
674            String referencedHFile = referredToRegionAndFile.getSecond();
675            snapshotFileAndSize = getSnapshotFileAndSize(fs, conf, table, referencedRegion, family,
676              referencedHFile, storeFile.hasFileSize() ? storeFile.getFileSize() : -1);
677          }
678          files.add(snapshotFileAndSize);
679        }
680      });
681
682    return files;
683  }
684
685  private static Pair<SnapshotFileInfo, Long> getSnapshotFileAndSize(FileSystem fs,
686    Configuration conf, TableName table, String region, String family, String hfile, long size)
687    throws IOException {
688    Path path = HFileLink.createPath(table, region, family, hfile);
689    SnapshotFileInfo fileInfo = SnapshotFileInfo.newBuilder().setType(SnapshotFileInfo.Type.HFILE)
690      .setHfile(path.toString()).build();
691    if (size == -1) {
692      size = HFileLink.buildFromHFileLinkPattern(conf, path).getFileStatus(fs).getLen();
693    }
694    return new Pair<>(fileInfo, size);
695  }
696
697  /**
698   * Given a list of file paths and sizes, create around ngroups in as balanced a way as possible.
699   * The groups created will have similar amounts of bytes.
700   * <p>
701   * The algorithm used is pretty straightforward; the file list is sorted by size, and then each
702   * group fetch the bigger file available, iterating through groups alternating the direction.
703   */
704  static List<List<Pair<SnapshotFileInfo, Long>>>
705    getBalancedSplits(final List<Pair<SnapshotFileInfo, Long>> files, final int ngroups) {
706    // Sort files by size, from small to big
707    Collections.sort(files, new Comparator<Pair<SnapshotFileInfo, Long>>() {
708      public int compare(Pair<SnapshotFileInfo, Long> a, Pair<SnapshotFileInfo, Long> b) {
709        long r = a.getSecond() - b.getSecond();
710        return (r < 0) ? -1 : ((r > 0) ? 1 : 0);
711      }
712    });
713
714    // create balanced groups
715    List<List<Pair<SnapshotFileInfo, Long>>> fileGroups = new LinkedList<>();
716    long[] sizeGroups = new long[ngroups];
717    int hi = files.size() - 1;
718    int lo = 0;
719
720    List<Pair<SnapshotFileInfo, Long>> group;
721    int dir = 1;
722    int g = 0;
723
724    while (hi >= lo) {
725      if (g == fileGroups.size()) {
726        group = new LinkedList<>();
727        fileGroups.add(group);
728      } else {
729        group = fileGroups.get(g);
730      }
731
732      Pair<SnapshotFileInfo, Long> fileInfo = files.get(hi--);
733
734      // add the hi one
735      sizeGroups[g] += fileInfo.getSecond();
736      group.add(fileInfo);
737
738      // change direction when at the end or the beginning
739      g += dir;
740      if (g == ngroups) {
741        dir = -1;
742        g = ngroups - 1;
743      } else if (g < 0) {
744        dir = 1;
745        g = 0;
746      }
747    }
748
749    if (LOG.isDebugEnabled()) {
750      for (int i = 0; i < sizeGroups.length; ++i) {
751        LOG.debug("export split=" + i + " size=" + StringUtils.humanReadableInt(sizeGroups[i]));
752      }
753    }
754
755    return fileGroups;
756  }
757
758  private static class ExportSnapshotInputFormat extends InputFormat<BytesWritable, NullWritable> {
759    @Override
760    public RecordReader<BytesWritable, NullWritable> createRecordReader(InputSplit split,
761      TaskAttemptContext tac) throws IOException, InterruptedException {
762      return new ExportSnapshotRecordReader(((ExportSnapshotInputSplit) split).getSplitKeys());
763    }
764
765    @Override
766    public List<InputSplit> getSplits(JobContext context) throws IOException, InterruptedException {
767      Configuration conf = context.getConfiguration();
768      Path snapshotDir = new Path(conf.get(CONF_SNAPSHOT_DIR));
769      FileSystem fs = FileSystem.get(snapshotDir.toUri(), conf);
770
771      List<Pair<SnapshotFileInfo, Long>> snapshotFiles = getSnapshotFiles(conf, fs, snapshotDir);
772      int mappers = conf.getInt(CONF_NUM_SPLITS, 0);
773      if (mappers == 0 && snapshotFiles.size() > 0) {
774        mappers = 1 + (snapshotFiles.size() / conf.getInt(CONF_MAP_GROUP, 10));
775        mappers = Math.min(mappers, snapshotFiles.size());
776        conf.setInt(CONF_NUM_SPLITS, mappers);
777        conf.setInt(MR_NUM_MAPS, mappers);
778      }
779
780      List<List<Pair<SnapshotFileInfo, Long>>> groups = getBalancedSplits(snapshotFiles, mappers);
781      List<InputSplit> splits = new ArrayList(groups.size());
782      for (List<Pair<SnapshotFileInfo, Long>> files : groups) {
783        splits.add(new ExportSnapshotInputSplit(files));
784      }
785      return splits;
786    }
787
788    private static class ExportSnapshotInputSplit extends InputSplit implements Writable {
789      private List<Pair<BytesWritable, Long>> files;
790      private long length;
791
792      public ExportSnapshotInputSplit() {
793        this.files = null;
794      }
795
796      public ExportSnapshotInputSplit(final List<Pair<SnapshotFileInfo, Long>> snapshotFiles) {
797        this.files = new ArrayList(snapshotFiles.size());
798        for (Pair<SnapshotFileInfo, Long> fileInfo : snapshotFiles) {
799          this.files.add(
800            new Pair<>(new BytesWritable(fileInfo.getFirst().toByteArray()), fileInfo.getSecond()));
801          this.length += fileInfo.getSecond();
802        }
803      }
804
805      private List<Pair<BytesWritable, Long>> getSplitKeys() {
806        return files;
807      }
808
809      @Override
810      public long getLength() throws IOException, InterruptedException {
811        return length;
812      }
813
814      @Override
815      public String[] getLocations() throws IOException, InterruptedException {
816        return new String[] {};
817      }
818
819      @Override
820      public void readFields(DataInput in) throws IOException {
821        int count = in.readInt();
822        files = new ArrayList<>(count);
823        length = 0;
824        for (int i = 0; i < count; ++i) {
825          BytesWritable fileInfo = new BytesWritable();
826          fileInfo.readFields(in);
827          long size = in.readLong();
828          files.add(new Pair<>(fileInfo, size));
829          length += size;
830        }
831      }
832
833      @Override
834      public void write(DataOutput out) throws IOException {
835        out.writeInt(files.size());
836        for (final Pair<BytesWritable, Long> fileInfo : files) {
837          fileInfo.getFirst().write(out);
838          out.writeLong(fileInfo.getSecond());
839        }
840      }
841    }
842
843    private static class ExportSnapshotRecordReader
844      extends RecordReader<BytesWritable, NullWritable> {
845      private final List<Pair<BytesWritable, Long>> files;
846      private long totalSize = 0;
847      private long procSize = 0;
848      private int index = -1;
849
850      ExportSnapshotRecordReader(final List<Pair<BytesWritable, Long>> files) {
851        this.files = files;
852        for (Pair<BytesWritable, Long> fileInfo : files) {
853          totalSize += fileInfo.getSecond();
854        }
855      }
856
857      @Override
858      public void close() {
859      }
860
861      @Override
862      public BytesWritable getCurrentKey() {
863        return files.get(index).getFirst();
864      }
865
866      @Override
867      public NullWritable getCurrentValue() {
868        return NullWritable.get();
869      }
870
871      @Override
872      public float getProgress() {
873        return (float) procSize / totalSize;
874      }
875
876      @Override
877      public void initialize(InputSplit split, TaskAttemptContext tac) {
878      }
879
880      @Override
881      public boolean nextKeyValue() {
882        if (index >= 0) {
883          procSize += files.get(index).getSecond();
884        }
885        return (++index < files.size());
886      }
887    }
888  }
889
890  // ==========================================================================
891  // Tool
892  // ==========================================================================
893
894  /**
895   * Run Map-Reduce Job to perform the files copy.
896   */
897  private void runCopyJob(final Path inputRoot, final Path outputRoot, final String snapshotName,
898    final Path snapshotDir, final boolean verifyChecksum, final String filesUser,
899    final String filesGroup, final int filesMode, final int mappers, final int bandwidthMB)
900    throws IOException, InterruptedException, ClassNotFoundException {
901    Configuration conf = getConf();
902    if (filesGroup != null) conf.set(CONF_FILES_GROUP, filesGroup);
903    if (filesUser != null) conf.set(CONF_FILES_USER, filesUser);
904    if (mappers > 0) {
905      conf.setInt(CONF_NUM_SPLITS, mappers);
906      conf.setInt(MR_NUM_MAPS, mappers);
907    }
908    conf.setInt(CONF_FILES_MODE, filesMode);
909    conf.setBoolean(CONF_CHECKSUM_VERIFY, verifyChecksum);
910    conf.set(CONF_OUTPUT_ROOT, outputRoot.toString());
911    conf.set(CONF_INPUT_ROOT, inputRoot.toString());
912    conf.setInt(CONF_BANDWIDTH_MB, bandwidthMB);
913    conf.set(CONF_SNAPSHOT_NAME, snapshotName);
914    conf.set(CONF_SNAPSHOT_DIR, snapshotDir.toString());
915
916    String jobname = conf.get(CONF_MR_JOB_NAME, "ExportSnapshot-" + snapshotName);
917    Job job = new Job(conf);
918    job.setJobName(jobname);
919    job.setJarByClass(ExportSnapshot.class);
920    TableMapReduceUtil.addDependencyJars(job);
921    job.setMapperClass(ExportMapper.class);
922    job.setInputFormatClass(ExportSnapshotInputFormat.class);
923    job.setOutputFormatClass(NullOutputFormat.class);
924    job.setMapSpeculativeExecution(false);
925    job.setNumReduceTasks(0);
926
927    // Acquire the delegation Tokens
928    Configuration srcConf = HBaseConfiguration.createClusterConf(conf, null, CONF_SOURCE_PREFIX);
929    TokenCache.obtainTokensForNamenodes(job.getCredentials(), new Path[] { inputRoot }, srcConf);
930    Configuration destConf = HBaseConfiguration.createClusterConf(conf, null, CONF_DEST_PREFIX);
931    TokenCache.obtainTokensForNamenodes(job.getCredentials(), new Path[] { outputRoot }, destConf);
932
933    // Run the MR Job
934    if (!job.waitForCompletion(true)) {
935      throw new ExportSnapshotException(job.getStatus().getFailureInfo());
936    }
937  }
938
939  private void verifySnapshot(final SnapshotDescription snapshotDesc, final Configuration baseConf,
940    final FileSystem fs, final Path rootDir, final Path snapshotDir) throws IOException {
941    // Update the conf with the current root dir, since may be a different cluster
942    Configuration conf = new Configuration(baseConf);
943    CommonFSUtils.setRootDir(conf, rootDir);
944    CommonFSUtils.setFsDefault(conf, CommonFSUtils.getRootDir(conf));
945    boolean isExpired = SnapshotDescriptionUtils.isExpiredSnapshot(snapshotDesc.getTtl(),
946      snapshotDesc.getCreationTime(), EnvironmentEdgeManager.currentTime());
947    if (isExpired) {
948      throw new SnapshotTTLExpiredException(ProtobufUtil.createSnapshotDesc(snapshotDesc));
949    }
950    SnapshotReferenceUtil.verifySnapshot(conf, fs, snapshotDir, snapshotDesc);
951  }
952
953  private void setConfigParallel(FileSystem outputFs, List<Path> traversedPath,
954    BiConsumer<FileSystem, Path> task, Configuration conf) throws IOException {
955    ExecutorService pool = Executors
956      .newFixedThreadPool(conf.getInt(CONF_COPY_MANIFEST_THREADS, DEFAULT_COPY_MANIFEST_THREADS));
957    List<Future<Void>> futures = new ArrayList<>();
958    for (Path dstPath : traversedPath) {
959      Future<Void> future = (Future<Void>) pool.submit(() -> task.accept(outputFs, dstPath));
960      futures.add(future);
961    }
962    try {
963      for (Future<Void> future : futures) {
964        future.get();
965      }
966    } catch (InterruptedException | ExecutionException e) {
967      throw new IOException(e);
968    } finally {
969      pool.shutdownNow();
970    }
971  }
972
973  private void setOwnerParallel(FileSystem outputFs, String filesUser, String filesGroup,
974    Configuration conf, List<Path> traversedPath) throws IOException {
975    setConfigParallel(outputFs, traversedPath, (fs, path) -> {
976      try {
977        fs.setOwner(path, filesUser, filesGroup);
978      } catch (IOException e) {
979        throw new RuntimeException(
980          "set owner for file " + path + " to " + filesUser + ":" + filesGroup + " failed", e);
981      }
982    }, conf);
983  }
984
985  private void setPermissionParallel(final FileSystem outputFs, final short filesMode,
986    final List<Path> traversedPath, final Configuration conf) throws IOException {
987    if (filesMode <= 0) {
988      return;
989    }
990    FsPermission perm = new FsPermission(filesMode);
991    setConfigParallel(outputFs, traversedPath, (fs, path) -> {
992      try {
993        fs.setPermission(path, perm);
994      } catch (IOException e) {
995        throw new RuntimeException(
996          "set permission for file " + path + " to " + filesMode + " failed", e);
997      }
998    }, conf);
999  }
1000
1001  private boolean verifyTarget = true;
1002  private boolean verifySource = true;
1003  private boolean verifyChecksum = true;
1004  private String snapshotName = null;
1005  private String targetName = null;
1006  private boolean overwrite = false;
1007  private String filesGroup = null;
1008  private String filesUser = null;
1009  private Path outputRoot = null;
1010  private Path inputRoot = null;
1011  private int bandwidthMB = Integer.MAX_VALUE;
1012  private int filesMode = 0;
1013  private int mappers = 0;
1014  private boolean resetTtl = false;
1015
1016  @Override
1017  protected void processOptions(CommandLine cmd) {
1018    snapshotName = cmd.getOptionValue(Options.SNAPSHOT.getLongOpt(), snapshotName);
1019    targetName = cmd.getOptionValue(Options.TARGET_NAME.getLongOpt(), targetName);
1020    if (cmd.hasOption(Options.COPY_TO.getLongOpt())) {
1021      outputRoot = new Path(cmd.getOptionValue(Options.COPY_TO.getLongOpt()));
1022    }
1023    if (cmd.hasOption(Options.COPY_FROM.getLongOpt())) {
1024      inputRoot = new Path(cmd.getOptionValue(Options.COPY_FROM.getLongOpt()));
1025    }
1026    mappers = getOptionAsInt(cmd, Options.MAPPERS.getLongOpt(), mappers);
1027    filesUser = cmd.getOptionValue(Options.CHUSER.getLongOpt(), filesUser);
1028    filesGroup = cmd.getOptionValue(Options.CHGROUP.getLongOpt(), filesGroup);
1029    filesMode = getOptionAsInt(cmd, Options.CHMOD.getLongOpt(), filesMode, 8);
1030    bandwidthMB = getOptionAsInt(cmd, Options.BANDWIDTH.getLongOpt(), bandwidthMB);
1031    overwrite = cmd.hasOption(Options.OVERWRITE.getLongOpt());
1032    // And verifyChecksum and verifyTarget with values read from old args in processOldArgs(...).
1033    verifyChecksum = !cmd.hasOption(Options.NO_CHECKSUM_VERIFY.getLongOpt());
1034    verifyTarget = !cmd.hasOption(Options.NO_TARGET_VERIFY.getLongOpt());
1035    verifySource = !cmd.hasOption(Options.NO_SOURCE_VERIFY.getLongOpt());
1036    resetTtl = cmd.hasOption(Options.RESET_TTL.getLongOpt());
1037  }
1038
1039  /**
1040   * Execute the export snapshot by copying the snapshot metadata, hfiles and wals.
1041   * @return 0 on success, and != 0 upon failure.
1042   */
1043  @Override
1044  public int doWork() throws IOException {
1045    Configuration conf = getConf();
1046
1047    // Check user options
1048    if (snapshotName == null) {
1049      System.err.println("Snapshot name not provided.");
1050      LOG.error("Use -h or --help for usage instructions.");
1051      return EXIT_FAILURE;
1052    }
1053
1054    if (outputRoot == null) {
1055      System.err
1056        .println("Destination file-system (--" + Options.COPY_TO.getLongOpt() + ") not provided.");
1057      LOG.error("Use -h or --help for usage instructions.");
1058      return EXIT_FAILURE;
1059    }
1060
1061    if (targetName == null) {
1062      targetName = snapshotName;
1063    }
1064    if (inputRoot == null) {
1065      inputRoot = CommonFSUtils.getRootDir(conf);
1066    } else {
1067      CommonFSUtils.setRootDir(conf, inputRoot);
1068    }
1069
1070    Configuration srcConf = HBaseConfiguration.createClusterConf(conf, null, CONF_SOURCE_PREFIX);
1071    FileSystem inputFs = FileSystem.get(inputRoot.toUri(), srcConf);
1072    Configuration destConf = HBaseConfiguration.createClusterConf(conf, null, CONF_DEST_PREFIX);
1073    FileSystem outputFs = FileSystem.get(outputRoot.toUri(), destConf);
1074    boolean skipTmp = conf.getBoolean(CONF_SKIP_TMP, false)
1075      || conf.get(SnapshotDescriptionUtils.SNAPSHOT_WORKING_DIR) != null;
1076    Path snapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, inputRoot);
1077    Path snapshotTmpDir =
1078      SnapshotDescriptionUtils.getWorkingSnapshotDir(targetName, outputRoot, destConf);
1079    Path outputSnapshotDir =
1080      SnapshotDescriptionUtils.getCompletedSnapshotDir(targetName, outputRoot);
1081    Path initialOutputSnapshotDir = skipTmp ? outputSnapshotDir : snapshotTmpDir;
1082    LOG.debug("inputFs={}, inputRoot={}", inputFs.getUri().toString(), inputRoot);
1083    LOG.debug("outputFs={}, outputRoot={}, skipTmp={}, initialOutputSnapshotDir={}", outputFs,
1084      outputRoot.toString(), skipTmp, initialOutputSnapshotDir);
1085
1086    // throw CorruptedSnapshotException if we can't read the snapshot info.
1087    SnapshotDescription sourceSnapshotDesc =
1088      SnapshotDescriptionUtils.readSnapshotInfo(inputFs, snapshotDir);
1089
1090    // Verify snapshot source before copying files
1091    if (verifySource) {
1092      LOG.info("Verify the source snapshot's expiration status and integrity.");
1093      verifySnapshot(sourceSnapshotDesc, srcConf, inputFs, inputRoot, snapshotDir);
1094    }
1095
1096    // Find the necessary directory which need to change owner and group
1097    Path needSetOwnerDir = SnapshotDescriptionUtils.getSnapshotRootDir(outputRoot);
1098    if (outputFs.exists(needSetOwnerDir)) {
1099      if (skipTmp) {
1100        needSetOwnerDir = outputSnapshotDir;
1101      } else {
1102        needSetOwnerDir = SnapshotDescriptionUtils.getWorkingSnapshotDir(outputRoot, destConf);
1103        if (outputFs.exists(needSetOwnerDir)) {
1104          needSetOwnerDir = snapshotTmpDir;
1105        }
1106      }
1107    }
1108
1109    // Check if the snapshot already exists
1110    if (outputFs.exists(outputSnapshotDir)) {
1111      if (overwrite) {
1112        if (!outputFs.delete(outputSnapshotDir, true)) {
1113          System.err.println("Unable to remove existing snapshot directory: " + outputSnapshotDir);
1114          return EXIT_FAILURE;
1115        }
1116      } else {
1117        System.err.println("The snapshot '" + targetName + "' already exists in the destination: "
1118          + outputSnapshotDir);
1119        return EXIT_FAILURE;
1120      }
1121    }
1122
1123    if (!skipTmp) {
1124      // Check if the snapshot already in-progress
1125      if (outputFs.exists(snapshotTmpDir)) {
1126        if (overwrite) {
1127          if (!outputFs.delete(snapshotTmpDir, true)) {
1128            System.err
1129              .println("Unable to remove existing snapshot tmp directory: " + snapshotTmpDir);
1130            return EXIT_FAILURE;
1131          }
1132        } else {
1133          System.err
1134            .println("A snapshot with the same name '" + targetName + "' may be in-progress");
1135          System.err
1136            .println("Please check " + snapshotTmpDir + ". If the snapshot has completed, ");
1137          System.err
1138            .println("consider removing " + snapshotTmpDir + " by using the -overwrite option");
1139          return EXIT_FAILURE;
1140        }
1141      }
1142    }
1143
1144    // Step 1 - Copy fs1:/.snapshot/<snapshot> to fs2:/.snapshot/.tmp/<snapshot>
1145    // The snapshot references must be copied before the hfiles otherwise the cleaner
1146    // will remove them because they are unreferenced.
1147    List<Path> travesedPaths = new ArrayList<>();
1148    boolean copySucceeded = false;
1149    try {
1150      LOG.info("Copy Snapshot Manifest from " + snapshotDir + " to " + initialOutputSnapshotDir);
1151      travesedPaths =
1152        FSUtils.copyFilesParallel(inputFs, snapshotDir, outputFs, initialOutputSnapshotDir, conf,
1153          conf.getInt(CONF_COPY_MANIFEST_THREADS, DEFAULT_COPY_MANIFEST_THREADS));
1154      copySucceeded = true;
1155    } catch (IOException e) {
1156      throw new ExportSnapshotException("Failed to copy the snapshot directory: from=" + snapshotDir
1157        + " to=" + initialOutputSnapshotDir, e);
1158    } finally {
1159      if (copySucceeded) {
1160        if (filesUser != null || filesGroup != null) {
1161          LOG.warn(
1162            (filesUser == null ? "" : "Change the owner of " + needSetOwnerDir + " to " + filesUser)
1163              + (filesGroup == null
1164                ? ""
1165                : ", Change the group of " + needSetOwnerDir + " to " + filesGroup));
1166          setOwnerParallel(outputFs, filesUser, filesGroup, conf, travesedPaths);
1167        }
1168        if (filesMode > 0) {
1169          LOG.warn("Change the permission of " + needSetOwnerDir + " to " + filesMode);
1170          setPermissionParallel(outputFs, (short) filesMode, travesedPaths, conf);
1171        }
1172      }
1173    }
1174
1175    // Write a new .snapshotinfo if the target name is different from the source name or we want to
1176    // reset TTL for target snapshot.
1177    if (!targetName.equals(snapshotName) || resetTtl) {
1178      SnapshotDescription.Builder snapshotDescBuilder =
1179        SnapshotDescriptionUtils.readSnapshotInfo(inputFs, snapshotDir).toBuilder();
1180      if (!targetName.equals(snapshotName)) {
1181        snapshotDescBuilder.setName(targetName);
1182      }
1183      if (resetTtl) {
1184        snapshotDescBuilder.setTtl(HConstants.DEFAULT_SNAPSHOT_TTL);
1185      }
1186      SnapshotDescriptionUtils.writeSnapshotInfo(snapshotDescBuilder.build(),
1187        initialOutputSnapshotDir, outputFs);
1188      if (filesUser != null || filesGroup != null) {
1189        outputFs.setOwner(
1190          new Path(initialOutputSnapshotDir, SnapshotDescriptionUtils.SNAPSHOTINFO_FILE), filesUser,
1191          filesGroup);
1192      }
1193      if (filesMode > 0) {
1194        outputFs.setPermission(
1195          new Path(initialOutputSnapshotDir, SnapshotDescriptionUtils.SNAPSHOTINFO_FILE),
1196          new FsPermission((short) filesMode));
1197      }
1198    }
1199
1200    // Step 2 - Start MR Job to copy files
1201    // The snapshot references must be copied before the files otherwise the files gets removed
1202    // by the HFileArchiver, since they have no references.
1203    try {
1204      runCopyJob(inputRoot, outputRoot, snapshotName, snapshotDir, verifyChecksum, filesUser,
1205        filesGroup, filesMode, mappers, bandwidthMB);
1206
1207      LOG.info("Finalize the Snapshot Export");
1208      if (!skipTmp) {
1209        // Step 3 - Rename fs2:/.snapshot/.tmp/<snapshot> fs2:/.snapshot/<snapshot>
1210        if (!outputFs.rename(snapshotTmpDir, outputSnapshotDir)) {
1211          throw new ExportSnapshotException("Unable to rename snapshot directory from="
1212            + snapshotTmpDir + " to=" + outputSnapshotDir);
1213        }
1214      }
1215
1216      // Step 4 - Verify snapshot integrity
1217      if (verifyTarget) {
1218        LOG.info("Verify the exported snapshot's expiration status and integrity.");
1219        SnapshotDescription targetSnapshotDesc =
1220          SnapshotDescriptionUtils.readSnapshotInfo(outputFs, outputSnapshotDir);
1221        verifySnapshot(targetSnapshotDesc, destConf, outputFs, outputRoot, outputSnapshotDir);
1222      }
1223
1224      LOG.info("Export Completed: " + targetName);
1225      return EXIT_SUCCESS;
1226    } catch (Exception e) {
1227      LOG.error("Snapshot export failed", e);
1228      if (!skipTmp) {
1229        outputFs.delete(snapshotTmpDir, true);
1230      }
1231      outputFs.delete(outputSnapshotDir, true);
1232      return EXIT_FAILURE;
1233    }
1234  }
1235
1236  @Override
1237  protected void printUsage() {
1238    super.printUsage();
1239    System.out.println("\n" + "Examples:\n" + "  hbase snapshot export \\\n"
1240      + "    --snapshot MySnapshot --copy-to hdfs://srv2:8082/hbase \\\n"
1241      + "    --chuser MyUser --chgroup MyGroup --chmod 700 --mappers 16\n" + "\n"
1242      + "  hbase snapshot export \\\n"
1243      + "    --snapshot MySnapshot --copy-from hdfs://srv2:8082/hbase \\\n"
1244      + "    --copy-to hdfs://srv1:50070/hbase");
1245  }
1246
1247  @Override
1248  protected void addOptions() {
1249    addRequiredOption(Options.SNAPSHOT);
1250    addOption(Options.COPY_TO);
1251    addOption(Options.COPY_FROM);
1252    addOption(Options.TARGET_NAME);
1253    addOption(Options.NO_CHECKSUM_VERIFY);
1254    addOption(Options.NO_TARGET_VERIFY);
1255    addOption(Options.NO_SOURCE_VERIFY);
1256    addOption(Options.OVERWRITE);
1257    addOption(Options.CHUSER);
1258    addOption(Options.CHGROUP);
1259    addOption(Options.CHMOD);
1260    addOption(Options.MAPPERS);
1261    addOption(Options.BANDWIDTH);
1262    addOption(Options.RESET_TTL);
1263  }
1264
1265  public static void main(String[] args) {
1266    new ExportSnapshot().doStaticMain(args);
1267  }
1268}