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.IOException;
021import java.net.URI;
022import java.security.PrivilegedExceptionAction;
023import java.util.Collections;
024import java.util.concurrent.TimeUnit;
025import org.apache.hadoop.conf.Configuration;
026import org.apache.hadoop.fs.FSDataInputStream;
027import org.apache.hadoop.fs.FSDataOutputStream;
028import org.apache.hadoop.fs.FileSystem;
029import org.apache.hadoop.fs.FileUtil;
030import org.apache.hadoop.fs.Path;
031import org.apache.hadoop.fs.permission.FsPermission;
032import org.apache.hadoop.hbase.HConstants;
033import org.apache.hadoop.hbase.TableName;
034import org.apache.hadoop.hbase.client.Admin;
035import org.apache.hadoop.hbase.client.Connection;
036import org.apache.hadoop.hbase.client.ConnectionFactory;
037import org.apache.hadoop.hbase.ipc.RpcServer;
038import org.apache.hadoop.hbase.security.User;
039import org.apache.hadoop.hbase.security.access.AccessChecker;
040import org.apache.hadoop.hbase.security.access.PermissionStorage;
041import org.apache.hadoop.hbase.security.access.ShadedAccessControlUtil;
042import org.apache.hadoop.hbase.security.access.UserPermission;
043import org.apache.hadoop.hbase.util.CommonFSUtils;
044import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
045import org.apache.hadoop.hbase.util.FSUtils;
046import org.apache.yetus.audience.InterfaceAudience;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import org.apache.hbase.thirdparty.com.google.common.collect.ListMultimap;
051
052import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription;
053
054/**
055 * Utility class to help manage {@link SnapshotDescription SnapshotDesriptions}.
056 * <p>
057 * Snapshots are laid out on disk like this:
058 *
059 * <pre>
060 * /hbase/.snapshots
061 *          /.tmp                &lt;---- working directory
062 *          /[snapshot name]     &lt;----- completed snapshot
063 * </pre>
064 *
065 * A completed snapshot named 'completed' then looks like (multiple regions, servers, files, etc.
066 * signified by '...' on the same directory depth).
067 *
068 * <pre>
069 * /hbase/.snapshots/completed
070 *                   .snapshotinfo          &lt;--- Description of the snapshot
071 *                   .tableinfo             &lt;--- Copy of the tableinfo
072 *                    /.logs
073 *                        /[server_name]
074 *                            /... [log files]
075 *                         ...
076 *                   /[region name]           &lt;---- All the region's information
077 *                   .regioninfo              &lt;---- Copy of the HRegionInfo
078 *                      /[column family name]
079 *                          /[hfile name]     &lt;--- name of the hfile in the real region
080 *                          ...
081 *                      ...
082 *                    ...
083 * </pre>
084 *
085 * Utility methods in this class are useful for getting the correct locations for different parts of
086 * the snapshot, as well as moving completed snapshots into place (see {@link #completeSnapshot},
087 * and writing the {@link SnapshotDescription} to the working snapshot directory.
088 */
089@InterfaceAudience.Private
090public final class SnapshotDescriptionUtils {
091
092  /**
093   * Filter that only accepts completed snapshot directories
094   */
095  public static class CompletedSnaphotDirectoriesFilter extends FSUtils.BlackListDirFilter {
096
097    /**
098     *     */
099    public CompletedSnaphotDirectoriesFilter(FileSystem fs) {
100      super(fs, Collections.singletonList(SNAPSHOT_TMP_DIR_NAME));
101    }
102  }
103
104  private static final Logger LOG = LoggerFactory.getLogger(SnapshotDescriptionUtils.class);
105  /**
106   * Version of the fs layout for a snapshot. Future snapshots may have different file layouts,
107   * which we may need to read in differently.
108   */
109  public static final int SNAPSHOT_LAYOUT_VERSION = SnapshotManifestV2.DESCRIPTOR_VERSION;
110
111  // snapshot directory constants
112  /**
113   * The file contains the snapshot basic information and it is under the directory of a snapshot.
114   */
115  public static final String SNAPSHOTINFO_FILE = ".snapshotinfo";
116
117  /** Temporary directory under the snapshot directory to store in-progress snapshots */
118  public static final String SNAPSHOT_TMP_DIR_NAME = ".tmp";
119
120  /**
121   * The configuration property that determines the filepath of the snapshot base working directory
122   */
123  public static final String SNAPSHOT_WORKING_DIR = "hbase.snapshot.working.dir";
124
125  // snapshot operation values
126  /** Default value if no start time is specified */
127  public static final long NO_SNAPSHOT_START_TIME_SPECIFIED = 0;
128
129  // Default value if no ttl is specified for Snapshot
130  private static final long NO_SNAPSHOT_TTL_SPECIFIED = 0;
131
132  public static final String MASTER_SNAPSHOT_TIMEOUT_MILLIS =
133    "hbase.snapshot.master.timeout.millis";
134
135  /** By default, wait 300 seconds for a snapshot to complete */
136  public static final long DEFAULT_MAX_WAIT_TIME = 60000 * 5;
137
138  public static final String SNAPSHOT_CORRUPTED_FILE = "_CORRUPTED";
139
140  private SnapshotDescriptionUtils() {
141    // private constructor for utility class
142  }
143
144  /**
145   * @param conf               {@link Configuration} from which to check for the timeout
146   * @param type               type of snapshot being taken
147   * @param defaultMaxWaitTime Default amount of time to wait, if none is in the configuration
148   * @return the max amount of time the master should wait for a snapshot to complete
149   */
150  public static long getMaxMasterTimeout(Configuration conf, SnapshotDescription.Type type,
151    long defaultMaxWaitTime) {
152    String confKey;
153    switch (type) {
154      case DISABLED:
155      default:
156        confKey = MASTER_SNAPSHOT_TIMEOUT_MILLIS;
157    }
158    return Math.max(conf.getLong(confKey, defaultMaxWaitTime),
159      conf.getLong(MASTER_SNAPSHOT_TIMEOUT_MILLIS, defaultMaxWaitTime));
160  }
161
162  /**
163   * Get the snapshot root directory. All the snapshots are kept under this directory, i.e.
164   * ${hbase.rootdir}/.snapshot
165   * @param rootDir hbase root directory
166   * @return the base directory in which all snapshots are kept
167   */
168  public static Path getSnapshotRootDir(final Path rootDir) {
169    return new Path(rootDir, HConstants.SNAPSHOT_DIR_NAME);
170  }
171
172  /**
173   * Get the directory for a specified snapshot. This directory is a sub-directory of snapshot root
174   * directory and all the data files for a snapshot are kept under this directory.
175   * @param snapshot snapshot being taken
176   * @param rootDir  hbase root directory
177   * @return the final directory for the completed snapshot
178   */
179  public static Path getCompletedSnapshotDir(final SnapshotDescription snapshot,
180    final Path rootDir) {
181    return getCompletedSnapshotDir(snapshot.getName(), rootDir);
182  }
183
184  /**
185   * Get the directory for a completed snapshot. This directory is a sub-directory of snapshot root
186   * directory and all the data files for a snapshot are kept under this directory.
187   * @param snapshotName name of the snapshot being taken
188   * @param rootDir      hbase root directory
189   * @return the final directory for the completed snapshot
190   */
191  public static Path getCompletedSnapshotDir(final String snapshotName, final Path rootDir) {
192    return getSpecifiedSnapshotDir(getSnapshotsDir(rootDir), snapshotName);
193  }
194
195  /**
196   * Get the general working directory for snapshots - where they are built, where they are
197   * temporarily copied on export, etc.
198   * @param rootDir root directory of the HBase installation
199   * @param conf    Configuration of the HBase instance
200   * @return Path to the snapshot tmp directory, relative to the passed root directory
201   */
202  public static Path getWorkingSnapshotDir(final Path rootDir, final Configuration conf) {
203    return new Path(
204      conf.get(SNAPSHOT_WORKING_DIR, getDefaultWorkingSnapshotDir(rootDir).toString()));
205  }
206
207  /**
208   * Get the directory to build a snapshot, before it is finalized
209   * @param snapshot snapshot that will be built
210   * @param rootDir  root directory of the hbase installation
211   * @param conf     Configuration of the HBase instance
212   * @return {@link Path} where one can build a snapshot
213   */
214  public static Path getWorkingSnapshotDir(SnapshotDescription snapshot, final Path rootDir,
215    Configuration conf) {
216    return getWorkingSnapshotDir(snapshot.getName(), rootDir, conf);
217  }
218
219  /**
220   * Get the directory to build a snapshot, before it is finalized
221   * @param snapshotName name of the snapshot
222   * @param rootDir      root directory of the hbase installation
223   * @param conf         Configuration of the HBase instance
224   * @return {@link Path} where one can build a snapshot
225   */
226  public static Path getWorkingSnapshotDir(String snapshotName, final Path rootDir,
227    Configuration conf) {
228    return getSpecifiedSnapshotDir(getWorkingSnapshotDir(rootDir, conf), snapshotName);
229  }
230
231  /**
232   * Get the flag file path if the snapshot is corrupted
233   * @param workingDir the directory where we build the specific snapshot
234   * @return {@link Path} snapshot corrupted flag file path
235   */
236  public static Path getCorruptedFlagFileForSnapshot(final Path workingDir) {
237    return new Path(workingDir, SNAPSHOT_CORRUPTED_FILE);
238  }
239
240  /**
241   * Get the directory within the given filepath to store the snapshot instance
242   * @param snapshotsDir directory to store snapshot directory within
243   * @param snapshotName name of the snapshot to take
244   * @return the final directory for the snapshot in the given filepath
245   */
246  private static final Path getSpecifiedSnapshotDir(final Path snapshotsDir, String snapshotName) {
247    return new Path(snapshotsDir, snapshotName);
248  }
249
250  /**
251   * @param rootDir hbase root directory
252   * @return the directory for all completed snapshots;
253   */
254  public static final Path getSnapshotsDir(Path rootDir) {
255    return new Path(rootDir, HConstants.SNAPSHOT_DIR_NAME);
256  }
257
258  /**
259   * Determines if the given workingDir is a subdirectory of the given "root directory"
260   * @param workingDir a directory to check
261   * @param rootDir    root directory of the HBase installation
262   * @return true if the given workingDir is a subdirectory of the given root directory, false
263   *         otherwise
264   */
265  public static boolean isSubDirectoryOf(final Path workingDir, final Path rootDir) {
266    return workingDir.toString().startsWith(rootDir.toString() + Path.SEPARATOR);
267  }
268
269  /**
270   * Determines if the given workingDir is a subdirectory of the default working snapshot directory
271   * @param workingDir a directory to check
272   * @param conf       configuration for the HBase cluster
273   * @return true if the given workingDir is a subdirectory of the default working directory for
274   *         snapshots, false otherwise
275   * @throws IOException if we can't get the root dir
276   */
277  public static boolean isWithinDefaultWorkingDir(final Path workingDir, Configuration conf)
278    throws IOException {
279    Path defaultWorkingDir = getDefaultWorkingSnapshotDir(CommonFSUtils.getRootDir(conf));
280    return workingDir.equals(defaultWorkingDir) || isSubDirectoryOf(workingDir, defaultWorkingDir);
281  }
282
283  /**
284   * Get the default working directory for snapshots - where they are built, where they are
285   * temporarily copied on export, etc.
286   * @param rootDir root directory of the HBase installation
287   * @return Path to the default snapshot tmp directory, relative to the passed root directory
288   */
289  private static Path getDefaultWorkingSnapshotDir(final Path rootDir) {
290    return new Path(getSnapshotsDir(rootDir), SNAPSHOT_TMP_DIR_NAME);
291  }
292
293  /**
294   * Convert the passed snapshot description into a 'full' snapshot description based on default
295   * parameters, if none have been supplied. This resolves any 'optional' parameters that aren't
296   * supplied to their default values.
297   * @param snapshot general snapshot descriptor
298   * @param conf     Configuration to read configured snapshot defaults if snapshot is not complete
299   * @return a valid snapshot description
300   * @throws IllegalArgumentException if the {@link SnapshotDescription} is not a complete
301   *                                  {@link SnapshotDescription}.
302   */
303  public static SnapshotDescription validate(SnapshotDescription snapshot, Configuration conf)
304    throws IllegalArgumentException, IOException {
305    if (!snapshot.hasTable()) {
306      throw new IllegalArgumentException(
307        "Descriptor doesn't apply to a table, so we can't build it.");
308    }
309
310    SnapshotDescription.Builder builder = snapshot.toBuilder();
311
312    // set the creation time, if one hasn't been set
313    long time = snapshot.getCreationTime();
314    if (time == SnapshotDescriptionUtils.NO_SNAPSHOT_START_TIME_SPECIFIED) {
315      time = EnvironmentEdgeManager.currentTime();
316      LOG.debug("Creation time not specified, setting to:" + time + " (current time:"
317        + EnvironmentEdgeManager.currentTime() + ").");
318      builder.setCreationTime(time);
319    }
320
321    long ttl = snapshot.getTtl();
322    // set default ttl(sec) if it is not set already or the value is out of the range
323    if (
324      ttl == SnapshotDescriptionUtils.NO_SNAPSHOT_TTL_SPECIFIED
325        || ttl > TimeUnit.MILLISECONDS.toSeconds(Long.MAX_VALUE)
326    ) {
327      final long defaultSnapshotTtl =
328        conf.getLong(HConstants.DEFAULT_SNAPSHOT_TTL_CONFIG_KEY, HConstants.DEFAULT_SNAPSHOT_TTL);
329      if (LOG.isDebugEnabled()) {
330        LOG.debug("Snapshot current TTL value: {} resetting it to default value: {}", ttl,
331          defaultSnapshotTtl);
332      }
333      ttl = defaultSnapshotTtl;
334    }
335    builder.setTtl(ttl);
336
337    if (!snapshot.hasVersion()) {
338      builder.setVersion(SnapshotDescriptionUtils.SNAPSHOT_LAYOUT_VERSION);
339      LOG.debug("Snapshot {} VERSION not specified, setting to {}", snapshot.getName(),
340        SnapshotDescriptionUtils.SNAPSHOT_LAYOUT_VERSION);
341    }
342
343    RpcServer.getRequestUser().ifPresent(user -> {
344      if (AccessChecker.isAuthorizationSupported(conf)) {
345        builder.setOwner(user.getShortName());
346        LOG.debug("Set {} as owner of Snapshot", user.getShortName());
347      }
348    });
349
350    snapshot = builder.build();
351
352    // set the acl to snapshot if security feature is enabled.
353    if (isSecurityAvailable(conf)) {
354      snapshot = writeAclToSnapshotDescription(snapshot, conf);
355    }
356    return snapshot;
357  }
358
359  /**
360   * Write the snapshot description into the working directory of a snapshot
361   * @param snapshot   description of the snapshot being taken
362   * @param workingDir working directory of the snapshot
363   * @param fs         {@link FileSystem} on which the snapshot should be taken
364   * @throws IOException if we can't reach the filesystem and the file cannot be cleaned up on
365   *                     failure
366   */
367  public static void writeSnapshotInfo(SnapshotDescription snapshot, Path workingDir, FileSystem fs)
368    throws IOException {
369    FsPermission perms =
370      CommonFSUtils.getFilePermissions(fs, fs.getConf(), HConstants.DATA_FILE_UMASK_KEY);
371    Path snapshotInfo = new Path(workingDir, SnapshotDescriptionUtils.SNAPSHOTINFO_FILE);
372    try (FSDataOutputStream out = CommonFSUtils.create(fs, snapshotInfo, perms, true)) {
373      snapshot.writeTo(out);
374    } catch (IOException e) {
375      // if we get an exception, try to remove the snapshot info
376      if (!fs.delete(snapshotInfo, false)) {
377        String msg = "Couldn't delete snapshot info file: " + snapshotInfo;
378        LOG.error(msg);
379        throw new IOException(msg);
380      }
381    }
382  }
383
384  /**
385   * Read in the {@link SnapshotDescription} stored for the snapshot in the passed directory
386   * @param fs          filesystem where the snapshot was taken
387   * @param snapshotDir directory where the snapshot was stored
388   * @return the stored snapshot description
389   * @throws CorruptedSnapshotException if the snapshot cannot be read
390   */
391  public static SnapshotDescription readSnapshotInfo(FileSystem fs, Path snapshotDir)
392    throws CorruptedSnapshotException {
393    Path snapshotInfo = new Path(snapshotDir, SNAPSHOTINFO_FILE);
394    try (FSDataInputStream in = fs.open(snapshotInfo)) {
395      return SnapshotDescription.parseFrom(in);
396    } catch (IOException e) {
397      throw new CorruptedSnapshotException("Couldn't read snapshot info from:" + snapshotInfo, e);
398    }
399  }
400
401  /**
402   * Commits the snapshot process by moving the working snapshot to the finalized filepath
403   * @param snapshotDir  The file path of the completed snapshots
404   * @param workingDir   The file path of the in progress snapshots
405   * @param fs           The file system of the completed snapshots
406   * @param workingDirFs The file system of the in progress snapshots
407   * @param conf         Configuration
408   * @throws SnapshotCreationException if the snapshot could not be moved
409   * @throws IOException               the filesystem could not be reached
410   */
411  public static void completeSnapshot(Path snapshotDir, Path workingDir, FileSystem fs,
412    FileSystem workingDirFs, final Configuration conf)
413    throws SnapshotCreationException, IOException {
414    LOG.debug(
415      "Sentinel is done, just moving the snapshot from " + workingDir + " to " + snapshotDir);
416    // If the working and completed snapshot directory are on the same file system, attempt
417    // to rename the working snapshot directory to the completed location. If that fails,
418    // or the file systems differ, attempt to copy the directory over, throwing an exception
419    // if this fails
420    URI workingURI = workingDirFs.getUri();
421    URI rootURI = fs.getUri();
422
423    if (
424      (shouldSkipRenameSnapshotDirectories(workingURI, rootURI)
425        || !fs.rename(workingDir, snapshotDir))
426        && !FileUtil.copy(workingDirFs, workingDir, fs, snapshotDir, true, true, conf)
427    ) {
428      throw new SnapshotCreationException("Failed to copy working directory(" + workingDir
429        + ") to completed directory(" + snapshotDir + ").");
430    }
431  }
432
433  static boolean shouldSkipRenameSnapshotDirectories(URI workingURI, URI rootURI) {
434    // check scheme, e.g. file, hdfs
435    if (workingURI.getScheme() == null && rootURI.getScheme() != null) {
436      return true;
437    }
438    if (workingURI.getScheme() != null && !workingURI.getScheme().equals(rootURI.getScheme())) {
439      return true;
440    }
441
442    // check Authority, e.g. localhost:port
443    if (workingURI.getAuthority() == null && rootURI.getAuthority() != null) {
444      return true;
445    }
446    if (
447      workingURI.getAuthority() != null && !workingURI.getAuthority().equals(rootURI.getAuthority())
448    ) {
449      return true;
450    }
451
452    // check UGI/userInfo
453    if (workingURI.getUserInfo() == null && rootURI.getUserInfo() != null) {
454      return true;
455    }
456    if (
457      workingURI.getUserInfo() != null && !workingURI.getUserInfo().equals(rootURI.getUserInfo())
458    ) {
459      return true;
460    }
461    return false;
462  }
463
464  /**
465   * Check if the user is this table snapshot's owner
466   * @param snapshot the table snapshot description
467   * @param user     the user
468   * @return true if the user is the owner of the snapshot, false otherwise or the snapshot owner
469   *         field is not present.
470   */
471  public static boolean isSnapshotOwner(org.apache.hadoop.hbase.client.SnapshotDescription snapshot,
472    User user) {
473    if (user == null) return false;
474    return user.getShortName().equals(snapshot.getOwner());
475  }
476
477  public static boolean isSecurityAvailable(Configuration conf) throws IOException {
478    try (Connection conn = ConnectionFactory.createConnection(conf);
479      Admin admin = conn.getAdmin()) {
480      return admin.tableExists(PermissionStorage.ACL_TABLE_NAME);
481    }
482  }
483
484  private static SnapshotDescription writeAclToSnapshotDescription(SnapshotDescription snapshot,
485    Configuration conf) throws IOException {
486    ListMultimap<String, UserPermission> perms =
487      User.runAsLoginUser(new PrivilegedExceptionAction<ListMultimap<String, UserPermission>>() {
488        @Override
489        public ListMultimap<String, UserPermission> run() throws Exception {
490          return PermissionStorage.getTablePermissions(conf,
491            TableName.valueOf(snapshot.getTable()));
492        }
493      });
494    return snapshot.toBuilder()
495      .setUsersAndPermissions(ShadedAccessControlUtil.toUserTablePermissions(perms)).build();
496  }
497
498  /**
499   * Method to check whether TTL has expired for specified snapshot creation time and snapshot ttl.
500   * NOTE: For backward compatibility (after the patch deployment on HMaster), any snapshot with ttl
501   * 0 is to be considered as snapshot to keep FOREVER. Default ttl value specified by
502   * {@link HConstants#DEFAULT_SNAPSHOT_TTL}
503   * @return true if ttl has expired, or, false, otherwise
504   */
505  public static boolean isExpiredSnapshot(long snapshotTtl, long snapshotCreatedTime,
506    long currentTime) {
507    return snapshotCreatedTime > 0 && snapshotTtl > HConstants.DEFAULT_SNAPSHOT_TTL
508      && snapshotTtl < TimeUnit.MILLISECONDS.toSeconds(Long.MAX_VALUE)
509      && (snapshotCreatedTime + TimeUnit.SECONDS.toMillis(snapshotTtl)) < currentTime;
510  }
511}