/*
* NOTE: This copyright does *not* cover user programs that use HQ
* program services by normal system calls through the application
* program interfaces provided as part of the Hyperic Plug-in Development
* Kit or the Hyperic Client Development Kit - this is merely considered
* normal use of the program, and does *not* fall under the heading of
* "derived work".
*
* Copyright (C) [2004, 2005, 2006], Hyperic, Inc.
* This file is part of HQ.
*
* HQ is free software; you can redistribute it and/or modify
* it under the terms version 2 of the GNU General Public License as
* published by the Free Software Foundation. This program is distributed
* in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA.
*/
package org.hyperic.hq.agent.server;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.UTFDataFormatException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hyperic.hq.agent.AgentKeystoreConfig;
import org.hyperic.hq.agent.db.DiskList;
import org.hyperic.hq.agent.stats.AgentStatsCollector;
import org.hyperic.hq.common.SystemException;
import org.hyperic.util.file.FileUtil;
import org.hyperic.util.security.KeystoreConfig;
import org.hyperic.util.security.KeystoreManager;
import org.hyperic.util.security.SecurityUtil;
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
public class AgentDListProvider implements AgentStorageProvider {
private static final Log log = LogFactory.getLog(AgentDListProvider.class);
private static final int RECSIZE = 4000;
private static final int OLD_RECSIZE = 1024;
private static final long MAXSIZE = 50 * 1024 * 1024; // 50MB
private static final long CHKSIZE = 10 * 1024 * 1024; // 10MB
private static final int CHKPERC = 50; // Only allow < 50% free
private final AgentStatsCollector agentStatsCollector = AgentStatsCollector.getInstance();
private final AtomicBoolean shutdown = new AtomicBoolean(false);
private HashMap<EncVal, EncVal> keyVals;
private HashMap<String, DiskList> lists;
private HashMap<String, ListInfo> overloads;
private File writeDir;
private File keyValFile;
private File keyValFileBackup;
// Dirty flag for when writing to keyvals. Set to true at startup
// to force an initial flush.
private final AtomicBoolean keyValDirty = new AtomicBoolean(true); // Dirty flag for when writing to keyvals
private long maxSize = MAXSIZE;
private long chkSize = CHKSIZE;
private int chkPerc = CHKPERC;
private final PooledPBEStringEncryptor encryptor;
public AgentDListProvider() {
keyVals = null;
lists = null;
try {
encryptor = createEncryptor();
} catch (Exception e) {
throw new SystemException(e);
}
}
protected PooledPBEStringEncryptor createEncryptor() throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableEntryException, IOException {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
encryptor.setPoolSize(1);
encryptor.setAlgorithm(SecurityUtil.DEFAULT_ENCRYPTION_ALGORITHM);
encryptor.setPassword(getKeyvalsPass());
return encryptor;
}
/**
* Get a description of this storage provider.
*
* @return A string describing the functionality of the object.
*/
public String getDescription(){
return "Agent D-list provider. Data is written to data/idx files for lists, and a single file for key/values";
}
private DiskList intrCreateList(String name, int recSize) throws IOException {
long _maxSize = maxSize;
long _chkSize = chkSize;
int _chkPerc = chkPerc;
ListInfo info = overloads.get(name);
if (info != null) {
_maxSize = info.maxSize;
_chkSize = info.chkSize;
_chkPerc = info.chkPerc;
}
return new DiskList(new File(this.writeDir, name), recSize, _chkSize, _chkPerc, _maxSize);
}
/**
* Create a list of non-standard record size.
*/
public void createList(String name, int recSize) throws AgentStorageException {
try {
DiskList dList = intrCreateList(name, recSize);
lists.put(name, dList);
} catch (IOException e) {
AgentStorageException toThrow = new AgentStorageException("Unable to create DiskList: " + e);
toThrow.initCause(e);
throw toThrow;
}
}
private ListInfo parseInfo(String info) throws AgentStorageException {
StringTokenizer st = new StringTokenizer(info, ":");
if (st.countTokens() != 4) {
throw new AgentStorageException(info + " is an invalid agent disklist configuration");
}
String s = st.nextToken().trim();
long factor;
if ("m".equalsIgnoreCase(s)) {
factor = 1024 * 1024;
} else if ("k".equalsIgnoreCase(s)) {
factor = 1024;
} else {
throw new AgentStorageException(info + " is an invalid agent disklist configuration");
}
ListInfo listInfo = new ListInfo();
try {
listInfo.maxSize = Long.parseLong(st.nextToken().trim()) * factor;
listInfo.chkSize = Long.parseLong(st.nextToken().trim()) * factor;
listInfo.chkPerc = Integer.parseInt(st.nextToken().trim());
} catch (NumberFormatException e) {
throw new AgentStorageException("Invalid agent disklist " +
"configuration: " + e);
}
return listInfo;
}
public void addOverloadedInfo(String listName, String info) {
try {
overloads.put(listName, parseInfo(info));
} catch (AgentStorageException ex) {
//use default values
log.error(ex);
}
}
/**
* Sets a value within the storage object.
*
* @param key Key of the value to set.
* @param value Value to set for 'key'.
*/
public void setValue(String key, String value) {
final boolean debug = log.isDebugEnabled();
boolean mapChanged = false;
if (value == null) {
if (debug) {
log.debug("Removing '" + key + "' from storage");
}
synchronized (keyVals) {
EncVal encryptableKey = new EncVal(encryptor, key);
EncVal removed = keyVals.remove(encryptableKey);
if (removed != null) {
mapChanged = true;
}
}
} else {
if (debug) {
log.debug("Setting '" + key + "' to '" + value + "'");
}
synchronized(keyVals){
EncVal encryptableKey = new EncVal(encryptor, key);
EncVal encryptableValue = new EncVal(encryptor, value);
if (isNewEntry(encryptableKey, encryptableValue)) {
keyVals.put(encryptableKey, encryptableValue);
mapChanged = true;
}
}
}
// After call to setValue() set dirty flag for flush to storage
if (mapChanged) {
keyValDirty.set(true);
}
}
private boolean isNewEntry(EncVal key, EncVal value) {
return !value.equals(keyVals.get(key));
}
/**
* Gets a value from the storage object.
*
* @param key Key of the value to get.
*
* @return The value associated with the key for the subsystem.
*/
public String getValue(String key) {
String res = null;
synchronized(keyVals){
EncVal encVal = keyVals.get(new EncVal(encryptor, key));
res = encVal == null ? null : encVal.getVal();
}
if(log.isDebugEnabled()){
log.debug("Got " + key + "='" + res + "'");
}
return res;
}
public Set<String> getKeys(){
//copy keys to avoid possible ConcurrentModificationException
Set<String> set = new HashSet<String>();
synchronized(keyVals){
for (EncVal v : keyVals.keySet()) {
set.add(v.getVal());
}
}
return set;
}
protected String getKeyvalsPass() throws KeyStoreException, IOException, NoSuchAlgorithmException, UnrecoverableEntryException {
KeystoreConfig keystoreConfig = new AgentKeystoreConfig();
KeyStore keystore = KeystoreManager.getKeystoreManager().getKeyStore(keystoreConfig);
KeyStore.Entry e = keystore.getEntry(keystoreConfig.getAlias(), new KeyStore.PasswordProtection(keystoreConfig.getFilePassword().toCharArray()));
if (e == null) {
throw new UnrecoverableEntryException("Encryptor password generation failure: No such alias") ;
}
// XXX scottmf - I'm a bit concerned about this. I tested the upgrade path on the agent on the new code with the
// ByteBuffer and it doesn't work, the agent throws a org.jasypt.exceptions.EncryptionOperationNotPossibleException.
// When I put back the old code with the replaceAll() everything works.
//final String p = ((PrivateKeyEntry)e).getPrivateKey().toString();
//return p.replaceAll("[^a-zA-Z0-9]", "_");
byte[] pk = ((PrivateKeyEntry)e).getPrivateKey().getEncoded();
ByteBuffer encryptionKey = Charset.forName("US-ASCII").encode(ByteBuffer.wrap(pk).toString());
return encryptionKey.toString();
}
public synchronized void flush() throws AgentStorageException {
flush(false);
}
private synchronized void flush(boolean toShutdown) throws AgentStorageException {
if (shutdown.get() && !toShutdown) {
return;
}
final long start = System.currentTimeMillis();
BufferedOutputStream bOs = null;
FileOutputStream fOs = null;
DataOutputStream dOs = null;
if (!keyValDirty.get()) {
return;
}
Entry<EncVal, EncVal> curr = null;
try {
fOs = new FileOutputStream(keyValFile);
bOs = new BufferedOutputStream(fOs);
dOs = new DataOutputStream(bOs);
synchronized(keyVals){
dOs.writeLong(keyVals.size());
for (Entry<EncVal, EncVal> entry : keyVals.entrySet()) {
curr = entry;
String encKey = entry.getKey().getEnc();
String encVal = entry.getValue().getEnc();
dOs.writeUTF(encKey);
dOs.writeUTF(encVal);
}
}
} catch(UTFDataFormatException e) {
if (curr != null) {
log.error("error writing key=" + curr.getKey().getVal() + ", value=" + curr.getValue().getVal(), e);
} else {
log.error(e,e);
}
} catch(IOException e) {
log.error("Error flushing data", e);
AgentStorageException toThrow = new AgentStorageException("Error flushing data: " + e);
toThrow.initCause(e);
throw toThrow;
} finally {
close(dOs);
close(bOs);
// After successful write, clear dirty flag.
keyValDirty.set(false);
close(fOs);
}
// After successful flush, update backup copy
try {
synchronized(keyVals){
FileUtil.copyFile(this.keyValFile, this.keyValFileBackup);
}
} catch (FileNotFoundException e) {
log.warn(e);
log.debug(e,e);
} catch (IOException e) {
log.error("Error backing up keyvals", e);
AgentStorageException toThrow = new AgentStorageException("Error backing up keyvals: " + e);
toThrow.initCause(e);
throw toThrow;
}
agentStatsCollector.addStat(System.currentTimeMillis() - start, AgentStatsCollector.DISK_LIST_KEYVALS_FLUSH_TIME);
}
private void close(OutputStream os) {
try {
if (os != null) {
os.flush();
os.close();
}
} catch (IOException e) {
log.error(e,e);
}
}
/**
* DList info string is a series of properties seperated by '|'
* Three properties are expected.
*
* Directory to place the data files
* Size in MB to start checking for unused blocks
* Maximum percentage of free blocks allowed
*
* Default is 'data|20|50'
*/
public void init(String info) throws AgentStorageException {
BufferedInputStream bIs;
FileInputStream fIs = null;
DataInputStream dIs;
long nEnts;
// Parse out configuration
StringTokenizer st = new StringTokenizer(info, "|");
if (st.countTokens() != 5) {
throw new AgentStorageException(info + " is an invalid agent storage provider configuration");
}
keyVals = new HashMap<EncVal, EncVal>();
lists = new HashMap<String, DiskList>();
overloads = new HashMap<String, ListInfo>();
String dir = st.nextToken();
this.writeDir = new File(dir);
this.keyValFile = new File(writeDir, "keyvals");
this.keyValFileBackup = new File(writeDir, "keyvals.backup");
String s = st.nextToken().trim();
long factor;
if ("m".equalsIgnoreCase(s)) {
factor = 1024 * 1024;
} else if ("k".equalsIgnoreCase(s)) {
factor = 1024;
} else {
throw new AgentStorageException(info + " is an invalid agent storage provider configuration");
}
try {
maxSize = Long.parseLong(st.nextToken().trim()) * factor;
chkSize = Long.parseLong(st.nextToken().trim()) * factor;
chkPerc = Integer.parseInt(st.nextToken().trim());
} catch (NumberFormatException e) {
throw new AgentStorageException("Invalid agent storage provider " +
"configuration: " + e);
}
if(this.writeDir.exists() == false){
// Try to create it
this.writeDir.mkdir();
}
if(this.writeDir.isDirectory() == false){
throw new AgentStorageException(dir + " is not a directory");
}
try {
fIs = new FileInputStream(this.keyValFile);
bIs = new BufferedInputStream(fIs);
dIs = new DataInputStream(bIs);
nEnts = dIs.readLong();
while(nEnts-- != 0){
String encKey = dIs.readUTF();
String encVal = dIs.readUTF();
String key = SecurityUtil.isMarkedEncrypted(encKey) ? SecurityUtil.decryptRecursiveUnmark(encryptor, encKey) : encKey;
String val = SecurityUtil.isMarkedEncrypted(encVal) ? SecurityUtil.decryptRecursiveUnmark(encryptor, encVal) : encVal;
this.keyVals.put(new EncVal(encryptor, key, encKey), new EncVal(encryptor, val, encVal));
}
} catch(FileNotFoundException exc) {
// Normal when it doesn't exist
log.debug("file not found (this is ok): " + exc);
} catch(IOException exc){
log.error("Error reading " + this.keyValFile + " loading " + "last known good version");
// Close old stream
close(fIs);
// Fall back to last known good keyvals file
try {
fIs = new FileInputStream(this.keyValFileBackup);
bIs = new BufferedInputStream(fIs);
dIs = new DataInputStream(bIs);
nEnts = dIs.readLong();
while (nEnts-- != 0) {
String encKey = dIs.readUTF();
String encVal = dIs.readUTF();
String key = SecurityUtil.encrypt(this.encryptor, encKey);
String val = SecurityUtil.encrypt(this.encryptor, encVal);
this.keyVals.put(new EncVal(encryptor, key, encKey), new EncVal(encryptor, val, encVal));
}
} catch (FileNotFoundException e) {
log.warn(e);
log.debug(e,e);
} catch (IOException e) {
AgentStorageException toThrow = new AgentStorageException("Error reading " + this.keyValFile + ": " + e);
toThrow.initCause(e);
throw toThrow;
}
} finally {
close(fIs);
}
}
public void addObjectToFolder(String folderName, Object obj, long createTime, int maxElementsInFolder) {
File folder = new File(this.writeDir + System.getProperty("file.separator") + folderName);
if (!folder.exists()) {
folder.mkdir();
}
File[] files = folder.listFiles();
int numberOfElementsInFolder = files.length;
if (numberOfElementsInFolder >= maxElementsInFolder) {
// sort the files by create time
Arrays.sort(files, new Comparator<File>() {
public int compare(File f1, File f2) {
return (Long.valueOf(f1.lastModified()).compareTo(f2.lastModified()));
}
});
int i = 0;
while (numberOfElementsInFolder >= maxElementsInFolder) {
files[i].delete();
numberOfElementsInFolder--;
i++;
}
}
ObjectOutputStream outputStream = null;
try {
outputStream = new ObjectOutputStream(new FileOutputStream(folder.getAbsolutePath()
+ System.getProperty("file.separator") + createTime));
outputStream.writeObject(obj);
} catch (Exception ex) {
} finally {
try {
if (outputStream != null) {
outputStream.flush();
outputStream.close();
}
} catch (IOException ex) {
}
}
}
public void deleteObjectsFromFolder(String folderName, String... objects) {
String folder = this.writeDir + System.getProperty("file.separator") + folderName;
for (String object : objects) {
File file = new File(folder + System.getProperty("file.separator") + object);
if (!file.exists()) {
log.warn("Cannot find file '" + object + "' to delete");
continue;
}
if (!file.delete()) {
log.warn("Cannot delete '" + object);
}
}
}
@SuppressWarnings("unchecked")
public <T> List<T> getObjectsFromFolder(String folderName, int maxNumOfObjects) {
List<T> objects = new ArrayList<T>();
File folder = new File(this.writeDir + System.getProperty("file.separator") + folderName);
if (!folder.exists()) {
return objects;
}
File[] files = folder.listFiles();
// sort the files by create time
Arrays.sort(files, new Comparator<File>() {
public int compare(File f1, File f2) {
return -(Long.valueOf(f1.lastModified()).compareTo(f2.lastModified()));
}
});
for (final File fileEntry : files) {
if (maxNumOfObjects <= 0) {
break;
}
ObjectInputStream inputStream = null;
try {
inputStream = new ObjectInputStream(new FileInputStream(fileEntry));
objects.add((T) inputStream.readObject());
maxNumOfObjects--;
} catch (Exception ex) {
log.error("Cannot read objects from '" + folderName + "'" + ex.getMessage());
} finally {
try {
inputStream.close();
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
return objects;
}
public void deleteObject(String objectName) {
File file = new File(this.writeDir + System.getProperty("file.separator") + objectName);
if (file.exists()) {
if (!file.delete()) {
log.warn("Cannot delete '" + objectName + "'");
}
} else {
log.warn("File does not exists '" + objectName + "'");
}
}
public void saveObject(Object obj, String objectName) {
File file = new File(this.writeDir + System.getProperty("file.separator") + objectName);
ObjectOutputStream outputStream = null;
try {
outputStream = new ObjectOutputStream(new FileOutputStream(file));
outputStream.writeObject(obj);
} catch (Exception ex) {
log.error("Cannot save object '" + objectName + "'", ex);
} finally {
try {
if (outputStream != null) {
outputStream.flush();
outputStream.close();
}
} catch (IOException ex) {
log.error(ex);
}
}
}
@SuppressWarnings("unchecked")
public <T> T getObject(String objectName) {
File file = new File(this.writeDir + System.getProperty("file.separator") + objectName);
if (!file.exists()) {
log.info("Did not find object '" + objectName + "' in the local storage");
return null;
}
ObjectInputStream inputStream = null;
try {
inputStream = new ObjectInputStream(new FileInputStream(file));
Object result = (inputStream.readObject());
return (T) result;
} catch (Exception ex) {
log.error("Cannot read object '" + objectName + "'" + ex.getMessage());
} finally {
try {
inputStream.close();
} catch (IOException e) {
log.error(e.getMessage());
}
}
return null;
}
private void close(FileInputStream fIs) {
try {
if(fIs != null) {
fIs.close();
}
} catch(IOException e){
log.debug(e,e);
}
}
public void dispose(){
if (shutdown.get()) {
return;
}
try {
shutdown.set(true);
flush(true);
} catch(Exception exc){
log.error("Error flushing key/vals storage", exc);
}
for (final Entry<String, DiskList> entry : lists.entrySet()) {
try {
DiskList dl = entry.getValue();
dl.close();
} catch(Exception exc){
log.error("Unable to dispose of disk list '" + entry.getKey() + "'", exc);
}
}
}
/*** LIST FUNCTIONALITY ***/
public void addToList(String listName, String value) throws AgentStorageException {
if (shutdown.get()) {
return;
}
DiskList dList = getDiskList(listName);
if (null == dList) {
log.error("Error adding data , cannot read list '" + listName + "' from storage");
return;
}
try {
if (log.isDebugEnabled()) {
log.debug("adding value to list=" + listName + ", value=" + value);
}
dList.addToList(value);
} catch(IOException exc){
log.error("Error adding to list '" + listName + "'", exc);
AgentStorageException toThrow = new AgentStorageException("Error adding data to list: " + exc);
toThrow.initCause(exc);
throw toThrow;
}
}
public void removeFromList(String listName, long recNumber) throws AgentStorageException {
if (shutdown.get()) {
return;
}
DiskList dList = getDiskList(listName);
if (null == dList) {
log.error("Error removing data , cannot read list '" + listName + "' " +
"from storage");
return;
}
try {
dList.removeRecord(recNumber);
} catch(IOException exc){
log.error("Error deleting from list '" + listName + "'", exc);
AgentStorageException t = new AgentStorageException("Error deleting data from list: " + exc);
t.initCause(exc);
throw t;
}
}
public void deleteList(String listName) {
if (shutdown.get()) {
return;
}
DiskList dList = getDiskList(listName);
if (null == dList) {
return ;
}
try {
dList.deleteAllRecords();
} catch(IOException exc){
log.error("Error deleting all records", exc);
}
}
public Iterator<String> getListIterator(String listName) {
DiskList dList = getDiskList(listName);
if (null == dList) {
return null;
}
return dList.getListIterator();
}
public void convertListToCurrentRecordSize(String listName) throws IOException{
DiskList dList = getDiskList(listName);
if (null == dList) {
return ;
}
dList.convertListToCurrentRecordSize(OLD_RECSIZE);
}
private DiskList getDiskList(String listName) {
DiskList dList;
synchronized(this.lists){
dList = this.lists.get(listName);
if(dList == null){
try {
dList = intrCreateList(listName, RECSIZE);
} catch(IOException exc){
log.error("Error loading disk list", exc);
return null; // XXX
}
this.lists.put(listName, dList);
}
}
return dList;
}
private static class ListInfo {
long maxSize;
long chkSize;
int chkPerc;
}
private class EncVal {
private final String val;
private String encrypted = null;
private final PooledPBEStringEncryptor encryptor;
private EncVal(PooledPBEStringEncryptor encryptor, String val, String encrypted) {
this.val = val;
this.encrypted = SecurityUtil.isMarkedEncrypted(encrypted) ? encrypted : null;
this.encryptor = encryptor;
}
private EncVal(PooledPBEStringEncryptor encryptor, String val) {
this.val = val;
this.encryptor = encryptor;
}
private String getVal() {
return val;
}
private String getEnc() {
if (encrypted == null) {
encrypted = SecurityUtil.encrypt(encryptor, val);
}
return encrypted;
}
@Override
public String toString() {
return val;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof EncVal) {
EncVal v = (EncVal) o;
return val.equals(v.val);
}
return false;
}
@Override
public int hashCode() {
return val.hashCode();
}
}
}