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 2020/12/07 19:42:54 UTC

[brooklyn-server] 03/06: support for suspend and shutdown and getStatus and makeRunning on API and with jclouds

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

commit 88a5d6c91034e08e61cb35fe8ebb788bf99ad98f
Author: Alex Heneveld <al...@cloudsoftcorp.com>
AuthorDate: Fri Dec 4 09:15:06 2020 +0000

    support for suspend and shutdown and getStatus and makeRunning on API and with jclouds
    
    (but shutdown actually just invokes suspend, because jclouds doesnt support "shutdown")
---
 .../api/location/MachineManagementMixins.java      |  14 +-
 .../core/entity/trait/StartableMethods.java        |  11 +-
 .../core/location/AbstractMachineLocation.java     |   4 +
 .../core/location/BasicMachineMetadata.java        |  25 +-
 .../core/location/MachineLifecycleUtils.java       | 288 +++++++++++++++++++++
 .../brooklyn/location/ssh/SshMachineLocation.java  |  12 +-
 .../jclouds/DefaultConnectivityResolver.java       |   5 +-
 .../brooklyn/location/jclouds/JcloudsLocation.java |  73 ++++--
 .../location/jclouds/JcloudsMachineLocation.java   |  10 +-
 .../jclouds/JcloudsSshMachineLocation.java         |  12 +-
 ...cloudsLocationSuspendResumeMachineLiveTest.java |  46 +++-
 .../resources/brooklyn/logback-logger-excludes.xml |   4 +
 .../lifecycle/MachineLifecycleEffectorTasks.java   |   6 +-
 13 files changed, 458 insertions(+), 52 deletions(-)

diff --git a/api/src/main/java/org/apache/brooklyn/api/location/MachineManagementMixins.java b/api/src/main/java/org/apache/brooklyn/api/location/MachineManagementMixins.java
index cf6fa25..7043675 100644
--- a/api/src/main/java/org/apache/brooklyn/api/location/MachineManagementMixins.java
+++ b/api/src/main/java/org/apache/brooklyn/api/location/MachineManagementMixins.java
@@ -21,6 +21,7 @@ package org.apache.brooklyn.api.location;
 import java.util.Map;
 
 import com.google.common.annotations.Beta;
+import org.apache.brooklyn.util.collections.MutableMap;
 
 /**
  * Defines mixins for interesting locations.
@@ -83,7 +84,9 @@ public class MachineManagementMixins {
     @Beta
     public interface ResumesMachines {
         /**
-         * Resume the indicated machine.
+         * Resume the indicated machine. Map should have at minimum the `id` of the machine and
+         * the `user` and any other location-specific config keys for connecting subsequently.
+         * May return the original {@link MachineLocation} if found or may return a new {@link MachineLocation} if data might have changed.
          */
         MachineLocation resumeMachine(Map<?, ?> flags);
     }
