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