org.apache.brooklyn.location.jclouds.JcloudsLocation.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.brooklyn.location.jclouds.JcloudsLocation.java

Source

/*
 * 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.location.jclouds;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
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;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

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.PortMapping;
import org.apache.brooklyn.core.location.cloud.AbstractCloudMachineProvisioningLocation;
import org.apache.brooklyn.core.location.cloud.AvailabilityZoneExtension;
import org.apache.brooklyn.core.location.cloud.names.AbstractCloudMachineNamer;
import org.apache.brooklyn.core.location.cloud.names.CloudMachineNamer;
import org.apache.brooklyn.core.mgmt.internal.LocalLocationManager;
import org.apache.brooklyn.core.mgmt.persist.LocationWithObjectStore;
import org.apache.brooklyn.core.mgmt.persist.PersistenceObjectStore;
import org.apache.brooklyn.core.mgmt.persist.jclouds.JcloudsBlobStoreBasedObjectStore;
import org.apache.brooklyn.location.jclouds.JcloudsPredicates.NodeInLocation;
import org.apache.brooklyn.location.jclouds.networking.JcloudsPortForwarderExtension;
import org.apache.brooklyn.location.jclouds.templates.PortableTemplateBuilder;
import org.apache.brooklyn.location.jclouds.zone.AwsAvailabilityZoneExtension;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.crypto.SecureKeys;
import org.apache.brooklyn.util.core.flags.MethodCoercions;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.core.internal.ssh.ShellTool;
import org.apache.brooklyn.util.core.internal.ssh.SshTool;
import org.apache.brooklyn.util.core.internal.winrm.WinRmTool;
import org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.TaskBuilder;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.core.text.TemplateProcessor;
import org.apache.brooklyn.util.exceptions.CompoundRuntimeException;
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.javalang.Enums;
import org.apache.brooklyn.util.javalang.Reflections;
import org.apache.brooklyn.util.net.Cidr;
import org.apache.brooklyn.util.net.Networking;
import org.apache.brooklyn.util.net.Protocol;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.repeat.Repeater;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.ssh.IptablesCommands;
import org.apache.brooklyn.util.ssh.IptablesCommands.Chain;
import org.apache.brooklyn.util.ssh.IptablesCommands.Policy;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.ByteSizeStrings;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.KeyValueParser;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.apache.commons.lang3.ArrayUtils;
import org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions;
import org.jclouds.cloudstack.compute.options.CloudStackTemplateOptions;
import org.jclouds.compute.ComputeService;
import org.jclouds.compute.RunNodesException;
import org.jclouds.compute.config.AdminAccessConfiguration;
import org.jclouds.compute.domain.ComputeMetadata;
import org.jclouds.compute.domain.Hardware;
import org.jclouds.compute.domain.Image;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.NodeMetadata.Status;
import org.jclouds.compute.domain.NodeMetadataBuilder;
import org.jclouds.compute.domain.OperatingSystem;
import org.jclouds.compute.domain.OsFamily;
import org.jclouds.compute.domain.Template;
import org.jclouds.compute.domain.TemplateBuilder;
import org.jclouds.compute.domain.TemplateBuilderSpec;
import org.jclouds.compute.functions.Sha512Crypt;
import org.jclouds.compute.options.TemplateOptions;
import org.jclouds.domain.Credentials;
import org.jclouds.domain.LocationScope;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.ec2.compute.options.EC2TemplateOptions;
import org.jclouds.googlecomputeengine.compute.options.GoogleComputeEngineTemplateOptions;
import org.jclouds.openstack.nova.v2_0.compute.options.NovaTemplateOptions;
import org.jclouds.rest.AuthorizationException;
import org.jclouds.scriptbuilder.domain.LiteralStatement;
import org.jclouds.scriptbuilder.domain.Statement;
import org.jclouds.scriptbuilder.domain.StatementList;
import org.jclouds.scriptbuilder.functions.InitAdminAccess;
import org.jclouds.scriptbuilder.statements.login.AdminAccess;
import org.jclouds.scriptbuilder.statements.login.ReplaceShadowPasswordEntry;
import org.jclouds.scriptbuilder.statements.ssh.AuthorizeRSAPublicKeys;
import org.jclouds.softlayer.compute.options.SoftLayerTemplateOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.base.Stopwatch;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.io.Files;
import com.google.common.net.HostAndPort;

/**
 * For provisioning and managing VMs in a particular provider/region, using jclouds.
 * Configuration flags are defined in {@link JcloudsLocationConfig}.
 */
