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/11/21 17:08:19 UTC

[4/8] brooklyn-server git commit: bundle-force-remove: don’t re-install if come across reference

bundle-force-remove: don’t re-install if come across reference


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

Branch: refs/heads/master
Commit: b97107768a5f5897e2dd33bdaef0927bf807bad7
Parents: 419f03f
Author: Aled Sage <al...@gmail.com>
Authored: Thu Nov 16 18:48:56 2017 +0000
Committer: Aled Sage <al...@gmail.com>
Committed: Fri Nov 17 15:05:17 2017 +0000

----------------------------------------------------------------------
 .../core/mgmt/ha/OsgiArchiveInstaller.java      |  45 +++-
 .../mgmt/ha/OsgiBundleInstallationResult.java   |  10 +
 .../core/typereg/BundleUpgradeParser.java       |  39 ++++
 .../BrooklynLauncherUpgradeCatalogOsgiTest.java | 203 +++++++++++++++++++
 4 files changed, 294 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b9710776/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 15d847e..41164cb 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
@@ -43,10 +43,10 @@ import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog;
 import org.apache.brooklyn.core.catalog.internal.CatalogInitialization;
 import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult.ResultCode;
 import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
-import org.apache.brooklyn.core.mgmt.rebind.RebindManagerImpl;
 import org.apache.brooklyn.core.server.BrooklynServerConfig;
 import org.apache.brooklyn.core.typereg.BasicBrooklynTypeRegistry;
 import org.apache.brooklyn.core.typereg.BasicManagedBundle;
+import org.apache.brooklyn.core.typereg.BundleUpgradeParser.CatalogUpgrades;
 import org.apache.brooklyn.core.typereg.RegisteredTypePredicates;
 import org.apache.brooklyn.core.typereg.RegisteredTypes;
 import org.apache.brooklyn.util.collections.MutableList;
@@ -72,6 +72,7 @@ import org.slf4j.LoggerFactory;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.base.Objects;
+import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
@@ -363,6 +364,16 @@ class OsgiArchiveInstaller {
         
         try {
             init();
+
+            // Before even trying to find or download the bundle, check if it is supposed to be forcibly replaced.
+            // If so, return the replacement (if any).
+            if (suppliedKnownBundleMetadata != null && suppliedKnownBundleMetadata.isNameResolved()) {
+                Maybe<VersionedName> forcedReplacementBundle = CatalogUpgrades.tryGetBundleForcedReplaced(mgmt(), suppliedKnownBundleMetadata.getVersionedName());
+                if (forcedReplacementBundle.isPresent()) {
+                    return generateForciblyRemovedResult(suppliedKnownBundleMetadata.getVersionedName(), Optional.fromNullable(forcedReplacementBundle.get()));
+                }
+            }
+            
             makeLocalZipFileFromInputStreamOrUrl();
             if (result.code!=null) return ReferenceWithError.newInstanceWithoutError(result);
             discoverManifestFromCatalogBom(false);
@@ -437,13 +448,19 @@ class OsgiArchiveInstaller {
                 }
             } 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 
 
+                // First check if this bundle is forcibly removed (and optionally upgraded).
+                // If so, don't install it - return the replacement, if any.
+                Maybe<VersionedName> forcedReplacementBundle = CatalogUpgrades.tryGetBundleForcedReplaced(mgmt(), inferredMetadata.getVersionedName());
+                if (forcedReplacementBundle.isPresent()) {
+                    return generateForciblyRemovedResult(inferredMetadata.getVersionedName(), Optional.fromNullable(forcedReplacementBundle.get()));
+                }
                 
                 result.metadata = inferredMetadata;
                 
@@ -699,7 +716,29 @@ class OsgiArchiveInstaller {
             close();
         }
     }