@@ -97,19 +100,20 @@ public class MachineManagementMixins {
         void shutdownMachine(MachineLocation location);
 
         /**
-         * Ensure that a machine that might have been shutdown is running, or throw if not possible.
-         * May return the original {@link MachineLocation} or may return a new {@link MachineLocation} if data might have changed.
+         * Start up the indicated machine that might have been shutdown, or throw if not possible.
+         * May return the original {@link MachineLocation} if found or may return a new {@link MachineLocation} if data might have changed.
          */
-        MachineLocation startupMachine(MachineLocation location);
+        MachineLocation startupMachine(Map<?, ?> flags);
 
         /** Issues a reboot command via the machine location provider (not on-box), or does a shutdown/startup pair
          * (but only if the implementation of {@link #shutdownMachine(MachineLocation)} does a true machine stop, not a suspend).
          */
-        MachineLocation rebootMachine(MachineLocation location);
+        void rebootMachine(MachineLocation location);
     }
 
     @Beta
     public interface GivesMetrics {
+
         /**
          * Gets metrics of a machine within a location. The actual metrics supported depends on the implementation, which should advise which config keys it supports.
          */
diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/trait/StartableMethods.java b/core/src/main/java/org/apache/brooklyn/core/entity/trait/StartableMethods.java
index 30b2348..933dd25 100644
--- a/core/src/main/java/org/apache/brooklyn/core/entity/trait/StartableMethods.java
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/trait/StartableMethods.java
@@ -48,22 +48,23 @@ public class StartableMethods {
 
     /** Common implementation for start in parent nodes; just invokes start on all children of the entity */
     public static void start(Entity e, Collection<? extends Location> locations) {
-        log.debug("Starting entity "+e+" at "+locations);
+        log.debug("Starting children of entity "+e+" at "+locations);
         DynamicTasks.get(startingChildren(e, locations), e);
+        log.debug("Started children of entity "+e);
     }
     
     /** Common implementation for stop in parent nodes; just invokes stop on all children of the entity */
     public static void stop(Entity e) {
-        log.debug("Stopping entity "+e);
+        log.debug("Stopping children of entity "+e);
         DynamicTasks.get(stoppingChildren(e), e);
-        if (log.isDebugEnabled()) log.debug("Stopped entity "+e);
+        log.debug("Stopped children of entity "+e);
     }
 
     /** Common implementation for restart in parent nodes; just invokes restart on all children of the entity */
     public static void restart(Entity e) {
-        log.debug("Restarting entity "+e);
+        log.debug("Restarting children of entity "+e);
         DynamicTasks.get(restartingChildren(e), e);
-        if (log.isDebugEnabled()) log.debug("Restarted entity "+e);
+        log.debug("Restarted children of entity "+e);
     }
     
     private static <T extends Entity> Iterable<T> filterStartableManagedEntities(Iterable<T> contenders) {
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/AbstractMachineLocation.java b/core/src/main/java/org/apache/brooklyn/core/location/AbstractMachineLocation.java
index ae9e819..3775016 100644
--- a/core/src/main/java/org/apache/brooklyn/core/location/AbstractMachineLocation.java
+++ b/core/src/main/java/org/apache/brooklyn/core/location/AbstractMachineLocation.java
@@ -18,14 +18,18 @@
  */
 package org.apache.brooklyn.core.location;
 
+import java.util.List;
 import java.util.Map;
 
+import java.util.Objects;
+import java.util.function.Function;
 import org.apache.brooklyn.api.location.MachineDetails;
 import org.apache.brooklyn.api.location.MachineLocation;
 import org.apache.brooklyn.api.location.OsDetails;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.entity.AbstractEntity;
+import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.mutex.MutexSupport;
 import org.apache.brooklyn.util.core.mutex.WithMutexes;
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/BasicMachineMetadata.java b/core/src/main/java/org/apache/brooklyn/core/location/BasicMachineMetadata.java
index fd0891f..0f7cef8 100644
--- a/core/src/main/java/org/apache/brooklyn/core/location/BasicMachineMetadata.java
+++ b/core/src/main/java/org/apache/brooklyn/core/location/BasicMachineMetadata.java
@@ -22,19 +22,33 @@ import org.apache.brooklyn.api.location.MachineManagementMixins;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
+import org.apache.brooklyn.core.location.MachineLifecycleUtils.GivesMachineStatus;
+import org.apache.brooklyn.core.location.MachineLifecycleUtils.MachineStatus;
 
-public class BasicMachineMetadata implements MachineManagementMixins.MachineMetadata {
+public class BasicMachineMetadata implements MachineManagementMixins.MachineMetadata, GivesMachineStatus {
 
     final String id, name, primaryIp;
     final Boolean isRunning;
+    final MachineStatus status;
     final Object originalMetadata;
-    
+
+    public BasicMachineMetadata(String id, String name, String primaryIp, MachineStatus status, Object originalMetadata) {
+        super();
+        this.id = id;
+        this.name = name;
+        this.primaryIp = primaryIp;
+        this.status = status;
+        this.isRunning = MachineStatus.RUNNING.equals(status);
+        this.originalMetadata = originalMetadata;
+    }
+    @Deprecated /** @deprecated since 1.1, use other constructor */
     public BasicMachineMetadata(String id, String name, String primaryIp, Boolean isRunning, Object originalMetadata) {
         super();
         this.id = id;
         this.name = name;
         this.primaryIp = primaryIp;
         this.isRunning = isRunning;
+        this.status = Boolean.TRUE.equals(isRunning) ? MachineStatus.RUNNING : MachineStatus.UNKNOWN;
         this.originalMetadata = originalMetadata;
     }
 
@@ -59,6 +73,13 @@ public class BasicMachineMetadata implements MachineManagementMixins.MachineMeta
     }
 
     @Override
+    public MachineStatus getStatus() {
+        if (status!=null) return status;
+        if (isRunning()) return MachineStatus.RUNNING;
+        return MachineStatus.UNKNOWN;
+    }
+
+    @Override
     public Object getOriginalMetadata() {
         return originalMetadata;
     }
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/MachineLifecycleUtils.java b/core/src/main/java/org/apache/brooklyn/core/location/MachineLifecycleUtils.java
new file mode 100644
index 0000000..75c86a6
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/MachineLifecycleUtils.java
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location;
+
+import com.google.common.base.Preconditions;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.apache.brooklyn.api.location.*;
+import org.apache.brooklyn.api.location.MachineManagementMixins.GivesMachineMetadata;
+import org.apache.brooklyn.api.location.MachineManagementMixins.GivesMetrics;
+import org.apache.brooklyn.api.location.MachineManagementMixins.MachineMetadata;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.objs.BrooklynObjectInternal.ConfigurationSupportInternal;
+import org.apache.brooklyn.util.JavaGroovyEquivalents;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.exceptions.ReferenceWithError;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.time.CountdownTimer;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.brooklyn.util.time.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MachineLifecycleUtils {
+
+    private static final Logger LOG = LoggerFactory.getLogger(MachineLifecycleUtils.class);
+
+    private final MachineLocation location;
+
+    public static ConfigKey<MachineStatus> STATUS = ConfigKeys.newConfigKey(MachineStatus.class, "status");
+
+    public MachineLifecycleUtils(MachineLocation l) {
+        this.location = l;
+    }
+
+    public enum MachineStatus {
+        /** Either the machine is unknown at the location, or the machine may be known but status unknown or unrecognized,
+         * or we are unable to find out. Can use {@link #exists()} to determine the difference between these cases (respectively: false, true, null). */
+        UNKNOWN,
+        RUNNING,
+        SHUTDOWN,
+        /** Note some providers report 'shutdown' as 'suspended' if they cannot tell the difference. */
+        SUSPENDED,
+        TRANSITIONING,
+        ERROR
+    }
+
+    public interface GivesMachineStatus {
+        MachineStatus getStatus();
+    }
+
+    /** true if the machine is known at its parent (at the actual provider), false if not known; null if cannot tell */
+    @Nullable
+    public Boolean exists() {
+        if (location.getParent() instanceof MachineManagementMixins.GivesMachineMetadata) {
+            MachineMetadata metadata = ((GivesMachineMetadata) location.getParent()).getMachineMetadata(location);
+            return (metadata != null);
+        }
+        if (location.getParent() instanceof MachineManagementMixins.GivesMetrics) {
+            ConfigBag metrics = ConfigBag.newInstance(((GivesMetrics) location.getParent()).getMachineMetrics(location));
+            return (!metrics.isEmpty());
+        }
+        return null;
+    }
+
+    @Nonnull
+    public MachineStatus getStatus() {
+        if (location.getParent() instanceof MachineManagementMixins.GivesMetrics) {
+            ConfigBag metrics = ConfigBag.newInstance(((GivesMetrics) location.getParent()).getMachineMetrics(location));
+            MachineStatus s = metrics.get(STATUS);
+            if (s!=null) {
+                return s;
+            }
+        }
+        if (location.getParent() instanceof MachineManagementMixins.GivesMachineMetadata) {
+            MachineMetadata metadata = ((GivesMachineMetadata) location.getParent()).getMachineMetadata(location);
+            if (metadata!=null) {
+                if (metadata instanceof GivesMachineStatus) {
+                    return ((GivesMachineStatus)metadata).getStatus();
+                }
+                if (metadata.isRunning()) {
+                    return MachineStatus.RUNNING;
+                } else {
+                    return MachineStatus.UNKNOWN;
+                }
+            }
+        }
+        return MachineStatus.UNKNOWN;
+    }
+
+    Duration timeout = Duration.minutes(30);
+    /** How long to delay on async operations, e.g. resume, including if machine was previuosly transitioning. Defaults 30 minutes. */
+    public Duration getTimeout() {
+        return timeout;
+    }
+    public void setTimeout(Duration timeout) {
+        this.timeout = Preconditions.checkNotNull(timeout);
+    }
+
+    /**
+     * Returns a masked error if the machine is already running.
+     * Unmasked error if the machine is not resumable and we cannot confirm it is already running.
+     * Otherwise returns the resumed machines status.
+     * <p>
+     * If in a transitional state will wait for the indicated timeout.
+     * <p>
+     * May return a different machine location than the one known here if critical metadata has changed (e.g. IP address);
+     * however the instance/ID will be the same.
+     */
+    // TODO kill this?
+    public ReferenceWithError<MachineLocation> resume() {
+        MachineStatus status = getStatus();
+        if (MachineStatus.RUNNING.equals(status)) return ReferenceWithError.newInstanceMaskingError(location, new Throwable("Already running"));
+
+        if (location.getParent() instanceof MachineManagementMixins.ResumesMachines) {
+            try {
+                return ReferenceWithError.newInstanceWithoutError( ((MachineManagementMixins.ResumesMachines) location.getParent()).resumeMachine(getConfigMapWithId()) );
+            } catch (Exception e) {
+                return ReferenceWithError.newInstanceThrowingError( location, e );
+            }
+        }
+
+        return ReferenceWithError.newInstanceThrowingError(location, new Throwable("Machine does not support resumption"));
+    }
+
+    /** Returns supplied machine if already running; otherwise if suspended, resumes; if shutdown, starts it up and may return same or different object.
+     * If can't restart, or can't detect, it returns an absent. */
+    public Maybe<MachineLocation> makeRunning() {
+
+        CountdownTimer timer = CountdownTimer.newInstanceStarted(getTimeout());
+
+        MachineStatus status = getStatus();
+        Duration sleep = Duration.ONE_SECOND;
+        while (MachineStatus.TRANSITIONING.equals(status)) {
+            if (timer.isExpired()) {
+                return Maybe.absent("Timeout waiting for "+location+" to be stable before running");
+            }
+            try {
+                Duration s = sleep;
+                Tasks.withBlockingDetails("waiting on "+location+" to be stable before running", () -> {
+                    Time.sleep(Duration.min(s, timer.getDurationRemaining()));
+                    return null;
+                });
+            } catch (Exception e) {
+                return Maybe.absent(new IllegalStateException("Error waiting for "+location+" to be stable before running", e));
+            }
+            status = getStatus();
+            sleep = Duration.min(sleep.multiply(1.2), Duration.ONE_MINUTE);
+        }
+
+        if (MachineStatus.RUNNING.equals(status)) return Maybe.of(location);
+
+        if (MachineStatus.SUSPENDED.equals(status) && location.getParent() instanceof MachineManagementMixins.ResumesMachines) {
+            return Maybe.of( ((MachineManagementMixins.ResumesMachines) location.getParent()).resumeMachine(getConfigMapWithId()) );
+        }
+        if (MachineStatus.SHUTDOWN.equals(status) && location.getParent() instanceof MachineManagementMixins.ShutsdownMachines) {
+            return Maybe.of( ((MachineManagementMixins.ShutsdownMachines) location.getParent()).startupMachine(getConfigMapWithId()) );
+        }
+
+        return Maybe.absent("Unable to make "+location+" running from status "+status+"; no methods for doing so are available");
+    }
+
+    public MachineLocation makeRunningOrRecreate(Map<String,?> newConfig) throws NoMachinesAvailableException {
+        Exception problemRunning = null;
+        Maybe<MachineLocation> result = null;
+        try {
+            result = makeRunning();
+            if (result.isPresent()) {
+                return result.get();
+            }
+            problemRunning = Maybe.Absent.getException(result);
+            // couldn't resume etc
+
+        } catch (Exception e) {
+            problemRunning = e;
+        }
+
+        if (problemRunning!=null) {
+            LOG.warn("Unable to make existing machine running (" + location + "), will destroy and re-create: " + problemRunning);
+            if (LOG.isTraceEnabled()) {
+                LOG.trace("Trace for: Unable to make existing machine running (" + location + "), will destroy and re-create: " + problemRunning, problemRunning);
+            }
+
+            DynamicTasks.queueIfPossible(Tasks.warning("Could not make existing machine running: " + Exceptions.collapseText(problemRunning), problemRunning));
+        }
+
+        if (!(location.getParent() instanceof MachineProvisioningLocation)) {
+            throw new IllegalStateException("Cannot destroy/recreate "+location+" because parent is not a provisioning location, and cannot resume due to: "+problemRunning);
+
+        }
+        ((MachineProvisioningLocation)location.getParent()).release(location);
+
+        return ((MachineProvisioningLocation)location.getParent()).obtain(newConfig);
+    }
+
+    public ConfigBag getConfig() {
+        return ((ConfigurationSupportInternal)location.config()).getBag();
+    }
+
+    public Map<String,?> getConfigMapWithId() {
+        MutableMap<String, Object> result = MutableMap.copyOf(getConfig().getAllConfig());
+        if (location.getParent() instanceof MachineManagementMixins.GivesMachineMetadata) {
+            MachineMetadata metadata = ((GivesMachineMetadata) location.getParent()).getMachineMetadata(location);
+            if (metadata!=null) {
+                result.add("id", metadata.getId());
+            }
+        }
+        return result;
+    }
+
+    /** If two locations point at the same instance; primarily looking at instance IDs, optionally also looking at addressibility.
+     *
+     * Locations are "primarily" the same if they have the same instance ID.
+     * (Would be nice to confirm that the parents are the same, but that's harder, and not necessary for our use cases.)
+     * <p>
+     * "Optionally" they can be addressible the same if they have the same public and private IPs and user access (maybe creds, depending on implementations).
+     * <p>
+     * Null may be returned if we cannot tell.
+     */
+    public static Boolean isSameInstance(MachineLocation m1, MachineLocation m2, boolean requireSameAccessDetails) {
+        Boolean primarilySame = null;
+        Location p1 = m1.getParent();
+        Location p2 = m2.getParent();
+        if (p1 instanceof GivesMachineMetadata || p2 instanceof GivesMachineMetadata) {
+            if (p1 instanceof GivesMachineMetadata && p2 instanceof GivesMachineMetadata) {
+                String m1id = ((GivesMachineMetadata)p1).getMachineMetadata(m1).getId();
+                String m2id = ((GivesMachineMetadata)p2).getMachineMetadata(m2).getId();
+                if (Objects.equals(m1id, m2id)) {
+                    if (m1id!=null) {
+                        primarilySame = true;
+                    }  else {
+                        // indeterminate
+                    }
+                } else {
+                    primarilySame = false;
+                }
+            } else {
+                return false;
+            }
+        }
+
+        if (Boolean.FALSE.equals(primarilySame)) return false;
+
+        if (requireSameAccessDetails) {
+            List<Function<MachineLocation,Object>> reqs = MutableList.of();
+            reqs.add(MachineLocation::getUser);
+            reqs.add(MachineLocation::getHostname);
+            reqs.add(MachineLocation::getAddress);
+            reqs.add(MachineLocation::getPrivateAddresses);
+            reqs.add(MachineLocation::getPublicAddresses);
+
+            for (Function<MachineLocation,Object> f: reqs) {
+                Object v1 = f.apply(m1);
+                if (!Objects.equals(v1, f.apply(m2))) return false;
+                if (JavaGroovyEquivalents.groovyTruth(v1)) primarilySame = true;
+            }
+        }
+
+        return primarilySame;
+    }
+
+}
diff --git a/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java b/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java
index d5ed481..577e1d9 100644
--- a/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java
+++ b/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java
@@ -950,12 +950,22 @@ public class SshMachineLocation extends AbstractMachineLocation implements Machi
         }
     }
 
+    Duration sshCheckTimeout = null;
+    @Beta
+    public void setSshCheckTimeout(Duration sshCheckTimeout) {
+        this.sshCheckTimeout = sshCheckTimeout;
+    }
+    @Beta
+    public Duration getSshCheckTimeout() {
+        return Maybe.ofDisallowingNull(sshCheckTimeout).or(Duration.millis(SSHABLE_CONNECT_TIMEOUT));
+    }
+
     public boolean isSshable() {
         String cmd = "date";
         try {
             try {
                 Socket s = new Socket();
-                s.connect(new InetSocketAddress(getAddress(), getPort()), SSHABLE_CONNECT_TIMEOUT);
+                s.connect(new InetSocketAddress(getAddress(), getPort()), (int) getSshCheckTimeout().toMilliseconds());
                 s.close();
             } catch (IOException e) {
                 if (LOG.isDebugEnabled()) LOG.debug(""+this+" not [yet] reachable (socket "+getAddress()+":"+getPort()+"): "+e);
diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/DefaultConnectivityResolver.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/DefaultConnectivityResolver.java
index ef02d2d..f5cf624 100644
--- a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/DefaultConnectivityResolver.java
+++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/DefaultConnectivityResolver.java
@@ -31,6 +31,7 @@ import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.EntityInitializer;
 import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.BrooklynVersion;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
@@ -172,6 +173,9 @@ public class DefaultConnectivityResolver extends InitializerPatternForConfigurab
         final Stopwatch timer = Stopwatch.createStarted();
         // Should only be null in tests.
         final Entity contextEntity = getContextEntity(config);
+        if (contextEntity==null && !BrooklynVersion.isDevelopmentEnvironment()) {
+            LOG.debug("No context entity found in config or current task when resolving "+location);
+        }
         if (shouldPublishNetworks() && !options.isRebinding() && contextEntity != null) {
             publishNetworks(node, contextEntity);
         }
@@ -468,7 +472,6 @@ public class DefaultConnectivityResolver extends InitializerPatternForConfigurab
                 return taskContext;
             }
         }
-        LOG.warn("No context entity found in config or current task");
         return null;
     }
 
diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java
index 516cbf1..b03575b 100644
--- a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java
+++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java
@@ -20,7 +20,11 @@ package org.apache.brooklyn.location.jclouds;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import java.util.stream.Collectors;
+import org.apache.brooklyn.api.location.*;
 import org.apache.brooklyn.api.location.MachineManagementMixins.MachineMetadata;
+import org.apache.brooklyn.core.location.*;
+import org.apache.brooklyn.core.location.MachineLifecycleUtils.MachineStatus;
 import static org.apache.brooklyn.util.JavaGroovyEquivalents.elvis;
 import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth;
 import static org.apache.brooklyn.util.ssh.BashCommands.sbinPath;
@@ -50,24 +54,13 @@ import javax.annotation.Nullable;
 import javax.xml.ws.WebServiceException;
 
 import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.location.LocationSpec;
-import org.apache.brooklyn.api.location.MachineLocation;
-import org.apache.brooklyn.api.location.MachineLocationCustomizer;
-import org.apache.brooklyn.api.location.MachineManagementMixins;
-import org.apache.brooklyn.api.location.NoMachinesAvailableException;
-import org.apache.brooklyn.api.location.PortRange;
 import org.apache.brooklyn.api.mgmt.AccessController;
 import org.apache.brooklyn.api.mgmt.Task;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
 import org.apache.brooklyn.core.config.ConfigUtils;
 import org.apache.brooklyn.core.config.Sanitizer;
-import org.apache.brooklyn.core.location.AbstractLocation;
-import org.apache.brooklyn.core.location.BasicMachineMetadata;
-import org.apache.brooklyn.core.location.LocationConfigKeys;
-import org.apache.brooklyn.core.location.LocationConfigUtils;
 import org.apache.brooklyn.core.location.LocationConfigUtils.OsCredential;
-import org.apache.brooklyn.core.location.PortRanges;
 import org.apache.brooklyn.core.location.access.PortForwardManager;
 import org.apache.brooklyn.core.location.access.PortForwardManagerLocationResolver;
 import org.apache.brooklyn.core.location.access.PortMapping;
@@ -554,10 +547,25 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im
             return null;
         return new BasicMachineMetadata(node.getId(), node.getName(),
             ((node instanceof NodeMetadata) ? Iterators.tryFind( ((NodeMetadata)node).getPublicAddresses().iterator(), Predicates.alwaysTrue() ).orNull() : null),
-            ((node instanceof NodeMetadata) ? ((NodeMetadata)node).getStatus()==Status.RUNNING : null),
+            ((node instanceof NodeMetadata) ? toMachineStatus( ((NodeMetadata)node).getStatus() ) : null),
             node);
     }
 
+    public static MachineStatus toMachineStatus(Status status) {
+        if (status==null) return null;
+        switch (status) {
+            case PENDING: return MachineStatus.TRANSITIONING;
+            case RUNNING: return MachineStatus.RUNNING;
+            case SUSPENDED: return MachineStatus.SUSPENDED;
+            case ERROR: return MachineStatus.ERROR;
+
+            case TERMINATED:
+            case UNRECOGNIZED:
+                //below
+        }
+        return MachineStatus.UNKNOWN;
+    }
+
     @Override
     public MachineManagementMixins.MachineMetadata getMachineMetadata(MachineLocation l) {
         if (l instanceof JcloudsSshMachineLocation) {
@@ -1212,7 +1220,7 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im
      */
     @Override
     public void suspendMachine(MachineLocation rawLocation) {
-        String instanceId = vmInstanceIds.remove(rawLocation);
+        String instanceId = vmInstanceIds.get(rawLocation);
         if (instanceId == null) {
             LOG.info("Attempt to suspend unknown machine " + rawLocation + " in " + this);
             throw new IllegalArgumentException("Unknown machine " + rawLocation);
@@ -1225,7 +1233,8 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im
             toThrow = e;
             LOG.error("Problem suspending machine " + rawLocation + " in " + this + ", instance id " + instanceId, e);
         }
-        removeChild(rawLocation);
+        // before 2020-12 we removed the child; we don't actually want to, as it still exists; and it can trigger a release which could destroy it
+        //removeChild(rawLocation);
         if (toThrow != null) {
             throw Exceptions.propagate(toThrow);
         }
@@ -1236,6 +1245,8 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im
      * <p/>
      * Note that this method does <b>not</b> call the lifecycle methods of any
      * {@link #getCustomizers(ConfigBag) customizers} attached to this location.
+     * <p/>
+     * Also note other machines with the same ID may be unmanaged as part of this.
      *
      * @param flags See {@link #registerMachine(ConfigBag)} for a description of required fields.
      * @see #registerMachine(ConfigBag)
@@ -1243,17 +1254,33 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im
     @Override
     public JcloudsMachineLocation resumeMachine(Map<?, ?> flags) {
         ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
-        LOG.info("{} using resuming node matching properties: {}", this, Sanitizer.sanitize(setup));
+        LOG.info("Resuming machine in {} matching properties {}", this, Sanitizer.sanitize(setup));
         ComputeService computeService = getComputeService(setup);
         NodeMetadata node = findNodeOrThrow(setup);
-        LOG.debug("{} resuming {}", this, node);
+        LOG.debug("{} resuming node {}", this, node);
         computeService.resumeNode(node.getId());
         // Load the node a second time once it is resumed to get an object with
         // hostname and addresses populated.
         node = findNodeOrThrow(setup);
-        LOG.debug("{} resumed {}", this, node);
+        LOG.debug("{} resumed node {}", this, node);
         JcloudsMachineLocation registered = registerMachineLocation(setup, node);
-        LOG.info("{} resumed and registered {}", this, registered);
+        boolean madeNew = true;
+        for (Location l : getChildren()) {
+            if (l instanceof JcloudsMachineLocation && !Boolean.FALSE.equals(MachineLifecycleUtils.isSameInstance((JcloudsMachineLocation)l, registered, false))) {
+                if (MachineLifecycleUtils.isSameInstance((JcloudsMachineLocation) l, registered, true)) {
+                    // use this machine
+                    if (madeNew) {
+                        removeChild(registered);
+                        madeNew = false;
+                    }
+                    registered = (JcloudsMachineLocation) l;
+                } else {
+                    // unmanage any old machine location which has no-longer-valid details
+                    removeChild(l);
+                }
+            }
+        }
+        LOG.info("Resumed {} in {}", registered, this);
         return registered;
     }
 
@@ -1264,20 +1291,16 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im
     }
 
     @Override
-    public MachineLocation startupMachine(MachineLocation l) {
-        if (!(l instanceof JcloudsSshMachineLocation)) {
-            throw new IllegalStateException("Cannot startup machine "+l+"; wrong type");
-        }
-        return resumeMachine(ImmutableMap.of("id", ((JcloudsSshMachineLocation)l).getJcloudsId()));
+    public MachineLocation startupMachine(Map<?, ?> flags) {
+        return resumeMachine(flags);
     }
 
     @Override
-    public MachineLocation rebootMachine(MachineLocation l) {
+    public void rebootMachine(MachineLocation l) {
         if (!(l instanceof JcloudsSshMachineLocation)) {
             throw new IllegalStateException("Cannot startup machine "+l+"; wrong type");
         }
         getComputeService().rebootNode(((JcloudsSshMachineLocation)l).getJcloudsId());
-        return l;
     }
 
     @Override
diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsMachineLocation.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsMachineLocation.java
index 83cfc2e..5290a3d 100644
--- a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsMachineLocation.java
+++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsMachineLocation.java
@@ -18,11 +18,16 @@
  */
 package org.apache.brooklyn.location.jclouds;
 
+import com.google.common.base.Optional;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.core.location.MachineLifecycleUtils;
 import org.apache.brooklyn.location.jclouds.api.JcloudsMachineLocationPublic;
+import org.apache.brooklyn.util.collections.MutableList;
 import org.jclouds.compute.domain.NodeMetadata;
 
-import com.google.common.base.Optional;
-
 public interface JcloudsMachineLocation extends JcloudsMachineLocationPublic {
     
     @Override
@@ -39,4 +44,5 @@ public interface JcloudsMachineLocation extends JcloudsMachineLocationPublic {
      */
     @Deprecated
     public NodeMetadata getNode();
+
 }
diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsSshMachineLocation.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsSshMachineLocation.java
index 905864f..68dd492 100644
--- a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsSshMachineLocation.java
+++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsSshMachineLocation.java
@@ -18,6 +18,7 @@
  */
 package org.apache.brooklyn.location.jclouds;
 
+import org.apache.brooklyn.api.location.Location;
 import static org.apache.brooklyn.location.jclouds.api.JcloudsLocationConfigPublic.USE_MACHINE_PUBLIC_ADDRESS_AS_PRIVATE_ADDRESS;
 import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth;
 
@@ -296,7 +297,16 @@ public class JcloudsSshMachineLocation extends SshMachineLocation implements Jcl
     public JcloudsLocation getParent() {
         return jcloudsParent;
     }
-    
+
+    @Override
+    public void setParent(Location newParent, boolean updateChildListParents) {
+        if (newParent==null || newParent instanceof JcloudsLocation) {
+            // used to clear parent when removing from parent, to prevent releasing it
+            jcloudsParent = (JcloudsLocation) newParent;
+        }
+        super.setParent(newParent, updateChildListParents);
+    }
+
     @Override
     public String getHostname() {
         // Changed behaviour in Brooklyn 0.9.0. Previously it just did node.getHostname(), which
diff --git a/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/JcloudsLocationSuspendResumeMachineLiveTest.java b/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/JcloudsLocationSuspendResumeMachineLiveTest.java
index dd8865a..f124cfa 100644
--- a/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/JcloudsLocationSuspendResumeMachineLiveTest.java
+++ b/locations/jclouds/src/test/java/org/apache/brooklyn/location/jclouds/JcloudsLocationSuspendResumeMachineLiveTest.java
@@ -19,17 +19,21 @@
 
 package org.apache.brooklyn.location.jclouds;
 
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertTrue;
-
+import com.google.common.collect.ImmutableMap;
 import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.core.location.MachineLifecycleUtils;
+import org.apache.brooklyn.core.location.MachineLifecycleUtils.MachineStatus;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.time.Duration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import com.google.common.collect.ImmutableMap;
-
 public class JcloudsLocationSuspendResumeMachineLiveTest extends AbstractJcloudsLiveTest {
 
     private static final Logger LOG = LoggerFactory.getLogger(JcloudsLocationSuspendResumeMachineLiveTest.class);
@@ -46,17 +50,45 @@ public class JcloudsLocationSuspendResumeMachineLiveTest extends AbstractJclouds
 
     @Test(groups = "Live")
     public void testObtainThenSuspendThenResumeMachine() throws Exception {
-        MachineLocation machine = obtainMachine(ImmutableMap.of(
-                "imageId", EUWEST_IMAGE_ID));
+        MachineLocation machine = obtainMachine(ConfigBag.newInstance()
+                .configure(JcloudsLocationConfig.IMAGE_ID, EUWEST_IMAGE_ID)
+                .configure(JcloudsLocationConfig.OPEN_IPTABLES, false)  // optimization
+                .getAllConfig());
         JcloudsSshMachineLocation sshMachine = (JcloudsSshMachineLocation) machine;
         assertTrue(sshMachine.isSshable(), "Cannot SSH to " + sshMachine);
 
         suspendMachine(machine);
+        ((SshMachineLocation)machine).setSshCheckTimeout(Duration.FIVE_SECONDS);
         assertFalse(sshMachine.isSshable(), "Should not be able to SSH to suspended machine");
 
+        ((SshMachineLocation)machine).setSshCheckTimeout(null);
         MachineLocation machine2 = resumeMachine(ImmutableMap.of("id", sshMachine.getJcloudsId()));
         assertTrue(machine2 instanceof JcloudsSshMachineLocation);
         assertTrue(((JcloudsSshMachineLocation) machine2).isSshable(), "Cannot SSH to " + machine2);
     }
 
+    @Test(groups = "Live")
+    public void testObtainThenShutdownThenRestart() throws Exception {
+        MachineLocation machine = obtainMachine(ConfigBag.newInstance()
+                .configure(JcloudsLocationConfig.IMAGE_ID, EUWEST_IMAGE_ID)
+                .configure(JcloudsLocationConfig.OPEN_IPTABLES, false)  // optimization
+                .getAllConfig());
+        JcloudsSshMachineLocation sshMachine = (JcloudsSshMachineLocation) machine;
+        Assert.assertEquals(new MachineLifecycleUtils(sshMachine).getStatus(), MachineStatus.RUNNING);
+        assertTrue(sshMachine.isSshable(), "Cannot SSH to " + sshMachine);
+
+        jcloudsLocation.shutdownMachine(sshMachine);
+        sshMachine.setSshCheckTimeout(Duration.FIVE_SECONDS);
+        assertFalse(sshMachine.isSshable(), "Should not be able to SSH to suspended machine");
+
+        Assert.assertEquals(new MachineLifecycleUtils(sshMachine).exists(), Boolean.TRUE);
+        Assert.assertEquals(new MachineLifecycleUtils(sshMachine).getStatus(), MachineStatus.SUSPENDED);  // shutdown suspends in AWS
+        sshMachine.setSshCheckTimeout(null);
+
+        MachineLocation machine2 = new MachineLifecycleUtils(sshMachine).makeRunning().get();
+        assertTrue(machine2 instanceof JcloudsSshMachineLocation);
+        assertTrue(((JcloudsSshMachineLocation) machine2).isSshable(), "Cannot SSH to " + machine2);
+        Assert.assertEquals(new MachineLifecycleUtils(machine2).getStatus(), MachineStatus.RUNNING);
+    }
+
 }
diff --git a/logging/logback-includes/src/main/resources/brooklyn/logback-logger-excludes.xml b/logging/logback-includes/src/main/resources/brooklyn/logback-logger-excludes.xml
index f4b4644..f488196 100644
--- a/logging/logback-includes/src/main/resources/brooklyn/logback-logger-excludes.xml
+++ b/logging/logback-includes/src/main/resources/brooklyn/logback-logger-excludes.xml
@@ -56,6 +56,10 @@
 		     (Turn them back on if you need to see how API-doc gets generated, and also see https://github.com/wordnik/swagger-core/issues/58) -->
 	</logger>
 
+    <!-- Gives spurious warnings -->
+    <logger name="org.jclouds.location.suppliers.implicit.GetRegionIdMatchingProviderURIOrNull" level="ERROR" additivity="false">
+        <appender-ref ref="FILE" />
+    </logger>
     <!-- The MongoDB Java driver is much too noisy at INFO. -->
     <logger name="org.mongodb.driver" level="WARN" additivity="false">
         <appender-ref ref="FILE" />
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java
index ec64c7e..8807d00 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java
@@ -364,7 +364,7 @@ public abstract class MachineLifecycleEffectorTasks {
 
         // Opportunity to block startup until other dependent components are available
         try (CloseableLatch latch = waitForCloseableLatch(entity(), SoftwareProcess.START_LATCH)) {
-            preStartAtMachineAsync(locationSF);
+            preStartAtMachineAsync(locationSF, parameters);
             DynamicTasks.queue("start (processes)", new StartProcessesAtMachineTask(locationSF));
             postStartAtMachineAsync(parameters);
         }
@@ -452,8 +452,8 @@ public abstract class MachineLifecycleEffectorTasks {
     /**
      * Wraps a call to {@link #preStartCustom(MachineLocation, ConfigBag)}, after setting the hostname and address.
      */
-    protected void preStartAtMachineAsync(final Supplier<MachineLocation> machineS) {
-        DynamicTasks.queue("pre-start", new PreStartTask(machineS.get()));
+    protected void preStartAtMachineAsync(final Supplier<MachineLocation> machineS, ConfigBag parameters) {
+        DynamicTasks.queue("pre-start", new PreStartTask(machineS.get(), parameters));
     }
 
     protected class PreStartTask implements Runnable {