You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2022/12/13 23:10:01 UTC

[brooklyn-server] branch master updated: fixing osgi loading issue where osgi lets resources come from other bundles than oneself

This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git


The following commit(s) were added to refs/heads/master by this push:
     new 931a6adf38 fixing osgi loading issue where osgi lets resources come from other bundles than oneself
931a6adf38 is described below

commit 931a6adf38881ab7cb1d2e7065c7f09f3dcf0b48
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Tue Dec 13 21:55:05 2022 +0000

    fixing osgi loading issue where osgi lets resources come from other bundles than oneself
    
    if package imports include its own packages and there are eg bundle v 1-A and 1-B
    
    always prefer local resources. (might not do that for classes, but the main issue was during bom processing.)
    
    also be lenient if such a problem does occur, but there are no types affected, warn rather than throw.
---
 .../catalog/internal/BasicBrooklynCatalog.java     | 14 ++--
 .../catalog/internal/CatalogInitialization.java    |  7 +-
 .../apache/brooklyn/core/mgmt/ha/OsgiManager.java  | 15 +++-
 .../BrooklynMementoPersisterToObjectStore.java     | 37 ++++++++--
 .../brooklyn/core/mgmt/rebind/RebindIteration.java | 11 +++
 .../jackson/BrooklynJacksonSerializationUtils.java |  3 +-
 .../brooklyn/core/typereg/BundleUpgradeParser.java | 80 +++++++++++++---------
 .../core/typereg/BundleUpgradeParserTest.java      | 46 +++++++++++++
 .../brooklyn/util/core/ClassLoaderUtilsTest.java   |  9 ++-
 .../brooklyn/rest/resources/ServerResource.java    |  8 ++-
 10 files changed, 175 insertions(+), 55 deletions(-)

diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
index f28f819b64..4be8e65385 100644
--- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
@@ -681,7 +681,6 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
 
         // brooklyn.libraries we treat specially, to append the list, with the child's list preferred in classloading order
         // `libraries` is supported in some places as a legacy syntax; it should always be `brooklyn.libraries` for new apps
-        // TODO in 0.8.0 require brooklyn.libraries, don't allow "libraries" on its own
         List<?> librariesAddedHereNames = MutableList.copyOf(getFirstAs(itemMetadataWithoutItemDef, List.class, "brooklyn.libraries", "libraries").orNull());
         Collection<CatalogBundle> librariesAddedHereBundles = CatalogItemDtoAbstract.parseLibraries(librariesAddedHereNames);
         
@@ -745,6 +744,12 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
 
         String id = getFirstAs(catalogMetadata, String.class, "id").orNull();
         String version = getFirstAs(catalogMetadata, String.class, "version").orNull();
+        if (log.isTraceEnabled()) log.trace("Installing "+id+":"+version);
+        if (parentMetadata.containsKey("version") && !Objects.equal(parentMetadata.get("version"), version))
+            log.warn("Bundle "+containingBundle+" declares version "+version+" for items overriding broader version "+parentMetadata.get("version"));
+        else if (!parentMetadata.containsKey("version") && containingBundle!=null && version!=null && !Objects.equal(new VersionedName("x", version).getOsgiVersionString(), containingBundle.getVersionedName().getOsgiVersionString()))
+            log.warn("Bundle "+containingBundle+" declares items at different version "+version);
+
         String symbolicName = getFirstAs(catalogMetadata, String.class, "symbolicName").orNull();
         String displayName = getFirstAs(catalogMetadata, String.class, "displayName").orNull();
         String name = getFirstAs(catalogMetadata, String.class, "name").orNull();
