org.apache.brooklyn.launcher.osgi.OsgiLauncherImpl Maven / Gradle / Ivy
Show all versions of brooklyn-karaf-init Show documentation
/*
* Copyright 2016 The Apache Software Foundation.
*
* Licensed 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.osgi;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Iterables;
import org.apache.brooklyn.api.framework.FrameworkLookup;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
import org.apache.brooklyn.core.BrooklynVersionService;
import org.apache.brooklyn.core.catalog.internal.CatalogInitialization;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.mgmt.ha.OsgiManager;
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.launcher.common.BasicLauncher;
import org.apache.brooklyn.launcher.common.BrooklynPropertiesFactoryHelper;
import org.apache.brooklyn.rest.BrooklynWebConfig;
import org.apache.brooklyn.rest.security.provider.BrooklynUserWithRandomPasswordSecurityProvider;
import org.apache.brooklyn.rest.security.provider.DelegatingSecurityProvider;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.RuntimeInterruptedException;
import org.apache.brooklyn.util.javalang.Threads;
import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.CountdownTimer;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.startlevel.FrameworkStartLevel;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Initializer for brooklyn-core when running in an OSGi environment.
*/
public class OsgiLauncherImpl extends BasicLauncher implements OsgiLauncher {
private static final Logger LOG = LoggerFactory.getLogger(OsgiLauncherImpl.class);
public static final String BROOKLYN_CONFIG_PID = "brooklyn";
/** Takes a JSON representation or comma separated list of OSGi filter expressions (cf {@link BundleContext#getServiceReferences(String, String)},
* where each filter must be satisfied before the catalog will be initialized.
*
* For example:
* (&(osgi.service.blueprint.compname=customBundleResolver))
to wait for a service registered via a blueprint with component name 'customBundleResolver', or
* or
* ["(&(osgi.service.blueprint.compname=customBundleResolver))","(&(osgi.service.blueprint.compname=customTypePlanTransformer))"]
* to wait for two services registered, 'customBundleResolver' and 'customTypePlanTransformer'
.
*
* This can be required where some catalog or rebind items depend on services installed during startup.
* */
public static final String BROOKLYN_OSGI_DEPENDENCIES_SERVICES_FILTERS = "brooklyn.osgi.dependencies.services.filters";
public static final String BROOKLYN_OSGI_DEPENDENCIES_SERVICES_TIMEOUT = "brooklyn.osgi.dependencies.services.timeout";
public static final String BROOKLYN_OSGI_STARTLEVEL_POSTINIT = "brooklyn.osgi.startlevel.postinit";
private Object reloadLock = new Object();
private BrooklynVersionService brooklynVersion;
private String globalBrooklynProperties;
private String localBrooklynProperties;
private String defaultCatalogLocation;
private ConfigurationAdmin configAdmin;
private ConfigSupplier configSupplier;
public static String getSoftwareName() {
String name = System.getProperty("karaf.name");
if (Strings.isNonBlank(name)) return name;
return "apache-brooklyn";
}
@Override
public OsgiLauncherImpl startPartOne() {
// make sure brooklyn-core bundle is started
brooklynVersion.getVersion();
if (getManagementContext()!=null) ((ManagementContextInternal)getManagementContext()).getBrooklynProperties().put(OsgiManager.OSGI_STARTUP_COMPLETE, false);
Configuration brooklynConfig = getConfiguration(BROOKLYN_CONFIG_PID);
// Note that this doesn't check whether the files exist, just that there are potential alternative sources for configuration.
if (brooklynConfig == null && Strings.isEmpty(globalBrooklynProperties) && Strings.isEmpty(localBrooklynProperties)) {
LOG.warn("Config Admin PID '" + BROOKLYN_CONFIG_PID + "' not found, not using external configuration. Create a brooklyn.cfg file in etc folder.");
}
configSupplier = new ConfigSupplier(brooklynConfig);
BrooklynPropertiesFactoryHelper helper = new BrooklynPropertiesFactoryHelper(
globalBrooklynProperties, localBrooklynProperties, configSupplier);
setBrooklynPropertiesBuilder(helper.createPropertiesBuilder());
return super.startPartOne();
}
private Configuration getConfiguration(String configPid) {
String filter = '(' + Constants.SERVICE_PID + '=' + configPid + ')';
Configuration[] configs;
try {
configs = configAdmin.listConfigurations(filter);
} catch (InvalidSyntaxException | IOException e) {
throw Exceptions.propagate(e);
}
if (configs != null && configs.length > 0) {
return configs[0];
} else {
return null;
}
}
@Override
protected void initManagementContext() {
super.initManagementContext();
((ManagementContextInternal)getManagementContext()).getBrooklynProperties().put(OsgiManager.OSGI_STARTUP_COMPLETE, false);
}
// init-method can't find the start method for some reason, provide an alternative.
@Override
public void initOsgi() {
if (getManagementContext()!=null) ((ManagementContextInternal)getManagementContext()).getBrooklynProperties().put(OsgiManager.OSGI_STARTUP_COMPLETE, false);
synchronized (reloadLock) {
final Stopwatch startupTimer = Stopwatch.createStarted();
BrooklynShutdownHooks.resetShutdownFlag();
LOG.info(getSoftwareName()+" initialization starting, installing catalog from "+defaultCatalogLocation);
catalogInitialization(new CatalogInitialization(String.format("file:%s", defaultCatalogLocation)));
startPartOne();
startupTimer.stop();
LOG.info(getSoftwareName()+" initialization - part one complete, took {}", startupTimer.toString());
}
}
Thread fallbackThread = null;
@Override
public void startOsgi() {
final Bundle bundle = FrameworkUtil.getBundle(this.getClass());
Framework f = (Framework) bundle.getBundleContext().getBundle(0);
int startLevel = f.adapt(FrameworkStartLevel.class).getStartLevel();
if (areServiceDependenciesReady(bundle, bundle + " bundle activation")) {
LOG.debug("Starting OSGi catalog/rebind (no service dependencies or all already satisfied, on bundle activation)");
doStartOsgiAfterBundlesRefreshed();
} else {
ServiceListener sl[] = { null };
sl[0] = new ServiceListener() {
@Override
public void serviceChanged(ServiceEvent event) {
if (areServiceDependenciesReady(bundle, "ServiceEvent[" + event.getServiceReference() + " / " + event.getType() + " - " + event.getSource() + "]")) {
LOG.debug("Starting OSGi catalog/rebind - all service dependencies satisfied");
bundle.getBundleContext().removeServiceListener(sl[0]);
if (fallbackThread!=null) {
fallbackThread.interrupt();
}
new Thread(() -> {
// do this in a new thread as it takes a while, shouldn't run in service listener thread
doStartOsgiAfterBundlesRefreshed();
}).start();
}
}
};
Duration timeout;
try {
timeout = Duration.parse((String) getBrooklynProperties().getConfig(BROOKLYN_OSGI_DEPENDENCIES_SERVICES_TIMEOUT));
} catch (Exception e) {
throw Exceptions.propagateAnnotated("Invalid duration specified for '"+BROOKLYN_OSGI_DEPENDENCIES_SERVICES_TIMEOUT+"'", e);
}
fallbackThread = new Thread(() -> {
CountdownTimer timer = timeout==null ? null : CountdownTimer.newInstanceStarted(timeout);
try {
if (timeout!=null) {
LOG.debug("Service dependencies timeout detected as " + timeout + "; will start catalog/rebind after that delay if service dependencies not fulfilled sooner");
} else {
LOG.debug("No timeout specified for '"+BROOKLYN_OSGI_DEPENDENCIES_SERVICES_TIMEOUT+"'; will wait indefinitely for service dependencies");
}
int iteration = 0;
do {
if (iteration>0) {
LOG.debug("Still waiting on service dependencies, " + iteration+"m so far" +
(timer!=null ? ", "+timer.getDurationRemaining()+" remaining until timeout" : " (will wait indefinitely)"));
}
Duration wait = Duration.ONE_MINUTE;
if (timer != null && timer.getDurationRemaining().isShorterThan(wait)) {
wait = timer.getDurationRemaining();
}
Time.sleep(wait);
iteration++;
} while (timer==null || timer.isNotExpired());
} catch (RuntimeInterruptedException e) {
// normal - thread aborted probably because service dependencies fulfilled
LOG.debug("OsgiLauncher fallback thread interrupted, probably because service dependencies fulfilled in another thread where OSGi catalog/rebind start should be occurring (or due to shutdown)");
Thread.interrupted();
return;
} finally {
fallbackThread = null;
}
LOG.warn("Starting OSGi catalog/rebind due to timeout waiting for service dependencies");
bundle.getBundleContext().removeServiceListener(sl[0]);
new Thread(() -> {
// do this in a new thread so it isn't interrupted if fallback[0] is interrupted
if (!doStartOsgiAfterBundlesRefreshed()) {
LOG.debug("Did not start OSGi after timeout; already started");
}
}).start();
});
bundle.getBundleContext().addServiceListener(sl[0]);
fallbackThread.start();
}
}
private boolean areServiceDependenciesReady(Bundle bundle, String context) {
Object deps = getBrooklynProperties().getConfig(BROOKLYN_OSGI_DEPENDENCIES_SERVICES_FILTERS);
if (deps!=null) {
List items = JavaStringEscapes.unwrapJsonishListStringIfPossible(deps.toString());
LOG.debug("OSGi catalog/rebind service dependency check, on " + context + ": " + items);
for (String item: items) {
ServiceReference r1[];
try {
r1 = bundle.getBundleContext().getServiceReferences((String)null, item);
} catch (Exception e) {
throw Exceptions.propagateAnnotated("Error getting service references satisfying '"+item+"'", e);
}
if (r1 == null || r1.length == 0) {
LOG.debug("OSGi catalog/rebind blocked, service dependency not yet fulfilled (will keep listening for services): '" + item + "'");
return false;
}
}
return true;
} else {
LOG.debug("No service dependencies specified, on " + context);
return true;
}
}
AtomicBoolean startedOsgiAfterBundlesRefreshed = new AtomicBoolean();
private boolean doStartOsgiAfterBundlesRefreshed() {
if (startedOsgiAfterBundlesRefreshed.getAndSet(true)) {
LOG.debug("OSGi catalog/rebind already started when invoked a second time", new Throwable("Trace for unexpected redundant OSGi catalog/rebind start"));
return false;
}
doStartOsgi();
Object newStartLevelS = null;
try {
newStartLevelS = getBrooklynProperties().getConfig(BROOKLYN_OSGI_STARTLEVEL_POSTINIT);
if (newStartLevelS==null || Strings.isBlank(""+newStartLevelS)) {
LOG.debug("No change required to OSGi start-level after OSGi catalog/rebind ("+BROOKLYN_OSGI_STARTLEVEL_POSTINIT+" unset)");
} else {
FrameworkStartLevel fsl = FrameworkUtil.getBundle(this.getClass()).getBundleContext().getBundle(0).adapt(FrameworkStartLevel.class);
int newStartLevel = TypeCoercions.coerce(newStartLevelS, Integer.class);
if (fsl.getStartLevel() notes = MutableList.of();
ManagementNodeState state = getManagementContext().getHighAvailabilityManager().getNodeState();
if (ManagementNodeState.MASTER.equals(state)) {
interstitial.append("RUNNING");
int appCount = getManagementContext().getApplications().size();
if (appCount>0) {
notes.add(appCount+" app" + (appCount!=1 ? "s" : "") +" under management");
}
int catalogSize = Iterables.size(getManagementContext().getTypeRegistry().getAll());
notes.add(catalogSize+" items in type registry");
// interstitial.append(" - "+notes);
// URL would be nice but we don't know that
} else if (ManagementNodeState.FAILED.equals(state)) {
interstitial.append("FAILED - see logs for more information");
} else {
interstitial.append("in state: "+state.name().toUpperCase());
}
LOG.info(interstitial.toString().toLowerCase()+(notes.isEmpty() ? "" : " - "+notes.stream().collect(Collectors.joining(", "))));
startupTimer.stop();
LOG.info(getSoftwareName()+" initialization - part two complete, took {}", startupTimer.toString());
new Thread(()-> {
// wait 50ms to let other log messages show above us
Time.sleep(Duration.millis(50));
LOG.info("======== " +interstitial+" ========");
}, getSoftwareName()+"-start-completion").start();
}
}
@Override
public void destroyOsgi() {
LOG.debug("Notified of system shutdown, calling shutdown hooks");
Threads.runShutdownHooks();
Thread t = fallbackThread;
if (t!=null) {
LOG.debug("Notified of system shutdown, cancelling service dependencies fallback thread");
t.interrupt();
}
}
@Override
protected void startingUp() {
super.startingUp();
ManagementContext managementContext = getManagementContext();
BrooklynProperties brooklynProperties = (BrooklynProperties) managementContext.getConfig();
if (BrooklynWebConfig.hasNoSecurityOptions(brooklynProperties)) {
LOG.info("No security provider options specified. Define a security provider or users to prevent a random password being created and logged.");
// Deprecated in 0.11.0. Add to release notes and remove in next release.
brooklynProperties.put(
BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE,
new BrooklynUserWithRandomPasswordSecurityProvider(managementContext));
managementContext.getScratchpad().put(
BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE,
new BrooklynUserWithRandomPasswordSecurityProvider(managementContext));
}
}
public void updateProperties(Map, ?> props) {
synchronized (reloadLock) {
LOG.info("Updating brooklyn config because of config admin changes.");
configSupplier.update(props);
getManagementContext().reloadBrooklynProperties();
}
}
public void setBrooklynVersion(BrooklynVersionService brooklynVersion) {
this.brooklynVersion = brooklynVersion;
}
public void setPersistenceLocation(@Nullable String persistenceLocationSpec) {
persistenceLocation(persistenceLocationSpec);
}
public void setBrooklynProperties(BrooklynProperties brooklynProperties) {
brooklynProperties(brooklynProperties);
}
public void setIgnorePersistenceErrors(boolean ignorePersistenceErrors) {
ignorePersistenceErrors(ignorePersistenceErrors);
}
public void setIgnoreCatalogErrors(boolean ignoreCatalogErrors) {
ignoreCatalogErrors(ignoreCatalogErrors);
}
public void setIgnoreAppErrors(boolean ignoreAppErrors) {
ignoreAppErrors(ignoreAppErrors);
}
public void setPersistMode(PersistMode persistMode) {
persistMode(persistMode);
}
public void setHighAvailabilityMode(HighAvailabilityMode highAvailabilityMode) {
highAvailabilityMode(highAvailabilityMode);
}
public void setPersistenceDir(@Nullable String persistenceDir) {
persistenceDir(persistenceDir);
}
public void setPersistPeriod(String persistPeriod) {
persistPeriod(Duration.parse(persistPeriod));
}
public void setHaHeartbeatTimeout(String val) {
haHeartbeatTimeout(Duration.parse(val));
}
public void setStartBrooklynNode(boolean val) {
startBrooklynNode(val);
}
public void setHaHeartbeatPeriod(String val) {
haHeartbeatPeriod(Duration.parse(val));
}
public void setCopyPersistedState(String destinationDir) {
copyPersistedState(destinationDir);
}
public void setConfigAdmin(ConfigurationAdmin configAdmin) {
this.configAdmin = configAdmin;
}
public void setGlobalBrooklynProperties(String globalBrooklynProperties) {
this.globalBrooklynProperties = globalBrooklynProperties;
}
public void setLocalBrooklynProperties(String localBrooklynProperties) {
this.localBrooklynProperties = localBrooklynProperties;
}
public void setDefaultCatalogLocation(String defaultCatalogLocation) {
this.defaultCatalogLocation = defaultCatalogLocation;
}
}