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 */
018
019package org.apache.hadoop.hbase.snapshot;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.Map;
032import java.util.Map.Entry;
033import java.util.Set;
034import java.util.TreeMap;
035import java.util.concurrent.ThreadPoolExecutor;
036
037import org.apache.hadoop.conf.Configuration;
038import org.apache.hadoop.fs.FileStatus;
039import org.apache.hadoop.fs.FileSystem;
040import org.apache.hadoop.fs.Path;
041import org.apache.hadoop.hbase.MetaTableAccessor;
042import org.apache.hadoop.hbase.TableName;
043import org.apache.hadoop.hbase.backup.HFileArchiver;
044import org.apache.hadoop.hbase.client.Connection;
045import org.apache.hadoop.hbase.client.ConnectionFactory;
046import org.apache.hadoop.hbase.client.RegionInfo;
047import org.apache.hadoop.hbase.client.RegionInfoBuilder;
048import org.apache.hadoop.hbase.client.TableDescriptor;
049import org.apache.hadoop.hbase.errorhandling.ForeignExceptionDispatcher;
050import org.apache.hadoop.hbase.io.HFileLink;
051import org.apache.hadoop.hbase.io.Reference;
052import org.apache.hadoop.hbase.mob.MobUtils;
053import org.apache.hadoop.hbase.monitoring.MonitoredTask;
054import org.apache.hadoop.hbase.monitoring.TaskMonitor;
055import org.apache.hadoop.hbase.regionserver.HRegion;
056import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
057import org.apache.hadoop.hbase.regionserver.StoreFileInfo;
058import org.apache.hadoop.hbase.security.access.AccessControlClient;
059import org.apache.hadoop.hbase.security.access.ShadedAccessControlUtil;
060import org.apache.hadoop.hbase.security.access.TablePermission;
061import org.apache.hadoop.hbase.util.Bytes;
062import org.apache.hadoop.hbase.util.FSUtils;
063import org.apache.hadoop.hbase.util.ModifyRegionUtils;
064import org.apache.hadoop.hbase.util.Pair;
065import org.apache.hadoop.io.IOUtils;
066import org.apache.yetus.audience.InterfaceAudience;
067import org.slf4j.Logger;
068import org.slf4j.LoggerFactory;
069import org.apache.hbase.thirdparty.com.google.common.collect.ListMultimap;
070import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
071import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription;
072import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotRegionManifest;
073
074/**
075 * Helper to Restore/Clone a Snapshot
076 *
077 * <p>The helper assumes that a table is already created, and by calling restore()
078 * the content present in the snapshot will be restored as the new content of the table.
079 *
080 * <p>Clone from Snapshot: If the target table is empty, the restore operation
081 * is just a "clone operation", where the only operations are:
082 * <ul>
083 *  <li>for each region in the snapshot create a new region
084 *    (note that the region will have a different name, since the encoding contains the table name)
085 *  <li>for each file in the region create a new HFileLink to point to the original file.
086 *  <li>restore the logs, if any
087 * </ul>
088 *
089 * <p>Restore from Snapshot:
090 * <ul>
091 *  <li>for each region in the table verify which are available in the snapshot and which are not
092 *    <ul>
093 *    <li>if the region is not present in the snapshot, remove it.
094 *    <li>if the region is present in the snapshot
095 *      <ul>
096 *      <li>for each file in the table region verify which are available in the snapshot
097 *        <ul>
098 *          <li>if the hfile is not present in the snapshot, remove it
099 *          <li>if the hfile is present, keep it (nothing to do)
100 *        </ul>
101 *      <li>for each file in the snapshot region but not in the table
102 *        <ul>
103 *          <li>create a new HFileLink that point to the original file
104 *        </ul>
105 *      </ul>
106 *    </ul>
107 *  <li>for each region in the snapshot not present in the current table state
108 *    <ul>
109 *    <li>create a new region and for each file in the region create a new HFileLink
110 *      (This is the same as the clone operation)
111 *    </ul>
112 *  <li>restore the logs, if any
113 * </ul>
114 */
115@InterfaceAudience.Private
116public class RestoreSnapshotHelper {
117  private static final Logger LOG = LoggerFactory.getLogger(RestoreSnapshotHelper.class);
118
119  private final Map<byte[], byte[]> regionsMap = new TreeMap<>(Bytes.BYTES_COMPARATOR);
120
121  private final Map<String, Pair<String, String> > parentsMap = new HashMap<>();
122
123  private final ForeignExceptionDispatcher monitor;
124  private final MonitoredTask status;
125
126  private final SnapshotManifest snapshotManifest;
127  private final SnapshotDescription snapshotDesc;
128  private final TableName snapshotTable;
129
130  private final TableDescriptor tableDesc;
131  private final Path rootDir;
132  private final Path tableDir;
133
134  private final Configuration conf;
135  private final FileSystem fs;
136  private final boolean createBackRefs;
137
138  public RestoreSnapshotHelper(final Configuration conf,
139      final FileSystem fs,
140      final SnapshotManifest manifest,
141      final TableDescriptor tableDescriptor,
142      final Path rootDir,
143      final ForeignExceptionDispatcher monitor,
144      final MonitoredTask status) {
145    this(conf, fs, manifest, tableDescriptor, rootDir, monitor, status, true);
146  }
147
148  public RestoreSnapshotHelper(final Configuration conf,
149      final FileSystem fs,
150      final SnapshotManifest manifest,
151      final TableDescriptor tableDescriptor,
152      final Path rootDir,
153      final ForeignExceptionDispatcher monitor,
154      final MonitoredTask status,
155      final boolean createBackRefs)
156  {
157    this.fs = fs;
158    this.conf = conf;
159    this.snapshotManifest = manifest;
160    this.snapshotDesc = manifest.getSnapshotDescription();
161    this.snapshotTable = TableName.valueOf(snapshotDesc.getTable());
162    this.tableDesc = tableDescriptor;
163    this.rootDir = rootDir;
164    this.tableDir = FSUtils.getTableDir(rootDir, tableDesc.getTableName());
165    this.monitor = monitor;
166    this.status = status;
167    this.createBackRefs = createBackRefs;
168  }
169
170  /**
171   * Restore the on-disk table to a specified snapshot state.
172   * @return the set of regions touched by the restore operation
173   */
174  public RestoreMetaChanges restoreHdfsRegions() throws IOException {
175    ThreadPoolExecutor exec = SnapshotManifest.createExecutor(conf, "RestoreSnapshot");
176    try {
177      return restoreHdfsRegions(exec);
178    } finally {
179      exec.shutdown();
180    }
181  }
182
183  private RestoreMetaChanges restoreHdfsRegions(final ThreadPoolExecutor exec) throws IOException {
184    LOG.info("starting restore table regions using snapshot=" + snapshotDesc);
185
186    Map<String, SnapshotRegionManifest> regionManifests = snapshotManifest.getRegionManifestsMap();
187    if (regionManifests == null) {
188      LOG.warn("Nothing to restore. Snapshot " + snapshotDesc + " looks empty");
189      return null;
190    }
191
192    RestoreMetaChanges metaChanges = new RestoreMetaChanges(tableDesc, parentsMap);
193
194    // Take a copy of the manifest.keySet() since we are going to modify
195    // this instance, by removing the regions already present in the restore dir.
196    Set<String> regionNames = new HashSet<>(regionManifests.keySet());
197
198    List<RegionInfo> tableRegions = getTableRegions();
199
200    RegionInfo mobRegion = MobUtils.getMobRegionInfo(snapshotManifest.getTableDescriptor()
201        .getTableName());
202    if (tableRegions != null) {
203      // restore the mob region in case
204      if (regionNames.contains(mobRegion.getEncodedName())) {
205        monitor.rethrowException();
206        status.setStatus("Restoring mob region...");
207        List<RegionInfo> mobRegions = new ArrayList<>(1);
208        mobRegions.add(mobRegion);
209        restoreHdfsMobRegions(exec, regionManifests, mobRegions);
210        regionNames.remove(mobRegion.getEncodedName());
211        status.setStatus("Finished restoring mob region.");
212      }
213    }
214    if (regionNames.contains(mobRegion.getEncodedName())) {
215      // add the mob region
216      monitor.rethrowException();
217      status.setStatus("Cloning mob region...");
218      cloneHdfsMobRegion(regionManifests, mobRegion);
219      regionNames.remove(mobRegion.getEncodedName());
220      status.setStatus("Finished cloning mob region.");
221    }
222
223    // Identify which region are still available and which not.
224    // NOTE: we rely upon the region name as: "table name, start key, end key"
225    if (tableRegions != null) {
226      monitor.rethrowException();
227      for (RegionInfo regionInfo: tableRegions) {
228        String regionName = regionInfo.getEncodedName();
229        if (regionNames.contains(regionName)) {
230          LOG.info("region to restore: " + regionName);
231          regionNames.remove(regionName);
232          metaChanges.addRegionToRestore(regionInfo);
233        } else {
234          LOG.info("region to remove: " + regionName);
235          metaChanges.addRegionToRemove(regionInfo);
236        }
237      }
238    }
239
240    // Regions to Add: present in the snapshot but not in the current table
241    List<RegionInfo> regionsToAdd = new ArrayList<>(regionNames.size());
242    if (regionNames.size() > 0) {
243      monitor.rethrowException();
244      for (String regionName: regionNames) {
245        LOG.info("region to add: " + regionName);
246        regionsToAdd.add(ProtobufUtil.toRegionInfo(regionManifests.get(regionName)
247            .getRegionInfo()));
248      }
249    }
250
251    // Create new regions cloning from the snapshot
252    // HBASE-19980: We need to call cloneHdfsRegions() before restoreHdfsRegions() because
253    // regionsMap is constructed in cloneHdfsRegions() and it can be used in restoreHdfsRegions().
254    monitor.rethrowException();
255    status.setStatus("Cloning regions...");
256    RegionInfo[] clonedRegions = cloneHdfsRegions(exec, regionManifests, regionsToAdd);
257    metaChanges.setNewRegions(clonedRegions);
258    status.setStatus("Finished cloning regions.");
259
260    // Restore regions using the snapshot data
261    monitor.rethrowException();
262    status.setStatus("Restoring table regions...");
263    restoreHdfsRegions(exec, regionManifests, metaChanges.getRegionsToRestore());
264    status.setStatus("Finished restoring all table regions.");
265
266    // Remove regions from the current table
267    monitor.rethrowException();
268    status.setStatus("Starting to delete excess regions from table");
269    removeHdfsRegions(exec, metaChanges.getRegionsToRemove());
270    status.setStatus("Finished deleting excess regions from table.");
271
272    LOG.info("finishing restore table regions using snapshot=" + snapshotDesc);
273
274    return metaChanges;
275  }
276
277  /**
278   * Describe the set of operations needed to update hbase:meta after restore.
279   */
280  public static class RestoreMetaChanges {
281    private final Map<String, Pair<String, String> > parentsMap;
282    private final TableDescriptor htd;
283
284    private List<RegionInfo> regionsToRestore = null;
285    private List<RegionInfo> regionsToRemove = null;
286    private List<RegionInfo> regionsToAdd = null;
287
288    public RestoreMetaChanges(TableDescriptor htd, Map<String, Pair<String, String> > parentsMap) {
289      this.parentsMap = parentsMap;
290      this.htd = htd;
291    }
292
293    public TableDescriptor getTableDescriptor() {
294      return htd;
295    }
296
297    /**
298     * Returns the map of parent-children_pair.
299     * @return the map
300     */
301    public Map<String, Pair<String, String>> getParentToChildrenPairMap() {
302      return this.parentsMap;
303    }
304
305    /**
306     * @return true if there're new regions
307     */
308    public boolean hasRegionsToAdd() {
309      return this.regionsToAdd != null && this.regionsToAdd.size() > 0;
310    }
311
312    /**
313     * Returns the list of new regions added during the on-disk restore.
314     * The caller is responsible to add the regions to META.
315     * e.g MetaTableAccessor.addRegionsToMeta(...)
316     * @return the list of regions to add to META
317     */
318    public List<RegionInfo> getRegionsToAdd() {
319      return this.regionsToAdd;
320    }
321
322    /**
323     * @return true if there're regions to restore
324     */
325    public boolean hasRegionsToRestore() {
326      return this.regionsToRestore != null && this.regionsToRestore.size() > 0;
327    }
328
329    /**
330     * Returns the list of 'restored regions' during the on-disk restore.
331     * The caller is responsible to add the regions to hbase:meta if not present.
332     * @return the list of regions restored
333     */
334    public List<RegionInfo> getRegionsToRestore() {
335      return this.regionsToRestore;
336    }
337
338    /**
339     * @return true if there're regions to remove
340     */
341    public boolean hasRegionsToRemove() {
342      return this.regionsToRemove != null && this.regionsToRemove.size() > 0;
343    }
344
345    /**
346     * Returns the list of regions removed during the on-disk restore.
347     * The caller is responsible to remove the regions from META.
348     * e.g. MetaTableAccessor.deleteRegions(...)
349     * @return the list of regions to remove from META
350     */
351    public List<RegionInfo> getRegionsToRemove() {
352      return this.regionsToRemove;
353    }
354
355    void setNewRegions(final RegionInfo[] hris) {
356      if (hris != null) {
357        regionsToAdd = Arrays.asList(hris);
358      } else {
359        regionsToAdd = null;
360      }
361    }
362
363    void addRegionToRemove(final RegionInfo hri) {
364      if (regionsToRemove == null) {
365        regionsToRemove = new LinkedList<>();
366      }
367      regionsToRemove.add(hri);
368    }
369
370    void addRegionToRestore(final RegionInfo hri) {
371      if (regionsToRestore == null) {
372        regionsToRestore = new LinkedList<>();
373      }
374      regionsToRestore.add(hri);
375    }
376
377    public void updateMetaParentRegions(Connection connection,
378        final List<RegionInfo> regionInfos) throws IOException {
379      if (regionInfos == null || parentsMap.isEmpty()) return;
380
381      // Extract region names and offlined regions
382      Map<String, RegionInfo> regionsByName = new HashMap<>(regionInfos.size());
383      List<RegionInfo> parentRegions = new LinkedList<>();
384      for (RegionInfo regionInfo: regionInfos) {
385        if (regionInfo.isSplitParent()) {
386          parentRegions.add(regionInfo);
387        } else {
388          regionsByName.put(regionInfo.getEncodedName(), regionInfo);
389        }
390      }
391
392      // Update Offline parents
393      for (RegionInfo regionInfo: parentRegions) {
394        Pair<String, String> daughters = parentsMap.get(regionInfo.getEncodedName());
395        if (daughters == null) {
396          // The snapshot contains an unreferenced region.
397          // It will be removed by the CatalogJanitor.
398          LOG.warn("Skip update of unreferenced offline parent: " + regionInfo);
399          continue;
400        }
401
402        // One side of the split is already compacted
403        if (daughters.getSecond() == null) {
404          daughters.setSecond(daughters.getFirst());
405        }
406
407        LOG.debug("Update splits parent " + regionInfo.getEncodedName() + " -> " + daughters);
408        MetaTableAccessor.addSplitsToParent(connection, regionInfo,
409          regionsByName.get(daughters.getFirst()),
410          regionsByName.get(daughters.getSecond()));
411      }
412    }
413  }
414
415  /**
416   * Remove specified regions from the file-system, using the archiver.
417   */
418  private void removeHdfsRegions(final ThreadPoolExecutor exec, final List<RegionInfo> regions)
419      throws IOException {
420    if (regions == null || regions.isEmpty()) return;
421    ModifyRegionUtils.editRegions(exec, regions, new ModifyRegionUtils.RegionEditTask() {
422      @Override
423      public void editRegion(final RegionInfo hri) throws IOException {
424        HFileArchiver.archiveRegion(conf, fs, hri);
425      }
426    });
427  }
428
429  /**
430   * Restore specified regions by restoring content to the snapshot state.
431   */
432  private void restoreHdfsRegions(final ThreadPoolExecutor exec,
433      final Map<String, SnapshotRegionManifest> regionManifests,
434      final List<RegionInfo> regions) throws IOException {
435    if (regions == null || regions.isEmpty()) return;
436    ModifyRegionUtils.editRegions(exec, regions, new ModifyRegionUtils.RegionEditTask() {
437      @Override
438      public void editRegion(final RegionInfo hri) throws IOException {
439        restoreRegion(hri, regionManifests.get(hri.getEncodedName()));
440      }
441    });
442  }
443
444  /**
445   * Restore specified mob regions by restoring content to the snapshot state.
446   */
447  private void restoreHdfsMobRegions(final ThreadPoolExecutor exec,
448      final Map<String, SnapshotRegionManifest> regionManifests,
449      final List<RegionInfo> regions) throws IOException {
450    if (regions == null || regions.isEmpty()) return;
451    ModifyRegionUtils.editRegions(exec, regions, new ModifyRegionUtils.RegionEditTask() {
452      @Override
453      public void editRegion(final RegionInfo hri) throws IOException {
454        restoreMobRegion(hri, regionManifests.get(hri.getEncodedName()));
455      }
456    });
457  }
458
459  private Map<String, List<SnapshotRegionManifest.StoreFile>> getRegionHFileReferences(
460      final SnapshotRegionManifest manifest) {
461    Map<String, List<SnapshotRegionManifest.StoreFile>> familyMap =
462      new HashMap<>(manifest.getFamilyFilesCount());
463    for (SnapshotRegionManifest.FamilyFiles familyFiles: manifest.getFamilyFilesList()) {
464      familyMap.put(familyFiles.getFamilyName().toStringUtf8(),
465        new ArrayList<>(familyFiles.getStoreFilesList()));
466    }
467    return familyMap;
468  }
469
470  /**
471   * Restore region by removing files not in the snapshot
472   * and adding the missing ones from the snapshot.
473   */
474  private void restoreRegion(final RegionInfo regionInfo,
475      final SnapshotRegionManifest regionManifest) throws IOException {
476    restoreRegion(regionInfo, regionManifest, new Path(tableDir, regionInfo.getEncodedName()));
477  }
478
479  /**
480   * Restore mob region by removing files not in the snapshot
481   * and adding the missing ones from the snapshot.
482   */
483  private void restoreMobRegion(final RegionInfo regionInfo,
484      final SnapshotRegionManifest regionManifest) throws IOException {
485    if (regionManifest == null) {
486      return;
487    }
488    restoreRegion(regionInfo, regionManifest,
489      MobUtils.getMobRegionPath(conf, tableDesc.getTableName()));
490  }
491
492  /**
493   * Restore region by removing files not in the snapshot
494   * and adding the missing ones from the snapshot.
495   */
496  private void restoreRegion(final RegionInfo regionInfo,
497      final SnapshotRegionManifest regionManifest, Path regionDir) throws IOException {
498    Map<String, List<SnapshotRegionManifest.StoreFile>> snapshotFiles =
499                getRegionHFileReferences(regionManifest);
500
501    String tableName = tableDesc.getTableName().getNameAsString();
502
503    // Restore families present in the table
504    for (Path familyDir: FSUtils.getFamilyDirs(fs, regionDir)) {
505      byte[] family = Bytes.toBytes(familyDir.getName());
506      Set<String> familyFiles = getTableRegionFamilyFiles(familyDir);
507      List<SnapshotRegionManifest.StoreFile> snapshotFamilyFiles =
508          snapshotFiles.remove(familyDir.getName());
509      if (snapshotFamilyFiles != null) {
510        List<SnapshotRegionManifest.StoreFile> hfilesToAdd = new ArrayList<>();
511        for (SnapshotRegionManifest.StoreFile storeFile: snapshotFamilyFiles) {
512          if (familyFiles.contains(storeFile.getName())) {
513            // HFile already present
514            familyFiles.remove(storeFile.getName());
515          } else {
516            // HFile missing
517            hfilesToAdd.add(storeFile);
518          }
519        }
520
521        // Remove hfiles not present in the snapshot
522        for (String hfileName: familyFiles) {
523          Path hfile = new Path(familyDir, hfileName);
524          LOG.trace("Removing hfile=" + hfileName +
525            " from region=" + regionInfo.getEncodedName() + " table=" + tableName);
526          HFileArchiver.archiveStoreFile(conf, fs, regionInfo, tableDir, family, hfile);
527        }
528
529        // Restore Missing files
530        for (SnapshotRegionManifest.StoreFile storeFile: hfilesToAdd) {
531          LOG.debug("Adding HFileLink " + storeFile.getName() +
532            " to region=" + regionInfo.getEncodedName() + " table=" + tableName);
533          restoreStoreFile(familyDir, regionInfo, storeFile, createBackRefs);
534        }
535      } else {
536        // Family doesn't exists in the snapshot
537        LOG.trace("Removing family=" + Bytes.toString(family) +
538          " from region=" + regionInfo.getEncodedName() + " table=" + tableName);
539        HFileArchiver.archiveFamilyByFamilyDir(fs, conf, regionInfo, familyDir, family);
540        fs.delete(familyDir, true);
541      }
542    }
543
544    // Add families not present in the table
545    for (Map.Entry<String, List<SnapshotRegionManifest.StoreFile>> familyEntry:
546                                                                      snapshotFiles.entrySet()) {
547      Path familyDir = new Path(regionDir, familyEntry.getKey());
548      if (!fs.mkdirs(familyDir)) {
549        throw new IOException("Unable to create familyDir=" + familyDir);
550      }
551
552      for (SnapshotRegionManifest.StoreFile storeFile: familyEntry.getValue()) {
553        LOG.trace("Adding HFileLink " + storeFile.getName() + " to table=" + tableName);
554        restoreStoreFile(familyDir, regionInfo, storeFile, createBackRefs);
555      }
556    }
557  }
558
559  /**
560   * @return The set of files in the specified family directory.
561   */
562  private Set<String> getTableRegionFamilyFiles(final Path familyDir) throws IOException {
563    FileStatus[] hfiles = FSUtils.listStatus(fs, familyDir);
564    if (hfiles == null) return Collections.emptySet();
565
566    Set<String> familyFiles = new HashSet<>(hfiles.length);
567    for (int i = 0; i < hfiles.length; ++i) {
568      String hfileName = hfiles[i].getPath().getName();
569      familyFiles.add(hfileName);
570    }
571
572    return familyFiles;
573  }
574
575  /**
576   * Clone specified regions. For each region create a new region
577   * and create a HFileLink for each hfile.
578   */
579  private RegionInfo[] cloneHdfsRegions(final ThreadPoolExecutor exec,
580      final Map<String, SnapshotRegionManifest> regionManifests,
581      final List<RegionInfo> regions) throws IOException {
582    if (regions == null || regions.isEmpty()) return null;
583
584    final Map<String, RegionInfo> snapshotRegions = new HashMap<>(regions.size());
585
586    // clone region info (change embedded tableName with the new one)
587    RegionInfo[] clonedRegionsInfo = new RegionInfo[regions.size()];
588    for (int i = 0; i < clonedRegionsInfo.length; ++i) {
589      // clone the region info from the snapshot region info
590      RegionInfo snapshotRegionInfo = regions.get(i);
591      clonedRegionsInfo[i] = cloneRegionInfo(snapshotRegionInfo);
592
593      // add the region name mapping between snapshot and cloned
594      String snapshotRegionName = snapshotRegionInfo.getEncodedName();
595      String clonedRegionName = clonedRegionsInfo[i].getEncodedName();
596      regionsMap.put(Bytes.toBytes(snapshotRegionName), Bytes.toBytes(clonedRegionName));
597      LOG.info("clone region=" + snapshotRegionName + " as " + clonedRegionName);
598
599      // Add mapping between cloned region name and snapshot region info
600      snapshotRegions.put(clonedRegionName, snapshotRegionInfo);
601    }
602
603    // create the regions on disk
604    ModifyRegionUtils.createRegions(exec, conf, rootDir,
605      tableDesc, clonedRegionsInfo, new ModifyRegionUtils.RegionFillTask() {
606        @Override
607        public void fillRegion(final HRegion region) throws IOException {
608          RegionInfo snapshotHri = snapshotRegions.get(region.getRegionInfo().getEncodedName());
609          cloneRegion(region, snapshotHri, regionManifests.get(snapshotHri.getEncodedName()));
610        }
611      });
612
613    return clonedRegionsInfo;
614  }
615
616  /**
617   * Clone the mob region. For the region create a new region
618   * and create a HFileLink for each hfile.
619   */
620  private void cloneHdfsMobRegion(final Map<String, SnapshotRegionManifest> regionManifests,
621      final RegionInfo region) throws IOException {
622    // clone region info (change embedded tableName with the new one)
623    Path clonedRegionPath = MobUtils.getMobRegionPath(rootDir, tableDesc.getTableName());
624    cloneRegion(clonedRegionPath, region, regionManifests.get(region.getEncodedName()));
625  }
626
627  /**
628   * Clone region directory content from the snapshot info.
629   *
630   * Each region is encoded with the table name, so the cloned region will have
631   * a different region name.
632   *
633   * Instead of copying the hfiles a HFileLink is created.
634   *
635   * @param regionDir {@link Path} cloned dir
636   * @param snapshotRegionInfo
637   */
638  private void cloneRegion(final Path regionDir, final RegionInfo snapshotRegionInfo,
639      final SnapshotRegionManifest manifest) throws IOException {
640    final String tableName = tableDesc.getTableName().getNameAsString();
641    for (SnapshotRegionManifest.FamilyFiles familyFiles: manifest.getFamilyFilesList()) {
642      Path familyDir = new Path(regionDir, familyFiles.getFamilyName().toStringUtf8());
643      for (SnapshotRegionManifest.StoreFile storeFile: familyFiles.getStoreFilesList()) {
644        LOG.info("Adding HFileLink " + storeFile.getName() + " to table=" + tableName);
645        restoreStoreFile(familyDir, snapshotRegionInfo, storeFile, createBackRefs);
646      }
647    }
648  }
649
650  /**
651   * Clone region directory content from the snapshot info.
652   *
653   * Each region is encoded with the table name, so the cloned region will have
654   * a different region name.
655   *
656   * Instead of copying the hfiles a HFileLink is created.
657   *
658   * @param region {@link HRegion} cloned
659   * @param snapshotRegionInfo
660   */
661  private void cloneRegion(final HRegion region, final RegionInfo snapshotRegionInfo,
662      final SnapshotRegionManifest manifest) throws IOException {
663    cloneRegion(new Path(tableDir, region.getRegionInfo().getEncodedName()), snapshotRegionInfo,
664      manifest);
665  }
666
667  /**
668   * Create a new {@link HFileLink} to reference the store file.
669   * <p>The store file in the snapshot can be a simple hfile, an HFileLink or a reference.
670   * <ul>
671   *   <li>hfile: abc -> table=region-abc
672   *   <li>reference: abc.1234 -> table=region-abc.1234
673   *   <li>hfilelink: table=region-hfile -> table=region-hfile
674   * </ul>
675   * @param familyDir destination directory for the store file
676   * @param regionInfo destination region info for the table
677   * @param createBackRef - Whether back reference should be created. Defaults to true.
678   * @param storeFile store file name (can be a Reference, HFileLink or simple HFile)
679   */
680  private void restoreStoreFile(final Path familyDir, final RegionInfo regionInfo,
681      final SnapshotRegionManifest.StoreFile storeFile, final boolean createBackRef)
682          throws IOException {
683    String hfileName = storeFile.getName();
684    if (HFileLink.isHFileLink(hfileName)) {
685      HFileLink.createFromHFileLink(conf, fs, familyDir, hfileName, createBackRef);
686    } else if (StoreFileInfo.isReference(hfileName)) {
687      restoreReferenceFile(familyDir, regionInfo, storeFile);
688    } else {
689      HFileLink.create(conf, fs, familyDir, regionInfo, hfileName, createBackRef);
690    }
691  }
692
693  /**
694   * Create a new {@link Reference} as copy of the source one.
695   * <p><blockquote><pre>
696   * The source table looks like:
697   *    1234/abc      (original file)
698   *    5678/abc.1234 (reference file)
699   *
700   * After the clone operation looks like:
701   *   wxyz/table=1234-abc
702   *   stuv/table=1234-abc.wxyz
703   *
704   * NOTE that the region name in the clone changes (md5 of regioninfo)
705   * and the reference should reflect that change.
706   * </pre></blockquote>
707   * @param familyDir destination directory for the store file
708   * @param regionInfo destination region info for the table
709   * @param storeFile reference file name
710   */
711  private void restoreReferenceFile(final Path familyDir, final RegionInfo regionInfo,
712      final SnapshotRegionManifest.StoreFile storeFile) throws IOException {
713    String hfileName = storeFile.getName();
714
715    // Extract the referred information (hfile name and parent region)
716    Path refPath =
717        StoreFileInfo.getReferredToFile(new Path(new Path(new Path(new Path(snapshotTable
718            .getNamespaceAsString(), snapshotTable.getQualifierAsString()), regionInfo
719            .getEncodedName()), familyDir.getName()), hfileName));
720    String snapshotRegionName = refPath.getParent().getParent().getName();
721    String fileName = refPath.getName();
722
723    // The new reference should have the cloned region name as parent, if it is a clone.
724    String clonedRegionName = Bytes.toString(regionsMap.get(Bytes.toBytes(snapshotRegionName)));
725    if (clonedRegionName == null) clonedRegionName = snapshotRegionName;
726
727    // The output file should be a reference link table=snapshotRegion-fileName.clonedRegionName
728    Path linkPath = null;
729    String refLink = fileName;
730    if (!HFileLink.isHFileLink(fileName)) {
731      refLink = HFileLink.createHFileLinkName(snapshotTable, snapshotRegionName, fileName);
732      linkPath = new Path(familyDir,
733        HFileLink.createHFileLinkName(snapshotTable, regionInfo.getEncodedName(), hfileName));
734    }
735
736    Path outPath = new Path(familyDir, refLink + '.' + clonedRegionName);
737
738    // Create the new reference
739    if (storeFile.hasReference()) {
740      Reference reference = Reference.convert(storeFile.getReference());
741      reference.write(fs, outPath);
742    } else {
743      InputStream in;
744      if (linkPath != null) {
745        in = HFileLink.buildFromHFileLinkPattern(conf, linkPath).open(fs);
746      } else {
747        linkPath = new Path(new Path(HRegion.getRegionDir(snapshotManifest.getSnapshotDir(),
748                        regionInfo.getEncodedName()), familyDir.getName()), hfileName);
749        in = fs.open(linkPath);
750      }
751      OutputStream out = fs.create(outPath);
752      IOUtils.copyBytes(in, out, conf);
753    }
754
755    // Add the daughter region to the map
756    String regionName = Bytes.toString(regionsMap.get(regionInfo.getEncodedNameAsBytes()));
757    if (regionName == null) {
758      regionName = regionInfo.getEncodedName();
759    }
760    LOG.debug("Restore reference " + regionName + " to " + clonedRegionName);
761    synchronized (parentsMap) {
762      Pair<String, String> daughters = parentsMap.get(clonedRegionName);
763      if (daughters == null) {
764        // In case one side of the split is already compacted, regionName is put as both first and
765        // second of Pair
766        daughters = new Pair<>(regionName, regionName);
767        parentsMap.put(clonedRegionName, daughters);
768      } else if (!regionName.equals(daughters.getFirst())) {
769        daughters.setSecond(regionName);
770      }
771    }
772  }
773
774  /**
775   * Create a new {@link RegionInfo} from the snapshot region info.
776   * Keep the same startKey, endKey, regionId and split information but change
777   * the table name.
778   *
779   * @param snapshotRegionInfo Info for region to clone.
780   * @return the new HRegion instance
781   */
782  public RegionInfo cloneRegionInfo(final RegionInfo snapshotRegionInfo) {
783    return cloneRegionInfo(tableDesc.getTableName(), snapshotRegionInfo);
784  }
785
786  public static RegionInfo cloneRegionInfo(TableName tableName, RegionInfo snapshotRegionInfo) {
787    return RegionInfoBuilder.newBuilder(tableName)
788        .setStartKey(snapshotRegionInfo.getStartKey())
789        .setEndKey(snapshotRegionInfo.getEndKey())
790        .setSplit(snapshotRegionInfo.isSplit())
791        .setRegionId(snapshotRegionInfo.getRegionId())
792        .setOffline(snapshotRegionInfo.isOffline())
793        .build();
794  }
795
796  /**
797   * @return the set of the regions contained in the table
798   */
799  private List<RegionInfo> getTableRegions() throws IOException {
800    LOG.debug("get table regions: " + tableDir);
801    FileStatus[] regionDirs = FSUtils.listStatus(fs, tableDir, new FSUtils.RegionDirFilter(fs));
802    if (regionDirs == null) return null;
803
804    List<RegionInfo> regions = new ArrayList<>(regionDirs.length);
805    for (int i = 0; i < regionDirs.length; ++i) {
806      RegionInfo hri = HRegionFileSystem.loadRegionInfoFileContent(fs, regionDirs[i].getPath());
807      regions.add(hri);
808    }
809    LOG.debug("found " + regions.size() + " regions for table=" +
810        tableDesc.getTableName().getNameAsString());
811    return regions;
812  }
813
814  /**
815   * Copy the snapshot files for a snapshot scanner, discards meta changes.
816   * @param conf
817   * @param fs
818   * @param rootDir
819   * @param restoreDir
820   * @param snapshotName
821   * @throws IOException
822   */
823  public static RestoreMetaChanges copySnapshotForScanner(Configuration conf, FileSystem fs,
824      Path rootDir, Path restoreDir, String snapshotName) throws IOException {
825    // ensure that restore dir is not under root dir
826    if (!restoreDir.getFileSystem(conf).getUri().equals(rootDir.getFileSystem(conf).getUri())) {
827      throw new IllegalArgumentException("Filesystems for restore directory and HBase root " +
828          "directory should be the same");
829    }
830    if (restoreDir.toUri().getPath().startsWith(rootDir.toUri().getPath())) {
831      throw new IllegalArgumentException("Restore directory cannot be a sub directory of HBase " +
832          "root directory. RootDir: " + rootDir + ", restoreDir: " + restoreDir);
833    }
834
835    Path snapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, rootDir);
836    SnapshotDescription snapshotDesc = SnapshotDescriptionUtils.readSnapshotInfo(fs, snapshotDir);
837    SnapshotManifest manifest = SnapshotManifest.open(conf, fs, snapshotDir, snapshotDesc);
838
839    MonitoredTask status = TaskMonitor.get().createStatus(
840        "Restoring  snapshot '" + snapshotName + "' to directory " + restoreDir);
841    ForeignExceptionDispatcher monitor = new ForeignExceptionDispatcher();
842
843    // we send createBackRefs=false so that restored hfiles do not create back reference links
844    // in the base hbase root dir.
845    RestoreSnapshotHelper helper = new RestoreSnapshotHelper(conf, fs,
846      manifest, manifest.getTableDescriptor(), restoreDir, monitor, status, false);
847    RestoreMetaChanges metaChanges = helper.restoreHdfsRegions(); // TODO: parallelize.
848
849    if (LOG.isDebugEnabled()) {
850      LOG.debug("Restored table dir:" + restoreDir);
851      FSUtils.logFileSystemState(fs, restoreDir, LOG);
852    }
853    return metaChanges;
854  }
855
856  public static void restoreSnapshotAcl(SnapshotDescription snapshot, TableName newTableName,
857      Configuration conf) throws IOException {
858    if (snapshot.hasUsersAndPermissions() && snapshot.getUsersAndPermissions() != null) {
859      LOG.info("Restore snapshot acl to table. snapshot: " + snapshot + ", table: " + newTableName);
860      ListMultimap<String, TablePermission> perms =
861          ShadedAccessControlUtil.toUserTablePermissions(snapshot.getUsersAndPermissions());
862      try (Connection conn = ConnectionFactory.createConnection(conf)) {
863        for (Entry<String, TablePermission> e : perms.entries()) {
864          String user = e.getKey();
865          TablePermission perm = e.getValue();
866          perm.setTableName(newTableName);
867          AccessControlClient.grant(conn, perm.getTableName(), user, perm.getFamily(),
868            perm.getQualifier(), perm.getActions());
869        }
870      } catch (Throwable e) {
871        throw new IOException("Grant acl into newly creatd table failed. snapshot: " + snapshot
872            + ", table: " + newTableName, e);
873      }
874    }
875  }
876}