/** * Copyright (C) 2008 Progress Software, Inc. All rights reserved. * http://fusesource.com * * The software in this package is published under the terms of the AGPL license * a copy of which has been included with this distribution in the license.txt file. */ package org.fusesource.cloudmix.agent; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.net.Inet4Address; import java.net.InetAddress; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import com.sun.jersey.api.NotFoundException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.fusesource.cloudmix.agent.logging.LogHandler; import org.fusesource.cloudmix.agent.logging.LogParser; import org.fusesource.cloudmix.common.GridClient; import org.fusesource.cloudmix.common.dto.AgentCfgUpdate; import org.fusesource.cloudmix.common.dto.AgentDetails; import org.fusesource.cloudmix.common.dto.ConfigurationUpdate; import org.fusesource.cloudmix.common.dto.ProcessList; import org.fusesource.cloudmix.common.dto.ProvisioningAction; import org.fusesource.cloudmix.common.dto.ProvisioningHistory; import org.fusesource.cloudmix.common.util.FileUtils; import org.fusesource.cloudmix.common.util.ObjectHelper; import org.springframework.beans.factory.InitializingBean; /** * Polls for features that should be installed/uninstalled and executes those * installations. * * @version $Revision: 1.1 $ */ public class InstallerAgent implements Callable<Object>, InitializingBean { public static final String PERSISTABLE_PROPERTY_AGENT_ID = "agent.id"; public static final String PERSISTABLE_PROPERTY_AGENT_NAME = "agent.name"; public static final String PERSISTABLE_PROPERTY_PROFILE_ID = "agent.profile"; public static final String CREATED_KEY = InstallerAgent.class.getName() + ".created"; public static final String STARTED_KEY = InstallerAgent.class.getName() + ".started"; public static final String TIMESTAMP_KEY = InstallerAgent.class.getName() + ".timestamp"; public static final String PROP_ACCESS_LOCK = "agent.profile"; private static final transient Log LOG = LogFactory.getLog(InstallerAgent.class); private static final String AGENT_STATE_FILE = "agent-state.dat"; // Persistent properties. protected AgentState agentState = new AgentState(); protected String propertyFilePath; protected String agentId; protected String agentName; protected String profile = "default"; protected String agentType; protected String[] supportPackageTypes = {}; protected String agentLink; protected AgentDetails agentDetails; protected boolean addedToClient; private Set<String> initialFeatures; private File workDirectory; private GridClient client; private String hostName; private int maxFeatures = 1; private ProvisioningHistory provisioningHistory; private Date lastAppliedHistory; private int lastActionsCount; private ExecutorService executor = Executors.newSingleThreadExecutor(); private BlockingQueue<ProvisioningHistory> historyQueue = new LinkedBlockingQueue<ProvisioningHistory>(); private String baseHref; private LogParser parser; public InstallerAgent() { lastActionsCount = 0; Runnable r = new Runnable() { public void run() { try { while (true) { // TODO: could ignore all but the last entry in the queue. ProvisioningHistory history = historyQueue.take(); onProvisioningHistoryChanged(history); } } catch (InterruptedException e) { // TODO: do something? } } }; executor.execute(r); } @Override public String toString() { try { getAgentId(); } catch (URISyntaxException e) { //ignore } return "InstallerAgent[id: " + agentId + " hostName: " + getHostName() + " profile: " + getProfile() + "]"; } /** * Sets a custom LogParser * * @param parser */ public void setParser(LogParser parser) { this.parser = parser; } /** * Gets the current LogParser * * @return LogParser */ public LogParser getParser() { return parser; } /** * Creates a LogHandler * * @param logPath * points to a log path * @return LogHandler */ public LogHandler getLogHandler(String logPath) { InputStream is = null; try { is = new FileInputStream(logPath); } catch (FileNotFoundException ex) { LOG.warn(logPath + " points to a non-existent log"); throw new RuntimeException(ex); } return getLogHandler(is); } /** * Creates a LogHandler * * @param logStream * represents a log stream * @return LogHandler */ public LogHandler getLogHandler(InputStream logStream) { LogHandler handler = getParser() == null ? new LogHandler(logStream) : new LogHandler(logStream, getParser()); return handler; } /** * Creates a LogHandler * * @param logStream * represents a log file * @return LogHandler */ public LogHandler getLogHandler(File logFile) { LogHandler handler = getParser() == null ? new LogHandler(logFile) : new LogHandler(logFile, getParser()); return handler; } public Object call() throws Exception { String theAgentId = getAgentId(); addToClient(getAgentDetails()); if (theAgentId != null) { if (LOG.isDebugEnabled()) { LOG.debug("Polling agent: " + theAgentId); } try { ProvisioningHistory value = getClient().pollAgentHistory(theAgentId); if (value != null) { asyncOnProvisioningHistoryChanged(value); // This may take some time so its done in a thread. } } catch (NotFoundException e) { System.out.println(e); forceAgentReregistration(); } } return null; } protected void asyncOnProvisioningHistoryChanged(ProvisioningHistory value) { try { historyQueue.put(value); } catch (InterruptedException e) { e.printStackTrace(); } } public void setWorkDirectory(File d) { workDirectory = d; } public File getWorkDirectory() { return workDirectory; } // Properties // ------------------------------------------------------------------------- public AgentDetails getAgentDetails() { if (agentDetails == null) { agentDetails = new AgentDetails(); loadPersistedAgentDetails(); populateInitialAgentDetails(agentDetails); } return agentDetails; } public void setAgentDetails(AgentDetails agentDetails) { this.agentDetails = agentDetails; } public AgentDetails updateAgentDetails() { // TODO why would we flush the agent details each time??? // agentDetails = null; agentDetails = getAgentDetails(); loadPersistedAgentDetails(); populateInitialAgentDetails(agentDetails); try { getClient().updateAgentDetails(getAgentId(), getAgentDetails()); } catch (URISyntaxException e) { LOG.info("Problem updating agent information ", e); e.printStackTrace(); } return agentDetails; } public GridClient getClient() { if (client == null) { client = new RestGridClient(); } return client; } public void setClient(GridClient gridControlerClient) { this.client = gridControlerClient; } public int getMaxFeatures() { return maxFeatures; } public void setMaxFeatures(int max) { maxFeatures = max; } public String getProfile() { return profile; } public void setProfile(String p) { profile = p; if (agentDetails != null) { agentDetails.setProfile(p); } } public String getBaseHref() { return baseHref; } /** * Sets the base link to this agent's web application. * <p/> * If this is a different link to the previously registered one then this * will be updated on the controller */ public void setBaseHref(String href) { this.baseHref = href; final String noHost = "http://0.0.0.0"; if (baseHref.startsWith(noHost)) { String portSuffix = baseHref.substring(noHost.length()); try { baseHref = "http://" + InetAddress.getLocalHost().getCanonicalHostName() + portSuffix; } catch (UnknownHostException ex) { baseHref = "http://localhost" + portSuffix; } } if (baseHref != null) { AgentDetails details = getAgentDetails(); String oldHref = details.getHref(); if (!ObjectHelper.equal(oldHref, baseHref)) { details.setHref(baseHref); LOG.debug("updating agent href to " + baseHref); updateAgentDetails(); LOG.debug("href is now " + getAgentDetails().getHref()); } } } public String getAgentName() { return agentName; } public void setAgentName(String aName) { agentName = aName; if (agentDetails != null) { agentDetails.setName(aName); } } public ProvisioningHistory getProvisioningHistory() { return provisioningHistory; } public String getAgentId() throws URISyntaxException { if (agentId == null) { agentId = addToClient(getAgentDetails()); } return agentId; } protected synchronized String addToClient(AgentDetails details) throws URISyntaxException { if (addedToClient) { return details.getId(); } if (provisioningHistory != null) { ProcessList processList = new ProcessList(); provisioningHistory.populate(processList); details.setProcesses(processList); } String generatedId = getClient().addAgentDetails(details); LOG.info("generated agent.id: " + generatedId); agentId = generatedId; details.setId(generatedId); persistAgentDetails(); addedToClient = true; return generatedId; } protected void forceAgentReregistration() { agentId = null; addedToClient = false; } public String getHostName() { if (hostName == null) { hostName = createHostName(); } return hostName; } public void setHostName(String hn) { hostName = hn; } /** * sets the path to the property file storing the properties of the agent * modifiable remotely * * @param path * to the Java property file */ public void setDetailsPropertyFilePath(String path) { propertyFilePath = path; } /** * gets the path to the property file storing the properties of the agent * modifiable remotely */ public String getDetailsPropertyFilePath() { return propertyFilePath; } // Implementation methods // ------------------------------------------------------------------------- protected void populateInitialAgentDetails(AgentDetails details) { details.setHostname(getHostName()); details.setHref(getBaseHref()); details.setPid(PidUtils.getPid()); details.setMaximumFeatures(getMaxFeatures()); details.setProfile(getProfile()); details.setName(getAgentName()); details.setOs(System.getProperty("os.name")); details.setAgentLink(null); details.setContainerType(null); details.setSupportPackageTypes(new String[] {}); Map<String, String> m = new HashMap<String, String>(System.getProperties().size()); for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) { m.put(entry.getKey().toString(), entry.getValue().toString()); } details.setSystemProperties(m); details.setContainerType(getContainerType()); details.setSupportPackageTypes(getSupportPackageTypes()); details.setAgentLink(getAgentLink()); initialFeatures = getFeatures(); details.setCurrentFeatures(initialFeatures); } public void setAgentLink(String agentLink) { this.agentLink = agentLink; } public String getAgentLink() { return agentLink; } public void setContainerType(String anAgentType) { agentType = anAgentType; } public String getContainerType() { return agentType; } public void setSupportPackageTypes(String[] supportPackageTypes) { this.supportPackageTypes = supportPackageTypes; } public String[] getSupportPackageTypes() { return supportPackageTypes; } protected Set<String> getFeatures() { return agentState.getAgentFeatures().keySet(); } /** * persist the agents details locally so they can be retrieved later after a * shutdown * * @return true if the details were persisted successfully, false otherwise */ public boolean persistAgentDetails() { if (getDetailsPropertyFilePath() == null) { return false; } synchronized (PROP_ACCESS_LOCK) { Properties props = loadProperties(getDetailsPropertyFilePath()); if (props == null) { props = new Properties(); } if (agentName != null) { props.setProperty(PERSISTABLE_PROPERTY_AGENT_NAME, agentName); } if (profile != null) { props.setProperty(PERSISTABLE_PROPERTY_PROFILE_ID, profile); } if (agentId != null && agentId.trim().length() > 0) { LOG.info("persisting agent.id: " + agentId); props.setProperty(PERSISTABLE_PROPERTY_AGENT_ID, agentId); } return persistProperties(props, getDetailsPropertyFilePath()); } } /** * load the persisted agents details from local storage * * @return true if the details were loaded successfully, false otherwise */ public boolean loadPersistedAgentDetails() { if (agentDetails == null) { agentDetails = new AgentDetails(); } Properties props = loadProperties(getDetailsPropertyFilePath()); if (props == null) { return false; } synchronized (PROP_ACCESS_LOCK) { String prop = props.getProperty(PERSISTABLE_PROPERTY_AGENT_ID); LOG.info("retrievd persistent agent.id: " + agentId); agentId = prop != null ? prop : agentId; agentDetails.setId(prop != null ? prop : agentDetails.getId()); prop = readProp(props, PERSISTABLE_PROPERTY_AGENT_NAME); setAgentName(prop != null ? prop : agentName); prop = readProp(props, PERSISTABLE_PROPERTY_PROFILE_ID); setProfile(prop != null ? prop : profile); return true; } } private String readProp(Properties props, String propName) { String value = props.getProperty(propName); return (value == null || value.trim().length() == 0) ? null : value; } protected void onProvisioningHistoryChanged(ProvisioningHistory aProvisioningHistory) { // logProvisioningHistory(aProvisioningHistory); if (LOG.isDebugEnabled()) { LOG.debug("timestamp of previous provisioning history: " + lastAppliedHistory); LOG.debug("timestamp of current provisioning history: " + (aProvisioningHistory != null ? aProvisioningHistory.getLastModified() : "???")); } if (aProvisioningHistory != null && (lastAppliedHistory == null || (lastAppliedHistory.getTime() != aProvisioningHistory.getLastModified().getTime()))) { if (LOG.isDebugEnabled()) { LOG.debug("provisioning instructions changed since last poll: " + aProvisioningHistory); } if (!validateAgent()) { return; } provisioningHistory = aProvisioningHistory; List<AgentCfgUpdate> cfgUpdates = aProvisioningHistory.getCfgUpdates(); if (cfgUpdates != null && cfgUpdates.size() > 0) { boolean cfgUpdated = false; for (AgentCfgUpdate update : cfgUpdates) { if (AgentCfgUpdate.PROPERTY_AGENT_NAME.equals(update.getProperty()) && changed(getAgentDetails().getName(), update.getValue())) { setAgentName(update.getValue()); getAgentDetails().setName(update.getValue()); cfgUpdated = true; } else if (AgentCfgUpdate.PROPERTY_PROFILE_ID.equals(update.getProperty()) && changed(getAgentDetails().getProfile(), update.getValue())) { setProfile(update.getValue()); getAgentDetails().setProfile(update.getValue()); cfgUpdated = true; } else if (AgentCfgUpdate.PROPERTY_AGENT_FORCE_REGISTER.equals(update.getProperty()) && "true".equals(update.getValue())) { forceAgentReregistration(); cfgUpdated = true; } } if (cfgUpdated) { persistAgentDetails(); } } if (lastActionsCount != aProvisioningHistory.getActions().size()) { LOG.debug("provisioning actions changed since last poll (was " + lastActionsCount + " and now " + aProvisioningHistory.getActions().size()); try { Map<String, ProvisioningAction> installActions = new HashMap<String, ProvisioningAction>(); Map<String, ProvisioningAction> uninstallActions = new HashMap<String, ProvisioningAction>(); getEffectiveActions(installActions, uninstallActions); LOG.debug("onProvisioningHistoryChanged - uninstall " + uninstallActions); for (ProvisioningAction action : uninstallActions.values()) { String featureName = action.getFeature(); Feature f = agentState.getAgentFeatures().get(featureName); if (f != null) { uninstallFeature(f); } else { LOG.warn("Cannot find installed feature " + featureName + " to uninstall"); } } LOG.debug("onProvisioningHistoryChanged - install: " + installActions); for (ProvisioningAction action : installActions.values()) { String credentials = null; if (getClient() instanceof RestGridClient) { credentials = ((RestGridClient) getClient()).getCredentials(); } String resource = action.getResource(); if (resource == null) { LOG.debug("Action has no resource! " + action); } else { installFeatures(action, credentials, resource); } } } catch (Exception e) { LOG.error("Error executing provisioning instructions", e); } finally { persistState(); updateAgentDetails(); } } lastAppliedHistory = aProvisioningHistory.getLastModified(); } else { if (LOG.isDebugEnabled()) { LOG.debug("No new instructions... "); } } } protected void installFeatures(ProvisioningAction action, String credentials, String resource) throws Exception { FeatureList features = new FeatureList(resource, credentials); Feature feature = features.getFeature(action.getFeature()); if (feature != null) { // Uninstall old version of feature if it still exists. Feature f = agentState.getAgentFeatures().get(feature.getName()); if (f != null) { uninstallFeature(f); } installFeature(feature, action.getCfgUpdates()); } } protected void installFeature(Feature feature, List<ConfigurationUpdate> featureCfgOverrides) throws Exception { LOG.info("Installing feature " + feature.getName()); installProperties(feature, featureCfgOverrides); Map<String, Object> featureProperties = feature.getAgentProperties(); for (Bundle bundle : feature.getBundles()) { Map<String, Object> bundleProperties = bundle.getAgentProperties(); if (installBundle(feature, bundle)) { LOG.info("Successfully added bundle " + bundle); bundleProperties.put(TIMESTAMP_KEY, new Date()); } } featureProperties.put(TIMESTAMP_KEY, new Date()); addAgentFeature(feature); } // TODO must we use Feature class??? // TODO rename to better name protected void addAgentFeature(Feature feature) { String name = feature.getName(); if (name == null) { throw new NullPointerException("Feature name is null!"); } agentState.getAgentFeatures().put(name, feature); getAgentDetails().getCurrentFeatures().add(name); updateAgentDetails(); } // TODO rename to better name protected void removeFeatureId(String featureId) { agentState.getAgentFeatures().remove(featureId); getAgentDetails().getCurrentFeatures().remove(featureId); updateAgentDetails(); } protected void uninstallFeature(Feature feature) throws Exception { LOG.info("Uninstalling feature " + feature); for (Bundle bundle : feature.getBundles()) { if (uninstallBundle(feature, bundle)) { LOG.info("Successfully removed bundle " + bundle); } } String featureId = feature.getName(); removeFeatureId(featureId); } protected void installProperties(Feature feature, List<ConfigurationUpdate> featureCfgOverrides) { Properties applicationProperties = feature.getProperties("applicationProperties"); Properties systemProperties = System.getProperties(); boolean changed = false; // time to apply the application's configuration overrides if (featureCfgOverrides != null && featureCfgOverrides.size() > 0) { if (applicationProperties == null) { applicationProperties = new Properties(); } for (ConfigurationUpdate cfgUpdate : featureCfgOverrides) { applicationProperties.put(cfgUpdate.getProperty(), cfgUpdate.getValue()); } } if (applicationProperties != null) { systemProperties.putAll(applicationProperties); changed = true; LOG.info(" ===>> adding app props to system"); } if (changed) { System.setProperties(systemProperties); LOG.info(" ===>> applying props updates"); } } public List<Bundle> getFeatureBundles(String featureName) { Feature f = agentState.getAgentFeatures().get(featureName); if (f != null) { return f.getBundles(); } else { return new ArrayList<Bundle>(); } } protected String createHostName() { String hn = "localhost"; try { hn = Inet4Address.getLocalHost().getCanonicalHostName(); LOG.info("determined hostname: " + hn); } catch (UnknownHostException e) { LOG.warn("Could not find out the host name: " + e, e); } return hn; } /** * Get's the list of installed actions * * @param installActions * This map will be filled with the install actions to perform */ public Map<String, ProvisioningAction> getInstalledActions() { Map<String, ProvisioningAction> installActions = new HashMap<String, ProvisioningAction>(); ProvisioningHistory history = getProvisioningHistory(); if (history == null) { return installActions; } List<ProvisioningAction> actions = history.getActions(); if (actions == null) { return installActions; } for (int i = 0; i < actions.size(); i++) { ProvisioningAction action = actions.get(i); if (ProvisioningAction.INSTALL_COMMAND.equals(action.getCommand())) { LOG.debug("added installed action :" + action.getFeature()); installActions.put(action.getFeature(), action); } else if (ProvisioningAction.UNINSTALL_COMMAND.equals(action.getCommand())) { LOG.debug("removed action :" + action.getFeature()); installActions.remove(action.getFeature()); } } return installActions; } /** * Calculates the effective actions to be taken by the agent. This is based * on the history. The history could instruct to install a feature and then * uninstall it later, in that case the net effect is 0. This method walks * the history and computes the effective actions for install and uninstall. * * @param installActions * This map will be filled with the install actions to perform * @param uninstallActions * This map will be fille with the uninstall actions to perform */ public void getEffectiveActions(Map<String, ProvisioningAction> installActions, Map<String, ProvisioningAction> uninstallActions) { ProvisioningHistory history = getProvisioningHistory(); if (history == null) { return; } List<ProvisioningAction> actions = history.getActions(); if (actions == null) { return; } for (int i = 0; i < actions.size(); i++) { ProvisioningAction action = actions.get(i); boolean isNew = i >= lastActionsCount; if (ProvisioningAction.INSTALL_COMMAND.equals(action.getCommand())) { LOG.debug("install action :" + action.getFeature() + " new: " + isNew); //Only add to install actions if this is a new action, otherwise //we'll end up reprovisioning the feature: if (isNew) { installActions.put(action.getFeature(), action); } uninstallActions.remove(action.getFeature()); } else if (ProvisioningAction.UNINSTALL_COMMAND.equals(action.getCommand())) { LOG.debug("uninstall action :" + action.getFeature()); uninstallActions.put(action.getFeature(), action); installActions.remove(action.getFeature()); } } lastActionsCount = history.getActions().size(); } // convenience private synchronized Properties loadProperties(String filePath) { try { if (filePath == null || "".equals(filePath) || !(new File(filePath)).exists()) { return null; } Properties properties = new Properties(); properties.load(new FileInputStream(filePath)); return properties; } catch (Exception e) { LOG.warn("error loading properties file " + filePath + ", exception " + e); return null; } } private synchronized boolean persistProperties(Properties props, String filePath) { try { if (props == null || filePath == null || "".equals(filePath)) { return false; } File propFile = new File(filePath); if (!propFile.exists()) { if (propFile.getParentFile() != null && !propFile.getParentFile().exists()) { FileUtils.createDirectory(propFile.getParentFile()); } propFile.createNewFile(); } props.store(new FileOutputStream(propFile), "agent details as of " + new Date()); return true; } catch (Exception e) { LOG.warn("error storing properties file " + filePath, e); return false; } } protected boolean installBundle(Feature feature, Bundle bundle) { return true; } protected boolean uninstallBundle(Feature feature, Bundle bundle) { return true; } protected boolean validateAgent() { return true; } protected boolean changed(String oldValue, String newValue) { return !((oldValue == null && newValue == null) || (oldValue != null && oldValue.equals(newValue))); } public void afterPropertiesSet() throws Exception { init(); } public void init() throws Exception { File dir = getWorkDirectory(); LOG.info("Starting CloudMix Agent with client: " + getClient() + " profile: " + getProfile() + " workingDir: " + dir); if (dir == null) { LOG.warn("No work directory specified. Not persisting agent state."); return; } else { if (FileUtils.createDirectory(dir) == null) { LOG.error("Cannot create work directory " + dir); throw new RuntimeException("Cannot create work directory " + dir); } loadState(); agentState.getAgentProperties().put(STARTED_KEY, new Date()); // TODO: (CM-2) Clean up previously installed features. This is currently // disabled as there are problems related to the order in which the agent // and its deployed features are started when restarting servicemix. // cleanInstalledFeatures(); } } protected void cleanInstalledFeatures() { Map<String, Feature> features = agentState.getAgentFeatures(); if (features != null) { for (String fn : features.keySet()) { Feature feature = features.get(fn); try { uninstallFeature(feature); } catch (Exception e) { LOG.error("Exception uninstalling feature " + feature, e); } } agentDetails = null; agentDetails = getAgentDetails(); } } protected void persistState() { try { File dir = getWorkDirectory(); if (dir == null || !dir.exists()) { // Persistence is not enabled. return; } File stateFile = new File(dir, AGENT_STATE_FILE); LOG.info("Saving agent state to " + stateFile); OutputStream os = new FileOutputStream(stateFile); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(agentState); // TODO: (CM-4) Use XStream for serializing agent state. Currently disabled // until SMX4 OSGi bundle issues can be resolved. // xstream.toXML(agentState, os); oos.close(); os.close(); } catch (Throwable t) { LOG.error("Error persisting agent state", t); LOG.debug(t); } } protected void loadState() throws Exception { File dir = getWorkDirectory(); if (dir == null || !dir.exists()) { // Persistence is not enabled. return; } File stateFile = new File(dir, AGENT_STATE_FILE); if (!stateFile.exists()) { LOG.info("agent state file " + stateFile + " does not exist"); agentState.getAgentProperties().put(CREATED_KEY, new Date()); persistState(); return; } try { InputStream is = new FileInputStream(stateFile); // TODO: (CM-4) Use XStream for serializing agent state. Currently disabled // until SMX4 OSGi bundle issues can be resolved. // Object o = xstream.fromXML(is); ObjectInputStream ois = new ObjectInputStream(is); Object o = ois.readObject(); agentState = (AgentState) o; is.close(); } catch (Exception e) { LOG.error("Error reading agent state", e); throw e; } } }