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 2017/10/28 10:02:57 UTC

[3/7] brooklyn-server git commit: PR #867: incorporate review comments

PR #867: incorporate review comments

And better handle pre-existing bundles.


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/85196899
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/85196899
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/85196899

Branch: refs/heads/master
Commit: 85196899f1a7b151767623f3f735c6d155dd5e28
Parents: 2a539c0
Author: Aled Sage <al...@gmail.com>
Authored: Tue Oct 24 13:50:01 2017 +0100
Committer: Aled Sage <al...@gmail.com>
Committed: Fri Oct 27 13:49:55 2017 +0100

----------------------------------------------------------------------
 .../rebind/mementos/ManagedBundleMemento.java   |   5 +
 .../camp/brooklyn/AbstractYamlTest.java         |   2 +-
 .../catalog/CatalogOsgiYamlEntityTest.java      |   2 +-
 .../catalog/CatalogOsgiYamlVersioningTest.java  |   2 +-
 .../brooklyn/test/lite/CampYamlLiteTest.java    |   2 +-
 .../catalog/internal/BasicBrooklynCatalog.java  |   2 +-
 .../core/mgmt/ha/OsgiArchiveInstaller.java      | 241 +++++++++-----
 .../core/mgmt/rebind/RebindIteration.java       |   2 +-
 .../rebind/dto/BasicManagedBundleMemento.java   |  20 +-
 .../core/typereg/BasicManagedBundle.java        |  20 +-
 .../AbstractBrooklynLauncherRebindTest.java     |  30 +-
 .../BrooklynLauncherRebindCatalogOsgiTest.java  | 321 ++++++++++++++++---
 12 files changed, 494 insertions(+), 155 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/api/src/main/java/org/apache/brooklyn/api/mgmt/rebind/mementos/ManagedBundleMemento.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/org/apache/brooklyn/api/mgmt/rebind/mementos/ManagedBundleMemento.java b/api/src/main/java/org/apache/brooklyn/api/mgmt/rebind/mementos/ManagedBundleMemento.java
index 7e487ff..ffcd783 100644
--- a/api/src/main/java/org/apache/brooklyn/api/mgmt/rebind/mementos/ManagedBundleMemento.java
+++ b/api/src/main/java/org/apache/brooklyn/api/mgmt/rebind/mementos/ManagedBundleMemento.java
@@ -18,6 +18,8 @@
  */
 package org.apache.brooklyn.api.mgmt.rebind.mementos;
 