-    
+
+    private ReferenceWithError<OsgiBundleInstallationResult> generateForciblyRemovedResult(VersionedName desiredBundle, Optional<VersionedName> replacementBundle) {
+        if (replacementBundle.isPresent()) {
+            result.metadata = osgiManager.getManagedBundle(replacementBundle.get());
+            if (result.getMetadata() != null) {
+                result.bundle = osgiManager.framework.getBundleContext().getBundle(result.getMetadata().getOsgiUniqueUrl());
+
+                log.debug("Bundle "+inferredMetadata+" forcibly replaced by bundle "+result.getMetadata()
+                        +"; install is no-op");
+                result.setIgnoringForciblyRemoved(inferredMetadata.getVersionedName(), replacementBundle);
+                return ReferenceWithError.newInstanceWithoutError(result);
+            } else {
+                throw new IllegalArgumentException("Bundle "+inferredMetadata+" forcibly replaced by bundle "
+                        +replacementBundle.get()+", but replacement not found");
+            }
+        } else {
+            log.debug("Bundle "+inferredMetadata+" forcibly removed, but no upgrade bundle supplied"
+                    + "; install is no-op");
+            result.setIgnoringForciblyRemoved(inferredMetadata.getVersionedName(), Optional.absent());
+            return ReferenceWithError.newInstanceWithoutError(result);
+        }
+    }
+
     @VisibleForTesting
     boolean isBlacklistedForPersistence(ManagedBundle managedBundle) {
         // We treat as "managed bundles" (to extract their catalog.bom) the contents of:

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b9710776/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java
index 68106d4..8c5b6dd 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java
@@ -27,6 +27,7 @@ import org.apache.brooklyn.util.osgi.VersionedName;
 import org.osgi.framework.Bundle;
 
 import com.google.common.annotations.Beta;
+import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 
 @Beta
@@ -44,6 +45,9 @@ public class OsgiBundleInstallationResult {
          * (safe in that behaviour won't be different or dangerous; 
          * could potentially be surprising, but ability to idempotently install things is nicer) */
         IGNORING_BUNDLE_AREADY_INSTALLED(false),
+        /** Bundle has been forcibly removed; not installing; 
+         * bundle metadata for an upgrade may be returned, if there is one */
+        IGNORING_BUNDLE_FORCIBLY_REMOVED(false),
         /** bundle could not be made insto a state where it could be installed; bundle is not installed, even if forced */
         ERROR_PREPARING_BUNDLE(true),
         /** bundle successfully installed to OSGi container but there was an error launching it, 
@@ -98,6 +102,12 @@ public class OsgiBundleInstallationResult {
         message = "Bundle "+getMetadata().getVersionedName()+" already installed as "+getMetadata().getId();
     }
     
+    void setIgnoringForciblyRemoved(VersionedName requestedBundle, Optional<VersionedName> replacementBundle) {
+        code = OsgiBundleInstallationResult.ResultCode.IGNORING_BUNDLE_FORCIBLY_REMOVED;
+        message = "Bundle "+requestedBundle+" forcibly removed, "
+                +(replacementBundle.isPresent() ? "upgraded to "+replacementBundle.get().getOsgiVersionString() : "no upgrade defined");
+    }
+    
     @Override
     public String toString() {
         return OsgiBundleInstallationResult.class.getSimpleName()+"["+code+", "+metadata+", "+message+"]";

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b9710776/core/src/main/java/org/apache/brooklyn/core/typereg/BundleUpgradeParser.java
----------------------------------------------------------------------
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 d808c24..8b16d69 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
@@ -308,9 +308,11 @@ public class BundleUpgradeParser {
         public Set<VersionedName> getUpgradesForBundle(VersionedName bundle) {
             return findUpgradesIn(bundle, upgradesProvidedByBundles);
         }
+        
         public Set<VersionedName> getUpgradesForType(VersionedName type) {
             return findUpgradesIn(type, upgradesProvidedByTypes);
         }
+        
         private static Set<VersionedName> findUpgradesIn(VersionedName item, Multimap<VersionedName,VersionRangedName> upgradesMap) {
             Set<VersionedName> result = new TreeSet<>(VersionedNameComparator.INSTANCE);
             for (Map.Entry<VersionedName,VersionRangedName> n: upgradesMap.entries()) {
@@ -350,6 +352,42 @@ public class BundleUpgradeParser {
             return builder().build();
         }
 
+        /**
+         * If the given bundle (vName) has been forcibly removed, return the bundle that
+         * upgrades it (if any).
+         * 
+         * The result will be one of:
+         * <ul>
+         *   <li>{@code Maybe.absent} if the bundle has not been forcibly removed
+         *   <li>{@code Maybe.of(replacementBundle} if the bundle has been forcibly removed
+         *       and there is a bundle for upgrading it.
+         *   <li>{@code Maybe.of(null} if the bundle has been forcibly removed, but there is no upgrade available.
+         * </ul>
+         * 
+         * See {@link BundleUpgradeParser#MANIFEST_HEADER_FORCE_REMOVE_BUNDLES}
+         */
+        @Beta
+        public static Maybe<VersionedName> tryGetBundleForcedReplaced(ManagementContext mgmt, VersionedName vName) {
+            // mgmt can be null in some edge cases, eg BasicCatalogItemRebindSupport.possiblyUpgradedBundle 
+            if (vName==null || mgmt==null) return Maybe.absent();
+            Maybe<OsgiManager> osgi = ((ManagementContextInternal)mgmt).getOsgiManager();
+            if (osgi.isAbsent()) {
+                // ignore upgrades if not osgi
+                return Maybe.absent();
+            }
+            
+            CatalogUpgrades cu = getFromManagementContext(mgmt);
+            if (!cu.isBundleRemoved(vName)) {
+                return Maybe.absent();
+            }
+            Set<VersionedName> upgrades = cu.getUpgradesForBundle(vName);
+            if (upgrades.isEmpty()) {
+                return Maybe.of((VersionedName)null);
+            } else {
+                return Maybe.of(upgrades.iterator().next());
+            }
+        }
+
         @Beta
         public static String getBundleUpgradedIfNecessary(ManagementContext mgmt, String vName) {
             // mgmt can be null in some edge cases, eg BasicCatalogItemRebindSupport.possiblyUpgradedBundle 
@@ -365,6 +403,7 @@ public class BundleUpgradeParser {
             }
             return getItemUpgradedIfNecessary(mgmt, vName, (cu,vn) -> cu.getUpgradesForBundle(vn));
         }
+        
         @Beta
         public static String getTypeUpgradedIfNecessary(ManagementContext mgmt, String vName) {
             if (vName==null || mgmt.getTypeRegistry().get(vName)!=null) {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b9710776/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherUpgradeCatalogOsgiTest.java
----------------------------------------------------------------------
diff --git a/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherUpgradeCatalogOsgiTest.java b/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherUpgradeCatalogOsgiTest.java
index 3bd73dc..01c2b68 100644
--- a/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherUpgradeCatalogOsgiTest.java
+++ b/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherUpgradeCatalogOsgiTest.java
@@ -21,6 +21,9 @@ package org.apache.brooklyn.launcher;
 import static org.apache.brooklyn.core.typereg.BundleUpgradeParser.MANIFEST_HEADER_FORCE_REMOVE_BUNDLES;
 import static org.apache.brooklyn.core.typereg.BundleUpgradeParser.MANIFEST_HEADER_FORCE_REMOVE_LEGACY_ITEMS;
 import static org.apache.brooklyn.core.typereg.BundleUpgradeParser.MANIFEST_HEADER_UPGRADE_FOR_BUNDLES;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
 
 import java.io.File;
 import java.net.URI;
@@ -29,6 +32,9 @@ import org.apache.brooklyn.api.entity.Application;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.Group;
 import org.apache.brooklyn.core.catalog.internal.CatalogInitialization;
+import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult;
+import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult.ResultCode;
+import org.apache.brooklyn.util.exceptions.ReferenceWithError;
 import org.apache.brooklyn.util.osgi.VersionedName;
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
@@ -205,4 +211,201 @@ public class BrooklynLauncherUpgradeCatalogOsgiTest extends AbstractBrooklynLaun
         
         launcher.terminate();
     }
+
+    @Test
+    public void testForciblyRemovedBundleNotAdded() throws Exception {
+        runForciblyRemovedBundleNotAdded(true);
+    }
+    
+    @Test
+    public void testForciblyRemovedBundleNotAddedWithNoUpgradeTarget() throws Exception {
+        runForciblyRemovedBundleNotAdded(false);
+    }
+    
+    protected void runForciblyRemovedBundleNotAdded(boolean hasUpgradeForBundle) throws Exception {
+        VersionedName one_1_0_0 = VersionedName.fromString("one:1.0.0");
+        VersionedName one_2_0_0 = VersionedName.fromString("one:2.0.0");
+        
+        BundleFile bundleV1 = bundleBuilder()
+                .name("org.example.bundleBeingUpgraded", "1.0.0")
+                .catalogBom(ImmutableList.<URI>of(), ImmutableSet.<VersionedName>of(one_1_0_0))
+                .build();
+
+        // A bundle that references the forcibly-replaced bundle via url;
+        // (add this to persisted state).
+        BundleFile bundleWithReferenceUrl = bundleBuilder()
+                .name("org.example.bundleWithReferenceUrl", "1.0.0")
+                .catalogBom(Joiner.on("\n").join(
+                        "brooklyn.catalog:",
+                        "  version: 1.0.0",
+                        "  brooklyn.libraries:",
+                        "  - "+bundleV1.getFile().toURI()))
+                .build();
+
+        newPersistedStateInitializer()
+                .bundle(bundleV1)
+                .bundle(bundleWithReferenceUrl)
+                .initState();
+        
+        BundleFile bundleV2 = bundleBuilder()
+                .name(bundleV1.getVersionedName().getSymbolicName(), "2.0.0")
+                .catalogBom(ImmutableList.<URI>of(), ImmutableSet.<VersionedName>of(one_2_0_0))
+                .manifestLines(ImmutableMap.<String, String>builder()
+                        .put(MANIFEST_HEADER_FORCE_REMOVE_BUNDLES, "\"*\"")
+                        .put(hasUpgradeForBundle ? MANIFEST_HEADER_UPGRADE_FOR_BUNDLES : "disabled-upgrade", "\"*\"")
+                        .build())
+                .build();
+
+        File initialBomFile = newTmpFile(createCatalogYaml(ImmutableList.of(bundleV2.getFile().toURI()), ImmutableList.of()));
+
+        BrooklynLauncher launcher = newLauncherForTests(initialBomFile.getAbsolutePath());
+        launcher.start();
+        assertHealthyMaster(launcher);
+        assertCatalogConsistsOfIds(launcher, ImmutableList.of(one_2_0_0));
+        assertManagedBundle(launcher, bundleV2.getVersionedName(), ImmutableSet.<VersionedName>of(one_2_0_0));
+        assertNotManagedBundle(launcher, bundleV1.getVersionedName());
+
+        // Try installing the removed bundle explicitly (force=false)
+        ReferenceWithError<OsgiBundleInstallationResult> resultWithoutForce = installBundle(launcher, bundleV1.getFile(), false);
+        resultWithoutForce.checkNoError();
+        ResultCode resultWithoutForceCode = resultWithoutForce.get().getCode();
+        String resultWithoutForceMessage = resultWithoutForce.get().getMessage();
+        assertEquals(resultWithoutForceCode, ResultCode.IGNORING_BUNDLE_FORCIBLY_REMOVED);
+        if (hasUpgradeForBundle) {
+            assertEquals(resultWithoutForce.get().getMetadata().getVersionedName(), bundleV2.getVersionedName());
+            assertTrue(resultWithoutForceMessage.contains("Bundle "+bundleV1.getVersionedName()+" forcibly removed, upgraded to 2.0.0"), "msg="+resultWithoutForceMessage);
+        } else {
+            assertNull(resultWithoutForce.get().getMetadata());
+            assertTrue(resultWithoutForceMessage.contains("Bundle "+bundleV1.getVersionedName()+" forcibly removed, no upgrade defined"), "msg="+resultWithoutForceMessage);
+        }
+
+        // Try installing the removed bundle explicitly (force=true)
+        ReferenceWithError<OsgiBundleInstallationResult> resultWithForce = installBundle(launcher, bundleV1.getFile(), true);
+        resultWithForce.checkNoError();
+        ResultCode resultWithForceCode = resultWithForce.get().getCode();
+        String resultWithForceMessage = resultWithForce.get().getMessage();
+        assertEquals(resultWithForceCode, ResultCode.IGNORING_BUNDLE_FORCIBLY_REMOVED);
+        if (hasUpgradeForBundle) {
+            assertEquals(resultWithForce.get().getMetadata().getVersionedName(), bundleV2.getVersionedName());
+            assertTrue(resultWithForceMessage.contains("Bundle "+bundleV1.getVersionedName()+" forcibly removed, upgraded to 2.0.0"), "msg="+resultWithForceMessage);
+        } else {
+            assertNull(resultWithoutForce.get().getMetadata());
+            assertTrue(resultWithForceMessage.contains("Bundle "+bundleV1.getVersionedName()+" forcibly removed, no upgrade defined"), "msg="+resultWithForceMessage);
+        }
+
+        launcher.terminate();
+    }
+    
+    @Test
+    public void testForciblyRemovedBundleNotAddedWhenReferencedByName() throws Exception {
+        VersionedName one_1_0_0 = VersionedName.fromString("one:1.0.0");
+        VersionedName one_2_0_0 = VersionedName.fromString("one:2.0.0");
+        
+        BundleFile bundleV1 = bundleBuilder()
+                .name("org.example.bundleBeingUpgraded", "1.0.0")
+                .catalogBom(ImmutableList.<URI>of(), ImmutableSet.<VersionedName>of(one_1_0_0))
+                .build();
+
+        // A bundle that references the forcibly-replaced bundle via name:version;
+        // (add this to persisted state).
+        BundleFile bundleWithReferenceByNameVersionName = bundleBuilder()
+                .name("org.example.bundleWithReferenceByName", "1.0.0")
+                .catalogBom(Joiner.on("\n").join(
+                        "brooklyn.catalog:",
+                        "  version: 1.0.0",
+                        "  brooklyn.libraries:",
+                        "  - name: "+bundleV1.getVersionedName().getSymbolicName(),
+                        "    version: 1.0.0"))
+                .build();
+
+        newPersistedStateInitializer()
+                .bundle(bundleV1)
+                .bundle(bundleWithReferenceByNameVersionName)
+                .initState();
+        
+        BundleFile bundleV2 = bundleBuilder()
+                .name(bundleV1.getVersionedName().getSymbolicName(), "2.0.0")
+                .catalogBom(ImmutableList.<URI>of(), ImmutableSet.<VersionedName>of(one_2_0_0))
+                .manifestLines(ImmutableMap.<String, String>builder()
+                        .put(MANIFEST_HEADER_FORCE_REMOVE_BUNDLES, "\"*\"")
+                        .put(MANIFEST_HEADER_UPGRADE_FOR_BUNDLES, "\"*\"")
+                        .build())
+                .build();
+
+        File initialBomFile = newTmpFile(createCatalogYaml(ImmutableList.of(bundleV2.getFile().toURI()), ImmutableList.of()));
+
+        BrooklynLauncher launcher = newLauncherForTests(initialBomFile.getAbsolutePath());
+        launcher.start();
+        assertHealthyMaster(launcher);
+        assertCatalogConsistsOfIds(launcher, ImmutableList.of(one_2_0_0));
+        assertManagedBundle(launcher, bundleV2.getVersionedName(), ImmutableSet.<VersionedName>of(one_2_0_0));
+        assertNotManagedBundle(launcher, bundleV1.getVersionedName());
+
+        // Try installing the removed bundle explicitly (force=false)
+        ReferenceWithError<OsgiBundleInstallationResult> resultWithoutForce = installBundle(launcher, bundleV1.getFile(), false);
+        resultWithoutForce.checkNoError();
+        ResultCode resultWithoutForceCode = resultWithoutForce.get().getCode();
+        String resultWithoutForceMessage = resultWithoutForce.get().getMessage();
+        assertEquals(resultWithoutForceCode, ResultCode.IGNORING_BUNDLE_FORCIBLY_REMOVED);
+        assertEquals(resultWithoutForce.get().getMetadata().getVersionedName(), bundleV2.getVersionedName());
+        assertTrue(resultWithoutForceMessage.contains("Bundle "+bundleV1.getVersionedName()+" forcibly removed, upgraded to 2.0.0"), "msg="+resultWithoutForceMessage);
+        
+        launcher.terminate();
+    }
+    
+    @Test
+    public void testForciblyRemovedBundleNotAddedWhenReferencedByMvnUrl() throws Exception {
+        VersionedName one_1_0_0 = VersionedName.fromString("one:1.0.0");
+        VersionedName one_2_0_0 = VersionedName.fromString("one:2.0.0");
+        
+        BundleFile bundleV1 = bundleBuilder()
+                .name("org.example.bundleBeingUpgraded", "1.0.0")
+                .catalogBom(ImmutableList.<URI>of(), ImmutableSet.<VersionedName>of(one_1_0_0))
+                .build();
+
+        // A bundle that references the forcibly-replaced bundle via name:version;
+        // (add this to persisted state).
+        BundleFile bundleWithReferenceByNameVersionName = bundleBuilder()
+                .name("org.example.bundleWithReferenceByName", "1.0.0")
+                .catalogBom(Joiner.on("\n").join(
+                        "brooklyn.catalog:",
+                        "  version: 1.0.0",
+                        "  brooklyn.libraries:",
+                        "  - mvn:org.example/bundleWithReferenceByName/1.0.0"))
+                .build();
+
+        newPersistedStateInitializer()
+                .bundle(bundleV1)
+                .bundle(bundleWithReferenceByNameVersionName)
+                .initState();
+        
+        BundleFile bundleV2 = bundleBuilder()
+                .name(bundleV1.getVersionedName().getSymbolicName(), "2.0.0")
+                .catalogBom(ImmutableList.<URI>of(), ImmutableSet.<VersionedName>of(one_2_0_0))
+                .manifestLines(ImmutableMap.<String, String>builder()
+                        .put(MANIFEST_HEADER_FORCE_REMOVE_BUNDLES, "\"*\"")
+                        .put(MANIFEST_HEADER_UPGRADE_FOR_BUNDLES, "\"*\"")
+                        .build())
+                .build();
+
+        File initialBomFile = newTmpFile(createCatalogYaml(ImmutableList.of(bundleV2.getFile().toURI()), ImmutableList.of()));
+
+        BrooklynLauncher launcher = newLauncherForTests(initialBomFile.getAbsolutePath());
+        launcher.start();
+        assertHealthyMaster(launcher);
+        assertCatalogConsistsOfIds(launcher, ImmutableList.of(one_2_0_0));
+        assertManagedBundle(launcher, bundleV2.getVersionedName(), ImmutableSet.<VersionedName>of(one_2_0_0));
+        assertNotManagedBundle(launcher, bundleV1.getVersionedName());
+
+        // Try installing the removed bundle explicitly (force=false)
+        ReferenceWithError<OsgiBundleInstallationResult> resultWithoutForce = installBundle(launcher, bundleV1.getFile(), false);
+        resultWithoutForce.checkNoError();
+        ResultCode resultWithoutForceCode = resultWithoutForce.get().getCode();
+        String resultWithoutForceMessage = resultWithoutForce.get().getMessage();
+        assertEquals(resultWithoutForceCode, ResultCode.IGNORING_BUNDLE_FORCIBLY_REMOVED);
+        assertEquals(resultWithoutForce.get().getMetadata().getVersionedName(), bundleV2.getVersionedName());
+        assertTrue(resultWithoutForceMessage.contains("Bundle "+bundleV1.getVersionedName()+" forcibly removed, upgraded to 2.0.0"), "msg="+resultWithoutForceMessage);
+        
+        launcher.terminate();
+    }
 }