/*******************************************************************************
* 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.rp.sh.impl;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.io.FileUtils;
import org.eclipse.kura.system.SystemService;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentException;
import org.osgi.service.deploymentadmin.DeploymentPackage;
import org.osgi.service.deploymentadmin.spi.DeploymentSession;
import org.osgi.service.deploymentadmin.spi.ResourceProcessor;
import org.osgi.service.deploymentadmin.spi.ResourceProcessorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ShellScriptResourceProcessorImpl implements ResourceProcessor {
private static final Logger s_logger = LoggerFactory.getLogger(ShellScriptResourceProcessorImpl.class);
private static final String PACKAGES_PATH_PROPNAME = "kura.packages";
private static final String INSTALL_ACTION = "install";
private static final String UNINSTALL_ACTION = "uninstall";
private File m_resourcesRootDirectory;
private DeploymentPackage m_sourceDP;
private DeploymentPackage m_targetDP;
private final Map<String, File> m_sourceResourceFiles = new HashMap<String, File>();
private final List<String> m_uninstalledResources = new ArrayList<String>();
private final List<String> m_installedResources = new ArrayList<String>();
private final List<String> m_droppedResources = new ArrayList<String>();
BundleContext m_bundleContext;
protected void activate(BundleContext bundleContext) {
s_logger.info("activate");
this.m_bundleContext = bundleContext;
final Properties kuraProperties;
final ServiceReference<SystemService> systemServiceRef = bundleContext.getServiceReference(SystemService.class);
if (systemServiceRef == null) {
throw new IllegalStateException("Unable to find instance of: " + SystemService.class.getName());
}
final SystemService systemService = bundleContext.getService(systemServiceRef);
if (systemService == null) {
throw new IllegalStateException("Unable to get instance of: " + SystemService.class.getName());
}
try {
kuraProperties = systemService.getProperties();
} finally {
bundleContext.ungetService(systemServiceRef);
}
String packagesPath = kuraProperties.getProperty(PACKAGES_PATH_PROPNAME);
if (packagesPath == null || 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/eurotech/kura/kura/packages");
packagesPath = kuraProperties.getProperty(PACKAGES_PATH_PROPNAME);
s_logger.warn("Overridding invalid kura.packages location");
}
this.m_resourcesRootDirectory = new File(packagesPath, "resources");
if (!this.m_resourcesRootDirectory.exists()) {
boolean success = this.m_resourcesRootDirectory.mkdirs();
if (!success) {
throw new ComponentException("Failed to make directory: " + this.m_resourcesRootDirectory.getPath());
}
}
}
protected void deactivate(BundleContext bundleContext) {
s_logger.info("deactivate");
}
@Override
public void begin(DeploymentSession dpSession) {
s_logger.info("begin");
this.m_sourceDP = dpSession.getSourceDeploymentPackage();
this.m_targetDP = dpSession.getTargetDeploymentPackage();
s_logger.info("Source Deployment Package name: '{}'", this.m_sourceDP.getName());
s_logger.info("Target Deployment Package name: '{}'", this.m_targetDP.getName());
}
@Override
public void cancel() {
s_logger.info("cancel");
}
@Override
public void commit() {
s_logger.info("commit");
// Delete dropped resources files
for (String resource : this.m_droppedResources) {
s_logger.info("Delete file for resource: '{}", resource);
File file = getDPResourceFile(resource);
if (file == null) {
s_logger.warn("Resource file missing for resource: '{}'", resource);
} else {
boolean deleted = file.delete();
if (!deleted) {
s_logger.warn("Failed to delete file for resource: '{}'", resource);
}
}
}
// Copy source resource files to Deployment Package resource directory
for (Map.Entry<String, File> entry : this.m_sourceResourceFiles.entrySet()) {
String resource = entry.getKey();
s_logger.info("Copy file for resource: '{}'", resource);
// Get the source resource file
File sourceResourceFile = entry.getValue();
// Construct the destination file
File dir = new File(getResourcesRootDirectory(), this.m_sourceDP.getName());
s_logger.info("getRootResourceDirectory() :'{}'", getResourcesRootDirectory());
s_logger.info("m_sourceDP.getName() :'{}'", this.m_sourceDP.getName());
File destFile = new File(dir, resource);
try {
s_logger.info("Copy file for resource: '{}' to path: '{}'", resource, destFile.getPath());
FileUtils.copyFile(sourceResourceFile, destFile);
} catch (IOException e) {
s_logger.warn("Failed to copy file for resource: '{}'", resource);
}
sourceResourceFile.delete();
}
}
@Override
public void dropAllResources() throws ResourceProcessorException {
s_logger.info("dropAllResources");
String[] resources = this.m_targetDP.getResources();
if (resources != null) {
for (String resource : resources) {
// FIXME?: we also get bundle resources here, not only
// resources associated to this Resource Processor.
ServiceReference<?> serviceReference = this.m_targetDP.getResourceProcessor(resource);
if (serviceReference != null) {
if (serviceReference
.compareTo(this.m_bundleContext.getServiceReference(ResourceProcessor.class)) == 0) {
this.m_droppedResources.add(resource);
}
}
}
}
}
@Override
public void dropped(String resource) throws ResourceProcessorException {
s_logger.info("Dropped resource: '{}'", resource);
this.m_droppedResources.add(resource);
}
@Override
public void prepare() throws ResourceProcessorException {
s_logger.info("prepare");
// Iterate over all resources belonging to the source Deployment Package.
// Some resources might be new and other resources might be updated.
for (Map.Entry<String, File> entry : this.m_sourceResourceFiles.entrySet()) {
String resource = entry.getKey();
// Get the source resource file
File sourceResourceFile = entry.getValue();
// Get the target resource file
File targetResourceFile = getDPResourceFile(resource);
if (targetResourceFile != null) {
s_logger.info("Executing uninstall action for resource: '{}'", resource);
try {
executeScript(targetResourceFile, UNINSTALL_ACTION);
this.m_uninstalledResources.add(resource);
} catch (Exception e) {
s_logger.error("Failed to execute uninstall action for resource: '{}'", resource, e);
throw new ResourceProcessorException(ResourceProcessorException.CODE_PREPARE,
"Failed to execute uninstall action for resource: " + resource, e);
}
}
try {
s_logger.info("Executing install action for resource: '{}'", resource);
executeScript(sourceResourceFile, INSTALL_ACTION);
this.m_installedResources.add(resource);
} catch (Exception e) {
s_logger.error("Failed to execute install action for resource: '{}'", resource, e);
throw new ResourceProcessorException(ResourceProcessorException.CODE_PREPARE,
"Failed to execute install action for resource: " + resource, e);
}
}
for (String resource : this.m_droppedResources) {
File targetResourceFile = getDPResourceFile(resource);
if (targetResourceFile == null) {
s_logger.warn(
"Target resource file missing for resource: '{}'. Proceed anyway but we might not be able to completely rollback the changes",
resource);
} else {
s_logger.info("Executing uninstall action for resource: '{}'", resource);
try {
executeScript(targetResourceFile, UNINSTALL_ACTION);
this.m_uninstalledResources.add(resource);
} catch (Exception e) {
s_logger.error("Failed to execute uninstall action for resource: '{}'", resource, e);
throw new ResourceProcessorException(ResourceProcessorException.CODE_PREPARE,
"Failed to execute uninstall action for resource: " + resource, e);
}
}
}
}
@Override
public void process(String resource, InputStream is) throws ResourceProcessorException {
s_logger.info("Processing resource: '{}'", resource);
// Check deltas.
// Caveat: we should calculate the resource deltas against the "target" Deployment Package, i.e.
// a previous version of the Deployment Package.
// The problem is that we start the framework with the osgi.clean property set to true
// and we reinstall all the Deployment Packages from an external directory.
// Since the OSGi cache is clean there will be no target Deployment Package when the Deployment Package is
// reinstalled.
// This Resource Processor stores Deployment Package resources
// as files in a persistent directory on disk.
// The delta is then calculated against the resources files in that directory.
File targetResourceFile = getDPResourceFile(resource);
if (targetResourceFile != null) {
s_logger.info("Resource: '{}' already exists in Deployment Package: '{}'", resource, this.m_sourceDP);
InputStream tis = null;
try {
tis = new FileInputStream(targetResourceFile);
byte[] d1 = computeDigest(tis);
byte[] d2 = computeDigest(is);
if (digestsMatch(d1, d2)) {
s_logger.info("Digests for source and target resource: '{}' match. No need to update resource",
resource);
return;
}
} catch (FileNotFoundException e) {
s_logger.warn("Unexpected exception. Proceed anyway", e);
} catch (NoSuchAlgorithmException e) {
s_logger.warn("Unexpected exception. Proceed anyway", e);
} catch (IOException e) {
s_logger.warn("Unexpected exception. Proceed anyway", e);
} finally {
if (tis != null) {
try {
tis.close();
} catch (IOException ex) {
s_logger.error("I/O Exception while closing BufferedReader!");
}
}
}
}
// Create temporary files for source resources
File tmpFile = null;
try {
tmpFile = File.createTempFile("shrp", null);
} catch (IOException e) {
s_logger.error("Failed to create temporary file for resource: '{}'", resource);
throw new ResourceProcessorException(ResourceProcessorException.CODE_PREPARE,
"Failed to create temporary file for resource: " + resource, e);
}
tmpFile.deleteOnExit();
try {
FileUtils.copyInputStreamToFile(is, tmpFile);
} catch (IOException e) {
s_logger.error("Failed to copy input stream for resource: '{}'", resource);
throw new ResourceProcessorException(ResourceProcessorException.CODE_PREPARE,
"Failed to copy input stream for resource: " + resource, e);
}
this.m_sourceResourceFiles.put(resource, tmpFile);
}
@Override
public void rollback() {
s_logger.info("rollback");
for (String resource : this.m_installedResources) {
// Get the source resource file
File sourceResourceFile = this.m_sourceResourceFiles.get(resource);
try {
s_logger.info("Executing uninstall action for resource: '{}'", resource);
executeScript(sourceResourceFile, UNINSTALL_ACTION);
} catch (Exception e) {
s_logger.warn("Failed to execute uninstall action for resource: '{}'", resource, e);
}
}
for (String resource : this.m_uninstalledResources) {
// Get the target resource file
File targetResourceFile = getDPResourceFile(resource);
if (targetResourceFile == null) {
s_logger.warn(
"Target resource file missing for resource: '{}'. Proceed anyway but we might not be able to completely rollback the changes",
resource);
} else {
s_logger.info("Executing install action for resource: '{}'", resource);
try {
executeScript(targetResourceFile, INSTALL_ACTION);
} catch (Exception e) {
s_logger.warn("Failed to execute install action for resource: '{}'", resource, e);
}
}
}
}
private static void executeScript(File file, String action) throws Exception {
String path = file.getCanonicalPath();
String[] cmdarray = { "/bin/bash", path, action };
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec(cmdarray);
ProcessMonitorThread pmt = new ProcessMonitorThread(proc, null, 0);
pmt.start();
pmt.join();
s_logger.error("Script stdout: {}", pmt.getStdout());
s_logger.error("Script stderr: {}", pmt.getStderr());
Exception e = pmt.getException();
if (e != null) {
s_logger.error("Exception executing install action for script: '{}'", e);
throw e;
} else {
if (!pmt.isTimedOut()) {
Integer exitValue = pmt.getExitValue();
if (exitValue != 0) {
s_logger.error("Install action for script: '{}' failed with exit value: {}", path, exitValue);
throw new Exception("Install action for script: " + path + " failed with exit value: " + exitValue);
}
}
}
}
// private File getTargetDPResourceFile(String resource) {
// File dir = getTargetDPResourceDirectory();
// if (dir == null) {
// return null;
// } else {
// File file = new File(dir, resource);
// if (file.exists()) {
// return file;
// }
// }
// return null;
// }
//
// private File getTargetDPResourceDirectory() {
// File root = getRootResourceDirectory();
// if (root == null) {
// return null;
// } else {
// File dir = new File(root, m_targetDP.getName());
// if (dir.exists()) {
// return dir;
// }
// }
// return null;
// }
private File getDPResourceFile(String resource) {
File dir = getDPResourceDirectory();
if (dir == null) {
return null;
} else {
File file = new File(dir, resource);
if (file.exists()) {
return file;
}
}
return null;
}
private File getDPResourceDirectory() {
File root = getResourcesRootDirectory();
if (root == null) {
return null;
} else {
String dpName;
if (!this.m_sourceDP.getName().isEmpty()) {
dpName = this.m_sourceDP.getName();
} else {
dpName = this.m_targetDP.getName();
}
File dir = new File(root, dpName);
if (dir.exists()) {
return dir;
}
}
return null;
}
private File getResourcesRootDirectory() {
return this.m_resourcesRootDirectory;
}
// private boolean belongsToTargetDP(String resource) {
// boolean result = false;
// String[] resources = m_targetDP.getResources();
// if (resources != null) {
// for (String targetResource : resources) {
// if (resource.equals(targetResource)) {
// result = true;
// break;
// }
// }
// }
// return result;
// }
private static byte[] computeDigest(InputStream is) throws NoSuchAlgorithmException, IOException {
MessageDigest md = MessageDigest.getInstance("MD5");
DigestInputStream dis = new DigestInputStream(is, md);
while (dis.read() != -1) {
;
}
byte[] digest = md.digest();
return digest;
}
private static boolean digestsMatch(byte[] d1, byte[] d2) {
if (d1 == null || d2 == null) {
return false;
}
if (d1.length != d2.length) {
return false;
}
for (int i = 0; i < d1.length; i++) {
if (d1[i] != d2[i]) {
return false;
}
}
return true;
}
}