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