@SuppressWarnings("serial")
public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation
        implements JcloudsLocationConfig, MachineManagementMixins.RichMachineProvisioningLocation<MachineLocation>,
        LocationWithObjectStore, MachineManagementMixins.SuspendResumeLocation {

    // TODO After converting from Groovy to Java, this is now very bad code! It relies entirely on putting
    // things into and taking them out of maps; it's not type-safe, and it's thus very error-prone.
    // In Groovy, that's considered ok but not in Java.

    // TODO test (and fix) ability to set config keys from flags

    // TODO we say config is inherited, but it isn't the case for many "deep" / jclouds properties
    // e.g. when we pass getRawLocalConfigBag() in and decorate it with additional flags
    // (inheritance only works when we call getConfig in this class)

    public static final Logger LOG = LoggerFactory.getLogger(JcloudsLocation.class);

    public static final String ROOT_USERNAME = "root";
    /** these userNames are known to be the preferred/required logins in some common/default images
     *  where root@ is not allowed to log in */
    public static final List<String> ROOT_ALIASES = ImmutableList.of("ubuntu", "ec2-user");
    public static final List<String> COMMON_USER_NAMES_TO_TRY = ImmutableList.<String>builder().add(ROOT_USERNAME)
            .addAll(ROOT_ALIASES).add("admin").build();

    private static final Pattern LIST_PATTERN = Pattern.compile("^\\[(.*)\\]$");
    private static final Pattern INTEGER_PATTERN = Pattern.compile("^\\d*$");

    private static final int NOTES_MAX_LENGTH = 1000;

    private final AtomicBoolean loggedSshKeysHint = new AtomicBoolean(false);
    private final AtomicBoolean listedAvailableTemplatesOnNoSuchTemplate = new AtomicBoolean(false);

    private final Map<String, Map<String, ? extends Object>> tagMapping = Maps.newLinkedHashMap();

    @SetFromFlag // so it's persisted
    private final Map<MachineLocation, String> vmInstanceIds = Maps.newLinkedHashMap();

    static {
        Networking.init();
    }

    public JcloudsLocation() {
        super();
    }

    /** typically wants at least ACCESS_IDENTITY and ACCESS_CREDENTIAL */
    public JcloudsLocation(Map<?, ?> conf) {
        super(conf);
    }

    @Override
    @Deprecated
    public JcloudsLocation configure(Map<?, ?> properties) {
        super.configure(properties);

        if (config().getLocalBag().containsKey("providerLocationId")) {
            LOG.warn("Using deprecated 'providerLocationId' key in " + this);
            if (!config().getLocalBag().containsKey(CLOUD_REGION_ID))
                config().addToLocalBag(MutableMap.of(CLOUD_REGION_ID.getName(),
                        (String) config().getLocalBag().getStringKey("providerLocationId")));
        }

        if (isDisplayNameAutoGenerated() || !groovyTruth(getDisplayName())) {
            setDisplayName(elvis(getProvider(), "unknown") + (groovyTruth(getRegion()) ? ":" + getRegion() : "")
                    + (groovyTruth(getEndpoint()) ? ":" + getEndpoint() : ""));
        }

        setCreationString(config().getLocalBag());

        if (getConfig(MACHINE_CREATION_SEMAPHORE) == null) {
            Integer maxConcurrent = getConfig(MAX_CONCURRENT_MACHINE_CREATIONS);
            if (maxConcurrent == null || maxConcurrent < 1) {
                throw new IllegalStateException(
                        MAX_CONCURRENT_MACHINE_CREATIONS.getName() + " must be >= 1, but was " + maxConcurrent);
            }
            config().set(MACHINE_CREATION_SEMAPHORE, new Semaphore(maxConcurrent, true));
        }
        return this;
    }

    @Override
    public void init() {
        super.init();
        if ("aws-ec2".equals(getProvider())) {
            addExtension(AvailabilityZoneExtension.class,
                    new AwsAvailabilityZoneExtension(getManagementContext(), this));
        }
    }

    @Override
    public JcloudsLocation newSubLocation(Map<?, ?> newFlags) {
        return newSubLocation(getClass(), newFlags);
    }

    @Override
    public JcloudsLocation newSubLocation(Class<? extends AbstractCloudMachineProvisioningLocation> type,
            Map<?, ?> newFlags) {
        // TODO should be able to use ConfigBag.newInstanceExtending; would require moving stuff around to api etc
        return (JcloudsLocation) getManagementContext().getLocationManager()
                .createLocation(LocationSpec.create(type).parent(this)
                        .configure(config().getLocalBag().getAllConfig()) // FIXME Should this just be inherited?
                        .configure(MACHINE_CREATION_SEMAPHORE, getMachineCreationSemaphore()).configure(newFlags));
    }

    @Override
    public String toString() {
        Object identity = getIdentity();
        String configDescription = config().getLocalBag().getDescription();
        if (configDescription != null && configDescription.startsWith(getClass().getSimpleName()))
            return configDescription;
        return getClass().getSimpleName() + "[" + getDisplayName() + ":" + (identity != null ? identity : null)
                + (configDescription != null ? "/" + configDescription : "") + "@" + getId() + "]";
    }

    @Override
    public String toVerboseString() {
        return Objects.toStringHelper(this).omitNullValues().add("id", getId()).add("name", getDisplayName())
                .add("identity", getIdentity()).add("description", config().getLocalBag().getDescription())
                .add("provider", getProvider()).add("region", getRegion()).add("endpoint", getEndpoint())
                .toString();
    }

    public String getProvider() {
        return getConfig(CLOUD_PROVIDER);
    }

    public String getIdentity() {
        return getConfig(ACCESS_IDENTITY);
    }

    public String getCredential() {
        return getConfig(ACCESS_CREDENTIAL);
    }

    /** returns the location ID used by the provider, if set, e.g. us-west-1 */
    public String getRegion() {
        return getConfig(CLOUD_REGION_ID);
    }

    public String getEndpoint() {
        return (String) config().getBag().getWithDeprecation(CLOUD_ENDPOINT, JCLOUDS_KEY_ENDPOINT);
    }

    public String getUser(ConfigBag config) {
        return (String) config.getWithDeprecation(USER, JCLOUDS_KEY_USERNAME);
    }

    public boolean isWindows(Template template, ConfigBag config) {
        return isWindows(template.getImage(), config);
    }

    /**
     * Whether VMs provisioned from this image will be Windows. Assume windows if the image
     * explicitly says so, or if image does not tell us then fall back to whether the config 
     * explicitly says windows in {@link JcloudsLocationConfig#OS_FAMILY}.
     * 
     * Will first look at {@link JcloudsLocationConfig#OS_FAMILY_OVERRIDE}, to check if that 
     * is set. If so, no further checks are done: the value is compared against {@link OsFamily#WINDOWS}.
     * 
     * We believe the config (e.g. from brooklyn.properties) because for some clouds there is 
     * insufficient meta-data so the Image might not tell us. Thus a user can work around it
     * by explicitly supplying configuration. 
     */
    public boolean isWindows(Image image, ConfigBag config) {
        OsFamily override = config.get(OS_FAMILY_OVERRIDE);
        if (override != null)
            return override == OsFamily.WINDOWS;

        OsFamily confFamily = config.get(OS_FAMILY);
        OperatingSystem os = (image != null) ? image.getOperatingSystem() : null;
        return (os != null && os.getFamily() != OsFamily.UNRECOGNIZED) ? (OsFamily.WINDOWS == os.getFamily())
                : (OsFamily.WINDOWS == confFamily);
    }

    /**
     * Whether the given VM is Windows.
     * 
     * @see {@link #isWindows(Image, ConfigBag)}
     */
    public boolean isWindows(NodeMetadata node, ConfigBag config) {
        OsFamily override = config.get(OS_FAMILY_OVERRIDE);
        if (override != null)
            return override == OsFamily.WINDOWS;

        OsFamily confFamily = config.get(OS_FAMILY);
        OperatingSystem os = (node != null) ? node.getOperatingSystem() : null;
        return (os != null && os.getFamily() != OsFamily.UNRECOGNIZED) ? (OsFamily.WINDOWS == os.getFamily())
                : (OsFamily.WINDOWS == confFamily);
    }

    public boolean isLocationFirewalldEnabled(SshMachineLocation location) {
        int result = location.execCommands("checking if firewalld is active",
                ImmutableList.of(IptablesCommands.firewalldServiceIsActive()));
        if (result == 0) {
            return true;
        }

        return false;
    }

    protected Semaphore getMachineCreationSemaphore() {
        return checkNotNull(getConfig(MACHINE_CREATION_SEMAPHORE), MACHINE_CREATION_SEMAPHORE.getName());
    }

    protected CloudMachineNamer getCloudMachineNamer(ConfigBag config) {
        String namerClass = config.get(LocationConfigKeys.CLOUD_MACHINE_NAMER_CLASS);
        if (Strings.isNonBlank(namerClass)) {
            Optional<CloudMachineNamer> cloudNamer = Reflections
                    .invokeConstructorWithArgs(getManagementContext().getCatalogClassLoader(), namerClass);
            if (cloudNamer.isPresent()) {
                return cloudNamer.get();
            } else {
                throw new IllegalStateException(
                        "Failed to create CloudMachineNamer " + namerClass + " for location " + this);
            }
        } else {
            return new JcloudsMachineNamer();
        }
    }

    protected Collection<JcloudsLocationCustomizer> getCustomizers(ConfigBag setup) {
        @SuppressWarnings("deprecation")
        JcloudsLocationCustomizer customizer = setup.get(JCLOUDS_LOCATION_CUSTOMIZER);
        Collection<JcloudsLocationCustomizer> customizers = setup.get(JCLOUDS_LOCATION_CUSTOMIZERS);
        @SuppressWarnings("deprecation")
        String customizerType = setup.get(JCLOUDS_LOCATION_CUSTOMIZER_TYPE);
        @SuppressWarnings("deprecation")
        String customizersSupplierType = setup.get(JCLOUDS_LOCATION_CUSTOMIZERS_SUPPLIER_TYPE);

        ClassLoader catalogClassLoader = getManagementContext().getCatalogClassLoader();
        List<JcloudsLocationCustomizer> result = new ArrayList<JcloudsLocationCustomizer>();
        if (customizer != null)
            result.add(customizer);
        if (customizers != null)
            result.addAll(customizers);
        if (Strings.isNonBlank(customizerType)) {
            Optional<JcloudsLocationCustomizer> customizerByType = Reflections
                    .invokeConstructorWithArgs(catalogClassLoader, customizerType, setup);
            if (customizerByType.isPresent()) {
                result.add(customizerByType.get());
            } else {
                customizerByType = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizerType);
                if (customizerByType.isPresent()) {
                    result.add(customizerByType.get());
                } else {
                    throw new IllegalStateException("Failed to create JcloudsLocationCustomizer "
                            + customizersSupplierType + " for location " + this);
                }
            }
        }
        if (Strings.isNonBlank(customizersSupplierType)) {
            Optional<Supplier<Collection<JcloudsLocationCustomizer>>> supplier = Reflections
                    .invokeConstructorWithArgs(catalogClassLoader, customizersSupplierType, setup);
            if (supplier.isPresent()) {
                result.addAll(supplier.get().get());
            } else {
                supplier = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizersSupplierType);
                if (supplier.isPresent()) {
                    result.addAll(supplier.get().get());
                } else {
                    throw new IllegalStateException("Failed to create JcloudsLocationCustomizer supplier "
                            + customizersSupplierType + " for location " + this);
                }
            }
        }
        return result;
    }

    protected Collection<MachineLocationCustomizer> getMachineCustomizers(ConfigBag setup) {
        Collection<MachineLocationCustomizer> customizers = setup.get(MACHINE_LOCATION_CUSTOMIZERS);
        return (customizers == null ? ImmutableList.<MachineLocationCustomizer>of() : customizers);
    }

    public void setDefaultImageId(String val) {
        config().set(DEFAULT_IMAGE_ID, val);
    }

    // TODO remove tagMapping, or promote it
    // (i think i favour removing it, letting the config come in from the entity)

    public void setTagMapping(Map<String, Map<String, ? extends Object>> val) {
        tagMapping.clear();
        tagMapping.putAll(val);
    }

    // TODO Decide on semantics. If I give "TomcatServer" and "Ubuntu", then must I get back an image that matches both?
    // Currently, just takes first match that it finds...
    @Override
    public Map<String, Object> getProvisioningFlags(Collection<String> tags) {
        Map<String, Object> result = Maps.newLinkedHashMap();
        Collection<String> unmatchedTags = Lists.newArrayList();
        for (String it : tags) {
            if (groovyTruth(tagMapping.get(it)) && !groovyTruth(result)) {
                result.putAll(tagMapping.get(it));
            } else {
                unmatchedTags.add(it);
            }
        }
        if (unmatchedTags.size() > 0) {
            LOG.debug("Location {}, failed to match provisioning tags {}", this, unmatchedTags);
        }
        return result;
    }

    public static final Set<ConfigKey<?>> getAllSupportedProperties() {
        Set<String> configsOnClass = Sets.newLinkedHashSet(Iterables.transform(
                ConfigUtils.getStaticKeysOnClass(JcloudsLocation.class), new Function<HasConfigKey<?>, String>() {
                    @Override
                    @Nullable
                    public String apply(@Nullable HasConfigKey<?> input) {
                        return input.getConfigKey().getName();
                    }
                }));
        Set<ConfigKey<?>> configKeysInList = ImmutableSet.<ConfigKey<?>>builder()
                .addAll(SUPPORTED_TEMPLATE_BUILDER_PROPERTIES.keySet())
                .addAll(SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES.keySet()).build();
        Set<String> configsInList = Sets
                .newLinkedHashSet(Iterables.transform(configKeysInList, new Function<ConfigKey<?>, String>() {
                    @Override
                    @Nullable
                    public String apply(@Nullable ConfigKey<?> input) {
                        return input.getName();
                    }
                }));

        SetView<String> extrasInList = Sets.difference(configsInList, configsOnClass);
        // notInList is normal
        if (!extrasInList.isEmpty())
            LOG.warn("JcloudsLocation supported properties differs from config defined on class: " + extrasInList);
        return Collections.unmodifiableSet(configKeysInList);
    }

    public ComputeService getComputeService() {
        return getComputeService(MutableMap.of());
    }

    public ComputeService getComputeService(Map<?, ?> flags) {
        ConfigBag conf = (flags == null || flags.isEmpty()) ? config().getBag()
                : ConfigBag.newInstanceExtending(config().getBag(), flags);
        return getComputeService(conf);
    }

    public ComputeService getComputeService(ConfigBag config) {
        return getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(config, true);
    }

    /** @deprecated since 0.7.0 use {@link #listMachines()} */
    @Deprecated
    public Set<? extends ComputeMetadata> listNodes() {
        return listNodes(MutableMap.of());
    }

    /** @deprecated since 0.7.0 use {@link #listMachines()}.
     * (no support for custom compute service flags; if that is needed, we'll have to introduce a new method,
     * but it seems there are no usages) */
    @Deprecated
    public Set<? extends ComputeMetadata> listNodes(Map<?, ?> flags) {
        return getComputeService(flags).listNodes();
    }

    @Override
    public Map<String, MachineManagementMixins.MachineMetadata> listMachines() {
        Set<? extends ComputeMetadata> nodes = getRegion() != null
                ? getComputeService().listNodesDetailsMatching(new NodeInLocation(getRegion(), true))
                : getComputeService().listNodes();
        Map<String, MachineManagementMixins.MachineMetadata> result = new LinkedHashMap<String, MachineManagementMixins.MachineMetadata>();

        for (ComputeMetadata node : nodes)
            result.put(node.getId(), getMachineMetadata(node));

        return result;
    }

    protected MachineManagementMixins.MachineMetadata getMachineMetadata(ComputeMetadata node) {
        if (node == null)
            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);
    }

    @Override
    public MachineManagementMixins.MachineMetadata getMachineMetadata(MachineLocation l) {
        if (l instanceof JcloudsSshMachineLocation) {
            return getMachineMetadata(((JcloudsSshMachineLocation) l).node);
        }
        return null;
    }

    @Override
    public void killMachine(String cloudServiceId) {
        getComputeService().destroyNode(cloudServiceId);
    }

    @Override
    public void killMachine(MachineLocation l) {
        MachineManagementMixins.MachineMetadata m = getMachineMetadata(l);
        if (m == null)
            throw new NoSuchElementException("Machine " + l + " is not known at " + this);
        killMachine(m.getId());
    }

    /** attaches a string describing where something is being created
     * (provider, region/location and/or endpoint, callerContext) */
    protected void setCreationString(ConfigBag config) {
        config.setDescription(elvis(config.get(CLOUD_PROVIDER), "unknown")
                + (config.containsKey(CLOUD_REGION_ID) ? ":" + config.get(CLOUD_REGION_ID) : "")
                + (config.containsKey(CLOUD_ENDPOINT) ? ":" + config.get(CLOUD_ENDPOINT) : "")
                + (config.containsKey(CALLER_CONTEXT) ? "@" + config.get(CALLER_CONTEXT) : ""));
    }

    // ----------------- obtaining a new machine ------------------------
    public MachineLocation obtain() throws NoMachinesAvailableException {
        return obtain(MutableMap.of());
    }

    public MachineLocation obtain(TemplateBuilder tb) throws NoMachinesAvailableException {
        return obtain(MutableMap.of(), tb);
    }

    public MachineLocation obtain(Map<?, ?> flags, TemplateBuilder tb) throws NoMachinesAvailableException {
        return obtain(MutableMap.builder().putAll(flags).put(TEMPLATE_BUILDER, tb).build());
    }

    /** core method for obtaining a VM using jclouds;
     * Map should contain CLOUD_PROVIDER and CLOUD_ENDPOINT or CLOUD_REGION, depending on the cloud,
     * as well as ACCESS_IDENTITY and ACCESS_CREDENTIAL,
     * plus any further properties to specify e.g. images, hardware profiles, accessing user
     * (for initial login, and a user potentially to create for subsequent ie normal access) */
    @Override
    public MachineLocation obtain(Map<?, ?> flags) throws NoMachinesAvailableException {
        ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
        Integer attempts = setup.get(MACHINE_CREATE_ATTEMPTS);
        List<Exception> exceptions = Lists.newArrayList();
        if (attempts == null || attempts < 1)
            attempts = 1;
        for (int i = 1; i <= attempts; i++) {
            try {
                return obtainOnce(setup);
            } catch (RuntimeException e) {
                LOG.warn("Attempt #{}/{} to obtain machine threw error: {}", new Object[] { i, attempts, e });
                exceptions.add(e);
            }
        }
        String msg = String.format("Failed to get VM after %d attempt%s.", attempts, attempts == 1 ? "" : "s");

        Exception cause = (exceptions.size() == 1) ? exceptions.get(0)
                : new CompoundRuntimeException(msg + " - " + "First cause is " + exceptions.get(0)
                        + " (listed in primary trace); " + "plus " + (exceptions.size() - 1)
                        + " more (e.g. the last is " + exceptions.get(exceptions.size() - 1) + ")",
                        exceptions.get(0), exceptions);

        if (exceptions.get(exceptions.size() - 1) instanceof NoMachinesAvailableException) {
            throw new NoMachinesAvailableException(msg, cause);
        } else {
            throw Exceptions.propagate(cause);
        }
    }

    protected MachineLocation obtainOnce(ConfigBag setup) throws NoMachinesAvailableException {
        AccessController.Response access = getManagementContext().getAccessController().canProvisionLocation(this);
        if (!access.isAllowed()) {
            throw new IllegalStateException(
                    "Access controller forbids provisioning in " + this + ": " + access.getMsg());
        }

        setCreationString(setup);
        boolean waitForSshable = !"false".equalsIgnoreCase(setup.get(WAIT_FOR_SSHABLE));
        boolean waitForWinRmable = !"false".equalsIgnoreCase(setup.get(WAIT_FOR_WINRM_AVAILABLE));
        boolean usePortForwarding = setup.get(USE_PORT_FORWARDING);
        boolean skipJcloudsSshing = Boolean.FALSE.equals(setup.get(USE_JCLOUDS_SSH_INIT)) || usePortForwarding;
        JcloudsPortForwarderExtension portForwarder = setup.get(PORT_FORWARDER);
        if (usePortForwarding)
            checkNotNull(portForwarder, "portForwarder, when use-port-forwarding enabled");

        final ComputeService computeService = getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(setup, true);
        CloudMachineNamer cloudMachineNamer = getCloudMachineNamer(setup);
        String groupId = elvis(setup.get(GROUP_ID), cloudMachineNamer.generateNewGroupId(setup));
        NodeMetadata node = null;
        JcloudsMachineLocation machineLocation = null;
        Duration semaphoreTimestamp = null;
        Duration templateTimestamp = null;
        Duration provisionTimestamp = null;
        Duration usableTimestamp = null;
        Duration customizedTimestamp = null;
        Stopwatch provisioningStopwatch = Stopwatch.createStarted();

        try {
            LOG.info("Creating VM " + setup.getDescription() + " in " + this);

            Semaphore machineCreationSemaphore = getMachineCreationSemaphore();
            boolean acquired = machineCreationSemaphore.tryAcquire(0, TimeUnit.SECONDS);
            if (!acquired) {
                LOG.info("Waiting in {} for machine-creation permit ({} other queuing requests already)",
                        new Object[] { this, machineCreationSemaphore.getQueueLength() });
                Stopwatch blockStopwatch = Stopwatch.createStarted();
                machineCreationSemaphore.acquire();
                LOG.info("Acquired in {} machine-creation permit, after waiting {}", this,
                        Time.makeTimeStringRounded(blockStopwatch));
            } else {
                LOG.debug("Acquired in {} machine-creation permit immediately", this);
            }
            semaphoreTimestamp = Duration.of(provisioningStopwatch);

            LoginCredentials userCredentials = null;
            Set<? extends NodeMetadata> nodes;
            Template template;
            try {
                // Setup the template
                template = buildTemplate(computeService, setup);
                boolean expectWindows = isWindows(template, setup);
                if (!skipJcloudsSshing) {
                    if (expectWindows) {
                        // TODO Was this too early to look at template.getImage? e.g. customizeTemplate could subsequently modify it.
                        LOG.warn("Ignoring invalid configuration for Windows provisioning of " + template.getImage()
                                + ": " + USE_JCLOUDS_SSH_INIT.getName() + " should be false");
                        skipJcloudsSshing = true;
                    } else if (waitForSshable) {
                        userCredentials = initTemplateForCreateUser(template, setup);
                    }
                }

                templateTimestamp = Duration.of(provisioningStopwatch);
                // "Name" metadata seems to set the display name; at least in AWS
                // TODO it would be nice if this salt comes from the location's ID (but we don't know that yet as the ssh machine location isn't created yet)
                // TODO in softlayer we want to control the suffix of the hostname which is 3 random hex digits
                template.getOptions().getUserMetadata().put("Name",
                        cloudMachineNamer.generateNewMachineUniqueNameFromGroupId(setup, groupId));

                if (setup.get(JcloudsLocationConfig.INCLUDE_BROOKLYN_USER_METADATA)) {
                    template.getOptions().getUserMetadata().put("brooklyn-user", System.getProperty("user.name"));

                    Object context = setup.get(CALLER_CONTEXT);
                    if (context instanceof Entity) {
                        Entity entity = (Entity) context;
                        template.getOptions().getUserMetadata().put("brooklyn-app-id", entity.getApplicationId());
                        template.getOptions().getUserMetadata().put("brooklyn-app-name",
                                entity.getApplication().getDisplayName());
                        template.getOptions().getUserMetadata().put("brooklyn-entity-id", entity.getId());
                        template.getOptions().getUserMetadata().put("brooklyn-entity-name",
                                entity.getDisplayName());
                        template.getOptions().getUserMetadata().put("brooklyn-server-creation-date",
                                Time.makeDateSimpleStampString());
                    }
                }

                customizeTemplate(setup, computeService, template);

                LOG.debug("jclouds using template {} / options {} to provision machine in {}",
                        new Object[] { template, template.getOptions(), setup.getDescription() });

                if (!setup.getUnusedConfig().isEmpty())
                    if (LOG.isDebugEnabled())
                        LOG.debug("NOTE: unused flags passed to obtain VM in " + setup.getDescription() + ": "
                                + Sanitizer.sanitize(setup.getUnusedConfig()));

                nodes = computeService.createNodesInGroup(groupId, 1, template);
                provisionTimestamp = Duration.of(provisioningStopwatch);
            } finally {
                machineCreationSemaphore.release();
            }

            node = Iterables.getOnlyElement(nodes, null);
            LOG.debug("jclouds created {} for {}", node, setup.getDescription());
            if (node == null)
                throw new IllegalStateException(
                        "No nodes returned by jclouds create-nodes in " + setup.getDescription());

            boolean windows = isWindows(node, setup);
            if (windows) {
                int newLoginPort = node.getLoginPort() == 22 ? 5985 : node.getLoginPort();
                String newLoginUser = "root".equals(node.getCredentials().getUser()) ? "Administrator"
                        : node.getCredentials().getUser();
                LOG.debug(
                        "jclouds created Windows VM {}; transforming connection details: loginPort from {} to {}; loginUser from {} to {}",
                        new Object[] { node, node.getLoginPort(), newLoginPort, node.getCredentials().getUser(),
                                newLoginUser });

                node = NodeMetadataBuilder.fromNodeMetadata(node).loginPort(newLoginPort)
                        .credentials(LoginCredentials.builder(node.getCredentials()).user(newLoginUser).build())
                        .build();
            }
            // FIXME How do we influence the node.getLoginPort, so it is set correctly for Windows?
            // Setup port-forwarding, if required
            Optional<HostAndPort> sshHostAndPortOverride;
            if (usePortForwarding) {
                sshHostAndPortOverride = Optional.of(portForwarder.openPortForwarding(node, node.getLoginPort(),
                        Optional.<Integer>absent(), Protocol.TCP, Cidr.UNIVERSAL));
            } else {
                sshHostAndPortOverride = Optional.absent();
            }

            LoginCredentials initialCredentials = node.getCredentials();
            if (skipJcloudsSshing) {
                boolean waitForConnectable = (windows) ? waitForWinRmable : waitForSshable;
                if (waitForConnectable) {
                    if (windows) {
                        // TODO Does jclouds support any windows user setup?
                        initialCredentials = waitForWinRmAvailable(computeService, node, sshHostAndPortOverride,
                                setup);
                    } else {
                        initialCredentials = waitForSshable(computeService, node, sshHostAndPortOverride, setup);
                    }
                    userCredentials = createUser(computeService, node, sshHostAndPortOverride, initialCredentials,
                            setup);
                }
            }

            // Figure out which login-credentials to use
            LoginCredentials customCredentials = setup.get(CUSTOM_CREDENTIALS);
            if (customCredentials != null) {
                userCredentials = customCredentials;
                //set userName and other data, from these credentials
                Object oldUsername = setup.put(USER, customCredentials.getUser());
                LOG.debug("node {} username {} / {} (customCredentials)",
                        new Object[] { node, customCredentials.getUser(), oldUsername });
                if (customCredentials.getOptionalPassword().isPresent())
                    setup.put(PASSWORD, customCredentials.getOptionalPassword().get());
                if (customCredentials.getOptionalPrivateKey().isPresent())
                    setup.put(PRIVATE_KEY_DATA, customCredentials.getOptionalPrivateKey().get());
            }
            if (userCredentials == null || (!userCredentials.getOptionalPassword().isPresent()
                    && !userCredentials.getOptionalPrivateKey().isPresent())) {
                // We either don't have any userCredentials, or it is missing both a password/key.
                // TODO See waitForSshable, which now handles if the node.getLoginCredentials has both a password+key
                userCredentials = extractVmCredentials(setup, node, initialCredentials);
            }
            if (userCredentials == null) {
                // TODO See waitForSshable, which now handles if the node.getLoginCredentials has both a password+key
                userCredentials = extractVmCredentials(setup, node, initialCredentials);
            }
            if (userCredentials != null) {
                node = NodeMetadataBuilder.fromNodeMetadata(node).credentials(userCredentials).build();
            } else {
                // only happens if something broke above...
                userCredentials = LoginCredentials.fromCredentials(node.getCredentials());
            }
            // store the credentials, in case they have changed
            setup.putIfNotNull(JcloudsLocationConfig.PASSWORD, userCredentials.getOptionalPassword().orNull());
            setup.putIfNotNull(JcloudsLocationConfig.PRIVATE_KEY_DATA,
                    userCredentials.getOptionalPrivateKey().orNull());

            // Wait for the VM to be reachable over SSH
            if (waitForSshable && !windows) {
                waitForSshable(computeService, node, sshHostAndPortOverride, ImmutableList.of(userCredentials),
                        setup);
            } else {
                LOG.debug("Skipping ssh check for {} ({}) due to config waitForSshable=false", node,
                        setup.getDescription());
            }
            usableTimestamp = Duration.of(provisioningStopwatch);

            //            JcloudsSshMachineLocation jcloudsSshMachineLocation = null;
            //            WinRmMachineLocation winRmMachineLocation = null;
            // Create a JcloudsSshMachineLocation, and register it
            if (windows) {
                machineLocation = registerWinRmMachineLocation(computeService, node, userCredentials,
                        sshHostAndPortOverride, setup);
            } else {
                machineLocation = registerJcloudsSshMachineLocation(computeService, node,
                        Optional.fromNullable(template), userCredentials, sshHostAndPortOverride, setup);
            }

            if (usePortForwarding && sshHostAndPortOverride.isPresent()) {
                // Now that we have the sshMachineLocation, we can associate the port-forwarding address with it.
                PortForwardManager portForwardManager = setup.get(PORT_FORWARDING_MANAGER);
                if (portForwardManager != null) {
                    portForwardManager.associate(node.getId(), sshHostAndPortOverride.get(), machineLocation,
                            node.getLoginPort());
                } else {
                    LOG.warn("No port-forward manager for {} so could not associate {} -> {} for {}",
                            new Object[] { this, node.getLoginPort(), sshHostAndPortOverride, machineLocation });
                }
            }

            if ("docker".equals(this.getProvider())) {
                if (windows) {
                    throw new UnsupportedOperationException("Docker not supported on Windows");
                }
                Map<Integer, Integer> portMappings = JcloudsUtil.dockerPortMappingsFor(this, node.getId());
                PortForwardManager portForwardManager = setup.get(PORT_FORWARDING_MANAGER);
                if (portForwardManager != null) {
                    for (Integer containerPort : portMappings.keySet()) {
                        Integer hostPort = portMappings.get(containerPort);
                        String dockerHost = ((JcloudsSshMachineLocation) machineLocation).getSshHostAndPort()
                                .getHostText();
                        portForwardManager.associate(node.getId(), HostAndPort.fromParts(dockerHost, hostPort),
                                machineLocation, containerPort);
                    }
                } else {
                    LOG.warn("No port-forward manager for {} so could not associate docker port-mappings for {}",
                            this, machineLocation);
                }
            }

            List<String> customisationForLogging = new ArrayList<String>();
            // Apply same securityGroups rules to iptables, if iptables is running on the node
            if (waitForSshable) {

                String setupScript = setup.get(JcloudsLocationConfig.CUSTOM_MACHINE_SETUP_SCRIPT_URL);
                List<String> setupScripts = setup.get(JcloudsLocationConfig.CUSTOM_MACHINE_SETUP_SCRIPT_URL_LIST);
                Collection<String> allScripts = new MutableList<String>().appendIfNotNull(setupScript)
                        .appendAll(setupScripts);
                for (String setupScriptItem : allScripts) {
                    if (Strings.isNonBlank(setupScriptItem)) {
                        customisationForLogging.add("custom setup script " + setupScriptItem);

                        String setupVarsString = setup.get(JcloudsLocationConfig.CUSTOM_MACHINE_SETUP_SCRIPT_VARS);
                        Map<String, String> substitutions = (setupVarsString != null)
                                ? Splitter.on(",").withKeyValueSeparator(":").split(setupVarsString)
                                : ImmutableMap.<String, String>of();
                        String scriptContent = ResourceUtils.create(this).getResourceAsString(setupScriptItem);
                        String script = TemplateProcessor.processTemplateContents(scriptContent,
                                getManagementContext(), substitutions);
                        if (windows) {
                            ((WinRmMachineLocation) machineLocation)
                                    .executeCommand(ImmutableList.copyOf((script.replace("\r", "").split("\n"))));
                        } else {
                            ((SshMachineLocation) machineLocation).execCommands("Customizing node " + this,
                                    ImmutableList.of(script));
                        }
                    }
                }

                if (setup.get(JcloudsLocationConfig.MAP_DEV_RANDOM_TO_DEV_URANDOM)) {
                    if (windows) {
                        LOG.warn("Ignoring flag MAP_DEV_RANDOM_TO_DEV_URANDOM on Windows location {}",
                                machineLocation);
                    } else {
                        customisationForLogging.add("point /dev/random to urandom");

                        ((SshMachineLocation) machineLocation).execCommands("using urandom instead of random",
                                Arrays.asList("sudo mv /dev/random /dev/random-real",
                                        "sudo ln -s /dev/urandom /dev/random"));
                    }
                }

                if (setup.get(GENERATE_HOSTNAME)) {
                    if (windows) {
                        // TODO: Generate Windows Hostname
                        LOG.warn("Ignoring flag GENERATE_HOSTNAME on Windows location {}", machineLocation);
                    } else {
                        customisationForLogging.add("configure hostname");

                        ((SshMachineLocation) machineLocation).execCommands("Generate hostname " + node.getName(),
                                Arrays.asList("sudo hostname " + node.getName(),
                                        "sudo sed -i \"s/HOSTNAME=.*/HOSTNAME=" + node.getName()
                                                + "/g\" /etc/sysconfig/network",
                                        "sudo bash -c \"echo 127.0.0.1   `hostname` >> /etc/hosts\""));
                    }
                }

                if (setup.get(OPEN_IPTABLES)) {
                    if (windows) {
                        LOG.warn("Ignoring DEPRECATED flag OPEN_IPTABLES on Windows location {}", machineLocation);
                    } else {
                        LOG.warn(
                                "Using DEPRECATED flag OPEN_IPTABLES (will not be supported in future versions) for {} at {}",
                                machineLocation, this);

                        @SuppressWarnings("unchecked")
                        Iterable<Integer> inboundPorts = (Iterable<Integer>) setup.get(INBOUND_PORTS);

                        if (inboundPorts == null || Iterables.isEmpty(inboundPorts)) {
                            LOG.info("No ports to open in iptables (no inbound ports) for {} at {}",
                                    machineLocation, this);
                        } else {
                            customisationForLogging.add("open iptables");

                            List<String> iptablesRules = Lists.newArrayList();

                            if (isLocationFirewalldEnabled((SshMachineLocation) machineLocation)) {
                                for (Integer port : inboundPorts) {
                                    iptablesRules.add(IptablesCommands.addFirewalldRule(Chain.INPUT, Protocol.TCP,
                                            port, Policy.ACCEPT));
                                }
                            } else {
                                iptablesRules = createIptablesRulesForNetworkInterface(inboundPorts);
                                iptablesRules.add(IptablesCommands.saveIptablesRules());
                            }
                            List<String> batch = Lists.newArrayList();
                            // Some entities, such as Riak (erlang based) have a huge range of ports, which leads to a script that
                            // is too large to run (fails with a broken pipe). Batch the rules into batches of 50
                            for (String rule : iptablesRules) {
                                batch.add(rule);
                                if (batch.size() == 50) {
                                    ((SshMachineLocation) machineLocation)
                                            .execCommands("Inserting iptables rules, 50 command batch", batch);
                                    batch.clear();
                                }
                            }
                            if (batch.size() > 0) {
                                ((SshMachineLocation) machineLocation).execCommands("Inserting iptables rules",
                                        batch);
                            }
                            ((SshMachineLocation) machineLocation).execCommands("List iptables rules",
                                    ImmutableList.of(IptablesCommands.listIptablesRule()));
                        }
                    }
                }

                if (setup.get(STOP_IPTABLES)) {
                    if (windows) {
                        LOG.warn("Ignoring DEPRECATED flag OPEN_IPTABLES on Windows location {}", machineLocation);
                    } else {
                        LOG.warn(
                                "Using DEPRECATED flag STOP_IPTABLES (will not be supported in future versions) for {} at {}",
                                machineLocation, this);

                        customisationForLogging.add("stop iptables");

                        List<String> cmds = ImmutableList.<String>of();
                        if (isLocationFirewalldEnabled((SshMachineLocation) machineLocation)) {
                            cmds = ImmutableList.of(IptablesCommands.firewalldServiceStop(),
                                    IptablesCommands.firewalldServiceStatus());
                        } else {
                            cmds = ImmutableList.of(IptablesCommands.iptablesServiceStop(),
                                    IptablesCommands.iptablesServiceStatus());
                        }
                        ((SshMachineLocation) machineLocation).execCommands("Stopping iptables", cmds);
                    }
                }

                List<String> extraKeyUrlsToAuth = setup.get(EXTRA_PUBLIC_KEY_URLS_TO_AUTH);
                if (extraKeyUrlsToAuth != null && !extraKeyUrlsToAuth.isEmpty()) {
                    if (windows) {
                        LOG.warn("Ignoring flag EXTRA_PUBLIC_KEY_URLS_TO_AUTH on Windows location",
                                machineLocation);
                    } else {
                        List<String> extraKeyDataToAuth = MutableList.of();
                        for (String keyUrl : extraKeyUrlsToAuth) {
                            extraKeyDataToAuth.add(ResourceUtils.create().getResourceAsString(keyUrl));
                        }
                        ((SshMachineLocation) machineLocation).execCommands("Authorizing ssh keys",
                                ImmutableList.of(new AuthorizeRSAPublicKeys(extraKeyDataToAuth)
                                        .render(org.jclouds.scriptbuilder.domain.OsFamily.UNIX)));
                    }
                }

            } else {
                // Otherwise we have deliberately not waited to be ssh'able, so don't try now to
                // ssh to exec these commands!
            }

            // Apply any optional app-specific customization.
            for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
                LOG.debug("Customizing machine {}, using customizer {}", machineLocation, customizer);
                customizer.customize(this, computeService, machineLocation);
            }
            for (MachineLocationCustomizer customizer : getMachineCustomizers(setup)) {
                LOG.debug("Customizing machine {}, using customizer {}", machineLocation, customizer);
                customizer.customize(machineLocation);
            }

            customizedTimestamp = Duration.of(provisioningStopwatch);

            try {
                String logMessage = "Finished VM " + setup.getDescription() + " creation:" + " "
                        + machineLocation.getUser() + "@" + machineLocation.getAddress() + ":"
                        + machineLocation.getPort()
                        + (Boolean.TRUE.equals(setup.get(LOG_CREDENTIALS))
                                ? "password=" + userCredentials.getOptionalPassword().or("<absent>") + " && key="
                                        + userCredentials.getOptionalPrivateKey().or("<absent>")
                                : "")
                        + " ready after " + Duration.of(provisioningStopwatch).toStringRounded() + " ("
                        + "semaphore obtained in " + Duration.of(semaphoreTimestamp).toStringRounded() + ";"
                        + template + " template built in "
                        + Duration.of(templateTimestamp).subtract(semaphoreTimestamp).toStringRounded() + ";" + " "
                        + node + " provisioned in "
                        + Duration.of(provisionTimestamp).subtract(templateTimestamp).toStringRounded() + ";" + " "
                        + machineLocation + " connection usable in "
                        + Duration.of(usableTimestamp).subtract(provisionTimestamp).toStringRounded() + ";"
                        + " and os customized in "
                        + Duration.of(customizedTimestamp).subtract(usableTimestamp).toStringRounded() + " - "
                        + Joiner.on(", ").join(customisationForLogging) + ")";
                LOG.info(logMessage);
            } catch (Exception e) {
                // TODO Remove try-catch! @Nakomis: why did you add it? What exception happened during logging?
                Exceptions.propagateIfFatal(e);
                LOG.warn("Problem generating log message summarising completion of jclouds machine provisioning "
                        + machineLocation + " by " + this, e);
            }

            return machineLocation;

        } catch (Exception e) {
            if (e instanceof RunNodesException && ((RunNodesException) e).getNodeErrors().size() > 0) {
                node = Iterables.get(((RunNodesException) e).getNodeErrors().keySet(), 0);
            }
            // sometimes AWS nodes come up busted (eg ssh not allowed); just throw it back (and maybe try for another one)
            boolean destroyNode = (node != null) && Boolean.TRUE.equals(setup.get(DESTROY_ON_FAILURE));

            if (e.toString().contains("VPCResourceNotSpecified")) {
                LOG.error(
                        "Detected that your EC2 account is a legacy 'classic' account, but the recommended instance type requires VPC. "
                                + "You can specify the 'eu-central-1' region to avoid this problem, or you can specify a classic-compatible instance type, "
                                + "or you can specify a subnet to use with 'networkName' "
                                + "(taking care that the subnet auto-assigns public IP's and allows ingress on all ports, "
                                + "as Brooklyn does not currently configure security groups for non-default VPC's; "
                                + "or setting up Brooklyn to be in the subnet or have a jump host or other subnet access configuration). "
                                + "For more information on VPC vs classic see http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-vpc.html.");
            }

            LOG.error(
                    "Failed to start VM for " + setup.getDescription() + (destroyNode ? " (destroying)" : "")
                            + (node != null ? "; node " + node : "") + " after "
                            + Duration.of(provisioningStopwatch).toStringRounded()
                            + (semaphoreTimestamp != null
                                    ? " (" + "semaphore obtained in "
                                            + Duration.of(semaphoreTimestamp).toStringRounded() + ";"
                                            + (templateTimestamp != null && semaphoreTimestamp != null
                                                    ? " template built in " + Duration.of(templateTimestamp)
                                                            .subtract(semaphoreTimestamp).toStringRounded() + ";"
                                                    : "")
                                            + (provisionTimestamp != null && templateTimestamp != null
                                                    ? " node provisioned in " + Duration.of(provisionTimestamp)
                                                            .subtract(templateTimestamp).toStringRounded() + ";"
                                                    : "")
                                            + (usableTimestamp != null && provisioningStopwatch != null
                                                    ? " connection usable in "
                                                            + Duration.of(usableTimestamp)
                                                                    .subtract(provisionTimestamp).toStringRounded()
                                                            + ";"
                                                    : "")
                                            + (customizedTimestamp != null && usableTimestamp != null
                                                    ? " and OS customized in " + Duration.of(customizedTimestamp)
                                                            .subtract(usableTimestamp).toStringRounded()
                                                    : "")
                                            + ")"
                                    : "")
                            + ": " + e.getMessage());
            LOG.debug(Throwables.getStackTraceAsString(e));

            if (destroyNode) {
                Stopwatch destroyingStopwatch = Stopwatch.createStarted();
                if (machineLocation != null) {
                    releaseSafely(machineLocation);
                } else {
                    releaseNodeSafely(node);
                }
                LOG.info("Destroyed " + (machineLocation != null ? "machine " + machineLocation : "node " + node)
                        + " in " + Duration.of(destroyingStopwatch).toStringRounded());
            }

            throw Exceptions.propagate(e);
        }
    }

    // ------------- suspend and resume ------------------------------------

    /**
     * Suspends the given location.
     * <p>
     * Note that this method does <b>not</b> call the lifecycle methods of any
     * {@link #getCustomizers(ConfigBag) customizers} attached to this location.
     */
    @Override
    public void suspendMachine(MachineLocation rawLocation) {
        String instanceId = vmInstanceIds.remove(rawLocation);
        if (instanceId == null) {
            LOG.info("Attempt to suspend unknown machine " + rawLocation + " in " + this);
            throw new IllegalArgumentException("Unknown machine " + rawLocation);
        }
        LOG.info("Suspending machine {} in {}, instance id {}", new Object[] { rawLocation, this, instanceId });
        Exception toThrow = null;
        try {
            getComputeService().suspendNode(instanceId);
        } catch (Exception e) {
            toThrow = e;
            LOG.error("Problem suspending machine " + rawLocation + " in " + this + ", instance id " + instanceId,
                    e);
        }
        removeChild(rawLocation);
        if (toThrow != null) {
            throw Exceptions.propagate(toThrow);
        }
    }

    /**
     * Brings an existing machine with the given details under management.
     * <p/>
     * Note that this method does <b>not</b> call the lifecycle methods of any
     * {@link #getCustomizers(ConfigBag) customizers} attached to this location.
     *
     * @param flags See {@link #registerMachine(ConfigBag)} for a description of required fields.
     * @see #registerMachine(ConfigBag)
     */
    @Override
    public MachineLocation resumeMachine(Map<?, ?> flags) {
        ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
        LOG.info("{} using resuming node matching properties: {}", this, Sanitizer.sanitize(setup));
        ComputeService computeService = getComputeService(setup);
        NodeMetadata node = findNodeOrThrow(setup);
        LOG.debug("{} resuming {}", 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);
        MachineLocation registered = registerMachineLocation(setup, node);
        LOG.info("{} resumed and registered {}", this, registered);
        return registered;
    }

    // ------------- constructing the template, etc ------------------------

    private static interface CustomizeTemplateBuilder {
        void apply(TemplateBuilder tb, ConfigBag props, Object v);
    }

    public static interface CustomizeTemplateOptions {
        void apply(TemplateOptions tb, ConfigBag props, Object v);
    }

    /** properties which cause customization of the TemplateBuilder */
    public static final Map<ConfigKey<?>, CustomizeTemplateBuilder> SUPPORTED_TEMPLATE_BUILDER_PROPERTIES = ImmutableMap
            .<ConfigKey<?>, CustomizeTemplateBuilder>builder().put(OS_64_BIT, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    Boolean os64Bit = TypeCoercions.coerce(v, Boolean.class);
                    if (os64Bit != null)
                        tb.os64Bit(os64Bit);
                }
            }).put(MIN_RAM, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    tb.minRam((int) (ByteSizeStrings.parse(Strings.toString(v), "mb") / 1000 / 1000));
                }
            }).put(MIN_CORES, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    tb.minCores(TypeCoercions.coerce(v, Double.class));
                }
            }).put(MIN_DISK, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    tb.minDisk((int) (ByteSizeStrings.parse(Strings.toString(v), "gb") / 1000 / 1000 / 1000));
                }
            }).put(HARDWARE_ID, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    tb.hardwareId(((CharSequence) v).toString());
                }
            }).put(IMAGE_ID, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    tb.imageId(((CharSequence) v).toString());
                }
            }).put(IMAGE_DESCRIPTION_REGEX, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    tb.imageDescriptionMatches(((CharSequence) v).toString());
                }
            }).put(IMAGE_NAME_REGEX, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    tb.imageNameMatches(((CharSequence) v).toString());
                }
            }).put(OS_FAMILY, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    Maybe<OsFamily> osFamily = Enums.valueOfIgnoreCase(OsFamily.class, v.toString());
                    if (osFamily.isAbsent())
                        throw new IllegalArgumentException("Invalid " + OS_FAMILY + " value " + v);
                    tb.osFamily(osFamily.get());
                }
            }).put(OS_VERSION_REGEX, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    tb.osVersionMatches(((CharSequence) v).toString());
                }
            }).put(TEMPLATE_SPEC, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    tb.from(TemplateBuilderSpec.parse(((CharSequence) v).toString()));
                }
            }).put(DEFAULT_IMAGE_ID, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    /* done in the code, but included here so that it is in the map */
                }
            }).put(TEMPLATE_BUILDER, new CustomizeTemplateBuilder() {
                public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                    /* done in the code, but included here so that it is in the map */
                }
            }).build();

    /** properties which cause customization of the TemplateOptions */
    public static final Map<ConfigKey<?>, CustomizeTemplateOptions> SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES = ImmutableMap
            .<ConfigKey<?>, CustomizeTemplateOptions>builder().put(SECURITY_GROUPS, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof EC2TemplateOptions) {
                        String[] securityGroups = toStringArray(v);
                        ((EC2TemplateOptions) t).securityGroups(securityGroups);
                    } else if (t instanceof NovaTemplateOptions) {
                        String[] securityGroups = toStringArray(v);
                        ((NovaTemplateOptions) t).securityGroups(securityGroups);
                    } else if (t instanceof SoftLayerTemplateOptions) {
                        String[] securityGroups = toStringArray(v);
                        ((SoftLayerTemplateOptions) t).securityGroups(securityGroups);
                    } else if (t instanceof GoogleComputeEngineTemplateOptions) {
                        String[] securityGroups = toStringArray(v);
                        ((GoogleComputeEngineTemplateOptions) t).securityGroups(securityGroups);
                    } else {
                        LOG.info(
                                "ignoring securityGroups({}) in VM creation because not supported for cloud/type ({})",
                                v, t.getClass());
                    }
                }
            }).put(INBOUND_PORTS, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    int[] inboundPorts = toIntPortArray(v);
                    if (LOG.isDebugEnabled())
                        LOG.debug("opening inbound ports {} for cloud/type {}", Arrays.toString(inboundPorts),
                                t.getClass());
                    t.inboundPorts(inboundPorts);
                }
            }).put(USER_METADATA_STRING, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof EC2TemplateOptions) {
                        // See AWS docs: http://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/UsingConfig_WinAMI.html#user-data-execution
                        if (v == null)
                            return;
                        String data = v.toString();
                        if (!(data.startsWith("<script>") || data.startsWith("<powershell>"))) {
                            data = "<script> " + data + " </script>";
                        }
                        ((EC2TemplateOptions) t).userData(data.getBytes());
                    } else if (t instanceof SoftLayerTemplateOptions) {
                        ((SoftLayerTemplateOptions) t).userData(Strings.toString(v));
                    } else {
                        // Try reflection: userData(String), or guestCustomizationScript(String);
                        // the latter is used by vCloud Director.
                        Class<? extends TemplateOptions> clazz = t.getClass();
                        Method userDataMethod = null;
                        try {
                            userDataMethod = clazz.getMethod("userData", String.class);
                        } catch (SecurityException e) {
                            LOG.info("Problem reflectively inspecting methods of " + t.getClass()
                                    + " for setting userData", e);
                        } catch (NoSuchMethodException e) {
                            try {
                                // For vCloud Director
                                userDataMethod = clazz.getMethod("guestCustomizationScript", String.class);
                            } catch (NoSuchMethodException e2) {
                                // expected on various other clouds
                            }
                        }
                        if (userDataMethod != null) {
                            try {
                                userDataMethod.invoke(t, Strings.toString(v));
                            } catch (InvocationTargetException e) {
                                LOG.info("Problem invoking " + userDataMethod.getName() + " of " + t.getClass()
                                        + ", for setting userData (rethrowing)", e);
                                throw Exceptions.propagate(e);
                            } catch (IllegalAccessException e) {
                                LOG.debug("Unable to reflectively invoke " + userDataMethod.getName() + " of "
                                        + t.getClass() + ", for setting userData (rethrowing)", e);
                                throw Exceptions.propagate(e);
                            }
                        } else {
                            LOG.info(
                                    "ignoring userDataString({}) in VM creation because not supported for cloud/type ({})",
                                    v, t.getClass());
                        }
                    }
                }
            }).put(USER_DATA_UUENCODED, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof EC2TemplateOptions) {
                        byte[] bytes = toByteArray(v);
                        ((EC2TemplateOptions) t).userData(bytes);
                    } else if (t instanceof SoftLayerTemplateOptions) {
                        ((SoftLayerTemplateOptions) t).userData(Strings.toString(v));
                    } else {
                        LOG.info("ignoring userData({}) in VM creation because not supported for cloud/type ({})",
                                v, t.getClass());
                    }
                }
            }).put(STRING_TAGS, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    List<String> tags = toListOfStrings(v);
                    if (LOG.isDebugEnabled())
                        LOG.debug("setting VM tags {} for {}", tags, t);
                    t.tags(tags);
                }
            }).put(USER_METADATA_MAP, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (v != null) {
                        t.userMetadata(toMapStringString(v));
                    }
                }
            }).put(EXTRA_PUBLIC_KEY_DATA_TO_AUTH, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof GoogleComputeEngineTemplateOptions) {
                        // see email to jclouds list, 29 Aug 2015; 
                        // GCE takes this to be the only login public key, 
                        // and setting this only works if you also overrideLoginPrivateKey
                        LOG.warn("Ignoring " + EXTRA_PUBLIC_KEY_DATA_TO_AUTH
                                + "; not supported in jclouds-gce implementation.");
                    }
                    t.authorizePublicKey(((CharSequence) v).toString());
                }
            }).put(RUN_AS_ROOT, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    t.runAsRoot((Boolean) v);
                }
            }).put(LOGIN_USER, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (v != null) {
                        t.overrideLoginUser(((CharSequence) v).toString());
                    }
                }
            }).put(LOGIN_USER_PASSWORD, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (v != null) {
                        t.overrideLoginPassword(((CharSequence) v).toString());
                    }
                }
            }).put(LOGIN_USER_PRIVATE_KEY_FILE, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (v != null) {
                        String privateKeyFileName = ((CharSequence) v).toString();
                        String privateKey;
                        try {
                            privateKey = Files.toString(new File(Os.tidyPath(privateKeyFileName)), Charsets.UTF_8);
                        } catch (IOException e) {
                            LOG.error(privateKeyFileName + "not found", e);
                            throw Exceptions.propagate(e);
                        }
                        t.overrideLoginPrivateKey(privateKey);
                    }
                }
            }).put(LOGIN_USER_PRIVATE_KEY_DATA, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (v != null) {
                        t.overrideLoginPrivateKey(((CharSequence) v).toString());
                    }
                }
            }).put(KEY_PAIR, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof EC2TemplateOptions) {
                        ((EC2TemplateOptions) t).keyPair(((CharSequence) v).toString());
                    } else if (t instanceof NovaTemplateOptions) {
                        ((NovaTemplateOptions) t).keyPairName(((CharSequence) v).toString());
                    } else if (t instanceof CloudStackTemplateOptions) {
                        ((CloudStackTemplateOptions) t).keyPair(((CharSequence) v).toString());
                    } else {
                        LOG.info("ignoring keyPair({}) in VM creation because not supported for cloud/type ({})", v,
                                t);
                    }
                }
            }).put(AUTO_GENERATE_KEYPAIRS, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof NovaTemplateOptions) {
                        ((NovaTemplateOptions) t).generateKeyPair((Boolean) v);
                    } else if (t instanceof CloudStackTemplateOptions) {
                        ((CloudStackTemplateOptions) t).generateKeyPair((Boolean) v);
                    } else {
                        LOG.info(
                                "ignoring auto-generate-keypairs({}) in VM creation because not supported for cloud/type ({})",
                                v, t);
                    }
                }
            }).put(AUTO_CREATE_FLOATING_IPS, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof NovaTemplateOptions) {
                        ((NovaTemplateOptions) t).autoAssignFloatingIp((Boolean) v);
                    } else {
                        LOG.info(
                                "ignoring auto-generate-floating-ips({}) in VM creation because not supported for cloud/type ({})",
                                v, t);
                    }
                }
            }).put(AUTO_ASSIGN_FLOATING_IP, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof NovaTemplateOptions) {
                        ((NovaTemplateOptions) t).autoAssignFloatingIp((Boolean) v);
                    } else if (t instanceof CloudStackTemplateOptions) {
                        ((CloudStackTemplateOptions) t).setupStaticNat((Boolean) v);
                    } else {
                        LOG.info(
                                "ignoring auto-assign-floating-ip({}) in VM creation because not supported for cloud/type ({})",
                                v, t);
                    }
                }
            }).put(NETWORK_NAME, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof AWSEC2TemplateOptions) {
                        // subnet ID is the sensible interpretation of network name in EC2
                        ((AWSEC2TemplateOptions) t).subnetId((String) v);

                    } else {
                        if (t instanceof GoogleComputeEngineTemplateOptions) {
                            // no warning needed
                            // we think this is the only jclouds endpoint which supports this option

                        } else if (t instanceof SoftLayerTemplateOptions) {
                            LOG.warn(
                                    "networkName is not be supported in SoftLayer; use `templateOptions` with `primaryNetworkComponentNetworkVlanId` or `primaryNetworkBackendComponentNetworkVlanId`");
                        } else if (!(t instanceof CloudStackTemplateOptions)
                                && !(t instanceof NovaTemplateOptions)) {
                            LOG.warn(
                                    "networkName is experimental in many jclouds endpoints may not be supported in this cloud");
                            // NB, from @andreaturli
                            //                                Cloudstack uses custom securityGroupIds and networkIds not the generic networks
                            //                                Openstack Nova uses securityGroupNames which is marked as @deprecated (suggests to use groups which is maybe even more confusing)
                            //                                Azure supports the custom networkSecurityGroupName
                        }

                        t.networks((String) v);
                    }
                }
            }).put(DOMAIN_NAME, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof SoftLayerTemplateOptions) {
                        ((SoftLayerTemplateOptions) t).domainName(TypeCoercions.coerce(v, String.class));
                    } else {
                        LOG.info(
                                "ignoring domain-name({}) in VM creation because not supported for cloud/type ({})",
                                v, t);
                    }
                }
            }).put(TEMPLATE_OPTIONS, new CustomizeTemplateOptions() {
                @Override
                public void apply(TemplateOptions options, ConfigBag config, Object v) {
                    if (v == null)
                        return;
                    @SuppressWarnings("unchecked")
                    Map<String, Object> optionsMap = (Map<String, Object>) v;
                    if (optionsMap.isEmpty())
                        return;

                    Class<? extends TemplateOptions> clazz = options.getClass();
                    for (final Map.Entry<String, Object> option : optionsMap.entrySet()) {
                        Maybe<?> result = MethodCoercions.tryFindAndInvokeBestMatchingMethod(options,
                                option.getKey(), option.getValue());
                        if (result.isAbsent()) {
                            LOG.warn(
                                    "Ignoring request to set template option {} because this is not supported by {}",
                                    new Object[] { option.getKey(), clazz.getCanonicalName() });
                        }
                    }
                }
            }).build();

    /** hook whereby template customizations can be made for various clouds */
    protected void customizeTemplate(ConfigBag setup, ComputeService computeService, Template template) {
        for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
            customizer.customize(this, computeService, template);
            customizer.customize(this, computeService, template.getOptions());
        }

        // these things are nice on softlayer
        if (template.getOptions() instanceof SoftLayerTemplateOptions) {
            SoftLayerTemplateOptions slT = ((SoftLayerTemplateOptions) template.getOptions());
            if (Strings.isBlank(slT.getDomainName()) || "jclouds.org".equals(slT.getDomainName())) {
                // set a quasi-sensible domain name if none was provided (better than the default, jclouds.org)
                // NB: things like brooklyn.local are disallowed
                slT.domainName("local.brooklyncentral.org");
            }
            // convert user metadata to tags and notes because user metadata is otherwise ignored
            Map<String, String> md = slT.getUserMetadata();
            if (md != null && !md.isEmpty()) {
                Set<String> tags = MutableSet.copyOf(slT.getTags());
                for (Map.Entry<String, String> entry : md.entrySet()) {
                    tags.add(AbstractCloudMachineNamer.sanitize(entry.getKey()) + ":"
                            + AbstractCloudMachineNamer.sanitize(entry.getValue()));
                }
                slT.tags(tags);

                if (!md.containsKey("notes")) {
                    String notes = "User Metadata\n=============\n\n  * "
                            + Joiner.on("\n  * ").withKeyValueSeparator(": ").join(md);
                    if (notes.length() > NOTES_MAX_LENGTH) {
                        String truncatedMsg = "...\n<truncated - notes total length is " + notes.length()
                                + " characters>";
                        notes = notes.substring(0, NOTES_MAX_LENGTH - truncatedMsg.length()) + truncatedMsg;
                    }
                    md.put("notes", notes);
                }
            }
        }
    }

    /** returns the jclouds Template which describes the image to be built, for the given config and compute service */
    public Template buildTemplate(ComputeService computeService, ConfigBag config) {
        TemplateBuilder templateBuilder = (TemplateBuilder) config.get(TEMPLATE_BUILDER);
        if (templateBuilder == null) {
            templateBuilder = new PortableTemplateBuilder<PortableTemplateBuilder<?>>();
        } else {
            LOG.debug("jclouds using templateBuilder {} as custom base for provisioning in {} for {}",
                    new Object[] { templateBuilder, this, config.getDescription() });
        }
        if (templateBuilder instanceof PortableTemplateBuilder<?>) {
            if (((PortableTemplateBuilder<?>) templateBuilder).imageChooser() == null) {
                Function<Iterable<? extends Image>, Image> chooser = config
                        .get(JcloudsLocationConfig.IMAGE_CHOOSER);
                chooser = BrooklynImageChooser.cloneFor(chooser, computeService);
                templateBuilder.imageChooser(chooser);
            } else {
                // an image chooser is already set, so do nothing
            }
        } else {
            // template builder supplied, and we cannot check image chooser status; warn, for now
            LOG.warn(
                    "Cannot check imageChooser status for {} due to manually supplied black-box TemplateBuilder; "
                            + "it is recommended to use a PortableTemplateBuilder if you supply a TemplateBuilder",
                    config.getDescription());
        }

        if (!Strings.isEmpty(config.get(CLOUD_REGION_ID))) {
            templateBuilder.locationId(config.get(CLOUD_REGION_ID));
        }

        // Apply the template builder and options properties
        for (Map.Entry<ConfigKey<?>, CustomizeTemplateBuilder> entry : SUPPORTED_TEMPLATE_BUILDER_PROPERTIES
                .entrySet()) {
            ConfigKey<?> name = entry.getKey();
            CustomizeTemplateBuilder code = entry.getValue();
            if (config.containsKey(name))
                code.apply(templateBuilder, config, config.get(name));
        }

        if (templateBuilder instanceof PortableTemplateBuilder) {
            ((PortableTemplateBuilder<?>) templateBuilder).attachComputeService(computeService);
            // do the default last, and only if nothing else specified (guaranteed to be a PTB if nothing else specified)
            if (groovyTruth(config.get(DEFAULT_IMAGE_ID))) {
                if (((PortableTemplateBuilder<?>) templateBuilder).isBlank()) {
                    templateBuilder.imageId(config.get(DEFAULT_IMAGE_ID).toString());
                }
            }
        }

        // Then apply any optional app-specific customization.
        for (JcloudsLocationCustomizer customizer : getCustomizers(config)) {
            customizer.customize(this, computeService, templateBuilder);
        }

        LOG.debug("jclouds using templateBuilder {} for provisioning in {} for {}",
                new Object[] { templateBuilder, this, config.getDescription() });

        // Finally try to build the template
        Template template;
        Image image;
        try {
            template = templateBuilder.build();
            if (template == null)
                throw new NullPointerException("No template found (templateBuilder.build returned null)");
            image = template.getImage();
            LOG.debug("jclouds found template " + template + " (image " + image + ") for provisioning in " + this
                    + " for " + config.getDescription());
            if (image == null)
                throw new NullPointerException(
                        "Template does not contain an image (templateBuilder.build returned invalid template)");
        } catch (AuthorizationException e) {
            LOG.warn("Error resolving template: not authorized (rethrowing: " + e + ")");
            throw new IllegalStateException(
                    "Not authorized to access cloud " + this + " to resolve " + templateBuilder, e);
        } catch (Exception e) {
            try {
                IOException ioe = Exceptions.getFirstThrowableOfType(e, IOException.class);
                if (ioe != null) {
                    LOG.warn("IOException found...", ioe);
                    throw ioe;
                }
                if (listedAvailableTemplatesOnNoSuchTemplate.compareAndSet(false, true)) {
                    // delay subsequent log.warns (put in synch block) so the "Loading..." message is obvious
                    LOG.warn("Unable to match required VM template constraints " + templateBuilder
                            + " when trying to provision VM in " + this + " (rethrowing): " + e);
                    logAvailableTemplates(config);
                }
            } catch (Exception e2) {
                LOG.warn(
                        "Error loading available images to report (following original error matching template which will be rethrown): "
                                + e2,
                        e2);
                throw new IllegalStateException(
                        "Unable to access cloud " + this + " to resolve " + templateBuilder + ": " + e, e);
            }
            throw new IllegalStateException("Unable to match required VM template constraints " + templateBuilder
                    + " when trying to provision VM in " + this + "; " + "see list of images in log. Root cause: "
                    + e, e);
        }
        TemplateOptions options = template.getOptions();

        boolean windows = isWindows(template, config);
        if (windows) {
            if (!(config.containsKey(JcloudsLocationConfig.USER_METADATA_STRING)
                    || config.containsKey(JcloudsLocationConfig.USER_METADATA_MAP))) {
                config.put(JcloudsLocationConfig.USER_METADATA_STRING,
                        WinRmMachineLocation.getDefaultUserMetadataString());
            }
        }

        for (Map.Entry<ConfigKey<?>, CustomizeTemplateOptions> entry : SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES
                .entrySet()) {
            ConfigKey<?> key = entry.getKey();
            CustomizeTemplateOptions code = entry.getValue();
            if (config.containsKey(key))
                code.apply(options, config, config.get(key));
        }

        return template;
    }

    protected void logAvailableTemplates(ConfigBag config) {
        LOG.info("Loading available images at " + this + " for reference...");
        ConfigBag m1 = ConfigBag.newInstanceCopying(config);
        if (m1.containsKey(IMAGE_ID)) {
            // if caller specified an image ID, remove that, but don't apply default filters
            m1.remove(IMAGE_ID);
            // TODO use key
            m1.putStringKey("anyOwner", true);
        }
        ComputeService computeServiceLessRestrictive = getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(m1,
                true);
        Set<? extends Image> imgs = computeServiceLessRestrictive.listImages();
        LOG.info("" + imgs.size() + " available images at " + this);
        for (Image img : imgs) {
            LOG.info(" Image: " + img);
        }

        Set<? extends Hardware> profiles = computeServiceLessRestrictive.listHardwareProfiles();
        LOG.info("" + profiles.size() + " available profiles at " + this);
        for (Hardware profile : profiles) {
            LOG.info(" Profile: " + profile);
        }

        Set<? extends org.jclouds.domain.Location> assignableLocations = computeServiceLessRestrictive
                .listAssignableLocations();
        LOG.info("" + assignableLocations.size() + " available locations at " + this);
        for (org.jclouds.domain.Location assignableLocation : assignableLocations) {
            LOG.info(" Location: " + assignableLocation);
        }
    }

    /**
     * Creates a temporary ssh machine location (i.e. will not be persisted), which uses the given credentials.
     * It ignores any credentials (e.g. password, key-phrase, etc) that are supplied in the config.
     */
    protected SshMachineLocation createTemporarySshMachineLocation(HostAndPort hostAndPort, LoginCredentials creds,
            ConfigBag config) {
        String initialUser = creds.getUser();
        Optional<String> initialPassword = creds.getOptionalPassword();
        Optional<String> initialPrivateKey = creds.getOptionalPrivateKey();

        Map<String, Object> sshProps = Maps.newLinkedHashMap(config.getAllConfig());
        sshProps.put("user", initialUser);
        sshProps.put("address", hostAndPort.getHostText());
        sshProps.put("port", hostAndPort.getPort());
        sshProps.put(AbstractLocation.TEMPORARY_LOCATION.getName(), true);
        sshProps.put(LocalLocationManager.CREATE_UNMANAGED.getName(), true);
        sshProps.remove("password");
        sshProps.remove("privateKeyData");
        sshProps.remove("privateKeyFile");
        sshProps.remove("privateKeyPassphrase");

        if (initialPassword.isPresent())
            sshProps.put("password", initialPassword.get());
        if (initialPrivateKey.isPresent())
            sshProps.put("privateKeyData", initialPrivateKey.get());

        if (isManaged()) {
            return getManagementContext().getLocationManager().createLocation(sshProps, SshMachineLocation.class);
        } else {
            return new SshMachineLocation(sshProps);
        }
    }

    /**
     * Creates a temporary WinRM machine location (i.e. will not be persisted), which uses the given credentials.
     * It ignores any credentials (e.g. password, key-phrase, etc) that are supplied in the config.
     */
    protected WinRmMachineLocation createTemporaryWinRmMachineLocation(HostAndPort hostAndPort,
            LoginCredentials creds, ConfigBag config) {
        String initialUser = creds.getUser();
        Optional<String> initialPassword = creds.getOptionalPassword();
        Optional<String> initialPrivateKey = creds.getOptionalPrivateKey();

        Map<String, Object> winrmProps = Maps.newLinkedHashMap(config.getAllConfig());
        winrmProps.put("user", initialUser);
        winrmProps.put("address", hostAndPort.getHostText());
        winrmProps.put("port", hostAndPort.getPort());
        winrmProps.put(AbstractLocation.TEMPORARY_LOCATION.getName(), true);
        winrmProps.put(LocalLocationManager.CREATE_UNMANAGED.getName(), true);
        winrmProps.remove("password");
        winrmProps.remove("privateKeyData");
        winrmProps.remove("privateKeyFile");
        winrmProps.remove("privateKeyPassphrase");

        if (initialPassword.isPresent())
            winrmProps.put("password", initialPassword.get());
        if (initialPrivateKey.isPresent())
            winrmProps.put("privateKeyData", initialPrivateKey.get());

        if (isManaged()) {
            return getManagementContext().getLocationManager().createLocation(winrmProps,
                    WinRmMachineLocation.class);
        } else {
            throw new UnsupportedOperationException(
                    "Cannot create temporary WinRmMachineLocation because " + this + " is not managed");
        }
    }

    /**
     * Create the user immediately - executing ssh commands as required.
     */
    protected LoginCredentials createUser(ComputeService computeService, NodeMetadata node,
            Optional<HostAndPort> hostAndPortOverride, LoginCredentials initialCredentials, ConfigBag config) {
        Image image = (node.getImageId() != null) ? computeService.getImage(node.getImageId()) : null;
        UserCreation userCreation = createUserStatements(image, config);

        if (!userCreation.statements.isEmpty()) {
            // If unsure of OS family, default to unix for rendering statements.
            org.jclouds.scriptbuilder.domain.OsFamily scriptOsFamily;
            if (isWindows(node, config)) {
                scriptOsFamily = org.jclouds.scriptbuilder.domain.OsFamily.WINDOWS;
            } else {
                scriptOsFamily = org.jclouds.scriptbuilder.domain.OsFamily.UNIX;
            }

            boolean windows = isWindows(node, config);

            if (windows) {
                LOG.warn("Unable to execute statements on WinRM in JcloudsLocation; skipping for " + node + ": "
                        + userCreation.statements);

            } else {
                List<String> commands = Lists.newArrayList();
                for (Statement statement : userCreation.statements) {
                    InitAdminAccess initAdminAccess = new InitAdminAccess(new AdminAccessConfiguration.Default());
                    initAdminAccess.visit(statement);
                    commands.add(statement.render(scriptOsFamily));
                }

                String initialUser = initialCredentials.getUser();
                String address = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getHostText()
                        : getFirstReachableAddress(node, config);
                int port = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getPort()
                        : node.getLoginPort();

                // TODO Retrying lots of times as workaround for vcloud-director. There the guest customizations
                // can cause the VM to reboot shortly after it was ssh'able.
                Map<String, Object> execProps = Maps.newLinkedHashMap();
                execProps.put(ShellTool.PROP_RUN_AS_ROOT.getName(), true);
                execProps.put(SshTool.PROP_SSH_TRIES.getName(), 50);
                execProps.put(SshTool.PROP_SSH_TRIES_TIMEOUT.getName(), 10 * 60 * 1000);

                if (LOG.isDebugEnabled()) {
                    LOG.debug("VM {}: executing user creation/setup via {}@{}:{}; commands: {}",
                            new Object[] { config.getDescription(), initialUser, address, port, commands });
                }

                HostAndPort hostAndPort = hostAndPortOverride.isPresent() ? hostAndPortOverride.get()
                        : HostAndPort.fromParts(address, port);
                SshMachineLocation sshLoc = createTemporarySshMachineLocation(hostAndPort, initialCredentials,
                        config);
                try {
                    // BROOKLYN-188: for SUSE, need to specify the path (for groupadd, useradd, etc)
                    Map<String, ?> env = ImmutableMap.of("PATH", sbinPath());

                    int exitcode = sshLoc.execScript(execProps, "create-user", commands, env);

                    if (exitcode != 0) {
                        LOG.warn("exit code {} when creating user for {}; usage may subsequently fail", exitcode,
                                node);
                    }
                } finally {
                    getManagementContext().getLocationManager().unmanage(sshLoc);
                    Streams.closeQuietly(sshLoc);
                }
            }
        }

        return userCreation.createdUserCredentials;
    }

    /**
     * Setup the TemplateOptions to create the user.
     */
    protected LoginCredentials initTemplateForCreateUser(Template template, ConfigBag config) {
        UserCreation userCreation = createUserStatements(template.getImage(), config);

        if (userCreation.statements.size() > 0) {
            TemplateOptions options = template.getOptions();
            options.runScript(new StatementList(userCreation.statements));
        }

        return userCreation.createdUserCredentials;
    }

    protected static class UserCreation {
        public final LoginCredentials createdUserCredentials;
        public final List<Statement> statements;

        public UserCreation(LoginCredentials creds, List<Statement> statements) {
            this.createdUserCredentials = creds;
            this.statements = statements;
        }
    }

    /**
     * Returns the commands required to create the user, to be used for connecting (e.g. over ssh)
     * to the machine; also returns the expected login credentials.
     * <p>
     * The returned login credentials may be null if we haven't done any user-setup and no specific
     * user was supplied (i.e. if {@code dontCreateUser} was true and {@code user} was null or blank).
     * In which case, the caller should use the jclouds node's login credentials.
     * <p>
     * There are quite a few configuration options. Depending on their values, the user-creation
     * behaves differently:
     * <ul>
     *   <li>{@code dontCreateUser} says not to run any user-setup commands at all. If {@code user} is
     *       non-empty (including with the default value), then that user will subsequently be used,
     *       otherwise the (inferred) {@code loginUser} will be used.
     *   <li>{@code loginUser} refers to the existing user that jclouds should use when setting up the VM.
     *       Normally this will be inferred from the image (i.e. doesn't need to be explicitly set), but sometimes
     *       the image gets it wrong so this can be a handy override.
     *   <li>{@code user} is the username for brooklyn to subsequently use when ssh'ing to the machine.
     *       If not explicitly set, its value will default to the username of the user running brooklyn.
     *       <ul>
     *         <li>If the {@code user} value is null or empty, then the (inferred) {@code loginUser} will
     *             subsequently be used, setting up the password/authorizedKeys for that loginUser.
     *         <li>If the {@code user} is "root", then setup the password/authorizedKeys for root.
     *         <li>If the {@code user} equals the (inferred) {@code loginUser}, then don't try to create this
     *             user but instead just setup the password/authorizedKeys for the user.
     *         <li>Otherwise create the given user, setting up the password/authorizedKeys (unless
     *             {@code dontCreateUser} is set, obviously).
     *       </ul>
     *   <li>{@code publicKeyData} is the key to authorize (i.e. add to .ssh/authorized_keys),
     *       if not null or blank. Note the default is to use {@code ~/.ssh/id_rsa.pub} or {@code ~/.ssh/id_dsa.pub}
     *       if either of those files exist for the user running brooklyn.
     *       Related is {@code publicKeyFile}, which is used to populate publicKeyData.
     *   <li>{@code password} is the password to set for the user. If null or blank, then a random password
     *       will be auto-generated and set.
     *   <li>{@code privateKeyData} is the key to use when subsequent ssh'ing, if not null or blank.
     *       Note the default is to use {@code ~/.ssh/id_rsa} or {@code ~/.ssh/id_dsa}.
     *       The subsequent preferences for ssh'ing are:
     *       <ul>
     *         <li>Use the {@code privateKeyData} if not null or blank (including if using default)
     *         <li>Use the {@code password} (or the auto-generated password if that is blank).
     *       </ul>
     *   <li>{@code grantUserSudo} determines whether or not the created user may run the sudo command.</li>
     * </ul>
     *
     * @param image  The image being used to create the VM
     * @param config Configuration for creating the VM
     * @return       The commands required to create the user, along with the expected login credentials for that user,
     * or null if we are just going to use those from jclouds.
     */
    protected UserCreation createUserStatements(@Nullable Image image, ConfigBag config) {
        //NB: private key is not installed remotely, just used to get/validate the public key

        boolean windows = isWindows(image, config);
        String user = getUser(config);
        String explicitLoginUser = config.get(LOGIN_USER);
        String loginUser = groovyTruth(explicitLoginUser) ? explicitLoginUser
                : (image != null && image.getDefaultCredentials() != null) ? image.getDefaultCredentials().identity
                        : null;
        boolean dontCreateUser = config.get(DONT_CREATE_USER);
        boolean grantUserSudo = config.get(GRANT_USER_SUDO);
        OsCredential credential = LocationConfigUtils.getOsCredential(config);
        credential.checkNoErrors().logAnyWarnings();
        String passwordToSet = Strings.isNonBlank(credential.getPassword()) ? credential.getPassword()
                : Identifiers.makeRandomId(12);
        List<Statement> statements = Lists.newArrayList();
        LoginCredentials createdUserCreds = null;

        if (dontCreateUser) {
            // dontCreateUser:
            // if caller has not specified a user, we'll just continue to use the loginUser;
            // if caller *has*, we set up our credentials assuming that user and credentials already exist

            if (Strings.isBlank(user)) {
                // createdUserCreds returned from this method will be null;
                // we will use the creds returned by jclouds on the node
                LOG.info("Not setting up any user (subsequently using loginUser {})", user, loginUser);
                config.put(USER, loginUser);

            } else {
                LOG.info(
                        "Not creating user {}, and not installing its password or authorizing keys (assuming it exists)",
                        user);

                if (credential.isUsingPassword()) {
                    createdUserCreds = LoginCredentials.builder().user(user).password(credential.getPassword())
                            .build();
                    if (Boolean.FALSE.equals(config.get(DISABLE_ROOT_AND_PASSWORD_SSH))) {
                        statements.add(org.jclouds.scriptbuilder.statements.ssh.SshStatements
                                .sshdConfig(ImmutableMap.of("PasswordAuthentication", "yes")));
                    }
                } else if (credential.hasKey()) {
                    createdUserCreds = LoginCredentials.builder().user(user)
                            .privateKey(credential.getPrivateKeyData()).build();
                }
            }

        } else if (windows) {
            // TODO Generate statements to create the user.
            // createdUserCreds returned from this method will be null;
            // we will use the creds returned by jclouds on the node
            LOG.warn("Not creating or configuring user on Windows VM, despite " + DONT_CREATE_USER.getName()
                    + " set to false");

            // TODO extractVmCredentials() will use user:publicKeyData defaults, if we don't override this.
            // For linux, how would we configure Brooklyn to use the node.getCredentials() - i.e. the version
            // that the cloud automatically generated?
            if (config.get(USER) != null)
                config.put(USER, "");
            if (config.get(PASSWORD) != null)
                config.put(PASSWORD, "");
            if (config.get(PRIVATE_KEY_DATA) != null)
                config.put(PRIVATE_KEY_DATA, "");
            if (config.get(PRIVATE_KEY_FILE) != null)
                config.put(PRIVATE_KEY_FILE, "");
            if (config.get(PUBLIC_KEY_DATA) != null)
                config.put(PUBLIC_KEY_DATA, "");
            if (config.get(PUBLIC_KEY_FILE) != null)
                config.put(PUBLIC_KEY_FILE, "");

        } else if (Strings.isBlank(user) || user.equals(loginUser) || user.equals(ROOT_USERNAME)) {
            boolean useKey = Strings.isNonBlank(credential.getPublicKeyData());

            // For subsequent ssh'ing, we'll be using the loginUser
            if (Strings.isBlank(user)) {
                user = loginUser;
                config.put(USER, user);
            }

            // Using the pre-existing loginUser; setup the publicKey/password so can login as expected

            // *Always* change the password (unless dontCreateUser was specified)
            statements.add(new ReplaceShadowPasswordEntry(Sha512Crypt.function(), user, passwordToSet));
            createdUserCreds = LoginCredentials.builder().user(user).password(passwordToSet).build();

            if (useKey) {
                statements.add(new AuthorizeRSAPublicKeys("~" + user + "/.ssh",
                        ImmutableList.of(credential.getPublicKeyData())));
                if (Strings.isNonBlank(credential.getPrivateKeyData())) {
                    createdUserCreds = LoginCredentials.builder().user(user)
                            .privateKey(credential.getPrivateKeyData()).build();
                }
            }

            if (!useKey || Boolean.FALSE.equals(config.get(DISABLE_ROOT_AND_PASSWORD_SSH))) {
                // ensure password is permitted for ssh
                statements.add(org.jclouds.scriptbuilder.statements.ssh.SshStatements
                        .sshdConfig(ImmutableMap.of("PasswordAuthentication", "yes")));
                if (user.equals(ROOT_USERNAME)) {
                    statements.add(org.jclouds.scriptbuilder.statements.ssh.SshStatements
                            .sshdConfig(ImmutableMap.of("PermitRootLogin", "yes")));
                }
            }

        } else {
            String pubKey = credential.getPublicKeyData();
            String privKey = credential.getPrivateKeyData();

            if (credential.isEmpty()) {
                /*
                 * TODO have an explicit `create_new_key_per_machine` config key.
                 * error if privateKeyData is set in this case.
                 * publicKeyData automatically added to EXTRA_SSH_KEY_URLS_TO_AUTH.
                 *
                 * if this config key is not set, use a key `brooklyn_id_rsa` and `.pub` in `MGMT_BASE_DIR`,
                 * with permission 0600, creating it if necessary, and logging the fact that this was created.
                 */
                if (!config.containsKey(PRIVATE_KEY_FILE) && loggedSshKeysHint.compareAndSet(false, true)) {
                    LOG.info("Default SSH keys not found or not usable; will create new keys for each machine. "
                            + "Create ~/.ssh/id_rsa or " + "set " + PRIVATE_KEY_FILE.getName() + " / "
                            + PRIVATE_KEY_PASSPHRASE.getName() + " / " + PASSWORD.getName() + " "
                            + "as appropriate for this location if you wish to be able to log in without Brooklyn.");
                }
                KeyPair newKeyPair = SecureKeys.newKeyPair();
                pubKey = SecureKeys.toPub(newKeyPair);
                privKey = SecureKeys.toPem(newKeyPair);
                LOG.debug(
                        "Brooklyn key being created for " + user + " at new machine " + this + " is:\n" + privKey);
            }
            // ensure credential is not used any more, as we have extracted all useful info
            credential = null;

            // Create the user
            // note AdminAccess requires _all_ fields set, due to http://code.google.com/p/jclouds/issues/detail?id=1095
            AdminAccess.Builder adminBuilder = AdminAccess.builder().adminUsername(user)
                    .grantSudoToAdminUser(grantUserSudo);
            adminBuilder.cryptFunction(Sha512Crypt.function());

            boolean useKey = Strings.isNonBlank(pubKey);
            adminBuilder.cryptFunction(Sha512Crypt.function());

            // always set this password; if not supplied, it will be a random string
            adminBuilder.adminPassword(passwordToSet);
            // log the password also, in case we need it
            LOG.debug("Password '" + passwordToSet + "' being created for user '" + user
                    + "' at the machine we are about to provision in " + this + "; "
                    + (useKey ? "however a key will be used to access it" : "this will be the only way to log in"));

            if (grantUserSudo && config.get(JcloudsLocationConfig.DISABLE_ROOT_AND_PASSWORD_SSH)) {
                // the default - set root password which we forget, because we have sudo acct
                // (and lock out root and passwords from ssh)
                adminBuilder.resetLoginPassword(true);
                adminBuilder.loginPassword(Identifiers.makeRandomId(12));
            } else {
                adminBuilder.resetLoginPassword(false);
                adminBuilder.loginPassword(Identifiers.makeRandomId(12) + "-ignored");
            }

            if (useKey) {
                adminBuilder.authorizeAdminPublicKey(true).adminPublicKey(pubKey);
            } else {
                adminBuilder.authorizeAdminPublicKey(false)
                        .adminPublicKey(Identifiers.makeRandomId(12) + "-ignored");
            }

            // jclouds wants us to give it the private key, otherwise it might refuse to authorize the public key
            // (in AdminAccess.build, if adminUsername != null && adminPassword != null);
            // we don't want to give it the private key, but we *do* want the public key authorized;
            // this code seems to trigger that.
            // (we build the creds below)
            adminBuilder.installAdminPrivateKey(false).adminPrivateKey(Identifiers.makeRandomId(12) + "-ignored");

            // lock SSH means no root login and no passwordless login
            // if we're using a password or we don't have sudo, then don't do this!
            adminBuilder.lockSsh(
                    useKey && grantUserSudo && config.get(JcloudsLocationConfig.DISABLE_ROOT_AND_PASSWORD_SSH));

            statements.add(adminBuilder.build());

            if (useKey) {
                createdUserCreds = LoginCredentials.builder().user(user).privateKey(privKey).build();
            } else if (passwordToSet != null) {
                createdUserCreds = LoginCredentials.builder().user(user).password(passwordToSet).build();
            }

            if (!useKey || Boolean.FALSE.equals(config.get(DISABLE_ROOT_AND_PASSWORD_SSH))) {
                // ensure password is permitted for ssh
                statements.add(org.jclouds.scriptbuilder.statements.ssh.SshStatements
                        .sshdConfig(ImmutableMap.of("PasswordAuthentication", "yes")));
            }
        }

        String customTemplateOptionsScript = config.get(CUSTOM_TEMPLATE_OPTIONS_SCRIPT_CONTENTS);
        if (Strings.isNonBlank(customTemplateOptionsScript)) {
            statements.add(new LiteralStatement(customTemplateOptionsScript));
        }

        LOG.debug("Machine we are about to create in " + this + " will be customized with: " + statements);

        return new UserCreation(createdUserCreds, statements);
    }

    // ----------------- registering existing machines ------------------------

    /**
     * @deprecated since 0.8.0 use {@link #registerMachine(NodeMetadata)} instead.
     */
    @Deprecated
    public JcloudsSshMachineLocation rebindMachine(NodeMetadata metadata) throws NoMachinesAvailableException {
        return (JcloudsSshMachineLocation) registerMachine(metadata);
    }

    protected MachineLocation registerMachine(NodeMetadata metadata) throws NoMachinesAvailableException {
        return registerMachine(MutableMap.of(), metadata);
    }

    /**
     * @deprecated since 0.8.0 use {@link #registerMachine(Map, NodeMetadata)} instead.
     */
    @Deprecated
    public JcloudsSshMachineLocation rebindMachine(Map<?, ?> flags, NodeMetadata metadata)
            throws NoMachinesAvailableException {
        return (JcloudsSshMachineLocation) registerMachine(flags, metadata);
    }

    protected MachineLocation registerMachine(Map<?, ?> flags, NodeMetadata metadata)
            throws NoMachinesAvailableException {
        ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
        if (!setup.containsKey("id"))
            setup.putStringKey("id", metadata.getId());
        setHostnameUpdatingCredentials(setup, metadata);
        return registerMachine(setup);
    }

    /**
     * Brings an existing machine with the given details under management.
     * <p>
     * This method will throw an exception if used to reconnect to a Windows VM.
     * @deprecated since 0.8.0 use {@link #registerMachine(ConfigBag)} instead.
     */
    @Deprecated
    public JcloudsSshMachineLocation rebindMachine(ConfigBag setup) throws NoMachinesAvailableException {
        return (JcloudsSshMachineLocation) registerMachine(setup);
    }

    /**
     * Brings an existing machine with the given details under management.
     * <p>
     * Required fields are:
     * <ul>
     *   <li>id: the jclouds VM id, e.g. "eu-west-1/i-5504f21d" (NB this is {@see JcloudsMachineLocation#getJcloudsId()} not #getId())
     *   <li>hostname: the public hostname or IP of the machine, e.g. "ec2-176-34-93-58.eu-west-1.compute.amazonaws.com"
     *   <li>userName: the username for sshing into the machine (for use if it is not a Windows system)
     * <ul>
     */
    public MachineLocation registerMachine(ConfigBag setup) throws NoMachinesAvailableException {
        NodeMetadata node = findNodeOrThrow(setup);
        return registerMachineLocation(setup, node);
    }

    protected MachineLocation registerMachineLocation(ConfigBag setup, NodeMetadata node) {
        ComputeService computeService = getComputeService(setup);
        if (isWindows(node, setup)) {
            return registerWinRmMachineLocation(computeService, node, null, Optional.<HostAndPort>absent(), setup);
        } else {
            try {
                return registerJcloudsSshMachineLocation(computeService, node, Optional.<Template>absent(), null,
                        Optional.<HostAndPort>absent(), setup);
            } catch (IOException e) {
                throw Exceptions.propagate(e);
            }
        }
    }

    /**
     * Finds a node matching the properties given in config or throws an exception.
     * @param config
     * @return
     */
    protected NodeMetadata findNodeOrThrow(ConfigBag config) {
        if (config.getDescription() == null) {
            setCreationString(config);
        }
        String user = checkNotNull(getUser(config), "user");
        String rawId = (String) config.getStringKey("id");
        String rawHostname = (String) config.getStringKey("hostname");
        Predicate<ComputeMetadata> predicate = getRebindToMachinePredicate(config);
        LOG.debug("Finding VM {} ({}@{}), in jclouds location for provider {} matching {}",
                new Object[] { rawId != null ? rawId : "<lookup>", user,
                        rawHostname != null ? rawHostname : "<unspecified>", getProvider(), predicate });
        ComputeService computeService = getComputeService(config);
        Set<? extends NodeMetadata> candidateNodes = computeService.listNodesDetailsMatching(predicate);
        if (candidateNodes.isEmpty()) {
            throw new IllegalArgumentException("Jclouds node not found for rebind with predicate " + predicate);
        } else if (candidateNodes.size() > 1) {
            throw new IllegalArgumentException(
                    "Jclouds node for rebind matched multiple with " + predicate + ": " + candidateNodes);
        }
        NodeMetadata node = Iterables.getOnlyElement(candidateNodes);

        String pkd = LocationConfigUtils.getOsCredential(config).checkNoErrors().logAnyWarnings()
                .getPrivateKeyData();
        if (Strings.isNonBlank(pkd)) {
            LoginCredentials expectedCredentials = LoginCredentials.fromCredentials(new Credentials(user, pkd));
            //override credentials
            node = NodeMetadataBuilder.fromNodeMetadata(node).credentials(expectedCredentials).build();
        }

        return node;
    }

    /**
     * @deprecated since 0.8.0 use {@link #registerMachine(Map)} instead.
     */
    @Deprecated
    public JcloudsSshMachineLocation rebindMachine(Map<?, ?> flags) throws NoMachinesAvailableException {
        return (JcloudsSshMachineLocation) registerMachine(flags);
    }

    public MachineLocation registerMachine(Map<?, ?> flags) throws NoMachinesAvailableException {
        ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
        return registerMachine(setup);
    }

    /**
     * @return a predicate that returns true if a {@link ComputeMetadata} instance is suitable for
     *      rebinding to given the configuration in {@link ConfigBag config}.
     */
    protected Predicate<ComputeMetadata> getRebindToMachinePredicate(ConfigBag config) {
        return new RebindToMachinePredicate(config);
    }

    /**
     * Determines whether a machine may be rebinded to by comparing the given id, hostname and region
     * against the node's id, hostname, provider id and public addresses.
     */
    private static class RebindToMachinePredicate implements Predicate<ComputeMetadata> {

        final String rawId;
        final String rawHostname;
        final String rawRegion;

        public RebindToMachinePredicate(ConfigBag config) {
            rawId = (String) config.getStringKey("id");
            rawHostname = (String) config.getStringKey("hostname");
            rawRegion = (String) config.getStringKey("region");
        }

        @Override
        public boolean apply(ComputeMetadata input) {
            // ID exact match
            if (rawId != null) {
                // Second is AWS format
                if (rawId.equals(input.getId())
                        || rawRegion != null && (rawRegion + "/" + rawId).equals(input.getId())) {
                    return true;
                }
            }

            // else do node metadata lookup
            if (input instanceof NodeMetadata) {
                NodeMetadata node = NodeMetadata.class.cast(input);
                if (rawHostname != null && rawHostname.equalsIgnoreCase(node.getHostname()))
                    return true;
                if (rawHostname != null && node.getPublicAddresses().contains(rawHostname))
                    return true;
                if (rawId != null && rawId.equalsIgnoreCase(node.getHostname()))
                    return true;
                if (rawId != null && node.getPublicAddresses().contains(rawId))
                    return true;
                // don't do private IPs because they might be repeated
                if (rawId != null && rawId.equalsIgnoreCase(node.getProviderId()))
                    return true;
                if (rawHostname != null && rawHostname.equalsIgnoreCase(node.getProviderId()))
                    return true;
            }

            return false;
        }

        @Override
        public String toString() {
            return Objects.toStringHelper(this).omitNullValues().add("id", rawId).add("hostname", rawHostname)
                    .add("region", rawRegion).toString();
        }
    }

    // -------------- create the SshMachineLocation instance, and connect to it etc ------------------------

    /** @deprecated since 0.7.0 use {@link #registerJcloudsSshMachineLocation(ComputeService, NodeMetadata, LoginCredentials, Optional, ConfigBag)} */
    @Deprecated
    protected final JcloudsSshMachineLocation registerJcloudsSshMachineLocation(NodeMetadata node,
            String vmHostname, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) throws IOException {
        LOG.warn("Using deprecated registerJcloudsSshMachineLocation: now wants computeService passed",
                new Throwable("source of deprecated registerJcloudsSshMachineLocation invocation"));
        return registerJcloudsSshMachineLocation(null, node, Optional.<Template>absent(), null, sshHostAndPort,
                setup);
    }

    /**
     * @deprecated since 0.9.0; see {@link #registerJcloudsSshMachineLocation(ComputeService, NodeMetadata, Optional, LoginCredentials, Optional, ConfigBag)}.
     *             Marked as final to warn those trying to sub-class. 
     */
    @Deprecated
    protected final JcloudsSshMachineLocation registerJcloudsSshMachineLocation(ComputeService computeService,
            NodeMetadata node, LoginCredentials userCredentials, Optional<HostAndPort> sshHostAndPort,
            ConfigBag setup) throws IOException {
        return registerJcloudsSshMachineLocation(computeService, node, Optional.<Template>absent(), userCredentials,
                sshHostAndPort, setup);
    }

    protected JcloudsSshMachineLocation registerJcloudsSshMachineLocation(ComputeService computeService,
            NodeMetadata node, Optional<Template> template, LoginCredentials userCredentials,
            Optional<HostAndPort> sshHostAndPort, ConfigBag setup) throws IOException {
        if (userCredentials == null)
            userCredentials = node.getCredentials();

        String vmHostname = getPublicHostname(node, sshHostAndPort, setup);

        JcloudsSshMachineLocation machine = createJcloudsSshMachineLocation(computeService, node, template,
                vmHostname, sshHostAndPort, userCredentials, setup);
        registerJcloudsMachineLocation(node.getId(), machine);
        return machine;
    }

    @VisibleForTesting
    protected void registerJcloudsMachineLocation(String nodeId, JcloudsMachineLocation machine) {
        machine.setParent(this);
        vmInstanceIds.put(machine, nodeId);
    }

    /**
     * @deprecated since 0.9.0; see {@link #createJcloudsSshMachineLocation(ComputeService, NodeMetadata, Optional, String, Optional, LoginCredentials, ConfigBag)}.
     *             Marked as final to warn those trying to sub-class. 
     */
    @Deprecated
    protected final JcloudsSshMachineLocation createJcloudsSshMachineLocation(ComputeService computeService,
            NodeMetadata node, String vmHostname, Optional<HostAndPort> sshHostAndPort,
            LoginCredentials userCredentials, ConfigBag setup) throws IOException {
        return createJcloudsSshMachineLocation(computeService, node, Optional.<Template>absent(), vmHostname,
                sshHostAndPort, userCredentials, setup);
    }

    protected JcloudsSshMachineLocation createJcloudsSshMachineLocation(ComputeService computeService,
            NodeMetadata node, Optional<Template> template, String vmHostname, Optional<HostAndPort> sshHostAndPort,
            LoginCredentials userCredentials, ConfigBag setup) throws IOException {
        Map<?, ?> sshConfig = extractSshConfig(setup, node);
        String nodeAvailabilityZone = extractAvailabilityZone(setup, node);
        String nodeRegion = extractRegion(setup, node);
        if (nodeRegion == null) {
            // e.g. rackspace doesn't have "region", so rackspace-uk is best we can say (but zone="LON")
            nodeRegion = extractProvider(setup, node);
        }

        String address = sshHostAndPort.isPresent() ? sshHostAndPort.get().getHostText() : vmHostname;
        try {
            Networking.getInetAddressWithFixedName(address);
            // fine, it resolves
        } catch (Exception e) {
            // occurs if an unresolvable hostname is given as vmHostname, and the machine only has private IP addresses but they are reachable
            // TODO cleanup use of getPublicHostname so its semantics are clearer, returning reachable hostname or ip, and
            // do this check/fix there instead of here!
            Exceptions.propagateIfFatal(e);
            LOG.debug("Could not resolve reported address '" + address + "' for " + vmHostname + " ("
                    + setup.getDescription() + "/" + node + "), requesting reachable address");
            if (computeService == null)
                throw Exceptions.propagate(e);
            // this has sometimes already been done in waitForReachable (unless skipped) but easy enough to do again
            address = getFirstReachableAddress(node, setup);
        }

        if (LOG.isDebugEnabled())
            LOG.debug("creating JcloudsSshMachineLocation representation for {}@{} ({}/{}) for {}/{}",
                    new Object[] { getUser(setup), address, Sanitizer.sanitize(sshConfig), sshHostAndPort,
                            setup.getDescription(), node });

        if (isManaged()) {
            return getManagementContext().getLocationManager().createLocation(LocationSpec
                    .create(JcloudsSshMachineLocation.class).configure("displayName", vmHostname)
                    .configure("address", address)
                    .configure(JcloudsSshMachineLocation.SSH_PORT,
                            sshHostAndPort.isPresent() ? sshHostAndPort.get().getPort() : node.getLoginPort())
                    // don't think "config" does anything
                    .configure(sshConfig)
                    // FIXME remove "config" -- inserted directly, above
                    .configure("config", sshConfig).configure("user", userCredentials.getUser())
                    .configure(SshMachineLocation.PASSWORD, userCredentials.getOptionalPassword().orNull())
                    .configure(SshMachineLocation.PRIVATE_KEY_DATA,
                            userCredentials.getOptionalPrivateKey().orNull())
                    .configure("jcloudsParent", this).configure("node", node)
                    .configure("template", template.orNull())
                    .configureIfNotNull(CLOUD_AVAILABILITY_ZONE_ID, nodeAvailabilityZone)
                    .configureIfNotNull(CLOUD_REGION_ID, nodeRegion)
                    .configure(CALLER_CONTEXT, setup.get(CALLER_CONTEXT))
                    .configure(SshMachineLocation.DETECT_MACHINE_DETAILS,
                            setup.get(SshMachineLocation.DETECT_MACHINE_DETAILS))
                    .configureIfNotNull(SshMachineLocation.SCRIPT_DIR, setup.get(SshMachineLocation.SCRIPT_DIR))
                    .configureIfNotNull(USE_PORT_FORWARDING, setup.get(USE_PORT_FORWARDING))
                    .configureIfNotNull(PORT_FORWARDER, setup.get(PORT_FORWARDER))
                    .configureIfNotNull(PORT_FORWARDING_MANAGER, setup.get(PORT_FORWARDING_MANAGER)));
        } else {
            LOG.warn("Using deprecated JcloudsSshMachineLocation constructor because " + this + " is not managed");
            return new JcloudsSshMachineLocation(MutableMap.builder().put("displayName", vmHostname)
                    .put("address", address)
                    .put("port", sshHostAndPort.isPresent() ? sshHostAndPort.get().getPort() : node.getLoginPort())
                    // don't think "config" does anything
                    .putAll(sshConfig)
                    // FIXME remove "config" -- inserted directly, above
                    .put("config", sshConfig).put("user", userCredentials.getUser())
                    .putIfNotNull(SshMachineLocation.PASSWORD.getName(),
                            userCredentials.getOptionalPassword().orNull())
                    .putIfNotNull(SshMachineLocation.PRIVATE_KEY_DATA.getName(),
                            userCredentials.getOptionalPrivateKey().orNull())
                    .put("callerContext", setup.get(CALLER_CONTEXT))
                    .putIfNotNull(CLOUD_AVAILABILITY_ZONE_ID.getName(), nodeAvailabilityZone)
                    .putIfNotNull(CLOUD_REGION_ID.getName(), nodeRegion)
                    .put(USE_PORT_FORWARDING, setup.get(USE_PORT_FORWARDING))
                    .put(PORT_FORWARDER, setup.get(PORT_FORWARDER))
                    .put(PORT_FORWARDING_MANAGER, setup.get(PORT_FORWARDING_MANAGER)).build(), this, node);
        }
    }

    protected JcloudsWinRmMachineLocation registerWinRmMachineLocation(ComputeService computeService,
            NodeMetadata node, LoginCredentials initialCredentials, Optional<HostAndPort> sshHostAndPort,
            ConfigBag setup) {
        if (initialCredentials == null)
            initialCredentials = node.getCredentials();

        String vmHostname = getPublicHostname(node, sshHostAndPort, setup);

        JcloudsWinRmMachineLocation machine = createWinRmMachineLocation(computeService, node, vmHostname,
                sshHostAndPort, setup);
        registerJcloudsMachineLocation(node.getId(), machine);
        return machine;
    }

    protected JcloudsWinRmMachineLocation createWinRmMachineLocation(ComputeService computeService,
            NodeMetadata node, String vmHostname, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) {
        String nodeAvailabilityZone = extractAvailabilityZone(setup, node);
        String nodeRegion = extractRegion(setup, node);
        if (nodeRegion == null) {
            // e.g. rackspace doesn't have "region", so rackspace-uk is best we can say (but zone="LON")
            nodeRegion = extractProvider(setup, node);
        }

        String address = sshHostAndPort.isPresent() ? sshHostAndPort.get().getHostText() : vmHostname;

        if (isManaged()) {
            return getManagementContext().getLocationManager().createLocation(LocationSpec
                    .create(JcloudsWinRmMachineLocation.class).configure("jcloudsParent", this)
                    .configure("displayName", vmHostname).configure("address", address)
                    .configure(WinRmMachineLocation.WINRM_PORT,
                            sshHostAndPort.isPresent() ? sshHostAndPort.get().getPort() : node.getLoginPort())
                    .configure("user", getUser(setup)).configure(WinRmMachineLocation.USER, setup.get(USER))
                    .configure(WinRmMachineLocation.PASSWORD, setup.get(PASSWORD)).configure("node", node)
                    .configureIfNotNull(CLOUD_AVAILABILITY_ZONE_ID, nodeAvailabilityZone)
                    .configureIfNotNull(CLOUD_REGION_ID, nodeRegion)
                    .configure(CALLER_CONTEXT, setup.get(CALLER_CONTEXT))
                    .configure(SshMachineLocation.DETECT_MACHINE_DETAILS,
                            setup.get(SshMachineLocation.DETECT_MACHINE_DETAILS))
                    .configureIfNotNull(SshMachineLocation.SCRIPT_DIR, setup.get(SshMachineLocation.SCRIPT_DIR))
                    .configureIfNotNull(USE_PORT_FORWARDING, setup.get(USE_PORT_FORWARDING))
                    .configureIfNotNull(PORT_FORWARDER, setup.get(PORT_FORWARDER))
                    .configureIfNotNull(PORT_FORWARDING_MANAGER, setup.get(PORT_FORWARDING_MANAGER)));
        } else {
            throw new UnsupportedOperationException(
                    "Cannot create WinRmMachineLocation because " + this + " is not managed");
        }
    }

    // -------------- give back the machines------------------

    protected Map<String, Object> extractSshConfig(ConfigBag setup, NodeMetadata node) {
        ConfigBag nodeConfig = new ConfigBag();
        if (node != null && node.getCredentials() != null) {
            nodeConfig.putIfNotNull(PASSWORD, node.getCredentials().getOptionalPassword().orNull());
            nodeConfig.putIfNotNull(PRIVATE_KEY_DATA, node.getCredentials().getOptionalPrivateKey().orNull());
        }
        return extractSshConfig(setup, nodeConfig).getAllConfig();
    }

    protected String extractAvailabilityZone(ConfigBag setup, NodeMetadata node) {
        return extractNodeLocationId(setup, node, LocationScope.ZONE);
    }

    protected String extractRegion(ConfigBag setup, NodeMetadata node) {
        return extractNodeLocationId(setup, node, LocationScope.REGION);
    }

    protected String extractProvider(ConfigBag setup, NodeMetadata node) {
        return extractNodeLocationId(setup, node, LocationScope.PROVIDER);
    }

    protected String extractNodeLocationId(ConfigBag setup, NodeMetadata node, LocationScope scope) {
        org.jclouds.domain.Location nodeLoc = node.getLocation();
        if (nodeLoc == null)
            return null;
        do {
            if (nodeLoc.getScope() == scope)
                return nodeLoc.getId();
            nodeLoc = nodeLoc.getParent();
        } while (nodeLoc != null);
        return null;
    }

    @Override
    public void release(MachineLocation rawMachine) {
        String instanceId = vmInstanceIds.remove(rawMachine);
        if (instanceId == null) {
            LOG.info("Attempted release of unknown machine " + rawMachine + " in " + toString());
            throw new IllegalArgumentException("Unknown machine " + rawMachine);
        }
        JcloudsMachineLocation machine = (JcloudsMachineLocation) rawMachine;

        LOG.info("Releasing machine {} in {}, instance id {}", new Object[] { machine, this, instanceId });

        Exception tothrow = null;

        ConfigBag setup = config().getBag();
        for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
            try {
                customizer.preRelease(machine);
            } catch (Exception e) {
                LOG.error("Problem invoking pre-release customizer " + customizer + " for machine " + machine
                        + " in " + this + ", instance id " + instanceId + "; ignoring and continuing, "
                        + (tothrow == null ? "will throw subsequently" : "swallowing due to previous error") + ": "
                        + e, e);
                if (tothrow == null)
                    tothrow = e;
            }
        }
        for (MachineLocationCustomizer customizer : getMachineCustomizers(setup)) {
            customizer.preRelease(machine);
        }

        try {
            // FIXME: Needs to release port forwarding for WinRmMachineLocations
            if (machine instanceof JcloudsMachineLocation) {
                releasePortForwarding((JcloudsMachineLocation) machine);
            }
        } catch (Exception e) {
            LOG.error("Problem releasing port-forwarding for machine " + machine + " in " + this + ", instance id "
                    + instanceId + "; ignoring and continuing, "
                    + (tothrow == null ? "will throw subsequently" : "swallowing due to previous error") + ": " + e,
                    e);
            if (tothrow == null)
                tothrow = e;
        }

        try {
            releaseNode(instanceId);
        } catch (Exception e) {
            LOG.error("Problem releasing machine " + machine + " in " + this + ", instance id " + instanceId
                    + "; ignoring and continuing, "
                    + (tothrow == null ? "will throw subsequently" : "swallowing due to previous error") + ": " + e,
                    e);
            if (tothrow == null)
                tothrow = e;
        }

        removeChild(machine);

        for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
            try {
                customizer.postRelease(machine);
            } catch (Exception e) {
                LOG.error("Problem invoking pre-release customizer " + customizer + " for machine " + machine
                        + " in " + this + ", instance id " + instanceId + "; ignoring and continuing, "
                        + (tothrow == null ? "will throw subsequently" : "swallowing due to previous error") + ": "
                        + e, e);
                if (tothrow == null)
                    tothrow = e;
            }
        }

        if (tothrow != null) {
            throw Exceptions.propagate(tothrow);
        }
    }

    protected void releaseSafely(MachineLocation machine) {
        try {
            release(machine);
        } catch (Exception e) {
            // rely on exception having been logged by #release(SshMachineLocation), so no-op
        }
    }

    protected void releaseNodeSafely(NodeMetadata node) {
        String instanceId = node.getId();
        LOG.info("Releasing node {} in {}, instance id {}", new Object[] { node, this, instanceId });

        try {
            releaseNode(instanceId);
        } catch (Exception e) {
            LOG.warn("Problem releasing node " + node + " in " + this + ", instance id " + instanceId
                    + "; discarding instance and continuing...", e);
        }
    }

    protected void releaseNode(String instanceId) {
        ComputeService computeService = null;
        try {
            computeService = getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(config().getBag(), true);
            computeService.destroyNode(instanceId);
        } finally {
            /*
                // we don't close the compute service; this means if we provision add'l it is fast;
                // however it also means an explicit System.exit may be needed for termination
                if (computeService != null) {
            try {
                computeService.getContext().close();
            } catch (Exception e) {
                LOG.error "Problem closing compute-service's context; continuing...", e
            }
                }
             */
        }
    }

    protected void releasePortForwarding(final JcloudsMachineLocation machine) {
        // TODO Implementation needs revisisted. It relies on deprecated PortForwardManager methods.

        boolean usePortForwarding = Boolean.TRUE.equals(machine.getConfig(USE_PORT_FORWARDING));
        final JcloudsPortForwarderExtension portForwarder = machine.getConfig(PORT_FORWARDER);
        PortForwardManager portForwardManager = machine.getConfig(PORT_FORWARDING_MANAGER);
        final String nodeId = machine.getJcloudsId();
        final Map<String, Runnable> subtasks = Maps.newLinkedHashMap();

        if (portForwarder == null) {
            LOG.debug("No port-forwarding to close (because portForwarder null) on release of " + machine);
        } else {
            final Optional<NodeMetadata> node = machine.getOptionalNode();
            // Release the port-forwarding for the login-port, which was explicitly created by JcloudsLocation
            if (usePortForwarding && node.isPresent()) {
                final HostAndPort hostAndPortOverride;
                if (machine instanceof SshMachineLocation) {
                    hostAndPortOverride = ((SshMachineLocation) machine).getSshHostAndPort();
                } else if (machine instanceof WinRmMachineLocation) {
                    String host = ((WinRmMachineLocation) machine).getAddress().getHostAddress();
                    int port = ((WinRmMachineLocation) machine).config().get(WinRmMachineLocation.WINRM_PORT);
                    hostAndPortOverride = HostAndPort.fromParts(host, port);
                } else {
                    LOG.warn("Unexpected machine {} of type {}; expected SSH or WinRM", machine,
                            (machine != null ? machine.getClass() : null));
                    hostAndPortOverride = null;
                }
                if (hostAndPortOverride != null) {
                    final int loginPort = node.get().getLoginPort();
                    subtasks.put("Close port-forward " + hostAndPortOverride + "->" + loginPort, new Runnable() {
                        public void run() {
                            LOG.debug("Closing port-forwarding at {} for machine {}: {}->{}",
                                    new Object[] { this, machine, hostAndPortOverride, loginPort });
                            portForwarder.closePortForwarding(node.get(), loginPort, hostAndPortOverride,
                                    Protocol.TCP);
                        }
                    });
                }
            }

            // Get all the other port-forwarding mappings for this VM, and release all of those
            Set<PortMapping> mappings;
            if (portForwardManager != null) {
                mappings = Sets.newLinkedHashSet();
                mappings.addAll(portForwardManager.getLocationPublicIpIds(machine));
                if (nodeId != null) {
                    mappings.addAll(portForwardManager.getPortMappingWithPublicIpId(nodeId));
                }
            } else {
                mappings = ImmutableSet.of();
            }

            for (final PortMapping mapping : mappings) {
                final HostAndPort publicEndpoint = mapping.getPublicEndpoint();
                final int targetPort = mapping.getPrivatePort();
                final Protocol protocol = Protocol.TCP;
                if (publicEndpoint != null && node.isPresent()) {
                    subtasks.put("Close port-forward " + publicEndpoint + "->" + targetPort, new Runnable() {
                        public void run() {
                            LOG.debug("Closing port-forwarding at {} for machine {}: {}->{}",
                                    new Object[] { this, machine, publicEndpoint, targetPort });
                            portForwarder.closePortForwarding(node.get(), targetPort, publicEndpoint, protocol);
                        }
                    });
                }
            }

            if (subtasks.size() > 0) {
                final TaskBuilder<Void> builder = TaskBuilder.<Void>builder().parallel(true)
                        .displayName("close port-forwarding at " + machine);
                for (Map.Entry<String, Runnable> entry : subtasks.entrySet()) {
                    builder.add(TaskBuilder.builder().displayName(entry.getKey()).body(entry.getValue()).build());
                }
                final Task<Void> task = builder.build();
                final DynamicTasks.TaskQueueingResult<Void> queueResult = DynamicTasks.queueIfPossible(task);
                if (queueResult.isQueuedOrSubmitted()) {
                    final String origDetails = Tasks
                            .setBlockingDetails("waiting for closing port-forwarding of " + machine);
                    try {
                        task.blockUntilEnded();
                    } finally {
                        Tasks.setBlockingDetails(origDetails);
                    }
                } else {
                    LOG.warn("Releasing port-forwarding of " + machine + " not executing in execution-context "
                            + "(e.g. not invoked inside effector); falling back to executing sequentially");
                    for (Runnable subtask : subtasks.values()) {
                        subtask.run();
                    }
                }
            }
        }

        // Forget all port mappings associated with this VM
        if (portForwardManager != null) {
            portForwardManager.forgetPortMappings(machine);
            if (nodeId != null) {
                portForwardManager.forgetPortMappings(nodeId);
            }
        }
    }

    // ------------ support methods --------------------

    /**
     * Extracts the user that jclouds tells us about (i.e. from the jclouds node).
     */
    protected LoginCredentials extractVmCredentials(ConfigBag setup, NodeMetadata node,
            LoginCredentials nodeCredentials) {
        String user = getUser(setup);
        OsCredential localCredentials = LocationConfigUtils.getOsCredential(setup).checkNoErrors();

        LOG.debug("Credentials extracted for {}: {}/{} with {}/{}",
                new Object[] { node, user, nodeCredentials.getUser(), localCredentials, nodeCredentials });

        if (Strings.isNonBlank(nodeCredentials.getUser())) {
            if (Strings.isBlank(user)) {
                setup.put(USER, user = nodeCredentials.getUser());
            } else if (ROOT_USERNAME.equals(user) && ROOT_ALIASES.contains(nodeCredentials.getUser())) {
                // deprecated, we used to default username to 'root'; now we leave null, then use autodetected credentials if no user specified
                LOG.warn("overriding username 'root' in favour of '" + nodeCredentials.getUser()
                        + "' at {}; this behaviour may be removed in future", node);
                setup.put(USER, user = nodeCredentials.getUser());
            }

            String pkd = Strings.maybeNonBlank(localCredentials.getPrivateKeyData())
                    .or(nodeCredentials.getOptionalPrivateKey().orNull());
            String pwd = Strings.maybeNonBlank(localCredentials.getPassword())
                    .or(nodeCredentials.getOptionalPassword().orNull());
            if (Strings.isBlank(user) || (Strings.isBlank(pkd) && pwd == null)) {
                String missing = (user == null ? "user" : "credential");
                LOG.warn("Not able to determine " + missing + " for " + this + " at " + node
                        + "; will likely fail subsequently");
                return null;
            } else {
                LoginCredentials.Builder resultBuilder = LoginCredentials.builder().user(user);
                if (pwd != null && (Strings.isBlank(pkd) || localCredentials.isUsingPassword()))
                    resultBuilder.password(pwd);
                else // pkd guaranteed non-blank due to above
                    resultBuilder.privateKey(pkd);
                return resultBuilder.build();
            }
        }

        LOG.warn("No node-credentials or admin-access available for node " + node + " in " + this
                + "; will likely fail subsequently");
        return null;
    }

    protected String getFirstReachableAddress(NodeMetadata node, ConfigBag setup) {
        String pollForFirstReachable = setup.get(POLL_FOR_FIRST_REACHABLE_ADDRESS);

        boolean enabled = !"false".equalsIgnoreCase(pollForFirstReachable);
        String result;
        if (enabled) {
            Duration timeout = "true".equals(pollForFirstReachable) ? Duration.FIVE_MINUTES
                    : Duration.of(pollForFirstReachable);
            result = JcloudsUtil.getFirstReachableAddress(node, timeout);
            LOG.debug("Using first-reachable address " + result + " for node " + node + " in " + this);
        } else {
            result = Iterables.getFirst(Iterables.concat(node.getPublicAddresses(), node.getPrivateAddresses()),
                    null);
            if (result == null) {
                throw new IllegalStateException("No addresses available for node " + node + " in " + this);
            }
            LOG.debug("Using first address " + result + " for node " + node + " in " + this);
        }
        return result;
    }

    protected LoginCredentials waitForWinRmAvailable(final ComputeService computeService, final NodeMetadata node,
            Optional<HostAndPort> hostAndPortOverride, ConfigBag setup) {
        return waitForWinRmAvailable(computeService, node, hostAndPortOverride,
                ImmutableList.of(node.getCredentials()), setup);
    }

    protected LoginCredentials waitForWinRmAvailable(final ComputeService computeService, final NodeMetadata node,
            Optional<HostAndPort> hostAndPortOverride, List<LoginCredentials> credentialsToTry, ConfigBag setup) {
        String waitForWinrmAvailable = setup.get(WAIT_FOR_WINRM_AVAILABLE);
        checkArgument(!"false".equalsIgnoreCase(waitForWinrmAvailable),
                "waitForWinRmAvailable called despite waitForWinRmAvailable=%s", waitForWinrmAvailable);
        Duration timeout = null;
        try {
            timeout = Duration.parse(waitForWinrmAvailable);
        } catch (Exception e) {
            // TODO will this just be a NumberFormatException? If so, catch that specificially
            // normal if 'true'; just fall back to default
            Exceptions.propagateIfFatal(e);
        }
        if (timeout == null) {
            timeout = Duration.parse(WAIT_FOR_WINRM_AVAILABLE.getDefaultValue());
        }

        Set<String> users = Sets.newLinkedHashSet();
        for (LoginCredentials creds : credentialsToTry) {
            users.add(creds.getUser());
        }
        String user = (users.size() == 1) ? Iterables.getOnlyElement(users)
                : "{" + Joiner.on(",").join(users) + "}";
        String vmIp = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getHostText()
                : getFirstReachableAddress(node, setup);
        if (vmIp == null)
            LOG.warn("Unable to extract IP for " + node + " (" + setup.getDescription()
                    + "): subsequent connection attempt will likely fail");
        int vmPort = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getPortOrDefault(5985) : 5985;

        String connectionDetails = user + "@" + vmIp + ":" + vmPort;
        final HostAndPort hostAndPort = hostAndPortOverride.isPresent() ? hostAndPortOverride.get()
                : HostAndPort.fromParts(vmIp, vmPort);
        final AtomicReference<LoginCredentials> credsSuccessful = new AtomicReference<LoginCredentials>();

        // Don't use config that relates to the final user credentials (those have nothing to do
        // with the initial credentials of the VM returned by the cloud provider).
        // The createTemporaryWinRmMachineLocation deals with removing that.
        ConfigBag winrmProps = ConfigBag.newInstanceCopying(setup);

        final Map<WinRmMachineLocation, LoginCredentials> machinesToTry = Maps.newLinkedHashMap();
        for (LoginCredentials creds : credentialsToTry) {
            machinesToTry.put(createTemporaryWinRmMachineLocation(hostAndPort, creds, winrmProps), creds);
        }
        try {
            Callable<Boolean> checker = new Callable<Boolean>() {
                public Boolean call() {
                    for (Map.Entry<WinRmMachineLocation, LoginCredentials> entry : machinesToTry.entrySet()) {
                        WinRmMachineLocation machine = entry.getKey();
                        WinRmToolResponse response = machine.executeCommand(
                                ImmutableMap.of(WinRmTool.PROP_EXEC_TRIES.getName(), 1),
                                ImmutableList.of("echo testing"));
                        boolean success = (response.getStatusCode() == 0);
                        if (success) {
                            credsSuccessful.set(entry.getValue());
                            return true;
                        }
                    }
                    return false;
                }
            };

            waitForReachable(checker, connectionDetails, credentialsToTry, setup, timeout);
        } finally {
            for (WinRmMachineLocation machine : machinesToTry.keySet()) {
                getManagementContext().getLocationManager().unmanage(machine);
            }
        }

        return credsSuccessful.get();
    }

    protected LoginCredentials waitForSshable(final ComputeService computeService, final NodeMetadata node,
            Optional<HostAndPort> hostAndPortOverride, ConfigBag setup) {
        LoginCredentials nodeCreds = node.getCredentials();
        String nodeUser = nodeCreds.getUser();
        String loginUserOverride = setup.get(LOGIN_USER);
        Set<String> users = MutableSet.of();

        if (Strings.isNonBlank(nodeUser)) {
            users.add(nodeUser);
        }

        if (Strings.isNonBlank(loginUserOverride)) {
            users.add(loginUserOverride);
        }

        // See https://issues.apache.org/jira/browse/BROOKLYN-186
        // Handle where jclouds gives us the wrong login user (!) and both a password + ssh key.
        // Try all the permutations to find the one that works.
        List<LoginCredentials> credentialsToTry = Lists.newArrayList();
        for (String user : users) {
            if (nodeCreds.getOptionalPassword().isPresent() && nodeCreds.getOptionalPrivateKey().isPresent()) {
                credentialsToTry.add(LoginCredentials.builder(nodeCreds).noPassword().user(user).build());
                credentialsToTry.add(LoginCredentials.builder(nodeCreds).noPrivateKey().user(user).build());
            } else {
                credentialsToTry.add(LoginCredentials.builder(nodeCreds).user(user).build());
            }
        }

        return waitForSshable(computeService, node, hostAndPortOverride, credentialsToTry, setup);
    }

    protected LoginCredentials waitForSshable(final ComputeService computeService, final NodeMetadata node,
            Optional<HostAndPort> hostAndPortOverride, List<LoginCredentials> credentialsToTry, ConfigBag setup) {
        String waitForSshable = setup.get(WAIT_FOR_SSHABLE);
        checkArgument(!"false".equalsIgnoreCase(waitForSshable),
                "waitForReachable called despite waitForSshable=%s for %s", waitForSshable, node);
        checkArgument(credentialsToTry.size() > 0, "waitForReachable called without credentials for %s", node);

        Duration timeout = null;
        try {
            timeout = Duration.parse(waitForSshable);
        } catch (Exception e) {
            // normal if 'true'; just fall back to default
        }
        if (timeout == null) {
            timeout = Duration.parse(WAIT_FOR_SSHABLE.getDefaultValue());
        }

        Set<String> users = Sets.newLinkedHashSet();
        for (LoginCredentials creds : credentialsToTry) {
            users.add(creds.getUser());
        }
        String user = (users.size() == 1) ? Iterables.getOnlyElement(users)
                : "{" + Joiner.on(",").join(users) + "}";
        String vmIp = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getHostText()
                : getFirstReachableAddress(node, setup);
        if (vmIp == null)
            LOG.warn("Unable to extract IP for " + node + " (" + setup.getDescription()
                    + "): subsequent connection attempt will likely fail");
        int vmPort = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getPortOrDefault(22) : 22;

        String connectionDetails = user + "@" + vmIp + ":" + vmPort;
        final HostAndPort hostAndPort = hostAndPortOverride.isPresent() ? hostAndPortOverride.get()
                : HostAndPort.fromParts(vmIp, vmPort);
        final AtomicReference<LoginCredentials> credsSuccessful = new AtomicReference<LoginCredentials>();

        // Don't use config that relates to the final user credentials (those have nothing to do 
        // with the initial credentials of the VM returned by the cloud provider).
        ConfigBag sshProps = ConfigBag.newInstanceCopying(setup);
        sshProps.remove("password");
        sshProps.remove("privateKeyData");
        sshProps.remove("privateKeyFile");
        sshProps.remove("privateKeyPassphrase");

        final Map<SshMachineLocation, LoginCredentials> machinesToTry = Maps.newLinkedHashMap();
        for (LoginCredentials creds : credentialsToTry) {
            machinesToTry.put(createTemporarySshMachineLocation(hostAndPort, creds, sshProps), creds);
        }
        try {
            Callable<Boolean> checker = new Callable<Boolean>() {
                public Boolean call() {
                    for (Map.Entry<SshMachineLocation, LoginCredentials> entry : machinesToTry.entrySet()) {
                        SshMachineLocation machine = entry.getKey();
                        int exitstatus = machine
                                .execScript(
                                        ImmutableMap.of(SshTool.PROP_SSH_TRIES_TIMEOUT.getName(),
                                                Duration.THIRTY_SECONDS.toMilliseconds(),
                                                SshTool.PROP_SSH_TRIES.getName(), 1),
                                        "check-connectivity", ImmutableList.of("true"));
                        boolean success = (exitstatus == 0);
                        if (success) {
                            credsSuccessful.set(entry.getValue());
                            return true;
                        }
                    }
                    return false;
                }
            };

            waitForReachable(checker, connectionDetails, credentialsToTry, setup, timeout);
        } finally {
            for (SshMachineLocation machine : machinesToTry.keySet()) {
                getManagementContext().getLocationManager().unmanage(machine);
                Streams.closeQuietly(machine);
            }
        }

        return credsSuccessful.get();
    }

    protected void waitForReachable(Callable<Boolean> checker, String hostAndPort,
            List<LoginCredentials> credentialsToLog, ConfigBag setup, Duration timeout) {
        if (LOG.isDebugEnabled()) {
            List<String> credsToString = Lists.newArrayList();
            for (LoginCredentials creds : credentialsToLog) {
                String user = creds.getUser();
                String password;
                String key;
                if (Boolean.TRUE.equals(setup.get(LOG_CREDENTIALS))) {
                    password = creds.getOptionalPassword().or("<absent>");
                    key = creds.getOptionalPrivateKey().or("<absent>");
                } else {
                    password = creds.getOptionalPassword().isPresent() ? "******" : "<absent>";
                    key = creds.getOptionalPrivateKey().isPresent() ? "******" : "<absent>";
                }
                credsToString.add("user=" + user + ", password=" + password + ", key=" + key);
            }

            LOG.debug(
                    "VM {}: reported online, now waiting {} for it to be contactable on {}; trying {} credential{}: {}",
                    new Object[] { setup.getDescription(), timeout, hostAndPort, credentialsToLog.size(),
                            Strings.s(credentialsToLog.size()), (credsToString.size() == 1) ? credsToString.get(0)
                                    : "(multiple!):" + Joiner.on("\n\t").join(credsToString) });
        }

        Stopwatch stopwatch = Stopwatch.createStarted();

        ReferenceWithError<Boolean> reachable = new Repeater().backoff(Duration.ONE_SECOND, 2, Duration.TEN_SECONDS) // exponential backoff, to 10 seconds
                .until(checker).limitTimeTo(timeout).runKeepingError();

        if (!reachable.getWithoutError()) {
            throw new IllegalStateException("Connection failed for " + hostAndPort + " (" + setup.getDescription()
                    + ") after waiting " + Time.makeTimeStringRounded(timeout), reachable.getError());
        }

        LOG.debug("VM {}: connection succeeded after {} on {}",
                new Object[] { setup.getDescription(), Time.makeTimeStringRounded(stopwatch), hostAndPort });
    }

    // -------------------- hostnames ------------------------
    // hostnames are complicated, but irregardless, this code could be cleaned up!

    protected void setHostnameUpdatingCredentials(ConfigBag setup, NodeMetadata metadata) {
        List<String> usersTried = new ArrayList<String>();

        String originalUser = getUser(setup);
        if (groovyTruth(originalUser)) {
            if (setHostname(setup, metadata, false))
                return;
            usersTried.add(originalUser);
        }

        LoginCredentials credentials = metadata.getCredentials();
        if (credentials != null) {
            if (Strings.isNonBlank(credentials.getUser()))
                setup.put(USER, credentials.getUser());
            if (Strings.isNonBlank(credentials.getOptionalPrivateKey().orNull()))
                setup.put(PRIVATE_KEY_DATA, credentials.getOptionalPrivateKey().orNull());
            if (setHostname(setup, metadata, false)) {
                if (originalUser != null && !originalUser.equals(getUser(setup))) {
                    LOG.warn("Switching to cloud-specified user at " + metadata + " as " + getUser(setup)
                            + " (failed to connect using: " + usersTried + ")");
                }
                return;
            }
            usersTried.add(getUser(setup));
        }

        for (String u : COMMON_USER_NAMES_TO_TRY) {
            setup.put(USER, u);
            if (setHostname(setup, metadata, false)) {
                LOG.warn("Auto-detected user at " + metadata + " as " + getUser(setup)
                        + " (failed to connect using: " + usersTried + ")");
                return;
            }
            usersTried.add(getUser(setup));
        }
        // just repeat, so we throw exception
        LOG.warn("Failed to log in to " + metadata + ", tried as users " + usersTried
                + " (throwing original exception)");
        setup.put(USER, originalUser);
        setHostname(setup, metadata, true);
    }

    protected boolean setHostname(ConfigBag setup, NodeMetadata metadata, boolean rethrow) {
        try {
            setup.put(SshTool.PROP_HOST, getPublicHostname(metadata, Optional.<HostAndPort>absent(), setup));
            return true;
        } catch (Exception e) {
            if (rethrow) {
                LOG.warn(
                        "couldn't connect to " + metadata + " when trying to discover hostname (rethrowing): " + e);
                throw Exceptions.propagate(e);
            }
            return false;
        }
    }

    protected String getPublicHostname(NodeMetadata node, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) {
        return getPublicHostname(node, sshHostAndPort, node.getCredentials(), setup);
    }

    /**
     * Attempts to obtain the hostname or IP of the node, as advertised by the cloud provider.
     * Prefers public, reachable IPs.
     * For some clouds (e.g. aws-ec2), it will attempt to find the public hostname.
     */
    protected String getPublicHostname(NodeMetadata node, Optional<HostAndPort> sshHostAndPort,
            LoginCredentials userCredentials, ConfigBag setup) {
        String provider = (setup != null) ? setup.get(CLOUD_PROVIDER) : null;
        if (provider == null)
            provider = getProvider();

        if ("aws-ec2".equals(provider)) {
            HostAndPort inferredHostAndPort = null;
            if (!sshHostAndPort.isPresent()) {
                try {
                    String vmIp = getFirstReachableAddress(node, setup);
                    int port = node.getLoginPort();
                    inferredHostAndPort = HostAndPort.fromParts(vmIp, port);
                } catch (Exception e) {
                    LOG.warn("Error reaching aws-ec2 instance " + node.getId() + "@" + node.getLocation()
                            + " on port " + node.getLoginPort() + "; falling back to jclouds metadata for address",
                            e);
                }
            }
            if (sshHostAndPort.isPresent() || inferredHostAndPort != null) {
                if (isWindows(node, setup)) {
                    if (inferredHostAndPort != null) {
                        LOG.warn("Cannot querying aws-ec2 Windows instance " + node.getId() + "@"
                                + node.getLocation()
                                + " over ssh for its hostname; falling back to first reachable IP");
                        return inferredHostAndPort.getHostText();
                    }
                } else {
                    HostAndPort hostAndPortToUse = sshHostAndPort.isPresent() ? sshHostAndPort.get()
                            : inferredHostAndPort;
                    try {
                        return getPublicHostnameAws(hostAndPortToUse, userCredentials, setup);
                    } catch (Exception e) {
                        if (inferredHostAndPort != null) {
                            LOG.warn("Error querying aws-ec2 instance " + node.getId() + "@" + node.getLocation()
                                    + " over ssh for its hostname; falling back to first reachable IP", e);
                            // We've already found a reachable address so settle for that, rather than doing it again
                            return inferredHostAndPort.getHostText();
                        } else {
                            LOG.warn("Error querying aws-ec2 instance " + node.getId() + "@" + node.getLocation()
                                    + " over ssh for its hostname; falling back to jclouds metadata for address",
                                    e);
                        }
                    }
                }
            }
        }

        return getPublicHostnameGeneric(node, setup);
    }

    private String getPublicHostnameGeneric(NodeMetadata node, @Nullable ConfigBag setup) {
        // JcloudsUtil.getFirstReachableAddress() (probably) already succeeded so at least one of the provided
        // public and private IPs is reachable. Prefer the public IP. Don't use hostname as a fallback
        // from the public address - if public address is missing why would hostname resolve to a 
        // public IP? It is sometimes wrong/abbreviated, resolving to the wrong IP, also e.g. on
        // rackspace, the hostname lacks the domain.
        //
        // TODO If POLL_FOR_FIRST_REACHABLE_ADDRESS=false, then won't have checked if any node is reachable.
        // TODO Some of the private addresses might not be reachable, should check connectivity before
        // making a choice.
        // TODO Choose an IP once and stick to it - multiple places call JcloudsUtil.getFirstReachableAddress(),
        // could even get different IP on each call.
        if (groovyTruth(node.getPublicAddresses())) {
            return node.getPublicAddresses().iterator().next();
        } else if (groovyTruth(node.getPrivateAddresses())) {
            return node.getPrivateAddresses().iterator().next();
        } else {
            return null;
        }
    }

    private String getPublicHostnameAws(HostAndPort hostAndPort, LoginCredentials userCredentials,
            ConfigBag setup) {
        SshMachineLocation sshLocByIp = null;
        try {
            // TODO messy way to get an SSH session
            sshLocByIp = createTemporarySshMachineLocation(hostAndPort, userCredentials, setup);

            ByteArrayOutputStream outStream = new ByteArrayOutputStream();
            ByteArrayOutputStream errStream = new ByteArrayOutputStream();
            int exitcode = sshLocByIp.execCommands(MutableMap.of("out", outStream, "err", errStream),
                    "get public AWS hostname", ImmutableList.of(BashCommands.INSTALL_CURL,
                            "echo `curl --silent --retry 20 http://169.254.169.254/latest/meta-data/public-hostname`; exit"));
            String outString = new String(outStream.toByteArray());
            String[] outLines = outString.split("\n");
            for (String line : outLines) {
                if (line.startsWith("ec2-"))
                    return line.trim();
            }
            throw new IllegalStateException(
                    "Could not obtain aws-ec2 hostname for vm " + hostAndPort + "; exitcode=" + exitcode
                            + "; stdout=" + outString + "; stderr=" + new String(errStream.toByteArray()));
        } finally {
            Streams.closeQuietly(sshLocByIp);
        }
    }

    /**
     * Attempts to obtain the private hostname or IP of the node, as advertised by the cloud provider.
     * 
     * For some clouds (e.g. aws-ec2), it will attempt to find the fully qualified hostname (as that works in public+private).
     */
    protected String getPrivateHostname(NodeMetadata node, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) {
        return getPrivateHostname(node, sshHostAndPort, node.getCredentials(), setup);
    }

    protected String getPrivateHostname(NodeMetadata node, Optional<HostAndPort> sshHostAndPort,
            LoginCredentials userCredentials, ConfigBag setup) {
        String provider = (setup != null) ? setup.get(CLOUD_PROVIDER) : null;
        if (provider == null)
            provider = getProvider();

        // TODO Discouraged to do cloud-specific things; think of this code for aws as an
        // exceptional situation rather than a pattern to follow. We need a better way to
        // do cloud-specific things.
        if ("aws-ec2".equals(provider)) {
            Maybe<String> result = getPrivateHostnameAws(node, sshHostAndPort, userCredentials, setup);
            if (result.isPresent())
                return result.get();
        }

        return getPrivateHostnameGeneric(node, setup);
    }

    private Maybe<String> getPrivateHostnameAws(NodeMetadata node, Optional<HostAndPort> sshHostAndPort,
            LoginCredentials userCredentials, ConfigBag setup) {
        // TODO Remove duplication from getPublicHostname.
        // TODO Don't like 
        HostAndPort inferredHostAndPort = null;
        if (!sshHostAndPort.isPresent()) {
            try {
                String vmIp = getFirstReachableAddress(node, setup);
                int port = node.getLoginPort();
                inferredHostAndPort = HostAndPort.fromParts(vmIp, port);
            } catch (Exception e) {
                LOG.warn("Error reaching aws-ec2 instance " + node.getId() + "@" + node.getLocation() + " on port "
                        + node.getLoginPort() + "; falling back to jclouds metadata for address", e);
            }
        }
        if (sshHostAndPort.isPresent() || inferredHostAndPort != null) {
            HostAndPort hostAndPortToUse = sshHostAndPort.isPresent() ? sshHostAndPort.get() : inferredHostAndPort;
            try {
                return Maybe.of(getPublicHostnameAws(hostAndPortToUse, userCredentials, setup));
            } catch (Exception e) {
                LOG.warn("Error querying aws-ec2 instance instance " + node.getId() + "@" + node.getLocation()
                        + " over ssh for its hostname; falling back to jclouds metadata for address", e);
            }
        }
        return Maybe.absent();
    }

    private String getPrivateHostnameGeneric(NodeMetadata node, @Nullable ConfigBag setup) {
        //prefer the private address to the hostname because hostname is sometimes wrong/abbreviated
        //(see that javadoc; also e.g. on rackspace/cloudstack, the hostname is not registered with any DNS).
        //Don't return local-only address (e.g. never 127.0.0.1)
        if (groovyTruth(node.getPrivateAddresses())) {
            for (String p : node.getPrivateAddresses()) {
                if (Networking.isLocalOnly(p))
                    continue;
                return p;
            }
        }
        if (groovyTruth(node.getPublicAddresses())) {
            return node.getPublicAddresses().iterator().next();
        } else if (groovyTruth(node.getHostname())) {
            return node.getHostname();
        } else {
            return null;
        }
    }

    // ------------ static converters (could go to a new file) ------------------

    public static File asFile(Object o) {
        if (o instanceof File)
            return (File) o;
        if (o == null)
            return null;
        return new File(o.toString());
    }

    public static String fileAsString(Object o) {
        if (o instanceof String)
            return (String) o;
        if (o instanceof File)
            return ((File) o).getAbsolutePath();
        if (o == null)
            return null;
        return o.toString();
    }

    protected static double toDouble(Object v) {
        if (v instanceof Number) {
            return ((Number) v).doubleValue();
        } else {
            throw new IllegalArgumentException("Invalid type for double: " + v + " of type " + v.getClass());
        }
    }

    protected static String[] toStringArray(Object v) {
        return toListOfStrings(v).toArray(new String[0]);
    }

    protected static List<String> toListOfStrings(Object v) {
        List<String> result = Lists.newArrayList();
        if (v instanceof Iterable) {
            for (Object o : (Iterable<?>) v) {
                result.add(o.toString());
            }
        } else if (v instanceof Object[]) {
            for (int i = 0; i < ((Object[]) v).length; i++) {
                result.add(((Object[]) v)[i].toString());
            }
        } else if (v instanceof String) {
            result.add((String) v);
        } else {
            throw new IllegalArgumentException("Invalid type for List<String>: " + v + " of type " + v.getClass());
        }
        return result;
    }

    protected static byte[] toByteArray(Object v) {
        if (v instanceof byte[]) {
            return (byte[]) v;
        } else if (v instanceof CharSequence) {
            return v.toString().getBytes();
        } else {
            throw new IllegalArgumentException("Invalid type for byte[]: " + v + " of type " + v.getClass());
        }
    }

    @VisibleForTesting
    static int[] toIntPortArray(Object v) {
        PortRange portRange = PortRanges.fromIterable(Collections.singletonList(v));
        int[] portArray = ArrayUtils.toPrimitive(Iterables.toArray(portRange, Integer.class));

        return portArray;
    }

    // Handles GString
    protected static Map<String, String> toMapStringString(Object v) {
        if (v instanceof Map<?, ?>) {
            Map<String, String> result = Maps.newLinkedHashMap();
            for (Map.Entry<?, ?> entry : ((Map<?, ?>) v).entrySet()) {
                String key = ((CharSequence) entry.getKey()).toString();
                String value = ((CharSequence) entry.getValue()).toString();
                result.put(key, value);
            }
            return result;
        } else if (v instanceof CharSequence) {
            return KeyValueParser.parseMap(v.toString());
        } else {
            throw new IllegalArgumentException(
                    "Invalid type for Map<String,String>: " + v + (v != null ? " of type " + v.getClass() : ""));
        }
    }

    private List<String> createIptablesRulesForNetworkInterface(Iterable<Integer> ports) {
        List<String> iptablesRules = Lists.newArrayList();
        for (Integer port : ports) {
            iptablesRules.add(IptablesCommands.insertIptablesRule(Chain.INPUT, Protocol.TCP, port, Policy.ACCEPT));
        }
        return iptablesRules;
    }

    @Override
    public PersistenceObjectStore newPersistenceObjectStore(String container) {
        return new JcloudsBlobStoreBasedObjectStore(this, container);
    }

}