/******************************************************************************* * Copyright (c) 2011, 2016 Eurotech and others * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Eurotech * Red Hat Inc - Clean up kura properties handling *******************************************************************************/ package org.eclipse.kura.deployment.agent.impl; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.apache.commons.io.FileUtils; import org.eclipse.kura.deployment.agent.DeploymentAgentService; import org.eclipse.kura.system.SystemService; import org.osgi.framework.Version; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.ComponentException; import org.osgi.service.deploymentadmin.DeploymentAdmin; import org.osgi.service.deploymentadmin.DeploymentException; import org.osgi.service.deploymentadmin.DeploymentPackage; import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; import org.osgi.service.event.EventProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author cdealti * * The bundles installed from deployment packages are managed by the deployment admin itself. * Once installed they are persisted in the persistent storage area provided by the framework. * The persistent storage area is wiped up if the framework is stated with the '-clean' option. * The way deployment packages and their bundles are stored in the persistence storage area * is implementation dependent and we should not rely on that. * * In order to be able to reinstall deployment packages across reboots of the framework with * the '-clean' option set, we need to store the deployment package files (.dp) in a different * persistent location. * * Limitations: * We should also keep the entire installation history. This is needed because deployment * packages can be partially upgraded through 'fix packages' and these must be reinstalled in the * right order. * We DO NOT support this yet. We assume that for every installed deployment package * there is a single deployment package file (.dp) that needs to be reinstalled. */ public class DeploymentAgent implements DeploymentAgentService { private static Logger s_logger = LoggerFactory.getLogger(DeploymentAgent.class); private static final String DPA_CONF_PATH_PROPNAME = "dpa.configuration"; private static final String PACKAGES_PATH_PROPNAME = "kura.packages"; private static final String CONN_TIMEOUT_PROPNAME = "dpa.connection.timeout"; private static final String READ_TIMEOUT_PROPNAME = "dpa.read.timeout"; private final static long THREAD_TERMINATION_TOUT = 1; // in seconds private static Future<?> s_installerTask; private static Future<?> s_uninstallerTask; private DeploymentAdmin m_deploymentAdmin; private EventAdmin m_eventAdmin; private SystemService m_systemService; private Queue<String> m_instPackageUrls; private Queue<String> m_uninstPackageNames; private ExecutorService m_installerExecutor; private ExecutorService m_uninstallerExecutor; private String m_dpaConfPath; private String m_packagesPath; private Properties m_deployedPackages; private int m_connTimeout; private int m_readTimeout; protected void activate(ComponentContext componentContext) { this.m_deployedPackages = new Properties(); this.m_dpaConfPath = System.getProperty(DPA_CONF_PATH_PROPNAME); if (this.m_dpaConfPath == null || this.m_dpaConfPath.isEmpty()) { throw new ComponentException("The value of '" + DPA_CONF_PATH_PROPNAME + "' is not defined"); } final Properties kuraProperties = this.m_systemService.getProperties(); this.m_packagesPath = kuraProperties.getProperty(PACKAGES_PATH_PROPNAME); if (this.m_packagesPath == null || this.m_packagesPath.isEmpty()) { throw new ComponentException("The value of '" + PACKAGES_PATH_PROPNAME + "' is not defined"); } if (kuraProperties.getProperty(PACKAGES_PATH_PROPNAME) != null && kuraProperties.getProperty(PACKAGES_PATH_PROPNAME).trim().equals("kura/packages")) { kuraProperties.setProperty(PACKAGES_PATH_PROPNAME, "/opt/eclipse/kura/kura/packages"); this.m_packagesPath = kuraProperties.getProperty(PACKAGES_PATH_PROPNAME); s_logger.warn("Overridding invalid kura.packages location"); } String sConnTimeout = kuraProperties.getProperty(CONN_TIMEOUT_PROPNAME); if (sConnTimeout != null) { this.m_connTimeout = Integer.valueOf(sConnTimeout); } String sReadTimeout = kuraProperties.getProperty(READ_TIMEOUT_PROPNAME); if (sReadTimeout != null) { this.m_readTimeout = Integer.valueOf(sReadTimeout); } File dpaConfFile = new File(this.m_dpaConfPath); if (dpaConfFile.getParentFile() != null && !dpaConfFile.getParentFile().exists()) { dpaConfFile.getParentFile().mkdirs(); } if (!dpaConfFile.exists()) { try { dpaConfFile.createNewFile(); } catch (IOException e) { throw new ComponentException("Cannot create empty DPA configuration file", e); } } File packagesDir = new File(this.m_packagesPath); if (!packagesDir.exists()) { if (!packagesDir.mkdirs()) { throw new ComponentException("Cannot create packages directory"); } } this.m_instPackageUrls = new ConcurrentLinkedQueue<String>(); this.m_uninstPackageNames = new ConcurrentLinkedQueue<String>(); this.m_installerExecutor = Executors.newSingleThreadExecutor(); this.m_uninstallerExecutor = Executors.newSingleThreadExecutor(); s_installerTask = this.m_installerExecutor.submit(new Runnable() { @Override public void run() { Thread.currentThread().setName("DeploymentAgent"); installer(); } }); s_uninstallerTask = this.m_uninstallerExecutor.submit(new Runnable() { @Override public void run() { Thread.currentThread().setName("DeploymentAgent:Uninstall"); uninstaller(); } }); installPackagesFromConfFile(); } protected void deactivate(ComponentContext componentContext) { if (s_installerTask != null && !s_installerTask.isDone()) { s_logger.debug("Cancelling DeploymentAgent task ..."); s_installerTask.cancel(true); s_logger.info("DeploymentAgent task cancelled? = {}", s_installerTask.isDone()); s_installerTask = null; } if (this.m_installerExecutor != null) { s_logger.debug("Terminating DeploymentAgent Thread ..."); this.m_installerExecutor.shutdownNow(); try { this.m_installerExecutor.awaitTermination(THREAD_TERMINATION_TOUT, TimeUnit.SECONDS); } catch (InterruptedException e) { s_logger.warn("Interrupted", e); } s_logger.info("DeploymentAgent Thread terminated? - {}", this.m_installerExecutor.isTerminated()); this.m_installerExecutor = null; } if (s_uninstallerTask != null && !s_uninstallerTask.isDone()) { s_logger.debug("Cancelling DeploymentAgent:Uninstall task ..."); s_uninstallerTask.cancel(true); s_logger.info("DeploymentAgent:Uninstall task cancelled? = {}", s_uninstallerTask.isDone()); s_uninstallerTask = null; } if (this.m_uninstallerExecutor != null) { s_logger.debug("Terminating DeploymentAgent:Uninstall Thread ..."); this.m_uninstallerExecutor.shutdownNow(); try { this.m_uninstallerExecutor.awaitTermination(THREAD_TERMINATION_TOUT, TimeUnit.SECONDS); } catch (InterruptedException e) { s_logger.warn("Interrupted", e); } s_logger.info("DeploymentAgent:Uninstall Thread terminated? - {}", this.m_uninstallerExecutor.isTerminated()); this.m_uninstallerExecutor = null; } this.m_dpaConfPath = null; this.m_deployedPackages = null; this.m_uninstPackageNames = null; this.m_instPackageUrls = null; } public void setDeploymentAdmin(DeploymentAdmin deploymentAdmin) { this.m_deploymentAdmin = deploymentAdmin; } public void unsetDeploymentAdmin(DeploymentAdmin deploymentAdmin) { this.m_deploymentAdmin = null; } protected void setEventAdmin(EventAdmin eventAdmin) { this.m_eventAdmin = eventAdmin; } protected void unsetEventAdmin(EventAdmin eventAdmin) { this.m_eventAdmin = null; } public void setSystemService(SystemService systemService) { this.m_systemService = systemService; } public void unsetSystemService(SystemService systemService) { this.m_systemService = null; } @Override public void installDeploymentPackageAsync(String url) throws Exception { if (this.m_instPackageUrls.contains(url)) { throw new Exception("Element already exists"); } this.m_instPackageUrls.offer(url); synchronized (this.m_instPackageUrls) { this.m_instPackageUrls.notifyAll(); } } @Override public void uninstallDeploymentPackageAsync(String name) throws Exception { if (this.m_uninstPackageNames.contains(name)) { throw new Exception("Element already exists"); } this.m_uninstPackageNames.offer(name); synchronized (this.m_uninstPackageNames) { this.m_uninstPackageNames.notifyAll(); } } @Override public boolean isInstallingDeploymentPackage(String url) { if (this.m_instPackageUrls.contains(url)) { return true; } return false; } @Override public boolean isUninstallingDeploymentPackage(String name) { if (this.m_uninstPackageNames.contains(name)) { return true; } return false; } private void installer() { do { try { try { while (this.m_instPackageUrls.isEmpty()) { synchronized (this.m_instPackageUrls) { this.m_instPackageUrls.wait(); } } String url = this.m_instPackageUrls.peek(); if (url != null) { s_logger.info("About to install package at URL {}", url); DeploymentPackage dp = null; Exception ex = null; try { dp = installDeploymentPackageInternal(url); } catch (Exception e) { ex = e; s_logger.error("Exception installing package at URL {}", url, e); } finally { boolean successful = dp != null ? true : false; s_logger.info("Posting INSTALLED event for package at URL {}: {}", url, successful ? "successful" : "unsuccessful"); this.m_instPackageUrls.poll(); postInstalledEvent(dp, url, successful, ex); } } } catch (InterruptedException e) { s_logger.info("Exiting..."); Thread.interrupted(); return; } } catch (Throwable t) { s_logger.error("Unexpected throwable", t); } } while (true); } private void uninstaller() { do { try { try { while (this.m_uninstPackageNames.isEmpty()) { synchronized (this.m_uninstPackageNames) { this.m_uninstPackageNames.wait(); } } String name = this.m_uninstPackageNames.peek(); if (name != null) { s_logger.info("About to uninstall package ", name); DeploymentPackage dp = null; boolean successful = false; Exception ex = null; try { dp = this.m_deploymentAdmin.getDeploymentPackage(name); if (dp != null) { dp.uninstall(); String sUrl = this.m_deployedPackages.getProperty(name); File dpFile = new File(new URL(sUrl).getPath()); if (!dpFile.delete()) { s_logger.warn("Cannot delete file at URL: {}", sUrl); } successful = true; removePackageFromConfFile(name); } } catch (Exception e) { ex = e; s_logger.error("Exception uninstalling package {}", name, e); } finally { s_logger.info("Posting UNINSTALLED event for package {}: {}", name, successful ? "successful" : "unsuccessful"); this.m_uninstPackageNames.poll(); postUninstalledEvent(name, successful, ex); } } } catch (InterruptedException e) { s_logger.info("Exiting..."); Thread.interrupted(); return; } } catch (Throwable t) { s_logger.error("Unexpected throwable", t); } } while (true); } private void postInstalledEvent(DeploymentPackage dp, String url, boolean successful, Exception e) { Map<String, Object> props = new HashMap<String, Object>(); if (dp != null) { props.put(EVENT_PACKAGE_NAME, dp.getName()); Version version = dp.getVersion(); props.put(EVENT_PACKAGE_VERSION, version.toString()); } else { props.put(EVENT_PACKAGE_NAME, "UNKNOWN"); props.put(EVENT_PACKAGE_VERSION, "UNKNOWN"); } props.put(EVENT_PACKAGE_URL, url); props.put(EVENT_SUCCESSFUL, successful); props.put(EVENT_EXCEPTION, e); EventProperties eventProps = new EventProperties(props); this.m_eventAdmin.postEvent(new Event(EVENT_INSTALLED_TOPIC, eventProps)); } private void postUninstalledEvent(String name, boolean successful, Exception e) { Map<String, Object> props = new HashMap<String, Object>(); props.put(EVENT_PACKAGE_NAME, name); props.put(EVENT_SUCCESSFUL, successful); props.put(EVENT_EXCEPTION, e); EventProperties eventProps = new EventProperties(props); this.m_eventAdmin.postEvent(new Event(EVENT_UNINSTALLED_TOPIC, eventProps)); } private void installPackagesFromConfFile() { if (this.m_dpaConfPath != null) { FileReader fr = null; try { fr = new FileReader(this.m_dpaConfPath); this.m_deployedPackages.load(fr); } catch (IOException e) { s_logger.error("Exception loading deployment packages configuration file", e); } finally { if (fr != null) { try { fr.close(); } catch (IOException e) { e.printStackTrace(); } } } } Set<Object> packageNames = this.m_deployedPackages.keySet(); for (Object packageName : packageNames) { String packageUrl = (String) this.m_deployedPackages.get(packageName); s_logger.info("Deploying package name {} at URL {}", packageName, packageUrl); try { installDeploymentPackageAsync(packageUrl); } catch (Exception e) { s_logger.error("Error installing package {}", packageName, e); } } } private DeploymentPackage installDeploymentPackageInternal(String urlSpec) throws DeploymentException, IOException, URISyntaxException { URL url = new URL(urlSpec); // Get the file base name from the URL String urlPath = url.getPath(); String[] parts = urlPath.split("/"); String dpBasename = parts[parts.length - 1]; String dpPersistentFilePath = this.m_packagesPath + File.separator + dpBasename; File dpPersistentFile = new File(dpPersistentFilePath); DeploymentPackage dp = null; File dpFile = null; InputStream dpInputStream = null; BufferedReader br = null; try { // Download the package to a temporary file unless it already resides // on the local filesystem. if (!url.getProtocol().equals("file")) { dpFile = File.createTempFile("dpa", null); dpFile.deleteOnExit(); FileUtils.copyURLToFile(url, dpFile, this.m_connTimeout, this.m_readTimeout); } else { dpFile = new File(url.getPath()); } dpInputStream = new FileInputStream(dpFile); dp = this.m_deploymentAdmin.installDeploymentPackage(dpInputStream); // Now we need to copy the deployment package file to the Kura // packages directory unless it's already there. if (!dpFile.getCanonicalPath().equals(dpPersistentFile.getCanonicalPath())) { s_logger.debug("dpFile.getCanonicalPath(): {}", dpFile.getCanonicalPath()); s_logger.debug("dpPersistentFile.getCanonicalPath(): {}", dpPersistentFile.getCanonicalPath()); FileUtils.copyFile(dpFile, dpPersistentFile); addPackageToConfFile(dp.getName(), "file:" + dpPersistentFilePath); } } catch (DeploymentException e) { throw e; } catch (IOException e) { throw e; } finally { if (br != null) { try { br.close(); } catch (IOException ex) { s_logger.error("I/O Exception while closing BufferedReader!"); } } if (dpInputStream != null) { try { dpInputStream.close(); } catch (IOException e) { s_logger.warn("Cannot close input stream", e); } } // The file from which we have installed the deployment package will be deleted // unless it's a persistent deployment package file. if (dpFile != null && !dpFile.getCanonicalPath().equals(dpPersistentFile.getCanonicalPath())) { dpFile.delete(); } } return dp; } private void addPackageToConfFile(String packageName, String packageUrl) { this.m_deployedPackages.setProperty(packageName, packageUrl); if (this.m_dpaConfPath == null) { s_logger.warn("Configuration file not specified"); return; } try { FileOutputStream fos = new FileOutputStream(this.m_dpaConfPath); this.m_deployedPackages.store(fos, null); fos.flush(); fos.getFD().sync(); fos.close(); } catch (IOException e) { s_logger.error("Error writing package configuration file", e); } } private void removePackageFromConfFile(String packageName) { this.m_deployedPackages.remove(packageName); if (this.m_dpaConfPath == null) { s_logger.warn("Configuration file not specified"); return; } try { FileOutputStream fos = new FileOutputStream(this.m_dpaConfPath); this.m_deployedPackages.store(fos, null); fos.flush(); fos.getFD().sync(); fos.close(); } catch (IOException e) { s_logger.error("Error writing package configuration file", e); } } }