@@ -1154,9 +1159,9 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
     }
 
     private void collectUrlReferencedCatalogItems(String url, ManagedBundle containingBundle, List<CatalogItemDtoAbstract<?, ?>> resultLegacyFormat, Map<RegisteredType, RegisteredType> resultNewFormat, boolean requireValidation, Map<Object, Object> parentMeta, int depth, boolean force, Boolean throwOnError) {
+        log.debug("Catalog load, loading referenced BOM at "+url+" as part of "+(containingBundle==null ? "non-bundled load" : containingBundle.getVersionedName())+" ("+(resultNewFormat!=null ? resultNewFormat.size() : resultLegacyFormat!=null ? resultLegacyFormat.size() : "(unknown)")+" items before load)");
         BrooklynClassLoadingContext loader = getClassLoadingContext("catalog url reference loader", parentMeta, null);
         String yaml;
-        log.debug("Catalog load, loading referenced BOM at "+url+" as part of "+(containingBundle==null ? "non-bundled load" : containingBundle.getVersionedName())+" ("+(resultNewFormat!=null ? resultNewFormat.size() : resultLegacyFormat!=null ? resultLegacyFormat.size() : "(unknown)")+" items before load)");
         if (url.startsWith("http")) {
             // give greater visibility to these
             log.info("Loading external referenced BOM at "+url+" as part of "+(containingBundle==null ? "non-bundled load" : containingBundle.getVersionedName()));
@@ -1168,6 +1173,7 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
             throw new IllegalStateException("Remote catalog url " + url + " in "+(containingBundle==null ? "non-bundled load" : containingBundle.getVersionedName())+" can't be fetched.", e);
         }
         try {
+            if (log.isTraceEnabled()) log.trace("Loaded yaml for "+containingBundle+" from "+url+":\n"+yaml);
             collectCatalogItemsFromCatalogBomRoot("BOM expected at "+url, yaml, containingBundle, resultLegacyFormat, resultNewFormat, requireValidation, parentMeta, depth, force, throwOnError);
         } catch (Exception e) {
             Exceptions.propagateAnnotated("Error loading "+url+" as part of "+(containingBundle==null ? "non-bundled load" : containingBundle.getVersionedName()), e);
@@ -1393,7 +1399,7 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
                 }
 
                 // couldn't resolve it with the plan transformers; retry with legacy "spec" transformers.
-                // TODO this legacy path is still needed where an entity is declared with nice abbreviated 'type: xxx' syntax, not the full-camp 'services: [ { type: xxx } ]' syntax.
+                // TODO this legacy path is still needed where an entity is declared with nice abbreviated 'type: ...' syntax, not the full-camp 'services: [ { type: ... } ]' syntax.
                 // would be nice to move that logic internally to CAMP and see if we can remove this altogether.
                 // (see org.apache.brooklyn.camp.brooklyn.spi.creation.CampResolver.createEntitySpecFromServicesBlock )
                 if (format == null) {
@@ -1567,7 +1573,7 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
             String candidateYamlWithKeyAdded = null;
             boolean legacyModeForOriginalBlueprint;
             if (optionalKeyForModifyingYaml!=null) {
-                /* often when added to a catalog we simply say "type: xxx" for the definition;
+                /* often when added to a catalog we simply say "type: ..." for the definition;
                  * the services: parent key at root (or brooklyn.policies, etc) needed by the camp parser
                  * are implicit, and added here */
                 if (item.containsKey(optionalKeyForModifyingYaml)) {
diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInitialization.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInitialization.java
index 14f95d2bab..99597d5737 100644
--- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInitialization.java
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInitialization.java
@@ -292,8 +292,7 @@ public class CatalogInitialization implements ManagementContextInjectable {
         } else {
             final OsgiManager osgiManager = maybesOsgiManager.get();
             final BundleContext bundleContext = osgiManager.getFramework().getBundleContext();
-            final CatalogUpgrades catalogUpgrades =
-                    catalogUpgradeScanner.scan(osgiManager, bundleContext, rebindLogger);
+            final CatalogUpgrades catalogUpgrades = catalogUpgradeScanner.scan(osgiManager, bundleContext, rebindLogger);
             CatalogUpgrades.storeInManagementContext(catalogUpgrades, managementContext);
         }
 
@@ -563,8 +562,8 @@ public class CatalogInitialization implements ManagementContextInjectable {
             }
         });
         bundlesToRemove.forEach(b -> {
-            ManagedBundle mb = getManagementContext().getOsgiManager().get().getManagedBundle(b.getVersionedName());
-            if (b.getBundle().getState() >= Bundle.INSTALLED && b.getBundle().getState() < Bundle.STARTING) {
+            log.debug("Considering to uninstall: "+b+" / "+getManagementContext().getOsgiManager().get().getManagedBundle(b.getVersionedName()));
+            if (b.getBundle()!=null && b.getBundle().getState() >= Bundle.INSTALLED && b.getBundle().getState() < Bundle.STARTING) {
                 // we installed it, catalog did not start it, so let's uninstall it
                 OsgiBundleInstallationResult result = getManagementContext().getOsgiManager().get().uninstallUploadedBundle(b.getMetadata());
                 log.debug("Result of uninstalling "+b+" due to catalog upgrade metadata instructions: "+result);
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
index edd4349396..7482dd88a3 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
@@ -807,7 +807,12 @@ public class OsgiManager {
             try {
                 Maybe<Bundle> bundle = findBundle(osgiBundle);
                 if (bundle.isPresent()) {
-                    URL result = bundle.get().getResource(name);
+                    URL result;
+                    if (!name.endsWith(".class")) {
+                        result = bundle.get().getEntry(name);  // see comments at getResources
+                        if (result != null) return result;
+                    }
+                    result = bundle.get().getResource(name);
                     if (result!=null) return result;
                 }
             } catch (Exception e) {
@@ -821,13 +826,19 @@ public class OsgiManager {
      * @return URL's to all resources matching the given name (using {@link Bundle#getResources(String)} in the referenced osgi bundles.
      */
     public Iterable<URL> getResources(String name, Iterable<? extends OsgiBundleWithUrl> osgiBundles) {
-        Set<URL> resources = Sets.newLinkedHashSet();
+        MutableSet<URL> resources = MutableSet.of();
         for (OsgiBundleWithUrl catalogBundle : osgiBundles) {
             try {
                 Maybe<Bundle> bundle = findBundle(catalogBundle);
                 if (bundle.isPresent()) {
+                    boolean isClass = name.endsWith(".class");
+                    if (!isClass) resources.putIfNotNull(bundle.get().getEntry(name));  // files in the same bundle are not always preferred
+                    // (observed with a bundle at 1.0.0-2022 being installed when 1.0.0-SNAPSHOT is installed, the former includes its packages in import-package,
+                    // with the result that it returns the contents of the 1.0.0-SNAPSHOT bundle instead of its own contents, which is not what we want);
+                    // see also in BundleUpgradeParser
                     Enumeration<URL> result = bundle.get().getResources(name);
                     resources.addAll(Collections.list(result));
+                    if (isClass) resources.putIfNotNull(bundle.get().getEntry(name));  //class resources, for consistency and to match loadClass, we prefer osgi behaviour
                 }
             } catch (Exception e) {
                 Exceptions.propagateIfFatal(e);
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/BrooklynMementoPersisterToObjectStore.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/BrooklynMementoPersisterToObjectStore.java
index 7593b1082a..82fd7c5cda 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/BrooklynMementoPersisterToObjectStore.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/BrooklynMementoPersisterToObjectStore.java
@@ -33,6 +33,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
 
 import javax.annotation.Nullable;
 import javax.xml.xpath.XPathConstants;
@@ -364,19 +365,34 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
 
                 } else {
                     String xmlId = (String) XmlUtil.xpathHandlingIllegalChars(contents, "/" + type.toCamelCase() + "/id");
+                    String xmlUrl = getXmlValue(contents, "/" + type.toCamelCase() + "/url").orNull();
+                    String xmlJavaType = getXmlValue(contents, "/" + type.toCamelCase() + "/type").orNull();
+                    String summary = MutableList.<String>of(contentsSubpath, type.toCamelCase()).appendIfNotNull(xmlId).appendIfNotNull(xmlUrl).appendIfNotNull(xmlJavaType).stream().collect(Collectors.joining(" / "));
+
                     String safeXmlId = Strings.makeValidFilename(xmlId);
-                    if (!Objects.equal(id, safeXmlId))
-                        LOG.warn("ID mismatch on " + type.toCamelCase() + ", " + id + " from path, " + safeXmlId + " from xml");
+                    if (!Objects.equal(id, safeXmlId)) {
+                        LOG.warn("ID mismatch on " + summary + ": xml id is " + safeXmlId);
+                    }
 
+                    boolean include;
                     if (type == BrooklynObjectType.MANAGED_BUNDLE) {
-                        // TODO could R/W to cache space directly, rather than memory copy then extra file copy
+                        // could R/W to cache space directly, rather than memory copy then extra file copy
                         byte[] jarData = readBytes(contentsSubpath + ".jar");
-                        if (jarData == null) {
-                            throw new IllegalStateException("No bundle data for " + contentsSubpath);
+                        include = (jarData != null);
+                        if (!include) {
+                            LOG.warn("No JAR data for "+summary+"; assuming deprecated and not meant to be installed, so ignoring");
+                            //throw new IllegalStateException("No bundle data for " + contentsSubpath+".jar; contents:\n"+contents);
+                        } else {
+                            LOG.debug("Including bundle "+summary+", with jar data size "+jarData.length);
+                            builder.bundleJar(id, ByteSource.wrap(jarData));
                         }
-                        builder.bundleJar(id, ByteSource.wrap(jarData));
+                    } else {
+                        LOG.debug("Including item "+summary);
+                        include = true;
+                    }
+                    if (include) {
+                        builder.put(type, xmlId, contents);
                     }
-                    builder.put(type, xmlId, contents);
                 }
             }
         };
@@ -399,6 +415,13 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
         return result;
     }
 
+    private Maybe<String> getXmlValue(String xml, String path) {
+        try {
+            return Maybe.ofDisallowingNull((String) XmlUtil.xpathHandlingIllegalChars(xml, path));
+        } catch (Exception e) { Exceptions.propagateIfFatal(e); /* otherwise ignore if no url */ }
+        return Maybe.absent();
+    }
+
     private static class XPathHelper {
         private String contents;
         private String prefix;
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java
index 03d8018707..a7675af57d 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java
@@ -360,6 +360,14 @@ public abstract class RebindIteration {
                 }
             });
         }
+
+        @Override
+        public String toString() {
+            return "InstallableManagedBundleImpl{" +
+                    "memento=" + memento +
+                    ", managedBundle=" + managedBundle +
+                    '}';
+        }
     }
 
     protected void installBundlesAndRebuildCatalog() {
@@ -1137,6 +1145,9 @@ public abstract class RebindIteration {
                             logRebindingWarn("Needed rebind catalog to resolve search path entry " + searchItemId + " (now " + fixedSearchItemId + ") for " + bType.getSimpleName().toLowerCase() + " " + contextSuchAsId +
                                     ", persistence should remove this in future but future versions will not support this and definitions should be fixed");
                         } else {
+                            // could do some magic if there is a peer with the same version use it, might be handy for snapshots especially
+                            // (snapshots upgrading bundle version "*" drop the qualifier so don't replace peers;
+                            // but as soon as you have a larger datestamp, it will replace the previous ones)
                             logRebindingWarn("Could not find search path entry " + searchItemId + " for " + bType.getSimpleName().toLowerCase() + " " + contextSuchAsId + ", ignoring");
                         }
                     }
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BrooklynJacksonSerializationUtils.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BrooklynJacksonSerializationUtils.java
index ad5f0efe97..240e914b64 100644
--- a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BrooklynJacksonSerializationUtils.java
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BrooklynJacksonSerializationUtils.java
@@ -252,7 +252,8 @@ public class BrooklynJacksonSerializationUtils {
                             Object type = rm.get("type");
                             if (type instanceof String && ((String)type).startsWith("org.apache.brooklyn.camp.brooklyn.spi.dsl.")) {
                                 // should always have a brooklyn:literal on outermost type, so that even with json pass through (used for steps etc) dsl gets preserved across convertDeeply
-                                log.warn("Failed to deserialize "+result+" as DSL; will treat as map");
+                                // apart from nested DSL expressions; and unf there is no easy way to tell here, so we just log debug either way
+                                log.debug("Data "+result+" looks like DSL but has no literal entry; probably it is a nested DSL expression, or otherwise it may be left as a map after deserialization");
                             }
                         }
                     }
diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/BundleUpgradeParser.java b/core/src/main/java/org/apache/brooklyn/core/typereg/BundleUpgradeParser.java
index 89a77258e3..ea685c0704 100644
--- a/core/src/main/java/org/apache/brooklyn/core/typereg/BundleUpgradeParser.java
+++ b/core/src/main/java/org/apache/brooklyn/core/typereg/BundleUpgradeParser.java
@@ -18,26 +18,19 @@
  */
 package org.apache.brooklyn.core.typereg;
 
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Dictionary;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
-
-import javax.annotation.Nonnull;
-
+import com.google.common.annotations.Beta;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Supplier;
+import com.google.common.collect.*;
 import org.apache.brooklyn.api.catalog.CatalogItem;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
 import org.apache.brooklyn.api.typereg.RegisteredType;
 import org.apache.brooklyn.core.mgmt.ha.OsgiManager;
 import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
 import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.core.osgi.Osgis;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.osgi.VersionedName;
@@ -50,16 +43,11 @@ import org.osgi.framework.VersionRange;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.annotations.Beta;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
-import com.google.common.base.Objects;
-import com.google.common.base.Supplier;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.LinkedHashMultimap;
-import com.google.common.collect.Multimap;
+import javax.annotation.Nonnull;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
  * Internal class for parsing bundle manifests to extract their upgrade instructions.
@@ -551,16 +539,16 @@ public class BundleUpgradeParser {
         // TODO Add support for the other options described in the proposal:
         //   https://docs.google.com/document/d/1Lm47Kx-cXPLe8BO34-qrL3ZMPosuUHJILYVQUswEH6Y/edit#
         //   section "Bundle Upgrade Metadata"
-        
+
         Dictionary<String, String> headers = bundle.getHeaders();
         String upgradesForBundlesHeader = headers.get(MANIFEST_HEADER_UPGRADE_FOR_BUNDLES);
-        Multimap<VersionedName,VersionRangedName> upgradesForBundles = parseUpgradeForBundlesHeader(upgradesForBundlesHeader, bundle);
+        Multimap<VersionedName, VersionRangedName> upgradesForBundles = parseUpgradeForBundlesHeader(upgradesForBundlesHeader, bundle);
         return CatalogUpgrades.builder()
                 .removedLegacyItems(parseForceRemoveLegacyItemsHeader(headers.get(MANIFEST_HEADER_FORCE_REMOVE_LEGACY_ITEMS), bundle, typeSupplier))
                 .removedBundles(parseForceRemoveBundlesHeader(headers.get(MANIFEST_HEADER_FORCE_REMOVE_BUNDLES), bundle))
                 .upgradeBundles(upgradesForBundles)
-                .upgradeTypes(parseUpgradeForTypesHeader(headers.get(MANIFEST_HEADER_UPGRADE_FOR_TYPES), bundle, typeSupplier, 
-                    upgradesForBundlesHeader==null ? null : upgradesForBundles))
+                .upgradeTypes(parseUpgradeForTypesHeader(headers.get(MANIFEST_HEADER_UPGRADE_FOR_TYPES), bundle, typeSupplier,
+                        upgradesForBundlesHeader == null ? null : upgradesForBundles))
                 .build();
     }
 
@@ -582,6 +570,7 @@ public class BundleUpgradeParser {
             // default target is this bundle
             (i) -> { return new VersionedName(bundle); });
     }
+
     private static Multimap<VersionedName, VersionRangedName> parseUpgradeForTypesHeader(String input, Bundle bundle, Supplier<? extends Iterable<? extends RegisteredType>> typeSupplier, Multimap<VersionedName, VersionRangedName> upgradesForBundles) {
         List<String> sourceVersions = null;
         if (upgradesForBundles!=null) {
@@ -609,8 +598,35 @@ public class BundleUpgradeParser {
             (i) -> { 
                 VersionedName targetTypeAtBundleVersion = new VersionedName(i.getSymbolicName(), bundle.getVersion());
                 if (!typeSupplierNames.contains(VersionedName.toOsgiVersionedName(targetTypeAtBundleVersion))) {
-                    throw new IllegalStateException("Bundle manifest for "+bundle+" declares it upgrades "+i+" "
-                        + "but does not declare an explicit target and does not contain inferred target "+targetTypeAtBundleVersion);
+                    String error = "Bundle manifest for " + bundle + " declares it upgrades " + i + " "
+                            + "but does not declare an explicit target and does not contain inferred target " + targetTypeAtBundleVersion;
+
+                    ManagementContext mgmt = Osgis.getManagementContext();
+                    if (mgmt!=null) {
+                        List<RegisteredType> matches = MutableList.copyOf(mgmt.getTypeRegistry().getMatching(rt -> Objects.equal(i.getSymbolicName(), rt.getSymbolicName()) && i.getOsgiVersionRange().includes(rt.getVersionedName().getOsgiVersion())));
+                        if (matches.isEmpty()) {
+                            /* can happen if a bundle imports its own packages and references a path/to/bom which is available in the bundle it imports, eg v 1.0.0-A and 1.0.0-B;
+                             * curiously osgi will sometimes read the resource from the imported bundle (see OsgiManager.getResources)
+                             */
+                            log.debug(error+"; however there are no such types to be upgrades so ignoring");
+                            return null;
+                        }
+                        CatalogUpgrades previousUpgrades = CatalogUpgrades.getFromManagementContext(mgmt);
+                        if (!previousUpgrades.getUpgradesForBundle(new VersionedName(bundle)).isEmpty()) {
+                            log.debug(error+"; however this bundle has applicable upgrade instructions so ignoring");
+                            return null;
+                        }
+                        matches = matches.stream().filter(rt -> previousUpgrades.getUpgradesForType(rt.getVersionedName()).isEmpty()).collect(Collectors.toList());
+                        if (matches.isEmpty()) {
+                            log.debug(error+"; however all such types have other upgrade instructions so ignoring");
+                            return null;
+                        }
+
+                        log.debug(error+"; types we cannot upgrade: "+matches+"; types available here: "+typeSupplierNames);
+                    } else {
+                        log.debug(error + "; details: mgmt context unavailable, osgi target '" + VersionedName.toOsgiVersionedName(targetTypeAtBundleVersion) + "', supplier names: " + typeSupplierNames);
+                    }
+                    throw new IllegalStateException(error);
                 }
                 return targetTypeAtBundleVersion; 
             });
@@ -766,7 +782,9 @@ public class BundleUpgradeParser {
                     } else {
                         throw new IllegalArgumentException("Wildcard entry key must be of the form \"*\" or \"*:range\"");
                     }
-                    result.put(target, source);
+                    if (target!=null) {
+                        result.put(target, source);
+                    }
                 }
             }
         }
diff --git a/core/src/test/java/org/apache/brooklyn/core/typereg/BundleUpgradeParserTest.java b/core/src/test/java/org/apache/brooklyn/core/typereg/BundleUpgradeParserTest.java
index 1b437b397b..91c2226f7e 100644
--- a/core/src/test/java/org/apache/brooklyn/core/typereg/BundleUpgradeParserTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/typereg/BundleUpgradeParserTest.java
@@ -89,6 +89,52 @@ public class BundleUpgradeParserTest {
         assertFalse(from1.includes(Version.valueOf("0.0.0.SNAPSHOT")));
         assertFalse(from1.includes(Version.valueOf("0.1.0.SNAPSHOT")));
     }
+
+    @Test
+    public void testVersionRangesWithTimestampsAndSnapshots() throws Exception {
+        VersionRange from0lessThan1 = VersionRangedName.fromString("foo:[0,1.0.0.2022)", false).getOsgiVersionRange();
+        assertTrue(from0lessThan1.includes(Version.valueOf("0.1.0.SNAPSHOT")));
+        assertTrue(from0lessThan1.includes(Version.valueOf("1.0.0")));
+        assertTrue(from0lessThan1.includes(Version.valueOf("1.0.0.2021")));
+        assertTrue(from0lessThan1.includes(Version.valueOf(BrooklynVersionSyntax.toValidOsgiVersion("1.0.0-2021"))));
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.0.2023")));
+        assertFalse(from0lessThan1.includes(Version.valueOf(BrooklynVersionSyntax.toValidOsgiVersion("1.0.0-2023"))));
+
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.0.2022")));
+        assertFalse(from0lessThan1.includes(Version.valueOf(BrooklynVersionSyntax.toValidOsgiVersion("1.0.0-2022"))));
+
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.0.SNAPSHOT")));
+        assertFalse(from0lessThan1.includes(Version.valueOf(BrooklynVersionSyntax.toValidOsgiVersion("1.0.0-SNAPSHOT"))));
+
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.1.2023")));
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.1.SNAPSHOT")));
+
+        // note however when given * for a snapshot we strip the snapshot qualifier, and so only match up to 1.0.0)
+        from0lessThan1 = VersionRangedName.fromString("foo:[0,1.0.0.SNAPSHOT)", false).getOsgiVersionRange();
+        assertTrue(from0lessThan1.includes(Version.valueOf("0.1.0.SNAPSHOT")));
+        assertTrue(from0lessThan1.includes(Version.valueOf("1.0.0")));
+        assertTrue(from0lessThan1.includes(Version.valueOf("1.0.0.2023")));
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.0.SNAPSHOT")));
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.1.2023")));
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.1.SNAPSHOT")));
+
+        from0lessThan1 = VersionRangedName.fromString("foo:[0,1.0.0)", false).getOsgiVersionRange();
+        assertTrue(from0lessThan1.includes(Version.valueOf("0.1.0.SNAPSHOT")));
+
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.0")));
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.0.2023")));
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.0.SNAPSHOT")));
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.1.2023")));
+        assertFalse(from0lessThan1.includes(Version.valueOf("1.0.1.SNAPSHOT")));
+
+        VersionRange from1lessThan101 = VersionRangedName.fromString("foo:(1.0.0,1.0.1)", false).getOsgiVersionRange();
+        assertFalse(from1lessThan101.includes(Version.valueOf("0.1.0.SNAPSHOT")));
+        assertFalse(from1lessThan101.includes(Version.valueOf("1.0.0")));
+        assertTrue(from1lessThan101.includes(Version.valueOf("1.0.0.2023")));
+        assertTrue(from1lessThan101.includes(Version.valueOf("1.0.0.SNAPSHOT")));
+        assertFalse(from1lessThan101.includes(Version.valueOf("1.0.1")));
+        assertFalse(from1lessThan101.includes(Version.valueOf("1.0.1.2023")));
+    }
     
     @Test
     public void testParseSingleQuotedVal() throws Exception {
diff --git a/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java b/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java
index db90666722..b868c46764 100644
--- a/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java
+++ b/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java
@@ -144,7 +144,7 @@ public class ClassLoaderUtilsTest {
 
         BundledName resource = new BundledName(classname).toResource();
         BundledName bn = new BundledName(resource.bundle, resource.version, "/" + resource.name);
-        Asserts.assertSize(cluEntity.getResources(bn.toString()), 1);
+        Asserts.assertSize(cluEntity.getResources(bn.toString()), 2);  // should no longer mask the other available option
     }
 
 
@@ -354,12 +354,15 @@ public class ClassLoaderUtilsTest {
         String bundledResource = resource.toString();
         URL resourceUrl = cl.getResource(resource.name);
         assertEquals(clu.getResource(bundledResource), resourceUrl);
-        assertEquals(clu.getResources(bundledResource), ImmutableList.of(resourceUrl), "Loading with "+clu);
+        Iterable<URL> allMatches = clu.getResources(bundledResource);
+        assertEquals(allMatches.iterator().next(), resourceUrl, "Loading with "+clu);
+        // may have others, eg if the bundle provides it via multiple sources
 
         BundledName rootResource = new BundledName(resource.bundle, resource.version, "/" + resource.name);
         String rootBundledResource = rootResource.toString();
         assertEquals(clu.getResource(rootBundledResource), resourceUrl);
-        assertEquals(clu.getResources(rootBundledResource), ImmutableList.of(resourceUrl));
+        allMatches = clu.getResources(rootBundledResource);
+        assertEquals(allMatches.iterator().next(), resourceUrl);
     }
 
     private void assertLoadFails(String bundledClassName, ClassLoaderUtils... clua) {
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/ServerResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/ServerResource.java
index 7614e2c157..00c3cf029f 100644
--- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/ServerResource.java
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/ServerResource.java
@@ -590,6 +590,8 @@ public class ServerResource extends AbstractBrooklynRestResource implements Serv
         File tempZipFile = null;
         String unzippedPath = null;
         try {
+            log.debug("Importing persisted state via REST API, starting");
+
             // create a temporary archive where persistence data supplied is written to
             tempZipFile = File.createTempFile("persistence-import",null);
             Files.write(tempZipFile.toPath(), persistenceData, StandardOpenOption.TRUNCATE_EXISTING);
@@ -623,15 +625,15 @@ public class ServerResource extends AbstractBrooklynRestResource implements Serv
                 ManagedBundleMemento bundleMemento = mementoManifest.getBundle(bundleJar.getKey());
                 ManagedBundle b = RebindIteration.newManagedBundle(bundleMemento);
                 bundles.put(b.getVersionedName(), new RebindIteration.InstallableManagedBundleImpl(bundleMemento, b));
-                log.debug("Installing "+bundleMemento+" for "+b+" as part of persisted state import");
+                log.debug("Importing persisted state, preparing bundle "+b+" from "+bundleMemento);
             }
             CatalogInitialization.PersistedCatalogState persistedCatalogState = new CatalogInitialization.PersistedCatalogState(bundles, Collections.emptySet());
             CatalogInitialization.RebindLogger rebindLogger = new CatalogInitialization.RebindLogger() {
                 @Override public void debug(String message, Object... args) {
-                    log.debug(message, args);
+                    log.debug("Importing persisted state: "+message, args);
                 }
                 @Override public void info(String message, Object... args) {
-                    log.warn(message, args);
+                    log.warn("Importing persisted state: "+message, args);
                 }
             };
             mgmtInternal().getCatalogInitialization().installPersistedBundles(persistedCatalogState, null, ((RebindManagerImpl)mgmt().getRebindManager()).newExceptionHandler(), rebindLogger);