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