blob: fe17b89ccd3fe379180e2a924a51d12358d40aec [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.launcher;
import java.io.Closeable;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import org.apache.brooklyn.api.entity.Application;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.PortRange;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.core.config.ConfigPredicates;
import org.apache.brooklyn.core.config.Sanitizer;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.location.PortRanges;
import org.apache.brooklyn.core.mgmt.internal.BrooklynShutdownHooks;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.mgmt.persist.PersistMode;
import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
import org.apache.brooklyn.entity.brooklynnode.LocalBrooklynNode;
import org.apache.brooklyn.entity.software.base.SoftwareProcess;
import org.apache.brooklyn.launcher.common.BasicLauncher;
import org.apache.brooklyn.launcher.common.BrooklynPropertiesFactoryHelper;
import org.apache.brooklyn.launcher.config.StopWhichAppsOnShutdown;
import org.apache.brooklyn.rest.BrooklynWebConfig;
import org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider;
import org.apache.brooklyn.rest.security.provider.BrooklynUserWithRandomPasswordSecurityProvider;
import org.apache.brooklyn.core.mgmt.ShutdownHandler;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.FatalRuntimeException;
import org.apache.brooklyn.util.exceptions.RuntimeInterruptedException;
import org.apache.brooklyn.util.net.Networking;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Stopwatch;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
/**
* Example usage is:
* * <pre>
* {@code
* BrooklynLauncher launcher = BrooklynLauncher.newInstance()
* .application(new WebClusterDatabaseExample().appDisplayName("Web-cluster example"))
* .location("localhost")
* .start();
*
* Entities.dumpInfo(launcher.getApplications());
* </pre>
*/
public class BrooklynLauncher extends BasicLauncher<BrooklynLauncher> {
private static final Logger LOG = LoggerFactory.getLogger(BrooklynLauncher.class);
/** Creates a configurable (fluent API) launcher for use starting the web console and Brooklyn applications. */
public static BrooklynLauncher newInstance() {
return new BrooklynLauncher();
}
private boolean startWebApps = true;
private PortRange port = null;
private Boolean useHttps = null;
private InetAddress bindAddress = null;
private InetAddress publicAddress = null;
private List<WebAppContextProvider> webApps = new LinkedList<>();
private Map<String, ?> webconsoleFlags = Maps.newLinkedHashMap();
private Boolean skipSecurityFilter = null;
private boolean ignoreWebErrors = false;
private StopWhichAppsOnShutdown stopWhichAppsOnShutdown = StopWhichAppsOnShutdown.THESE_IF_NOT_PERSISTED;
private ShutdownHandler shutdownHandler;
private Function<ManagementContext,Void> customizeManagement = null;
private volatile BrooklynWebServer webServer;
private String globalBrooklynPropertiesFile = Os.mergePaths(Os.home(), ".brooklyn", "brooklyn.properties");
private String localBrooklynPropertiesFile;
private Supplier<Map<?, ?>> brooklynPropertiesSupplier;
public BrooklynServerDetails getServerDetails() {
if (!isStarted()) throw new IllegalStateException("Cannot retrieve server details until started");
return new BrooklynServerDetails(webServer, getManagementContext());
}
/**
* Specifies whether the launcher will start the Brooklyn web console
* (and any additional webapps specified); default true.
*/
public BrooklynLauncher webconsole(boolean startWebApps) {
this.startWebApps = startWebApps;
return this;
}
public BrooklynLauncher installSecurityFilter(Boolean val) {
this.skipSecurityFilter = (val == null ? null : !val);
return this;
}
/**
* As {@link #webconsolePort(PortRange)} taking a single port
*/
public BrooklynLauncher webconsolePort(int port) {
return webconsolePort(PortRanges.fromInteger(port));
}
/**
* As {@link #webconsolePort(PortRange)} taking a string range
*/
public BrooklynLauncher webconsolePort(String port) {
if (port==null) return webconsolePort((PortRange)null);
return webconsolePort(PortRanges.fromString(port));
}
/**
* Specifies the port where the web console (and any additional webapps specified) will listen;
* default (null) means "8081+" being the first available >= 8081 (or "8443+" for https).
*/
public BrooklynLauncher webconsolePort(PortRange port) {
this.port = port;
return this;
}
/**
* Specifies whether the webconsole should use https.
*/
public BrooklynLauncher webconsoleHttps(Boolean useHttps) {
this.useHttps = useHttps;
return this;
}
/**
* Specifies the NIC where the web console (and any additional webapps specified) will be bound;
* default 0.0.0.0, unless no security is specified (e.g. users) in which case it is localhost.
*/
public BrooklynLauncher bindAddress(InetAddress bindAddress) {
this.bindAddress = bindAddress;
return this;
}
/**
* Specifies the address that the management context's REST API will be available on. Defaults
* to {@link #bindAddress} if it is not 0.0.0.0.
* @see #bindAddress(java.net.InetAddress)
*/
public BrooklynLauncher publicAddress(InetAddress publicAddress) {
this.publicAddress = publicAddress;
return this;
}
/**
* Specifies additional flags to be passed to {@link BrooklynWebServer}.
*/
public BrooklynLauncher webServerFlags(Map<String,?> webServerFlags) {
this.webconsoleFlags = webServerFlags;
return this;
}
/**
* Specifies an additional webapp to host on the webconsole port.
* @param contextPath The context path (e.g. "/hello", or equivalently just "hello") where the webapp will be hosted.
* "/" will override the brooklyn console webapp.
* @param warUrl The URL from which the WAR should be loaded, supporting classpath:// protocol in addition to file:// and http(s)://.
*/
public BrooklynLauncher webapp(String contextPath, String warUrl) {
webApps.add(new WebAppContextProvider(contextPath, warUrl));
return this;
}
/**
* @see #webapp(String, String)
*/
public BrooklynLauncher webapp(WebAppContextProvider contextProvider) {
webApps.add(contextProvider);
return this;
}
public BrooklynLauncher ignoreWebErrors(boolean ignoreWebErrors) {
this.ignoreWebErrors = ignoreWebErrors;
return this;
}
public BrooklynLauncher stopWhichAppsOnShutdown(StopWhichAppsOnShutdown stopWhich) {
this.stopWhichAppsOnShutdown = stopWhich;
return this;
}
public BrooklynLauncher customizeManagement(Function<ManagementContext,Void> customizeManagement) {
this.customizeManagement = customizeManagement;
return this;
}
public BrooklynLauncher shutdownOnExit(boolean val) {
LOG.warn("Call to deprecated `shutdownOnExit`", new Throwable("source of deprecated call"));
stopWhichAppsOnShutdown = StopWhichAppsOnShutdown.THESE_IF_NOT_PERSISTED;
return this;
}
/**
* A listener to call when the user requests a shutdown (i.e. through the REST API)
*/
public BrooklynLauncher shutdownHandler(ShutdownHandler shutdownHandler) {
this.shutdownHandler = shutdownHandler;
return this;
}
@Override
protected void initManagementContext() {
initBrooklynPropertiesBuilder();
super.initManagementContext();
if (customizeManagement!=null) {
customizeManagement.apply(getManagementContext());
}
}
protected void initBrooklynPropertiesBuilder() {
if (getBrooklynPropertiesBuilder() == null) {
setBrooklynPropertiesBuilder(
new BrooklynPropertiesFactoryHelper(
globalBrooklynPropertiesFile,
localBrooklynPropertiesFile,
getBrooklynProperties(),
getBrooklynPropertiesSupplier())
.createPropertiesBuilder());
}
}
private Supplier<Map<?, ?>> getBrooklynPropertiesSupplier() {
return brooklynPropertiesSupplier;
}
@Override
protected void startingUp() {
super.startingUp();
// Start webapps as soon as mgmt context available -- can use them to detect progress of other processes
if (startWebApps) {
try {
startWebApps();
} catch (Exception e) {
handleSubsystemStartupError(ignoreWebErrors, "core web apps", e);
}
}
}
protected void startWebApps() {
ManagementContext managementContext = getManagementContext();
BrooklynProperties brooklynProperties = (BrooklynProperties) managementContext.getConfig();
String securityProvider = managementContext.getConfig().getConfig(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME);
// The security provider will let anyone in, but still require a password to be entered.
// Skip password request dialog if we know the provider will let users through.
boolean anyoneSecurityProvider = AnyoneSecurityProvider.class.getName().equals(securityProvider);
boolean noSecurityOptions = BrooklynWebConfig.hasNoSecurityOptions(managementContext.getConfig());
boolean skipSecurity = Boolean.TRUE.equals(skipSecurityFilter) || anyoneSecurityProvider || noSecurityOptions;
// No security options in properties and no command line options overriding.
if (Boolean.TRUE.equals(skipSecurityFilter)) {
if (bindAddress == null) {
LOG.info("Starting Brooklyn web-console with security explicitly disabled, on loopback because no bind address specified");
bindAddress = Networking.LOOPBACK;
} else {
LOG.info("Starting Brooklyn web-console with security explicitly disabled, on bind address {}", bindAddress.getHostAddress());
}
} else if (anyoneSecurityProvider) {
String bindAddressMsg = (bindAddress == null ? "<any>" : bindAddress.getHostAddress());
LOG.info("Starting Brooklyn web-console with AnyoneSecurityProvider (no authentication), on bind address {}", bindAddressMsg);
} else if (noSecurityOptions) {
String bindAddressMsg = (bindAddress == null ? "<any>" : bindAddress.getHostAddress());
LOG.info("Starting Brooklyn web-console with no security options (defaulting to no authentication), on bind address {}", bindAddressMsg);
} else {
String bindAddressMsg = (bindAddress == null ? "<any>" : bindAddress.getHostAddress());
Map<?,?> securityProps = brooklynProperties.submap(ConfigPredicates.nameStartsWith(BrooklynWebConfig.BASE_NAME_SECURITY)).asMapWithStringKeys();
LOG.debug("Starting Brooklyn (bind address {}), using security properties: {}", bindAddressMsg, Sanitizer.sanitize(securityProps));
}
if (bindAddress == null) bindAddress = Networking.ANY_NIC;
LOG.debug("Starting Brooklyn web-console with bindAddress "+bindAddress+" and properties "+brooklynProperties);
try {
webServer = new BrooklynWebServer(webconsoleFlags, managementContext);
webServer.setBindAddress(bindAddress);
webServer.setPublicAddress(publicAddress);
if (port!=null) webServer.setPort(port);
if (useHttps!=null) webServer.setHttpsEnabled(useHttps);
webServer.setShutdownHandler(shutdownHandler);
webServer.putAttributes(brooklynProperties);
webServer.skipSecurity(skipSecurity);
for (WebAppContextProvider webapp : webApps) {
webServer.addWar(webapp);
}
webServer.start();
} catch (Exception e) {
LOG.warn("Failed to start Brooklyn web-console (rethrowing): " + Exceptions.collapseText(e));
throw new FatalRuntimeException("Failed to start Brooklyn web-console: " + Exceptions.collapseText(e), e);
}
}
@Override
protected void startBrooklynNode() {
if (webServer == null || !startWebApps) {
LOG.info("Skipping BrooklynNode entity creation, BrooklynWebServer not running");
return;
}
super.startBrooklynNode();
}
protected EntitySpec<LocalBrooklynNode> customizeBrooklynNodeSpec(EntitySpec<LocalBrooklynNode> brooklynNodeSpec) {
return brooklynNodeSpec
.configure(SoftwareProcess.RUN_DIR, System.getenv("ROOT"))
.configure(SoftwareProcess.INSTALL_DIR, System.getenv("BROOKLYN_HOME"))
.configure(BrooklynNode.ENABLED_HTTP_PROTOCOLS, ImmutableList.of(webServer.getHttpsEnabled() ? "https" : "http"))
.configure(webServer.getHttpsEnabled() ? BrooklynNode.HTTPS_PORT : BrooklynNode.HTTP_PORT, PortRanges.fromInteger(webServer.getActualPort()))
.configure(BrooklynNode.WEB_CONSOLE_BIND_ADDRESS, bindAddress)
.configure(BrooklynNode.WEB_CONSOLE_PUBLIC_ADDRESS, publicAddress)
.configure(BrooklynNode.NO_WEB_CONSOLE_AUTHENTICATION, Boolean.TRUE.equals(skipSecurityFilter));
}
protected void startApps() {
if ((stopWhichAppsOnShutdown==StopWhichAppsOnShutdown.ALL) ||
(stopWhichAppsOnShutdown==StopWhichAppsOnShutdown.ALL_IF_NOT_PERSISTED && getPersistMode()==PersistMode.DISABLED)) {
BrooklynShutdownHooks.invokeStopAppsOnShutdown(getManagementContext());
}
for (Application app : getApplications()) {
if (app instanceof Startable) {
if ((stopWhichAppsOnShutdown==StopWhichAppsOnShutdown.THESE) ||
(stopWhichAppsOnShutdown==StopWhichAppsOnShutdown.THESE_IF_NOT_PERSISTED && getPersistMode()==PersistMode.DISABLED)) {
BrooklynShutdownHooks.invokeStopOnShutdown(app);
}
}
}
super.startApps();
}
/**
* Terminates this launch, but does <em>not</em> stop the applications (i.e. external processes
* are left running, etc). However, by terminating the management console the brooklyn applications
* become unusable.
*/
public void terminate() {
if (!isStarted()) return; // no-op
if (webServer != null) {
try {
webServer.stop();
} catch (Exception e) {
LOG.warn("Error stopping web-server; continuing with termination", e);
}
}
ManagementContext managementContext = getManagementContext();
// TODO Do we want to do this as part of managementContext.terminate, so after other threads are terminated etc?
// Otherwise the app can change between this persist and the terminate.
if (getPersistMode() != PersistMode.DISABLED) {
try {
Stopwatch stopwatch = Stopwatch.createStarted();
if (managementContext.getHighAvailabilityManager() != null && managementContext.getHighAvailabilityManager().getPersister() != null) {
managementContext.getHighAvailabilityManager().getPersister().waitForWritesCompleted(Duration.TEN_SECONDS);
}
managementContext.getRebindManager().waitForPendingComplete(Duration.TEN_SECONDS, true);
LOG.info("Finished waiting for persist; took "+Time.makeTimeStringRounded(stopwatch));
} catch (RuntimeInterruptedException e) {
Thread.currentThread().interrupt(); // keep going with shutdown
LOG.warn("Persistence interrupted during shutdown: "+e, e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // keep going with shutdown
LOG.warn("Persistence interrupted during shutdown: "+e, e);
} catch (TimeoutException e) {
LOG.warn("Timeout after 10 seconds waiting for persistence to write all data; continuing");
}
}
if (managementContext instanceof ManagementContextInternal) {
((ManagementContextInternal)managementContext).terminate();
}
for (Location loc : getLocations()) {
if (loc instanceof Closeable) {
Streams.closeQuietly((Closeable)loc);
}
}
}
public BrooklynLauncher globalBrooklynPropertiesFile(String file) {
globalBrooklynPropertiesFile = file;
return this;
}
public BrooklynLauncher localBrooklynPropertiesFile(String file) {
localBrooklynPropertiesFile = file;
return this;
}
/**
* Poperties returned by the supplier have less precedence (get overwritten) by global & local properties
*/
public BrooklynLauncher brooklynPropertiesSupplier(Supplier<Map<?, ?>> brooklynPropertiesSupplier) {
this.brooklynPropertiesSupplier = brooklynPropertiesSupplier;
return this;
}
}