/* * 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(); } } }