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.security.access;
019
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.util.Set;
023import org.apache.hadoop.fs.FSDataInputStream;
024import org.apache.hadoop.fs.FileSystem;
025import org.apache.hadoop.fs.Path;
026import org.apache.hadoop.hbase.ActiveClusterSuffix;
027import org.apache.hadoop.hbase.Coprocessor;
028import org.apache.hadoop.hbase.CoprocessorEnvironment;
029import org.apache.hadoop.hbase.HBaseInterfaceAudience;
030import org.apache.hadoop.hbase.HConstants;
031import org.apache.hadoop.hbase.TableName;
032import org.apache.hadoop.hbase.WriteAttemptedOnReadOnlyClusterException;
033import org.apache.hadoop.hbase.coprocessor.ObserverContext;
034import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
035import org.apache.hadoop.hbase.exceptions.DeserializationException;
036import org.apache.hadoop.hbase.master.MasterFileSystem;
037import org.apache.hadoop.hbase.master.region.MasterRegionFactory;
038import org.apache.hadoop.hbase.util.FSUtils;
039import org.apache.yetus.audience.InterfaceAudience;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
044public abstract class AbstractReadOnlyController implements Coprocessor {
045  private static final Logger LOG = LoggerFactory.getLogger(AbstractReadOnlyController.class);
046
047  private static final Set<TableName> writableTables =
048    Set.of(TableName.META_TABLE_NAME, MasterRegionFactory.TABLE_NAME);
049
050  public static boolean
051    isWritableInReadOnlyMode(final ObserverContext<? extends RegionCoprocessorEnvironment> c) {
052    return writableTables.contains(c.getEnvironment().getRegionInfo().getTable());
053  }
054
055  public static boolean isWritableInReadOnlyMode(final TableName tableName) {
056    return writableTables.contains(tableName);
057  }
058
059  protected void internalReadOnlyGuard() throws WriteAttemptedOnReadOnlyClusterException {
060    throw new WriteAttemptedOnReadOnlyClusterException("Operation not allowed in Read-Only Mode");
061  }
062
063  @Override
064  public void start(CoprocessorEnvironment env) throws IOException {
065  }
066
067  @Override
068  public void stop(CoprocessorEnvironment env) {
069  }
070
071  /**
072   * Checks whether another cluster is currently active on this storage location by reading the
073   * {@value HConstants#ACTIVE_CLUSTER_SUFFIX_FILE_NAME} file on the filesystem.
074   * @param fs                 the filesystem to read from
075   * @param rootDir            the HBase root directory
076   * @param localClusterSuffix the local cluster's ActiveClusterSuffix identity
077   * @return true if the active cluster file exists and belongs to a different cluster; false if the
078   *         file does not exist or belongs to this cluster
079   */
080  public static boolean isAnotherClusterActive(FileSystem fs, Path rootDir,
081    ActiveClusterSuffix localClusterSuffix) {
082    try {
083      ActiveClusterSuffix fileData =
084        FSUtils.getClusterIdFile(fs, rootDir, new ActiveClusterSuffix.Parser());
085      if (fileData == null) {
086        return false;
087      }
088      return !localClusterSuffix.equals(fileData);
089    } catch (IOException e) {
090      LOG.warn(
091        "Failed to read active cluster suffix file from {} at {}. "
092          + "Assuming another cluster is active to prevent potential data corruption.",
093        HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME, rootDir, e);
094      return true;
095    }
096  }
097
098  public static void manageActiveClusterIdFile(boolean readOnlyEnabled, MasterFileSystem mfs) {
099    FileSystem fs = mfs.getFileSystem();
100    Path rootDir = mfs.getRootDir();
101    Path activeClusterFile = new Path(rootDir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME);
102
103    try {
104      if (readOnlyEnabled) {
105        // ENABLING READ-ONLY (false -> true), delete the active cluster file.
106        LOG.debug("Global read-only mode is being ENABLED. Deleting active cluster file: {}",
107          activeClusterFile);
108        try (FSDataInputStream in = fs.open(activeClusterFile)) {
109          ActiveClusterSuffix actualClusterFileData =
110            ActiveClusterSuffix.parseFrom(in.readAllBytes());
111          ActiveClusterSuffix expectedClusterFileData = mfs.getActiveClusterSuffix();
112          if (expectedClusterFileData.equals(actualClusterFileData)) {
113            fs.delete(activeClusterFile, false);
114            LOG.info("Successfully deleted active cluster file: {}", activeClusterFile);
115          } else {
116            LOG.debug(
117              "Active cluster file data does not match expected data. "
118                + "Not deleting the file to avoid potential inconsistency. "
119                + "Actual data: {}, Expected data: {}",
120              actualClusterFileData, expectedClusterFileData);
121          }
122        } catch (FileNotFoundException e) {
123          LOG.debug("Active cluster file does not exist at: {}. No need to delete.",
124            activeClusterFile);
125        } catch (IOException e) {
126          LOG.error(
127            "Failed to delete active cluster file: {}. "
128              + "Read-only flag will be updated, but file system state is inconsistent.",
129            activeClusterFile, e);
130        } catch (DeserializationException e) {
131          LOG.error("Failed to deserialize ActiveClusterSuffix from file {}", activeClusterFile, e);
132        }
133      } else {
134        // DISABLING READ-ONLY (true -> false), create the active cluster file id file
135        int wait = mfs.getConfiguration().getInt(HConstants.THREAD_WAKE_FREQUENCY, 10 * 1000);
136        if (!fs.exists(activeClusterFile)) {
137          FSUtils.setClusterIdFile(fs, rootDir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME,
138            mfs.getActiveClusterSuffix(), wait);
139        } else {
140          try (FSDataInputStream in = fs.open(activeClusterFile)) {
141            ActiveClusterSuffix existingData = ActiveClusterSuffix.parseFrom(in.readAllBytes());
142            ActiveClusterSuffix localData = mfs.getActiveClusterSuffix();
143            if (localData.equals(existingData)) {
144              LOG.debug("Active cluster file already exists at {} and belongs to this cluster. "
145                + "No need to create it again.", activeClusterFile);
146            } else {
147              LOG.error(
148                "Active cluster file at {} belongs to a different cluster. "
149                  + "ID from active cluster file: {}, ID of this cluster: {}. "
150                  + "Another cluster is already the active cluster.",
151                activeClusterFile, existingData, localData);
152            }
153          } catch (DeserializationException e) {
154            LOG.error("Failed to deserialize ActiveClusterSuffix from file {}", activeClusterFile,
155              e);
156          }
157        }
158      }
159    } catch (IOException e) {
160      // We still update the flag, but log that the operation failed.
161      LOG.error("Failed to perform file operation for read-only switch. "
162        + "Flag will be updated, but file system state may be inconsistent.", e);
163    }
164  }
165}