blob: 1bcf6062e311a148e4d009808888e6349a909a5e [file] [log] [blame]
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.qpid.server.store.berkeleydb;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import com.sleepycat.bind.tuple.IntegerBinding;
import com.sleepycat.bind.tuple.TupleBase;
import com.sleepycat.bind.tuple.TupleInput;
import com.sleepycat.bind.tuple.TupleOutput;
import com.sleepycat.je.Cursor;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;
import com.sleepycat.je.rep.ReplicatedEnvironment;
import com.sleepycat.je.rep.ReplicationConfig;
/**
* Standalone tool to remove one or more configuration records from a BDB store.
* Intended for exceptional use only.
*
* If targeting a BDB HA store, then it is important to establish which node was
* most recently master and perform the update there.
*/
public class OrphanConfigurationRecordPurger
{
private static final String USAGE_STRING =
"usage: " + (String.format("java %s\n"
+ " -dryRun # Dry run mode\n"
+ " -parentRootCategory <dir> # Parent root category\n"
+ " -storePath <dir> # Store path\n"
+ " [-ha # HA mode\n"
+ " -nodeName <nodename> # HA node name\n"
+ " -nodeHost <nodehost> # HA node host\n"
+ " -groupName <groupName>] # HA group name\n",
OrphanConfigurationRecordPurger.class.getName()));
private static final String VERSION_DB_NAME = "DB_VERSION";
private static final String CONFIGURED_OBJECTS_DB_NAME = "CONFIGURED_OBJECTS";
private static final String CONFIGURED_OBJECT_HIERARCHY_DB_NAME = "CONFIGURED_OBJECT_HIERARCHY";
private static final Set<Integer> ALLOWED_VERSIONS = new HashSet<>(Arrays.asList(8, 9));
private static final DatabaseConfig READ_ONLY_DB_CONFIG = DatabaseConfig.DEFAULT.setAllowCreate(false).setReadOnly(true).setTransactional(true);
private static final DatabaseConfig READ_WRITE_DB_CONFIG = READ_ONLY_DB_CONFIG.setReadOnly(false);
private String _parentRootCategory;
private String _storePath;
private boolean _dryRun;
private boolean _ha;
private String _nodeName;
private String _nodeHost;
private String _groupName;
public static void main(String[] argv) throws Exception
{
final OrphanConfigurationRecordPurger purger = new OrphanConfigurationRecordPurger();
purger.parseArgs(argv);
purger.purge();
}
private void purge() throws Exception
{
EnvironmentConfig config = EnvironmentConfig.DEFAULT;
config.setAllowCreate(false);
config.setTransactional(true);
try (Environment env = createEnvironment(config))
{
final int version = getVersion(env, READ_ONLY_DB_CONFIG);
if (!ALLOWED_VERSIONS.contains(version))
{
throw new IllegalStateException(String.format("Store has unexpected version. Found %d expected %s",
version, ALLOWED_VERSIONS));
}
final Transaction tx = env.beginTransaction(null, TransactionConfig.DEFAULT);
boolean success = false;
try
{
purgeOrphans(env, tx);
success = true;
}
finally
{
if (!success)
{
System.out.println("No config or config hierarchy records purged.");
tx.abort();
}
else if (_dryRun)
{
System.out.println("No config or config hierarchy records purged - -dryRun flag specified.");
tx.abort();
}
else
{
tx.commit();
System.out.format("Config records(s) and associated hierarchy records purged.");
}
}
}
}
private Environment createEnvironment(final EnvironmentConfig config) throws Exception
{
final Environment env;
if (_ha)
{
final ReplicationConfig repConfig = (ReplicationConfig) ReplicationConfig.DEFAULT
.setNodeHostPort(_nodeHost)
.setGroupName(_groupName)
.setNodeName(_nodeName)
.setDesignatedPrimary(true)
.setElectableGroupSizeOverride(1);
env = new ReplicatedEnvironment(new File(_storePath), repConfig, config);
}
else
{
env = new Environment(new File(_storePath), config);
}
return env;
}
private int getVersion(final Environment env, final DatabaseConfig dbConfig)
{
try (Database versionDb = env.openDatabase(null, VERSION_DB_NAME, dbConfig);
Cursor cursor = versionDb.openCursor(null, null))
{
DatabaseEntry key = new DatabaseEntry();
DatabaseEntry value = new DatabaseEntry();
int version = 0;
while (cursor.getNext(key, value, null) == OperationStatus.SUCCESS)
{
int ver = IntegerBinding.entryToInt(key);
if (ver > version)
{
version = ver;
}
}
return version;
}
}
private void purgeOrphans(Environment env, final Transaction tx) throws Exception
{
try(Database configDb = env.openDatabase(tx, CONFIGURED_OBJECTS_DB_NAME, READ_WRITE_DB_CONFIG))
{
final Set<UUID> records = new HashSet<>();
try (Cursor configCursor = configDb.openCursor(tx, null))
{
final DatabaseEntry key = new DatabaseEntry();
final DatabaseEntry value = new DatabaseEntry();
while (configCursor.getNext(key, value, LockMode.DEFAULT) == OperationStatus.SUCCESS)
{
final UUID recId = entryToUuid(new TupleInput(key.getData()));
records.add(recId);
}
}
int configRecordDeleted = 0;
int configHierarchyRecordsDeleted = 0;
try (Database hierarchyDb = env.openDatabase(null, CONFIGURED_OBJECT_HIERARCHY_DB_NAME,
READ_WRITE_DB_CONFIG))
{
boolean loopAgain;
do
{
loopAgain = false;
try (Cursor hierarchyCursor = hierarchyDb.openCursor(tx, null))
{
DatabaseEntry key = new DatabaseEntry();
DatabaseEntry value = new DatabaseEntry();
boolean parentReferencingRecordFound = false;
while (hierarchyCursor.getNext(key, value, LockMode.DEFAULT) == OperationStatus.SUCCESS)
{
final TupleInput keyInput = new TupleInput(key.getData());
final UUID childId = entryToUuid(keyInput);
final String parentType = keyInput.readString();
final UUID parentId = entryToUuid(new TupleInput(value.getData()));
if (_parentRootCategory.equals(parentType))
{
parentReferencingRecordFound = true;
}
else if (!records.contains(parentId))
{
System.out.format("Orphan UUID : %s (has unknown parent with UUID %s of type %s)\n",
childId, parentId, parentType);
hierarchyCursor.delete();
configHierarchyRecordsDeleted++;
loopAgain = true;
DatabaseEntry uuidKey = new DatabaseEntry();
final TupleOutput tupleOutput = uuidToKey(childId);
TupleBase.outputToEntry(tupleOutput, uuidKey);
final OperationStatus delete = configDb.delete(tx, uuidKey);
if (delete == OperationStatus.SUCCESS)
{
records.remove(childId);
configRecordDeleted++;
}
}
}
if (!parentReferencingRecordFound)
{
throw new IllegalStateException(String.format(
"No hierarchy record found with root category type (%s)."
+ " Cannot modify store.", _parentRootCategory));
}
}
}
while(loopAgain);
System.out.format("Identified %d orphaned configured object record(s) "
+ "and %d hierarchy records for purging\n",
configRecordDeleted, configHierarchyRecordsDeleted);
}
}
}
private TupleOutput uuidToKey(final UUID uuid)
{
DatabaseEntry key = new DatabaseEntry();
TupleOutput output = new TupleOutput();
output.writeLong(uuid.getMostSignificantBits());
output.writeLong(uuid.getLeastSignificantBits());
return output;
}
private UUID entryToUuid(final TupleInput input)
{
final long mostSigBits = input.readLong();
final long leastSigBits = input.readLong();
return new UUID(mostSigBits, leastSigBits);
}
private void parseArgs(final String[] argv)
{
final int argCount = argv.length;
if (argCount == 0)
{
printUsage(null);
}
int argc = 0;
while (argc < argCount)
{
String thisArg = argv[argc++];
switch (thisArg)
{
case "-parentRootCategory":
if (argc < argCount)
{
_parentRootCategory = argv[argc++];
}
else
{
printUsage("-parentRootCategory requires an argument");
}
break;
case "-storePath":
if (argc < argCount)
{
_storePath = argv[argc++];
}
else
{
printUsage("-storePath requires an argument");
}
break;
case "-ha":
_ha = true;
break;
case "-nodeName":
if (argc < argCount)
{
_nodeName = argv[argc++];
}
else
{
printUsage("-nodeName requires an argument");
}
break;
case "-nodeHost":
if (argc < argCount)
{
_nodeHost = argv[argc++];
}
else
{
printUsage("-nodeHost requires an argument");
}
break;
case "-groupName":
if (argc < argCount)
{
_groupName = argv[argc++];
}
else
{
printUsage("-groupName requires an argument");
}
break;
case "-dryRun":
_dryRun = true;
break;
default:
printUsage(thisArg + " is not a valid argument");
break;
}
}
if (_storePath == null)
{
printUsage("-storePath is a required argument");
}
if (_parentRootCategory == null)
{
printUsage("-parentRootCategory is a required argument");
}
if (_ha)
{
if (_nodeName == null)
{
printUsage("-nodeName is a required argument when in ha mode");
}
if (_nodeHost == null)
{
printUsage("-nodeHost is a required argument when in ha mode");
}
if (_groupName == null)
{
printUsage("-groupName is a required argument when in ha mode");
}
}
}
private void printUsage(String msg)
{
if (msg != null)
{
System.err.println(msg);
}
System.err.println(USAGE_STRING);
System.exit(-1);
}
}