brooklyn.location.jclouds.JcloudsLocation.java Source code

Java tutorial

Introduction

Here is the source code for 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 brooklyn.location.jclouds;

import static brooklyn.util.JavaGroovyEquivalents.elvis;
import static brooklyn.util.JavaGroovyEquivalents.groovyTruth;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.jclouds.compute.options.RunScriptOptions.Builder.overrideLoginCredentials;
import static org.jclouds.scriptbuilder.domain.Statements.exec;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
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.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import org.jclouds.abiquo.compute.options.AbiquoTemplateOptions;
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.ExecResponse;
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.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.domain.Statements;
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 brooklyn.config.ConfigKey;
import brooklyn.config.ConfigKey.HasConfigKey;
import brooklyn.config.ConfigUtils;
import brooklyn.entity.basic.Entities;
import brooklyn.entity.rebind.persister.LocationWithObjectStore;
import brooklyn.entity.rebind.persister.PersistenceObjectStore;
import brooklyn.entity.rebind.persister.jclouds.JcloudsBlobStoreBasedObjectStore;
import brooklyn.location.LocationSpec;
import brooklyn.location.MachineLocation;
import brooklyn.location.MachineManagementMixins.MachineMetadata;
import brooklyn.location.MachineManagementMixins.RichMachineProvisioningLocation;
import brooklyn.location.NoMachinesAvailableException;
import brooklyn.location.access.PortForwardManager;
import brooklyn.location.access.PortMapping;
import brooklyn.location.basic.BasicMachineMetadata;
import brooklyn.location.basic.LocationConfigKeys;
import brooklyn.location.basic.LocationConfigUtils;
import brooklyn.location.basic.LocationConfigUtils.OsCredential;
import brooklyn.location.basic.SshMachineLocation;
import brooklyn.location.cloud.AbstractCloudMachineProvisioningLocation;
import brooklyn.location.cloud.AvailabilityZoneExtension;
import brooklyn.location.cloud.CloudMachineNamer;
import brooklyn.location.jclouds.JcloudsPredicates.NodeInLocation;
import brooklyn.location.jclouds.networking.JcloudsPortForwarderExtension;
import brooklyn.location.jclouds.templates.PortableTemplateBuilder;
import brooklyn.location.jclouds.zone.AwsAvailabilityZoneExtension;
import brooklyn.management.AccessController;
import brooklyn.util.ResourceUtils;
import brooklyn.util.collections.MutableList;
import brooklyn.util.collections.MutableMap;
import brooklyn.util.config.ConfigBag;
import brooklyn.util.crypto.SecureKeys;
import brooklyn.util.exceptions.CompoundRuntimeException;
import brooklyn.util.exceptions.Exceptions;
import brooklyn.util.exceptions.ReferenceWithError;
import brooklyn.util.flags.SetFromFlag;
import brooklyn.util.flags.TypeCoercions;
import brooklyn.util.guava.Maybe;
import brooklyn.util.internal.ssh.ShellTool;
import brooklyn.util.internal.ssh.SshTool;
import brooklyn.util.javalang.Enums;
import brooklyn.util.javalang.Reflections;
import brooklyn.util.net.Cidr;
import brooklyn.util.net.Networking;
import brooklyn.util.net.Protocol;
import brooklyn.util.os.Os;
import brooklyn.util.repeat.Repeater;
import brooklyn.util.ssh.BashCommands;
import brooklyn.util.ssh.IptablesCommands;
import brooklyn.util.ssh.IptablesCommands.Chain;
import brooklyn.util.ssh.IptablesCommands.Policy;
import brooklyn.util.stream.Streams;
import brooklyn.util.text.ByteSizeStrings;
import brooklyn.util.text.Identifiers;
import brooklyn.util.text.KeyValueParser;
import brooklyn.util.text.Strings;
import brooklyn.util.text.TemplateProcessor;
import brooklyn.util.time.Duration;
import brooklyn.util.time.Time;

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;
import com.google.common.primitives.Ints;
import com.google.common.reflect.TypeToken;

