/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.brooklyn.entity.software.base; import static com.google.common.base.Preconditions.checkNotNull; import static org.apache.brooklyn.util.JavaGroovyEquivalents.elvis; import java.io.File; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.util.Map; import org.apache.brooklyn.api.entity.EntityLocal; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.entity.BrooklynConfigKeys; import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.util.core.task.DynamicTasks; import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.core.text.TemplateProcessor; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.os.Os; import org.apache.brooklyn.util.stream.ReaderInputStream; import org.apache.brooklyn.util.text.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.Beta; import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; /** * An abstract implementation of the {@link SoftwareProcessDriver}. */ public abstract class AbstractSoftwareProcessDriver implements SoftwareProcessDriver { private static final Logger log = LoggerFactory.getLogger(AbstractSoftwareProcessDriver.class); protected final EntityLocal entity; protected final ResourceUtils resource; protected final Location location; public AbstractSoftwareProcessDriver(EntityLocal entity, Location location) { this.entity = checkNotNull(entity, "entity"); this.location = checkNotNull(location, "location"); this.resource = ResourceUtils.create(entity); } /* * (non-Javadoc) * @see org.apache.brooklyn.entity.software.base.SoftwareProcessDriver#rebind() */ @Override public void rebind() { // no-op } /** * Start the entity. * <p> * This installs, configures and launches the application process. However, * users can also call the {@link #install()}, {@link #customize()} and * {@link #launch()} steps independently. The {@link #postLaunch()} will * be called after the {@link #launch()} metheod is executed, but the * process may not be completely initialised at this stage, so care is * required when implementing these stages. * <p> * The {@link BrooklynConfigKeys#ENTITY_STARTED} key can be set on the location * or the entity to skip the startup process if the entity is already running, * according to the {@link #isRunning()} method. To force the startup to be * skipped, {@link BrooklynConfigKeys#SKIP_ENTITY_START} can be set on the entity. * The {@link BrooklynConfigKeys#SKIP_ENTITY_INSTALLATION} key can also be used to * skip the {@link #setup()}, {@link #copyInstallResources()} and * {@link #install()} methods if set on the entity or location. * * @see #stop() */ @Override public void start() { boolean skipStart = false; Optional<Boolean> locationRunning = Optional.fromNullable(getLocation().getConfig(BrooklynConfigKeys.SKIP_ENTITY_START_IF_RUNNING)); Optional<Boolean> entityRunning = Optional.fromNullable(entity.getConfig(BrooklynConfigKeys.SKIP_ENTITY_START_IF_RUNNING)); Optional<Boolean> entityStarted = Optional.fromNullable(entity.getConfig(BrooklynConfigKeys.SKIP_ENTITY_START)); if (locationRunning.or(entityRunning).or(false)) { skipStart = isRunning(); } else { skipStart = entityStarted.or(false); } if (!skipStart) { DynamicTasks.queue("install", new Runnable() { public void run() { Optional<Boolean> locationInstalled = Optional.fromNullable(getLocation().getConfig(BrooklynConfigKeys.SKIP_ENTITY_INSTALLATION)); Optional<Boolean> entityInstalled = Optional.fromNullable(entity.getConfig(BrooklynConfigKeys.SKIP_ENTITY_INSTALLATION)); boolean skipInstall = locationInstalled.or(entityInstalled).or(false); if (!skipInstall) { DynamicTasks.queue("copy-pre-install-resources", new Runnable() { public void run() { waitForConfigKey(BrooklynConfigKeys.PRE_INSTALL_RESOURCES_LATCH); copyPreInstallResources(); }}); DynamicTasks.queue("pre-install", new Runnable() { public void run() { preInstall(); }}); DynamicTasks.queue("pre-install-command", new Runnable() { public void run() { runPreInstallCommand(); }}); DynamicTasks.queue("setup", new Runnable() { public void run() { waitForConfigKey(BrooklynConfigKeys.SETUP_LATCH); setup(); }}); DynamicTasks.queue("copy-install-resources", new Runnable() { public void run() { waitForConfigKey(BrooklynConfigKeys.INSTALL_RESOURCES_LATCH); copyInstallResources(); }}); DynamicTasks.queue("install (main)", new Runnable() { public void run() { waitForConfigKey(BrooklynConfigKeys.INSTALL_LATCH); install(); }}); } DynamicTasks.queue("post-install-command", new Runnable() { public void run() { runPostInstallCommand(); }}); }}); DynamicTasks.queue("customize", new Runnable() { public void run() { waitForConfigKey(BrooklynConfigKeys.CUSTOMIZE_LATCH); customize(); }}); DynamicTasks.queue("launch", new Runnable() { public void run() { DynamicTasks.queue("copy-runtime-resources", new Runnable() { public void run() { waitForConfigKey(BrooklynConfigKeys.RUNTIME_RESOURCES_LATCH); copyRuntimeResources(); }}); DynamicTasks.queue("pre-launch-command", new Runnable() { public void run() { runPreLaunchCommand(); }}); DynamicTasks.queue("launch (main)", new Runnable() { public void run() { waitForConfigKey(BrooklynConfigKeys.LAUNCH_LATCH); launch(); }}); DynamicTasks.queue("post-launch-command", new Runnable() { public void run() { runPostLaunchCommand(); }}); }}); } DynamicTasks.queue("post-launch", new Runnable() { public void run() { postLaunch(); }}); } @Override public abstract void stop(); /** * Implement this method in child classes to add some pre-install behavior */ public void preInstall() {} public abstract void runPreInstallCommand(); public abstract void setup(); public abstract void install(); public abstract void runPostInstallCommand(); public abstract void customize(); public abstract void runPreLaunchCommand(); public abstract void launch(); /** Only run if launch is run (if start is not skipped). */ public abstract void runPostLaunchCommand(); @Override public void kill() { stop(); } /** * Implement this method in child classes to add some post-launch behavior. * This is run even if start is skipped and launch is not run. */ public void postLaunch() {} @Override public void restart() { DynamicTasks.queue("stop (best effort)", new Runnable() { public void run() { DynamicTasks.markInessential(); boolean previouslyRunning = isRunning(); try { ServiceStateLogic.setExpectedState(getEntity(), Lifecycle.STOPPING); stop(); } catch (Exception e) { // queue a failed task so that there is visual indication that this task had a failure, // without interrupting the parent if (previouslyRunning) { log.warn(getEntity() + " restart: stop failed, when was previously running (ignoring)", e); DynamicTasks.queue(Tasks.fail("Primary job failure (when previously running)", e)); } else { log.debug(getEntity() + " restart: stop failed (but was not previously running, so not a surprise)", e); DynamicTasks.queue(Tasks.fail("Primary job failure (when not previously running)", e)); } // the above queued tasks will cause this task to be indicated as failed, with an indication of severity } } }); if (doFullStartOnRestart()) { DynamicTasks.waitForLast(); ServiceStateLogic.setExpectedState(getEntity(), Lifecycle.STARTING); start(); } else { DynamicTasks.queue("launch", new Runnable() { public void run() { ServiceStateLogic.setExpectedState(getEntity(), Lifecycle.STARTING); launch(); }}); DynamicTasks.queue("post-launch", new Runnable() { public void run() { postLaunch(); }}); } } @Beta /** ideally restart() would take options, e.g. whether to do full start, skip installs, etc; * however in the absence here is a toggle - not sure how well it works; * default is false which is similar to previous behaviour (with some seemingly-obvious tidies), * meaning install and configure will NOT be done on restart. */ protected boolean doFullStartOnRestart() { return false; } @Override public EntityLocal getEntity() { return entity; } @Override public Location getLocation() { return location; } public InputStream getResource(String url) { return resource.getResourceFromUrl(url); } /** * Files and templates to be copied to the server <em>before</em> pre-install. This allows the {@link #preInstall()} * process to have access to all required resources. * <p> * Will be prefixed with the entity's {@link #getInstallDir() install directory} if relative. * * @see SoftwareProcess#PRE_INSTALL_FILES * @see SoftwareProcess#PRE_INSTALL_TEMPLATES * @see #copyRuntimeResources() */ public void copyPreInstallResources() { copyResources(entity.getConfig(SoftwareProcess.PRE_INSTALL_FILES), entity.getConfig(SoftwareProcess.PRE_INSTALL_TEMPLATES)); } /** * Files and templates to be copied to the server <em>before</em> installation. This allows the {@link #install()} * process to have access to all required resources. * <p> * Will be prefixed with the entity's {@link #getInstallDir() install directory} if relative. * * @see SoftwareProcess#INSTALL_FILES * @see SoftwareProcess#INSTALL_TEMPLATES * @see #copyRuntimeResources() */ public void copyInstallResources() { copyResources(entity.getConfig(SoftwareProcess.INSTALL_FILES), entity.getConfig(SoftwareProcess.INSTALL_TEMPLATES)); } private void copyResources(Map<String, String> files, Map<String, String> templates) { // Ensure environment variables are not looked up here, otherwise sub-classes might // lookup port numbers and fail with ugly error if port is not set; better to wait // until in Entity's code (e.g. customize) where such checks are done explicitly. boolean hasAnythingToCopy = ((files != null && files.size() > 0) || (templates != null && templates.size() > 0)); if (hasAnythingToCopy) { createDirectory(getInstallDir(), "create install directory"); // TODO see comment in copyResource, that should be queued as a task like the above // (better reporting in activities console) if (files != null && files.size() > 0) { for (String source : files.keySet()) { String target = files.get(source); String destination = Os.isAbsolutish(target) ? target : Os.mergePathsUnix(getInstallDir(), target); copyResource(source, destination, true); } } if (templates != null && templates.size() > 0) { for (String source : templates.keySet()) { String target = templates.get(source); String destination = Os.isAbsolutish(target) ? target : Os.mergePathsUnix(getInstallDir(), target); copyTemplate(source, destination, true, MutableMap.<String, Object>of()); } } } } protected abstract void createDirectory(String directoryName, String summaryForLogging); /** * Files and templates to be copied to the server <em>after</em> customisation. This allows overwriting of * existing files such as entity configuration which may be copied from the installation directory * during the {@link #customize()} process. * <p> * Will be prefixed with the entity's {@link #getRunDir() run directory} if relative. * * @see SoftwareProcess#RUNTIME_FILES * @see SoftwareProcess#RUNTIME_TEMPLATES * @see #copyInstallResources() */ public void copyRuntimeResources() { try { createDirectory(getRunDir(), "create run directory"); Map<String, String> runtimeFiles = entity.getConfig(SoftwareProcess.RUNTIME_FILES); if (runtimeFiles != null && runtimeFiles.size() > 0) { for (String source : runtimeFiles.keySet()) { String target = runtimeFiles.get(source); String destination = Os.isAbsolutish(target) ? target : Os.mergePathsUnix(getRunDir(), target); copyResource(source, destination, true); } } Map<String, String> runtimeTemplates = entity.getConfig(SoftwareProcess.RUNTIME_TEMPLATES); if (runtimeTemplates != null && runtimeTemplates.size() > 0) { for (String source : runtimeTemplates.keySet()) { String target = runtimeTemplates.get(source); String destination = Os.isAbsolutish(target) ? target : Os.mergePathsUnix(getRunDir(), target); copyTemplate(source, destination, true, MutableMap.<String, Object>of()); } } } catch (Exception e) { log.warn("Error copying runtime resources", e); throw Exceptions.propagate(e); } } /** * @param template File to template and copy. * @param target Destination on server. * @return The exit code the SSH command run. */ public int copyTemplate(File template, String target) { return copyTemplate(template.toURI().toASCIIString(), target); } /** * @param template URI of file to template and copy, e.g. file://.., http://.., classpath://.. * @param target Destination on server. * @return The exit code of the SSH command run. */ public int copyTemplate(String template, String target) { return copyTemplate(template, target, false, ImmutableMap.<String, String>of()); } /** * @param template URI of file to template and copy, e.g. file://.., http://.., classpath://.. * @param target Destination on server. * @param extraSubstitutions Extra substitutions for the templater to use, for example * "foo" -> "bar", and in a template ${foo}. * @return The exit code of the SSH command run. */ public int copyTemplate(String template, String target, boolean createParent, Map<String, ?> extraSubstitutions) { String data = processTemplate(template, extraSubstitutions); return copyResource(MutableMap.<Object,Object>of(), new StringReader(data), target, createParent); } public abstract int copyResource(Map<Object,Object> sshFlags, String source, String target, boolean createParentDir); public abstract int copyResource(Map<Object,Object> sshFlags, InputStream source, String target, boolean createParentDir); /** * @param file File to copy. * @param target Destination on server. * @return The exit code the SSH command run. */ public int copyResource(File file, String target) { return copyResource(file.toURI().toASCIIString(), target); } /** * @param resource URI of file to copy, e.g. file://.., http://.., classpath://.. * @param target Destination on server. * @return The exit code of the SSH command run */ public int copyResource(String resource, String target) { return copyResource(MutableMap.of(), resource, target); } public int copyResource(String resource, String target, boolean createParentDir) { return copyResource(MutableMap.of(), resource, target, createParentDir); } @SuppressWarnings({ "rawtypes", "unchecked" }) public int copyResource(Map sshFlags, String source, String target) { return copyResource(sshFlags, source, target, false); } /** * @see #copyResource(Map, InputStream, String, boolean) */ public int copyResource(Reader source, String target) { return copyResource(MutableMap.of(), source, target, false); } /** * @see #copyResource(Map, InputStream, String, boolean) */ public int copyResource(Map<Object,Object> sshFlags, Reader source, String target, boolean createParent) { return copyResource(sshFlags, new ReaderInputStream(source), target, createParent); } /** * @see #copyResource(Map, InputStream, String, boolean) */ public int copyResource(InputStream source, String target) { return copyResource(MutableMap.of(), source, target, false); } public String getResourceAsString(String url) { return resource.getResourceAsString(url); } public String processTemplate(File templateConfigFile, Map<String,Object> extraSubstitutions) { return processTemplate(templateConfigFile.toURI().toASCIIString(), extraSubstitutions); } public String processTemplate(File templateConfigFile) { return processTemplate(templateConfigFile.toURI().toASCIIString()); } /** Takes the contents of a template file from the given URL (often a classpath://com/myco/myprod/myfile.conf or .sh) * and replaces "${entity.xxx}" with the result of entity.getXxx() and similar for other driver, location; * as well as replacing config keys on the management context * <p> * uses Freemarker templates under the covers **/ public String processTemplate(String templateConfigUrl) { return processTemplate(templateConfigUrl, ImmutableMap.<String,String>of()); } public String processTemplate(String templateConfigUrl, Map<String,? extends Object> extraSubstitutions) { return processTemplateContents(getResourceAsString(templateConfigUrl), extraSubstitutions); } public String processTemplateContents(String templateContents) { return processTemplateContents(templateContents, ImmutableMap.<String,String>of()); } public String processTemplateContents(String templateContents, Map<String,? extends Object> extraSubstitutions) { return TemplateProcessor.processTemplateContents(templateContents, this, extraSubstitutions); } protected void waitForConfigKey(ConfigKey<?> configKey) { Object val = entity.getConfig(configKey); if (val != null) log.debug("{} finished waiting for {} (value {}); continuing...", new Object[] {this, configKey, val}); } /** * @deprecated since 0.5.0; instead rely on {@link org.apache.brooklyn.api.entity.drivers.downloads.DownloadResolverManager} to include local-repo, such as: * * <pre> * {@code * DownloadResolver resolver = Entities.newDownloader(this); * List<String> urls = resolver.getTargets(); * } * </pre> */ protected String getEntityVersionLabel() { return getEntityVersionLabel("_"); } /** * @deprecated since 0.5.0; instead rely on {@link org.apache.brooklyn.api.entity.drivers.downloads.DownloadResolverManager} to include local-repo */ protected String getEntityVersionLabel(String separator) { return elvis(entity.getEntityType().getSimpleName(), entity.getClass().getName())+(getVersion() != null ? separator+getVersion() : ""); } public String getVersion() { return getEntity().getConfig(SoftwareProcess.SUGGESTED_VERSION); } public abstract String getRunDir(); public abstract String getInstallDir(); }