blob: a8c6600f064453e68012e8765cce5a6d3b53aded [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.brooklynnode;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.InetAddress;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.drivers.downloads.DownloadSubstituters;
import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.ExistingFileBehaviour;
import org.apache.brooklyn.entity.java.JavaSoftwareProcessSshDriver;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.file.ArchiveBuilder;
import org.apache.brooklyn.util.core.file.ArchiveUtils;
import org.apache.brooklyn.util.core.internal.ssh.SshTool;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.net.Networking;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.Strings;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
public class BrooklynNodeSshDriver extends JavaSoftwareProcessSshDriver implements BrooklynNodeDriver {
public BrooklynNodeSshDriver(BrooklynNodeImpl entity, SshMachineLocation machine) {
super(entity, machine);
}
@Override
public BrooklynNodeImpl getEntity() {
return (BrooklynNodeImpl) super.getEntity();
}
public String getBrooklynHome() {
return getRunDir();
}
@Override
protected String getLogFileLocation() {
return Os.mergePathsUnix(getRunDir(), "console");
}
protected String getPidFile() {
return Os.mergePathsUnix(getRunDir(), "pid_java");
}
@Override
protected String getInstallLabelExtraSalt() {
String downloadUrl = entity.getConfig(BrooklynNode.DOWNLOAD_URL);
String uploadUrl = entity.getConfig(BrooklynNode.DISTRO_UPLOAD_URL);
if (Objects.equal(downloadUrl, BrooklynNode.DOWNLOAD_URL.getConfigKey().getDefaultValue()) &&
Objects.equal(uploadUrl, BrooklynNode.DISTRO_UPLOAD_URL.getDefaultValue())) {
// if both are at the default value, then no salt
return null;
}
return Identifiers.makeIdFromHash(Objects.hashCode(downloadUrl, uploadUrl));
}
@Override
public void preInstall() {
resolver = Entities.newDownloader(this);
String subpath = entity.getConfig(BrooklynNode.SUBPATH_IN_ARCHIVE);
if (subpath==null) {
// assume the dir name is `basename-VERSION` where download link is `basename-VERSION-dist.tar.gz`
String uploadUrl = entity.getConfig(BrooklynNode.DISTRO_UPLOAD_URL);
String origDownloadName = uploadUrl;
if (origDownloadName==null)
origDownloadName = entity.getAttribute(BrooklynNode.DOWNLOAD_URL);
if (origDownloadName!=null) {
// BasicDownloadResolver makes it crazy hard to get the template-evaluated value of DOWNLOAD_URL
origDownloadName = DownloadSubstituters.substitute(origDownloadName, DownloadSubstituters.getBasicEntitySubstitutions(this));
origDownloadName = Urls.decode(origDownloadName);
origDownloadName = Urls.getBasename(origDownloadName);
String downloadName = origDownloadName;
downloadName = Strings.removeFromEnd(downloadName, ".tar.gz");
downloadName = Strings.removeFromEnd(downloadName, ".tgz");
downloadName = Strings.removeFromEnd(downloadName, ".zip");
if (!downloadName.equals(origDownloadName)) {
downloadName = Strings.removeFromEnd(downloadName, "-dist");
subpath = downloadName;
}
}
}
if (subpath==null) subpath = format("brooklyn-dist-%s", getVersion());
setExpandedInstallDir(Os.mergePaths(getInstallDir(), resolver.getUnpackedDirectoryName(subpath)));
}
@Override
public void clearInstallDir() {
super.setInstallDir(null);
super.setExpandedInstallDir(null);
}
@Override
public void install() {
String uploadUrl = entity.getConfig(BrooklynNode.DISTRO_UPLOAD_URL);
// Need to explicitly give file, because for snapshot URLs you don't get a clean filename from the URL.
// This filename is used to generate the first URL to try: [BROOKLYN_VERSION_BELOW]
// file://$HOME/.brooklyn/repository/BrooklynNode/0.8.1-SNAPSHOT/brooklynnode-0.8.0-snapshot.tar.gz
// (DOWNLOAD_URL overrides this and has a default which comes from maven)
List<String> urls = resolver.getTargets();
String saveAs = resolver.getFilename();
newScript("createInstallDir")
.body.append("mkdir -p "+getInstallDir())
.failOnNonZeroResultCode()
.execute();
List<String> commands = Lists.newArrayList();
// TODO use machine.installTo ... but that only works w a single location currently
if (uploadUrl != null) {
// Only upload if not already installed
boolean exists = newScript("checkIfInstalled")
.body.append("cd "+getInstallDir(), "test -f BROOKLYN")
.execute() == 0;
if (!exists) {
InputStream distroStream = resource.getResourceFromUrl(uploadUrl);
getMachine().copyTo(distroStream, getInstallDir()+"/"+saveAs);
}
} else {
commands.addAll(BashCommands.commandsToDownloadUrlsAs(urls, saveAs));
}
commands.add(BashCommands.INSTALL_TAR);
commands.add("tar xzfv " + saveAs);
newScript(INSTALLING).
failOnNonZeroResultCode().
body.append(commands).execute();
}
@Override
public void customize() {
newScript(CUSTOMIZING)
.failOnNonZeroResultCode()
.body.append(
// workaround for AMP distribution placing everything in the root of this archive, but
// brooklyn distribution placing everything in a subdirectory: check to see if subdirectory
// with expected name exists; symlink to same directory if it doesn't
// FIXME remove when all downstream usages don't use this
format("[ -d %1$s ] || ln -s . %1$s", getExpandedInstallDir(), getExpandedInstallDir()),
// previously we only copied bin,conf and set BROOKLYN_HOME to the install dir;
// but that does not play nicely if installing dists other than brooklyn
// (such as what is built by our artifact)
format("cp -R %s/* .", getExpandedInstallDir()),
"mkdir -p ./lib/")
.execute();
SshMachineLocation machine = getMachine();
BrooklynNode entity = getEntity();
String brooklynGlobalPropertiesRemotePath = entity.getConfig(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_REMOTE_PATH);
String brooklynGlobalPropertiesContents = entity.getConfig(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_CONTENTS);
String brooklynGlobalPropertiesUri = entity.getConfig(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_URI);
String brooklynLocalPropertiesRemotePath = processTemplateContents(entity.getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_REMOTE_PATH));
String brooklynLocalPropertiesContents = entity.getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_CONTENTS);
String brooklynLocalPropertiesUri = entity.getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_URI);
String brooklynCatalogRemotePath = entity.getConfig(BrooklynNode.BROOKLYN_CATALOG_REMOTE_PATH);
String brooklynCatalogContents = entity.getConfig(BrooklynNode.BROOKLYN_CATALOG_CONTENTS);
String brooklynCatalogUri = entity.getConfig(BrooklynNode.BROOKLYN_CATALOG_URI);
// Override the ~/.brooklyn/brooklyn.properties if required
if (brooklynGlobalPropertiesContents != null || brooklynGlobalPropertiesUri != null) {
ExistingFileBehaviour onExisting = entity.getConfig(BrooklynNode.ON_EXISTING_PROPERTIES_FILE);
Integer checkExists = DynamicTasks.queue(SshEffectorTasks.ssh("ls \""+brooklynGlobalPropertiesRemotePath+"\"").allowingNonZeroExitCode()).get();
boolean doUpload = true;
if (checkExists==0) {
switch (onExisting) {
case USE_EXISTING: doUpload = false; break;
case OVERWRITE: break;
case DO_NOT_USE:
throw new IllegalStateException("Properties file "+brooklynGlobalPropertiesContents+" already exists and "+
"even though it is not being used, content for it was supplied");
case FAIL:
throw new IllegalStateException("Properties file "+brooklynGlobalPropertiesContents+" already exists and "+
BrooklynNode.ON_EXISTING_PROPERTIES_FILE+" response is to fail");
default:
throw new IllegalStateException("Properties file "+brooklynGlobalPropertiesContents+" already exists and "+
BrooklynNode.ON_EXISTING_PROPERTIES_FILE+" response "+onExisting+" is unknown");
}
}
if (onExisting==ExistingFileBehaviour.DO_NOT_USE) {
log.warn("Global properties supplied when told not to use them; no global properties exists, so it will be installed, but it will not be used.");
}
if (doUpload)
uploadFileContents(brooklynGlobalPropertiesContents, brooklynGlobalPropertiesUri, brooklynGlobalPropertiesRemotePath);
}
// Upload a local-brooklyn.properties if required
if (brooklynLocalPropertiesContents != null || brooklynLocalPropertiesUri != null) {
uploadFileContents(brooklynLocalPropertiesContents, brooklynLocalPropertiesUri, brooklynLocalPropertiesRemotePath);
}
// Override the ~/.brooklyn/catalog.xml if required
if (brooklynCatalogContents != null || brooklynCatalogUri != null) {
uploadFileContents(brooklynCatalogContents, brooklynCatalogUri, brooklynCatalogRemotePath);
}
// Copy additional resources to the server
for (Map.Entry<String,String> entry : getEntity().getAttribute(BrooklynNode.COPY_TO_RUNDIR).entrySet()) {
Map<String, String> substitutions = ImmutableMap.of("RUN", getRunDir());
String localResource = entry.getKey();
String remotePath = entry.getValue();
String resolvedRemotePath = remotePath;
for (Map.Entry<String,String> substitution : substitutions.entrySet()) {
String key = substitution.getKey();
String val = substitution.getValue();
resolvedRemotePath = resolvedRemotePath.replace("${"+key+"}", val).replace("$"+key, val);
}
machine.copyTo(MutableMap.of("permissions", "0600"), resource.getResourceFromUrl(localResource), resolvedRemotePath);
}
for (String entry : getEntity().getClasspath()) {
// If a local folder, then create archive from contents first
if (Urls.isDirectory(entry)) {
File jarFile = ArchiveBuilder.jar().addDirContentsAt(new File(entry), "").create();
entry = jarFile.getAbsolutePath();
}
// Determine filename
String destFile = entry.contains("?") ? entry.substring(0, entry.indexOf('?')) : entry;
destFile = destFile.substring(destFile.lastIndexOf('/') + 1);
ArchiveUtils.deploy(MutableMap.<String, Object>of(), entry, machine, getRunDir(), Os.mergePaths(getRunDir(), "lib"), destFile);
}
String cmd = entity.getConfig(BrooklynNode.EXTRA_CUSTOMIZATION_SCRIPT);
if (Strings.isNonBlank(cmd)) {
DynamicTasks.queueIfPossible( SshEffectorTasks.ssh(cmd).summary("Bespoke BrooklynNode customization script")
.requiringExitCodeZero() )
.orSubmitAndBlock(getEntity());
}
}
@SuppressWarnings("deprecation")
@Override
public void launch() {
String app = getEntity().getAttribute(BrooklynNode.APP);
String locations = getEntity().getAttribute(BrooklynNode.LOCATIONS);
boolean hasLocalBrooklynProperties = getEntity().getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_CONTENTS) != null || getEntity().getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_URI) != null;
String localBrooklynPropertiesPath = processTemplateContents(getEntity().getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_REMOTE_PATH));
InetAddress bindAddress = getEntity().getAttribute(BrooklynNode.WEB_CONSOLE_BIND_ADDRESS);
InetAddress publicAddress = getEntity().getAttribute(BrooklynNode.WEB_CONSOLE_PUBLIC_ADDRESS);
String cmd = entity.getConfig(BrooklynNode.LAUNCH_COMMAND);
if (Strings.isBlank(cmd)) cmd = "./bin/brooklyn";
cmd = "nohup " + cmd + " launch";
if (app != null) {
cmd += " --app "+app;
}
if (locations != null) {
cmd += " --locations "+locations;
}
if (entity.getConfig(BrooklynNode.ON_EXISTING_PROPERTIES_FILE)==ExistingFileBehaviour.DO_NOT_USE) {
cmd += " --noGlobalBrooklynProperties";
}
if (hasLocalBrooklynProperties) {
cmd += " --localBrooklynProperties "+localBrooklynPropertiesPath;
}
Integer webPort = null;
if (getEntity().isHttpProtocolEnabled("http")) {
webPort = getEntity().getAttribute(BrooklynNode.HTTP_PORT);
Networking.checkPortsValid(ImmutableMap.of("webPort", webPort));
} else if (getEntity().isHttpProtocolEnabled("https")) {
webPort = getEntity().getAttribute(BrooklynNode.HTTPS_PORT);
Networking.checkPortsValid(ImmutableMap.of("webPort", webPort));
}
if (webPort!=null) {
cmd += " --port "+webPort;
} else if (getEntity().getEnabledHttpProtocols().isEmpty()) {
// TODO sensors will probably not work in this mode
cmd += " --noConsole";
} else {
throw new IllegalStateException("Unknown web protocol in "+BrooklynNode.ENABLED_HTTP_PROTOCOLS+" "
+ "("+getEntity().getEnabledHttpProtocols()+"); expecting 'http' or 'https'");
}
if (bindAddress != null) {
cmd += " --bindAddress "+bindAddress.getHostAddress();
}
if (publicAddress != null) {
cmd += " --publicAddress "+publicAddress.getHostName();
}
if (getEntity().getAttribute(BrooklynNode.NO_WEB_CONSOLE_AUTHENTICATION)) {
cmd += " --noConsoleSecurity";
}
if (Strings.isNonBlank(getEntity().getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS))) {
cmd += " "+getEntity().getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS);
}
cmd += format(" >> %s/console 2>&1 </dev/null &", getRunDir());
log.info("Starting brooklyn on {} using command {}", getMachine(), cmd);
// relies on brooklyn script creating pid file
newScript(ImmutableMap.of("usePidFile",
entity.getConfig(BrooklynNode.LAUNCH_COMMAND_CREATES_PID_FILE) ? false : getPidFile()),
LAUNCHING).
body.append(
format("export BROOKLYN_CLASSPATH=%s", getRunDir()+"/lib/\"*\""),
format("export BROOKLYN_HOME=%s", getBrooklynHome()),
format(cmd)
).failOnNonZeroResultCode().execute();
}
@Override
public boolean isRunning() {
Map<String,String> flags = ImmutableMap.of("usePidFile", getPidFile());
int result = newScript(flags, CHECK_RUNNING).execute();
return result == 0;
}
@Override
public void stop() {
Map<String,String> flags = ImmutableMap.of("usePidFile", getPidFile());
newScript(flags, STOPPING).execute();
}
@Override
public void kill() {
Map<String,String> flags = ImmutableMap.of("usePidFile", getPidFile());
newScript(flags, KILLING).execute();
}
@Override
public Map<String, String> getShellEnvironment() {
Map<String, String> orig = super.getShellEnvironment();
String origClasspath = orig.get("CLASSPATH");
String newClasspath = (origClasspath == null ? "" : origClasspath+":") +
getRunDir()+"/conf/" + ":" +
getRunDir()+"/lib/\"*\"";
Map<String,String> results = new LinkedHashMap<String,String>();
results.putAll(orig);
results.put("BROOKLYN_CLASSPATH", newClasspath);
results.put("BROOKLYN_HOME", getBrooklynHome());
results.put("RUN", getRunDir());
return results;
}
private void uploadFileContents(String contents, String alternativeUri, String remotePath) {
checkNotNull(remotePath, "remotePath");
SshMachineLocation machine = getMachine();
String tempRemotePath = String.format("%s/upload.tmp", getRunDir());
if (contents == null && alternativeUri == null) {
throw new IllegalStateException("No contents supplied for file " + remotePath);
}
InputStream stream = contents != null
? new ByteArrayInputStream(contents.getBytes())
: resource.getResourceFromUrl(alternativeUri);
Map<String, String> flags = MutableMap.of(SshTool.PROP_PERMISSIONS.getName(), "0600");
machine.copyTo(flags, stream, tempRemotePath);
newScript(CUSTOMIZING)
.failOnNonZeroResultCode()
.body.append(
format("mkdir -p %s", remotePath.subSequence(0, remotePath.lastIndexOf("/"))),
format("cp -p %s %s", tempRemotePath, remotePath),
format("rm -f %s", tempRemotePath))
.execute();
}
}