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 static org.junit.jupiter.api.Assertions.assertEquals; 021import static org.junit.jupiter.api.Assertions.assertFalse; 022import static org.junit.jupiter.api.Assertions.assertNotNull; 023import static org.junit.jupiter.api.Assertions.assertThrows; 024import static org.junit.jupiter.api.Assertions.assertTrue; 025 026import java.io.IOException; 027import java.util.List; 028import org.apache.hadoop.conf.Configuration; 029import org.apache.hadoop.fs.FSDataInputStream; 030import org.apache.hadoop.fs.FSDataOutputStream; 031import org.apache.hadoop.fs.FileSystem; 032import org.apache.hadoop.fs.Path; 033import org.apache.hadoop.hbase.ActiveClusterSuffix; 034import org.apache.hadoop.hbase.HBaseTestingUtil; 035import org.apache.hadoop.hbase.HConstants; 036import org.apache.hadoop.hbase.ReadOnlyTransitionException; 037import org.apache.hadoop.hbase.TableName; 038import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 039import org.apache.hadoop.hbase.client.Put; 040import org.apache.hadoop.hbase.client.Table; 041import org.apache.hadoop.hbase.client.TableDescriptor; 042import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 043import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; 044import org.apache.hadoop.hbase.coprocessor.SimpleRegionObserver; 045import org.apache.hadoop.hbase.master.HMaster; 046import org.apache.hadoop.hbase.master.MasterFileSystem; 047import org.apache.hadoop.hbase.regionserver.HRegion; 048import org.apache.hadoop.hbase.regionserver.HRegionServer; 049import org.apache.hadoop.hbase.regionserver.NoOpScanPolicyObserver; 050import org.apache.hadoop.hbase.regionserver.RegionCoprocessorHost; 051import org.apache.hadoop.hbase.testclassification.MediumTests; 052import org.apache.hadoop.hbase.testclassification.SecurityTests; 053import org.apache.hadoop.hbase.util.Bytes; 054import org.apache.hadoop.hbase.util.CoprocessorConfigurationUtil; 055import org.junit.jupiter.api.AfterEach; 056import org.junit.jupiter.api.BeforeEach; 057import org.junit.jupiter.api.Tag; 058import org.junit.jupiter.api.Test; 059import org.slf4j.Logger; 060import org.slf4j.LoggerFactory; 061 062@Tag(SecurityTests.TAG) 063@Tag(MediumTests.TAG) 064public class TestReadOnlyManageActiveClusterFile { 065 066 private static final Logger LOG = 067 LoggerFactory.getLogger(TestReadOnlyManageActiveClusterFile.class); 068 private final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 069 private static final int NUM_RS = 1; 070 private static Configuration conf; 071 HMaster master; 072 HRegionServer regionServer; 073 074 MasterFileSystem mfs; 075 Path rootDir; 076 FileSystem fs; 077 Path activeClusterFile; 078 079 @BeforeEach 080 public void setup() throws Exception { 081 conf = TEST_UTIL.getConfiguration(); 082 083 // Set up test class with Read-Only mode disabled so a table can be created 084 conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); 085 // Start the test cluster 086 TEST_UTIL.startMiniCluster(NUM_RS); 087 master = TEST_UTIL.getMiniHBaseCluster().getMaster(); 088 regionServer = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0); 089 090 mfs = master.getMasterFileSystem(); 091 rootDir = mfs.getRootDir(); 092 fs = mfs.getFileSystem(); 093 activeClusterFile = new Path(rootDir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME); 094 } 095 096 @AfterEach 097 public void tearDown() throws Exception { 098 TEST_UTIL.shutdownMiniCluster(); 099 } 100 101 private void setReadOnlyMode(boolean enabled) { 102 conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, enabled); 103 master.getConfigurationManager().notifyAllObservers(conf); 104 regionServer.getConfigurationManager().notifyAllObservers(conf); 105 } 106 107 private void restartCluster() throws IOException, InterruptedException { 108 TEST_UTIL.getMiniHBaseCluster().shutdown(); 109 TEST_UTIL.restartHBaseCluster(NUM_RS); 110 TEST_UTIL.waitUntilNoRegionsInTransition(); 111 112 master = TEST_UTIL.getMiniHBaseCluster().getMaster(); 113 regionServer = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0); 114 115 MasterFileSystem mfs = master.getMasterFileSystem(); 116 fs = mfs.getFileSystem(); 117 activeClusterFile = new Path(mfs.getRootDir(), HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME); 118 } 119 120 private void overwriteExistingFile() throws IOException { 121 try (FSDataOutputStream out = fs.create(activeClusterFile, true)) { 122 out.writeBytes("newClusterId"); 123 } 124 } 125 126 private boolean activeClusterIdFileExists() throws IOException { 127 return fs.exists(activeClusterFile); 128 } 129 130 @Test 131 public void testActiveClusterIdFileCreationWhenReadOnlyDisabled() 132 throws IOException, InterruptedException { 133 setReadOnlyMode(false); 134 assertTrue(activeClusterIdFileExists()); 135 } 136 137 @Test 138 public void testActiveClusterIdFileDeletionWhenReadOnlyEnabled() 139 throws IOException, InterruptedException { 140 setReadOnlyMode(true); 141 assertFalse(activeClusterIdFileExists()); 142 } 143 144 @Test 145 public void testDeleteActiveClusterIdFileWhenSwitchingToReadOnlyIfOwnedByCluster() 146 throws IOException, InterruptedException { 147 // At the start cluster is in active mode hence set readonly mode and restart 148 conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true); 149 // Restart the cluster to trigger the deletion of the active cluster ID file 150 restartCluster(); 151 // Should delete the active cluster ID file since it is owned by the cluster 152 assertFalse(activeClusterIdFileExists()); 153 } 154 155 @Test 156 public void testDoNotDeleteActiveClusterIdFileWhenSwitchingToReadOnlyIfNotOwnedByCluster() 157 throws IOException, InterruptedException { 158 // Change the content of Active Cluster file to simulate the scenario where the file is not 159 // owned by the cluster and then set readonly mode and restart 160 overwriteExistingFile(); 161 // At the start cluster is in active mode hence set readonly mode and restart 162 conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true); 163 // Restart the cluster to trigger the deletion of the active cluster ID file 164 restartCluster(); 165 // As Active cluster file is not owned by the cluster, it should not be deleted even when 166 // switching to readonly mode 167 assertTrue(activeClusterIdFileExists()); 168 } 169 170 @Test 171 public void testCannotDisableReadOnlyWhenAnotherClusterIsActive() throws Exception { 172 // First enable read-only mode (simulating a replica cluster) 173 setReadOnlyMode(true); 174 assertFalse(activeClusterIdFileExists()); 175 176 // Now write an active cluster file with a DIFFERENT cluster's data (simulating another active 177 // cluster owning the storage) 178 overwriteExistingFile(); 179 assertTrue(activeClusterIdFileExists()); 180 181 // Attempt to disable read-only mode, but get an exception because another active cluster 182 // already exists. 183 master.getConfiguration().setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); 184 185 // Master's updateConfiguration should throw because another cluster is active 186 assertThrows(ReadOnlyTransitionException.class, () -> master.updateConfiguration()); 187 188 // Verify read-only coprocessors are still loaded on the master 189 assertTrue(CoprocessorConfigurationUtil.areReadOnlyCoprocessorsLoaded(master.getConfiguration(), 190 CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY)); 191 192 // Verify read-only coprocessors are still loaded on the region server 193 assertTrue(CoprocessorConfigurationUtil.areReadOnlyCoprocessorsLoaded( 194 regionServer.getConfiguration(), CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY)); 195 } 196 197 @Test 198 public void testCanDisableReadOnlyWhenOwnClusterIsActive() throws Exception { 199 // Enable read-only mode 200 setReadOnlyMode(true); 201 assertFalse(activeClusterIdFileExists()); 202 203 // Verify read-only coprocessors are loaded 204 assertTrue(CoprocessorConfigurationUtil.areReadOnlyCoprocessorsLoaded(master.getConfiguration(), 205 CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY)); 206 207 // Disable read-only mode (our own cluster, no conflicting active cluster file) 208 setReadOnlyMode(false); 209 210 // Active cluster file should be recreated 211 assertTrue(activeClusterIdFileExists()); 212 213 // Verify read-only coprocessors are removed 214 assertFalse(CoprocessorConfigurationUtil.areReadOnlyCoprocessorsLoaded( 215 master.getConfiguration(), CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY)); 216 } 217 218 @Test 219 public void testRegionCoprocessorsStillLoadWhenReadOnlyTransitionBlocked() throws Exception { 220 // Create a table so we have a region to work with 221 TableName tableName = TableName.valueOf("testCoprocessorLoadTable"); 222 TableDescriptor desc = TableDescriptorBuilder.newBuilder(tableName) 223 .setColumnFamily(ColumnFamilyDescriptorBuilder.of("cf")).build(); 224 TEST_UTIL.getAdmin().createTable(desc); 225 List<HRegion> regions = regionServer.getRegions(tableName); 226 assertFalse(regions.isEmpty()); 227 HRegion region = regions.get(0); 228 229 // Enable read-only mode to load ReadOnly coprocessors on the region 230 Configuration readOnlyConf = new Configuration(conf); 231 readOnlyConf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true); 232 region.onConfigurationChange(readOnlyConf); 233 234 // Verify ReadOnly coprocessors are loaded 235 RegionCoprocessorHost regionCPHost = region.getCoprocessorHost(); 236 assertNotNull(regionCPHost.findCoprocessor(RegionReadOnlyController.class.getName()), 237 "RegionReadOnlyController should be loaded after enabling read-only mode"); 238 239 // Simulate another active cluster by writing a foreign cluster ID to the active cluster file 240 overwriteExistingFile(); 241 assertTrue(activeClusterIdFileExists()); 242 243 // Build a config that attempts to disable read-only AND adds new coprocessors 244 Configuration newConf = new Configuration(conf); 245 newConf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); 246 247 // Add a system and user region coprocessors 248 newConf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, SimpleRegionObserver.class.getName()); 249 newConf.set(CoprocessorHost.USER_REGION_COPROCESSOR_CONF_KEY, 250 NoOpScanPolicyObserver.class.getName()); 251 252 // Trigger dynamic configuration change on the region 253 region.onConfigurationChange(newConf); 254 255 // Verify ReadOnly coprocessors are still loaded since the read-only transition was blocked 256 regionCPHost = region.getCoprocessorHost(); 257 assertTrue( 258 CoprocessorConfigurationUtil.areReadOnlyCoprocessorsLoaded(region.getConfiguration(), 259 CoprocessorHost.REGION_COPROCESSOR_CONF_KEY), 260 "ReadOnly coprocessors should remain in the region configuration"); 261 262 // Verify new system and user coprocessors were loaded despite the blocked read-only transition 263 assertNotNull(regionCPHost.findCoprocessor(SimpleRegionObserver.class.getName()), 264 "SimpleRegionObserver should be loaded even when read-only transition is blocked"); 265 assertNotNull(regionCPHost.findCoprocessor(NoOpScanPolicyObserver.class.getName()), 266 "NoOpScanPolicyObserver should be loaded even when read-only transition is blocked"); 267 } 268 269 @Test 270 public void testRegionServerCannotDisableReadOnlyWhenAnotherClusterIsActive() throws Exception { 271 // First enable read-only mode 272 setReadOnlyMode(true); 273 assertFalse(activeClusterIdFileExists()); 274 275 // Write an active cluster file with a different cluster's data 276 overwriteExistingFile(); 277 assertTrue(activeClusterIdFileExists()); 278 279 // Attempt to disable read-only mode, but get an exception because another active cluster 280 // already exists 281 regionServer.getConfiguration().setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); 282 283 // RegionServer's updateConfiguration should throw because another cluster is active 284 assertThrows(ReadOnlyTransitionException.class, () -> regionServer.updateConfiguration()); 285 286 // Verify read-only coprocessors are still loaded on the region server 287 assertTrue(CoprocessorConfigurationUtil.areReadOnlyCoprocessorsLoaded( 288 regionServer.getConfiguration(), CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY)); 289 } 290 291 @Test 292 public void testBlockedThenSuccessfulReadOnlyTransition() throws Exception { 293 // Put cluster in read-only mode with a foreign active cluster file 294 setReadOnlyMode(true); 295 assertFalse(activeClusterIdFileExists()); 296 overwriteExistingFile(); 297 assertTrue(activeClusterIdFileExists()); 298 299 // Attempt to disable read-only mode. This should fail because another cluster is active. 300 // We need to run updateConfiguration() to ensure the readOnlyTransitionBlocked boolean in 301 // HBaseServerBase gets set. 302 master.getConfiguration().setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); 303 assertThrows(ReadOnlyTransitionException.class, () -> master.updateConfiguration()); 304 regionServer.getConfiguration().setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); 305 assertThrows(ReadOnlyTransitionException.class, () -> regionServer.updateConfiguration()); 306 307 // Try to create a table. This should fail because the cluster is still in read-only mode 308 TableName tableName = TableName.valueOf("testBlockedTransitionTable"); 309 TableDescriptor desc = TableDescriptorBuilder.newBuilder(tableName) 310 .setColumnFamily(ColumnFamilyDescriptorBuilder.of("cf")).build(); 311 assertThrows(IOException.class, () -> TEST_UTIL.getAdmin().createTable(desc)); 312 313 // Delete the foreign active cluster file 314 fs.delete(activeClusterFile, false); 315 assertFalse(activeClusterIdFileExists()); 316 317 // Disable read-only mode again. This should succeed now that no foreign active cluster file 318 // exists. We need to run updateConfiguration() to ensure the readOnlyTransitionBlocked boolean 319 // in HBaseServerBase was reset. 320 master.getConfiguration().setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); 321 master.updateConfiguration(); 322 regionServer.getConfiguration().setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); 323 regionServer.updateConfiguration(); 324 325 // Verify the active.cluster.suffix.id file contains this cluster's ID 326 assertTrue(activeClusterIdFileExists()); 327 try (FSDataInputStream in = fs.open(activeClusterFile)) { 328 ActiveClusterSuffix actual = ActiveClusterSuffix.parseFrom(in.readAllBytes()); 329 ActiveClusterSuffix expected = ActiveClusterSuffix.fromConfig(conf, mfs.getClusterId()); 330 assertEquals(expected, actual); 331 } 332 333 // Create a table and add a row to verify read-only mode has been disabled 334 TEST_UTIL.getAdmin().createTable(desc); 335 try (Table table = TEST_UTIL.getConnection().getTable(tableName)) { 336 Put put = new Put(Bytes.toBytes("row1")); 337 put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("q1"), Bytes.toBytes("value1")); 338 table.put(put); 339 } 340 } 341}