/* * 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.core.catalog.internal; import java.io.File; import java.util.Collection; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.brooklyn.api.catalog.CatalogItem; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState; import org.apache.brooklyn.core.catalog.CatalogLoadMode; import org.apache.brooklyn.core.mgmt.ManagementContextInjectable; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.server.BrooklynServerConfig; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.util.core.flags.TypeCoercions; 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.guava.Maybe; import org.apache.brooklyn.util.javalang.JavaClassNames; import org.apache.brooklyn.util.os.Os; import org.apache.brooklyn.util.text.Strings; import com.google.common.annotations.Beta; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @Beta public class CatalogInitialization implements ManagementContextInjectable { /* A1) if not persisting, go to B1 A2) if --catalog-reset, delete persisted catalog items A3) if there is a persisted catalog (and it wasn't not deleted by A2), read it and go to C1 A4) go to B1 B1) look for --catalog-initial, if so read it, then go to C1 B2) look for BrooklynServerConfig.BROOKLYN_CATALOG_URL, if so, read it, supporting YAML or XML (warning if XML), then go to C1 B3) look for ~/.brooklyn/catalog.bom, if exists, read it then go to C1 B4) look for ~/.brooklyn/brooklyn.xml, if exists, warn, read it then go to C1 B5) read all classpath://brooklyn/default.catalog.bom items, if they exist (and for now they will) B6) go to C1 C1) if --catalog-add, read and add those items D1) if persisting, read the rest of the persisted items (entities etc) */ private static final Logger log = LoggerFactory.getLogger(CatalogInitialization.class); private String initialUri; private boolean reset; private String additionsUri; private boolean force; private boolean disallowLocal = false; private List<Function<CatalogInitialization, Void>> callbacks = MutableList.of(); private boolean /** has run an unofficial initialization (i.e. an early load, triggered by an early read of the catalog) */ hasRunUnofficialInitialization = false, /** has run an official initialization, but it is not a permanent one (e.g. during a hot standby mode, or a run failed) */ hasRunTransientOfficialInitialization = false, /** has run an official initialization which is permanent (node is master, and the new catalog is now set) */ hasRunFinalInitialization = false; /** is running a populate method; used to prevent recursive loops */ private boolean isPopulating = false; private ManagementContext managementContext; private boolean isStartingUp = false; private boolean failOnStartupErrors = false; private Object populatingCatalogMutex = new Object(); public CatalogInitialization(String initialUri, boolean reset, String additionUri, boolean force) { this.initialUri = initialUri; this.reset = reset; this.additionsUri = additionUri; this.force = force; } public CatalogInitialization() { this(null, false, null, false); } @Override public void setManagementContext(ManagementContext managementContext) { Preconditions.checkNotNull(managementContext, "management context"); if (this.managementContext!=null && managementContext!=this.managementContext) throw new IllegalStateException("Cannot switch management context, from "+this.managementContext+" to "+managementContext); this.managementContext = managementContext; } /** Called by the framework to set true while starting up, and false afterwards, * in order to assist in appropriate logging and error handling. */ public void setStartingUp(boolean isStartingUp) { this.isStartingUp = isStartingUp; } public void setFailOnStartupErrors(boolean startupFailOnCatalogErrors) { this.failOnStartupErrors = startupFailOnCatalogErrors; } public CatalogInitialization addPopulationCallback(Function<CatalogInitialization, Void> callback) { callbacks.add(callback); return this; } public ManagementContext getManagementContext() { return Preconditions.checkNotNull(managementContext, "management context has not been injected into "+this); } public boolean isInitialResetRequested() { return reset; } /** Returns true if the canonical initialization has completed, * that is, an initialization which is done when a node is rebinded as master * (or an initialization done by the startup routines when not running persistence); * see also {@link #hasRunAnyInitialization()}. */ public boolean hasRunFinalInitialization() { return hasRunFinalInitialization; } /** Returns true if an official initialization has run, * even if it was a transient run, e.g. so that the launch sequence can tell whether rebind has triggered initialization */ public boolean hasRunOfficialInitialization() { return hasRunFinalInitialization || hasRunTransientOfficialInitialization; } /** Returns true if the initializer has run at all, * including transient initializations which might be needed before a canonical becoming-master rebind, * for instance because the catalog is being accessed before loading rebind information * (done by {@link #populateUnofficial(BasicBrooklynCatalog)}) */ public boolean hasRunAnyInitialization() { return hasRunFinalInitialization || hasRunTransientOfficialInitialization || hasRunUnofficialInitialization; } /** makes or updates the mgmt catalog, based on the settings in this class * @param nodeState the management node for which this is being read; if master, then we expect this run to be the last one, * and so subsequent applications should ignore any initialization data (e.g. on a subsequent promotion to master, * after a master -> standby -> master cycle) * @param needsInitialItemsLoaded whether the catalog needs the initial items loaded * @param needsAdditionalItemsLoaded whether the catalog needs the additions loaded * @param optionalExcplicitItemsForResettingCatalog * if supplied, the catalog is reset to contain only these items, before calling any other initialization * for use primarily when rebinding */ public void populateCatalog(ManagementNodeState nodeState, boolean needsInitialItemsLoaded, boolean needsAdditionsLoaded, Collection<CatalogItem<?, ?>> optionalExcplicitItemsForResettingCatalog) { if (log.isDebugEnabled()) { String message = "Populating catalog for "+nodeState+", needsInitial="+needsInitialItemsLoaded+", needsAdditional="+needsAdditionsLoaded+", explicitItems="+(optionalExcplicitItemsForResettingCatalog==null ? "null" : optionalExcplicitItemsForResettingCatalog.size())+"; from "+JavaClassNames.callerNiceClassAndMethod(1); if (!ManagementNodeState.isHotProxy(nodeState)) { log.debug(message); } else { // in hot modes, make this message trace so we don't get too much output then log.trace(message); } } synchronized (populatingCatalogMutex) { try { if (hasRunFinalInitialization() && (needsInitialItemsLoaded || needsAdditionsLoaded)) { // if we have already run "final" then we should only ever be used to reset the catalog, // not to initialize or add; e.g. we are being given a fixed list on a subsequent master rebind after the initial master rebind log.warn("Catalog initialization called to populate initial, even though it has already run the final official initialization"); } isPopulating = true; BasicBrooklynCatalog catalog = (BasicBrooklynCatalog) managementContext.getCatalog(); if (!catalog.getCatalog().isLoaded()) { catalog.load(); } else { if (needsInitialItemsLoaded && hasRunAnyInitialization()) { // an indication that something caused it to load early; not severe, but unusual if (hasRunTransientOfficialInitialization) { log.debug("Catalog initialization now populating, but has noted a previous official run which was not final (probalby loaded while in a standby mode, or a previous run failed); overwriting any items installed earlier"); } else { log.warn("Catalog initialization now populating, but has noted a previous unofficial run (it may have been an early web request); overwriting any items installed earlier"); } catalog.reset(ImmutableList.<CatalogItem<?,?>>of()); } } populateCatalogImpl(catalog, needsInitialItemsLoaded, needsAdditionsLoaded, optionalExcplicitItemsForResettingCatalog); if (nodeState == ManagementNodeState.MASTER) { // TODO ideally this would remain false until it has *persisted* the changed catalog; // if there is a subsequent startup failure the forced additions will not be persisted, // but nor will they be loaded on a subsequent run. // callers will have to restart a brooklyn, or reach into this class to change this field, // or (recommended) manually adjust the catalog. // TODO also, if a node comes up in standby, the addition might not take effector for a while // // however since these options are mainly for use on the very first brooklyn run, it's not such a big deal; // once up and running the typical way to add items is via the REST API hasRunFinalInitialization = true; } } catch (Throwable e) { log.warn("Error populating catalog (rethrowing): "+e, e); throw Exceptions.propagate(e); } finally { if (!hasRunFinalInitialization) { hasRunTransientOfficialInitialization = true; } isPopulating = false; } } } private void populateCatalogImpl(BasicBrooklynCatalog catalog, boolean needsInitialItemsLoaded, boolean needsAdditionsLoaded, Collection<CatalogItem<?, ?>> optionalItemsForResettingCatalog) { applyCatalogLoadMode(); if (optionalItemsForResettingCatalog!=null) { catalog.reset(optionalItemsForResettingCatalog); } if (needsInitialItemsLoaded) { populateInitial(catalog); } if (needsAdditionsLoaded) { populateAdditions(catalog); populateViaCallbacks(catalog); } } private enum PopulateMode { YAML, XML, AUTODETECT } protected void populateInitial(BasicBrooklynCatalog catalog) { if (disallowLocal) { if (!hasRunFinalInitialization()) { log.debug("CLI initial catalog not being read when local catalog load mode is disallowed."); } return; } // B1) look for --catalog-initial, if so read it, then go to C1 // B2) look for BrooklynServerConfig.BROOKLYN_CATALOG_URL, if so, read it, supporting YAML or XML (warning if XML), then go to C1 // B3) look for ~/.brooklyn/catalog.bom, if exists, read it then go to C1 // B4) look for ~/.brooklyn/brooklyn.xml, if exists, warn, read it then go to C1 // B5) read all classpath://brooklyn/default.catalog.bom items, if they exist (and for now they will) // B6) go to C1 if (initialUri!=null) { populateInitialFromUri(catalog, initialUri, PopulateMode.AUTODETECT); return; } String catalogUrl = managementContext.getConfig().getConfig(BrooklynServerConfig.BROOKLYN_CATALOG_URL); if (Strings.isNonBlank(catalogUrl)) { populateInitialFromUri(catalog, catalogUrl, PopulateMode.AUTODETECT); return; } catalogUrl = Os.mergePaths(BrooklynServerConfig.getMgmtBaseDir( managementContext.getConfig() ), "catalog.bom"); if (new File(catalogUrl).exists()) { populateInitialFromUri(catalog, new File(catalogUrl).toURI().toString(), PopulateMode.YAML); return; } catalogUrl = Os.mergePaths(BrooklynServerConfig.getMgmtBaseDir( managementContext.getConfig() ), "catalog.xml"); if (new File(catalogUrl).exists()) { populateInitialFromUri(catalog, new File(catalogUrl).toURI().toString(), PopulateMode.XML); return; } // otherwise look for for classpath:/brooklyn/default.catalog.bom -- // there is one on the classpath which says to scan, and provides a few templates; // if one is supplied by user in the conf/ dir that will override the item from the classpath // (TBD - we might want to scan for all such bom's?) catalogUrl = "classpath:/brooklyn/default.catalog.bom"; if (new ResourceUtils(this).doesUrlExist(catalogUrl)) { populateInitialFromUri(catalog, catalogUrl, PopulateMode.YAML); return; } log.info("No catalog found on classpath or specified; catalog will not be initialized."); return; } private void populateInitialFromUri(BasicBrooklynCatalog catalog, String catalogUrl, PopulateMode mode) { log.debug("Loading initial catalog from {}", catalogUrl); Exception problem = null; Object result = null; String contents = null; try { contents = new ResourceUtils(this).getResourceAsString(catalogUrl); } catch (Exception e) { Exceptions.propagateIfFatal(e); if (problem==null) problem = e; } if (contents!=null && (mode==PopulateMode.YAML || mode==PopulateMode.AUTODETECT)) { // try YAML first try { catalog.reset(MutableList.<CatalogItem<?,?>>of()); result = catalog.addItems(contents); } catch (Exception e) { Exceptions.propagateIfFatal(e); if (problem==null) problem = e; } } if (result==null && contents!=null && (mode==PopulateMode.XML || mode==PopulateMode.AUTODETECT)) { // then try XML try { populateInitialFromUriXml(catalog, catalogUrl, contents); // clear YAML problem problem = null; } catch (Exception e) { Exceptions.propagateIfFatal(e); if (problem==null) problem = e; } } if (result!=null) { log.debug("Loaded initial catalog from {}: {}", catalogUrl, result); } if (problem!=null) { log.warn("Error importing catalog from " + catalogUrl + ": " + problem, problem); // TODO inform mgmt of error } } // deprecated XML format @SuppressWarnings("deprecation") private void populateInitialFromUriXml(BasicBrooklynCatalog catalog, String catalogUrl, String contents) { CatalogDto dto = CatalogDto.newDtoFromXmlContents(contents, catalogUrl); if (dto!=null) { catalog.reset(dto); } } boolean hasRunAdditions = false; protected void populateAdditions(BasicBrooklynCatalog catalog) { if (Strings.isNonBlank(additionsUri)) { if (disallowLocal) { if (!hasRunAdditions) { log.warn("CLI additions supplied but not supported when catalog load mode disallows local loads; ignoring."); } return; } if (!hasRunAdditions) { log.debug("Adding to catalog from CLI: "+additionsUri+" (force: "+force+")"); } Iterable<? extends CatalogItem<?, ?>> items = catalog.addItems( new ResourceUtils(this).getResourceAsString(additionsUri), force); if (!hasRunAdditions) log.debug("Added to catalog from CLI: "+items); else log.debug("Added to catalog from CLI: count "+Iterables.size(items)); hasRunAdditions = true; } } protected void populateViaCallbacks(BasicBrooklynCatalog catalog) { for (Function<CatalogInitialization, Void> callback: callbacks) callback.apply(this); } private Object setFromCLMMutex = new Object(); private boolean setFromCatalogLoadMode = false; /** @deprecated since introduced in 0.7.0, only for legacy compatibility with * {@link CatalogLoadMode} {@link BrooklynServerConfig#CATALOG_LOAD_MODE}, * allowing control of catalog loading from a brooklyn property */ @Deprecated public void applyCatalogLoadMode() { synchronized (setFromCLMMutex) { if (setFromCatalogLoadMode) return; setFromCatalogLoadMode = true; Maybe<Object> clmm = ((ManagementContextInternal)managementContext).getConfig().getConfigRaw(BrooklynServerConfig.CATALOG_LOAD_MODE, false); if (clmm.isAbsent()) return; org.apache.brooklyn.core.catalog.CatalogLoadMode clm = TypeCoercions.coerce(clmm.get(), org.apache.brooklyn.core.catalog.CatalogLoadMode.class); log.warn("Legacy CatalogLoadMode "+clm+" set: applying, but this should be changed to use new CLI --catalogXxx commands"); switch (clm) { case LOAD_BROOKLYN_CATALOG_URL: reset = true; break; case LOAD_BROOKLYN_CATALOG_URL_IF_NO_PERSISTED_STATE: // now the default break; case LOAD_PERSISTED_STATE: disallowLocal = true; break; } } } /** Creates the catalog based on parameters set here, if not yet loaded, * but ignoring persisted state and warning if persistence is on and we are starting up * (because the official persistence is preferred and the catalog will be subsequently replaced); * for use when the catalog is accessed before persistence is completed. * <p> * This method is primarily used during testing, which in many cases does not enforce the full startup order * and which wants a local catalog in any case. It may also be invoked if a client requests the catalog * while the server is starting up. */ public void populateUnofficial(BasicBrooklynCatalog catalog) { synchronized (populatingCatalogMutex) { // check isPopulating in case this method gets called from inside another populate call if (hasRunAnyInitialization() || isPopulating) return; log.debug("Populating catalog unofficially ("+catalog+")"); isPopulating = true; try { if (isStartingUp) { log.warn("Catalog access requested when not yet initialized; populating best effort rather than through recommended pathway. Catalog data may be replaced subsequently."); } populateCatalogImpl(catalog, true, true, null); } finally { hasRunUnofficialInitialization = true; isPopulating = false; } } } public void handleException(Throwable throwable, Object details) { if (throwable instanceof InterruptedException) throw new RuntimeInterruptedException((InterruptedException) throwable); if (throwable instanceof RuntimeInterruptedException) throw (RuntimeInterruptedException) throwable; String throwableText = Exceptions.collapseText(throwable); log.error("Error loading catalog item '"+details+"': "+throwableText); log.debug("Trace for error loading catalog item '"+details+"': "+throwableText, throwable); // TODO give more detail when adding ((ManagementContextInternal)getManagementContext()).errors().add(throwable); if (isStartingUp && failOnStartupErrors) { throw new FatalRuntimeException("Unable to load catalog item '"+details+"': "+throwableText, throwable); } } }