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.master;
019
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import org.apache.hadoop.conf.Configuration;
023import org.apache.hadoop.fs.FileStatus;
024import org.apache.hadoop.fs.FileSystem;
025import org.apache.hadoop.fs.Path;
026import org.apache.hadoop.fs.permission.FsAction;
027import org.apache.hadoop.fs.permission.FsPermission;
028import org.apache.hadoop.hbase.ActiveClusterSuffix;
029import org.apache.hadoop.hbase.ClusterId;
030import org.apache.hadoop.hbase.HConstants;
031import org.apache.hadoop.hbase.backup.HFileArchiver;
032import org.apache.hadoop.hbase.client.RegionInfo;
033import org.apache.hadoop.hbase.exceptions.DeserializationException;
034import org.apache.hadoop.hbase.fs.HFileSystem;
035import org.apache.hadoop.hbase.log.HBaseMarkers;
036import org.apache.hadoop.hbase.mob.MobConstants;
037import org.apache.hadoop.hbase.replication.ReplicationUtils;
038import org.apache.hadoop.hbase.security.access.SnapshotScannerHDFSAclHelper;
039import org.apache.hadoop.hbase.util.Bytes;
040import org.apache.hadoop.hbase.util.CommonFSUtils;
041import org.apache.hadoop.hbase.util.ConfigurationUtil;
042import org.apache.hadoop.hbase.util.FSUtils;
043import org.apache.yetus.audience.InterfaceAudience;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047/**
048 * This class abstracts a bunch of operations the HMaster needs to interact with the underlying file
049 * system like creating the initial layout, checking file system status, etc.
050 */
051@InterfaceAudience.Private
052public class MasterFileSystem {
053  private static final Logger LOG = LoggerFactory.getLogger(MasterFileSystem.class);
054
055  /** Parameter name for HBase instance root directory permission */
056  public static final String HBASE_DIR_PERMS = "hbase.rootdir.perms";
057
058  /** Parameter name for HBase WAL directory permission */
059  public static final String HBASE_WAL_DIR_PERMS = "hbase.wal.dir.perms";
060
061  // HBase configuration
062  private final Configuration conf;
063  // Persisted unique cluster ID
064  private ClusterId clusterId;
065  // Persisted unique Active Cluster Suffix
066  private ActiveClusterSuffix activeClusterSuffix;
067  // Keep around for convenience.
068  private final FileSystem fs;
069  // Keep around for convenience.
070  private final FileSystem walFs;
071  // root log directory on the FS
072  private final Path rootdir;
073  // hbase temp directory used for table construction and deletion
074  private final Path tempdir;
075  // root hbase directory on the FS
076  private final Path walRootDir;
077
078  /*
079   * In a secure env, the protected sub-directories and files under the HBase rootDir would be
080   * restricted. The sub-directory will have '700' except the bulk load staging dir, which will have
081   * '711'. The default '700' can be overwritten by setting the property 'hbase.rootdir.perms'. The
082   * protected files (version file, clusterId file) will have '600'. The rootDir itself will be
083   * created with HDFS default permissions if it does not exist. We will check the rootDir
084   * permissions to make sure it has 'x' for all to ensure access to the staging dir. If it does
085   * not, we will add it.
086   */
087  // Permissions for the directories under rootDir that need protection
088  private final FsPermission secureRootSubDirPerms;
089  // Permissions for the files under rootDir that need protection
090  private final FsPermission secureRootFilePerms = new FsPermission("600");
091  // Permissions for bulk load staging directory under rootDir
092  private final FsPermission HiddenDirPerms = FsPermission.valueOf("-rwx--x--x");
093
094  private boolean isSecurityEnabled;
095
096  public MasterFileSystem(Configuration conf) throws IOException {
097    this.conf = conf;
098    // Set filesystem to be that of this.rootdir else we get complaints about
099    // mismatched filesystems if hbase.rootdir is hdfs and fs.defaultFS is
100    // default localfs. Presumption is that rootdir is fully-qualified before
101    // we get to here with appropriate fs scheme.
102    this.rootdir = CommonFSUtils.getRootDir(conf);
103    this.tempdir = new Path(this.rootdir, HConstants.HBASE_TEMP_DIRECTORY);
104    // Cover both bases, the old way of setting default fs and the new.
105    // We're supposed to run on 0.20 and 0.21 anyways.
106    this.fs = this.rootdir.getFileSystem(conf);
107    this.walRootDir = CommonFSUtils.getWALRootDir(conf);
108    this.walFs = CommonFSUtils.getWALFileSystem(conf);
109    CommonFSUtils.setFsDefault(conf, new Path(this.walFs.getUri()));
110    walFs.setConf(conf);
111    CommonFSUtils.setFsDefault(conf, new Path(this.fs.getUri()));
112    // make sure the fs has the same conf
113    fs.setConf(conf);
114    this.secureRootSubDirPerms = new FsPermission(conf.get("hbase.rootdir.perms", "700"));
115    this.isSecurityEnabled = "kerberos".equalsIgnoreCase(conf.get("hbase.security.authentication"));
116    // setup the filesystem variable
117    createInitialFileSystemLayout();
118    HFileSystem.addLocationsOrderInterceptor(conf);
119  }
120
121  /**
122   * Create initial layout in filesystem.
123   * <ol>
124   * <li>Check if the meta region exists and is readable, if not create it. Create hbase.version and
125   * the hbase:meta directory if not one.</li>
126   * </ol>
127   * Idempotent.
128   */
129  private void createInitialFileSystemLayout() throws IOException {
130    final String[] protectedSubDirs =
131      new String[] { HConstants.BASE_NAMESPACE_DIR, HConstants.HFILE_ARCHIVE_DIRECTORY,
132        HConstants.HBCK_SIDELINEDIR_NAME, MobConstants.MOB_DIR_NAME };
133
134    // With the introduction of RegionProcedureStore,
135    // there's no need to create MasterProcWAL dir here anymore. See HBASE-23715
136    final String[] protectedSubLogDirs =
137      new String[] { HConstants.HREGION_LOGDIR_NAME, HConstants.HREGION_OLDLOGDIR_NAME,
138        HConstants.CORRUPT_DIR_NAME, ReplicationUtils.REMOTE_WAL_DIR_NAME };
139    // check if the root directory exists
140    checkRootDir(this.rootdir, conf, this.fs);
141
142    // Check the directories under rootdir.
143    checkTempDir(this.tempdir, conf, this.fs);
144    for (String subDir : protectedSubDirs) {
145      checkSubDir(new Path(this.rootdir, subDir), HBASE_DIR_PERMS);
146    }
147
148    final String perms;
149    if (!this.walRootDir.equals(this.rootdir)) {
150      perms = HBASE_WAL_DIR_PERMS;
151    } else {
152      perms = HBASE_DIR_PERMS;
153    }
154    for (String subDir : protectedSubLogDirs) {
155      checkSubDir(new Path(this.walRootDir, subDir), perms);
156    }
157
158    checkStagingDir();
159
160    // Handle the last few special files and set the final rootDir permissions
161    // rootDir needs 'x' for all to support bulk load staging dir
162    if (isSecurityEnabled) {
163      fs.setPermission(new Path(rootdir, HConstants.VERSION_FILE_NAME), secureRootFilePerms);
164      fs.setPermission(new Path(rootdir, HConstants.CLUSTER_ID_FILE_NAME), secureRootFilePerms);
165      fs.setPermission(new Path(rootdir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME),
166        secureRootFilePerms);
167    }
168    FsPermission currentRootPerms = fs.getFileStatus(this.rootdir).getPermission();
169    if (
170      !currentRootPerms.getUserAction().implies(FsAction.EXECUTE)
171        || !currentRootPerms.getGroupAction().implies(FsAction.EXECUTE)
172        || !currentRootPerms.getOtherAction().implies(FsAction.EXECUTE)
173    ) {
174      LOG.warn("rootdir permissions do not contain 'excute' for user, group or other. "
175        + "Automatically adding 'excute' permission for all");
176      fs.setPermission(this.rootdir,
177        new FsPermission(currentRootPerms.getUserAction().or(FsAction.EXECUTE),
178          currentRootPerms.getGroupAction().or(FsAction.EXECUTE),
179          currentRootPerms.getOtherAction().or(FsAction.EXECUTE)));
180    }
181  }
182
183  public FileSystem getFileSystem() {
184    return this.fs;
185  }
186
187  public FileSystem getWALFileSystem() {
188    return this.walFs;
189  }
190
191  public Configuration getConfiguration() {
192    return this.conf;
193  }
194
195  /** Returns HBase root dir. */
196  public Path getRootDir() {
197    return this.rootdir;
198  }
199
200  /** Returns HBase root log dir. */
201  public Path getWALRootDir() {
202    return this.walRootDir;
203  }
204
205  /** Returns the directory for a give {@code region}. */
206  public Path getRegionDir(RegionInfo region) {
207    return FSUtils.getRegionDirFromRootDir(getRootDir(), region);
208  }
209
210  /** Returns HBase temp dir. */
211  public Path getTempDir() {
212    return this.tempdir;
213  }
214
215  /** Returns The unique identifier generated for this cluster */
216  public ClusterId getClusterId() {
217    return clusterId;
218  }
219
220  /**
221   * Get the rootdir. Make sure its wholesome and exists before returning.
222   * @return hbase.rootdir (after checks for existence and bootstrapping if needed populating the
223   *         directory with necessary bootup files).
224   */
225  private void checkRootDir(final Path rd, final Configuration c, final FileSystem fs)
226    throws IOException {
227    int threadWakeFrequency = c.getInt(HConstants.THREAD_WAKE_FREQUENCY, 10 * 1000);
228    // If FS is in safe mode wait till out of it.
229    FSUtils.waitOnSafeMode(c, threadWakeFrequency);
230
231    // Filesystem is good. Go ahead and check for hbase.rootdir.
232    FileStatus status;
233    try {
234      status = fs.getFileStatus(rd);
235    } catch (FileNotFoundException e) {
236      status = null;
237    }
238    int versionFileWriteAttempts = c.getInt(HConstants.VERSION_FILE_WRITE_ATTEMPTS,
239      HConstants.DEFAULT_VERSION_FILE_WRITE_ATTEMPTS);
240    try {
241      if (status == null) {
242        if (!fs.mkdirs(rd)) {
243          throw new IOException("Can not create configured '" + HConstants.HBASE_DIR + "' " + rd);
244        }
245        // DFS leaves safe mode with 0 DNs when there are 0 blocks.
246        // We used to handle this by checking the current DN count and waiting until
247        // it is nonzero. With security, the check for datanode count doesn't work --
248        // it is a privileged op. So instead we adopt the strategy of the jobtracker
249        // and simply retry file creation during bootstrap indefinitely. As soon as
250        // there is one datanode it will succeed. Permission problems should have
251        // already been caught by mkdirs above.
252        FSUtils.setVersion(fs, rd, threadWakeFrequency, versionFileWriteAttempts);
253      } else {
254        if (!status.isDirectory()) {
255          throw new IllegalArgumentException(
256            "Configured '" + HConstants.HBASE_DIR + "' " + rd + " is not a directory.");
257        }
258        // as above
259        FSUtils.checkVersion(fs, rd, true, threadWakeFrequency, versionFileWriteAttempts);
260      }
261    } catch (DeserializationException de) {
262      LOG.error(HBaseMarkers.FATAL, "Please fix invalid configuration for '{}' {}",
263        HConstants.HBASE_DIR, rd, de);
264      throw new IOException(de);
265    } catch (IllegalArgumentException iae) {
266      LOG.error(HBaseMarkers.FATAL, "Please fix invalid configuration for '{}' {}",
267        HConstants.HBASE_DIR, rd, iae);
268      throw iae;
269    }
270    // Make sure cluster ID exists
271    if (
272      !FSUtils.checkFileExistsInHbaseRootDir(fs, rootdir, HConstants.CLUSTER_ID_FILE_NAME,
273        threadWakeFrequency)
274    ) {
275      FSUtils.setClusterIdFile(fs, rootdir, HConstants.CLUSTER_ID_FILE_NAME, new ClusterId(),
276        threadWakeFrequency);
277    }
278    clusterId = FSUtils.getClusterIdFile(fs, rootdir, new ClusterId.Parser());
279    negotiateActiveClusterSuffixFile(threadWakeFrequency);
280  }
281
282  /**
283   * Make sure the hbase temp directory exists and is empty. NOTE that this method is only executed
284   * once just after the master becomes the active one.
285   */
286  void checkTempDir(final Path tmpdir, final Configuration c, final FileSystem fs)
287    throws IOException {
288    // If the temp directory exists, clear the content (left over, from the previous run)
289    if (fs.exists(tmpdir)) {
290      // Archive table in temp, maybe left over from failed deletion,
291      // if not the cleaner will take care of them.
292      for (Path tableDir : FSUtils.getTableDirs(fs, tmpdir)) {
293        HFileArchiver.archiveRegions(c, fs, this.rootdir, tableDir,
294          FSUtils.getRegionDirs(fs, tableDir));
295        if (!FSUtils.getRegionDirs(fs, tableDir).isEmpty()) {
296          LOG.warn("Found regions in tmp dir after archiving table regions, {}", tableDir);
297        }
298      }
299      // if acl sync to hdfs is enabled, then skip delete tmp dir because ACLs are set
300      if (!SnapshotScannerHDFSAclHelper.isAclSyncToHdfsEnabled(c) && !fs.delete(tmpdir, true)) {
301        throw new IOException("Unable to clean the temp directory: " + tmpdir);
302      }
303    }
304
305    // Create the temp directory
306    if (!fs.exists(tmpdir)) {
307      if (isSecurityEnabled) {
308        if (!fs.mkdirs(tmpdir, secureRootSubDirPerms)) {
309          throw new IOException("HBase temp directory '" + tmpdir + "' creation failure.");
310        }
311      } else {
312        if (!fs.mkdirs(tmpdir)) {
313          throw new IOException("HBase temp directory '" + tmpdir + "' creation failure.");
314        }
315      }
316    }
317  }
318
319  /**
320   * Make sure the directories under rootDir have good permissions. Create if necessary.
321   */
322  private void checkSubDir(final Path p, final String dirPermsConfName) throws IOException {
323    FileSystem fs = p.getFileSystem(conf);
324    FsPermission dirPerms = new FsPermission(conf.get(dirPermsConfName, "700"));
325    if (!fs.exists(p)) {
326      if (isSecurityEnabled) {
327        if (!fs.mkdirs(p, secureRootSubDirPerms)) {
328          throw new IOException("HBase directory '" + p + "' creation failure.");
329        }
330      } else {
331        if (!fs.mkdirs(p)) {
332          throw new IOException("HBase directory '" + p + "' creation failure.");
333        }
334      }
335    }
336    if (isSecurityEnabled && !dirPerms.equals(fs.getFileStatus(p).getPermission())) {
337      // check whether the permission match
338      LOG.warn("Found HBase directory permissions NOT matching expected permissions for "
339        + p.toString() + " permissions=" + fs.getFileStatus(p).getPermission() + ", expecting "
340        + dirPerms + ". Automatically setting the permissions. "
341        + "You can change the permissions by setting \"" + dirPermsConfName
342        + "\" in hbase-site.xml " + "and restarting the master");
343      fs.setPermission(p, dirPerms);
344    }
345  }
346
347  /**
348   * Check permissions for bulk load staging directory. This directory has special hidden
349   * permissions. Create it if necessary.
350   */
351  private void checkStagingDir() throws IOException {
352    Path p = new Path(this.rootdir, HConstants.BULKLOAD_STAGING_DIR_NAME);
353    try {
354      if (!this.fs.exists(p)) {
355        if (!this.fs.mkdirs(p, HiddenDirPerms)) {
356          throw new IOException("Failed to create staging directory " + p.toString());
357        }
358      }
359      this.fs.setPermission(p, HiddenDirPerms);
360
361    } catch (IOException e) {
362      LOG.error("Failed to create or set permission on staging directory " + p.toString());
363      throw new IOException(
364        "Failed to create or set permission on staging directory " + p.toString(), e);
365    }
366  }
367
368  public void deleteFamilyFromFS(RegionInfo region, byte[] familyName) throws IOException {
369    deleteFamilyFromFS(rootdir, region, familyName);
370  }
371
372  public void deleteFamilyFromFS(Path rootDir, RegionInfo region, byte[] familyName)
373    throws IOException {
374    // archive family store files
375    Path tableDir = CommonFSUtils.getTableDir(rootDir, region.getTable());
376    HFileArchiver.archiveFamily(fs, conf, region, tableDir, familyName);
377
378    // delete the family folder
379    Path familyDir =
380      new Path(tableDir, new Path(region.getEncodedName(), Bytes.toString(familyName)));
381    if (fs.delete(familyDir, true) == false) {
382      if (fs.exists(familyDir)) {
383        throw new IOException(
384          "Could not delete family " + Bytes.toString(familyName) + " from FileSystem for region "
385            + region.getRegionNameAsString() + "(" + region.getEncodedName() + ")");
386      }
387    }
388  }
389
390  public void stop() {
391  }
392
393  public void logFileSystemState(Logger log) throws IOException {
394    CommonFSUtils.logFileSystemState(fs, rootdir, log);
395  }
396
397  private void negotiateActiveClusterSuffixFile(long wait) throws IOException {
398    this.activeClusterSuffix = ActiveClusterSuffix.fromConfig(conf, getClusterId());
399    if (!ConfigurationUtil.isReadOnlyModeEnabledInConf(conf)) {
400      try {
401        // verify the contents against the config set
402        ActiveClusterSuffix acs =
403          FSUtils.getClusterIdFile(fs, rootdir, new ActiveClusterSuffix.Parser());
404        if (acs == null) {
405          throw new FileNotFoundException("[Read-replica feature] Active Cluster Suffix File "
406            + new Path(rootdir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME) + " not found");
407        }
408        LOG.debug(
409          "Negotiating active cluster suffix file. File {} : File Suffix {} : Configured suffix {} :  Cluster ID : {}",
410          new Path(rootdir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME), acs, activeClusterSuffix,
411          getClusterId());
412        // Suffix file exists and we're in read/write mode. Content should match.
413        if (!this.activeClusterSuffix.equals(acs)) {
414          // throw error
415          LOG.error(
416            "[Read-replica feature] Another cluster is running in active (read-write) mode on this "
417              + "storage location. Active cluster ID: {}, This cluster ID {}. Rootdir location {} ",
418            acs, activeClusterSuffix, rootdir);
419          throw new IOException("Cannot start master, because another cluster is running in active "
420            + "(read-write) mode on this storage location. Active Cluster Id: " + acs
421            + ", This cluster Id: " + activeClusterSuffix);
422        }
423        LOG.info(
424          "[Read-replica feature] This is the active cluster on this storage location with cluster id: {}",
425          activeClusterSuffix);
426      } catch (FileNotFoundException fnfe) {
427        // We're in read/write mode, but suffix file missing, let's create it
428        FSUtils.setClusterIdFile(fs, rootdir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME,
429          activeClusterSuffix, wait);
430        LOG.info("[Read-replica feature] Created Active cluster suffix file: {}, with content: {}",
431          HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME, activeClusterSuffix);
432      }
433    } else {
434      // This is a read-only cluster, don't care about suffix file
435      LOG.info("[Read-replica feature] Replica cluster is being started in Read Only Mode");
436    }
437  }
438
439  public ActiveClusterSuffix getActiveClusterSuffix() {
440    return activeClusterSuffix;
441  }
442}