blob: 59dac4ff8b316603dbd6153dc43456c756a8b8ce [file] [log] [blame]
/*
* 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("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();
}});
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("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", 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("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", 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();
public abstract void runPostLaunchCommand();
@Override
public void kill() {
stop();
}
/**
* Implement this method in child classes to add some post-launch behavior
*/
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();
}