/* * Jopr Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also 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 and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.jboss.on.common.jbossas; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import com.jboss.jbossnetwork.product.jbpm.handlers.ActionHandlerMessageLog; import com.jboss.jbossnetwork.product.jbpm.handlers.BaseHandler; import com.jboss.jbossnetwork.product.jbpm.handlers.ContextVariables; import com.jboss.jbossnetwork.product.jbpm.handlers.ControlActionFacade; import com.jboss.jbossnetwork.product.jbpm.handlers.SoftwareValue; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jbpm.context.exe.ContextInstance; import org.jbpm.graph.def.Action; import org.jbpm.graph.def.Node; import org.jbpm.graph.def.ProcessDefinition; import org.jbpm.graph.def.SuperState; import org.jbpm.graph.exe.ProcessInstance; import org.jbpm.instantiation.Delegation; import org.jbpm.instantiation.FieldInstantiator; import org.rhq.core.domain.configuration.Configuration; import org.rhq.core.domain.configuration.PropertySimple; import org.rhq.core.domain.content.PackageDetailsKey; import org.rhq.core.domain.content.transfer.ContentResponseResult; import org.rhq.core.domain.content.transfer.DeployIndividualPackageResponse; import org.rhq.core.domain.content.transfer.DeployPackageStep; import org.rhq.core.domain.content.transfer.ResourcePackageDetails; import org.rhq.core.pluginapi.content.ContentContext; /** * Class responsible for managing the running of a JBPM process to apply a patch to a JBoss instance. This class will * take care of populating the context with whatever variables are necessary, including the creation of any temporary * directories that may be needed. The results of the workflow run will be parsed and converted into the necessary * domain model and returned to the caller. * * @author Jason Dobies */ public class JBPMWorkflowManager { private ContentContext contentContext; private ControlActionFacade controlFacade; private JBossASPaths jbossPaths; private final Log log = LogFactory.getLog(this.getClass()); public JBPMWorkflowManager(ContentContext contentContext, ControlActionFacade controlFacade, JBossASPaths jbossPaths) { this.contentContext = contentContext; this.controlFacade = controlFacade; this.jbossPaths = jbossPaths; } /** * Runs the JBPM process included in the provided package description. This method will make calls back into * the plugin container as necessary to retrieve information or execute operations. * * @param packageDetails contains data to describe the package being installed * @return plugin container domain model representation of the result of attempting to install the package * @throws Exception if there are any unexpected errors during the process */ public DeployIndividualPackageResponse run(ResourcePackageDetails packageDetails) throws Exception { checkCompatibility(packageDetails); // Grab the JBPM process byte[] metadataBytes = packageDetails.getMetadata(); if (metadataBytes == null) { throw new IllegalArgumentException("The 'metadata' field of the 'packageDetails' parameter is null."); } String process = new String(metadataBytes); // Generate the list of steps to execute first. If the workflow fails, we won't have log entries for the // unexecuted steps. Link these steps up against the log and include unexecuted steps from this list // in the response. List<DeployPackageStep> unexecutedSteps = translateSteps(packageDetails); // Load the JBPM process into the executor ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(process); ProcessInstance processInstance = new ProcessInstance(processDefinition); ContextInstance context = processInstance.getContextInstance(); // Populate the variables we'll need in the handlers context.setVariable(ContextVariables.CONTENT_CONTEXT, contentContext); context.setVariable(ContextVariables.CONTROL_ACTION_FACADE, controlFacade); context.setVariable(ContextVariables.PACKAGE_DETAILS_KEY, packageDetails.getKey()); SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssSSS"); Date timestamp = new Date(); String formattedTimestamp = format.format(timestamp); context.setVariable(ContextVariables.TIMESTAMP, formattedTimestamp); try { File downloadDir = createTempDir("jon_download"); if (downloadDir != null) { context.setVariable(ContextVariables.DOWNLOAD_DIR, downloadDir.getAbsolutePath()); } File patchDir = createTempDir("jon_patch"); if (patchDir != null) { context.setVariable(ContextVariables.PATCH_DIR, patchDir.getAbsolutePath()); } } catch (IOException e) { // No need to throw this error, a handler will check for these to be valid and fail the step accordingly log.error("Error creating temporary directories", e); } // Populate the variables describing the AS instance String jbossHomeDir = jbossPaths.getHomeDir(); log.debug("jbossHomeDir: " + (jbossHomeDir == null ? " is NULL" : jbossHomeDir)); jbossHomeDir += File.separator; // Just to make sure it ends with the separator context.setVariable(ContextVariables.JBOSS_HOME_DIR, jbossHomeDir); String jbossClientDir = jbossHomeDir + File.separator + "client" + File.separator; log.debug("jbossClientDir: " + jbossClientDir); context.setVariable(ContextVariables.JBOSS_CLIENT_DIR, jbossClientDir); String jbossServerDir = jbossPaths.getServerDir(); log.debug("jbossServerDir: " + (jbossServerDir == null ? " is NULL" : jbossServerDir)); jbossServerDir += File.separator; // Just to make sure context.setVariable(ContextVariables.JBOSS_SERVER_DIR, jbossServerDir); // The workflow will reference values inside of this object for substitution // Ultimately, we should parse out the workflows to use the domain object directly SoftwareValue softwareValue = resourcePackageDetailsToSoftwareValue(packageDetails); context.setVariable(ContextVariables.SOFTWARE, softwareValue); // Perform the workflow try { processInstance.signal(); } catch (Exception e) { log.error("Error received from the workflow", e); } // Parse the logs from the workflow execution into the domain model List logs = processInstance.getLoggingInstance().getLogs(); DeployIndividualPackageResponse response = parseLogs(logs, unexecutedSteps, packageDetails.getKey()); return response; } /** * Translates the metadata inside the given package into a list of readable steps that will be executed * during this package's installation. This call will <em>not</em> execute the package deployment nor cause * any changes to be made to the resource. * * @param packageDetails generate the steps for this package; cannot be <code>null</code> * @return translated steps if the metadata in this package was populated; <code>null</code> otherwise. * @throws Exception if there is an error during the translation */ public List<DeployPackageStep> translateSteps(ResourcePackageDetails packageDetails) throws Exception { checkCompatibility(packageDetails); // Grab the JBPM process byte[] metadataBytes = packageDetails.getMetadata(); if (metadataBytes == null) { return null; } String process = new String(metadataBytes); // Load the JBPM process into the executor ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(process); // Iterate over the nodes manually, creating a domain representation of the step in the process SuperState mainProcess = (SuperState) processDefinition.getNode("main_process"); if (mainProcess == null) { log.warn("Could not retrieve main process for package [" + packageDetails + "]"); return null; } List<DeployPackageStep> steps = new ArrayList<DeployPackageStep>(); List<Node> nodes = mainProcess.getNodes(); for (Node node : nodes) { Action action = node.getAction(); if (action != null) { Delegation delegation = action.getActionDelegation(); String configProps = delegation.getConfiguration(); String actionHandlerClassName = delegation.getClassName(); FieldInstantiator instantiator = new FieldInstantiator(); BaseHandler handler = (BaseHandler) instantiator.instantiate(Class.forName(actionHandlerClassName), configProps); handler.setPropertyDefaults(); String description = handler.getDescription(); DeployPackageStep step = new DeployPackageStep(node.getName(), description); step.setStepResult(ContentResponseResult.NOT_PERFORMED); steps.add(step); } } return steps; } /** * Parses through the entire log list returned from the workflow application and converts the relevant entries into * the domain model's package installation steps. Any steps that are not present in the log list will be added * from the unexecuted step list and marked as "not executed". * * @param logs logs made available from the process instance after executing the workflow * @param unexecutedSteps list of all steps that are to be executed, generated prior to running the workflow * @param packageDetailsKey identifies the package that was deployed * @return domain representation of the result of the workflow */ private DeployIndividualPackageResponse parseLogs(List logs, List<DeployPackageStep> unexecutedSteps, PackageDetailsKey packageDetailsKey) { List<DeployPackageStep> steps = new ArrayList<DeployPackageStep>(); ContentResponseResult overallResult = ContentResponseResult.SUCCESS; for (Object uncastedLog : logs) { if (uncastedLog instanceof ActionHandlerMessageLog) { ActionHandlerMessageLog messageLog = (ActionHandlerMessageLog) uncastedLog; DeployPackageStep executedStep = messageLog.getStep(); steps.add(executedStep); // The executed and unexecuted steps are link by their step key and the fact that the equals method // uses this key (as such, their descriptions may be different). This is currently simply a counter. // That counter is handled in different ways for translate v. logging an actual step. They should // still map up, however this log message should output enough of a description for the reader // to ensure that the unexecuted step being removed does correspond to the step found in the logs. if (log.isDebugEnabled()) { String executedStepKey = executedStep.getStepKey(); DeployPackageStep unexecutedStep = null; for (DeployPackageStep s : unexecutedSteps) { if (s.getStepKey().equals(executedStepKey)) { unexecutedStep = s; break; } } if (unexecutedStep != null && log.isDebugEnabled()) { log.debug("Mapped up steps:"); log.debug("Executed Step: " + executedStep); log.debug("Unexecuted Step: " + unexecutedStep); } } boolean unexecutedStepDeleted = unexecutedSteps.remove(executedStep); if (!unexecutedStepDeleted) { log.warn("Could not remove the following step from the unexecuted step list: " + executedStep); } // If any steps fail, mark the entire response as failed if (executedStep.getStepResult() == ContentResponseResult.FAILURE) { overallResult = ContentResponseResult.FAILURE; } } } // For every step that was supposed to run but didn't (those remaining in the unexecuted step list), tack // them on to the end as not executed. for (DeployPackageStep unexecutedStep : unexecutedSteps) { unexecutedStep.setStepResult(ContentResponseResult.NOT_PERFORMED); steps.add(unexecutedStep); } DeployIndividualPackageResponse response = new DeployIndividualPackageResponse(packageDetailsKey, overallResult); response.setDeploymentSteps(steps); return response; } /** * Creates a temporary directory in which to store the bits being downloaded from the server. The name of the * directory will be generated by Java's built in algorithm and returned to the caller. * * @param prefix will be the first part of the file, with Java file utilities coming up with the remainder of the * file name * @return name of the unique, randomly generated temporary directory * @throws IOException if the process cannot create a temporary file */ private File createTempDir(String prefix) throws IOException { // Let's reuse the algorithm the JDK uses to determine a unique name: // 1) create a temp file to get a unique name using JDK createTempFile // 2) then quickly delete the file and... // 3) convert it to a directory // ccrouch, version 1.4 File tmpDir = File.createTempFile(prefix, "", null); // create file with unique name boolean deleteSuccessful = tmpDir.delete(); // delete the tmp file and... boolean mkdirSuccessful = tmpDir.mkdirs(); // ...convert it to a directory if (deleteSuccessful && mkdirSuccessful) { return tmpDir; } else { return null; } } /** * Will throw an exception if the package does not contain a set of compatible installation instructions. * * @param packageDetails package in question * @throws UnsupportedOperationException if the instruction compatibility version is not supported */ private void checkCompatibility(ResourcePackageDetails packageDetails) { Configuration extraProperties = packageDetails.getExtraProperties(); // Check to make sure we know how to parse the instruction set String compatibilityVersion = safeGet(extraProperties, "instructionCompatibilityVersion"); if (compatibilityVersion != null && !compatibilityVersion.equals("1.4")) { throw new UnsupportedOperationException("Instruction set for this package is not supported. Version: " + compatibilityVersion); } } /** * Converts the domain model package representation into the legacy software object that is referenced from the JBPM * workflows. * * @param pkg sent from the server to be installed, the values for the software entity will be taken from here * @return object mirroring the domain's package representation usable for substitutions into the workflow */ private SoftwareValue resourcePackageDetailsToSoftwareValue(ResourcePackageDetails pkg) { Configuration extraProperties = pkg.getExtraProperties(); SoftwareValue softwareValue = new SoftwareValue(); softwareValue.setDownloadUrl(pkg.getLocation()); softwareValue.setFilename(pkg.getFileName()); softwareValue.setFileSize(pkg.getFileSize()); softwareValue.setInstructionCompatibilityVersion(safeGet(extraProperties, "instructionCompatibilityVersion")); softwareValue.setIssueReference(safeGet(extraProperties, "jiraId")); softwareValue.setLicenseName(pkg.getLicenseName()); softwareValue.setLicenseVersion(pkg.getLicenseVersion()); softwareValue.setLongDescription(pkg.getLongDescription()); softwareValue.setMD5(pkg.getMD5()); softwareValue.setSHA256(pkg.getSHA256()); softwareValue.setShortDescription(pkg.getShortDescription()); softwareValue.setTitle(pkg.getName()); return softwareValue; } /** * Utility to extract a potentially null value from a configuration. * * @param configuration may be <code>null</code> * @param key value being retrieved * @return value if it is found in the configuration; <code>null</code> otherwise. */ private String safeGet(Configuration configuration, String key) { if (configuration == null) { return null; } PropertySimple simple = configuration.getSimple(key); return (simple != null) ? simple.getStringValue() : null; } }