/**
 * 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,
        RichMachineProvisioningLocation<SshMachineLocation>, LocationWithObjectStore {

    // 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 boolean loggedSshKeysHint = false;

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

    @SetFromFlag // so it's persisted
    private final Map<JcloudsSshMachineLocation, 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
    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);
            }
            setConfig(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);
    }

    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().getCatalog().getRootClassLoader(), namerClass, config);
            if (cloudNamer.isPresent()) {
                return cloudNamer.get();
            } else {
                throw new IllegalStateException(
                        "Failed to create CloudMachineNamer " + namerClass + " for location " + this);
            }
        } else {
            return new JcloudsMachineNamer(config);
        }
    }

    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().getCatalog().getRootClassLoader();
        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;
    }

    public void setDefaultImageId(String val) {
        setConfig(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...
    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 getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(conf, 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, MachineMetadata> listMachines() {
        Set<? extends ComputeMetadata> nodes = getRegion() != null
                ? getComputeService().listNodesDetailsMatching(new NodeInLocation(getRegion(), true))
                : getComputeService().listNodes();
        Map<String, MachineMetadata> result = new LinkedHashMap<String, MachineMetadata>();

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

        return result;
    }

    protected 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);
    }

    public 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) {
        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 JcloudsSshMachineLocation obtain() throws NoMachinesAvailableException {
        return obtain(MutableMap.of());
    }

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

    public JcloudsSshMachineLocation 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) */
    public JcloudsSshMachineLocation 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 JcloudsSshMachineLocation 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 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());
        NodeMetadata node = null;
        JcloudsSshMachineLocation sshMachineLocation = null;

        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);
            }

            Stopwatch provisioningStopwatch = Stopwatch.createStarted();
            Duration templateTimestamp, provisionTimestamp, usableTimestamp, customizedTimestamp;

            LoginCredentials userCredentials = null;
            Set<? extends NodeMetadata> nodes;
            Template template;
            try {
                // Setup the template
                template = buildTemplate(computeService, setup);
                if (waitForSshable && !skipJcloudsSshing) {
                    userCredentials = initTemplateForCreateUser(template, setup);
                }

                //FIXME initialCredentials = initUserTemplateOptions(template, setup);
                for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
                    customizer.customize(this, computeService, template);
                    customizer.customize(this, computeService, template.getOptions());
                }
                LOG.debug("jclouds using template {} / options {} to provision machine in {}",
                        new Object[] { template, template.getOptions(), setup.getDescription() });

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

                templateTimestamp = Duration.of(provisioningStopwatch);
                template.getOptions().getUserMetadata().put("Name",
                        cloudMachineNamer.generateNewMachineUniqueNameFromGroupId(groupId));

                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());

            // 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();
            }

            if (waitForSshable && skipJcloudsSshing) {
                // once that host:port is definitely reachable, we can create the user
                waitForReachable(computeService, node, sshHostAndPortOverride, node.getCredentials(), setup);
                userCredentials = createUser(computeService, node, sshHostAndPortOverride, 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 = extractVmCredentials(setup, node);
            }
            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) {
                waitForReachable(computeService, node, sshHostAndPortOverride, userCredentials, setup);
            } else {
                LOG.debug("Skipping ssh check for {} ({}) due to config waitForSshable=false", node,
                        setup.getDescription());
            }
            usableTimestamp = Duration.of(provisioningStopwatch);

            // Create a JcloudsSshMachineLocation, and register it
            sshMachineLocation = registerJcloudsSshMachineLocation(computeService, node, userCredentials,
                    sshHostAndPortOverride, setup);
            if (template != null && sshMachineLocation.getTemplate() == null) {
                sshMachineLocation.template = template;
            }

            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(), sshMachineLocation,
                            node.getLoginPort());
                } else {
                    LOG.warn("No port-forward manager for {} so could not associate {} -> {} for {}",
                            new Object[] { this, node.getLoginPort(), sshHostAndPortOverride, sshMachineLocation });
                }
            }

            if ("docker".equals(this.getProvider())) {
                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 = sshMachineLocation.getSshHostAndPort().getHostText();
                        portForwardManager.associate(node.getId(), HostAndPort.fromParts(dockerHost, hostPort),
                                sshMachineLocation, containerPort);
                    }
                } else {
                    LOG.warn("No port-forward manager for {} so could not associate docker port-mappings for {}",
                            this, sshMachineLocation);
                }
            }

            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);
                if (Strings.isNonBlank(setupScript)) {
                    customisationForLogging.add("custom setup script " + setupScript);

                    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(setupScript);
                    String script = TemplateProcessor.processTemplateContents(scriptContent, getManagementContext(),
                            substitutions);
                    sshMachineLocation.execCommands("Customizing node " + this, ImmutableList.of(script));
                }

                if (setup.get(JcloudsLocationConfig.MAP_DEV_RANDOM_TO_DEV_URANDOM)) {
                    customisationForLogging.add("point /dev/random to urandom");

                    sshMachineLocation.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)) {
                    customisationForLogging.add("configure hostname");

                    sshMachineLocation.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)) {
                    @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 {}", sshMachineLocation,
                                this);
                    } else {
                        customisationForLogging.add("open iptables");

                        List<String> 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.execCommands("Inserting iptables rules, 50 command batch",
                                        batch);
                                batch.clear();
                            }
                        }
                        if (batch.size() > 0) {
                            sshMachineLocation.execCommands("Inserting iptables rules", batch);
                        }
                        sshMachineLocation.execCommands("List iptables rules",
                                ImmutableList.of(IptablesCommands.listIptablesRule()));
                    }
                }

                if (setup.get(STOP_IPTABLES)) {
                    customisationForLogging.add("stop iptables");

                    List<String> cmds = ImmutableList.of(IptablesCommands.iptablesServiceStop(),
                            IptablesCommands.iptablesServiceStatus());
                    sshMachineLocation.execCommands("Stopping iptables", cmds);
                }

                List<String> extraKeyUrlsToAuth = setup.get(EXTRA_PUBLIC_KEY_URLS_TO_AUTH);
                if (extraKeyUrlsToAuth != null && !extraKeyUrlsToAuth.isEmpty()) {
                    List<String> extraKeyDataToAuth = MutableList.of();
                    for (String keyUrl : extraKeyUrlsToAuth) {
                        extraKeyDataToAuth.add(ResourceUtils.create().getResourceAsString(keyUrl));
                    }
                    sshMachineLocation.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)) {
                customizer.customize(this, computeService, sshMachineLocation);
            }

            customizedTimestamp = Duration.of(provisioningStopwatch);

            LOG.info("Finished VM " + setup.getDescription() + " creation:" + " " + sshMachineLocation.getUser()
                    + "@" + sshMachineLocation.getAddress() + ":" + sshMachineLocation.getPort()
                    + (Boolean.TRUE.equals(setup.get(LOG_CREDENTIALS))
                            ? "password=" + userCredentials.getOptionalPassword().or("<absent>") + " && key="
                                    + userCredentials.getOptionalPrivateKey().or("<absent>")
                            : "")
                    + " ready after " + Duration.of(provisioningStopwatch).toStringRounded() + " (" + template
                    + " template built in " + Duration.of(templateTimestamp).toStringRounded() + ";" + " " + node
                    + " provisioned in "
                    + Duration.of(provisionTimestamp).subtract(templateTimestamp).toStringRounded() + ";" + " "
                    + sshMachineLocation + " ssh usable in "
                    + Duration.of(usableTimestamp).subtract(provisionTimestamp).toStringRounded() + ";"
                    + " and os customized in "
                    + Duration.of(customizedTimestamp).subtract(usableTimestamp).toStringRounded() + " - "
                    + Joiner.on(", ").join(customisationForLogging) + ")");

            return sshMachineLocation;
        } 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));

            LOG.error("Failed to start VM for {}{}: {}", new Object[] { setup.getDescription(),
                    (destroyNode ? " (destroying " + node + ")" : ""), e.getMessage() });
            LOG.debug(Throwables.getStackTraceAsString(e));

            if (destroyNode) {
                if (sshMachineLocation != null) {
                    releaseSafely(sshMachineLocation);
                } else {
                    releaseNodeSafely(node);
                }
            }

            throw Exceptions.propagate(e);
        }
    }

    // ------------- 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 = toIntArray(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) {
                        if (v == null)
                            return;
                        ((EC2TemplateOptions) t).userData(v.toString().getBytes());
                        // TODO avail in next jclouds thanks to @andreaturli
                        //                          } else if (t instanceof SoftLayerTemplateOptions) {
                        //                              ((SoftLayerTemplateOptions)t).userData(Strings.toString(v));
                    } 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);
                        // TODO avail in next jclouds thanks to @andreaturli
                        //                        } 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) {
                    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(OVERRIDE_RAM, new CustomizeTemplateOptions() {
                public void apply(TemplateOptions t, ConfigBag props, Object v) {
                    if (t instanceof AbiquoTemplateOptions) {
                        ((AbiquoTemplateOptions) t).overrideRam((Integer) v);
                    } else {
                        LOG.info(
                                "ignoring overrideRam({}) 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) {
                    t.networks((String) v);
                }
            }).put(TEMPLATE_OPTIONS, new CustomizeTemplateOptions() {
                @Override
                public void apply(TemplateOptions options, ConfigBag config, Object v) {
                    if (v == null)
                        return;
                    @SuppressWarnings("unchecked")
                    Map<String, String> optionsMap = (Map<String, String>) v;
                    if (optionsMap.isEmpty())
                        return;

                    Class<? extends TemplateOptions> clazz = options.getClass();
                    Iterable<Method> methods = Arrays.asList(clazz.getMethods());
                    for (final Map.Entry<String, String> option : optionsMap.entrySet()) {
                        Optional<Method> methodOptional = Iterables.tryFind(methods, new Predicate<Method>() {
                            @Override
                            public boolean apply(@Nullable Method input) {
                                // Matches a method with the expected name, and a single parameter that TypeCoercions
                                // can coerce to
                                if (input == null)
                                    return false;
                                if (!input.getName().equals(option.getKey()))
                                    return false;
                                Type[] parameterTypes = input.getGenericParameterTypes();
                                return parameterTypes.length == 1 && TypeCoercions
                                        .tryCoerce(option.getValue(), TypeToken.of(parameterTypes[0]))
                                        .isPresentAndNonNull();
                            }
                        });
                        if (methodOptional.isPresent()) {
                            try {
                                Method method = methodOptional.get();
                                method.invoke(options, TypeCoercions.coerce(option.getValue(),
                                        TypeToken.of(method.getGenericParameterTypes()[0])));
                            } catch (IllegalAccessException e) {
                                throw Exceptions.propagate(e);
                            } catch (InvocationTargetException e) {
                                throw Exceptions.propagate(e);
                            }
                        } else {
                            LOG.warn(
                                    "Ignoring request to set template option {} because this is not supported by {}",
                                    new Object[] { option.getKey(), clazz.getCanonicalName() });
                        }
                    }
                }
            }).build();

    private static boolean listedAvailableTemplatesOnNoSuchTemplate = false;

    /** 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) {
                templateBuilder.imageChooser(config.get(JcloudsLocationConfig.IMAGE_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;
        try {
            template = templateBuilder.build();
            if (template == null)
                throw new NullPointerException("No template found (templateBuilder.build returned null)");
            LOG.debug("jclouds found template " + template + " (image " + template.getImage()
                    + ") for provisioning in " + this + " for " + config.getDescription());
            if (template.getImage() == 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 {
                synchronized (this) {
                    // 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);
                    if (!listedAvailableTemplatesOnNoSuchTemplate) {
                        listedAvailableTemplatesOnNoSuchTemplate = true;
                        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();

        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);
        }
    }

    protected SshMachineLocation createTemporarySshMachineLocation(HostAndPort hostAndPort, LoginCredentials creds,
            ConfigBag config) {
        Optional<String> initialPassword = creds.getOptionalPassword();
        Optional<String> initialPrivateKey = creds.getOptionalPrivateKey();
        String initialUser = creds.getUser();

        Map<String, Object> sshProps = Maps.newLinkedHashMap(config.getAllConfig());
        sshProps.put("user", initialUser);
        sshProps.put("address", hostAndPort.getHostText());
        sshProps.put("port", hostAndPort.getPort());
        if (initialPassword.isPresent())
            sshProps.put("password", initialPassword.get());
        if (initialPrivateKey.isPresent())
            sshProps.put("privateKeyData", initialPrivateKey.get());
        if (initialPrivateKey.isPresent())
            sshProps.put("privateKeyData", initialPrivateKey.get());

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

    /**
     * Create the user immediately - executing ssh commands as required.
     */
    protected LoginCredentials createUser(ComputeService computeService, NodeMetadata node,
            Optional<HostAndPort> hostAndPortOverride, 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 (node.getOperatingSystem() == null) {
                scriptOsFamily = org.jclouds.scriptbuilder.domain.OsFamily.UNIX;
            } else {
                scriptOsFamily = (node.getOperatingSystem()
                        .getFamily() == org.jclouds.compute.domain.OsFamily.WINDOWS)
                                ? org.jclouds.scriptbuilder.domain.OsFamily.WINDOWS
                                : org.jclouds.scriptbuilder.domain.OsFamily.UNIX;
            }

            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));
            }

            LoginCredentials initialCredentials = node.getCredentials();
            Optional<String> initialPassword = initialCredentials.getOptionalPassword();
            Optional<String> initialPrivateKey = initialCredentials.getOptionalPrivateKey();
            String initialUser = initialCredentials.getUser();
            String address = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getHostText()
                    : JcloudsUtil.getFirstReachableAddress(computeService.getContext(), node);
            int port = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getPort() : node.getLoginPort();

            Map<String, Object> sshProps = Maps.newLinkedHashMap(config.getAllConfig());
            sshProps.put("user", initialUser);
            sshProps.put("address", address);
            sshProps.put("port", port);
            if (initialPassword.isPresent())
                sshProps.put("password", initialPassword.get());
            if (initialPrivateKey.isPresent())
                sshProps.put("privateKeyData", initialPrivateKey.get());

            // 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 });
            }

            SshMachineLocation sshLoc = null;
            try {
                if (isManaged()) {
                    sshLoc = getManagementContext().getLocationManager().createLocation(sshProps,
                            SshMachineLocation.class);
                } else {
                    sshLoc = new SshMachineLocation(sshProps);
                }

                int exitcode = sshLoc.execScript(execProps, "create-user", commands);
                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

        LoginCredentials createdUserCreds = null;
        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();

        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();
                } else if (credential.hasKey()) {
                    createdUserCreds = LoginCredentials.builder().user(user)
                            .privateKey(credential.getPrivateKeyData()).build();
                }
            }

        } else if (Strings.isBlank(user) || user.equals(loginUser) || user.equals(ROOT_USERNAME)) {
            // 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 (Strings.isNonBlank(credential.getPublicKeyData())) {
                statements.add(new AuthorizeRSAPublicKeys("~" + user + "/.ssh",
                        ImmutableList.of(credential.getPublicKeyData())));
                if (!credential.isUsingPassword() && Strings.isNonBlank(credential.getPrivateKeyData())) {
                    createdUserCreds = LoginCredentials.builder().user(user)
                            .privateKey(credential.getPrivateKeyData()).build();
                }
            }

        } 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 (!loggedSshKeysHint && !config.containsKey(PRIVATE_KEY_FILE)) {
                    loggedSshKeysHint = 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);

            boolean useKey = Strings.isNonBlank(pubKey);

            // 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();
            }
        }

        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);
    }

    // ----------------- rebinding to existing machine ------------------------

    public JcloudsSshMachineLocation rebindMachine(NodeMetadata metadata) throws NoMachinesAvailableException {
        return rebindMachine(MutableMap.of(), metadata);
    }

    public JcloudsSshMachineLocation rebindMachine(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 rebindMachine(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 JcloudsSshMachineLocation#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 ssh'ing into the machine
     * <ul>
     */
    public JcloudsSshMachineLocation rebindMachine(ConfigBag setup) throws NoMachinesAvailableException {
        try {
            if (setup.getDescription() == null)
                setCreationString(setup);

            final String rawId = (String) setup.getStringKey("id");
            final String rawHostname = (String) setup.getStringKey("hostname");
            String user = checkNotNull(getUser(setup), "user");
            final String rawRegion = (String) setup.getStringKey("region");

            LOG.info("Rebinding to VM {} ({}@{}), in jclouds location for provider {}",
                    new Object[] { rawId != null ? rawId : "<lookup>", user,
                            (rawHostname != null ? rawHostname : "<unspecified>"), getProvider() });

            ComputeService computeService = getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(setup, true);

            Set<? extends NodeMetadata> candidateNodes = computeService
                    .listNodesDetailsMatching(new Predicate<ComputeMetadata>() {
                        @Override
                        public boolean apply(ComputeMetadata input) {
                            // ID exact match
                            if (rawId != null) {
                                if (rawId.equals(input.getId()))
                                    return true;
                                // AWS format
                                if (rawRegion != null && (rawRegion + "/" + rawId).equals(input.getId()))
                                    return true;
                            }
                            // else do node metadata lookup
                            if (!(input instanceof NodeMetadata))
                                return false;
                            if (rawHostname != null
                                    && rawHostname.equalsIgnoreCase(((NodeMetadata) input).getHostname()))
                                return true;
                            if (rawHostname != null
                                    && ((NodeMetadata) input).getPublicAddresses().contains(rawHostname))
                                return true;

                            if (rawId != null && rawId.equalsIgnoreCase(((NodeMetadata) input).getHostname()))
                                return true;
                            if (rawId != null && ((NodeMetadata) input).getPublicAddresses().contains(rawId))
                                return true;
                            // don't do private IP's because those might be repeated

                            if (rawId != null && rawId.equalsIgnoreCase(((NodeMetadata) input).getProviderId()))
                                return true;
                            if (rawHostname != null
                                    && rawHostname.equalsIgnoreCase(((NodeMetadata) input).getProviderId()))
                                return true;

                            return false;
                        }
                    });

            if (candidateNodes.isEmpty())
                throw new IllegalArgumentException("Jclouds node not found for rebind, looking for id=" + rawId
                        + " and hostname=" + rawHostname);
            if (candidateNodes.size() > 1)
                throw new IllegalArgumentException("Jclouds node for rebind matching multiple, looking for id="
                        + rawId + " and hostname=" + rawHostname + ": " + candidateNodes);

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

            // TODO confirm we can SSH ?
            // NB if rawHostname not set, get the hostname using getPublicHostname(node, Optional.<HostAndPort>absent(), setup);

            return registerJcloudsSshMachineLocation(computeService, node, null, Optional.<HostAndPort>absent(),
                    setup);

        } catch (IOException e) {
            throw Exceptions.propagate(e);
        }
    }

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

    // -------------- 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, null, sshHostAndPort, setup);
    }

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

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

        JcloudsSshMachineLocation machine = createJcloudsSshMachineLocation(computeService, node, vmHostname,
                sshHostAndPort, setup);
        machine.setParent(this);
        vmInstanceIds.put(machine, node.getId());
        return machine;
    }

    /** @deprecated since 0.7.0 use variant which takes compute service; no longer called internally,
     * so marked final to force any overrides to switch to new syntax */
    @Deprecated
    protected final JcloudsSshMachineLocation createJcloudsSshMachineLocation(NodeMetadata node, String vmHostname,
            Optional<HostAndPort> sshHostAndPort, ConfigBag setup) throws IOException {
        return createJcloudsSshMachineLocation(null, node, vmHostname, sshHostAndPort, setup);
    }

    protected JcloudsSshMachineLocation createJcloudsSshMachineLocation(ComputeService computeService,
            NodeMetadata node, String vmHostname, Optional<HostAndPort> sshHostAndPort, 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 = JcloudsUtil.getFirstReachableAddress(computeService.getContext(), node);
        }

        if (LOG.isDebugEnabled())
            LOG.debug("creating JcloudsSshMachineLocation representation for {}@{} ({}/{}) for {}/{}",
                    new Object[] { getUser(setup), address, Entities.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())
                    .configure("user", getUser(setup))
                    // don't think "config" does anything
                    .configure(sshConfig)
                    // FIXME remove "config" -- inserted directly, above
                    .configure("config", sshConfig).configure("jcloudsParent", this).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 {
            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())
                    .put("user", getUser(setup))
                    // don't think "config" does anything
                    .putAll(sshConfig)
                    // FIXME remove "config" -- inserted directly, above
                    .put("config", sshConfig).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);
        }
    }

    // -------------- 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(SshMachineLocation machine) {
        String instanceId = vmInstanceIds.remove(machine);
        if (instanceId == null) {
            LOG.info("Attempted release of unknown machine " + machine + " in " + toString());
            throw new IllegalArgumentException("Unknown machine " + machine);
        }

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

        Exception tothrow = null;
        try {
            releasePortForwarding(machine);
        } catch (Exception e) {
            LOG.error("Problem releasing port-forwarding for machine " + machine + " in " + this + ", instance id "
                    + instanceId + "; discarding instance and continuing...", e);
            tothrow = e;
        }

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

        removeChild(machine);

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

    protected void releaseSafely(SshMachineLocation 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(SshMachineLocation machine) {
        // TODO Implementation needs revisisted. It relies on deprecated PortForwardManager methods.

        boolean usePortForwarding = Boolean.TRUE.equals(machine.getConfig(USE_PORT_FORWARDING));
        JcloudsPortForwarderExtension portForwarder = machine.getConfig(PORT_FORWARDER);
        PortForwardManager portForwardManager = machine.getConfig(PORT_FORWARDING_MANAGER);
        NodeMetadata node = (machine instanceof JcloudsSshMachineLocation)
                ? ((JcloudsSshMachineLocation) machine).getNode()
                : null;

        if (portForwarder == null) {
            LOG.debug("No port-forwarding to close (because portForwarder null) on release of " + machine);
        } else {
            // Release the port-forwarding for the login-port, which was explicilty created by JcloudsLocation 
            if (usePortForwarding && node != null) {
                HostAndPort sshHostAndPortOverride = machine.getSshHostAndPort();
                LOG.debug("Closing port-forwarding at {} for machine {}: {}->{}",
                        new Object[] { this, machine, sshHostAndPortOverride, node.getLoginPort() });
                portForwarder.closePortForwarding(node, node.getLoginPort(), sshHostAndPortOverride, 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 (node != null) {
                    mappings.addAll(portForwardManager.getPortMappingWithPublicIpId(node.getId()));
                }
            } else {
                mappings = ImmutableSet.of();
            }

            for (PortMapping mapping : mappings) {
                HostAndPort publicEndpoint = mapping.getPublicEndpoint();
                int targetPort = mapping.getPrivatePort();
                Protocol protocol = Protocol.TCP;
                if (publicEndpoint != null) {
                    LOG.debug("Closing port-forwarding at {} for machine {}: {}->{}",
                            new Object[] { this, machine, publicEndpoint, targetPort });
                    portForwarder.closePortForwarding(node, targetPort, publicEndpoint, protocol);
                }
            }
        }

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

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

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

        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 void waitForReachable(final ComputeService computeService, final NodeMetadata node,
            Optional<HostAndPort> hostAndPortOverride, final LoginCredentials expectedCredentials,
            ConfigBag setup) {
        String waitForSshable = setup.get(WAIT_FOR_SSHABLE);
        checkArgument(!"false".equalsIgnoreCase(waitForSshable),
                "waitForReachable called despite waitForSshable=%s", waitForSshable);

        String vmIp = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getHostText()
                : JcloudsUtil.getFirstReachableAddress(computeService.getContext(), node);
        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;

        long delayMs = -1;
        try {
            delayMs = Time.parseTimeString("" + waitForSshable);
        } catch (Exception e) {
            // normal if 'true'; just fall back to default
        }
        if (delayMs < 0)
            delayMs = Time.parseTimeString(WAIT_FOR_SSHABLE.getDefaultValue());

        String user = expectedCredentials.getUser();
        if (LOG.isDebugEnabled()) {
            Optional<String> password;
            Optional<String> key;
            if (Boolean.TRUE.equals(setup.get(LOG_CREDENTIALS))) {
                password = expectedCredentials.getOptionalPassword();
                key = expectedCredentials.getOptionalPrivateKey();
            } else {
                password = expectedCredentials.getOptionalPassword().isPresent() ? Optional.of("******")
                        : Optional.<String>absent();
                key = expectedCredentials.getOptionalPrivateKey().isPresent() ? Optional.of("******")
                        : Optional.<String>absent();
            }
            LOG.debug(
                    "VM {}: reported online, now waiting {} for it to be sshable on {}@{}:{}{}; using credentials password={}; key={}",
                    new Object[] { setup.getDescription(), Time.makeTimeStringRounded(delayMs), user, vmIp, vmPort,
                            Objects.equal(user, getUser(setup)) ? ""
                                    : " (setup user is different: " + getUser(setup) + ")",
                            password.or("<absent>"), key.or("<absent>") });
        }

        Callable<Boolean> checker;
        if (hostAndPortOverride.isPresent()) {
            final SshMachineLocation machine = createTemporarySshMachineLocation(hostAndPortOverride.get(),
                    expectedCredentials, setup);
            checker = new Callable<Boolean>() {
                public Boolean call() {
                    int exitstatus = machine.execScript("check-connectivity", ImmutableList.of("hostname"));
                    return exitstatus == 0;
                }
            };
        } else {
            checker = new Callable<Boolean>() {
                public Boolean call() {
                    Statement statement = Statements.newStatementList(exec("hostname"));
                    ExecResponse response = computeService.runScriptOnNode(node.getId(), statement,
                            overrideLoginCredentials(expectedCredentials).runAsRoot(false));
                    return response.getExitStatus() == 0;
                }
            };
        }

        Stopwatch stopwatch = Stopwatch.createStarted();

        ReferenceWithError<Boolean> reachable = new Repeater().every(1, SECONDS).until(checker)
                .limitTimeTo(delayMs, MILLISECONDS).runKeepingError();

        if (!reachable.getWithoutError()) {
            throw new IllegalStateException("SSH failed for " + user + "@" + vmIp + " (" + setup.getDescription()
                    + ") after waiting " + Time.makeTimeStringRounded(delayMs), reachable.getError());
        }

        LOG.debug("VM {}: is sshable after {} on {}@{}",
                new Object[] { setup.getDescription(), Time.makeTimeStringRounded(stopwatch), user, vmIp });
    }

    // -------------------- 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;
        }
    }

    /**
     * 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, 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 = JcloudsUtil.getFirstReachableAddress(this.getComputeService().getContext(), node);
                    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 getPublicHostnameAws(hostAndPortToUse, setup);
                } catch (Exception e) {
                    LOG.warn("Error querying aws-ec2 instance 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
                    if (inferredHostAndPort != null)
                        return inferredHostAndPort.getHostText();
                }
            }
        }

        return getPublicHostnameGeneric(node, setup);
    }

    private String getPublicHostnameGeneric(NodeMetadata node, @Nullable ConfigBag setup) {
        //prefer the public address to the hostname because hostname is sometimes wrong/abbreviated
        //(see that javadoc; also e.g. on rackspace, the hostname lacks the domain)
        if (groovyTruth(node.getPublicAddresses())) {
            return node.getPublicAddresses().iterator().next();
        } else if (groovyTruth(node.getHostname())) {
            return node.getHostname();
        } else if (groovyTruth(node.getPrivateAddresses())) {
            return node.getPrivateAddresses().iterator().next();
        } else {
            return null;
        }
    }

    private String getPublicHostnameAws(HostAndPort sshHostAndPort, ConfigBag setup) {
        SshMachineLocation sshLocByIp = null;
        try {
            ConfigBag sshConfig = extractSshConfig(setup, new ConfigBag());

            // TODO messy way to get an SSH session
            if (isManaged()) {
                sshLocByIp = getManagementContext().getLocationManager()
                        .createLocation(LocationSpec.create(SshMachineLocation.class)
                                .configure("address", sshHostAndPort.getHostText())
                                .configure("port", sshHostAndPort.getPort()).configure("user", getUser(setup))
                                .configure(sshConfig.getAllConfig()));
            } else {
                MutableMap<Object, Object> locationProps = MutableMap.builder()
                        .put("address", sshHostAndPort.getHostText()).put("port", sshHostAndPort.getPort())
                        .put("user", getUser(setup)).putAll(sshConfig.getAllConfig()).build();
                sshLocByIp = new SshMachineLocation(locationProps);
            }

            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 " + sshHostAndPort + "; exitcode=" + exitcode
                            + "; stdout=" + outString + "; stderr=" + new String(errStream.toByteArray()));
        } finally {
            Streams.closeQuietly(sshLocByIp);
        }
    }

    // ------------ 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());
        }
    }

    @VisibleForTesting
    static int[] toIntArray(Object v) {
        int[] result;
        if (v instanceof Iterable) {
            result = new int[Iterables.size((Iterable<?>) v)];
            int i = 0;
            for (Object o : (Iterable<?>) v) {
                result[i++] = (Integer) o;
            }
        } else if (v instanceof int[]) {
            result = (int[]) v;
        } else if (v instanceof Object[]) {
            result = new int[((Object[]) v).length];
            for (int i = 0; i < result.length; i++) {
                result[i] = (Integer) ((Object[]) v)[i];
            }
        } else if (v instanceof Integer) {
            result = new int[] { (Integer) v };
        } else if (v instanceof String) {
            Matcher listMatcher = LIST_PATTERN.matcher(v.toString());
            boolean intList = true;
            if (listMatcher.matches()) {
                List<String> strings = KeyValueParser.parseList(listMatcher.group(1));
                List<Integer> integers = new ArrayList<Integer>();
                for (String string : strings) {
                    if (INTEGER_PATTERN.matcher(string).matches()) {
                        integers.add(Integer.parseInt(string));
                    } else {
                        intList = false;
                        break;
                    }
                }
                result = Ints.toArray(integers);
            } else {
                intList = false;
                result = null;
            }
            if (!intList) {
                throw new IllegalArgumentException("Invalid type for int[]: " + v + " of type " + v.getClass());
            }
        } else {
            throw new IllegalArgumentException("Invalid type for int[]: " + v + " of type " + v.getClass());
        }
        return result;
    }

    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());
        }
    }

    // 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);
    }

}