You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/12/23 12:06:31 UTC

[08/71] [abbrv] incubator-brooklyn git commit: Merge commit 'e430723' into reorg2

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java
----------------------------------------------------------------------
diff --cc brooklyn-server/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java
index 0000000,7578b8c..20897d9
mode 000000,100644..100644
--- a/brooklyn-server/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java
+++ b/brooklyn-server/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java
@@@ -1,0 -1,3122 +1,3147 @@@
+ /*
+  * Licensed to the Apache Software Foundation (ASF) under one
+  * or more contributor license agreements.  See the NOTICE file
+  * distributed with this work for additional information
+  * regarding copyright ownership.  The ASF licenses this file
+  * to you under the Apache License, Version 2.0 (the
+  * "License"); you may not use this file except in compliance
+  * with the License.  You may obtain a copy of the License at
+  *
+  *     http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing,
+  * software distributed under the License is distributed on an
+  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  * KIND, either express or implied.  See the License for the
+  * specific language governing permissions and limitations
+  * under the License.
+  */
+ package org.apache.brooklyn.location.jclouds;
+ 
+ import static com.google.common.base.Preconditions.checkArgument;
+ import static com.google.common.base.Preconditions.checkNotNull;
 -import static java.util.concurrent.TimeUnit.SECONDS;
+ import static org.apache.brooklyn.util.JavaGroovyEquivalents.elvis;
+ import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth;
+ import static org.apache.brooklyn.util.ssh.BashCommands.sbinPath;
+ 
+ import java.io.ByteArrayOutputStream;
+ import java.io.File;
+ import java.io.IOException;
+ import java.lang.reflect.InvocationTargetException;
+ import java.lang.reflect.Method;
+ import java.security.KeyPair;
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.Collection;
+ import java.util.Collections;
+ import java.util.LinkedHashMap;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.NoSuchElementException;
+ import java.util.Set;
+ import java.util.concurrent.Callable;
+ import java.util.concurrent.Semaphore;
+ import java.util.concurrent.TimeUnit;
+ import java.util.concurrent.atomic.AtomicBoolean;
+ import java.util.concurrent.atomic.AtomicReference;
+ import java.util.regex.Pattern;
+ 
+ import javax.annotation.Nullable;
+ 
+ import org.apache.brooklyn.api.entity.Entity;
+ import org.apache.brooklyn.api.location.LocationSpec;
+ import org.apache.brooklyn.api.location.MachineLocation;
+ import org.apache.brooklyn.api.location.MachineLocationCustomizer;
+ import org.apache.brooklyn.api.location.MachineManagementMixins;
+ import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+ import org.apache.brooklyn.api.location.PortRange;
+ import org.apache.brooklyn.api.mgmt.AccessController;
+ import org.apache.brooklyn.api.mgmt.Task;
+ import org.apache.brooklyn.config.ConfigKey;
+ import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
+ import org.apache.brooklyn.core.config.ConfigUtils;
+ import org.apache.brooklyn.core.config.Sanitizer;
+ import org.apache.brooklyn.core.location.AbstractLocation;
+ import org.apache.brooklyn.core.location.BasicMachineMetadata;
+ import org.apache.brooklyn.core.location.LocationConfigKeys;
+ import org.apache.brooklyn.core.location.LocationConfigUtils;
+ import org.apache.brooklyn.core.location.LocationConfigUtils.OsCredential;
+ import org.apache.brooklyn.core.location.PortRanges;
+ import org.apache.brooklyn.core.location.access.PortForwardManager;
+ import org.apache.brooklyn.core.location.access.PortMapping;
+ import org.apache.brooklyn.core.location.cloud.AbstractCloudMachineProvisioningLocation;
+ import org.apache.brooklyn.core.location.cloud.AvailabilityZoneExtension;
+ import org.apache.brooklyn.core.location.cloud.names.AbstractCloudMachineNamer;
+ import org.apache.brooklyn.core.location.cloud.names.CloudMachineNamer;
+ import org.apache.brooklyn.core.mgmt.internal.LocalLocationManager;
+ import org.apache.brooklyn.core.mgmt.persist.LocationWithObjectStore;
+ import org.apache.brooklyn.core.mgmt.persist.PersistenceObjectStore;
+ import org.apache.brooklyn.core.mgmt.persist.jclouds.JcloudsBlobStoreBasedObjectStore;
+ import org.apache.brooklyn.location.jclouds.JcloudsPredicates.NodeInLocation;
+ import org.apache.brooklyn.location.jclouds.networking.JcloudsPortForwarderExtension;
+ import org.apache.brooklyn.location.jclouds.templates.PortableTemplateBuilder;
+ import org.apache.brooklyn.location.jclouds.zone.AwsAvailabilityZoneExtension;
+ import org.apache.brooklyn.location.ssh.SshMachineLocation;
+ import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
+ import org.apache.brooklyn.util.collections.MutableList;
+ import org.apache.brooklyn.util.collections.MutableMap;
+ import org.apache.brooklyn.util.collections.MutableSet;
+ import org.apache.brooklyn.util.core.ResourceUtils;
+ import org.apache.brooklyn.util.core.config.ConfigBag;
+ import org.apache.brooklyn.util.core.crypto.SecureKeys;
+ import org.apache.brooklyn.util.core.flags.MethodCoercions;
+ import org.apache.brooklyn.util.core.flags.SetFromFlag;
+ import org.apache.brooklyn.util.core.flags.TypeCoercions;
+ import org.apache.brooklyn.util.core.internal.ssh.ShellTool;
+ import org.apache.brooklyn.util.core.internal.ssh.SshTool;
+ import org.apache.brooklyn.util.core.internal.winrm.WinRmTool;
+ import org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse;
+ import org.apache.brooklyn.util.core.task.DynamicTasks;
+ import org.apache.brooklyn.util.core.task.TaskBuilder;
+ import org.apache.brooklyn.util.core.task.Tasks;
+ import org.apache.brooklyn.util.core.text.TemplateProcessor;
+ import org.apache.brooklyn.util.exceptions.CompoundRuntimeException;
+ import org.apache.brooklyn.util.exceptions.Exceptions;
+ import org.apache.brooklyn.util.exceptions.ReferenceWithError;
+ import org.apache.brooklyn.util.guava.Maybe;
+ import org.apache.brooklyn.util.javalang.Enums;
+ import org.apache.brooklyn.util.javalang.Reflections;
+ import org.apache.brooklyn.util.net.Cidr;
+ import org.apache.brooklyn.util.net.Networking;
+ import org.apache.brooklyn.util.net.Protocol;
+ import org.apache.brooklyn.util.os.Os;
+ import org.apache.brooklyn.util.repeat.Repeater;
+ import org.apache.brooklyn.util.ssh.BashCommands;
+ import org.apache.brooklyn.util.ssh.IptablesCommands;
+ import org.apache.brooklyn.util.ssh.IptablesCommands.Chain;
+ import org.apache.brooklyn.util.ssh.IptablesCommands.Policy;
+ import org.apache.brooklyn.util.stream.Streams;
+ import org.apache.brooklyn.util.text.ByteSizeStrings;
+ import org.apache.brooklyn.util.text.Identifiers;
+ import org.apache.brooklyn.util.text.KeyValueParser;
+ import org.apache.brooklyn.util.text.Strings;
+ import org.apache.brooklyn.util.time.Duration;
+ import org.apache.brooklyn.util.time.Time;
+ import org.apache.commons.lang3.ArrayUtils;
+ import org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions;
+ import org.jclouds.cloudstack.compute.options.CloudStackTemplateOptions;
+ import org.jclouds.compute.ComputeService;
+ import org.jclouds.compute.RunNodesException;
+ import org.jclouds.compute.config.AdminAccessConfiguration;
+ import org.jclouds.compute.domain.ComputeMetadata;
+ import org.jclouds.compute.domain.Hardware;
+ import org.jclouds.compute.domain.Image;
+ import org.jclouds.compute.domain.NodeMetadata;
+ import org.jclouds.compute.domain.NodeMetadata.Status;
+ import org.jclouds.compute.domain.NodeMetadataBuilder;
+ import org.jclouds.compute.domain.OperatingSystem;
+ import org.jclouds.compute.domain.OsFamily;
+ import org.jclouds.compute.domain.Template;
+ import org.jclouds.compute.domain.TemplateBuilder;
+ import org.jclouds.compute.domain.TemplateBuilderSpec;
+ import org.jclouds.compute.functions.Sha512Crypt;
+ import org.jclouds.compute.options.TemplateOptions;
+ import org.jclouds.domain.Credentials;
+ import org.jclouds.domain.LocationScope;
+ import org.jclouds.domain.LoginCredentials;
+ import org.jclouds.ec2.compute.options.EC2TemplateOptions;
+ import org.jclouds.googlecomputeengine.compute.options.GoogleComputeEngineTemplateOptions;
+ import org.jclouds.openstack.nova.v2_0.compute.options.NovaTemplateOptions;
+ import org.jclouds.rest.AuthorizationException;
+ import org.jclouds.scriptbuilder.domain.LiteralStatement;
+ import org.jclouds.scriptbuilder.domain.Statement;
+ import org.jclouds.scriptbuilder.domain.StatementList;
+ import org.jclouds.scriptbuilder.functions.InitAdminAccess;
+ import org.jclouds.scriptbuilder.statements.login.AdminAccess;
+ import org.jclouds.scriptbuilder.statements.login.ReplaceShadowPasswordEntry;
+ import org.jclouds.scriptbuilder.statements.ssh.AuthorizeRSAPublicKeys;
+ import org.jclouds.softlayer.compute.options.SoftLayerTemplateOptions;
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
+ 
+ import com.google.common.annotations.VisibleForTesting;
+ import com.google.common.base.Charsets;
+ import com.google.common.base.Function;
+ import com.google.common.base.Joiner;
+ import com.google.common.base.Objects;
+ import com.google.common.base.Optional;
+ import com.google.common.base.Predicate;
+ import com.google.common.base.Predicates;
+ import com.google.common.base.Splitter;
+ import com.google.common.base.Stopwatch;
+ import com.google.common.base.Supplier;
+ import com.google.common.base.Throwables;
+ import com.google.common.collect.ImmutableList;
+ import com.google.common.collect.ImmutableMap;
+ import com.google.common.collect.ImmutableSet;
+ import com.google.common.collect.Iterables;
+ import com.google.common.collect.Iterators;
+ import com.google.common.collect.Lists;
+ import com.google.common.collect.Maps;
+ import com.google.common.collect.Sets;
+ import com.google.common.collect.Sets.SetView;
+ import com.google.common.io.Files;
+ import com.google.common.net.HostAndPort;
+ 
 -import io.cloudsoft.winrm4j.pywinrm.Session;
 -import io.cloudsoft.winrm4j.pywinrm.WinRMFactory;
 -
