http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java new file mode 100644 index 0000000..d355721 --- /dev/null +++ b/locations/jclouds/src/main/java/org/apache/brooklyn/location/jclouds/JcloudsLocation.java @@ -0,0 +1,2861 @@ +/* + * 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 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.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.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.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.management.AccessController; +import org.apache.brooklyn.location.MachineLocationCustomizer; +import org.apache.brooklyn.location.MachineManagementMixins; +import org.apache.brooklyn.location.NoMachinesAvailableException; +import org.apache.brooklyn.location.access.PortMapping; +import org.apache.brooklyn.location.basic.AbstractLocation; +import org.apache.brooklyn.location.cloud.AbstractCloudMachineProvisioningLocation; +import org.apache.brooklyn.location.cloud.names.AbstractCloudMachineNamer; +import org.apache.brooklyn.location.jclouds.networking.JcloudsPortForwarderExtension; +import org.apache.brooklyn.location.jclouds.zone.AwsAvailabilityZoneExtension; +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.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.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.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 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 brooklyn.config.ConfigKey; +import brooklyn.config.ConfigKey.HasConfigKey; +import brooklyn.config.ConfigUtils; +import brooklyn.entity.basic.Sanitizer; +import brooklyn.entity.rebind.persister.LocationWithObjectStore; +import brooklyn.entity.rebind.persister.PersistenceObjectStore; +import brooklyn.entity.rebind.persister.jclouds.JcloudsBlobStoreBasedObjectStore; +import org.apache.brooklyn.location.LocationSpec; +import org.apache.brooklyn.location.MachineLocation; +import org.apache.brooklyn.location.access.PortForwardManager; +import org.apache.brooklyn.location.basic.BasicMachineMetadata; +import org.apache.brooklyn.location.basic.LocationConfigKeys; +import org.apache.brooklyn.location.basic.LocationConfigUtils; +import org.apache.brooklyn.location.basic.LocationConfigUtils.OsCredential; +import org.apache.brooklyn.location.basic.SshMachineLocation; +import org.apache.brooklyn.location.basic.WinRmMachineLocation; +import org.apache.brooklyn.location.cloud.AvailabilityZoneExtension; +import org.apache.brooklyn.location.cloud.names.CloudMachineNamer; +import org.apache.brooklyn.location.jclouds.JcloudsPredicates.NodeInLocation; +import org.apache.brooklyn.location.jclouds.templates.PortableTemplateBuilder; +import brooklyn.util.ResourceUtils; +import brooklyn.util.collections.MutableList; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.collections.MutableSet; +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.MethodCoercions; +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 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.SuspendsMachines { + + // 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 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, 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; + + 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); + 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()) + LOG.debug("NOTE: unused flags passed to obtain VM in "+setup.getDescription()+": "+ + 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(); + } + + if (skipJcloudsSshing) { + boolean waitForConnectable = (windows) ? waitForWinRmable : waitForSshable; + if (waitForConnectable) { + if (windows) { + // TODO Does jclouds support any windows user setup? + waitForWinRmAvailable(computeService, node, sshHostAndPortOverride, node.getCredentials(), setup); + } else { + waitForSshable(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 && !windows) { + waitForSshable(computeService, node, sshHostAndPortOverride, 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; + } + } + + 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)) { + customizer.customize(this, computeService, machineLocation); + } + for (MachineLocationCustomizer customizer : getMachineCustomizers(setup)) { + 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() + + " ("+template+" template built in "+Duration.of(templateTimestamp).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 {}{}: {}", + new Object[] {setup.getDescription(), (destroyNode ? " (destroying "+node+")" : ""), e.getMessage()}); + LOG.debug(Throwables.getStackTraceAsString(e)); + + if (destroyNode) { + if (machineLocation != null) { + releaseSafely(machineLocation); + } else { + releaseNodeSafely(node); + } + } + + 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); + } + } + + // ------------- 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) { + // 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) { + t.authorizePublicKey(((CharSequence)v).toString()); + }}) + .put(RUN_AS_ROOT, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + t.runAsRoot((Boolean)v); + }}) + .put(LOGIN_USER, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + if (v != null) { + t.overrideLoginUser(((CharSequence)v).toString()); + } + }}) + .put(LOGIN_USER_PASSWORD, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + if (v != null) { + t.overrideLoginPassword(((CharSequence)v).toString()); + } + }}) + .put(LOGIN_USER_PRIVATE_KEY_FILE, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + if (v != null) { + String privateKeyFileName = ((CharSequence)v).toString(); + String privateKey; + try { + privateKey = Files.toString(new File(Os.tidyPath(privateKeyFileName)), Charsets.UTF_8); + } catch (IOException e) { + LOG.error(privateKeyFileName + "not found", e); + throw Exceptions.propagate(e); + } + t.overrideLoginPrivateKey(privateKey); + } + }}) + .put(LOGIN_USER_PRIVATE_KEY_DATA, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + if (v != null) { + t.overrideLoginPrivateKey(((CharSequence)v).toString()); + } + }}) + .put(KEY_PAIR, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + if (t instanceof EC2TemplateOptions) { + ((EC2TemplateOptions)t).keyPair(((CharSequence)v).toString()); + } else if (t instanceof NovaTemplateOptions) { + ((NovaTemplateOptions)t).keyPairName(((CharSequence)v).toString()); + } else if (t instanceof CloudStackTemplateOptions) { + ((CloudStackTemplateOptions) t).keyPair(((CharSequence) v).toString()); + } else { + LOG.info("ignoring keyPair({}) in VM creation because not supported for cloud/type ({})", v, t); + } + }}) + .put(AUTO_GENERATE_KEYPAIRS, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + if (t instanceof NovaTemplateOptions) { + ((NovaTemplateOptions)t).generateKeyPair((Boolean)v); + } else if (t instanceof CloudStackTemplateOptions) { + ((CloudStackTemplateOptions) t).generateKeyPair((Boolean) v); + } else { + LOG.info("ignoring auto-generate-keypairs({}) in VM creation because not supported for cloud/type ({})", v, t); + } + }}) + .put(AUTO_CREATE_FLOATING_IPS, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + if (t instanceof NovaTemplateOptions) { + ((NovaTemplateOptions)t).autoAssignFloatingIp((Boolean)v); + } else { + LOG.info("ignoring auto-generate-floating-ips({}) in VM creation because not supported for cloud/type ({})", v, t); + } + }}) + .put(AUTO_ASSIGN_FLOATING_IP, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + if (t instanceof NovaTemplateOptions) { + ((NovaTemplateOptions)t).autoAssignFloatingIp((Boolean)v); + } else if (t instanceof CloudStackTemplateOptions) { + ((CloudStackTemplateOptions)t).setupStaticNat((Boolean)v); + } else { + LOG.info("ignoring auto-assign-floating-ip({}) in VM creation because not supported for cloud/type ({})", v, t); + } + }}) + .put(NETWORK_NAME, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + if (t instanceof AWSEC2TemplateOptions) { + // subnet ID is the sensible interpretation of network name in EC2 + ((AWSEC2TemplateOptions)t).subnetId((String)v); + + } else { + if (t instanceof GoogleComputeEngineTemplateOptions) { + // no warning needed + // we think this is the only jclouds endpoint which supports this option + + } else if (t instanceof SoftLayerTemplateOptions) { + LOG.warn("networkName is not be supported in SoftLayer; use `templateOptions` with `primaryNetworkComponentNetworkVlanId` or `primaryNetworkBackendComponentNetworkVlanId`"); + } else if (!(t instanceof CloudStackTemplateOptions) && !(t instanceof NovaTemplateOptions)) { + LOG.warn("networkName is experimental in many jclouds endpoints may not be supported in this cloud"); + // NB, from @andreaturli +// Cloudstack uses custom securityGroupIds and networkIds not the generic networks +// Openstack Nova uses securityGroupNames which is marked as @deprecated (suggests to use groups which is maybe even more confusing) +// Azure supports the custom networkSecurityGroupName + } + + t.networks((String)v); + } + }}) + .put(DOMAIN_NAME, new CustomizeTemplateOptions() { + public void apply(TemplateOptions t, ConfigBag props, Object v) { + if (t instanceof SoftLayerTemplateOptions) { + ((SoftLayerTemplateOptions)t).domainName(TypeCoercions.coerce(v, String.class)); + } else { + LOG.info("ignoring domain-name({}) in VM creation because not supported for cloud/type ({})", v, t); + } + }}) + .put(TEMPLATE_OPTIONS, new CustomizeTemplateOptions() { + @Override + public void apply(TemplateOptions options, ConfigBag config, Object v) { + if (v == null) return; + @SuppressWarnings("unchecked") Map<String, Object> optionsMap = (Map<String, Object>) v; + if (optionsMap.isEmpty()) return; + + Class<? extends TemplateOptions> clazz = options.getClass(); + for(final Map.Entry<String, Object> option : optionsMap.entrySet()) { + Maybe<?> result = MethodCoercions.tryFindAndInvokeBestMatchingMethod(options, option.getKey(), option.getValue()); + if(result.isAbsent()) { + LOG.warn("Ignoring request to set template option {} because this is not supported by {}", new Object[] { option.getKey(), clazz.getCanonicalName() }); + } + } + }}) + .build(); + + /** hook whereby template customizations can be made for various clouds */ + protected void customizeTemplate(ConfigBag setup, ComputeService computeService, Template template) { + for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) { + customizer.customize(this, computeService, template); + customizer.customize(this, computeService, template.getOptions()); + } + + // these things are nice on softlayer + if (template.getOptions() instanceof SoftLayerTemplateOptions) { + SoftLayerTemplateOptions slT = ((SoftLayerTemplateOptions)template.getOptions()); + if (Strings.isBlank(slT.getDomainName()) || "jclouds.org".equals(slT.getDomainName())) { + // set a quasi-sensible domain name if none was provided (better than the default, jclouds.org) + // NB: things like brooklyn.local are disallowed + slT.domainName("local.brooklyncentral.org"); + } + // convert user metadata to tags and notes because user metadata is otherwise ignored + Map<String, String> md = slT.getUserMetadata(); + if (md!=null && !md.isEmpty()) { + Set<String> tags = MutableSet.copyOf(slT.getTags()); + for (Map.Entry<String,String> entry: md.entrySet()) { + tags.add(AbstractCloudMachineNamer.sanitize(entry.getKey())+":"+AbstractCloudMachineNamer.sanitize(entry.getValue())); + } + slT.tags(tags); + + if (!md.containsKey("notes")) { + String notes = "User Metadata\n=============\n\n * " + Joiner.on("\n * ").withKeyValueSeparator(": ").join(md); + if (notes.length() > NOTES_MAX_LENGTH) { + String truncatedMsg = "...\n<truncated - notes total length is " + notes.length() + " characters>"; + notes = notes.substring(0, NOTES_MAX_LENGTH - truncatedMsg.length()) + truncatedMsg; + } + md.put("notes", notes); + } + } + } + } + + /** returns the jclouds Template which describes the image to be built, for the given config and compute service */ + public Template buildTemplate(ComputeService computeService, ConfigBag config) { + TemplateBuilder templateBuilder = (TemplateBuilder) config.get(TEMPLATE_BUILDER); + if (templateBuilder==null) { + templateBuilder = new PortableTemplateBuilder<PortableTemplateBuilder<?>>(); + } else { + LOG.debug("jclouds using templateBuilder {} as custom base for provisioning in {} for {}", new Object[] { + templateBuilder, this, config.getDescription()}); + } + if (templateBuilder instanceof PortableTemplateBuilder<?>) { + if (((PortableTemplateBuilder<?>)templateBuilder).imageChooser()==null) { + Function<Iterable<? extends Image>, Image> chooser = config.get(JcloudsLocationConfig.IMAGE_CHOOSER); + chooser = BrooklynImageChooser.cloneFor(chooser, computeService); + templateBuilder.imageChooser(chooser); + } else { + // an image chooser is already set, so do nothing + } + } else { + // template builder supplied, and we cannot check image chooser status; warn, for now + LOG.warn("Cannot check imageChooser status for {} due to manually supplied black-box TemplateBuilder; " + + "it is recommended to use a PortableTemplateBuilder if you supply a TemplateBuilder", config.getDescription()); + } + + if (!Strings.isEmpty(config.get(CLOUD_REGION_ID))) { + templateBuilder.locationId(config.get(CLOUD_REGION_ID)); + } + + // Apply the template builder and options properties + for (Map.Entry<ConfigKey<?>, CustomizeTemplateBuilder> entry : SUPPORTED_TEMPLATE_BUILDER_PROPERTIES.entrySet()) { + ConfigKey<?> name = entry.getKey(); + CustomizeTemplateBuilder code = entry.getValue(); + if (config.containsKey(name)) + code.apply(templateBuilder, config, config.get(name)); + } + + if (templateBuilder instanceof PortableTemplateBuilder) { + ((PortableTemplateBuilder<?>)templateBuilder).attachComputeService(computeService); + // do the default last, and only if nothing else specified (guaranteed to be a PTB if nothing else specified) + if (groovyTruth(config.get(DEFAULT_IMAGE_ID))) { + if (((PortableTemplateBuilder<?>)templateBuilder).isBlank()) { + templateBuilder.imageId(config.get(DEFAULT_IMAGE_ID).toString()); + } + } + } + + // Then apply any optional app-specific customization. + for (JcloudsLocationCustomizer customizer : getCustomizers(config)) { + customizer.customize(this, computeService, templateBuilder); + } + + LOG.debug("jclouds using templateBuilder {} for provisioning in {} for {}", new Object[] { + templateBuilder, this, config.getDescription()}); + + // Finally try to build the template + Template template; + Image image; + try { + template = templateBuilder.build(); + if (template==null) throw new NullPointerException("No template found (templateBuilder.build returned null)"); + image = template.getImage(); + LOG.debug("jclouds found template "+template+" (image "+image+") for provisioning in "+this+" for "+config.getDescription()); + if (image==null) throw new NullPointerException("Template does not contain an image (templateBuilder.build returned invalid template)"); + } catch (AuthorizationException e) { + LOG.warn("Error resolving template: not authorized (rethrowing: "+e+")"); + throw new IllegalStateException("Not authorized to access cloud "+this+" to resolve "+templateBuilder, e); + } catch (Exception e) { + try { + IOException ioe = Exceptions.getFirstThrowableOfType(e, IOException.class); + if (ioe != null) { + LOG.warn("IOException found...", ioe); + throw ioe; + } + if (listedAvailableTemplatesOnNoSuchTemplate.compareAndSet(false, true)) { + // delay subsequent log.warns (put in synch block) so the "Loading..." message is obvious + LOG.warn("Unable to match required VM template constraints "+templateBuilder+" when trying to provision VM in "+this+" (rethrowing): "+e); + logAvailableTemplates(config); + } + } catch (Exception e2) { + LOG.warn("Error loading available images to report (following original error matching template which will be rethrown): "+e2, e2); + throw new IllegalStateException("Unable to access cloud "+this+" to resolve "+templateBuilder+": "+e, e); + } + throw new IllegalStateException("Unable to match required VM template constraints "+templateBuilder+" when trying to provision VM in "+this+"; " + + "see list of images in log. Root cause: "+e, e); + } + TemplateOptions options = template.getOptions(); + + boolean windows = isWindows(template, config); + if (windows) { + if (!(config.containsKey(JcloudsLocationConfig.USER_METADATA_STRING) || config.containsKey(JcloudsLocationConfig.USER_METADATA_MAP))) { + config.put(JcloudsLocationConfig.USER_METADATA_STRING, WinRmMachineLocation.getDefaultUserMetadataString()); + } + } + + for (Map.Entry<ConfigKey<?>, CustomizeTemplateOptions> entry : SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES.entrySet()) { + ConfigKey<?> key = entry.getKey(); + CustomizeTemplateOptions code = entry.getValue(); + if (config.containsKey(key)) + code.apply(options, config, config.get(key)); + } + + return template; + } + + protected void logAvailableTemplates(ConfigBag config) { + LOG.info("Loading available images at "+this+" for reference..."); + ConfigBag m1 = ConfigBag.newInstanceCopying(config); + if (m1.containsKey(IMAGE_ID)) { + // if caller specified an image ID, remove that, but don't apply default filters + m1.remove(IMAGE_ID); + // TODO use key + m1.putStringKey("anyOwner", true); + } + ComputeService computeServiceLessRestrictive = getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(m1, true); + Set<? extends Image> imgs = computeServiceLessRestrictive.listImages(); + LOG.info(""+imgs.size()+" available images at "+this); + for (Image img: imgs) { + LOG.info(" Image: "+img); + } + + Set<? extends Hardware> profiles = computeServiceLessRestrictive.listHardwareProfiles(); + LOG.info(""+profiles.size()+" available profiles at "+this); + for (Hardware profile: profiles) { + LOG.info(" Profile: "+profile); + } + + Set<? extends org.jclouds.domain.Location> assignableLocations = computeServiceLessRestrictive.listAssignableLocations(); + LOG.info(""+assignableLocations.size()+" available locations at "+this); + for (org.jclouds.domain.Location assignableLocation: assignableLocations) { + LOG.info(" Location: "+assignableLocation); + } + } + + 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()); + sshProps.put(AbstractLocation.TEMPORARY_LOCATION.getName(), true); + 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 (isWindows(node, config)) { + scriptOsFamily = org.jclouds.scriptbuilder.domain.OsFamily.WINDOWS; + } else { + scriptOsFamily = org.jclouds.scriptbuilder.domain.OsFamily.UNIX; + } + + boolean windows = isWindows(node, config); + + if (windows) { + LOG.warn("Unable to execute statements on WinRM in JcloudsLocation; skipping for "+node+": "+userCreation.statements); + + } else { + List<String> commands = Lists.newArrayList(); + for (Statement statement : userCreation.statements) { + InitAdminAccess initAdminAccess = new InitAdminAccess(new AdminAccessConfiguration.Default()); + initAdminAccess.visit(statement); + commands.add(statement.render(scriptOsFamily)); + } + + LoginCredentials initialCredentials = node.getCredentials(); + Optional<String> initialPassword = initialCredentials.getOptionalPassword(); + Optional<String> initialPrivateKey = initialCredentials.getOptionalPrivateKey(); +
<TRUNCATED>