+import javax.annotation.Nullable;
+
 import com.google.common.io.ByteSource;
 
 public interface ManagedBundleMemento extends Memento {
@@ -27,6 +29,9 @@ public interface ManagedBundleMemento extends Memento {
 
     String getUrl();
     
+    @Nullable
+    String getChecksum();
+    
     ByteSource getJarContent();
     void setJarContent(ByteSource byteSource);
 

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlTest.java
index 3d4bbd2..091598f 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlTest.java
@@ -263,7 +263,7 @@ public abstract class AbstractYamlTest {
             File bf = bundleMaker.createTempZip("test", MutableMap.of(
                 new ZipEntry(BasicBrooklynCatalog.CATALOG_BOM), new ByteArrayInputStream(catalogYaml.getBytes())));
             ReferenceWithError<OsgiBundleInstallationResult> b = ((ManagementContextInternal)mgmt).getOsgiManager().get().installDeferredStart(
-                new BasicManagedBundle(bundleName.getSymbolicName(), bundleName.getVersionString(), null), 
+                new BasicManagedBundle(bundleName.getSymbolicName(), bundleName.getVersionString(), null, null), 
                 new FileInputStream(bf),
                 false);
             

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlEntityTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlEntityTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlEntityTest.java
index 6d7df19..ae0df91 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlEntityTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlEntityTest.java
@@ -333,7 +333,7 @@ public class CatalogOsgiYamlEntityTest extends AbstractYamlTest {
             addCatalogOSGiEntity(id, SIMPLE_ENTITY_TYPE, true);
             Asserts.shouldHaveFailedPreviously();
         } catch (Exception e) {
-            Asserts.expectedFailureContainsIgnoreCase(e, id, "already installed", "cannot install a different bundle at a same non-snapshot version");
+            Asserts.expectedFailureContainsIgnoreCase(e, id, "already installed", "cannot install a different bundle with the same non-snapshot version");
         }
     }
 

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlVersioningTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlVersioningTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlVersioningTest.java
index 672c722..9042663 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlVersioningTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlVersioningTest.java
@@ -74,7 +74,7 @@ public class CatalogOsgiYamlVersioningTest extends CatalogYamlVersioningTest {
     @Override
     protected void checkAddSameVersionFailsWhenIconIsDifferent(Exception e) {
         Asserts.expectedFailureContainsIgnoreCase(e, 
-            "cannot install a different bundle at a same non-snapshot version");
+            "cannot install a different bundle with the same non-snapshot version");
         assertExpectedFailureIncludesSampleId(e);
     }
     

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/test/lite/CampYamlLiteTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/test/lite/CampYamlLiteTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/test/lite/CampYamlLiteTest.java
index 92b053e..d4b67c5 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/test/lite/CampYamlLiteTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/test/lite/CampYamlLiteTest.java
@@ -213,7 +213,7 @@ public class CampYamlLiteTest {
         // install bundle for class access but without loading its catalog.bom, 
         // since we only have mock matchers here
         // (if we don't do this, the default routines install it and try to process the catalog.bom, failing)
-        ((ManagementContextInternal)mgmt).getOsgiManager().get().installDeferredStart(new BasicManagedBundle(null, null, bundleUrl), null, false).get();
+        ((ManagementContextInternal)mgmt).getOsgiManager().get().installDeferredStart(new BasicManagedBundle(null, null, bundleUrl, null), null, false).get();
     }
 
     private void assertMgmtHasSampleMyCatalogApp(String symbolicName, String bundleUrl) {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
----------------------------------------------------------------------
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 810ffc1..c1c893c 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
@@ -1503,7 +1503,7 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
 
         OsgiBundleInstallationResult result = null;
         try {
-            result = osgiManager.install(new BasicManagedBundle(vn.getSymbolicName(), vn.getVersionString(), null), new FileInputStream(bf), true, true, forceUpdate).get();
+            result = osgiManager.install(new BasicManagedBundle(vn.getSymbolicName(), vn.getVersionString(), null, null), new FileInputStream(bf), true, true, forceUpdate).get();
         } catch (FileNotFoundException e) {
             throw Exceptions.propagate(e);
         } finally {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java
index 968fbf9..e3b91fa 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java
@@ -26,6 +26,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.jar.Attributes;
@@ -362,6 +363,7 @@ class OsgiArchiveInstaller {
             final boolean updating;
             result.metadata = osgiManager.getManagedBundle(inferredMetadata.getVersionedName());
             if (result.getMetadata()!=null) {
+                
                 // already have a managed bundle - check if this is using a new/different URL
                 if (suppliedKnownBundleMetadata!=null && suppliedKnownBundleMetadata.getUrl()!=null) {
                     String knownIdForThisUrl = osgiManager.managedBundlesRecord.getManagedBundleIdFromUrl(suppliedKnownBundleMetadata.getUrl());
@@ -376,68 +378,106 @@ class OsgiArchiveInstaller {
                         osgiManager.managedBundlesRecord.setManagedBundleUrl(suppliedKnownBundleMetadata.getUrl(), result.getMetadata().getId());
                     }
                 }
+
+                result.bundle = osgiManager.framework.getBundleContext().getBundle(result.getMetadata().getOsgiUniqueUrl());
+
+                // Check if exactly this bundle is already installed
+                if (result.bundle != null && checksumsMatch(result.getMetadata(), inferredMetadata)) {
+                    // e.g. repeatedly installing the same bundle
+                    log.trace("Bundle "+inferredMetadata+" matches already installed managed bundle "+result.getMetadata()
+                            +"; install is no-op");
+                    result.setIgnoringAlreadyInstalled();
+                    return ReferenceWithError.newInstanceWithoutError(result);
+                } else if (isEquivalentBundleAlreadyOsgiInstalled(osgiManager, inferredMetadata, zipFile)) {
+                    // e.g. happens if pre-installed bundle is brought under management, and then add it again via a mvn-style url.
+                    // We wouldn't know the checksum from the pre-installed bundle.
+                    log.trace("Bundle "+inferredMetadata+" matches metadata of managed bundle "+result.getMetadata()
+                            +" (but not OSGi bundle location "+result.getMetadata().getOsgiUniqueUrl()+"), "
+                            + "and matches already installed OSGi bundle; ; install is no-op");
+                    result.setIgnoringAlreadyInstalled();
+                    return ReferenceWithError.newInstanceWithoutError(result);
+                }
+                
                 if (canUpdate()) { 
-                    
-                    result.bundle = osgiManager.framework.getBundleContext().getBundle(result.getMetadata().getOsgiUniqueUrl());
-                    if (result.getBundle()==null) {
-                        if (hasBundleOnInstall(osgiManager, inferredMetadata, zipFile)) {
-                            // e.g. happens if system-bundle is brought under management, and then try to add it again:
-                            // if added from "initial catalog" has it, and then added from persisted state as well.
-                            result.setIgnoringAlreadyInstalled();
-                            return ReferenceWithError.newInstanceWithoutError(result);
-                        } else {
-                            log.warn("Brooklyn thought is was already managing bundle "+result.getMetadata().getVersionedName()+" but it's not installed to framework; reinstalling it");
-                            updating = false;
-                        }
+                    if (result.getBundle() == null) {
+                        log.warn("Brooklyn thought is was already managing bundle "+result.getMetadata().getVersionedName()
+                                +" but it's not installed to framework at location "+result.getMetadata().getOsgiUniqueUrl()+"; reinstalling it");
+                        updating = false;
                     } else {
+                        log.trace("Updating existing brooklyn-managed bundle "+result);
                         updating = true;
                     }
                 } else {
-                    if (result.getMetadata().getChecksum()==null || inferredMetadata.getChecksum()==null) {
-                        log.warn("Missing bundle checksum data for "+result+"; assuming bundle replacement is permitted");
-                    } else if (!Objects.equal(result.getMetadata().getChecksum(), inferredMetadata.getChecksum())) {
+                    List<Bundle> existingBundles = findBundlesByVersion(osgiManager, inferredMetadata);
+                    if (existingBundles.size() > 0 && (result.getMetadata().getChecksum()==null || inferredMetadata.getChecksum()==null)) {
+                        // e.g. Checksum would be missing if we brought under management a pre-installed bundle with an unusable url.
+                        log.info("Missing bundle checksum data for "+result+"; assuming bundle matches existing brooklyn-managed bundle (not re-installing)");
+                        result.setIgnoringAlreadyInstalled();
+                        return ReferenceWithError.newInstanceWithoutError(result);
+                    } else if (result.bundle != null || existingBundles.size() > 0) {
                         throw new IllegalArgumentException("Bundle "+result.getMetadata().getVersionedName()+" already installed; "
-                            + "cannot install a different bundle at a same non-snapshot version");
+                                + "cannot install a different bundle with the same non-snapshot version");
+                    } else {
+                        throw new IllegalArgumentException("Bundle "+result.getMetadata().getVersionedName()+" already a brooklyn-managed bundle, but not found in OSGi framework; "
+                                + "will not re-install without use of 'force'");
                     }
-                    result.setIgnoringAlreadyInstalled();
-                    return ReferenceWithError.newInstanceWithoutError(result);
                 }
             } else {
+                // No such Brooklyn-managed bundle.
+                
+                // Check if likely-looking bundle already installed to OSGi subsystem, but brooklyn not aware of it.
+                // This will often happen on a karaf restart where bundle was cached by karaf, so we need to allow it;
+                // can also happen if brooklyn.libraries references an existing bundle.
+                //
+                // If we're not certain that the bundle is identical 
+
+                
                 result.metadata = inferredMetadata;
-                // no such Brooklyn-managed bundle
-                Maybe<Bundle> b = Osgis.bundleFinder(osgiManager.framework).symbolicName(result.getMetadata().getSymbolicName()).version(result.getMetadata().getSuppliedVersionString()).find();
-                if (b.isPresent()) {
-                    // bundle already installed to OSGi subsystem but brooklyn not aware of it;
-                    // this will often happen on a karaf restart where bundle was cached by karaf,
-                    // so we need to allow it. can also happen if brooklyn.libraries references an existing bundle.
+                
+                // search for already-installed bundles.
+                List<Bundle> existingBundles = findBundlesByVersion(osgiManager, inferredMetadata);
+                Maybe<Bundle> existingEquivalentBundle = tryFindEquivalentBundle(existingBundles, inferredMetadata, zipFile);
+                
+                if (existingEquivalentBundle.isPresent()) {
+                    // Identical bundle (by osgi location or binary content) already installed; just bring that under management.
+                    // This will often happen on a karaf restart: bundles from persisted state match those cached by karaf,
+                    isBringingExistingOsgiInstalledBundleUnderBrooklynManagement = true;
+                    result.bundle = existingEquivalentBundle.get();
                     
-                    // determine if we are simply bringing existing installed under Brooklyn management (because url or binary content identical and not forced)
-                    // or if the bundle should be reinstalled/updated
-                    if (!force) {
-                        if (!isBringingExistingOsgiInstalledBundleUnderBrooklynManagement) {
-                            isBringingExistingOsgiInstalledBundleUnderBrooklynManagement = hasBundleOnInstall(osgiManager, inferredMetadata, zipFile);
-                        }
-                    } else {
+                } else if (existingBundles.size() > 0) {
+                    Bundle existingBundle = existingBundles.get(0);
+                    
+                    if (force) {
                         if (isBringingExistingOsgiInstalledBundleUnderBrooklynManagement) {
                             log.debug("Request to install "+inferredMetadata+" was forced, so forcing reinstallation "
-                                + "of existing OSGi installed (but not Brooklyn-managed) bundle "+b.get());
+                                + "of existing OSGi installed (but not Brooklyn-managed) bundle "+existingBundle);
                             isBringingExistingOsgiInstalledBundleUnderBrooklynManagement = false;
                         }
                     }
                     
                     if (isBringingExistingOsgiInstalledBundleUnderBrooklynManagement) {
-                        result.bundle = b.get();
-                        
+                        // We were explicitly asked to bring an existing OSGi bundle under management; 
+                        // no equivalence check required
+                        result.bundle = existingBundle;
                     } else {
-                        // needs reinstallation
-                        // we could update instead of uninstall/reinstall
-                        // note however either way we won't be able to rollback if there is a failure
+                        // Uninstall and re-install the bundle.
+                        // This is a good idea for brooklyn managed bundles that were in the karaf cache (when we can't 
+                        // determine that they are definitely identical).
+                        // It's less good for pre-installed bundles, but if the user has said to deploy it or has
+                        // referenced it in `brooklyn.libraries` then we'll go for it anyway! Let's hope they didn't 
+                        // reference `org.apache.brooklyn.core` or some such.
+                        // 
+                        // We are this extreme because we want rebind to always work! If a user did a `force` install
+                        // of a bundle, then we want to do the same on rebind (rather than risk failing).
+                        //
+                        // Instead of uninstall, we could update the bundle.
+                        // Note however either way we won't be able to rollback if there is a failure
                         log.debug("Brooklyn install of "+result.getMetadata().getVersionedName()+" detected already loaded in OSGi; uninstalling that to reinstall as Brooklyn-managed");
-                        b.get().uninstall();
+                        existingBundle.uninstall();
                         result.bundle = null;
                     }
                 }
-                // normal install
+                
                 updating = false;
             }
             
@@ -449,6 +489,7 @@ class OsgiArchiveInstaller {
                         log.debug("Brooklyn install of "+result.getMetadata().getVersionedName()+" detected already loaded "+result.getBundle()+" in OSGi can be re-used, skipping OSGi install");
                     } else {
                         assert result.getBundle()==null;
+                        log.debug("Installing bundle "+result.getMetadata().getVersionedName()+", using OSGi location "+result.getMetadata().getOsgiUniqueUrl());
                         result.bundle = osgiManager.framework.getBundleContext().installBundle(result.getMetadata().getOsgiUniqueUrl(), fin);
                     }
                 } else {
@@ -464,14 +505,18 @@ class OsgiArchiveInstaller {
                 osgiManager.managedBundlesRecord.addManagedBundle(result, zipFile);
                 result.code = OsgiBundleInstallationResult.ResultCode.INSTALLED_NEW_BUNDLE;
                 result.message = "Installed Brooklyn catalog bundle "+result.getMetadata().getVersionedName()+" with ID "+result.getMetadata().getId()+" ["+result.bundle.getBundleId()+"]";
-                ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
-                mgmt().getRebindManager().getChangeListener().onManaged(result.getMetadata());
+                if (!isBlacklistedForPersistence(result.getMetadata())) {
+                    ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
+                    mgmt().getRebindManager().getChangeListener().onManaged(result.getMetadata());
+                }
             } else {
                 oldZipFile = osgiManager.managedBundlesRecord.updateManagedBundleFile(result, zipFile);
                 result.code = OsgiBundleInstallationResult.ResultCode.UPDATED_EXISTING_BUNDLE;
                 result.message = "Updated Brooklyn catalog bundle "+result.getMetadata().getVersionedName()+" as existing ID "+result.getMetadata().getId()+" ["+result.bundle.getBundleId()+"]";
-                ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
-                mgmt().getRebindManager().getChangeListener().onChanged(result.getMetadata());
+                if (!isBlacklistedForPersistence(result.getMetadata())) {
+                    ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
+                    mgmt().getRebindManager().getChangeListener().onChanged(result.getMetadata());
+                }
             }
             log.debug(result.message + " (partial): OSGi bundle installed, with bundle start and Brooklyn management to follow");
             // can now delete and close (copy has been made and is available from OsgiManager)
@@ -505,8 +550,10 @@ class OsgiArchiveInstaller {
                                 + "installation will likely be corrupted and correct version should be manually installed.", e);
                         }
                         
-                        ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
-                        mgmt().getRebindManager().getChangeListener().onChanged(result.getMetadata());
+                        if (!isBlacklistedForPersistence(result.getMetadata())) {
+                            ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
+                            mgmt().getRebindManager().getChangeListener().onChanged(result.getMetadata());
+                        }
                     } else {
                         if (isBringingExistingOsgiInstalledBundleUnderBrooklynManagement) {
                             log.debug("Uninstalling bundle "+result.getVersionedName()+" from Brooklyn management only (rollback needed but it was already installed to OSGi)");
@@ -514,8 +561,10 @@ class OsgiArchiveInstaller {
                             log.debug("Uninstalling bundle "+result.getVersionedName()+" (roll back of failed fresh install, no previous version to revert to)");
                         }                        
                         osgiManager.uninstallUploadedBundle(result.getMetadata(), false, isBringingExistingOsgiInstalledBundleUnderBrooklynManagement);
-                        ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
-                        mgmt().getRebindManager().getChangeListener().onUnmanaged(result.getMetadata());
+                        if (!isBlacklistedForPersistence(result.getMetadata())) {
+                            ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
+                            mgmt().getRebindManager().getChangeListener().onUnmanaged(result.getMetadata());
+                        }
                     }
                 }
                 public void run() {
@@ -631,47 +680,75 @@ class OsgiArchiveInstaller {
             close();
         }
     }
-
-    private static boolean hasBundleOnInstall(OsgiManager osgiManager, ManagedBundle desired, File zipFile) {
+    
+    private static boolean isBlacklistedForPersistence(ManagedBundle managedBundle) {
+        // Specifically, we treat as "managed bundles" (to extract their catalog.bom) the contents of:
+        //   - org.apache.brooklyn.core
+        //   - org.apache.brooklyn.policy
+        //   - org.apache.brooklyn.test-framework
+        //   - org.apache.brooklyn.software-*
+        //   - org.apache.brooklyn.library-catalog
+        //   - org.apache.brooklyn.karaf-init (not sure why this one could end up in persisted state!)
+        return managedBundle.getSymbolicName().startsWith("org.apache.brooklyn.");
+    }
+    
+    private static List<Bundle> findBundlesByVersion(OsgiManager osgiManager, ManagedBundle desired) {
+        return Osgis.bundleFinder(osgiManager.framework).symbolicName(desired.getSymbolicName()).version(desired.getOsgiVersionString()).findAll();
+    }
+    
+    private static boolean checksumsMatch(ManagedBundle actual, ManagedBundle desired) {
+        return actual.getChecksum() != null && Objects.equal(actual.getChecksum(), desired.getChecksum());
+    }
+    
+    private static boolean isEquivalentBundleAlreadyOsgiInstalled(OsgiManager osgiManager, ManagedBundle desired, File zipFile) {
+        for (Bundle bundle : findBundlesByVersion(osgiManager, desired)) {
+            if (isEquivalentBundle(bundle, desired, zipFile)) {
+                return true;
+            }
+        }
+        
+        return false;
+    }
+    
+    private static Maybe<Bundle> tryFindEquivalentBundle(Iterable<? extends Bundle> bundles, ManagedBundle desired, File zipFile) {
+        for (Bundle bundle : bundles) {
+            if (isEquivalentBundle(bundle, desired, zipFile)) {
+                return Maybe.of(bundle);
+            }
+        }
+        
+        return Maybe.absent();
+    }
+    
+    private static boolean isEquivalentBundle(Bundle bundle, ManagedBundle desired, File zipFile) {
         // Would be nice to also use `desired.getChecksum()`, but not clear if we can get
         // MD5 checksum from an installed OSGi bundle.
         
-        Maybe<Bundle> b = Osgis.bundleFinder(osgiManager.framework).symbolicName(desired.getSymbolicName()).version(desired.getOsgiVersionString()).find();
-        if (b.isPresent()) {
-            // bundle already installed to OSGi subsystem (but brooklyn not aware of it as "managed bundle");
-            // this will often happen on a karaf restart where bundle was cached by karaf,
-            // so we need to allow it. can also happen if brooklyn.libraries references an existing bundle.
-            
-            // determine if we are simply bringing existing installed under Brooklyn management (because url or binary content identical and not forced)
-            // or if the bundle should be reinstalled/updated
-            if (Objects.equal(b.get().getLocation(), desired.getUrl())) {
-                // installation request was for identical location, so assume we are simply bringing under mgmt
-                log.debug("Request to install "+desired+" from same location "+b.get().getLocation()+
-                    " as existing OSGi installed (but not Brooklyn-managed) bundle "+b.get()+", so skipping reinstall");
-                return true;
-            } else {
-                // different locations, but see if we can compare input stream contents
-                // (prevents needless uninstall/reinstall of already installed bundles)
-                try {
-                    if (Streams.compare(new FileInputStream(zipFile), new URL(b.get().getLocation()).openStream())) {
-                        log.debug("Request to install "+desired+" has same contents"+
-                            " as existing OSGi installed (but not Brooklyn-managed) bundle "+b.get()+", so skipping reinstall");
-                        return true;
-                    } else {
-                        log.debug("Request to install "+desired+" has different contents"+
-                            " as existing OSGi installed (but not Brooklyn-managed) bundle "+b.get()+", so will do reinstall");
-                        return false;
-                    }
-                } catch (Exception e) {
-                    Exceptions.propagateIfFatal(e);
-                    // probably an invalid URL on installed bundle; that's allowed
-                    log.debug("Request to install "+desired+" could not compare contents"+
-                        " with existing OSGi installed (but not Brooklyn-managed) bundle "+b.get()+", so will do reinstall (error "+e+" loading from "+b.get().getLocation()+")");
+        if (Objects.equal(bundle.getLocation(), desired.getUrl())) {
+            // installation request was for identical location, so assume we are simply bringing under mgmt
+            log.debug("Request to install "+desired+" from same location "+bundle.getLocation()+
+                " as existing OSGi installed (but not Brooklyn-managed) bundle "+bundle+", so skipping reinstall");
+            return true;
+        } else {
+            // different locations, but see if we can compare input stream contents
+            // (prevents needless uninstall/reinstall of already installed bundles)
+            try {
+                if (Streams.compare(new FileInputStream(zipFile), new URL(bundle.getLocation()).openStream())) {
+                    log.debug("Request to install "+desired+" has same contents"+
+                        " as existing OSGi installed (but not Brooklyn-managed) bundle "+bundle+", so skipping reinstall");
+                    return true;
+                } else {
+                    log.debug("Request to install "+desired+" has different contents"+
+                        " as existing OSGi installed (but not Brooklyn-managed) bundle "+bundle+", so will do reinstall (if no other equivalents found)");
                     return false;
                 }
+            } catch (Exception e) {
+                Exceptions.propagateIfFatal(e);
+                // probably an invalid URL on installed bundle; that's allowed
+                log.debug("Request to install "+desired+" could not compare contents"+
+                    " with existing OSGi installed (but not Brooklyn-managed) bundle "+bundle+", so will do reinstall if not other equivalents found (error "+e+" loading from "+bundle.getLocation()+")");
+                return false;
             }
-        } else {
-            return false;
         }
     }
     

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java
----------------------------------------------------------------------
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 755107a..8a667eb 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
@@ -1233,7 +1233,7 @@ public abstract class RebindIteration {
         }
 
         protected ManagedBundle newManagedBundle(ManagedBundleMemento memento) {
-            ManagedBundle result = new BasicManagedBundle(memento.getSymbolicName(), memento.getVersion(), memento.getUrl());
+            ManagedBundle result = new BasicManagedBundle(memento.getSymbolicName(), memento.getVersion(), memento.getUrl(), memento.getChecksum());
             FlagUtils.setFieldsFromFlags(ImmutableMap.of("id", memento.getId()), result);
             return result;
         }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/dto/BasicManagedBundleMemento.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/dto/BasicManagedBundleMemento.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/dto/BasicManagedBundleMemento.java
index 505e7c8..37afb58 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/dto/BasicManagedBundleMemento.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/dto/BasicManagedBundleMemento.java
@@ -42,7 +42,8 @@ public class BasicManagedBundleMemento extends AbstractMemento implements Manage
         protected String symbolicName;
         protected String version;
         protected String url;
-
+        protected String checksum;
+        
         public Builder symbolicName(String symbolicName) {
             this.symbolicName = symbolicName;
             return self();
@@ -58,11 +59,17 @@ public class BasicManagedBundleMemento extends AbstractMemento implements Manage
             return self();
         }
 
+        public Builder checksum(String checksum) {
+            this.checksum = checksum;
+            return self();
+        }
+
         public Builder from(ManagedBundleMemento other) {
             super.from(other);
             symbolicName = other.getSymbolicName();
             version = other.getVersion();
             url = other.getUrl();
+            checksum = other.getChecksum();
             return self();
         }
 
@@ -74,6 +81,7 @@ public class BasicManagedBundleMemento extends AbstractMemento implements Manage
     private String symbolicName;
     private String version;
     private String url;
+    private String checksum;
     transient private ByteSource jarContent;
 
     @SuppressWarnings("unused") // For deserialisation
@@ -84,6 +92,7 @@ public class BasicManagedBundleMemento extends AbstractMemento implements Manage
         this.symbolicName = builder.symbolicName;
         this.version = builder.version;
         this.url = builder.url;
+        this.checksum = builder.checksum;
     }
 
     @Override
@@ -102,6 +111,11 @@ public class BasicManagedBundleMemento extends AbstractMemento implements Manage
     }
 
     @Override
+    public String getChecksum() {
+        return checksum;
+    }
+
+    @Override
     public ByteSource getJarContent() {
         return jarContent;
     }
@@ -129,7 +143,7 @@ public class BasicManagedBundleMemento extends AbstractMemento implements Manage
         return super.newVerboseStringHelper()
                 .add("symbolicName", getSymbolicName())
                 .add("version", getVersion())
-                .add("url", getUrl());
+                .add("url", getUrl())
+                .add("checksum", getChecksum());
     }
-
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
index 8372b99..860bf5a 100644
--- a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
+++ b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
@@ -20,6 +20,8 @@ package org.apache.brooklyn.core.typereg;
 
 import java.util.Map;
 
+import javax.annotation.Nullable;
+
 import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle;
 import org.apache.brooklyn.api.mgmt.rebind.RebindSupport;
 import org.apache.brooklyn.api.typereg.ManagedBundle;
@@ -46,7 +48,7 @@ public class BasicManagedBundle extends AbstractBrooklynObject implements Manage
     /** Creates an empty one, with an ID, expecting other fields will be populated. */
     public BasicManagedBundle() {}
 
-    public BasicManagedBundle(String name, String version, String url) {
+    public BasicManagedBundle(String name, String version, String url, @Nullable String checksum) {
         if (name == null && version == null) {
             Preconditions.checkNotNull(url, "Either a URL or both name and version are required");
         } else {
@@ -56,6 +58,7 @@ public class BasicManagedBundle extends AbstractBrooklynObject implements Manage
         this.symbolicName = name;
         this.version = version;
         this.url = url;
+        this.checksum = checksum;
     }
     
     @Override
@@ -101,9 +104,16 @@ public class BasicManagedBundle extends AbstractBrooklynObject implements Manage
         this.url = url;
     }
 
+    /**
+     * Gets the (internal) value to be used as the location in bundleContext.install(location). 
+     * It thus allows us to tell if a cached OSGi bundle is the same as a bundle we are about to 
+     * install (e.g. one we get from persisted state), or have retrieved from the initial catalog.
+     * 
+     * Care should be taken to set the checksum <em>before</em> using the OSGi unique url.
+     */
     @Override
     public String getOsgiUniqueUrl() {
-        return "brooklyn:"+getId();
+        return "brooklyn:" + (checksum != null ? checksum : getId());
     }
     
     @Override
@@ -175,6 +185,7 @@ public class BasicManagedBundle extends AbstractBrooklynObject implements Manage
     public String getChecksum() {
         return checksum;
     }
+    
     public void setChecksum(String md5Checksum) {
         this.checksum = md5Checksum;
     }
@@ -184,8 +195,9 @@ public class BasicManagedBundle extends AbstractBrooklynObject implements Manage
         throw new UnsupportedOperationException();
     }
 
-    public static ManagedBundle of(CatalogBundle bundleUrl) {
-        return new BasicManagedBundle(bundleUrl.getSymbolicName(), bundleUrl.getSuppliedVersionString(), bundleUrl.getUrl());
+    public static ManagedBundle of(CatalogBundle bundle) {
+        String checksum = (bundle instanceof ManagedBundle) ? ((ManagedBundle)bundle).getChecksum() : null;
+        return new BasicManagedBundle(bundle.getSymbolicName(), bundle.getSuppliedVersionString(), bundle.getUrl(), checksum);
     }
 
     public void setPersistenceNeeded(boolean val) {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/launcher/src/test/java/org/apache/brooklyn/launcher/AbstractBrooklynLauncherRebindTest.java
----------------------------------------------------------------------
diff --git a/launcher/src/test/java/org/apache/brooklyn/launcher/AbstractBrooklynLauncherRebindTest.java b/launcher/src/test/java/org/apache/brooklyn/launcher/AbstractBrooklynLauncherRebindTest.java
index 89bcae0..400e3a8 100644
--- a/launcher/src/test/java/org/apache/brooklyn/launcher/AbstractBrooklynLauncherRebindTest.java
+++ b/launcher/src/test/java/org/apache/brooklyn/launcher/AbstractBrooklynLauncherRebindTest.java
@@ -267,19 +267,32 @@ public abstract class AbstractBrooklynLauncherRebindTest {
     }
     
     protected String createCatalogYaml(Iterable<URI> libraries, Iterable<VersionedName> entities) {
-        if (Iterables.isEmpty(libraries) && Iterables.isEmpty(entities)) {
-            return "brooklyn.catalog: {}\n";
-        }
-        
+        return createCatalogYaml(libraries, ImmutableList.of(), entities, null);
+    }
+
+    protected String createCatalogYaml(Iterable<URI> libraryUris, Iterable<VersionedName> libraryNames, Iterable<VersionedName> entities) {
+        return createCatalogYaml(libraryUris, libraryNames, entities, null);
+    }
+    
+    protected String createCatalogYaml(Iterable<URI> libraryUris, Iterable<VersionedName> libraryNames, Iterable<VersionedName> entities, String randomNoise) {
         StringBuilder result = new StringBuilder();
         result.append("brooklyn.catalog:\n");
-        if (!Iterables.isEmpty(libraries)) {
+        if (randomNoise != null) {
+            result.append("  description: "+randomNoise+"\n");
+        }
+        if (!(Iterables.isEmpty(libraryUris) && Iterables.isEmpty(libraryNames))) {
             result.append("  brooklyn.libraries:\n");
         }
-        for (URI library : libraries) {
+        for (URI library : libraryUris) {
             result.append("    - " + library+"\n");
         }
-        if (!Iterables.isEmpty(entities)) {
+        for (VersionedName library : libraryNames) {
+            result.append("    - name: "+library.getSymbolicName()+"\n");
+            result.append("      version: \""+library.getVersionString()+"\"\n");
+        }
+        if (Iterables.isEmpty(entities)) {
+            result.append("  items: []\n");
+        } else {
             result.append("  items:\n");
         }
         for (VersionedName entity : entities) {
@@ -292,6 +305,9 @@ public abstract class AbstractBrooklynLauncherRebindTest {
         return result.toString();
     }
     
+
+    
+    
     public PersistedStateInitializer newPersistedStateInitializer() {
         return new PersistedStateInitializer();
     }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/85196899/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherRebindCatalogOsgiTest.java
----------------------------------------------------------------------
diff --git a/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherRebindCatalogOsgiTest.java b/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherRebindCatalogOsgiTest.java
index d4e8c75..8985147 100644
--- a/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherRebindCatalogOsgiTest.java
+++ b/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherRebindCatalogOsgiTest.java
@@ -19,30 +19,31 @@
 package org.apache.brooklyn.launcher;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
 
 import java.io.File;
-import java.nio.charset.StandardCharsets;
 import java.io.FileInputStream;
-import java.net.URI;
-import java.util.LinkedHashSet;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
-import org.apache.brooklyn.api.typereg.ManagedBundle;
-import org.apache.brooklyn.api.typereg.RegisteredType;
 import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog;
 import org.apache.brooklyn.core.catalog.internal.CatalogInitialization;
+import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult;
 import org.apache.brooklyn.core.mgmt.ha.OsgiManager;
 import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
-import org.apache.brooklyn.entity.stock.BasicEntity;
 import org.apache.brooklyn.test.support.TestResourceUnavailableException;
 import org.apache.brooklyn.util.core.osgi.Osgis;
+import org.apache.brooklyn.util.exceptions.ReferenceWithError;
 import org.apache.brooklyn.util.os.Os;
 import org.apache.brooklyn.util.osgi.OsgiTestResources;
 import org.apache.brooklyn.util.osgi.VersionedName;
+import org.apache.brooklyn.util.text.Identifiers;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.launch.Framework;
-import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.Test;
 
 import com.google.common.base.Joiner;
@@ -62,12 +63,29 @@ public class BrooklynLauncherRebindCatalogOsgiTest extends AbstractBrooklynLaunc
     private static final Set<VersionedName> COM_EXAMPLE_BUNDLE_CATALOG_IDS = ImmutableSet.of(
             new VersionedName("com.example.simpleTest", "0.0.0-SNAPSHOT"));
 
+    /**
+     * Whether we reuse OSGi Framework depends if we want it to feel like rebinding on a new machine 
+     * (so no cached bundles), or a local restart. We can also use {@code reuseOsgi = true} to emulate
+     * system bundles (by pre-installing them into the reused framework at the start of the test).
+     */
     private boolean reuseOsgi = false;
     
-    @BeforeMethod(alwaysRun=true)
+    private List<Bundle> reusedBundles = new ArrayList<>();
+    
+    @AfterMethod(alwaysRun=true)
     @Override
-    public void setUp() throws Exception {
-        super.setUp();
+    public void tearDown() throws Exception {
+        try {
+            reuseOsgi = false;
+            for (Bundle bundle : reusedBundles) {
+                if (bundle != null && bundle.getState() != Bundle.UNINSTALLED) {
+                    bundle.uninstall();
+                }
+            }
+            reusedBundles.clear();
+        } finally {
+            super.tearDown();
+        }
     }
     
     @Override
@@ -110,7 +128,7 @@ public class BrooklynLauncherRebindCatalogOsgiTest extends AbstractBrooklynLaunc
     /**
      * See https://issues.apache.org/jira/browse/BROOKLYN-546.
      * 
-     * We built up to launcher3, which will have three things:
+     * We built up to launcher2, which will have three things:
      *  1. a pre-installed "system bundle"
      *  2. an initial catalog that references this same system bundle (hence the bundle will be "managed")
      *  3. persisted state that references this same system bundle.
@@ -130,54 +148,201 @@ public class BrooklynLauncherRebindCatalogOsgiTest extends AbstractBrooklynLaunc
     @Test
     public void testRebindWithSystemBundleInCatalog() throws Exception {
         Set<VersionedName> bundleItems = ImmutableSet.of(VersionedName.fromString("one:1.0.0-SNAPSHOT"));
-        String bundleBom = createCatalogYaml(ImmutableList.of(), bundleItems);
-        VersionedName bundleName = new VersionedName("org.example.testRebindWithSystemBundleInCatalog", "1.0.0.SNAPSHOT");
-        File bundleFile = newTmpBundle(ImmutableMap.of(BasicBrooklynCatalog.CATALOG_BOM, bundleBom.getBytes(StandardCharsets.UTF_8)), bundleName);
+        VersionedName bundleName = new VersionedName("org.example.brooklynLauncherRebindCatalogOsgiTest."+Identifiers.makeRandomId(4), "1.0.0.SNAPSHOT");
+        File bundleFile = newTmpBundle(bundleItems, bundleName);
         File bundleFileCopy = newTmpCopy(bundleFile);
 
         File initialBomFile = newTmpFile(createCatalogYaml(ImmutableList.of(bundleFile.toURI()), ImmutableList.of()));
 
-        Framework reusedFamework = null;
-        Bundle pseudoSystemBundle = null;
-        try {
-            reuseOsgi = true;
-            
-            // Launch and terminate brooklyn to ensure there is a reusable OSGi Framework
-            BrooklynLauncher launcher = newLauncherForTests(CATALOG_EMPTY_INITIAL);
-            launcher.start();
-            launcher.terminate();
-            Os.deleteRecursively(persistenceDir);
-            
-            // Add our bundle, so it feels for all intents and purposes like a "system bundle"
-            reusedFamework = OsgiManager.tryPeekFrameworkForReuse().get();
-            pseudoSystemBundle = installBundle(reusedFamework, bundleFileCopy);
-            
-            // Launch brooklyn, where initial catalog includes a duplicate of the system bundle
-            BrooklynLauncher launcher2 = newLauncherForTests(initialBomFile.getAbsolutePath());
-            launcher2.start();
-            assertCatalogConsistsOfIds(launcher2, bundleItems);
-            assertManagedBundle(launcher2, bundleName, bundleItems);
-            launcher2.terminate();
-
-            // Launch brooklyn, where persisted state now includes the initial catalog's bundle
-            BrooklynLauncher launcher3 = newLauncherForTests(initialBomFile.getAbsolutePath());
-            launcher3.start();
-            assertCatalogConsistsOfIds(launcher3, bundleItems);
-            assertManagedBundle(launcher3, bundleName, bundleItems);
-            
-            // Should not have replaced the original "system bundle"
-            reusedFamework.getBundleContext().getBundles();
-            List<Bundle> matchingBundles = Osgis.bundleFinder(reusedFamework).symbolicName(bundleName.getSymbolicName()).version(bundleName.getOsgiVersionString()).findAll();
-            assertEquals(matchingBundles, ImmutableList.of(pseudoSystemBundle));
-
-            launcher3.terminate();
+        reuseOsgi = true;
+        
+        // Add our bundle, so it feels for all intents and purposes like a "system bundle"
+        Framework reusedFramework = initReusableOsgiFramework();
+        Bundle pseudoSystemBundle = installBundle(reusedFramework, bundleFileCopy);
+        reusedBundles.add(pseudoSystemBundle);
+        
+        // Launch brooklyn, where initial catalog includes a duplicate of the system bundle
+        BrooklynLauncher launcher = newLauncherForTests(initialBomFile.getAbsolutePath());
+        launcher.start();
+        assertCatalogConsistsOfIds(launcher, bundleItems);
+        assertManagedBundle(launcher, bundleName, bundleItems);
+        launcher.terminate();
 
-        } finally {
-            reuseOsgi = false;
-            if (pseudoSystemBundle != null && pseudoSystemBundle.getState() != Bundle.UNINSTALLED) {
-                pseudoSystemBundle.uninstall();
-            }
+        // Launch brooklyn, where persisted state now includes the initial catalog's bundle
+        BrooklynLauncher launcher2 = newLauncherForTests(initialBomFile.getAbsolutePath());
+        launcher2.start();
+        assertCatalogConsistsOfIds(launcher2, bundleItems);
+        assertManagedBundle(launcher2, bundleName, bundleItems);
+        
+        // Should not have replaced the original "system bundle"
+        assertOnlyBundle(reusedFramework, bundleName, pseudoSystemBundle);
+
+        launcher2.terminate();
+    }
+
+    @Test
+    public void testInstallPreexistingBundle() throws Exception {
+        Set<VersionedName> bundleItems = ImmutableSet.of(VersionedName.fromString("one:1.0.0-SNAPSHOT"));
+        VersionedName bundleName = new VersionedName("org.example.brooklynLauncherRebindCatalogOsgiTest."+Identifiers.makeRandomId(4), "1.0.0.SNAPSHOT");
+        File bundleFile = newTmpBundle(bundleItems, bundleName);
+        File bundleFileCopy = newTmpCopy(bundleFile);
+
+        reuseOsgi = true;
+        
+        // Add our bundle, so it feels for all intents and purposes like a "system bundle"
+        Framework reusedFramework = initReusableOsgiFramework();
+        Bundle pseudoSystemBundle = installBundle(reusedFramework, bundleFileCopy);
+        reusedBundles.add(pseudoSystemBundle);
+        
+        // Launch brooklyn, and explicitly install pre-existing bundle.
+        // Should bring it under brooklyn-management (should not re-install it).
+        BrooklynLauncher launcher = newLauncherForTests(CATALOG_EMPTY_INITIAL);
+        launcher.start();
+        installBrooklynBundle(launcher, bundleFile, false).getWithError();
+        
+        assertOnlyBundle(launcher, bundleName, pseudoSystemBundle);
+        assertCatalogConsistsOfIds(launcher, bundleItems);
+        assertManagedBundle(launcher, bundleName, bundleItems);
+        launcher.terminate();
+
+        // Launch brooklyn again (because will have persisted the pre-installed bundle)
+        BrooklynLauncher launcher2 = newLauncherForTests(CATALOG_EMPTY_INITIAL);
+        launcher2.start();
+        assertOnlyBundle(reusedFramework, bundleName, pseudoSystemBundle);
+        assertCatalogConsistsOfIds(launcher2, bundleItems);
+        assertManagedBundle(launcher2, bundleName, bundleItems);
+        launcher2.terminate();
+    }
+
+    @Test
+    public void testInstallPreexistingBundleViaIndirectBrooklynLibrariesReference() throws Exception {
+        Set<VersionedName> bundleItems = ImmutableSet.of(VersionedName.fromString("one:1.0.0-SNAPSHOT"));
+        VersionedName systemBundleName = new VersionedName("org.example.brooklynLauncherRebindCatalogOsgiTest.system"+Identifiers.makeRandomId(4), "1.0.0.SNAPSHOT");
+        File systemBundleFile = newTmpBundle(bundleItems, systemBundleName);
+
+        String bundleBom = createCatalogYaml(ImmutableList.of(), ImmutableList.of(systemBundleName), ImmutableList.of());
+        VersionedName bundleName = new VersionedName("org.example.brooklynLauncherRebindCatalogOsgiTest.initial"+Identifiers.makeRandomId(4), "1.0.0.SNAPSHOT");
+        File bundleFile = newTmpBundle(ImmutableMap.of(BasicBrooklynCatalog.CATALOG_BOM, bundleBom.getBytes(StandardCharsets.UTF_8)), bundleName);
+        
+        File initialBomFile = newTmpFile(createCatalogYaml(ImmutableList.of(bundleFile.toURI()), ImmutableList.of()));
+        
+        reuseOsgi = true;
+        
+        // Add our bundle, so it feels for all intents and purposes like a "system bundle"
+        Framework reusedFramework = initReusableOsgiFramework();
+        Bundle pseudoSystemBundle = installBundle(reusedFramework, systemBundleFile);
+        reusedBundles.add(pseudoSystemBundle);
+        
+        // Launch brooklyn, with initial catalog pointing at bundle that points at system bundle.
+        // Should bring it under brooklyn-management (without re-installing it).
+        BrooklynLauncher launcher = newLauncherForTests(initialBomFile.getAbsolutePath());
+        launcher.start();
+        assertOnlyBundle(launcher, systemBundleName, pseudoSystemBundle);
+        assertCatalogConsistsOfIds(launcher, bundleItems);
+        assertManagedBundle(launcher, systemBundleName, bundleItems);
+        assertManagedBundle(launcher, bundleName, ImmutableSet.of());
+        launcher.terminate();
+
+        // Launch brooklyn again (because will have persisted both those bundles)
+        BrooklynLauncher launcher2 = newLauncherForTests(CATALOG_EMPTY_INITIAL);
+        launcher2.start();
+        assertOnlyBundle(launcher2, systemBundleName, pseudoSystemBundle);
+        assertCatalogConsistsOfIds(launcher2, bundleItems);
+        assertManagedBundle(launcher2, systemBundleName, bundleItems);
+        assertManagedBundle(launcher2, bundleName, ImmutableSet.of());
+        launcher2.terminate();
+    }
+
+    @Test
+    public void testInstallPreexistingBundleViaInitialBomBrooklynLibrariesReference() throws Exception {
+        runInstallPreexistingBundleViaInitialBomBrooklynLibrariesReference(false);
+    }
+    
+    // Aled thought we supported version ranges in 'brooklyn.libraries', but doesn't work here.
+    @Test(groups="Broken")
+    public void testInstallPreexistingBundleViaInitialBomBrooklynLibrariesReferenceWithVersionRange() throws Exception {
+        runInstallPreexistingBundleViaInitialBomBrooklynLibrariesReference(true);
+    }
+    
+    protected void runInstallPreexistingBundleViaInitialBomBrooklynLibrariesReference(boolean useVersionRange) throws Exception {
+        Set<VersionedName> bundleItems = ImmutableSet.of(VersionedName.fromString("one:1.0.0"));
+        VersionedName systemBundleName = new VersionedName("org.example.brooklynLauncherRebindCatalogOsgiTest.system"+Identifiers.makeRandomId(4), "1.0.0");
+        File systemBundleFile = newTmpBundle(bundleItems, systemBundleName);
+
+        VersionedName systemBundleNameRef;
+        if (useVersionRange) {
+            systemBundleNameRef = new VersionedName(systemBundleName.getSymbolicName(), "[1,2)");
+        } else {
+            systemBundleNameRef = systemBundleName;
         }
+        File initialBomFile = newTmpFile(createCatalogYaml(ImmutableList.of(), ImmutableList.of(systemBundleNameRef), ImmutableList.of()));
+        
+        reuseOsgi = true;
+        
+        // Add our bundle, so it feels for all intents and purposes like a "system bundle"
+        Framework reusedFramework = initReusableOsgiFramework();
+        Bundle pseudoSystemBundle = installBundle(reusedFramework, systemBundleFile);
+        reusedBundles.add(pseudoSystemBundle);
+        
+        // Launch brooklyn, with initial catalog pointing at system bundle.
+        // Should bring it under brooklyn-management (without re-installing it).
+        BrooklynLauncher launcher = newLauncherForTests(initialBomFile.getAbsolutePath());
+        launcher.start();
+        assertOnlyBundle(launcher, systemBundleName, pseudoSystemBundle);
+        assertCatalogConsistsOfIds(launcher, bundleItems);
+        assertManagedBundle(launcher, systemBundleName, bundleItems);
+        launcher.terminate();
+
+        // Launch brooklyn again (because will have persisted both those bundles)
+        BrooklynLauncher launcher2 = newLauncherForTests(CATALOG_EMPTY_INITIAL);
+        launcher2.start();
+        assertOnlyBundle(launcher2, systemBundleName, pseudoSystemBundle);
+        assertCatalogConsistsOfIds(launcher2, bundleItems);
+        assertManagedBundle(launcher2, systemBundleName, bundleItems);
+        launcher2.terminate();
+    }
+
+    @Test
+    public void testInstallReplacesPreexistingBundleWithoutForce() throws Exception {
+        runInstallReplacesPreexistingBundle(false);
+    }
+    
+    @Test
+    public void testInstallReplacesPreexistingBundleWithForce() throws Exception {
+        runInstallReplacesPreexistingBundle(true);
+    }
+    
+    protected void runInstallReplacesPreexistingBundle(boolean force) throws Exception {
+        Set<VersionedName> bundleItems = ImmutableSet.of(VersionedName.fromString("one:1.0.0-SNAPSHOT"));
+        VersionedName bundleName = new VersionedName("org.example.brooklynLauncherRebindCatalogOsgiTest."+Identifiers.makeRandomId(4), "1.0.0.SNAPSHOT");
+        File systemBundleFile = newTmpBundle(bundleItems, bundleName);
+        File replacementBundleFile = newTmpBundle(bundleItems, bundleName, "randomDifference"+Identifiers.makeRandomId(4));
+
+        reuseOsgi = true;
+        
+        // Add our bundle, so it feels for all intents and purposes like a "system bundle"
+        Framework reusedFramework = initReusableOsgiFramework();
+        Bundle pseudoSystemBundle = installBundle(reusedFramework, systemBundleFile);
+        reusedBundles.add(pseudoSystemBundle);
+        
+        // Launch brooklyn, and explicitly install pre-existing bundle.
+        // Should bring it under brooklyn-management (should not re-install it).
+        BrooklynLauncher launcher = newLauncherForTests(CATALOG_EMPTY_INITIAL);
+        launcher.start();
+        installBrooklynBundle(launcher, replacementBundleFile, force).getWithError();
+        
+        assertOnlyBundleReplaced(launcher, bundleName, pseudoSystemBundle);
+        assertCatalogConsistsOfIds(launcher, bundleItems);
+        assertManagedBundle(launcher, bundleName, bundleItems);
+        launcher.terminate();
+
+        // Launch brooklyn again (because will have persisted the pre-installed bundle)
+        BrooklynLauncher launcher2 = newLauncherForTests(CATALOG_EMPTY_INITIAL);
+        launcher2.start();
+        assertOnlyBundleReplaced(reusedFramework, bundleName, pseudoSystemBundle);
+        assertCatalogConsistsOfIds(launcher2, bundleItems);
+        assertManagedBundle(launcher2, bundleName, bundleItems);
+        launcher2.terminate();
     }
 
     @Test
@@ -267,4 +432,54 @@ public class BrooklynLauncherRebindCatalogOsgiTest extends AbstractBrooklynLaunc
             return framework.getBundleContext().installBundle(bundle.toURI().toString(), stream);
         }
     }
+    
+    private ReferenceWithError<OsgiBundleInstallationResult> installBrooklynBundle(BrooklynLauncher launcher, File bundleFile, boolean force) throws Exception {
+        OsgiManager osgiManager = ((ManagementContextInternal)launcher.getManagementContext()).getOsgiManager().get();
+        try (FileInputStream bundleStream = new FileInputStream(bundleFile)) {
+            return osgiManager.install(null, bundleStream, true, true, force);
+        }
+    }
+    
+    private void assertOnlyBundle(BrooklynLauncher launcher, VersionedName bundleName, Bundle expectedBundle) {
+        Framework framework = ((ManagementContextInternal)launcher.getManagementContext()).getOsgiManager().get().getFramework();
+        assertOnlyBundle(framework, bundleName, expectedBundle);
+    }
+    
+    private void assertOnlyBundleReplaced(BrooklynLauncher launcher, VersionedName bundleName, Bundle expectedBundle) {
+        Framework framework = ((ManagementContextInternal)launcher.getManagementContext()).getOsgiManager().get().getFramework();
+        assertOnlyBundleReplaced(framework, bundleName, expectedBundle);
+    }
+    
+    private void assertOnlyBundle(Framework framework, VersionedName bundleName, Bundle expectedBundle) {
+        List<Bundle> matchingBundles = Osgis.bundleFinder(framework).symbolicName(bundleName.getSymbolicName()).version(bundleName.getOsgiVersionString()).findAll();
+        assertTrue(matchingBundles.contains(expectedBundle), "Bundle missing; matching="+matchingBundles);
+        assertEquals(matchingBundles.size(), 1, "Extra bundles; matching="+matchingBundles);
+    }
+    
+    private void assertOnlyBundleReplaced(Framework framework, VersionedName bundleName, Bundle expectedBundle) {
+        List<Bundle> matchingBundles = Osgis.bundleFinder(framework).symbolicName(bundleName.getSymbolicName()).version(bundleName.getOsgiVersionString()).findAll();
+        assertFalse(matchingBundles.contains(expectedBundle), "Bundle still present; matching="+matchingBundles);
+        assertEquals(matchingBundles.size(), 1, "Extra bundles; matching="+matchingBundles);
+    }
+    
+    private Framework initReusableOsgiFramework() {
+        if (!reuseOsgi) throw new IllegalStateException("Must first set reuseOsgi");
+        
+        if (OsgiManager.tryPeekFrameworkForReuse().isAbsent()) {
+            BrooklynLauncher launcher = newLauncherForTests(CATALOG_EMPTY_INITIAL);
+            launcher.start();
+            launcher.terminate();
+            Os.deleteRecursively(persistenceDir);
+        }
+        return OsgiManager.tryPeekFrameworkForReuse().get();
+    }
+
+    private File newTmpBundle(Set<VersionedName> catalogItems, VersionedName bundleName) {
+        return newTmpBundle(catalogItems, bundleName, null);
+    }
+    
+    private File newTmpBundle(Set<VersionedName> catalogItems, VersionedName bundleName, String randomNoise) {
+        String bundleBom = createCatalogYaml(ImmutableList.of(), ImmutableList.of(), catalogItems, randomNoise);
+        return newTmpBundle(ImmutableMap.of(BasicBrooklynCatalog.CATALOG_BOM, bundleBom.getBytes(StandardCharsets.UTF_8)), bundleName);
+    }
 }