+ /**
+  * For provisioning and managing VMs in a particular provider/region, using jclouds.
+  * Configuration flags are defined in {@link JcloudsLocationConfig}.
+  */
+ @SuppressWarnings("serial")
+ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation implements
+         JcloudsLocationConfig, MachineManagementMixins.RichMachineProvisioningLocation<MachineLocation>,
+         LocationWithObjectStore, MachineManagementMixins.SuspendResumeLocation {
+ 
+     // TODO After converting from Groovy to Java, this is now very bad code! It relies entirely on putting
+     // things into and taking them out of maps; it's not type-safe, and it's thus very error-prone.
+     // In Groovy, that's considered ok but not in Java.
+ 
+     // TODO test (and fix) ability to set config keys from flags
+ 
+     // TODO we say config is inherited, but it isn't the case for many "deep" / jclouds properties
+     // e.g. when we pass getRawLocalConfigBag() in and decorate it with additional flags
+     // (inheritance only works when we call getConfig in this class)
+ 
+     public static final Logger LOG = LoggerFactory.getLogger(JcloudsLocation.class);
+ 
+     public static final String ROOT_USERNAME = "root";
+     /** these userNames are known to be the preferred/required logins in some common/default images
+      *  where root@ is not allowed to log in */
+     public static final List<String> ROOT_ALIASES = ImmutableList.of("ubuntu", "ec2-user");
+     public static final List<String> COMMON_USER_NAMES_TO_TRY = ImmutableList.<String>builder().add(ROOT_USERNAME).addAll(ROOT_ALIASES).add("admin").build();
+ 
+     private static final Pattern LIST_PATTERN = Pattern.compile("^\\[(.*)\\]$");
+     private static final Pattern INTEGER_PATTERN = Pattern.compile("^\\d*$");
+ 
+     private static final int NOTES_MAX_LENGTH = 1000;
+ 
+     private final AtomicBoolean loggedSshKeysHint = new AtomicBoolean(false);
+     private final AtomicBoolean listedAvailableTemplatesOnNoSuchTemplate = new AtomicBoolean(false);
+ 
+     private final Map<String,Map<String, ? extends Object>> tagMapping = Maps.newLinkedHashMap();
+ 
+     @SetFromFlag // so it's persisted
+     private final Map<MachineLocation,String> vmInstanceIds = Maps.newLinkedHashMap();
+     
+     static { Networking.init(); }
+ 
+     public JcloudsLocation() {
+         super();
+     }
+ 
+     /** typically wants at least ACCESS_IDENTITY and ACCESS_CREDENTIAL */
+     public JcloudsLocation(Map<?,?> conf) {
+        super(conf);
+     }
+ 
+     @Override
+     @Deprecated
+     public JcloudsLocation configure(Map<?,?> properties) {
+         super.configure(properties);
+ 
+         if (config().getLocalBag().containsKey("providerLocationId")) {
+             LOG.warn("Using deprecated 'providerLocationId' key in "+this);
+             if (!config().getLocalBag().containsKey(CLOUD_REGION_ID))
+                 config().addToLocalBag(MutableMap.of(CLOUD_REGION_ID.getName(), (String)config().getLocalBag().getStringKey("providerLocationId")));
+         }
+ 
+         if (isDisplayNameAutoGenerated() || !groovyTruth(getDisplayName())) {
+             setDisplayName(elvis(getProvider(), "unknown") +
+                    (groovyTruth(getRegion()) ? ":"+getRegion() : "") +
+                    (groovyTruth(getEndpoint()) ? ":"+getEndpoint() : ""));
+         }
+ 
+         setCreationString(config().getLocalBag());
+ 
+         if (getConfig(MACHINE_CREATION_SEMAPHORE) == null) {
+             Integer maxConcurrent = getConfig(MAX_CONCURRENT_MACHINE_CREATIONS);
+             if (maxConcurrent == null || maxConcurrent < 1) {
+                 throw new IllegalStateException(MAX_CONCURRENT_MACHINE_CREATIONS.getName() + " must be >= 1, but was "+maxConcurrent);
+             }
+             config().set(MACHINE_CREATION_SEMAPHORE, new Semaphore(maxConcurrent, true));
+         }
+         return this;
+     }
+ 
+     @Override
+     public void init() {
+         super.init();
+         if ("aws-ec2".equals(getProvider())) {
+             addExtension(AvailabilityZoneExtension.class, new AwsAvailabilityZoneExtension(getManagementContext(), this));
+         }
+     }
+ 
+     @Override
+     public JcloudsLocation newSubLocation(Map<?,?> newFlags) {
+         return newSubLocation(getClass(), newFlags);
+     }
+ 
+     @Override
+     public JcloudsLocation newSubLocation(Class<? extends AbstractCloudMachineProvisioningLocation> type, Map<?,?> newFlags) {
+         // TODO should be able to use ConfigBag.newInstanceExtending; would require moving stuff around to api etc
+         return (JcloudsLocation) getManagementContext().getLocationManager().createLocation(LocationSpec.create(type)
+                 .parent(this)
+                 .configure(config().getLocalBag().getAllConfig())  // FIXME Should this just be inherited?
+                 .configure(MACHINE_CREATION_SEMAPHORE, getMachineCreationSemaphore())
+                 .configure(newFlags));
+     }
+ 
+     @Override
+     public String toString() {
+         Object identity = getIdentity();
+         String configDescription = config().getLocalBag().getDescription();
+         if (configDescription!=null && configDescription.startsWith(getClass().getSimpleName()))
+             return configDescription;
+         return getClass().getSimpleName()+"["+getDisplayName()+":"+(identity != null ? identity : null)+
+                 (configDescription!=null ? "/"+configDescription : "") + "@" + getId() + "]";
+     }
+ 
+     @Override
+     public String toVerboseString() {
+         return Objects.toStringHelper(this).omitNullValues()
+                 .add("id", getId()).add("name", getDisplayName()).add("identity", getIdentity())
+                 .add("description", config().getLocalBag().getDescription()).add("provider", getProvider())
+                 .add("region", getRegion()).add("endpoint", getEndpoint())
+                 .toString();
+     }
+ 
+     public String getProvider() {
+         return getConfig(CLOUD_PROVIDER);
+     }
+ 
+     public String getIdentity() {
+         return getConfig(ACCESS_IDENTITY);
+     }
+ 
+     public String getCredential() {
+         return getConfig(ACCESS_CREDENTIAL);
+     }
+ 
+     /** returns the location ID used by the provider, if set, e.g. us-west-1 */
+     public String getRegion() {
+         return getConfig(CLOUD_REGION_ID);
+     }
+ 
+     public String getEndpoint() {
+         return (String) config().getBag().getWithDeprecation(CLOUD_ENDPOINT, JCLOUDS_KEY_ENDPOINT);
+     }
+ 
+     public String getUser(ConfigBag config) {
+         return (String) config.getWithDeprecation(USER, JCLOUDS_KEY_USERNAME);
+     }
+ 
+     public boolean isWindows(Template template, ConfigBag config) {
+         return isWindows(template.getImage(), config);
+     }
+     
+     /**
+      * Whether VMs provisioned from this image will be Windows. Assume windows if the image
+      * explicitly says so, or if image does not tell us then fall back to whether the config 
+      * explicitly says windows in {@link JcloudsLocationConfig#OS_FAMILY}.
+      * 
+      * Will first look at {@link JcloudsLocationConfig#OS_FAMILY_OVERRIDE}, to check if that 
+      * is set. If so, no further checks are done: the value is compared against {@link OsFamily#WINDOWS}.
+      * 
+      * We believe the config (e.g. from brooklyn.properties) because for some clouds there is 
+      * insufficient meta-data so the Image might not tell us. Thus a user can work around it
+      * by explicitly supplying configuration. 
+      */
+     public boolean isWindows(Image image, ConfigBag config) {
+         OsFamily override = config.get(OS_FAMILY_OVERRIDE);
+         if (override != null) return override == OsFamily.WINDOWS;
+         
+         OsFamily confFamily = config.get(OS_FAMILY);
+         OperatingSystem os = (image != null) ? image.getOperatingSystem() : null;
+         return (os != null && os.getFamily() != OsFamily.UNRECOGNIZED) 
+                 ? (OsFamily.WINDOWS == os.getFamily()) 
+                 : (OsFamily.WINDOWS == confFamily);
+     }
+ 
+     /**
+      * Whether the given VM is Windows.
+      * 
+      * @see {@link #isWindows(Image, ConfigBag)}
+      */
+     public boolean isWindows(NodeMetadata node, ConfigBag config) {
+         OsFamily override = config.get(OS_FAMILY_OVERRIDE);
+         if (override != null) return override == OsFamily.WINDOWS;
+         
+         OsFamily confFamily = config.get(OS_FAMILY);
+         OperatingSystem os = (node != null) ? node.getOperatingSystem() : null;
+         return (os != null && os.getFamily() != OsFamily.UNRECOGNIZED) 
+                 ? (OsFamily.WINDOWS == os.getFamily()) 
+                 : (OsFamily.WINDOWS == confFamily);
+     }
+ 
+     public boolean isLocationFirewalldEnabled(SshMachineLocation location) {
+         int result = location.execCommands("checking if firewalld is active", 
+                 ImmutableList.of(IptablesCommands.firewalldServiceIsActive()));
+         if (result == 0) {
+             return true;
+         }
+         
+         return false;
+     }
+     
+     protected Semaphore getMachineCreationSemaphore() {
+         return checkNotNull(getConfig(MACHINE_CREATION_SEMAPHORE), MACHINE_CREATION_SEMAPHORE.getName());
+     }
+ 
+     protected CloudMachineNamer getCloudMachineNamer(ConfigBag config) {
+         String namerClass = config.get(LocationConfigKeys.CLOUD_MACHINE_NAMER_CLASS);
+         if (Strings.isNonBlank(namerClass)) {
+             Optional<CloudMachineNamer> cloudNamer = Reflections.invokeConstructorWithArgs(getManagementContext().getCatalogClassLoader(), namerClass);
+             if (cloudNamer.isPresent()) {
+                 return cloudNamer.get();
+             } else {
+                 throw new IllegalStateException("Failed to create CloudMachineNamer "+namerClass+" for location "+this);
+             }
+         } else {
+             return new JcloudsMachineNamer();
+         }
+     }
+ 
+     protected Collection<JcloudsLocationCustomizer> getCustomizers(ConfigBag setup) {
+         @SuppressWarnings("deprecation")
+         JcloudsLocationCustomizer customizer = setup.get(JCLOUDS_LOCATION_CUSTOMIZER);
+         Collection<JcloudsLocationCustomizer> customizers = setup.get(JCLOUDS_LOCATION_CUSTOMIZERS);
+         @SuppressWarnings("deprecation")
+         String customizerType = setup.get(JCLOUDS_LOCATION_CUSTOMIZER_TYPE);
+         @SuppressWarnings("deprecation")
+         String customizersSupplierType = setup.get(JCLOUDS_LOCATION_CUSTOMIZERS_SUPPLIER_TYPE);
+ 
+         ClassLoader catalogClassLoader = getManagementContext().getCatalogClassLoader();
+         List<JcloudsLocationCustomizer> result = new ArrayList<JcloudsLocationCustomizer>();
+         if (customizer != null) result.add(customizer);
+         if (customizers != null) result.addAll(customizers);
+         if (Strings.isNonBlank(customizerType)) {
+             Optional<JcloudsLocationCustomizer> customizerByType = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizerType, setup);
+             if (customizerByType.isPresent()) {
+                 result.add(customizerByType.get());
+             } else {
+                 customizerByType = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizerType);
+                 if (customizerByType.isPresent()) {
+                     result.add(customizerByType.get());
+                 } else {
+                     throw new IllegalStateException("Failed to create JcloudsLocationCustomizer "+customizersSupplierType+" for location "+this);
+                 }
+             }
+         }
+         if (Strings.isNonBlank(customizersSupplierType)) {
+             Optional<Supplier<Collection<JcloudsLocationCustomizer>>> supplier = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizersSupplierType, setup);
+             if (supplier.isPresent()) {
+                 result.addAll(supplier.get().get());
+             } else {
+                 supplier = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizersSupplierType);
+                 if (supplier.isPresent()) {
+                     result.addAll(supplier.get().get());
+                 } else {
+                     throw new IllegalStateException("Failed to create JcloudsLocationCustomizer supplier "+customizersSupplierType+" for location "+this);
+                 }
+             }
+         }
+         return result;
+     }
+ 
+     protected Collection<MachineLocationCustomizer> getMachineCustomizers(ConfigBag setup) {
+         Collection<MachineLocationCustomizer> customizers = setup.get(MACHINE_LOCATION_CUSTOMIZERS);
+         return (customizers == null ? ImmutableList.<MachineLocationCustomizer>of() : customizers);
+     }
+ 
+     public void setDefaultImageId(String val) {
+         config().set(DEFAULT_IMAGE_ID, val);
+     }
+ 
+     // TODO remove tagMapping, or promote it
+     // (i think i favour removing it, letting the config come in from the entity)
+ 
+     public void setTagMapping(Map<String,Map<String, ? extends Object>> val) {
+         tagMapping.clear();
+         tagMapping.putAll(val);
+     }
+ 
+     // TODO Decide on semantics. If I give "TomcatServer" and "Ubuntu", then must I get back an image that matches both?
+     // Currently, just takes first match that it finds...
+     @Override
+     public Map<String,Object> getProvisioningFlags(Collection<String> tags) {
+         Map<String,Object> result = Maps.newLinkedHashMap();
+         Collection<String> unmatchedTags = Lists.newArrayList();
+         for (String it : tags) {
+             if (groovyTruth(tagMapping.get(it)) && !groovyTruth(result)) {
+                 result.putAll(tagMapping.get(it));
+             } else {
+                 unmatchedTags.add(it);
+             }
+         }
+         if (unmatchedTags.size() > 0) {
+             LOG.debug("Location {}, failed to match provisioning tags {}", this, unmatchedTags);
+         }
+         return result;
+     }
+ 
+     public static final Set<ConfigKey<?>> getAllSupportedProperties() {
+         Set<String> configsOnClass = Sets.newLinkedHashSet(
+             Iterables.transform(ConfigUtils.getStaticKeysOnClass(JcloudsLocation.class),
+                 new Function<HasConfigKey<?>,String>() {
+                     @Override @Nullable
+                     public String apply(@Nullable HasConfigKey<?> input) {
+                         return input.getConfigKey().getName();
+                     }
+                 }));
+         Set<ConfigKey<?>> configKeysInList = ImmutableSet.<ConfigKey<?>>builder()
+                 .addAll(SUPPORTED_TEMPLATE_BUILDER_PROPERTIES.keySet())
+                 .addAll(SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES.keySet())
+                 .build();
+         Set<String> configsInList = Sets.newLinkedHashSet(
+             Iterables.transform(configKeysInList,
+             new Function<ConfigKey<?>,String>() {
+                 @Override @Nullable
+                 public String apply(@Nullable ConfigKey<?> input) {
+                     return input.getName();
+                 }
+             }));
+ 
+         SetView<String> extrasInList = Sets.difference(configsInList, configsOnClass);
+         // notInList is normal
+         if (!extrasInList.isEmpty())
+             LOG.warn("JcloudsLocation supported properties differs from config defined on class: " + extrasInList);
+         return Collections.unmodifiableSet(configKeysInList);
+     }
+ 
+     public ComputeService getComputeService() {
+         return getComputeService(MutableMap.of());
+     }
+     public ComputeService getComputeService(Map<?,?> flags) {
+         ConfigBag conf = (flags==null || flags.isEmpty())
+                 ? config().getBag()
+                 : ConfigBag.newInstanceExtending(config().getBag(), flags);
+         return getComputeService(conf);
+     }
+ 
+     public ComputeService getComputeService(ConfigBag config) {
+         return getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(config, true);
+     }
+ 
+     /** @deprecated since 0.7.0 use {@link #listMachines()} */ @Deprecated
+     public Set<? extends ComputeMetadata> listNodes() {
+         return listNodes(MutableMap.of());
+     }
+     /** @deprecated since 0.7.0 use {@link #listMachines()}.
+      * (no support for custom compute service flags; if that is needed, we'll have to introduce a new method,
+      * but it seems there are no usages) */ @Deprecated
+     public Set<? extends ComputeMetadata> listNodes(Map<?,?> flags) {
+         return getComputeService(flags).listNodes();
+     }
+ 
+     @Override
+     public Map<String, MachineManagementMixins.MachineMetadata> listMachines() {
+         Set<? extends ComputeMetadata> nodes =
+             getRegion()!=null ? getComputeService().listNodesDetailsMatching(new NodeInLocation(getRegion(), true))
+                 : getComputeService().listNodes();
+         Map<String,MachineManagementMixins.MachineMetadata> result = new LinkedHashMap<String, MachineManagementMixins.MachineMetadata>();
+ 
+         for (ComputeMetadata node: nodes)
+             result.put(node.getId(), getMachineMetadata(node));
+ 
+         return result;
+     }
+ 
+     protected MachineManagementMixins.MachineMetadata getMachineMetadata(ComputeMetadata node) {
+         if (node==null)
+             return null;
+         return new BasicMachineMetadata(node.getId(), node.getName(),
+             ((node instanceof NodeMetadata) ? Iterators.tryFind( ((NodeMetadata)node).getPublicAddresses().iterator(), Predicates.alwaysTrue() ).orNull() : null),
+             ((node instanceof NodeMetadata) ? ((NodeMetadata)node).getStatus()==Status.RUNNING : null),
+             node);
+     }
+ 
+     @Override
+     public MachineManagementMixins.MachineMetadata getMachineMetadata(MachineLocation l) {
+         if (l instanceof JcloudsSshMachineLocation) {
+             return getMachineMetadata( ((JcloudsSshMachineLocation)l).node );
+         }
+         return null;
+     }
+ 
+     @Override
+     public void killMachine(String cloudServiceId) {
+         getComputeService().destroyNode(cloudServiceId);
+     }
+ 
+     @Override
+     public void killMachine(MachineLocation l) {
+         MachineManagementMixins.MachineMetadata m = getMachineMetadata(l);
+         if (m==null) throw new NoSuchElementException("Machine "+l+" is not known at "+this);
+         killMachine(m.getId());
+     }
+ 
+     /** attaches a string describing where something is being created
+      * (provider, region/location and/or endpoint, callerContext) */
+     protected void setCreationString(ConfigBag config) {
+         config.setDescription(elvis(config.get(CLOUD_PROVIDER), "unknown")+
+                 (config.containsKey(CLOUD_REGION_ID) ? ":"+config.get(CLOUD_REGION_ID) : "")+
+                 (config.containsKey(CLOUD_ENDPOINT) ? ":"+config.get(CLOUD_ENDPOINT) : "")+
+                 (config.containsKey(CALLER_CONTEXT) ? "@"+config.get(CALLER_CONTEXT) : ""));
+     }
+ 
+     // ----------------- obtaining a new machine ------------------------
+     public MachineLocation obtain() throws NoMachinesAvailableException {
+         return obtain(MutableMap.of());
+     }
+     public MachineLocation obtain(TemplateBuilder tb) throws NoMachinesAvailableException {
+         return obtain(MutableMap.of(), tb);
+     }
+     public MachineLocation obtain(Map<?,?> flags, TemplateBuilder tb) throws NoMachinesAvailableException {
+         return obtain(MutableMap.builder().putAll(flags).put(TEMPLATE_BUILDER, tb).build());
+     }
+ 
+     /** core method for obtaining a VM using jclouds;
+      * Map should contain CLOUD_PROVIDER and CLOUD_ENDPOINT or CLOUD_REGION, depending on the cloud,
+      * as well as ACCESS_IDENTITY and ACCESS_CREDENTIAL,
+      * plus any further properties to specify e.g. images, hardware profiles, accessing user
+      * (for initial login, and a user potentially to create for subsequent ie normal access) */
+     @Override
+     public MachineLocation obtain(Map<?,?> flags) throws NoMachinesAvailableException {
+         ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
+         Integer attempts = setup.get(MACHINE_CREATE_ATTEMPTS);
+         List<Exception> exceptions = Lists.newArrayList();
+         if (attempts == null || attempts < 1) attempts = 1;
+         for (int i = 1; i <= attempts; i++) {
+             try {
+                 return obtainOnce(setup);
+             } catch (RuntimeException e) {
+                 LOG.warn("Attempt #{}/{} to obtain machine threw error: {}", new Object[]{i, attempts, e});
+                 exceptions.add(e);
+             }
+         }
+         String msg = String.format("Failed to get VM after %d attempt%s.", attempts, attempts == 1 ? "" : "s");
+ 
+         Exception cause = (exceptions.size() == 1)
+                 ? exceptions.get(0)
+                 : new CompoundRuntimeException(msg + " - "
+                     + "First cause is "+exceptions.get(0)+" (listed in primary trace); "
+                     + "plus " + (exceptions.size()-1) + " more (e.g. the last is "+exceptions.get(exceptions.size()-1)+")",
+                     exceptions.get(0), exceptions);
+ 
+         if (exceptions.get(exceptions.size()-1) instanceof NoMachinesAvailableException) {
+             throw new NoMachinesAvailableException(msg, cause);
+         } else {
+             throw Exceptions.propagate(cause);
+         }
+     }
+ 
+     protected MachineLocation obtainOnce(ConfigBag setup) throws NoMachinesAvailableException {
+         AccessController.Response access = getManagementContext().getAccessController().canProvisionLocation(this);
+         if (!access.isAllowed()) {
+             throw new IllegalStateException("Access controller forbids provisioning in "+this+": "+access.getMsg());
+         }
+ 
+         setCreationString(setup);
+         boolean waitForSshable = !"false".equalsIgnoreCase(setup.get(WAIT_FOR_SSHABLE));
+         boolean waitForWinRmable = !"false".equalsIgnoreCase(setup.get(WAIT_FOR_WINRM_AVAILABLE));
+         boolean usePortForwarding = setup.get(USE_PORT_FORWARDING);
+         boolean skipJcloudsSshing = Boolean.FALSE.equals(setup.get(USE_JCLOUDS_SSH_INIT)) || usePortForwarding;
+         JcloudsPortForwarderExtension portForwarder = setup.get(PORT_FORWARDER);
+         if (usePortForwarding) checkNotNull(portForwarder, "portForwarder, when use-port-forwarding enabled");
+ 
+         final ComputeService computeService = getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(setup, true);
+         CloudMachineNamer cloudMachineNamer = getCloudMachineNamer(setup);
+         String groupId = elvis(setup.get(GROUP_ID), cloudMachineNamer.generateNewGroupId(setup));
+         NodeMetadata node = null;
+         JcloudsMachineLocation machineLocation = null;
+         Duration semaphoreTimestamp = null;
+         Duration templateTimestamp = null;
+         Duration provisionTimestamp = null;
+         Duration usableTimestamp = null;
+         Duration customizedTimestamp = null;
+         Stopwatch provisioningStopwatch = Stopwatch.createStarted();
+         
+         try {
+             LOG.info("Creating VM "+setup.getDescription()+" in "+this);
+ 
+             Semaphore machineCreationSemaphore = getMachineCreationSemaphore();
+             boolean acquired = machineCreationSemaphore.tryAcquire(0, TimeUnit.SECONDS);
+             if (!acquired) {
+                 LOG.info("Waiting in {} for machine-creation permit ({} other queuing requests already)", new Object[] {this, machineCreationSemaphore.getQueueLength()});
+                 Stopwatch blockStopwatch = Stopwatch.createStarted();
+                 machineCreationSemaphore.acquire();
+                 LOG.info("Acquired in {} machine-creation permit, after waiting {}", this, Time.makeTimeStringRounded(blockStopwatch));
+             } else {
+                 LOG.debug("Acquired in {} machine-creation permit immediately", this);
+             }
+             semaphoreTimestamp = Duration.of(provisioningStopwatch);
+ 
+             LoginCredentials userCredentials = null;
+             Set<? extends NodeMetadata> nodes;
+             Template template;
+             try {
+                 // Setup the template
+                 template = buildTemplate(computeService, setup);
+                 boolean expectWindows = isWindows(template, setup);
+                 if (!skipJcloudsSshing) {
+                     if (expectWindows) {
+                         // TODO Was this too early to look at template.getImage? e.g. customizeTemplate could subsequently modify it.
+                         LOG.warn("Ignoring invalid configuration for Windows provisioning of "+template.getImage()+": "+USE_JCLOUDS_SSH_INIT.getName()+" should be false");
+                         skipJcloudsSshing = true;
+                     } else if (waitForSshable) {
+                         userCredentials = initTemplateForCreateUser(template, setup);
+                     }
+                 }
+ 
+                 templateTimestamp = Duration.of(provisioningStopwatch);
+                 // "Name" metadata seems to set the display name; at least in AWS
+                 // TODO it would be nice if this salt comes from the location's ID (but we don't know that yet as the ssh machine location isn't created yet)
+                 // TODO in softlayer we want to control the suffix of the hostname which is 3 random hex digits
+                 template.getOptions().getUserMetadata().put("Name", cloudMachineNamer.generateNewMachineUniqueNameFromGroupId(setup, groupId));
+                 
+                 if (setup.get(JcloudsLocationConfig.INCLUDE_BROOKLYN_USER_METADATA)) {
+                     template.getOptions().getUserMetadata().put("brooklyn-user", System.getProperty("user.name"));
+                     
+                     Object context = setup.get(CALLER_CONTEXT);
+                     if (context instanceof Entity) {
+                         Entity entity = (Entity)context;
+                         template.getOptions().getUserMetadata().put("brooklyn-app-id", entity.getApplicationId());
+                         template.getOptions().getUserMetadata().put("brooklyn-app-name", entity.getApplication().getDisplayName());
+                         template.getOptions().getUserMetadata().put("brooklyn-entity-id", entity.getId());
+                         template.getOptions().getUserMetadata().put("brooklyn-entity-name", entity.getDisplayName());
+                         template.getOptions().getUserMetadata().put("brooklyn-server-creation-date", Time.makeDateSimpleStampString());
+                     }
+                 }
+                 
+                 customizeTemplate(setup, computeService, template);
+                 
+                 LOG.debug("jclouds using template {} / options {} to provision machine in {}",
+                         new Object[] {template, template.getOptions(), setup.getDescription()});
+ 
+                 if (!setup.getUnusedConfig().isEmpty())
+                     if (LOG.isDebugEnabled())
+                         LOG.debug("NOTE: unused flags passed to obtain VM in "+setup.getDescription()+": "
+                                 + Sanitizer.sanitize(setup.getUnusedConfig()));
+                 
+                 nodes = computeService.createNodesInGroup(groupId, 1, template);
+                 provisionTimestamp = Duration.of(provisioningStopwatch);
+             } finally {
+                 machineCreationSemaphore.release();
+             }
+ 
+             node = Iterables.getOnlyElement(nodes, null);
+             LOG.debug("jclouds created {} for {}", node, setup.getDescription());
+             if (node == null)
+                 throw new IllegalStateException("No nodes returned by jclouds create-nodes in " + setup.getDescription());
+ 
+             boolean windows = isWindows(node, setup);
+             if (windows) {
+                 int newLoginPort = node.getLoginPort() == 22 ? 5985 : node.getLoginPort();
+                 String newLoginUser = "root".equals(node.getCredentials().getUser()) ? "Administrator" : node.getCredentials().getUser();
+                 LOG.debug("jclouds created Windows VM {}; transforming connection details: loginPort from {} to {}; loginUser from {} to {}", 
+                         new Object[] {node, node.getLoginPort(), newLoginPort, node.getCredentials().getUser(), newLoginUser});
+                 
+                 node = NodeMetadataBuilder.fromNodeMetadata(node)
+                         .loginPort(newLoginPort)
+                         .credentials(LoginCredentials.builder(node.getCredentials()).user(newLoginUser).build())
+                         .build();
+             }
+             // FIXME How do we influence the node.getLoginPort, so it is set correctly for Windows?
+             // Setup port-forwarding, if required
+             Optional<HostAndPort> sshHostAndPortOverride;
+             if (usePortForwarding) {
+                 sshHostAndPortOverride = Optional.of(portForwarder.openPortForwarding(
+                         node,
+                         node.getLoginPort(),
+                         Optional.<Integer>absent(),
+                         Protocol.TCP,
+                         Cidr.UNIVERSAL));
+             } else {
+                 sshHostAndPortOverride = Optional.absent();
+             }
+ 
+             LoginCredentials initialCredentials = node.getCredentials();
+             if (skipJcloudsSshing) {
+                 boolean waitForConnectable = (windows) ? waitForWinRmable : waitForSshable;
+                 if (waitForConnectable) {
+                     if (windows) {
+                         // TODO Does jclouds support any windows user setup?
+                         initialCredentials = waitForWinRmAvailable(computeService, node, sshHostAndPortOverride, setup);
+                     } else {
+                         initialCredentials = waitForSshable(computeService, node, sshHostAndPortOverride, setup);
+                     }
+                     userCredentials = createUser(computeService, node, sshHostAndPortOverride, initialCredentials, setup);
+                 }
+             }
+ 
+             // Figure out which login-credentials to use
+             LoginCredentials customCredentials = setup.get(CUSTOM_CREDENTIALS);
+             if (customCredentials != null) {
+                 userCredentials = customCredentials;
+                 //set userName and other data, from these credentials
+                 Object oldUsername = setup.put(USER, customCredentials.getUser());
+                 LOG.debug("node {} username {} / {} (customCredentials)", new Object[] { node, customCredentials.getUser(), oldUsername });
+                 if (customCredentials.getOptionalPassword().isPresent()) setup.put(PASSWORD, customCredentials.getOptionalPassword().get());
+                 if (customCredentials.getOptionalPrivateKey().isPresent()) setup.put(PRIVATE_KEY_DATA, customCredentials.getOptionalPrivateKey().get());
+             }
+             if (userCredentials == null || (!userCredentials.getOptionalPassword().isPresent() && !userCredentials.getOptionalPrivateKey().isPresent())) {
+                 // We either don't have any userCredentials, or it is missing both a password/key.
+                 // TODO See waitForSshable, which now handles if the node.getLoginCredentials has both a password+key
+                 userCredentials = extractVmCredentials(setup, node, initialCredentials);
+             }
+             if (userCredentials == null) {
+                 // TODO See waitForSshable, which now handles if the node.getLoginCredentials has both a password+key
+                 userCredentials = extractVmCredentials(setup, node, initialCredentials);
+             }
+             if (userCredentials != null) {
+                 node = NodeMetadataBuilder.fromNodeMetadata(node).credentials(userCredentials).build();
+             } else {
+                 // only happens if something broke above...
+                 userCredentials = LoginCredentials.fromCredentials(node.getCredentials());
+             }
+             // store the credentials, in case they have changed
+             setup.putIfNotNull(JcloudsLocationConfig.PASSWORD, userCredentials.getOptionalPassword().orNull());
+             setup.putIfNotNull(JcloudsLocationConfig.PRIVATE_KEY_DATA, userCredentials.getOptionalPrivateKey().orNull());
+ 
+             // Wait for the VM to be reachable over SSH
+             if (waitForSshable && !windows) {
+                 waitForSshable(computeService, node, sshHostAndPortOverride, ImmutableList.of(userCredentials), setup);
+             } else {
+                 LOG.debug("Skipping ssh check for {} ({}) due to config waitForSshable=false", node, setup.getDescription());
+             }
+             usableTimestamp = Duration.of(provisioningStopwatch);
+ 
+ //            JcloudsSshMachineLocation jcloudsSshMachineLocation = null;
+ //            WinRmMachineLocation winRmMachineLocation = null;
+             // Create a JcloudsSshMachineLocation, and register it
+             if (windows) {
+                 machineLocation = registerWinRmMachineLocation(computeService, node, userCredentials, sshHostAndPortOverride, setup);
+             } else {
 -                machineLocation = registerJcloudsSshMachineLocation(computeService, node, userCredentials, sshHostAndPortOverride, setup);
 -                if (template!=null && machineLocation.getTemplate()==null) {
 -                    ((JcloudsSshMachineLocation)machineLocation).template = template;
 -                }
++                machineLocation = registerJcloudsSshMachineLocation(computeService, node, Optional.fromNullable(template), userCredentials, sshHostAndPortOverride, setup);
+             }
+ 
+             if (usePortForwarding && sshHostAndPortOverride.isPresent()) {
+                 // Now that we have the sshMachineLocation, we can associate the port-forwarding address with it.
+                 PortForwardManager portForwardManager = setup.get(PORT_FORWARDING_MANAGER);
+                 if (portForwardManager != null) {
+                     portForwardManager.associate(node.getId(), sshHostAndPortOverride.get(), machineLocation, node.getLoginPort());
+                 } else {
+                     LOG.warn("No port-forward manager for {} so could not associate {} -> {} for {}",
+                             new Object[] {this, node.getLoginPort(), sshHostAndPortOverride, machineLocation});
+                 }
+             }
+ 
+             if ("docker".equals(this.getProvider())) {
+                 if (windows) {
+                     throw new UnsupportedOperationException("Docker not supported on Windows");
+                 }
+                 Map<Integer, Integer> portMappings = JcloudsUtil.dockerPortMappingsFor(this, node.getId());
+                 PortForwardManager portForwardManager = setup.get(PORT_FORWARDING_MANAGER);
+                 if (portForwardManager != null) {
+                     for(Integer containerPort : portMappings.keySet()) {
+                         Integer hostPort = portMappings.get(containerPort);
+                         String dockerHost = ((JcloudsSshMachineLocation)machineLocation).getSshHostAndPort().getHostText();
+                         portForwardManager.associate(node.getId(), HostAndPort.fromParts(dockerHost, hostPort), machineLocation, containerPort);
+                     }
+                 } else {
+                     LOG.warn("No port-forward manager for {} so could not associate docker port-mappings for {}",
+                             this, machineLocation);
+                 }
+             }
+ 
+             List<String> customisationForLogging = new ArrayList<String>();
+             // Apply same securityGroups rules to iptables, if iptables is running on the node
+             if (waitForSshable) {
+ 
+                 String setupScript = setup.get(JcloudsLocationConfig.CUSTOM_MACHINE_SETUP_SCRIPT_URL);
+                 List<String> setupScripts = setup.get(JcloudsLocationConfig.CUSTOM_MACHINE_SETUP_SCRIPT_URL_LIST);
+                 Collection<String> allScripts = new MutableList<String>().appendIfNotNull(setupScript).appendAll(setupScripts);
+                 for (String setupScriptItem : allScripts) {
+                     if (Strings.isNonBlank(setupScriptItem)) {
+                         customisationForLogging.add("custom setup script " + setupScriptItem);
+ 
+                         String setupVarsString = setup.get(JcloudsLocationConfig.CUSTOM_MACHINE_SETUP_SCRIPT_VARS);
+                         Map<String, String> substitutions = (setupVarsString != null)
+                                 ? Splitter.on(",").withKeyValueSeparator(":").split(setupVarsString)
+                                 : ImmutableMap.<String, String>of();
+                         String scriptContent = ResourceUtils.create(this).getResourceAsString(setupScriptItem);
+                         String script = TemplateProcessor.processTemplateContents(scriptContent, getManagementContext(), substitutions);
+                         if (windows) {
+                             ((WinRmMachineLocation)machineLocation).executeScript(ImmutableList.copyOf((script.replace("\r", "").split("\n"))));
+                         } else {
+                             ((SshMachineLocation)machineLocation).execCommands("Customizing node " + this, ImmutableList.of(script));
+                         }
+                     }
+                 }
+ 
+                 if (setup.get(JcloudsLocationConfig.MAP_DEV_RANDOM_TO_DEV_URANDOM)) {
+                     if (windows) {
+                         LOG.warn("Ignoring flag MAP_DEV_RANDOM_TO_DEV_URANDOM on Windows location {}", machineLocation);
+                     } else {
+                         customisationForLogging.add("point /dev/random to urandom");
+ 
+                         ((SshMachineLocation)machineLocation).execCommands("using urandom instead of random",
+                                 Arrays.asList("sudo mv /dev/random /dev/random-real", "sudo ln -s /dev/urandom /dev/random"));
+                     }
+                 }
+ 
+ 
+                 if (setup.get(GENERATE_HOSTNAME)) {
+                     if (windows) {
+                         // TODO: Generate Windows Hostname
+                         LOG.warn("Ignoring flag GENERATE_HOSTNAME on Windows location {}", machineLocation);
+                     } else {
+                         customisationForLogging.add("configure hostname");
+ 
+                         ((SshMachineLocation)machineLocation).execCommands("Generate hostname " + node.getName(),
+                                 Arrays.asList("sudo hostname " + node.getName(),
+                                         "sudo sed -i \"s/HOSTNAME=.*/HOSTNAME=" + node.getName() + "/g\" /etc/sysconfig/network",
+                                         "sudo bash -c \"echo 127.0.0.1   `hostname` >> /etc/hosts\"")
+                         );
+                     }
+                 }
+ 
+                 if (setup.get(OPEN_IPTABLES)) {
+                     if (windows) {
+                         LOG.warn("Ignoring DEPRECATED flag OPEN_IPTABLES on Windows location {}", machineLocation);
+                     } else {
+                         LOG.warn("Using DEPRECATED flag OPEN_IPTABLES (will not be supported in future versions) for {} at {}", machineLocation, this);
+                         
+                         @SuppressWarnings("unchecked")
+                         Iterable<Integer> inboundPorts = (Iterable<Integer>) setup.get(INBOUND_PORTS);
+ 
+                         if (inboundPorts == null || Iterables.isEmpty(inboundPorts)) {
+                             LOG.info("No ports to open in iptables (no inbound ports) for {} at {}", machineLocation, this);
+                         } else {
+                             customisationForLogging.add("open iptables");
+ 
+                             List<String> iptablesRules = Lists.newArrayList();
+ 
+                             if (isLocationFirewalldEnabled((SshMachineLocation)machineLocation)) {
+                                 for (Integer port : inboundPorts) {
+                                     iptablesRules.add(IptablesCommands.addFirewalldRule(Chain.INPUT, Protocol.TCP, port, Policy.ACCEPT));
+                                  }
+                             } else {
+                                 iptablesRules = createIptablesRulesForNetworkInterface(inboundPorts);
+                                 iptablesRules.add(IptablesCommands.saveIptablesRules());
+                             }
+                             List<String> batch = Lists.newArrayList();
+                             // Some entities, such as Riak (erlang based) have a huge range of ports, which leads to a script that
+                             // is too large to run (fails with a broken pipe). Batch the rules into batches of 50
+                             for (String rule : iptablesRules) {
+                                 batch.add(rule);
+                                 if (batch.size() == 50) {
+                                     ((SshMachineLocation)machineLocation).execCommands("Inserting iptables rules, 50 command batch", batch);
+                                     batch.clear();
+                                 }
+                             }
+                             if (batch.size() > 0) {
+                                 ((SshMachineLocation)machineLocation).execCommands("Inserting iptables rules", batch);
+                             }
+                             ((SshMachineLocation)machineLocation).execCommands("List iptables rules", ImmutableList.of(IptablesCommands.listIptablesRule()));
+                         }
+                     }
+                 }
+ 
+                 if (setup.get(STOP_IPTABLES)) {
+                     if (windows) {
+                         LOG.warn("Ignoring DEPRECATED flag OPEN_IPTABLES on Windows location {}", machineLocation);
+                     } else {
+                         LOG.warn("Using DEPRECATED flag STOP_IPTABLES (will not be supported in future versions) for {} at {}", machineLocation, this);
+                         
+                         customisationForLogging.add("stop iptables");
+ 
+                         List<String> cmds = ImmutableList.<String>of();
+                         if (isLocationFirewalldEnabled((SshMachineLocation)machineLocation)) {
+                             cmds = ImmutableList.of(IptablesCommands.firewalldServiceStop(), IptablesCommands.firewalldServiceStatus());
+                         } else {
+                             cmds = ImmutableList.of(IptablesCommands.iptablesServiceStop(), IptablesCommands.iptablesServiceStatus());
+                         }
+                         ((SshMachineLocation)machineLocation).execCommands("Stopping iptables", cmds);
+                     }
+                 }
+ 
+                 List<String> extraKeyUrlsToAuth = setup.get(EXTRA_PUBLIC_KEY_URLS_TO_AUTH);
+                 if (extraKeyUrlsToAuth!=null && !extraKeyUrlsToAuth.isEmpty()) {
+                     if (windows) {
+                         LOG.warn("Ignoring flag EXTRA_PUBLIC_KEY_URLS_TO_AUTH on Windows location", machineLocation);
+                     } else {
+                         List<String> extraKeyDataToAuth = MutableList.of();
+                         for (String keyUrl : extraKeyUrlsToAuth) {
+                             extraKeyDataToAuth.add(ResourceUtils.create().getResourceAsString(keyUrl));
+                         }
+                         ((SshMachineLocation)machineLocation).execCommands("Authorizing ssh keys",
+                                 ImmutableList.of(new AuthorizeRSAPublicKeys(extraKeyDataToAuth).render(org.jclouds.scriptbuilder.domain.OsFamily.UNIX)));
+                     }
+                 }
+ 
+             } else {
+                 // Otherwise we have deliberately not waited to be ssh'able, so don't try now to
+                 // ssh to exec these commands!
+             }
+ 
+             // Apply any optional app-specific customization.
+             for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
+                 LOG.debug("Customizing machine {}, using customizer {}", machineLocation, customizer);
+                 customizer.customize(this, computeService, machineLocation);
+             }
+             for (MachineLocationCustomizer customizer : getMachineCustomizers(setup)) {
+                 LOG.debug("Customizing machine {}, using customizer {}", machineLocation, customizer);
+                 customizer.customize(machineLocation);
+             }
+ 
+             customizedTimestamp = Duration.of(provisioningStopwatch);
+ 
+             try {
+                 String logMessage = "Finished VM "+setup.getDescription()+" creation:"
+                         + " "+machineLocation.getUser()+"@"+machineLocation.getAddress()+":"+machineLocation.getPort()
+                         + (Boolean.TRUE.equals(setup.get(LOG_CREDENTIALS))
+                                 ? "password=" + userCredentials.getOptionalPassword().or("<absent>")
+                                 + " && key=" + userCredentials.getOptionalPrivateKey().or("<absent>")
+                                 : "")
+                         + " ready after "+Duration.of(provisioningStopwatch).toStringRounded()
+                         + " ("
+                         + "semaphore obtained in "+Duration.of(semaphoreTimestamp).toStringRounded()+";"
+                         + template+" template built in "+Duration.of(templateTimestamp).subtract(semaphoreTimestamp).toStringRounded()+";"
+                         + " "+node+" provisioned in "+Duration.of(provisionTimestamp).subtract(templateTimestamp).toStringRounded()+";"
+                         + " "+machineLocation+" connection usable in "+Duration.of(usableTimestamp).subtract(provisionTimestamp).toStringRounded()+";"
+                         + " and os customized in "+Duration.of(customizedTimestamp).subtract(usableTimestamp).toStringRounded()+" - "+Joiner.on(", ").join(customisationForLogging)+")";
+                 LOG.info(logMessage);
+             } catch (Exception e){
+                 // TODO Remove try-catch! @Nakomis: why did you add it? What exception happened during logging?
+                 Exceptions.propagateIfFatal(e);
+                 LOG.warn("Problem generating log message summarising completion of jclouds machine provisioning "+machineLocation+" by "+this, e);
+             }
+ 
+             return machineLocation;
+             
+         } catch (Exception e) {
+             if (e instanceof RunNodesException && ((RunNodesException)e).getNodeErrors().size() > 0) {
+                 node = Iterables.get(((RunNodesException)e).getNodeErrors().keySet(), 0);
+             }
+             // sometimes AWS nodes come up busted (eg ssh not allowed); just throw it back (and maybe try for another one)
+             boolean destroyNode = (node != null) && Boolean.TRUE.equals(setup.get(DESTROY_ON_FAILURE));
+ 
+             if (e.toString().contains("VPCResourceNotSpecified")) {
+                 LOG.error("Detected that your EC2 account is a legacy 'classic' account, but the recommended instance type requires VPC. "
+                     + "You can specify the 'eu-central-1' region to avoid this problem, or you can specify a classic-compatible instance type, "
+                     + "or you can specify a subnet to use with 'networkName' "
+                     + "(taking care that the subnet auto-assigns public IP's and allows ingress on all ports, "
+                     + "as Brooklyn does not currently configure security groups for non-default VPC's; "
+                     + "or setting up Brooklyn to be in the subnet or have a jump host or other subnet access configuration). "
+                     + "For more information on VPC vs classic see http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-vpc.html.");
+             }
+             
+             LOG.error("Failed to start VM for "+setup.getDescription() + (destroyNode ? " (destroying)" : "")
+                     + (node != null ? "; node "+node : "")
+                     + " after "+Duration.of(provisioningStopwatch).toStringRounded()
+                     + (semaphoreTimestamp != null ? " ("
+                             + "semaphore obtained in "+Duration.of(semaphoreTimestamp).toStringRounded()+";"
+                             + (templateTimestamp != null && semaphoreTimestamp != null ? " template built in "+Duration.of(templateTimestamp).subtract(semaphoreTimestamp).toStringRounded()+";" : "")
+                             + (provisionTimestamp != null && templateTimestamp != null ? " node provisioned in "+Duration.of(provisionTimestamp).subtract(templateTimestamp).toStringRounded()+";" : "")
+                             + (usableTimestamp != null && provisioningStopwatch != null ? " connection usable in "+Duration.of(usableTimestamp).subtract(provisionTimestamp).toStringRounded()+";" : "")
+                             + (customizedTimestamp != null && usableTimestamp != null ? " and OS customized in "+Duration.of(customizedTimestamp).subtract(usableTimestamp).toStringRounded() : "")
+                             + ")"
+                             : "")
+                     + ": "+e.getMessage());
+             LOG.debug(Throwables.getStackTraceAsString(e));
+ 
+             if (destroyNode) {
+                 Stopwatch destroyingStopwatch = Stopwatch.createStarted();
+                 if (machineLocation != null) {
+                     releaseSafely(machineLocation);
+                 } else {
+                     releaseNodeSafely(node);
+                 }
+                 LOG.info("Destroyed " + (machineLocation != null ? "machine " + machineLocation : "node " + node)
+                         + " in " + Duration.of(destroyingStopwatch).toStringRounded());
+             }
+ 
+             throw Exceptions.propagate(e);
+         }
+     }
+ 
+     // ------------- suspend and resume ------------------------------------
+ 
+     /**
+      * Suspends the given location.
+      * <p>
+      * Note that this method does <b>not</b> call the lifecycle methods of any
+      * {@link #getCustomizers(ConfigBag) customizers} attached to this location.
+      */
+     @Override
+     public void suspendMachine(MachineLocation rawLocation) {
+         String instanceId = vmInstanceIds.remove(rawLocation);
+         if (instanceId == null) {
+             LOG.info("Attempt to suspend unknown machine " + rawLocation + " in " + this);
+             throw new IllegalArgumentException("Unknown machine " + rawLocation);
+         }
+         LOG.info("Suspending machine {} in {}, instance id {}", new Object[]{rawLocation, this, instanceId});
+         Exception toThrow = null;
+         try {
+             getComputeService().suspendNode(instanceId);
+         } catch (Exception e) {
+             toThrow = e;
+             LOG.error("Problem suspending machine " + rawLocation + " in " + this + ", instance id " + instanceId, e);
+         }
+         removeChild(rawLocation);
+         if (toThrow != null) {
+             throw Exceptions.propagate(toThrow);
+         }
+     }
+ 
+     /**
+      * Brings an existing machine with the given details under management.
+      * <p/>
+      * Note that this method does <b>not</b> call the lifecycle methods of any
+      * {@link #getCustomizers(ConfigBag) customizers} attached to this location.
+      *
+      * @param flags See {@link #registerMachine(ConfigBag)} for a description of required fields.
+      * @see #registerMachine(ConfigBag)
+      */
+     @Override
+     public MachineLocation resumeMachine(Map<?, ?> flags) {
+         ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
+         LOG.info("{} using resuming node matching properties: {}", this, Sanitizer.sanitize(setup));
+         ComputeService computeService = getComputeService(setup);
+         NodeMetadata node = findNodeOrThrow(setup);
+         LOG.debug("{} resuming {}", this, node);
+         computeService.resumeNode(node.getId());
+         // Load the node a second time once it is resumed to get an object with
+         // hostname and addresses populated.
+         node = findNodeOrThrow(setup);
+         LOG.debug("{} resumed {}", this, node);
+         MachineLocation registered = registerMachineLocation(setup, node);
+         LOG.info("{} resumed and registered {}", this, registered);
+         return registered;
+     }
+ 
+     // ------------- constructing the template, etc ------------------------
+ 
+     private static interface CustomizeTemplateBuilder {
+         void apply(TemplateBuilder tb, ConfigBag props, Object v);
+     }
+ 
+     public static interface CustomizeTemplateOptions {
+         void apply(TemplateOptions tb, ConfigBag props, Object v);
+     }
+ 
+     /** properties which cause customization of the TemplateBuilder */
+     public static final Map<ConfigKey<?>,CustomizeTemplateBuilder> SUPPORTED_TEMPLATE_BUILDER_PROPERTIES = ImmutableMap.<ConfigKey<?>,CustomizeTemplateBuilder>builder()
+             .put(OS_64_BIT, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         Boolean os64Bit = TypeCoercions.coerce(v, Boolean.class);
+                         if (os64Bit!=null)
+                             tb.os64Bit(os64Bit);
+                     }})
+             .put(MIN_RAM, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         tb.minRam( (int)(ByteSizeStrings.parse(Strings.toString(v), "mb")/1000/1000) );
+                     }})
+             .put(MIN_CORES, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         tb.minCores(TypeCoercions.coerce(v, Double.class));
+                     }})
+             .put(MIN_DISK, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         tb.minDisk( (int)(ByteSizeStrings.parse(Strings.toString(v), "gb")/1000/1000/1000) );
+                     }})
+             .put(HARDWARE_ID, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         tb.hardwareId(((CharSequence)v).toString());
+                     }})
+             .put(IMAGE_ID, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         tb.imageId(((CharSequence)v).toString());
+                     }})
+             .put(IMAGE_DESCRIPTION_REGEX, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         tb.imageDescriptionMatches(((CharSequence)v).toString());
+                     }})
+             .put(IMAGE_NAME_REGEX, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         tb.imageNameMatches(((CharSequence)v).toString());
+                     }})
+             .put(OS_FAMILY, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         Maybe<OsFamily> osFamily = Enums.valueOfIgnoreCase(OsFamily.class, v.toString());
+                         if (osFamily.isAbsent())
+                             throw new IllegalArgumentException("Invalid "+OS_FAMILY+" value "+v);
+                         tb.osFamily(osFamily.get());
+                     }})
+             .put(OS_VERSION_REGEX, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         tb.osVersionMatches( ((CharSequence)v).toString() );
+                     }})
+             .put(TEMPLATE_SPEC, new CustomizeTemplateBuilder() {
+                 public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         tb.from(TemplateBuilderSpec.parse(((CharSequence)v).toString()));
+                     }})
+             .put(DEFAULT_IMAGE_ID, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         /* done in the code, but included here so that it is in the map */
+                     }})
+             .put(TEMPLATE_BUILDER, new CustomizeTemplateBuilder() {
+                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                         /* done in the code, but included here so that it is in the map */
+                     }})
+             .build();
+ 
+     /** properties which cause customization of the TemplateOptions */
+     public static final Map<ConfigKey<?>,CustomizeTemplateOptions> SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES = ImmutableMap.<ConfigKey<?>,CustomizeTemplateOptions>builder()
+             .put(SECURITY_GROUPS, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (t instanceof EC2TemplateOptions) {
+                             String[] securityGroups = toStringArray(v);
+                             ((EC2TemplateOptions)t).securityGroups(securityGroups);
+                         } else if (t instanceof NovaTemplateOptions) {
+                             String[] securityGroups = toStringArray(v);
+                             ((NovaTemplateOptions)t).securityGroups(securityGroups);
+                         } else if (t instanceof SoftLayerTemplateOptions) {
+                             String[] securityGroups = toStringArray(v);
+                             ((SoftLayerTemplateOptions)t).securityGroups(securityGroups);
+                         } else if (t instanceof GoogleComputeEngineTemplateOptions) {
+                             String[] securityGroups = toStringArray(v);
+                             ((GoogleComputeEngineTemplateOptions)t).securityGroups(securityGroups);
+                         } else {
+                             LOG.info("ignoring securityGroups({}) in VM creation because not supported for cloud/type ({})", v, t.getClass());
+                         }
+                     }})
+             .put(INBOUND_PORTS, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         int[] inboundPorts = toIntPortArray(v);
+                         if (LOG.isDebugEnabled()) LOG.debug("opening inbound ports {} for cloud/type {}", Arrays.toString(inboundPorts), t.getClass());
+                         t.inboundPorts(inboundPorts);
+                     }})
+             .put(USER_METADATA_STRING, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (t instanceof EC2TemplateOptions) {
+                             // See AWS docs: http://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/UsingConfig_WinAMI.html#user-data-execution
+                             if (v==null) return;
+                             String data = v.toString();
+                             if (!(data.startsWith("<script>") || data.startsWith("<powershell>"))) {
+                                 data = "<script> " + data + " </script>";
+                             }
+                             ((EC2TemplateOptions)t).userData(data.getBytes());
+                         } else if (t instanceof SoftLayerTemplateOptions) {
+                             ((SoftLayerTemplateOptions)t).userData(Strings.toString(v));
+                         } else {
+                             // Try reflection: userData(String), or guestCustomizationScript(String);
+                             // the latter is used by vCloud Director.
+                             Class<? extends TemplateOptions> clazz = t.getClass();
+                             Method userDataMethod = null;
+                             try {
+                                 userDataMethod = clazz.getMethod("userData", String.class);
+                             } catch (SecurityException e) {
+                                 LOG.info("Problem reflectively inspecting methods of "+t.getClass()+" for setting userData", e);
+                             } catch (NoSuchMethodException e) {
+                                 try {
+                                     // For vCloud Director
+                                     userDataMethod = clazz.getMethod("guestCustomizationScript", String.class);
+                                 } catch (NoSuchMethodException e2) {
+                                     // expected on various other clouds
+                                 }
+                             }
+                             if (userDataMethod != null) {
+                                 try {
+                                     userDataMethod.invoke(t, Strings.toString(v));
+                                 } catch (InvocationTargetException e) {
+                                     LOG.info("Problem invoking "+userDataMethod.getName()+" of "+t.getClass()+", for setting userData (rethrowing)", e);
+                                     throw Exceptions.propagate(e);
+                                 } catch (IllegalAccessException e) {
+                                     LOG.debug("Unable to reflectively invoke "+userDataMethod.getName()+" of "+t.getClass()+", for setting userData (rethrowing)", e);
+                                     throw Exceptions.propagate(e);
+                                 }
+                             } else {
+                                 LOG.info("ignoring userDataString({}) in VM creation because not supported for cloud/type ({})", v, t.getClass());
+                             }
+                         }
+                     }})
+             .put(USER_DATA_UUENCODED, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (t instanceof EC2TemplateOptions) {
+                             byte[] bytes = toByteArray(v);
+                             ((EC2TemplateOptions)t).userData(bytes);
+                         } else if (t instanceof SoftLayerTemplateOptions) {
+                             ((SoftLayerTemplateOptions)t).userData(Strings.toString(v));
+                         } else {
+                             LOG.info("ignoring userData({}) in VM creation because not supported for cloud/type ({})", v, t.getClass());
+                         }
+                     }})
+             .put(STRING_TAGS, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         List<String> tags = toListOfStrings(v);
+                         if (LOG.isDebugEnabled()) LOG.debug("setting VM tags {} for {}", tags, t);
+                         t.tags(tags);
+                     }})
+             .put(USER_METADATA_MAP, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (v != null) {
+                             t.userMetadata(toMapStringString(v));
+                         }
+                     }})
+             .put(EXTRA_PUBLIC_KEY_DATA_TO_AUTH, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (t instanceof GoogleComputeEngineTemplateOptions) {
+                             // see email to jclouds list, 29 Aug 2015; 
+                             // GCE takes this to be the only login public key, 
+                             // and setting this only works if you also overrideLoginPrivateKey
+                             LOG.warn("Ignoring "+EXTRA_PUBLIC_KEY_DATA_TO_AUTH+"; not supported in jclouds-gce implementation.");
+                         }
+                         t.authorizePublicKey(((CharSequence)v).toString());
+                     }})
+             .put(RUN_AS_ROOT, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         t.runAsRoot((Boolean)v);
+                     }})
+             .put(LOGIN_USER, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (v != null) {
+                             t.overrideLoginUser(((CharSequence)v).toString());
+                         }
+                     }})
+             .put(LOGIN_USER_PASSWORD, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (v != null) {
+                             t.overrideLoginPassword(((CharSequence)v).toString());
+                         }
+                     }})
+             .put(LOGIN_USER_PRIVATE_KEY_FILE, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (v != null) {
+                             String privateKeyFileName = ((CharSequence)v).toString();
+                             String privateKey;
+                             try {
+                                 privateKey = Files.toString(new File(Os.tidyPath(privateKeyFileName)), Charsets.UTF_8);
+                             } catch (IOException e) {
+                                 LOG.error(privateKeyFileName + "not found", e);
+                                 throw Exceptions.propagate(e);
+                             }
+                             t.overrideLoginPrivateKey(privateKey);
+                         }
+                     }})
+             .put(LOGIN_USER_PRIVATE_KEY_DATA, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (v != null) {
+                             t.overrideLoginPrivateKey(((CharSequence)v).toString());
+                         }
+                     }})
+             .put(KEY_PAIR, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (t instanceof EC2TemplateOptions) {
+                             ((EC2TemplateOptions)t).keyPair(((CharSequence)v).toString());
+                         } else if (t instanceof NovaTemplateOptions) {
+                             ((NovaTemplateOptions)t).keyPairName(((CharSequence)v).toString());
+                         } else if (t instanceof CloudStackTemplateOptions) {
+                             ((CloudStackTemplateOptions) t).keyPair(((CharSequence) v).toString());
+                         } else {
+                             LOG.info("ignoring keyPair({}) in VM creation because not supported for cloud/type ({})", v, t);
+                         }
+                     }})
+             .put(AUTO_GENERATE_KEYPAIRS, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (t instanceof NovaTemplateOptions) {
+                             ((NovaTemplateOptions)t).generateKeyPair((Boolean)v);
+                         } else if (t instanceof CloudStackTemplateOptions) {
+                             ((CloudStackTemplateOptions) t).generateKeyPair((Boolean) v);
+                         } else {
+                             LOG.info("ignoring auto-generate-keypairs({}) in VM creation because not supported for cloud/type ({})", v, t);
+                         }
+                     }})
+             .put(AUTO_CREATE_FLOATING_IPS, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (t instanceof NovaTemplateOptions) {
+                             ((NovaTemplateOptions)t).autoAssignFloatingIp((Boolean)v);
+                         } else {
+                             LOG.info("ignoring auto-generate-floating-ips({}) in VM creation because not supported for cloud/type ({})", v, t);
+                         }
+                     }})
+             .put(AUTO_ASSIGN_FLOATING_IP, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (t instanceof NovaTemplateOptions) {
+                             ((NovaTemplateOptions)t).autoAssignFloatingIp((Boolean)v);
+                         } else if (t instanceof CloudStackTemplateOptions) {
+                             ((CloudStackTemplateOptions)t).setupStaticNat((Boolean)v);
+                         } else {
+                             LOG.info("ignoring auto-assign-floating-ip({}) in VM creation because not supported for cloud/type ({})", v, t);
+                         }
+                     }})
+             .put(NETWORK_NAME, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (t instanceof AWSEC2TemplateOptions) {
+                             // subnet ID is the sensible interpretation of network name in EC2
+                             ((AWSEC2TemplateOptions)t).subnetId((String)v);
+                             
+                         } else {
+                             if (t instanceof GoogleComputeEngineTemplateOptions) {
+                                 // no warning needed
+                                 // we think this is the only jclouds endpoint which supports this option
+                                 
+                             } else if (t instanceof SoftLayerTemplateOptions) {
+                                 LOG.warn("networkName is not be supported in SoftLayer; use `templateOptions` with `primaryNetworkComponentNetworkVlanId` or `primaryNetworkBackendComponentNetworkVlanId`");
+                             } else if (!(t instanceof CloudStackTemplateOptions) && !(t instanceof NovaTemplateOptions)) {
+                                 LOG.warn("networkName is experimental in many jclouds endpoints may not be supported in this cloud");
+                                 // NB, from @andreaturli
+ //                                Cloudstack uses custom securityGroupIds and networkIds not the generic networks
+ //                                Openstack Nova uses securityGroupNames which is marked as @deprecated (suggests to use groups which is maybe even more confusing)
+ //                                Azure supports the custom networkSecurityGroupName
+                             }
+                             
+                             t.networks((String)v);
+                         }
+                     }})
+             .put(DOMAIN_NAME, new CustomizeTemplateOptions() {
+                     public void apply(TemplateOptions t, ConfigBag props, Object v) {
+                         if (t instanceof SoftLayerTemplateOptions) {
+                             ((SoftLayerTemplateOptions)t).domainName(TypeCoercions.coerce(v, String.class));
+                         } else {
+                             LOG.info("ignoring domain-name({}) in VM creation because not supported for cloud/type ({})", v, t);                            
+                         }
+                     }})
+             .put(TEMPLATE_OPTIONS, new CustomizeTemplateOptions() {
+                 @Override
+                 public void apply(TemplateOptions options, ConfigBag config, Object v) {
+                     if (v == null) return;
+                     @SuppressWarnings("unchecked") Map<String, Object> optionsMap = (Map<String, Object>) v;
+                     if (optionsMap.isEmpty()) return;
+ 
+                     Class<? extends TemplateOptions> clazz = options.getClass();
+                     for(final Map.Entry<String, Object> option : optionsMap.entrySet()) {
+                         Maybe<?> result = MethodCoercions.tryFindAndInvokeBestMatchingMethod(options, option.getKey(), option.getValue());
+                         if(result.isAbsent()) {
+                             LOG.warn("Ignoring request to set template option {} because this is not supported by {}", new Object[] { option.getKey(), clazz.getCanonicalName() });
+                         }
+                     }
+                 }})
+             .build();
+ 
+     /** hook whereby template customizations can be made for various clouds */
+     protected void customizeTemplate(ConfigBag setup, ComputeService computeService, Template template) {
+         for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
+             customizer.customize(this, computeService, template);
+             customizer.customize(this, computeService, template.getOptions());
+         }
+ 
+         // these things are nice on softlayer
+         if (template.getOptions() instanceof SoftLayerTemplateOptions) {
+             SoftLayerTemplateOptions slT = ((SoftLayerTemplateOptions)template.getOptions());
+             if (Strings.isBlank(slT.getDomainName()) || "jclouds.org".equals(slT.getDomainName())) {
+                 // set a quasi-sensible domain name if none was provided (better than the default, jclouds.org)
+                 // NB: things like brooklyn.local are disallowed
+                 slT.domainName("local.brooklyncentral.org");
+             }
+             // convert user metadata to tags and notes because user metadata is otherwise ignored
+             Map<String, String> md = slT.getUserMetadata();
+             if (md!=null && !md.isEmpty()) {
+                 Set<String> tags = MutableSet.copyOf(slT.getTags());
+                 for (Map.Entry<String,String> entry: md.entrySet()) {
+                     tags.add(AbstractCloudMachineNamer.sanitize(entry.getKey())+":"+AbstractCloudMachineNamer.sanitize(entry.getValue()));
+                 }
+                 slT.tags(tags);
+ 
+                 if (!md.containsKey("notes")) {
+                     String notes = "User Metadata\n=============\n\n  * " + Joiner.on("\n  * ").withKeyValueSeparator(": ").join(md);
+                     if (notes.length() > NOTES_MAX_LENGTH) {
+                         String truncatedMsg = "...\n<truncated - notes total length is " + notes.length() + " characters>";
+                         notes = notes.substring(0, NOTES_MAX_LENGTH - truncatedMsg.length()) + truncatedMsg;
+                     }
+                     md.put("notes", notes);
+                 }
+             }
+         }
+     }
+ 
+     /** returns the jclouds Template which describes the image to be built, for the given config and compute service */
+     public Template buildTemplate(ComputeService computeService, ConfigBag config) {
+         TemplateBuilder templateBuilder = (TemplateBuilder) config.get(TEMPLATE_BUILDER);
+         if (templateBuilder==null) {
+             templateBuilder = new PortableTemplateBuilder<PortableTemplateBuilder<?>>();
+         } else {
+             LOG.debug("jclouds using templateBuilder {} as custom base for provisioning in {} for {}", new Object[] {
+                     templateBuilder, this, config.getDescription()});
+         }
+         if (templateBuilder instanceof PortableTemplateBuilder<?>) {
+             if (((PortableTemplateBuilder<?>)templateBuilder).imageChooser()==null) {
+                 Function<Iterable<? extends Image>, Image> chooser = config.get(JcloudsLocationConfig.IMAGE_CHOOSER);
+                 chooser = BrooklynImageChooser.cloneFor(chooser, computeService);
+                 templateBuilder.imageChooser(chooser);
+             } else {
+                 // an image chooser is already set, so do nothing
+             }
+         } else {
+             // template builder supplied, and we cannot check image chooser status; warn, for now
+             LOG.warn("Cannot check imageChooser status for {} due to manually supplied black-box TemplateBuilder; "
+                 + "it is recommended to use a PortableTemplateBuilder if you supply a TemplateBuilder", config.getDescription());
+         }
+ 
+         if (!Strings.isEmpty(config.get(CLOUD_REGION_ID))) {
+             templateBuilder.locationId(config.get(CLOUD_REGION_ID));
+         }
+ 
+         // Apply the template builder and options properties
+         for (Map.Entry<ConfigKey<?>, CustomizeTemplateBuilder> entry : SUPPORTED_TEMPLATE_BUILDER_PROPERTIES.entrySet()) {
+             ConfigKey<?> name = entry.getKey();
+             CustomizeTemplateBuilder code = entry.getValue();
+             if (config.containsKey(name))
+                 code.apply(templateBuilder, config, config.get(name));
+         }
+ 
+         if (templateBuilder instanceof PortableTemplateBuilder) {
+             ((PortableTemplateBuilder<?>)templateBuilder).attachComputeService(computeService);
+             // do the default last, and only if nothing else specified (guaranteed to be a PTB if nothing else specified)
+             if (groovyTruth(config.get(DEFAULT_IMAGE_ID))) {
+                 if (((PortableTemplateBuilder<?>)templateBuilder).isBlank()) {
+                     templateBuilder.imageId(config.get(DEFAULT_IMAGE_ID).toString());
+                 }
+             }
+         }
+ 
+         // Then apply any optional app-specific customization.
+         for (JcloudsLocationCustomizer customizer : getCustomizers(config)) {
+             customizer.customize(this, computeService, templateBuilder);
+         }
+ 
+         LOG.debug("jclouds using templateBuilder {} for provisioning in {} for {}", new Object[] {
+             templateBuilder, this, config.getDescription()});
+ 
+         // Finally try to build the template
+         Template template;
+         Image image;
+         try {
+     

<TRUNCATED>