org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver Maven / Gradle / Ivy
Show all versions of brooklyn-software-base Show documentation
/*
* 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 org.apache.brooklyn.util.JavaGroovyEquivalents.elvis;
import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.entity.drivers.downloads.DownloadResolver;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.BrooklynLogging;
import org.apache.brooklyn.core.effector.EffectorTasks;
import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.feed.ConfigToAttributes;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.entity.software.base.lifecycle.NaiveScriptRunner;
import org.apache.brooklyn.entity.software.base.lifecycle.ScriptHelper;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.core.internal.ssh.SshTool;
import org.apache.brooklyn.util.core.internal.ssh.sshj.SshjTool;
import org.apache.brooklyn.util.core.json.ShellEnvironmentSerializer;
import org.apache.brooklyn.util.core.mutex.WithMutexes;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.stream.KnownSizeInputStream;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.StringPredicates;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Joiner;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* An abstract SSH implementation of the {@link AbstractSoftwareProcessDriver}.
*
* This provides conveniences for clients implementing the install/customize/launch/isRunning/stop lifecycle
* over SSH. These conveniences include checking whether software is already installed,
* creating/using a PID file for some operations, and reading ssh-specific config from the entity
* to override/augment ssh flags on the session.
*/
public abstract class AbstractSoftwareProcessSshDriver extends AbstractSoftwareProcessDriver implements NaiveScriptRunner {
public static final Logger log = LoggerFactory.getLogger(AbstractSoftwareProcessSshDriver.class);
public static final Logger logSsh = LoggerFactory.getLogger(BrooklynLogging.SSH_IO);
// we cache these for efficiency and in case the entity becomes unmanaged
private volatile String installDir;
private volatile String runDir;
private volatile String expandedInstallDir;
private final Object installDirSetupMutex = new Object();
protected volatile DownloadResolver resolver;
@Override
public void prepare() {
// Check if we should create a download resolver?
String downloadUrl = getEntity().config().get(SoftwareProcess.DOWNLOAD_URL);
if (Strings.isNonEmpty(downloadUrl)) {
resolver = Entities.newDownloader(this);
String formatString = getArchiveNameFormat();
if (Strings.isNonEmpty(formatString)) {
setExpandedInstallDir(Os.mergePaths(getInstallDir(), resolver.getUnpackedDirectoryName(String.format(formatString, getVersion()))));
} else {
setExpandedInstallDir(getInstallDir());
}
}
}
/** include this flag in newScript creation to prevent entity-level flags from being included;
* any SSH-specific flags passed to newScript override flags from the entity,
* and flags from the entity override flags on the location
* (where there aren't conflicts, flags from all three are used however) */
public static final String IGNORE_ENTITY_SSH_FLAGS = SshEffectorTasks.IGNORE_ENTITY_SSH_FLAGS.getName();
public AbstractSoftwareProcessSshDriver(EntityLocal entity, SshMachineLocation machine) {
super(entity, machine);
// FIXME this assumes we own the location, and causes warnings about configuring location after deployment;
// better would be to wrap the ssh-execution-provider to supply these flags
if (getSshFlags()!=null && !getSshFlags().isEmpty())
machine.configure(getSshFlags());
// ensure these are set using the routines below, not a global ConfigToAttributes.apply()
getInstallDir();
getRunDir();
}
/** returns location (tighten type, since we know it is an ssh machine location here) */
@Override
public SshMachineLocation getLocation() {
return (SshMachineLocation) super.getLocation();
}
protected void setInstallDir(String installDir) {
this.installDir = installDir;
entity.sensors().set(SoftwareProcess.INSTALL_DIR, installDir);
}
@Override
public String getInstallDir() {
if (installDir != null) return installDir;
String existingVal = getEntity().getAttribute(SoftwareProcess.INSTALL_DIR);
if (Strings.isNonBlank(existingVal)) { // e.g. on rebind
installDir = existingVal;
return installDir;
}
synchronized (installDirSetupMutex) {
// previously we looked at sensor value, but we shouldn't as it might have been converted from the config key value
// *before* we computed the install label, or that label may have changed since previous install; now force a recompute
setInstallLabel();
// set it null first so that we force a recompute
setInstallDir(null);
setInstallDir(Os.tidyPath(ConfigToAttributes.apply(getEntity(), SoftwareProcess.INSTALL_DIR)));
return installDir;
}
}
protected void setInstallLabel() {
if (((EntityInternal)getEntity()).config().getLocalRaw(SoftwareProcess.INSTALL_UNIQUE_LABEL).isPresentAndNonNull()) return;
getEntity().config().set(SoftwareProcess.INSTALL_UNIQUE_LABEL,
getEntity().getEntityType().getSimpleName()+
(Strings.isNonBlank(getVersion()) ? "_"+getVersion() : "")+
(Strings.isNonBlank(getInstallLabelExtraSalt()) ? "_"+getInstallLabelExtraSalt() : "") );
}
/** allows subclasses to return extra salt (ie unique hash)
* for cases where install dirs need to be distinct e.g. based on extra plugins being placed in the install dir;
* {@link #setInstallLabel()} uses entity-type simple name and version already
*
* this salt should not be too long and must not contain invalid path chars.
* a hash code of other relevant info is not a bad choice.
**/
protected String getInstallLabelExtraSalt() {
return null;
}
protected void setRunDir(String runDir) {
this.runDir = runDir;
entity.sensors().set(SoftwareProcess.RUN_DIR, runDir);
}
@Override
public String getRunDir() {
if (runDir != null) return runDir;
String existingVal = getEntity().getAttribute(SoftwareProcess.RUN_DIR);
if (Strings.isNonBlank(existingVal)) { // e.g. on rebind
runDir = existingVal;
return runDir;
}
setRunDir(Os.tidyPath(ConfigToAttributes.apply(getEntity(), SoftwareProcess.RUN_DIR)));
return runDir;
}
public void setExpandedInstallDir(String val) {
String oldVal = getEntity().getAttribute(SoftwareProcess.EXPANDED_INSTALL_DIR);
if (Strings.isNonBlank(oldVal) && !oldVal.equals(val)) {
log.info("Resetting expandedInstallDir (to "+val+" from "+oldVal+") for "+getEntity());
}
expandedInstallDir = val;
getEntity().sensors().set(SoftwareProcess.EXPANDED_INSTALL_DIR, val);
}
public String getExpandedInstallDir() {
if (expandedInstallDir != null) return expandedInstallDir;
String existingVal = getEntity().getAttribute(SoftwareProcess.EXPANDED_INSTALL_DIR);
if (Strings.isNonBlank(existingVal)) { // e.g. on rebind
expandedInstallDir = existingVal;
return expandedInstallDir;
}
String untidiedVal = ConfigToAttributes.apply(getEntity(), SoftwareProcess.EXPANDED_INSTALL_DIR);
if (Strings.isNonBlank(untidiedVal)) {
setExpandedInstallDir(Os.tidyPath(untidiedVal));
return expandedInstallDir;
} else {
throw new IllegalStateException("expandedInstallDir is null; most likely install was not called for "+getEntity());
}
}
public SshMachineLocation getMachine() { return getLocation(); }
public String getHostname() { return entity.getAttribute(Attributes.HOSTNAME); }
public String getAddress() { return entity.getAttribute(Attributes.ADDRESS); }
public String getSubnetHostname() { return entity.getAttribute(Attributes.SUBNET_HOSTNAME); }
public String getSubnetAddress() { return entity.getAttribute(Attributes.SUBNET_ADDRESS); }
protected Map getSshFlags() {
return SshEffectorTasks.getSshFlags(getEntity(), getMachine());
}
/**
* @deprecated since 0.10.0 This method will become private in a future release.
*/
@Deprecated
public int execute(String command, String summaryForLogging) {
return execute(ImmutableList.of(command), summaryForLogging);
}
/**
* @deprecated since 0.10.0 This method will become private in a future release.
*/
@Override
@Deprecated
public int execute(List script, String summaryForLogging) {
return execute(Maps.newLinkedHashMap(), script, summaryForLogging);
}
/**
* @deprecated since 0.10.0 This method will become private in a future release.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
@Deprecated
public int execute(Map flags2, List script, String summaryForLogging) {
// TODO replace with SshEffectorTasks.ssh ?; remove the use of flags
// TODO log the stdin/stdout/stderr upon error
Map flags = Maps.newLinkedHashMap();
if (!flags2.containsKey(IGNORE_ENTITY_SSH_FLAGS)) {
flags.putAll(getSshFlags());
}
flags.putAll(flags2);
Map environment = (Map) flags.get("env");
if (environment == null) {
// Important only to call getShellEnvironment() if env was not supplied; otherwise it
// could cause us to resolve config (e.g. block for attributeWhenReady) too early.
environment = getShellEnvironment();
}
if (Tasks.current()!=null) {
// attach tags here, as well as in ScriptHelper, because they may have just been read from the driver
if (environment!=null) {
Tasks.addTagDynamically(BrooklynTaskTags.tagForEnvStream(BrooklynTaskTags.STREAM_ENV, environment));
}
if (BrooklynTaskTags.stream(Tasks.current(), BrooklynTaskTags.STREAM_STDIN)==null) {
Tasks.addTagDynamically(BrooklynTaskTags.tagForStreamSoft(BrooklynTaskTags.STREAM_STDIN,
Streams.byteArrayOfString(Strings.join(script, "\n"))));
}
if (BrooklynTaskTags.stream(Tasks.current(), BrooklynTaskTags.STREAM_STDOUT)==null) {
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
Tasks.addTagDynamically(BrooklynTaskTags.tagForStreamSoft(BrooklynTaskTags.STREAM_STDOUT, stdout));
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
Tasks.addTagDynamically(BrooklynTaskTags.tagForStreamSoft(BrooklynTaskTags.STREAM_STDERR, stderr));
flags.put("out", stdout);
flags.put("err", stderr);
}
}
if (!flags.containsKey("logPrefix")) flags.put("logPrefix", ""+entity.getId()+"@"+getLocation().getDisplayName());
return getMachine().execScript(flags, summaryForLogging, script, environment);
}
@Override
public void copyPreInstallResources() {
final WithMutexes mutexSupport = getLocation().mutexes();
String mutexId = "installation lock at host";
mutexSupport.acquireMutex(mutexId, "pre-installation lock at host for files and templates");
try {
super.copyPreInstallResources();
} catch (Exception e) {
log.warn("Error copying pre-install resources", e);
throw Exceptions.propagate(e);
} finally {
mutexSupport.releaseMutex(mutexId);
}
}
@Override
public void copyInstallResources() {
final WithMutexes mutexSupport = getLocation().mutexes();
String mutexId = "installation lock at host";
mutexSupport.acquireMutex(mutexId, "installation lock at host for files and templates");
try {
super.copyInstallResources();
} catch (Exception e) {
log.warn("Error copying install resources", e);
throw Exceptions.propagate(e);
} finally {
mutexSupport.releaseMutex(mutexId);
}
}
@Override
public void copyCustomizeResources() {
final WithMutexes mutexSupport = getLocation().mutexes();
String mutexId = "installation lock at host";
mutexSupport.acquireMutex(mutexId, "installation lock at host for files and templates");
try {
super.copyCustomizeResources();
} catch (Exception e) {
log.warn("Error copying customize resources", e);
throw Exceptions.propagate(e);
} finally {
mutexSupport.releaseMutex(mutexId);
}
}
private void executeSuccessfully(ConfigKey configKey, String label) {
if(Strings.isNonBlank(getEntity().getConfig(configKey))) {
log.debug("Executing {} on entity {}", label, entity.getDisplayName());
int result = execute(ImmutableList.of(getEntity().getConfig(configKey)), label);
if (0 != result) {
log.debug("Executing {} failed with return code {}", label, result);
throw new IllegalStateException("commands for " + configKey.getName() + " failed with return code " + result);
}
}
}
@Override
public void runPreInstallCommand() {
executeSuccessfully(BrooklynConfigKeys.PRE_INSTALL_COMMAND, "running pre-install commands");
}
@Override
public void runPostInstallCommand() {
executeSuccessfully(BrooklynConfigKeys.POST_INSTALL_COMMAND, "running post-install commands");
}
@Override
public void runPreCustomizeCommand() {
executeSuccessfully(BrooklynConfigKeys.PRE_CUSTOMIZE_COMMAND, "running pre-customize commands");
}
@Override
public void runPostCustomizeCommand() {
executeSuccessfully(BrooklynConfigKeys.POST_CUSTOMIZE_COMMAND, "running post-customize commands");
}
@Override
public void runPreLaunchCommand() {
executeSuccessfully(BrooklynConfigKeys.PRE_LAUNCH_COMMAND, "running pre-launch commands");
}
@Override
public void runPostLaunchCommand() {
executeSuccessfully(BrooklynConfigKeys.POST_LAUNCH_COMMAND, "running post-launch commands");
}
/**
* The environment variables to be set when executing the commands (for install, run, check running, etc).
* @see SoftwareProcess#SHELL_ENVIRONMENT
*/
public Map getShellEnvironment() {
Map env = entity.getConfig(SoftwareProcess.SHELL_ENVIRONMENT);
ShellEnvironmentSerializer envSerializer = new ShellEnvironmentSerializer(((EntityInternal)entity).getManagementContext());
return envSerializer.serialize(env);
}
/**
* @param sshFlags Extra flags to be used when making an SSH connection to the entity's machine.
* If the map contains the key {@link #IGNORE_ENTITY_SSH_FLAGS} then only the
* given flags are used. Otherwise, the given flags are combined with (and take
* precendence over) the flags returned by {@link #getSshFlags()}.
* @param source URI of file to copy, e.g. file://.., http://.., classpath://..
* @param target Destination on server, relative to {@link #getRunDir()} if not absolute path
* @param createParentDir Whether to create the parent target directory, if it doesn't already exist
* @return The exit code of the SSH command run
*/
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public int copyResource(Map