/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.brooklyn.location.jclouds;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.brooklyn.util.JavaGroovyEquivalents.elvis;
import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth;
import static org.apache.brooklyn.util.ssh.BashCommands.sbinPath;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.location.MachineLocation;
import org.apache.brooklyn.api.location.MachineLocationCustomizer;
import org.apache.brooklyn.api.location.MachineManagementMixins;
import org.apache.brooklyn.api.location.NoMachinesAvailableException;
import org.apache.brooklyn.api.location.PortRange;
import org.apache.brooklyn.api.mgmt.AccessController;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
import org.apache.brooklyn.core.config.ConfigUtils;
import org.apache.brooklyn.core.config.Sanitizer;
import org.apache.brooklyn.core.location.AbstractLocation;
import org.apache.brooklyn.core.location.BasicMachineMetadata;
import org.apache.brooklyn.core.location.LocationConfigKeys;
import org.apache.brooklyn.core.location.LocationConfigUtils;
import org.apache.brooklyn.core.location.LocationConfigUtils.OsCredential;
import org.apache.brooklyn.core.location.PortRanges;
import org.apache.brooklyn.core.location.access.PortForwardManager;
import org.apache.brooklyn.core.location.access.PortMapping;
import org.apache.brooklyn.core.location.cloud.AbstractCloudMachineProvisioningLocation;
import org.apache.brooklyn.core.location.cloud.AvailabilityZoneExtension;
import org.apache.brooklyn.core.location.cloud.names.AbstractCloudMachineNamer;
import org.apache.brooklyn.core.location.cloud.names.CloudMachineNamer;
import org.apache.brooklyn.core.mgmt.internal.LocalLocationManager;
import org.apache.brooklyn.core.mgmt.persist.LocationWithObjectStore;
import org.apache.brooklyn.core.mgmt.persist.PersistenceObjectStore;
import org.apache.brooklyn.core.mgmt.persist.jclouds.JcloudsBlobStoreBasedObjectStore;
import org.apache.brooklyn.location.jclouds.JcloudsPredicates.NodeInLocation;
import org.apache.brooklyn.location.jclouds.networking.JcloudsPortForwarderExtension;
import org.apache.brooklyn.location.jclouds.templates.PortableTemplateBuilder;
import org.apache.brooklyn.location.jclouds.zone.AwsAvailabilityZoneExtension;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.crypto.SecureKeys;
import org.apache.brooklyn.util.core.flags.MethodCoercions;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.core.internal.ssh.ShellTool;
import org.apache.brooklyn.util.core.internal.ssh.SshTool;
import org.apache.brooklyn.util.core.internal.winrm.WinRmTool;
import org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.TaskBuilder;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.core.text.TemplateProcessor;
import org.apache.brooklyn.util.exceptions.CompoundRuntimeException;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.ReferenceWithError;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.Enums;
import org.apache.brooklyn.util.javalang.Reflections;
import org.apache.brooklyn.util.net.Cidr;
import org.apache.brooklyn.util.net.Networking;
import org.apache.brooklyn.util.net.Protocol;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.repeat.Repeater;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.ssh.IptablesCommands;
import org.apache.brooklyn.util.ssh.IptablesCommands.Chain;
import org.apache.brooklyn.util.ssh.IptablesCommands.Policy;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.ByteSizeStrings;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.KeyValueParser;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.apache.commons.lang3.ArrayUtils;
import org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions;
import org.jclouds.cloudstack.compute.options.CloudStackTemplateOptions;
import org.jclouds.compute.ComputeService;
import org.jclouds.compute.RunNodesException;
import org.jclouds.compute.config.AdminAccessConfiguration;
import org.jclouds.compute.domain.ComputeMetadata;
import org.jclouds.compute.domain.Hardware;
import org.jclouds.compute.domain.Image;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.NodeMetadata.Status;
import org.jclouds.compute.domain.NodeMetadataBuilder;
import org.jclouds.compute.domain.OperatingSystem;
import org.jclouds.compute.domain.OsFamily;
import org.jclouds.compute.domain.Template;
import org.jclouds.compute.domain.TemplateBuilder;
import org.jclouds.compute.domain.TemplateBuilderSpec;
import org.jclouds.compute.functions.Sha512Crypt;
import org.jclouds.compute.options.TemplateOptions;
import org.jclouds.domain.Credentials;
import org.jclouds.domain.LocationScope;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.ec2.compute.options.EC2TemplateOptions;
import org.jclouds.googlecomputeengine.compute.options.GoogleComputeEngineTemplateOptions;
import org.jclouds.openstack.nova.v2_0.compute.options.NovaTemplateOptions;
import org.jclouds.rest.AuthorizationException;
import org.jclouds.scriptbuilder.domain.LiteralStatement;
import org.jclouds.scriptbuilder.domain.Statement;
import org.jclouds.scriptbuilder.domain.StatementList;
import org.jclouds.scriptbuilder.functions.InitAdminAccess;
import org.jclouds.scriptbuilder.statements.login.AdminAccess;
import org.jclouds.scriptbuilder.statements.login.ReplaceShadowPasswordEntry;
import org.jclouds.scriptbuilder.statements.ssh.AuthorizeRSAPublicKeys;
import org.jclouds.softlayer.compute.options.SoftLayerTemplateOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.base.Stopwatch;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.io.Files;
import com.google.common.net.HostAndPort;
/**
* For provisioning and managing VMs in a particular provider/region, using jclouds.
* Configuration flags are defined in {@link JcloudsLocationConfig}.
*/
@SuppressWarnings("serial")
public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation implements
JcloudsLocationConfig, MachineManagementMixins.RichMachineProvisioningLocation<MachineLocation>,
LocationWithObjectStore, MachineManagementMixins.SuspendResumeLocation {
// TODO After converting from Groovy to Java, this is now very bad code! It relies entirely on putting
// things into and taking them out of maps; it's not type-safe, and it's thus very error-prone.
// In Groovy, that's considered ok but not in Java.
// TODO test (and fix) ability to set config keys from flags
// TODO we say config is inherited, but it isn't the case for many "deep" / jclouds properties
// e.g. when we pass getRawLocalConfigBag() in and decorate it with additional flags
// (inheritance only works when we call getConfig in this class)
public static final Logger LOG = LoggerFactory.getLogger(JcloudsLocation.class);
public static final String ROOT_USERNAME = "root";
/** these userNames are known to be the preferred/required logins in some common/default images
* where root@ is not allowed to log in */
public static final List<String> ROOT_ALIASES = ImmutableList.of("ubuntu", "ec2-user");
public static final List<String> COMMON_USER_NAMES_TO_TRY = ImmutableList.<String>builder().add(ROOT_USERNAME).addAll(ROOT_ALIASES).add("admin").build();
private static final Pattern LIST_PATTERN = Pattern.compile("^\\[(.*)\\]$");
private static final Pattern INTEGER_PATTERN = Pattern.compile("^\\d*$");
private static final int NOTES_MAX_LENGTH = 1000;
private final AtomicBoolean loggedSshKeysHint = new AtomicBoolean(false);
private final AtomicBoolean listedAvailableTemplatesOnNoSuchTemplate = new AtomicBoolean(false);
private final Map<String,Map<String, ? extends Object>> tagMapping = Maps.newLinkedHashMap();
@SetFromFlag // so it's persisted
private final Map<MachineLocation,String> vmInstanceIds = Maps.newLinkedHashMap();
static { Networking.init(); }
public JcloudsLocation() {
super();
}
/** typically wants at least ACCESS_IDENTITY and ACCESS_CREDENTIAL */
public JcloudsLocation(Map<?,?> conf) {
super(conf);
}
@Override
@Deprecated
public JcloudsLocation configure(Map<?,?> properties) {
super.configure(properties);
if (config().getLocalBag().containsKey("providerLocationId")) {
LOG.warn("Using deprecated 'providerLocationId' key in "+this);
if (!config().getLocalBag().containsKey(CLOUD_REGION_ID))
config().addToLocalBag(MutableMap.of(CLOUD_REGION_ID.getName(), (String)config().getLocalBag().getStringKey("providerLocationId")));
}
if (isDisplayNameAutoGenerated() || !groovyTruth(getDisplayName())) {
setDisplayName(elvis(getProvider(), "unknown") +
(groovyTruth(getRegion()) ? ":"+getRegion() : "") +
(groovyTruth(getEndpoint()) ? ":"+getEndpoint() : ""));
}
setCreationString(config().getLocalBag());
if (getConfig(MACHINE_CREATION_SEMAPHORE) == null) {
Integer maxConcurrent = getConfig(MAX_CONCURRENT_MACHINE_CREATIONS);
if (maxConcurrent == null || maxConcurrent < 1) {
throw new IllegalStateException(MAX_CONCURRENT_MACHINE_CREATIONS.getName() + " must be >= 1, but was "+maxConcurrent);
}
config().set(MACHINE_CREATION_SEMAPHORE, new Semaphore(maxConcurrent, true));
}
return this;
}
@Override
public void init() {
super.init();
if ("aws-ec2".equals(getProvider())) {
addExtension(AvailabilityZoneExtension.class, new AwsAvailabilityZoneExtension(getManagementContext(), this));
}
}
@Override
public JcloudsLocation newSubLocation(Map<?,?> newFlags) {
return newSubLocation(getClass(), newFlags);
}
@Override
public JcloudsLocation newSubLocation(Class<? extends AbstractCloudMachineProvisioningLocation> type, Map<?,?> newFlags) {
// TODO should be able to use ConfigBag.newInstanceExtending; would require moving stuff around to api etc
return (JcloudsLocation) getManagementContext().getLocationManager().createLocation(LocationSpec.create(type)
.parent(this)
.configure(config().getLocalBag().getAllConfig()) // FIXME Should this just be inherited?
.configure(MACHINE_CREATION_SEMAPHORE, getMachineCreationSemaphore())
.configure(newFlags));
}
@Override
public String toString() {
Object identity = getIdentity();
String configDescription = config().getLocalBag().getDescription();
if (configDescription!=null && configDescription.startsWith(getClass().getSimpleName()))
return configDescription;
return getClass().getSimpleName()+"["+getDisplayName()+":"+(identity != null ? identity : null)+
(configDescription!=null ? "/"+configDescription : "") + "@" + getId() + "]";
}
@Override
public String toVerboseString() {
return Objects.toStringHelper(this).omitNullValues()
.add("id", getId()).add("name", getDisplayName()).add("identity", getIdentity())
.add("description", config().getLocalBag().getDescription()).add("provider", getProvider())
.add("region", getRegion()).add("endpoint", getEndpoint())
.toString();
}
public String getProvider() {
return getConfig(CLOUD_PROVIDER);
}
public String getIdentity() {
return getConfig(ACCESS_IDENTITY);
}
public String getCredential() {
return getConfig(ACCESS_CREDENTIAL);
}
/** returns the location ID used by the provider, if set, e.g. us-west-1 */
public String getRegion() {
return getConfig(CLOUD_REGION_ID);
}
public String getEndpoint() {
return (String) config().getBag().getWithDeprecation(CLOUD_ENDPOINT, JCLOUDS_KEY_ENDPOINT);
}
public String getUser(ConfigBag config) {
return (String) config.getWithDeprecation(USER, JCLOUDS_KEY_USERNAME);
}
public boolean isWindows(Template template, ConfigBag config) {
return isWindows(template.getImage(), config);
}
/**
* Whether VMs provisioned from this image will be Windows. Assume windows if the image
* explicitly says so, or if image does not tell us then fall back to whether the config
* explicitly says windows in {@link JcloudsLocationConfig#OS_FAMILY}.
*
* Will first look at {@link JcloudsLocationConfig#OS_FAMILY_OVERRIDE}, to check if that
* is set. If so, no further checks are done: the value is compared against {@link OsFamily#WINDOWS}.
*
* We believe the config (e.g. from brooklyn.properties) because for some clouds there is
* insufficient meta-data so the Image might not tell us. Thus a user can work around it
* by explicitly supplying configuration.
*/
public boolean isWindows(Image image, ConfigBag config) {
OsFamily override = config.get(OS_FAMILY_OVERRIDE);
if (override != null) return override == OsFamily.WINDOWS;
OsFamily confFamily = config.get(OS_FAMILY);
OperatingSystem os = (image != null) ? image.getOperatingSystem() : null;
return (os != null && os.getFamily() != OsFamily.UNRECOGNIZED)
? (OsFamily.WINDOWS == os.getFamily())
: (OsFamily.WINDOWS == confFamily);
}
/**
* Whether the given VM is Windows.
*
* @see {@link #isWindows(Image, ConfigBag)}
*/
public boolean isWindows(NodeMetadata node, ConfigBag config) {
OsFamily override = config.get(OS_FAMILY_OVERRIDE);
if (override != null) return override == OsFamily.WINDOWS;
OsFamily confFamily = config.get(OS_FAMILY);
OperatingSystem os = (node != null) ? node.getOperatingSystem() : null;
return (os != null && os.getFamily() != OsFamily.UNRECOGNIZED)
? (OsFamily.WINDOWS == os.getFamily())
: (OsFamily.WINDOWS == confFamily);
}
public boolean isLocationFirewalldEnabled(SshMachineLocation location) {
int result = location.execCommands("checking if firewalld is active",
ImmutableList.of(IptablesCommands.firewalldServiceIsActive()));
if (result == 0) {
return true;
}
return false;
}
protected Semaphore getMachineCreationSemaphore() {
return checkNotNull(getConfig(MACHINE_CREATION_SEMAPHORE), MACHINE_CREATION_SEMAPHORE.getName());
}
protected CloudMachineNamer getCloudMachineNamer(ConfigBag config) {
String namerClass = config.get(LocationConfigKeys.CLOUD_MACHINE_NAMER_CLASS);
if (Strings.isNonBlank(namerClass)) {
Optional<CloudMachineNamer> cloudNamer = Reflections.invokeConstructorWithArgs(getManagementContext().getCatalogClassLoader(), namerClass);
if (cloudNamer.isPresent()) {
return cloudNamer.get();
} else {
throw new IllegalStateException("Failed to create CloudMachineNamer "+namerClass+" for location "+this);
}
} else {
return new JcloudsMachineNamer();
}
}
protected Collection<JcloudsLocationCustomizer> getCustomizers(ConfigBag setup) {
@SuppressWarnings("deprecation")
JcloudsLocationCustomizer customizer = setup.get(JCLOUDS_LOCATION_CUSTOMIZER);
Collection<JcloudsLocationCustomizer> customizers = setup.get(JCLOUDS_LOCATION_CUSTOMIZERS);
@SuppressWarnings("deprecation")
String customizerType = setup.get(JCLOUDS_LOCATION_CUSTOMIZER_TYPE);
@SuppressWarnings("deprecation")
String customizersSupplierType = setup.get(JCLOUDS_LOCATION_CUSTOMIZERS_SUPPLIER_TYPE);
ClassLoader catalogClassLoader = getManagementContext().getCatalogClassLoader();
List<JcloudsLocationCustomizer> result = new ArrayList<JcloudsLocationCustomizer>();
if (customizer != null) result.add(customizer);
if (customizers != null) result.addAll(customizers);
if (Strings.isNonBlank(customizerType)) {
Optional<JcloudsLocationCustomizer> customizerByType = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizerType, setup);
if (customizerByType.isPresent()) {
result.add(customizerByType.get());
} else {
customizerByType = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizerType);
if (customizerByType.isPresent()) {
result.add(customizerByType.get());
} else {
throw new IllegalStateException("Failed to create JcloudsLocationCustomizer "+customizersSupplierType+" for location "+this);
}
}
}
if (Strings.isNonBlank(customizersSupplierType)) {
Optional<Supplier<Collection<JcloudsLocationCustomizer>>> supplier = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizersSupplierType, setup);
if (supplier.isPresent()) {
result.addAll(supplier.get().get());
} else {
supplier = Reflections.invokeConstructorWithArgs(catalogClassLoader, customizersSupplierType);
if (supplier.isPresent()) {
result.addAll(supplier.get().get());
} else {
throw new IllegalStateException("Failed to create JcloudsLocationCustomizer supplier "+customizersSupplierType+" for location "+this);
}
}
}
return result;
}
protected Collection<MachineLocationCustomizer> getMachineCustomizers(ConfigBag setup) {
Collection<MachineLocationCustomizer> customizers = setup.get(MACHINE_LOCATION_CUSTOMIZERS);
return (customizers == null ? ImmutableList.<MachineLocationCustomizer>of() : customizers);
}
public void setDefaultImageId(String val) {
config().set(DEFAULT_IMAGE_ID, val);
}
// TODO remove tagMapping, or promote it
// (i think i favour removing it, letting the config come in from the entity)
public void setTagMapping(Map<String,Map<String, ? extends Object>> val) {
tagMapping.clear();
tagMapping.putAll(val);
}
// TODO Decide on semantics. If I give "TomcatServer" and "Ubuntu", then must I get back an image that matches both?
// Currently, just takes first match that it finds...
@Override
public Map<String,Object> getProvisioningFlags(Collection<String> tags) {
Map<String,Object> result = Maps.newLinkedHashMap();
Collection<String> unmatchedTags = Lists.newArrayList();
for (String it : tags) {
if (groovyTruth(tagMapping.get(it)) && !groovyTruth(result)) {
result.putAll(tagMapping.get(it));
} else {
unmatchedTags.add(it);
}
}
if (unmatchedTags.size() > 0) {
LOG.debug("Location {}, failed to match provisioning tags {}", this, unmatchedTags);
}
return result;
}
public static final Set<ConfigKey<?>> getAllSupportedProperties() {
Set<String> configsOnClass = Sets.newLinkedHashSet(
Iterables.transform(ConfigUtils.getStaticKeysOnClass(JcloudsLocation.class),
new Function<HasConfigKey<?>,String>() {
@Override @Nullable
public String apply(@Nullable HasConfigKey<?> input) {
return input.getConfigKey().getName();
}
}));
Set<ConfigKey<?>> configKeysInList = ImmutableSet.<ConfigKey<?>>builder()
.addAll(SUPPORTED_TEMPLATE_BUILDER_PROPERTIES.keySet())
.addAll(SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES.keySet())
.build();
Set<String> configsInList = Sets.newLinkedHashSet(
Iterables.transform(configKeysInList,
new Function<ConfigKey<?>,String>() {
@Override @Nullable
public String apply(@Nullable ConfigKey<?> input) {
return input.getName();
}
}));
SetView<String> extrasInList = Sets.difference(configsInList, configsOnClass);
// notInList is normal
if (!extrasInList.isEmpty())
LOG.warn("JcloudsLocation supported properties differs from config defined on class: " + extrasInList);
return Collections.unmodifiableSet(configKeysInList);
}
public ComputeService getComputeService() {
return getComputeService(MutableMap.of());
}
public ComputeService getComputeService(Map<?,?> flags) {
ConfigBag conf = (flags==null || flags.isEmpty())
? config().getBag()
: ConfigBag.newInstanceExtending(config().getBag(), flags);
return getComputeService(conf);
}
public ComputeService getComputeService(ConfigBag config) {
return getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(config, true);
}
/** @deprecated since 0.7.0 use {@link #listMachines()} */ @Deprecated
public Set<? extends ComputeMetadata> listNodes() {
return listNodes(MutableMap.of());
}
/** @deprecated since 0.7.0 use {@link #listMachines()}.
* (no support for custom compute service flags; if that is needed, we'll have to introduce a new method,
* but it seems there are no usages) */ @Deprecated
public Set<? extends ComputeMetadata> listNodes(Map<?,?> flags) {
return getComputeService(flags).listNodes();
}
@Override
public Map<String, MachineManagementMixins.MachineMetadata> listMachines() {
Set<? extends ComputeMetadata> nodes =
getRegion()!=null ? getComputeService().listNodesDetailsMatching(new NodeInLocation(getRegion(), true))
: getComputeService().listNodes();
Map<String,MachineManagementMixins.MachineMetadata> result = new LinkedHashMap<String, MachineManagementMixins.MachineMetadata>();
for (ComputeMetadata node: nodes)
result.put(node.getId(), getMachineMetadata(node));
return result;
}
protected MachineManagementMixins.MachineMetadata getMachineMetadata(ComputeMetadata node) {
if (node==null)
return null;
return new BasicMachineMetadata(node.getId(), node.getName(),
((node instanceof NodeMetadata) ? Iterators.tryFind( ((NodeMetadata)node).getPublicAddresses().iterator(), Predicates.alwaysTrue() ).orNull() : null),
((node instanceof NodeMetadata) ? ((NodeMetadata)node).getStatus()==Status.RUNNING : null),
node);
}
@Override
public MachineManagementMixins.MachineMetadata getMachineMetadata(MachineLocation l) {
if (l instanceof JcloudsSshMachineLocation) {
return getMachineMetadata( ((JcloudsSshMachineLocation)l).node );
}
return null;
}
@Override
public void killMachine(String cloudServiceId) {
getComputeService().destroyNode(cloudServiceId);
}
@Override
public void killMachine(MachineLocation l) {
MachineManagementMixins.MachineMetadata m = getMachineMetadata(l);
if (m==null) throw new NoSuchElementException("Machine "+l+" is not known at "+this);
killMachine(m.getId());
}
/** attaches a string describing where something is being created
* (provider, region/location and/or endpoint, callerContext) */
protected void setCreationString(ConfigBag config) {
config.setDescription(elvis(config.get(CLOUD_PROVIDER), "unknown")+
(config.containsKey(CLOUD_REGION_ID) ? ":"+config.get(CLOUD_REGION_ID) : "")+
(config.containsKey(CLOUD_ENDPOINT) ? ":"+config.get(CLOUD_ENDPOINT) : "")+
(config.containsKey(CALLER_CONTEXT) ? "@"+config.get(CALLER_CONTEXT) : ""));
}
// ----------------- obtaining a new machine ------------------------
public MachineLocation obtain() throws NoMachinesAvailableException {
return obtain(MutableMap.of());
}
public MachineLocation obtain(TemplateBuilder tb) throws NoMachinesAvailableException {
return obtain(MutableMap.of(), tb);
}
public MachineLocation obtain(Map<?,?> flags, TemplateBuilder tb) throws NoMachinesAvailableException {
return obtain(MutableMap.builder().putAll(flags).put(TEMPLATE_BUILDER, tb).build());
}
/** core method for obtaining a VM using jclouds;
* Map should contain CLOUD_PROVIDER and CLOUD_ENDPOINT or CLOUD_REGION, depending on the cloud,
* as well as ACCESS_IDENTITY and ACCESS_CREDENTIAL,
* plus any further properties to specify e.g. images, hardware profiles, accessing user
* (for initial login, and a user potentially to create for subsequent ie normal access) */
@Override
public MachineLocation obtain(Map<?,?> flags) throws NoMachinesAvailableException {
ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
Integer attempts = setup.get(MACHINE_CREATE_ATTEMPTS);
List<Exception> exceptions = Lists.newArrayList();
if (attempts == null || attempts < 1) attempts = 1;
for (int i = 1; i <= attempts; i++) {
try {
return obtainOnce(setup);
} catch (RuntimeException e) {
LOG.warn("Attempt #{}/{} to obtain machine threw error: {}", new Object[]{i, attempts, e});
exceptions.add(e);
}
}
String msg = String.format("Failed to get VM after %d attempt%s.", attempts, attempts == 1 ? "" : "s");
Exception cause = (exceptions.size() == 1)
? exceptions.get(0)
: new CompoundRuntimeException(msg + " - "
+ "First cause is "+exceptions.get(0)+" (listed in primary trace); "
+ "plus " + (exceptions.size()-1) + " more (e.g. the last is "+exceptions.get(exceptions.size()-1)+")",
exceptions.get(0), exceptions);
if (exceptions.get(exceptions.size()-1) instanceof NoMachinesAvailableException) {
throw new NoMachinesAvailableException(msg, cause);
} else {
throw Exceptions.propagate(cause);
}
}
protected MachineLocation obtainOnce(ConfigBag setup) throws NoMachinesAvailableException {
AccessController.Response access = getManagementContext().getAccessController().canProvisionLocation(this);
if (!access.isAllowed()) {
throw new IllegalStateException("Access controller forbids provisioning in "+this+": "+access.getMsg());
}
setCreationString(setup);
boolean waitForSshable = !"false".equalsIgnoreCase(setup.get(WAIT_FOR_SSHABLE));
boolean waitForWinRmable = !"false".equalsIgnoreCase(setup.get(WAIT_FOR_WINRM_AVAILABLE));
boolean usePortForwarding = setup.get(USE_PORT_FORWARDING);
boolean skipJcloudsSshing = Boolean.FALSE.equals(setup.get(USE_JCLOUDS_SSH_INIT)) || usePortForwarding;
JcloudsPortForwarderExtension portForwarder = setup.get(PORT_FORWARDER);
if (usePortForwarding) checkNotNull(portForwarder, "portForwarder, when use-port-forwarding enabled");
final ComputeService computeService = getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(setup, true);
CloudMachineNamer cloudMachineNamer = getCloudMachineNamer(setup);
String groupId = elvis(setup.get(GROUP_ID), cloudMachineNamer.generateNewGroupId(setup));
NodeMetadata node = null;
JcloudsMachineLocation machineLocation = null;
Duration semaphoreTimestamp = null;
Duration templateTimestamp = null;
Duration provisionTimestamp = null;
Duration usableTimestamp = null;
Duration customizedTimestamp = null;
Stopwatch provisioningStopwatch = Stopwatch.createStarted();
try {
LOG.info("Creating VM "+setup.getDescription()+" in "+this);
Semaphore machineCreationSemaphore = getMachineCreationSemaphore();
boolean acquired = machineCreationSemaphore.tryAcquire(0, TimeUnit.SECONDS);
if (!acquired) {
LOG.info("Waiting in {} for machine-creation permit ({} other queuing requests already)", new Object[] {this, machineCreationSemaphore.getQueueLength()});
Stopwatch blockStopwatch = Stopwatch.createStarted();
machineCreationSemaphore.acquire();
LOG.info("Acquired in {} machine-creation permit, after waiting {}", this, Time.makeTimeStringRounded(blockStopwatch));
} else {
LOG.debug("Acquired in {} machine-creation permit immediately", this);
}
semaphoreTimestamp = Duration.of(provisioningStopwatch);
LoginCredentials userCredentials = null;
Set<? extends NodeMetadata> nodes;
Template template;
try {
// Setup the template
template = buildTemplate(computeService, setup);
boolean expectWindows = isWindows(template, setup);
if (!skipJcloudsSshing) {
if (expectWindows) {
// TODO Was this too early to look at template.getImage? e.g. customizeTemplate could subsequently modify it.
LOG.warn("Ignoring invalid configuration for Windows provisioning of "+template.getImage()+": "+USE_JCLOUDS_SSH_INIT.getName()+" should be false");
skipJcloudsSshing = true;
} else if (waitForSshable) {
userCredentials = initTemplateForCreateUser(template, setup);
}
}
templateTimestamp = Duration.of(provisioningStopwatch);
// "Name" metadata seems to set the display name; at least in AWS
// TODO it would be nice if this salt comes from the location's ID (but we don't know that yet as the ssh machine location isn't created yet)
// TODO in softlayer we want to control the suffix of the hostname which is 3 random hex digits
template.getOptions().getUserMetadata().put("Name", cloudMachineNamer.generateNewMachineUniqueNameFromGroupId(setup, groupId));
if (setup.get(JcloudsLocationConfig.INCLUDE_BROOKLYN_USER_METADATA)) {
template.getOptions().getUserMetadata().put("brooklyn-user", System.getProperty("user.name"));
Object context = setup.get(CALLER_CONTEXT);
if (context instanceof Entity) {
Entity entity = (Entity)context;
template.getOptions().getUserMetadata().put("brooklyn-app-id", entity.getApplicationId());
template.getOptions().getUserMetadata().put("brooklyn-app-name", entity.getApplication().getDisplayName());
template.getOptions().getUserMetadata().put("brooklyn-entity-id", entity.getId());
template.getOptions().getUserMetadata().put("brooklyn-entity-name", entity.getDisplayName());
template.getOptions().getUserMetadata().put("brooklyn-server-creation-date", Time.makeDateSimpleStampString());
}
}
customizeTemplate(setup, computeService, template);
LOG.debug("jclouds using template {} / options {} to provision machine in {}",
new Object[] {template, template.getOptions(), setup.getDescription()});
if (!setup.getUnusedConfig().isEmpty())
if (LOG.isDebugEnabled())
LOG.debug("NOTE: unused flags passed to obtain VM in "+setup.getDescription()+": "
+ Sanitizer.sanitize(setup.getUnusedConfig()));
nodes = computeService.createNodesInGroup(groupId, 1, template);
provisionTimestamp = Duration.of(provisioningStopwatch);
} finally {
machineCreationSemaphore.release();
}
node = Iterables.getOnlyElement(nodes, null);
LOG.debug("jclouds created {} for {}", node, setup.getDescription());
if (node == null)
throw new IllegalStateException("No nodes returned by jclouds create-nodes in " + setup.getDescription());
boolean windows = isWindows(node, setup);
if (windows) {
int newLoginPort = node.getLoginPort() == 22 ? 5985 : node.getLoginPort();
String newLoginUser = "root".equals(node.getCredentials().getUser()) ? "Administrator" : node.getCredentials().getUser();
LOG.debug("jclouds created Windows VM {}; transforming connection details: loginPort from {} to {}; loginUser from {} to {}",
new Object[] {node, node.getLoginPort(), newLoginPort, node.getCredentials().getUser(), newLoginUser});
node = NodeMetadataBuilder.fromNodeMetadata(node)
.loginPort(newLoginPort)
.credentials(LoginCredentials.builder(node.getCredentials()).user(newLoginUser).build())
.build();
}
// FIXME How do we influence the node.getLoginPort, so it is set correctly for Windows?
// Setup port-forwarding, if required
Optional<HostAndPort> sshHostAndPortOverride;
if (usePortForwarding) {
sshHostAndPortOverride = Optional.of(portForwarder.openPortForwarding(
node,
node.getLoginPort(),
Optional.<Integer>absent(),
Protocol.TCP,
Cidr.UNIVERSAL));
} else {
sshHostAndPortOverride = Optional.absent();
}
LoginCredentials initialCredentials = node.getCredentials();
if (skipJcloudsSshing) {
boolean waitForConnectable = (windows) ? waitForWinRmable : waitForSshable;
if (waitForConnectable) {
if (windows) {
// TODO Does jclouds support any windows user setup?
initialCredentials = waitForWinRmAvailable(computeService, node, sshHostAndPortOverride, setup);
} else {
initialCredentials = waitForSshable(computeService, node, sshHostAndPortOverride, setup);
}
userCredentials = createUser(computeService, node, sshHostAndPortOverride, initialCredentials, setup);
}
}
// Figure out which login-credentials to use
LoginCredentials customCredentials = setup.get(CUSTOM_CREDENTIALS);
if (customCredentials != null) {
userCredentials = customCredentials;
//set userName and other data, from these credentials
Object oldUsername = setup.put(USER, customCredentials.getUser());
LOG.debug("node {} username {} / {} (customCredentials)", new Object[] { node, customCredentials.getUser(), oldUsername });
if (customCredentials.getOptionalPassword().isPresent()) setup.put(PASSWORD, customCredentials.getOptionalPassword().get());
if (customCredentials.getOptionalPrivateKey().isPresent()) setup.put(PRIVATE_KEY_DATA, customCredentials.getOptionalPrivateKey().get());
}
if (userCredentials == null || (!userCredentials.getOptionalPassword().isPresent() && !userCredentials.getOptionalPrivateKey().isPresent())) {
// We either don't have any userCredentials, or it is missing both a password/key.
// TODO See waitForSshable, which now handles if the node.getLoginCredentials has both a password+key
userCredentials = extractVmCredentials(setup, node, initialCredentials);
}
if (userCredentials == null) {
// TODO See waitForSshable, which now handles if the node.getLoginCredentials has both a password+key
userCredentials = extractVmCredentials(setup, node, initialCredentials);
}
if (userCredentials != null) {
node = NodeMetadataBuilder.fromNodeMetadata(node).credentials(userCredentials).build();
} else {
// only happens if something broke above...
userCredentials = LoginCredentials.fromCredentials(node.getCredentials());
}
// store the credentials, in case they have changed
setup.putIfNotNull(JcloudsLocationConfig.PASSWORD, userCredentials.getOptionalPassword().orNull());
setup.putIfNotNull(JcloudsLocationConfig.PRIVATE_KEY_DATA, userCredentials.getOptionalPrivateKey().orNull());
// Wait for the VM to be reachable over SSH
if (waitForSshable && !windows) {
waitForSshable(computeService, node, sshHostAndPortOverride, ImmutableList.of(userCredentials), setup);
} else {
LOG.debug("Skipping ssh check for {} ({}) due to config waitForSshable=false", node, setup.getDescription());
}
usableTimestamp = Duration.of(provisioningStopwatch);
// JcloudsSshMachineLocation jcloudsSshMachineLocation = null;
// WinRmMachineLocation winRmMachineLocation = null;
// Create a JcloudsSshMachineLocation, and register it
if (windows) {
machineLocation = registerWinRmMachineLocation(computeService, node, userCredentials, sshHostAndPortOverride, setup);
} else {
machineLocation = registerJcloudsSshMachineLocation(computeService, node, Optional.fromNullable(template), userCredentials, sshHostAndPortOverride, setup);
}
if (usePortForwarding && sshHostAndPortOverride.isPresent()) {
// Now that we have the sshMachineLocation, we can associate the port-forwarding address with it.
PortForwardManager portForwardManager = setup.get(PORT_FORWARDING_MANAGER);
if (portForwardManager != null) {
portForwardManager.associate(node.getId(), sshHostAndPortOverride.get(), machineLocation, node.getLoginPort());
} else {
LOG.warn("No port-forward manager for {} so could not associate {} -> {} for {}",
new Object[] {this, node.getLoginPort(), sshHostAndPortOverride, machineLocation});
}
}
if ("docker".equals(this.getProvider())) {
if (windows) {
throw new UnsupportedOperationException("Docker not supported on Windows");
}
Map<Integer, Integer> portMappings = JcloudsUtil.dockerPortMappingsFor(this, node.getId());
PortForwardManager portForwardManager = setup.get(PORT_FORWARDING_MANAGER);
if (portForwardManager != null) {
for(Integer containerPort : portMappings.keySet()) {
Integer hostPort = portMappings.get(containerPort);
String dockerHost = ((JcloudsSshMachineLocation)machineLocation).getSshHostAndPort().getHostText();
portForwardManager.associate(node.getId(), HostAndPort.fromParts(dockerHost, hostPort), machineLocation, containerPort);
}
} else {
LOG.warn("No port-forward manager for {} so could not associate docker port-mappings for {}",
this, machineLocation);
}
}
List<String> customisationForLogging = new ArrayList<String>();
// Apply same securityGroups rules to iptables, if iptables is running on the node
if (waitForSshable) {
String setupScript = setup.get(JcloudsLocationConfig.CUSTOM_MACHINE_SETUP_SCRIPT_URL);
List<String> setupScripts = setup.get(JcloudsLocationConfig.CUSTOM_MACHINE_SETUP_SCRIPT_URL_LIST);
Collection<String> allScripts = new MutableList<String>().appendIfNotNull(setupScript).appendAll(setupScripts);
for (String setupScriptItem : allScripts) {
if (Strings.isNonBlank(setupScriptItem)) {
customisationForLogging.add("custom setup script " + setupScriptItem);
String setupVarsString = setup.get(JcloudsLocationConfig.CUSTOM_MACHINE_SETUP_SCRIPT_VARS);
Map<String, String> substitutions = (setupVarsString != null)
? Splitter.on(",").withKeyValueSeparator(":").split(setupVarsString)
: ImmutableMap.<String, String>of();
String scriptContent = ResourceUtils.create(this).getResourceAsString(setupScriptItem);
String script = TemplateProcessor.processTemplateContents(scriptContent, getManagementContext(), substitutions);
if (windows) {
((WinRmMachineLocation)machineLocation).executeCommand(ImmutableList.copyOf((script.replace("\r", "").split("\n"))));
} else {
((SshMachineLocation)machineLocation).execCommands("Customizing node " + this, ImmutableList.of(script));
}
}
}
if (setup.get(JcloudsLocationConfig.MAP_DEV_RANDOM_TO_DEV_URANDOM)) {
if (windows) {
LOG.warn("Ignoring flag MAP_DEV_RANDOM_TO_DEV_URANDOM on Windows location {}", machineLocation);
} else {
customisationForLogging.add("point /dev/random to urandom");
((SshMachineLocation)machineLocation).execCommands("using urandom instead of random",
Arrays.asList("sudo mv /dev/random /dev/random-real", "sudo ln -s /dev/urandom /dev/random"));
}
}
if (setup.get(GENERATE_HOSTNAME)) {
if (windows) {
// TODO: Generate Windows Hostname
LOG.warn("Ignoring flag GENERATE_HOSTNAME on Windows location {}", machineLocation);
} else {
customisationForLogging.add("configure hostname");
((SshMachineLocation)machineLocation).execCommands("Generate hostname " + node.getName(),
Arrays.asList("sudo hostname " + node.getName(),
"sudo sed -i \"s/HOSTNAME=.*/HOSTNAME=" + node.getName() + "/g\" /etc/sysconfig/network",
"sudo bash -c \"echo 127.0.0.1 `hostname` >> /etc/hosts\"")
);
}
}
if (setup.get(OPEN_IPTABLES)) {
if (windows) {
LOG.warn("Ignoring DEPRECATED flag OPEN_IPTABLES on Windows location {}", machineLocation);
} else {
LOG.warn("Using DEPRECATED flag OPEN_IPTABLES (will not be supported in future versions) for {} at {}", machineLocation, this);
@SuppressWarnings("unchecked")
Iterable<Integer> inboundPorts = (Iterable<Integer>) setup.get(INBOUND_PORTS);
if (inboundPorts == null || Iterables.isEmpty(inboundPorts)) {
LOG.info("No ports to open in iptables (no inbound ports) for {} at {}", machineLocation, this);
} else {
customisationForLogging.add("open iptables");
List<String> iptablesRules = Lists.newArrayList();
if (isLocationFirewalldEnabled((SshMachineLocation)machineLocation)) {
for (Integer port : inboundPorts) {
iptablesRules.add(IptablesCommands.addFirewalldRule(Chain.INPUT, Protocol.TCP, port, Policy.ACCEPT));
}
} else {
iptablesRules = createIptablesRulesForNetworkInterface(inboundPorts);
iptablesRules.add(IptablesCommands.saveIptablesRules());
}
List<String> batch = Lists.newArrayList();
// Some entities, such as Riak (erlang based) have a huge range of ports, which leads to a script that
// is too large to run (fails with a broken pipe). Batch the rules into batches of 50
for (String rule : iptablesRules) {
batch.add(rule);
if (batch.size() == 50) {
((SshMachineLocation)machineLocation).execCommands("Inserting iptables rules, 50 command batch", batch);
batch.clear();
}
}
if (batch.size() > 0) {
((SshMachineLocation)machineLocation).execCommands("Inserting iptables rules", batch);
}
((SshMachineLocation)machineLocation).execCommands("List iptables rules", ImmutableList.of(IptablesCommands.listIptablesRule()));
}
}
}
if (setup.get(STOP_IPTABLES)) {
if (windows) {
LOG.warn("Ignoring DEPRECATED flag OPEN_IPTABLES on Windows location {}", machineLocation);
} else {
LOG.warn("Using DEPRECATED flag STOP_IPTABLES (will not be supported in future versions) for {} at {}", machineLocation, this);
customisationForLogging.add("stop iptables");
List<String> cmds = ImmutableList.<String>of();
if (isLocationFirewalldEnabled((SshMachineLocation)machineLocation)) {
cmds = ImmutableList.of(IptablesCommands.firewalldServiceStop(), IptablesCommands.firewalldServiceStatus());
} else {
cmds = ImmutableList.of(IptablesCommands.iptablesServiceStop(), IptablesCommands.iptablesServiceStatus());
}
((SshMachineLocation)machineLocation).execCommands("Stopping iptables", cmds);
}
}
List<String> extraKeyUrlsToAuth = setup.get(EXTRA_PUBLIC_KEY_URLS_TO_AUTH);
if (extraKeyUrlsToAuth!=null && !extraKeyUrlsToAuth.isEmpty()) {
if (windows) {
LOG.warn("Ignoring flag EXTRA_PUBLIC_KEY_URLS_TO_AUTH on Windows location", machineLocation);
} else {
List<String> extraKeyDataToAuth = MutableList.of();
for (String keyUrl : extraKeyUrlsToAuth) {
extraKeyDataToAuth.add(ResourceUtils.create().getResourceAsString(keyUrl));
}
((SshMachineLocation)machineLocation).execCommands("Authorizing ssh keys",
ImmutableList.of(new AuthorizeRSAPublicKeys(extraKeyDataToAuth).render(org.jclouds.scriptbuilder.domain.OsFamily.UNIX)));
}
}
} else {
// Otherwise we have deliberately not waited to be ssh'able, so don't try now to
// ssh to exec these commands!
}
// Apply any optional app-specific customization.
for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
LOG.debug("Customizing machine {}, using customizer {}", machineLocation, customizer);
customizer.customize(this, computeService, machineLocation);
}
for (MachineLocationCustomizer customizer : getMachineCustomizers(setup)) {
LOG.debug("Customizing machine {}, using customizer {}", machineLocation, customizer);
customizer.customize(machineLocation);
}
customizedTimestamp = Duration.of(provisioningStopwatch);
try {
String logMessage = "Finished VM "+setup.getDescription()+" creation:"
+ " "+machineLocation.getUser()+"@"+machineLocation.getAddress()+":"+machineLocation.getPort()
+ (Boolean.TRUE.equals(setup.get(LOG_CREDENTIALS))
? "password=" + userCredentials.getOptionalPassword().or("<absent>")
+ " && key=" + userCredentials.getOptionalPrivateKey().or("<absent>")
: "")
+ " ready after "+Duration.of(provisioningStopwatch).toStringRounded()
+ " ("
+ "semaphore obtained in "+Duration.of(semaphoreTimestamp).toStringRounded()+";"
+ template+" template built in "+Duration.of(templateTimestamp).subtract(semaphoreTimestamp).toStringRounded()+";"
+ " "+node+" provisioned in "+Duration.of(provisionTimestamp).subtract(templateTimestamp).toStringRounded()+";"
+ " "+machineLocation+" connection usable in "+Duration.of(usableTimestamp).subtract(provisionTimestamp).toStringRounded()+";"
+ " and os customized in "+Duration.of(customizedTimestamp).subtract(usableTimestamp).toStringRounded()+" - "+Joiner.on(", ").join(customisationForLogging)+")";
LOG.info(logMessage);
} catch (Exception e){
// TODO Remove try-catch! @Nakomis: why did you add it? What exception happened during logging?
Exceptions.propagateIfFatal(e);
LOG.warn("Problem generating log message summarising completion of jclouds machine provisioning "+machineLocation+" by "+this, e);
}
return machineLocation;
} catch (Exception e) {
if (e instanceof RunNodesException && ((RunNodesException)e).getNodeErrors().size() > 0) {
node = Iterables.get(((RunNodesException)e).getNodeErrors().keySet(), 0);
}
// sometimes AWS nodes come up busted (eg ssh not allowed); just throw it back (and maybe try for another one)
boolean destroyNode = (node != null) && Boolean.TRUE.equals(setup.get(DESTROY_ON_FAILURE));
if (e.toString().contains("VPCResourceNotSpecified")) {
LOG.error("Detected that your EC2 account is a legacy 'classic' account, but the recommended instance type requires VPC. "
+ "You can specify the 'eu-central-1' region to avoid this problem, or you can specify a classic-compatible instance type, "
+ "or you can specify a subnet to use with 'networkName' "
+ "(taking care that the subnet auto-assigns public IP's and allows ingress on all ports, "
+ "as Brooklyn does not currently configure security groups for non-default VPC's; "
+ "or setting up Brooklyn to be in the subnet or have a jump host or other subnet access configuration). "
+ "For more information on VPC vs classic see http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-vpc.html.");
}
LOG.error("Failed to start VM for "+setup.getDescription() + (destroyNode ? " (destroying)" : "")
+ (node != null ? "; node "+node : "")
+ " after "+Duration.of(provisioningStopwatch).toStringRounded()
+ (semaphoreTimestamp != null ? " ("
+ "semaphore obtained in "+Duration.of(semaphoreTimestamp).toStringRounded()+";"
+ (templateTimestamp != null && semaphoreTimestamp != null ? " template built in "+Duration.of(templateTimestamp).subtract(semaphoreTimestamp).toStringRounded()+";" : "")
+ (provisionTimestamp != null && templateTimestamp != null ? " node provisioned in "+Duration.of(provisionTimestamp).subtract(templateTimestamp).toStringRounded()+";" : "")
+ (usableTimestamp != null && provisioningStopwatch != null ? " connection usable in "+Duration.of(usableTimestamp).subtract(provisionTimestamp).toStringRounded()+";" : "")
+ (customizedTimestamp != null && usableTimestamp != null ? " and OS customized in "+Duration.of(customizedTimestamp).subtract(usableTimestamp).toStringRounded() : "")
+ ")"
: "")
+ ": "+e.getMessage());
LOG.debug(Throwables.getStackTraceAsString(e));
if (destroyNode) {
Stopwatch destroyingStopwatch = Stopwatch.createStarted();
if (machineLocation != null) {
releaseSafely(machineLocation);
} else {
releaseNodeSafely(node);
}
LOG.info("Destroyed " + (machineLocation != null ? "machine " + machineLocation : "node " + node)
+ " in " + Duration.of(destroyingStopwatch).toStringRounded());
}
throw Exceptions.propagate(e);
}
}
// ------------- suspend and resume ------------------------------------
/**
* Suspends the given location.
* <p>
* Note that this method does <b>not</b> call the lifecycle methods of any
* {@link #getCustomizers(ConfigBag) customizers} attached to this location.
*/
@Override
public void suspendMachine(MachineLocation rawLocation) {
String instanceId = vmInstanceIds.remove(rawLocation);
if (instanceId == null) {
LOG.info("Attempt to suspend unknown machine " + rawLocation + " in " + this);
throw new IllegalArgumentException("Unknown machine " + rawLocation);
}
LOG.info("Suspending machine {} in {}, instance id {}", new Object[]{rawLocation, this, instanceId});
Exception toThrow = null;
try {
getComputeService().suspendNode(instanceId);
} catch (Exception e) {
toThrow = e;
LOG.error("Problem suspending machine " + rawLocation + " in " + this + ", instance id " + instanceId, e);
}
removeChild(rawLocation);
if (toThrow != null) {
throw Exceptions.propagate(toThrow);
}
}
/**
* Brings an existing machine with the given details under management.
* <p/>
* Note that this method does <b>not</b> call the lifecycle methods of any
* {@link #getCustomizers(ConfigBag) customizers} attached to this location.
*
* @param flags See {@link #registerMachine(ConfigBag)} for a description of required fields.
* @see #registerMachine(ConfigBag)
*/
@Override
public MachineLocation resumeMachine(Map<?, ?> flags) {
ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
LOG.info("{} using resuming node matching properties: {}", this, Sanitizer.sanitize(setup));
ComputeService computeService = getComputeService(setup);
NodeMetadata node = findNodeOrThrow(setup);
LOG.debug("{} resuming {}", this, node);
computeService.resumeNode(node.getId());
// Load the node a second time once it is resumed to get an object with
// hostname and addresses populated.
node = findNodeOrThrow(setup);
LOG.debug("{} resumed {}", this, node);
MachineLocation registered = registerMachineLocation(setup, node);
LOG.info("{} resumed and registered {}", this, registered);
return registered;
}
// ------------- constructing the template, etc ------------------------
private static interface CustomizeTemplateBuilder {
void apply(TemplateBuilder tb, ConfigBag props, Object v);
}
public static interface CustomizeTemplateOptions {
void apply(TemplateOptions tb, ConfigBag props, Object v);
}
/** properties which cause customization of the TemplateBuilder */
public static final Map<ConfigKey<?>,CustomizeTemplateBuilder> SUPPORTED_TEMPLATE_BUILDER_PROPERTIES = ImmutableMap.<ConfigKey<?>,CustomizeTemplateBuilder>builder()
.put(OS_64_BIT, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
Boolean os64Bit = TypeCoercions.coerce(v, Boolean.class);
if (os64Bit!=null)
tb.os64Bit(os64Bit);
}})
.put(MIN_RAM, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
tb.minRam( (int)(ByteSizeStrings.parse(Strings.toString(v), "mb")/1000/1000) );
}})
.put(MIN_CORES, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
tb.minCores(TypeCoercions.coerce(v, Double.class));
}})
.put(MIN_DISK, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
tb.minDisk( (int)(ByteSizeStrings.parse(Strings.toString(v), "gb")/1000/1000/1000) );
}})
.put(HARDWARE_ID, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
tb.hardwareId(((CharSequence)v).toString());
}})
.put(IMAGE_ID, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
tb.imageId(((CharSequence)v).toString());
}})
.put(IMAGE_DESCRIPTION_REGEX, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
tb.imageDescriptionMatches(((CharSequence)v).toString());
}})
.put(IMAGE_NAME_REGEX, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
tb.imageNameMatches(((CharSequence)v).toString());
}})
.put(OS_FAMILY, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
Maybe<OsFamily> osFamily = Enums.valueOfIgnoreCase(OsFamily.class, v.toString());
if (osFamily.isAbsent())
throw new IllegalArgumentException("Invalid "+OS_FAMILY+" value "+v);
tb.osFamily(osFamily.get());
}})
.put(OS_VERSION_REGEX, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
tb.osVersionMatches( ((CharSequence)v).toString() );
}})
.put(TEMPLATE_SPEC, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
tb.from(TemplateBuilderSpec.parse(((CharSequence)v).toString()));
}})
.put(DEFAULT_IMAGE_ID, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
/* done in the code, but included here so that it is in the map */
}})
.put(TEMPLATE_BUILDER, new CustomizeTemplateBuilder() {
public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
/* done in the code, but included here so that it is in the map */
}})
.build();
/** properties which cause customization of the TemplateOptions */
public static final Map<ConfigKey<?>,CustomizeTemplateOptions> SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES = ImmutableMap.<ConfigKey<?>,CustomizeTemplateOptions>builder()
.put(SECURITY_GROUPS, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (t instanceof EC2TemplateOptions) {
String[] securityGroups = toStringArray(v);
((EC2TemplateOptions)t).securityGroups(securityGroups);
} else if (t instanceof NovaTemplateOptions) {
String[] securityGroups = toStringArray(v);
((NovaTemplateOptions)t).securityGroups(securityGroups);
} else if (t instanceof SoftLayerTemplateOptions) {
String[] securityGroups = toStringArray(v);
((SoftLayerTemplateOptions)t).securityGroups(securityGroups);
} else if (t instanceof GoogleComputeEngineTemplateOptions) {
String[] securityGroups = toStringArray(v);
((GoogleComputeEngineTemplateOptions)t).securityGroups(securityGroups);
} else {
LOG.info("ignoring securityGroups({}) in VM creation because not supported for cloud/type ({})", v, t.getClass());
}
}})
.put(INBOUND_PORTS, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
int[] inboundPorts = toIntPortArray(v);
if (LOG.isDebugEnabled()) LOG.debug("opening inbound ports {} for cloud/type {}", Arrays.toString(inboundPorts), t.getClass());
t.inboundPorts(inboundPorts);
}})
.put(USER_METADATA_STRING, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (t instanceof EC2TemplateOptions) {
// See AWS docs: http://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/UsingConfig_WinAMI.html#user-data-execution
if (v==null) return;
String data = v.toString();
if (!(data.startsWith("<script>") || data.startsWith("<powershell>"))) {
data = "<script> " + data + " </script>";
}
((EC2TemplateOptions)t).userData(data.getBytes());
} else if (t instanceof SoftLayerTemplateOptions) {
((SoftLayerTemplateOptions)t).userData(Strings.toString(v));
} else {
// Try reflection: userData(String), or guestCustomizationScript(String);
// the latter is used by vCloud Director.
Class<? extends TemplateOptions> clazz = t.getClass();
Method userDataMethod = null;
try {
userDataMethod = clazz.getMethod("userData", String.class);
} catch (SecurityException e) {
LOG.info("Problem reflectively inspecting methods of "+t.getClass()+" for setting userData", e);
} catch (NoSuchMethodException e) {
try {
// For vCloud Director
userDataMethod = clazz.getMethod("guestCustomizationScript", String.class);
} catch (NoSuchMethodException e2) {
// expected on various other clouds
}
}
if (userDataMethod != null) {
try {
userDataMethod.invoke(t, Strings.toString(v));
} catch (InvocationTargetException e) {
LOG.info("Problem invoking "+userDataMethod.getName()+" of "+t.getClass()+", for setting userData (rethrowing)", e);
throw Exceptions.propagate(e);
} catch (IllegalAccessException e) {
LOG.debug("Unable to reflectively invoke "+userDataMethod.getName()+" of "+t.getClass()+", for setting userData (rethrowing)", e);
throw Exceptions.propagate(e);
}
} else {
LOG.info("ignoring userDataString({}) in VM creation because not supported for cloud/type ({})", v, t.getClass());
}
}
}})
.put(USER_DATA_UUENCODED, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (t instanceof EC2TemplateOptions) {
byte[] bytes = toByteArray(v);
((EC2TemplateOptions)t).userData(bytes);
} else if (t instanceof SoftLayerTemplateOptions) {
((SoftLayerTemplateOptions)t).userData(Strings.toString(v));
} else {
LOG.info("ignoring userData({}) in VM creation because not supported for cloud/type ({})", v, t.getClass());
}
}})
.put(STRING_TAGS, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
List<String> tags = toListOfStrings(v);
if (LOG.isDebugEnabled()) LOG.debug("setting VM tags {} for {}", tags, t);
t.tags(tags);
}})
.put(USER_METADATA_MAP, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (v != null) {
t.userMetadata(toMapStringString(v));
}
}})
.put(EXTRA_PUBLIC_KEY_DATA_TO_AUTH, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (t instanceof GoogleComputeEngineTemplateOptions) {
// see email to jclouds list, 29 Aug 2015;
// GCE takes this to be the only login public key,
// and setting this only works if you also overrideLoginPrivateKey
LOG.warn("Ignoring "+EXTRA_PUBLIC_KEY_DATA_TO_AUTH+"; not supported in jclouds-gce implementation.");
}
t.authorizePublicKey(((CharSequence)v).toString());
}})
.put(RUN_AS_ROOT, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
t.runAsRoot((Boolean)v);
}})
.put(LOGIN_USER, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (v != null) {
t.overrideLoginUser(((CharSequence)v).toString());
}
}})
.put(LOGIN_USER_PASSWORD, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (v != null) {
t.overrideLoginPassword(((CharSequence)v).toString());
}
}})
.put(LOGIN_USER_PRIVATE_KEY_FILE, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (v != null) {
String privateKeyFileName = ((CharSequence)v).toString();
String privateKey;
try {
privateKey = Files.toString(new File(Os.tidyPath(privateKeyFileName)), Charsets.UTF_8);
} catch (IOException e) {
LOG.error(privateKeyFileName + "not found", e);
throw Exceptions.propagate(e);
}
t.overrideLoginPrivateKey(privateKey);
}
}})
.put(LOGIN_USER_PRIVATE_KEY_DATA, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (v != null) {
t.overrideLoginPrivateKey(((CharSequence)v).toString());
}
}})
.put(KEY_PAIR, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (t instanceof EC2TemplateOptions) {
((EC2TemplateOptions)t).keyPair(((CharSequence)v).toString());
} else if (t instanceof NovaTemplateOptions) {
((NovaTemplateOptions)t).keyPairName(((CharSequence)v).toString());
} else if (t instanceof CloudStackTemplateOptions) {
((CloudStackTemplateOptions) t).keyPair(((CharSequence) v).toString());
} else {
LOG.info("ignoring keyPair({}) in VM creation because not supported for cloud/type ({})", v, t);
}
}})
.put(AUTO_GENERATE_KEYPAIRS, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (t instanceof NovaTemplateOptions) {
((NovaTemplateOptions)t).generateKeyPair((Boolean)v);
} else if (t instanceof CloudStackTemplateOptions) {
((CloudStackTemplateOptions) t).generateKeyPair((Boolean) v);
} else {
LOG.info("ignoring auto-generate-keypairs({}) in VM creation because not supported for cloud/type ({})", v, t);
}
}})
.put(AUTO_CREATE_FLOATING_IPS, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (t instanceof NovaTemplateOptions) {
((NovaTemplateOptions)t).autoAssignFloatingIp((Boolean)v);
} else {
LOG.info("ignoring auto-generate-floating-ips({}) in VM creation because not supported for cloud/type ({})", v, t);
}
}})
.put(AUTO_ASSIGN_FLOATING_IP, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (t instanceof NovaTemplateOptions) {
((NovaTemplateOptions)t).autoAssignFloatingIp((Boolean)v);
} else if (t instanceof CloudStackTemplateOptions) {
((CloudStackTemplateOptions)t).setupStaticNat((Boolean)v);
} else {
LOG.info("ignoring auto-assign-floating-ip({}) in VM creation because not supported for cloud/type ({})", v, t);
}
}})
.put(NETWORK_NAME, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (t instanceof AWSEC2TemplateOptions) {
// subnet ID is the sensible interpretation of network name in EC2
((AWSEC2TemplateOptions)t).subnetId((String)v);
} else {
if (t instanceof GoogleComputeEngineTemplateOptions) {
// no warning needed
// we think this is the only jclouds endpoint which supports this option
} else if (t instanceof SoftLayerTemplateOptions) {
LOG.warn("networkName is not be supported in SoftLayer; use `templateOptions` with `primaryNetworkComponentNetworkVlanId` or `primaryNetworkBackendComponentNetworkVlanId`");
} else if (!(t instanceof CloudStackTemplateOptions) && !(t instanceof NovaTemplateOptions)) {
LOG.warn("networkName is experimental in many jclouds endpoints may not be supported in this cloud");
// NB, from @andreaturli
// Cloudstack uses custom securityGroupIds and networkIds not the generic networks
// Openstack Nova uses securityGroupNames which is marked as @deprecated (suggests to use groups which is maybe even more confusing)
// Azure supports the custom networkSecurityGroupName
}
t.networks((String)v);
}
}})
.put(DOMAIN_NAME, new CustomizeTemplateOptions() {
public void apply(TemplateOptions t, ConfigBag props, Object v) {
if (t instanceof SoftLayerTemplateOptions) {
((SoftLayerTemplateOptions)t).domainName(TypeCoercions.coerce(v, String.class));
} else {
LOG.info("ignoring domain-name({}) in VM creation because not supported for cloud/type ({})", v, t);
}
}})
.put(TEMPLATE_OPTIONS, new CustomizeTemplateOptions() {
@Override
public void apply(TemplateOptions options, ConfigBag config, Object v) {
if (v == null) return;
@SuppressWarnings("unchecked") Map<String, Object> optionsMap = (Map<String, Object>) v;
if (optionsMap.isEmpty()) return;
Class<? extends TemplateOptions> clazz = options.getClass();
for(final Map.Entry<String, Object> option : optionsMap.entrySet()) {
Maybe<?> result = MethodCoercions.tryFindAndInvokeBestMatchingMethod(options, option.getKey(), option.getValue());
if(result.isAbsent()) {
LOG.warn("Ignoring request to set template option {} because this is not supported by {}", new Object[] { option.getKey(), clazz.getCanonicalName() });
}
}
}})
.build();
/** hook whereby template customizations can be made for various clouds */
protected void customizeTemplate(ConfigBag setup, ComputeService computeService, Template template) {
for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
customizer.customize(this, computeService, template);
customizer.customize(this, computeService, template.getOptions());
}
// these things are nice on softlayer
if (template.getOptions() instanceof SoftLayerTemplateOptions) {
SoftLayerTemplateOptions slT = ((SoftLayerTemplateOptions)template.getOptions());
if (Strings.isBlank(slT.getDomainName()) || "jclouds.org".equals(slT.getDomainName())) {
// set a quasi-sensible domain name if none was provided (better than the default, jclouds.org)
// NB: things like brooklyn.local are disallowed
slT.domainName("local.brooklyncentral.org");
}
// convert user metadata to tags and notes because user metadata is otherwise ignored
Map<String, String> md = slT.getUserMetadata();
if (md!=null && !md.isEmpty()) {
Set<String> tags = MutableSet.copyOf(slT.getTags());
for (Map.Entry<String,String> entry: md.entrySet()) {
tags.add(AbstractCloudMachineNamer.sanitize(entry.getKey())+":"+AbstractCloudMachineNamer.sanitize(entry.getValue()));
}
slT.tags(tags);
if (!md.containsKey("notes")) {
String notes = "User Metadata\n=============\n\n * " + Joiner.on("\n * ").withKeyValueSeparator(": ").join(md);
if (notes.length() > NOTES_MAX_LENGTH) {
String truncatedMsg = "...\n<truncated - notes total length is " + notes.length() + " characters>";
notes = notes.substring(0, NOTES_MAX_LENGTH - truncatedMsg.length()) + truncatedMsg;
}
md.put("notes", notes);
}
}
}
}
/** returns the jclouds Template which describes the image to be built, for the given config and compute service */
public Template buildTemplate(ComputeService computeService, ConfigBag config) {
TemplateBuilder templateBuilder = (TemplateBuilder) config.get(TEMPLATE_BUILDER);
if (templateBuilder==null) {
templateBuilder = new PortableTemplateBuilder<PortableTemplateBuilder<?>>();
} else {
LOG.debug("jclouds using templateBuilder {} as custom base for provisioning in {} for {}", new Object[] {
templateBuilder, this, config.getDescription()});
}
if (templateBuilder instanceof PortableTemplateBuilder<?>) {
if (((PortableTemplateBuilder<?>)templateBuilder).imageChooser()==null) {
Function<Iterable<? extends Image>, Image> chooser = config.get(JcloudsLocationConfig.IMAGE_CHOOSER);
chooser = BrooklynImageChooser.cloneFor(chooser, computeService);
templateBuilder.imageChooser(chooser);
} else {
// an image chooser is already set, so do nothing
}
} else {
// template builder supplied, and we cannot check image chooser status; warn, for now
LOG.warn("Cannot check imageChooser status for {} due to manually supplied black-box TemplateBuilder; "
+ "it is recommended to use a PortableTemplateBuilder if you supply a TemplateBuilder", config.getDescription());
}
if (!Strings.isEmpty(config.get(CLOUD_REGION_ID))) {
templateBuilder.locationId(config.get(CLOUD_REGION_ID));
}
// Apply the template builder and options properties
for (Map.Entry<ConfigKey<?>, CustomizeTemplateBuilder> entry : SUPPORTED_TEMPLATE_BUILDER_PROPERTIES.entrySet()) {
ConfigKey<?> name = entry.getKey();
CustomizeTemplateBuilder code = entry.getValue();
if (config.containsKey(name))
code.apply(templateBuilder, config, config.get(name));
}
if (templateBuilder instanceof PortableTemplateBuilder) {
((PortableTemplateBuilder<?>)templateBuilder).attachComputeService(computeService);
// do the default last, and only if nothing else specified (guaranteed to be a PTB if nothing else specified)
if (groovyTruth(config.get(DEFAULT_IMAGE_ID))) {
if (((PortableTemplateBuilder<?>)templateBuilder).isBlank()) {
templateBuilder.imageId(config.get(DEFAULT_IMAGE_ID).toString());
}
}
}
// Then apply any optional app-specific customization.
for (JcloudsLocationCustomizer customizer : getCustomizers(config)) {
customizer.customize(this, computeService, templateBuilder);
}
LOG.debug("jclouds using templateBuilder {} for provisioning in {} for {}", new Object[] {
templateBuilder, this, config.getDescription()});
// Finally try to build the template
Template template;
Image image;
try {
template = templateBuilder.build();
if (template==null) throw new NullPointerException("No template found (templateBuilder.build returned null)");
image = template.getImage();
LOG.debug("jclouds found template "+template+" (image "+image+") for provisioning in "+this+" for "+config.getDescription());
if (image==null) throw new NullPointerException("Template does not contain an image (templateBuilder.build returned invalid template)");
} catch (AuthorizationException e) {
LOG.warn("Error resolving template: not authorized (rethrowing: "+e+")");
throw new IllegalStateException("Not authorized to access cloud "+this+" to resolve "+templateBuilder, e);
} catch (Exception e) {
try {
IOException ioe = Exceptions.getFirstThrowableOfType(e, IOException.class);
if (ioe != null) {
LOG.warn("IOException found...", ioe);
throw ioe;
}
if (listedAvailableTemplatesOnNoSuchTemplate.compareAndSet(false, true)) {
// delay subsequent log.warns (put in synch block) so the "Loading..." message is obvious
LOG.warn("Unable to match required VM template constraints "+templateBuilder+" when trying to provision VM in "+this+" (rethrowing): "+e);
logAvailableTemplates(config);
}
} catch (Exception e2) {
LOG.warn("Error loading available images to report (following original error matching template which will be rethrown): "+e2, e2);
throw new IllegalStateException("Unable to access cloud "+this+" to resolve "+templateBuilder+": "+e, e);
}
throw new IllegalStateException("Unable to match required VM template constraints "+templateBuilder+" when trying to provision VM in "+this+"; "
+ "see list of images in log. Root cause: "+e, e);
}
TemplateOptions options = template.getOptions();
boolean windows = isWindows(template, config);
if (windows) {
if (!(config.containsKey(JcloudsLocationConfig.USER_METADATA_STRING) || config.containsKey(JcloudsLocationConfig.USER_METADATA_MAP))) {
config.put(JcloudsLocationConfig.USER_METADATA_STRING, WinRmMachineLocation.getDefaultUserMetadataString());
}
}
for (Map.Entry<ConfigKey<?>, CustomizeTemplateOptions> entry : SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES.entrySet()) {
ConfigKey<?> key = entry.getKey();
CustomizeTemplateOptions code = entry.getValue();
if (config.containsKey(key))
code.apply(options, config, config.get(key));
}
return template;
}
protected void logAvailableTemplates(ConfigBag config) {
LOG.info("Loading available images at "+this+" for reference...");
ConfigBag m1 = ConfigBag.newInstanceCopying(config);
if (m1.containsKey(IMAGE_ID)) {
// if caller specified an image ID, remove that, but don't apply default filters
m1.remove(IMAGE_ID);
// TODO use key
m1.putStringKey("anyOwner", true);
}
ComputeService computeServiceLessRestrictive = getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(m1, true);
Set<? extends Image> imgs = computeServiceLessRestrictive.listImages();
LOG.info(""+imgs.size()+" available images at "+this);
for (Image img: imgs) {
LOG.info(" Image: "+img);
}
Set<? extends Hardware> profiles = computeServiceLessRestrictive.listHardwareProfiles();
LOG.info(""+profiles.size()+" available profiles at "+this);
for (Hardware profile: profiles) {
LOG.info(" Profile: "+profile);
}
Set<? extends org.jclouds.domain.Location> assignableLocations = computeServiceLessRestrictive.listAssignableLocations();
LOG.info(""+assignableLocations.size()+" available locations at "+this);
for (org.jclouds.domain.Location assignableLocation: assignableLocations) {
LOG.info(" Location: "+assignableLocation);
}
}
/**
* Creates a temporary ssh machine location (i.e. will not be persisted), which uses the given credentials.
* It ignores any credentials (e.g. password, key-phrase, etc) that are supplied in the config.
*/
protected SshMachineLocation createTemporarySshMachineLocation(HostAndPort hostAndPort, LoginCredentials creds, ConfigBag config) {
String initialUser = creds.getUser();
Optional<String> initialPassword = creds.getOptionalPassword();
Optional<String> initialPrivateKey = creds.getOptionalPrivateKey();
Map<String,Object> sshProps = Maps.newLinkedHashMap(config.getAllConfig());
sshProps.put("user", initialUser);
sshProps.put("address", hostAndPort.getHostText());
sshProps.put("port", hostAndPort.getPort());
sshProps.put(AbstractLocation.TEMPORARY_LOCATION.getName(), true);
sshProps.put(LocalLocationManager.CREATE_UNMANAGED.getName(), true);
sshProps.remove("password");
sshProps.remove("privateKeyData");
sshProps.remove("privateKeyFile");
sshProps.remove("privateKeyPassphrase");
if (initialPassword.isPresent()) sshProps.put("password", initialPassword.get());
if (initialPrivateKey.isPresent()) sshProps.put("privateKeyData", initialPrivateKey.get());
if (isManaged()) {
return getManagementContext().getLocationManager().createLocation(sshProps, SshMachineLocation.class);
} else {
return new SshMachineLocation(sshProps);
}
}
/**
* Creates a temporary WinRM machine location (i.e. will not be persisted), which uses the given credentials.
* It ignores any credentials (e.g. password, key-phrase, etc) that are supplied in the config.
*/
protected WinRmMachineLocation createTemporaryWinRmMachineLocation(HostAndPort hostAndPort, LoginCredentials creds, ConfigBag config) {
String initialUser = creds.getUser();
Optional<String> initialPassword = creds.getOptionalPassword();
Optional<String> initialPrivateKey = creds.getOptionalPrivateKey();
Map<String,Object> winrmProps = Maps.newLinkedHashMap(config.getAllConfig());
winrmProps.put("user", initialUser);
winrmProps.put("address", hostAndPort.getHostText());
winrmProps.put("port", hostAndPort.getPort());
winrmProps.put(AbstractLocation.TEMPORARY_LOCATION.getName(), true);
winrmProps.put(LocalLocationManager.CREATE_UNMANAGED.getName(), true);
winrmProps.remove("password");
winrmProps.remove("privateKeyData");
winrmProps.remove("privateKeyFile");
winrmProps.remove("privateKeyPassphrase");
if (initialPassword.isPresent()) winrmProps.put("password", initialPassword.get());
if (initialPrivateKey.isPresent()) winrmProps.put("privateKeyData", initialPrivateKey.get());
if (isManaged()) {
return getManagementContext().getLocationManager().createLocation(winrmProps, WinRmMachineLocation.class);
} else {
throw new UnsupportedOperationException("Cannot create temporary WinRmMachineLocation because " + this + " is not managed");
}
}
/**
* Create the user immediately - executing ssh commands as required.
*/
protected LoginCredentials createUser(ComputeService computeService, NodeMetadata node, Optional<HostAndPort> hostAndPortOverride, LoginCredentials initialCredentials, ConfigBag config) {
Image image = (node.getImageId() != null) ? computeService.getImage(node.getImageId()) : null;
UserCreation userCreation = createUserStatements(image, config);
if (!userCreation.statements.isEmpty()) {
// If unsure of OS family, default to unix for rendering statements.
org.jclouds.scriptbuilder.domain.OsFamily scriptOsFamily;
if (isWindows(node, config)) {
scriptOsFamily = org.jclouds.scriptbuilder.domain.OsFamily.WINDOWS;
} else {
scriptOsFamily = org.jclouds.scriptbuilder.domain.OsFamily.UNIX;
}
boolean windows = isWindows(node, config);
if (windows) {
LOG.warn("Unable to execute statements on WinRM in JcloudsLocation; skipping for "+node+": "+userCreation.statements);
} else {
List<String> commands = Lists.newArrayList();
for (Statement statement : userCreation.statements) {
InitAdminAccess initAdminAccess = new InitAdminAccess(new AdminAccessConfiguration.Default());
initAdminAccess.visit(statement);
commands.add(statement.render(scriptOsFamily));
}
String initialUser = initialCredentials.getUser();
String address = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getHostText() : getFirstReachableAddress(node, config);
int port = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getPort() : node.getLoginPort();
// TODO Retrying lots of times as workaround for vcloud-director. There the guest customizations
// can cause the VM to reboot shortly after it was ssh'able.
Map<String,Object> execProps = Maps.newLinkedHashMap();
execProps.put(ShellTool.PROP_RUN_AS_ROOT.getName(), true);
execProps.put(SshTool.PROP_SSH_TRIES.getName(), 50);
execProps.put(SshTool.PROP_SSH_TRIES_TIMEOUT.getName(), 10*60*1000);
if (LOG.isDebugEnabled()) {
LOG.debug("VM {}: executing user creation/setup via {}@{}:{}; commands: {}", new Object[] {
config.getDescription(), initialUser, address, port, commands});
}
HostAndPort hostAndPort = hostAndPortOverride.isPresent() ? hostAndPortOverride.get() : HostAndPort.fromParts(address, port);
SshMachineLocation sshLoc = createTemporarySshMachineLocation(hostAndPort, initialCredentials, config);
try {
// BROOKLYN-188: for SUSE, need to specify the path (for groupadd, useradd, etc)
Map<String, ?> env = ImmutableMap.of("PATH", sbinPath());
int exitcode = sshLoc.execScript(execProps, "create-user", commands, env);
if (exitcode != 0) {
LOG.warn("exit code {} when creating user for {}; usage may subsequently fail", exitcode, node);
}
} finally {
getManagementContext().getLocationManager().unmanage(sshLoc);
Streams.closeQuietly(sshLoc);
}
}
}
return userCreation.createdUserCredentials;
}
/**
* Setup the TemplateOptions to create the user.
*/
protected LoginCredentials initTemplateForCreateUser(Template template, ConfigBag config) {
UserCreation userCreation = createUserStatements(template.getImage(), config);
if (userCreation.statements.size() > 0) {
TemplateOptions options = template.getOptions();
options.runScript(new StatementList(userCreation.statements));
}
return userCreation.createdUserCredentials;
}
protected static class UserCreation {
public final LoginCredentials createdUserCredentials;
public final List<Statement> statements;
public UserCreation(LoginCredentials creds, List<Statement> statements) {
this.createdUserCredentials = creds;
this.statements = statements;
}
}
/**
* Returns the commands required to create the user, to be used for connecting (e.g. over ssh)
* to the machine; also returns the expected login credentials.
* <p>
* The returned login credentials may be null if we haven't done any user-setup and no specific
* user was supplied (i.e. if {@code dontCreateUser} was true and {@code user} was null or blank).
* In which case, the caller should use the jclouds node's login credentials.
* <p>
* There are quite a few configuration options. Depending on their values, the user-creation
* behaves differently:
* <ul>
* <li>{@code dontCreateUser} says not to run any user-setup commands at all. If {@code user} is
* non-empty (including with the default value), then that user will subsequently be used,
* otherwise the (inferred) {@code loginUser} will be used.
* <li>{@code loginUser} refers to the existing user that jclouds should use when setting up the VM.
* Normally this will be inferred from the image (i.e. doesn't need to be explicitly set), but sometimes
* the image gets it wrong so this can be a handy override.
* <li>{@code user} is the username for brooklyn to subsequently use when ssh'ing to the machine.
* If not explicitly set, its value will default to the username of the user running brooklyn.
* <ul>
* <li>If the {@code user} value is null or empty, then the (inferred) {@code loginUser} will
* subsequently be used, setting up the password/authorizedKeys for that loginUser.
* <li>If the {@code user} is "root", then setup the password/authorizedKeys for root.
* <li>If the {@code user} equals the (inferred) {@code loginUser}, then don't try to create this
* user but instead just setup the password/authorizedKeys for the user.
* <li>Otherwise create the given user, setting up the password/authorizedKeys (unless
* {@code dontCreateUser} is set, obviously).
* </ul>
* <li>{@code publicKeyData} is the key to authorize (i.e. add to .ssh/authorized_keys),
* if not null or blank. Note the default is to use {@code ~/.ssh/id_rsa.pub} or {@code ~/.ssh/id_dsa.pub}
* if either of those files exist for the user running brooklyn.
* Related is {@code publicKeyFile}, which is used to populate publicKeyData.
* <li>{@code password} is the password to set for the user. If null or blank, then a random password
* will be auto-generated and set.
* <li>{@code privateKeyData} is the key to use when subsequent ssh'ing, if not null or blank.
* Note the default is to use {@code ~/.ssh/id_rsa} or {@code ~/.ssh/id_dsa}.
* The subsequent preferences for ssh'ing are:
* <ul>
* <li>Use the {@code privateKeyData} if not null or blank (including if using default)
* <li>Use the {@code password} (or the auto-generated password if that is blank).
* </ul>
* <li>{@code grantUserSudo} determines whether or not the created user may run the sudo command.</li>
* </ul>
*
* @param image The image being used to create the VM
* @param config Configuration for creating the VM
* @return The commands required to create the user, along with the expected login credentials for that user,
* or null if we are just going to use those from jclouds.
*/
protected UserCreation createUserStatements(@Nullable Image image, ConfigBag config) {
//NB: private key is not installed remotely, just used to get/validate the public key
boolean windows = isWindows(image, config);
String user = getUser(config);
String explicitLoginUser = config.get(LOGIN_USER);
String loginUser = groovyTruth(explicitLoginUser) ? explicitLoginUser : (image != null && image.getDefaultCredentials() != null) ? image.getDefaultCredentials().identity : null;
boolean dontCreateUser = config.get(DONT_CREATE_USER);
boolean grantUserSudo = config.get(GRANT_USER_SUDO);
OsCredential credential = LocationConfigUtils.getOsCredential(config);
credential.checkNoErrors().logAnyWarnings();
String passwordToSet = Strings.isNonBlank(credential.getPassword()) ? credential.getPassword() : Identifiers.makeRandomId(12);
List<Statement> statements = Lists.newArrayList();
LoginCredentials createdUserCreds = null;
if (dontCreateUser) {
// dontCreateUser:
// if caller has not specified a user, we'll just continue to use the loginUser;
// if caller *has*, we set up our credentials assuming that user and credentials already exist
if (Strings.isBlank(user)) {
// createdUserCreds returned from this method will be null;
// we will use the creds returned by jclouds on the node
LOG.info("Not setting up any user (subsequently using loginUser {})", user, loginUser);
config.put(USER, loginUser);
} else {
LOG.info("Not creating user {}, and not installing its password or authorizing keys (assuming it exists)", user);
if (credential.isUsingPassword()) {
createdUserCreds = LoginCredentials.builder().user(user).password(credential.getPassword()).build();
if (Boolean.FALSE.equals(config.get(DISABLE_ROOT_AND_PASSWORD_SSH))) {
statements.add(org.jclouds.scriptbuilder.statements.ssh.SshStatements.sshdConfig(ImmutableMap.of("PasswordAuthentication", "yes")));
}
} else if (credential.hasKey()) {
createdUserCreds = LoginCredentials.builder().user(user).privateKey(credential.getPrivateKeyData()).build();
}
}
} else if (windows) {
// TODO Generate statements to create the user.
// createdUserCreds returned from this method will be null;
// we will use the creds returned by jclouds on the node
LOG.warn("Not creating or configuring user on Windows VM, despite "+DONT_CREATE_USER.getName()+" set to false");
// TODO extractVmCredentials() will use user:publicKeyData defaults, if we don't override this.
// For linux, how would we configure Brooklyn to use the node.getCredentials() - i.e. the version
// that the cloud automatically generated?
if (config.get(USER) != null) config.put(USER, "");
if (config.get(PASSWORD) != null) config.put(PASSWORD, "");
if (config.get(PRIVATE_KEY_DATA) != null) config.put(PRIVATE_KEY_DATA, "");
if (config.get(PRIVATE_KEY_FILE) != null) config.put(PRIVATE_KEY_FILE, "");
if (config.get(PUBLIC_KEY_DATA) != null) config.put(PUBLIC_KEY_DATA, "");
if (config.get(PUBLIC_KEY_FILE) != null) config.put(PUBLIC_KEY_FILE, "");
} else if (Strings.isBlank(user) || user.equals(loginUser) || user.equals(ROOT_USERNAME)) {
boolean useKey = Strings.isNonBlank(credential.getPublicKeyData());
// For subsequent ssh'ing, we'll be using the loginUser
if (Strings.isBlank(user)) {
user = loginUser;
config.put(USER, user);
}
// Using the pre-existing loginUser; setup the publicKey/password so can login as expected
// *Always* change the password (unless dontCreateUser was specified)
statements.add(new ReplaceShadowPasswordEntry(Sha512Crypt.function(), user, passwordToSet));
createdUserCreds = LoginCredentials.builder().user(user).password(passwordToSet).build();
if (useKey) {
statements.add(new AuthorizeRSAPublicKeys("~"+user+"/.ssh", ImmutableList.of(credential.getPublicKeyData())));
if (Strings.isNonBlank(credential.getPrivateKeyData())) {
createdUserCreds = LoginCredentials.builder().user(user).privateKey(credential.getPrivateKeyData()).build();
}
}
if (!useKey || Boolean.FALSE.equals(config.get(DISABLE_ROOT_AND_PASSWORD_SSH))) {
// ensure password is permitted for ssh
statements.add(org.jclouds.scriptbuilder.statements.ssh.SshStatements.sshdConfig(ImmutableMap.of("PasswordAuthentication", "yes")));
if (user.equals(ROOT_USERNAME)) {
statements.add(org.jclouds.scriptbuilder.statements.ssh.SshStatements.sshdConfig(ImmutableMap.of("PermitRootLogin", "yes")));
}
}
} else {
String pubKey = credential.getPublicKeyData();
String privKey = credential.getPrivateKeyData();
if (credential.isEmpty()) {
/*
* TODO have an explicit `create_new_key_per_machine` config key.
* error if privateKeyData is set in this case.
* publicKeyData automatically added to EXTRA_SSH_KEY_URLS_TO_AUTH.
*
* if this config key is not set, use a key `brooklyn_id_rsa` and `.pub` in `MGMT_BASE_DIR`,
* with permission 0600, creating it if necessary, and logging the fact that this was created.
*/
if (!config.containsKey(PRIVATE_KEY_FILE) && loggedSshKeysHint.compareAndSet(false, true)) {
LOG.info("Default SSH keys not found or not usable; will create new keys for each machine. "
+ "Create ~/.ssh/id_rsa or "
+ "set "+PRIVATE_KEY_FILE.getName()+" / "+PRIVATE_KEY_PASSPHRASE.getName()+" / "+PASSWORD.getName()+" "
+ "as appropriate for this location if you wish to be able to log in without Brooklyn.");
}
KeyPair newKeyPair = SecureKeys.newKeyPair();
pubKey = SecureKeys.toPub(newKeyPair);
privKey = SecureKeys.toPem(newKeyPair);
LOG.debug("Brooklyn key being created for "+user+" at new machine "+this+" is:\n"+privKey);
}
// ensure credential is not used any more, as we have extracted all useful info
credential = null;
// Create the user
// note AdminAccess requires _all_ fields set, due to http://code.google.com/p/jclouds/issues/detail?id=1095
AdminAccess.Builder adminBuilder = AdminAccess.builder()
.adminUsername(user)
.grantSudoToAdminUser(grantUserSudo);
adminBuilder.cryptFunction(Sha512Crypt.function());
boolean useKey = Strings.isNonBlank(pubKey);
adminBuilder.cryptFunction(Sha512Crypt.function());
// always set this password; if not supplied, it will be a random string
adminBuilder.adminPassword(passwordToSet);
// log the password also, in case we need it
LOG.debug("Password '"+passwordToSet+"' being created for user '"+user+"' at the machine we are about to provision in "+this+"; "+
(useKey ? "however a key will be used to access it" : "this will be the only way to log in"));
if (grantUserSudo && config.get(JcloudsLocationConfig.DISABLE_ROOT_AND_PASSWORD_SSH)) {
// the default - set root password which we forget, because we have sudo acct
// (and lock out root and passwords from ssh)
adminBuilder.resetLoginPassword(true);
adminBuilder.loginPassword(Identifiers.makeRandomId(12));
} else {
adminBuilder.resetLoginPassword(false);
adminBuilder.loginPassword(Identifiers.makeRandomId(12)+"-ignored");
}
if (useKey) {
adminBuilder.authorizeAdminPublicKey(true).adminPublicKey(pubKey);
} else {
adminBuilder.authorizeAdminPublicKey(false).adminPublicKey(Identifiers.makeRandomId(12)+"-ignored");
}
// jclouds wants us to give it the private key, otherwise it might refuse to authorize the public key
// (in AdminAccess.build, if adminUsername != null && adminPassword != null);
// we don't want to give it the private key, but we *do* want the public key authorized;
// this code seems to trigger that.
// (we build the creds below)
adminBuilder.installAdminPrivateKey(false).adminPrivateKey(Identifiers.makeRandomId(12)+"-ignored");
// lock SSH means no root login and no passwordless login
// if we're using a password or we don't have sudo, then don't do this!
adminBuilder.lockSsh(useKey && grantUserSudo && config.get(JcloudsLocationConfig.DISABLE_ROOT_AND_PASSWORD_SSH));
statements.add(adminBuilder.build());
if (useKey) {
createdUserCreds = LoginCredentials.builder().user(user).privateKey(privKey).build();
} else if (passwordToSet!=null) {
createdUserCreds = LoginCredentials.builder().user(user).password(passwordToSet).build();
}
if (!useKey || Boolean.FALSE.equals(config.get(DISABLE_ROOT_AND_PASSWORD_SSH))) {
// ensure password is permitted for ssh
statements.add(org.jclouds.scriptbuilder.statements.ssh.SshStatements.sshdConfig(ImmutableMap.of("PasswordAuthentication", "yes")));
}
}
String customTemplateOptionsScript = config.get(CUSTOM_TEMPLATE_OPTIONS_SCRIPT_CONTENTS);
if (Strings.isNonBlank(customTemplateOptionsScript)) {
statements.add(new LiteralStatement(customTemplateOptionsScript));
}
LOG.debug("Machine we are about to create in "+this+" will be customized with: "+
statements);
return new UserCreation(createdUserCreds, statements);
}
// ----------------- registering existing machines ------------------------
/**
* @deprecated since 0.8.0 use {@link #registerMachine(NodeMetadata)} instead.
*/
@Deprecated
public JcloudsSshMachineLocation rebindMachine(NodeMetadata metadata) throws NoMachinesAvailableException {
return (JcloudsSshMachineLocation) registerMachine(metadata);
}
protected MachineLocation registerMachine(NodeMetadata metadata) throws NoMachinesAvailableException {
return registerMachine(MutableMap.of(), metadata);
}
/**
* @deprecated since 0.8.0 use {@link #registerMachine(Map, NodeMetadata)} instead.
*/
@Deprecated
public JcloudsSshMachineLocation rebindMachine(Map<?,?> flags, NodeMetadata metadata) throws NoMachinesAvailableException {
return (JcloudsSshMachineLocation) registerMachine(flags, metadata);
}
protected MachineLocation registerMachine(Map<?, ?> flags, NodeMetadata metadata) throws NoMachinesAvailableException {
ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
if (!setup.containsKey("id")) setup.putStringKey("id", metadata.getId());
setHostnameUpdatingCredentials(setup, metadata);
return registerMachine(setup);
}
/**
* Brings an existing machine with the given details under management.
* <p>
* This method will throw an exception if used to reconnect to a Windows VM.
* @deprecated since 0.8.0 use {@link #registerMachine(ConfigBag)} instead.
*/
@Deprecated
public JcloudsSshMachineLocation rebindMachine(ConfigBag setup) throws NoMachinesAvailableException {
return (JcloudsSshMachineLocation) registerMachine(setup);
}
/**
* Brings an existing machine with the given details under management.
* <p>
* Required fields are:
* <ul>
* <li>id: the jclouds VM id, e.g. "eu-west-1/i-5504f21d" (NB this is {@see JcloudsMachineLocation#getJcloudsId()} not #getId())
* <li>hostname: the public hostname or IP of the machine, e.g. "ec2-176-34-93-58.eu-west-1.compute.amazonaws.com"
* <li>userName: the username for sshing into the machine (for use if it is not a Windows system)
* <ul>
*/
public MachineLocation registerMachine(ConfigBag setup) throws NoMachinesAvailableException {
NodeMetadata node = findNodeOrThrow(setup);
return registerMachineLocation(setup, node);
}
protected MachineLocation registerMachineLocation(ConfigBag setup, NodeMetadata node) {
ComputeService computeService = getComputeService(setup);
if (isWindows(node, setup)) {
return registerWinRmMachineLocation(computeService, node, null, Optional.<HostAndPort>absent(), setup);
} else {
try {
return registerJcloudsSshMachineLocation(computeService, node, Optional.<Template>absent(), null, Optional.<HostAndPort>absent(), setup);
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
}
/**
* Finds a node matching the properties given in config or throws an exception.
* @param config
* @return
*/
protected NodeMetadata findNodeOrThrow(ConfigBag config) {
if (config.getDescription() == null) {
setCreationString(config);
}
String user = checkNotNull(getUser(config), "user");
String rawId = (String) config.getStringKey("id");
String rawHostname = (String) config.getStringKey("hostname");
Predicate<ComputeMetadata> predicate = getRebindToMachinePredicate(config);
LOG.debug("Finding VM {} ({}@{}), in jclouds location for provider {} matching {}", new Object[]{
rawId != null ? rawId : "<lookup>",
user,
rawHostname != null ? rawHostname : "<unspecified>",
getProvider(),
predicate
});
ComputeService computeService = getComputeService(config);
Set<? extends NodeMetadata> candidateNodes = computeService.listNodesDetailsMatching(predicate);
if (candidateNodes.isEmpty()) {
throw new IllegalArgumentException("Jclouds node not found for rebind with predicate " + predicate);
} else if (candidateNodes.size() > 1) {
throw new IllegalArgumentException("Jclouds node for rebind matched multiple with " + predicate + ": " + candidateNodes);
}
NodeMetadata node = Iterables.getOnlyElement(candidateNodes);
String pkd = LocationConfigUtils.getOsCredential(config).checkNoErrors().logAnyWarnings().getPrivateKeyData();
if (Strings.isNonBlank(pkd)) {
LoginCredentials expectedCredentials = LoginCredentials.fromCredentials(new Credentials(user, pkd));
//override credentials
node = NodeMetadataBuilder.fromNodeMetadata(node).credentials(expectedCredentials).build();
}
return node;
}
/**
* @deprecated since 0.8.0 use {@link #registerMachine(Map)} instead.
*/
@Deprecated
public JcloudsSshMachineLocation rebindMachine(Map<?, ?> flags) throws NoMachinesAvailableException {
return (JcloudsSshMachineLocation) registerMachine(flags);
}
public MachineLocation registerMachine(Map<?,?> flags) throws NoMachinesAvailableException {
ConfigBag setup = ConfigBag.newInstanceExtending(config().getBag(), flags);
return registerMachine(setup);
}
/**
* @return a predicate that returns true if a {@link ComputeMetadata} instance is suitable for
* rebinding to given the configuration in {@link ConfigBag config}.
*/
protected Predicate<ComputeMetadata> getRebindToMachinePredicate(ConfigBag config) {
return new RebindToMachinePredicate(config);
}
/**
* Determines whether a machine may be rebinded to by comparing the given id, hostname and region
* against the node's id, hostname, provider id and public addresses.
*/
private static class RebindToMachinePredicate implements Predicate<ComputeMetadata> {
final String rawId;
final String rawHostname;
final String rawRegion;
public RebindToMachinePredicate(ConfigBag config) {
rawId = (String) config.getStringKey("id");
rawHostname = (String) config.getStringKey("hostname");
rawRegion = (String) config.getStringKey("region");
}
@Override
public boolean apply(ComputeMetadata input) {
// ID exact match
if (rawId != null) {
// Second is AWS format
if (rawId.equals(input.getId()) || rawRegion != null && (rawRegion + "/" + rawId).equals(input.getId())) {
return true;
}
}
// else do node metadata lookup
if (input instanceof NodeMetadata) {
NodeMetadata node = NodeMetadata.class.cast(input);
if (rawHostname != null && rawHostname.equalsIgnoreCase(node.getHostname()))
return true;
if (rawHostname != null && node.getPublicAddresses().contains(rawHostname))
return true;
if (rawId != null && rawId.equalsIgnoreCase(node.getHostname()))
return true;
if (rawId != null && node.getPublicAddresses().contains(rawId))
return true;
// don't do private IPs because they might be repeated
if (rawId != null && rawId.equalsIgnoreCase(node.getProviderId()))
return true;
if (rawHostname != null && rawHostname.equalsIgnoreCase(node.getProviderId()))
return true;
}
return false;
}
@Override
public String toString() {
return Objects.toStringHelper(this)
.omitNullValues()
.add("id", rawId)
.add("hostname", rawHostname)
.add("region", rawRegion)
.toString();
}
}
// -------------- create the SshMachineLocation instance, and connect to it etc ------------------------
/** @deprecated since 0.7.0 use {@link #registerJcloudsSshMachineLocation(ComputeService, NodeMetadata, LoginCredentials, Optional, ConfigBag)} */
@Deprecated
protected final JcloudsSshMachineLocation registerJcloudsSshMachineLocation(NodeMetadata node, String vmHostname, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) throws IOException {
LOG.warn("Using deprecated registerJcloudsSshMachineLocation: now wants computeService passed", new Throwable("source of deprecated registerJcloudsSshMachineLocation invocation"));
return registerJcloudsSshMachineLocation(null, node, Optional.<Template>absent(), null, sshHostAndPort, setup);
}
/**
* @deprecated since 0.9.0; see {@link #registerJcloudsSshMachineLocation(ComputeService, NodeMetadata, Optional, LoginCredentials, Optional, ConfigBag)}.
* Marked as final to warn those trying to sub-class.
*/
@Deprecated
protected final JcloudsSshMachineLocation registerJcloudsSshMachineLocation(ComputeService computeService, NodeMetadata node, LoginCredentials userCredentials, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) throws IOException {
return registerJcloudsSshMachineLocation(computeService, node, Optional.<Template>absent(), userCredentials, sshHostAndPort, setup);
}
protected JcloudsSshMachineLocation registerJcloudsSshMachineLocation(ComputeService computeService, NodeMetadata node, Optional<Template> template, LoginCredentials userCredentials, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) throws IOException {
if (userCredentials == null)
userCredentials = node.getCredentials();
String vmHostname = getPublicHostname(node, sshHostAndPort, setup);
JcloudsSshMachineLocation machine = createJcloudsSshMachineLocation(computeService, node, template, vmHostname, sshHostAndPort, userCredentials, setup);
registerJcloudsMachineLocation(node.getId(), machine);
return machine;
}
@VisibleForTesting
protected void registerJcloudsMachineLocation(String nodeId, JcloudsMachineLocation machine) {
machine.setParent(this);
vmInstanceIds.put(machine, nodeId);
}
/**
* @deprecated since 0.9.0; see {@link #createJcloudsSshMachineLocation(ComputeService, NodeMetadata, Optional, String, Optional, LoginCredentials, ConfigBag)}.
* Marked as final to warn those trying to sub-class.
*/
@Deprecated
protected final JcloudsSshMachineLocation createJcloudsSshMachineLocation(ComputeService computeService, NodeMetadata node, String vmHostname, Optional<HostAndPort> sshHostAndPort, LoginCredentials userCredentials, ConfigBag setup) throws IOException {
return createJcloudsSshMachineLocation(computeService, node, Optional.<Template>absent(), vmHostname, sshHostAndPort, userCredentials, setup);
}
protected JcloudsSshMachineLocation createJcloudsSshMachineLocation(ComputeService computeService, NodeMetadata node, Optional<Template> template, String vmHostname, Optional<HostAndPort> sshHostAndPort, LoginCredentials userCredentials, ConfigBag setup) throws IOException {
Map<?,?> sshConfig = extractSshConfig(setup, node);
String nodeAvailabilityZone = extractAvailabilityZone(setup, node);
String nodeRegion = extractRegion(setup, node);
if (nodeRegion == null) {
// e.g. rackspace doesn't have "region", so rackspace-uk is best we can say (but zone="LON")
nodeRegion = extractProvider(setup, node);
}
String address = sshHostAndPort.isPresent() ? sshHostAndPort.get().getHostText() : vmHostname;
try {
Networking.getInetAddressWithFixedName(address);
// fine, it resolves
} catch (Exception e) {
// occurs if an unresolvable hostname is given as vmHostname, and the machine only has private IP addresses but they are reachable
// TODO cleanup use of getPublicHostname so its semantics are clearer, returning reachable hostname or ip, and
// do this check/fix there instead of here!
Exceptions.propagateIfFatal(e);
LOG.debug("Could not resolve reported address '"+address+"' for "+vmHostname+" ("+setup.getDescription()+"/"+node+"), requesting reachable address");
if (computeService==null) throw Exceptions.propagate(e);
// this has sometimes already been done in waitForReachable (unless skipped) but easy enough to do again
address = getFirstReachableAddress(node, setup);
}
if (LOG.isDebugEnabled())
LOG.debug("creating JcloudsSshMachineLocation representation for {}@{} ({}/{}) for {}/{}",
new Object[] {
getUser(setup),
address,
Sanitizer.sanitize(sshConfig),
sshHostAndPort,
setup.getDescription(),
node
});
if (isManaged()) {
return getManagementContext().getLocationManager().createLocation(LocationSpec.create(JcloudsSshMachineLocation.class)
.configure("displayName", vmHostname)
.configure("address", address)
.configure(JcloudsSshMachineLocation.SSH_PORT, sshHostAndPort.isPresent() ? sshHostAndPort.get().getPort() : node.getLoginPort())
// don't think "config" does anything
.configure(sshConfig)
// FIXME remove "config" -- inserted directly, above
.configure("config", sshConfig)
.configure("user", userCredentials.getUser())
.configure(SshMachineLocation.PASSWORD, userCredentials.getOptionalPassword().orNull())
.configure(SshMachineLocation.PRIVATE_KEY_DATA, userCredentials.getOptionalPrivateKey().orNull())
.configure("jcloudsParent", this)
.configure("node", node)
.configure("template", template.orNull())
.configureIfNotNull(CLOUD_AVAILABILITY_ZONE_ID, nodeAvailabilityZone)
.configureIfNotNull(CLOUD_REGION_ID, nodeRegion)
.configure(CALLER_CONTEXT, setup.get(CALLER_CONTEXT))
.configure(SshMachineLocation.DETECT_MACHINE_DETAILS, setup.get(SshMachineLocation.DETECT_MACHINE_DETAILS))
.configureIfNotNull(SshMachineLocation.SCRIPT_DIR, setup.get(SshMachineLocation.SCRIPT_DIR))
.configureIfNotNull(USE_PORT_FORWARDING, setup.get(USE_PORT_FORWARDING))
.configureIfNotNull(PORT_FORWARDER, setup.get(PORT_FORWARDER))
.configureIfNotNull(PORT_FORWARDING_MANAGER, setup.get(PORT_FORWARDING_MANAGER)));
} else {
LOG.warn("Using deprecated JcloudsSshMachineLocation constructor because "+this+" is not managed");
return new JcloudsSshMachineLocation(MutableMap.builder()
.put("displayName", vmHostname)
.put("address", address)
.put("port", sshHostAndPort.isPresent() ? sshHostAndPort.get().getPort() : node.getLoginPort())
// don't think "config" does anything
.putAll(sshConfig)
// FIXME remove "config" -- inserted directly, above
.put("config", sshConfig)
.put("user", userCredentials.getUser())
.putIfNotNull(SshMachineLocation.PASSWORD.getName(), userCredentials.getOptionalPassword().orNull())
.putIfNotNull(SshMachineLocation.PRIVATE_KEY_DATA.getName(), userCredentials.getOptionalPrivateKey().orNull())
.put("callerContext", setup.get(CALLER_CONTEXT))
.putIfNotNull(CLOUD_AVAILABILITY_ZONE_ID.getName(), nodeAvailabilityZone)
.putIfNotNull(CLOUD_REGION_ID.getName(), nodeRegion)
.put(USE_PORT_FORWARDING, setup.get(USE_PORT_FORWARDING))
.put(PORT_FORWARDER, setup.get(PORT_FORWARDER))
.put(PORT_FORWARDING_MANAGER, setup.get(PORT_FORWARDING_MANAGER))
.build(),
this,
node);
}
}
protected JcloudsWinRmMachineLocation registerWinRmMachineLocation(ComputeService computeService, NodeMetadata node, LoginCredentials initialCredentials, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) {
if (initialCredentials==null)
initialCredentials = node.getCredentials();
String vmHostname = getPublicHostname(node, sshHostAndPort, setup);
JcloudsWinRmMachineLocation machine = createWinRmMachineLocation(computeService, node, vmHostname, sshHostAndPort, setup);
registerJcloudsMachineLocation(node.getId(), machine);
return machine;
}
protected JcloudsWinRmMachineLocation createWinRmMachineLocation(ComputeService computeService, NodeMetadata node, String vmHostname, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) {
String nodeAvailabilityZone = extractAvailabilityZone(setup, node);
String nodeRegion = extractRegion(setup, node);
if (nodeRegion == null) {
// e.g. rackspace doesn't have "region", so rackspace-uk is best we can say (but zone="LON")
nodeRegion = extractProvider(setup, node);
}
String address = sshHostAndPort.isPresent() ? sshHostAndPort.get().getHostText() : vmHostname;
if (isManaged()) {
return getManagementContext().getLocationManager().createLocation(LocationSpec.create(JcloudsWinRmMachineLocation.class)
.configure("jcloudsParent", this)
.configure("displayName", vmHostname)
.configure("address", address)
.configure(WinRmMachineLocation.WINRM_PORT, sshHostAndPort.isPresent() ? sshHostAndPort.get().getPort() : node.getLoginPort())
.configure("user", getUser(setup))
.configure(WinRmMachineLocation.USER, setup.get(USER))
.configure(WinRmMachineLocation.PASSWORD, setup.get(PASSWORD))
.configure("node", node)
.configureIfNotNull(CLOUD_AVAILABILITY_ZONE_ID, nodeAvailabilityZone)
.configureIfNotNull(CLOUD_REGION_ID, nodeRegion)
.configure(CALLER_CONTEXT, setup.get(CALLER_CONTEXT))
.configure(SshMachineLocation.DETECT_MACHINE_DETAILS, setup.get(SshMachineLocation.DETECT_MACHINE_DETAILS))
.configureIfNotNull(SshMachineLocation.SCRIPT_DIR, setup.get(SshMachineLocation.SCRIPT_DIR))
.configureIfNotNull(USE_PORT_FORWARDING, setup.get(USE_PORT_FORWARDING))
.configureIfNotNull(PORT_FORWARDER, setup.get(PORT_FORWARDER))
.configureIfNotNull(PORT_FORWARDING_MANAGER, setup.get(PORT_FORWARDING_MANAGER)));
} else {
throw new UnsupportedOperationException("Cannot create WinRmMachineLocation because " + this + " is not managed");
}
}
// -------------- give back the machines------------------
protected Map<String,Object> extractSshConfig(ConfigBag setup, NodeMetadata node) {
ConfigBag nodeConfig = new ConfigBag();
if (node!=null && node.getCredentials() != null) {
nodeConfig.putIfNotNull(PASSWORD, node.getCredentials().getOptionalPassword().orNull());
nodeConfig.putIfNotNull(PRIVATE_KEY_DATA, node.getCredentials().getOptionalPrivateKey().orNull());
}
return extractSshConfig(setup, nodeConfig).getAllConfig();
}
protected String extractAvailabilityZone(ConfigBag setup, NodeMetadata node) {
return extractNodeLocationId(setup, node, LocationScope.ZONE);
}
protected String extractRegion(ConfigBag setup, NodeMetadata node) {
return extractNodeLocationId(setup, node, LocationScope.REGION);
}
protected String extractProvider(ConfigBag setup, NodeMetadata node) {
return extractNodeLocationId(setup, node, LocationScope.PROVIDER);
}
protected String extractNodeLocationId(ConfigBag setup, NodeMetadata node, LocationScope scope) {
org.jclouds.domain.Location nodeLoc = node.getLocation();
if(nodeLoc == null) return null;
do {
if (nodeLoc.getScope() == scope) return nodeLoc.getId();
nodeLoc = nodeLoc.getParent();
} while (nodeLoc != null);
return null;
}
@Override
public void release(MachineLocation rawMachine) {
String instanceId = vmInstanceIds.remove(rawMachine);
if (instanceId == null) {
LOG.info("Attempted release of unknown machine "+rawMachine+" in "+toString());
throw new IllegalArgumentException("Unknown machine "+rawMachine);
}
JcloudsMachineLocation machine = (JcloudsMachineLocation) rawMachine;
LOG.info("Releasing machine {} in {}, instance id {}", new Object[] {machine, this, instanceId});
Exception tothrow = null;
ConfigBag setup = config().getBag();
for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
try {
customizer.preRelease(machine);
} catch (Exception e) {
LOG.error("Problem invoking pre-release customizer "+customizer+" for machine "+machine+" in "+this+", instance id "+instanceId+
"; ignoring and continuing, "
+ (tothrow==null ? "will throw subsequently" : "swallowing due to previous error")+": "+e, e);
if (tothrow==null) tothrow = e;
}
}
for (MachineLocationCustomizer customizer : getMachineCustomizers(setup)) {
customizer.preRelease(machine);
}
try {
// FIXME: Needs to release port forwarding for WinRmMachineLocations
if (machine instanceof JcloudsMachineLocation) {
releasePortForwarding((JcloudsMachineLocation)machine);
}
} catch (Exception e) {
LOG.error("Problem releasing port-forwarding for machine "+machine+" in "+this+", instance id "+instanceId+
"; ignoring and continuing, "
+ (tothrow==null ? "will throw subsequently" : "swallowing due to previous error")+": "+e, e);
if (tothrow==null) tothrow = e;
}
try {
releaseNode(instanceId);
} catch (Exception e) {
LOG.error("Problem releasing machine "+machine+" in "+this+", instance id "+instanceId+
"; ignoring and continuing, "
+ (tothrow==null ? "will throw subsequently" : "swallowing due to previous error")+": "+e, e);
if (tothrow==null) tothrow = e;
}
removeChild(machine);
for (JcloudsLocationCustomizer customizer : getCustomizers(setup)) {
try {
customizer.postRelease(machine);
} catch (Exception e) {
LOG.error("Problem invoking pre-release customizer "+customizer+" for machine "+machine+" in "+this+", instance id "+instanceId+
"; ignoring and continuing, "
+ (tothrow==null ? "will throw subsequently" : "swallowing due to previous error")+": "+e, e);
if (tothrow==null) tothrow = e;
}
}
if (tothrow != null) {
throw Exceptions.propagate(tothrow);
}
}
protected void releaseSafely(MachineLocation machine) {
try {
release(machine);
} catch (Exception e) {
// rely on exception having been logged by #release(SshMachineLocation), so no-op
}
}
protected void releaseNodeSafely(NodeMetadata node) {
String instanceId = node.getId();
LOG.info("Releasing node {} in {}, instance id {}", new Object[] {node, this, instanceId});
try {
releaseNode(instanceId);
} catch (Exception e) {
LOG.warn("Problem releasing node "+node+" in "+this+", instance id "+instanceId+
"; discarding instance and continuing...", e);
}
}
protected void releaseNode(String instanceId) {
ComputeService computeService = null;
try {
computeService = getConfig(COMPUTE_SERVICE_REGISTRY).findComputeService(config().getBag(), true);
computeService.destroyNode(instanceId);
} finally {
/*
// we don't close the compute service; this means if we provision add'l it is fast;
// however it also means an explicit System.exit may be needed for termination
if (computeService != null) {
try {
computeService.getContext().close();
} catch (Exception e) {
LOG.error "Problem closing compute-service's context; continuing...", e
}
}
*/
}
}
protected void releasePortForwarding(final JcloudsMachineLocation machine) {
// TODO Implementation needs revisisted. It relies on deprecated PortForwardManager methods.
boolean usePortForwarding = Boolean.TRUE.equals(machine.getConfig(USE_PORT_FORWARDING));
final JcloudsPortForwarderExtension portForwarder = machine.getConfig(PORT_FORWARDER);
PortForwardManager portForwardManager = machine.getConfig(PORT_FORWARDING_MANAGER);
final String nodeId = machine.getJcloudsId();
final Map<String, Runnable> subtasks = Maps.newLinkedHashMap();
if (portForwarder == null) {
LOG.debug("No port-forwarding to close (because portForwarder null) on release of " + machine);
} else {
final Optional<NodeMetadata> node = machine.getOptionalNode();
// Release the port-forwarding for the login-port, which was explicitly created by JcloudsLocation
if (usePortForwarding && node.isPresent()) {
final HostAndPort hostAndPortOverride;
if (machine instanceof SshMachineLocation) {
hostAndPortOverride = ((SshMachineLocation)machine).getSshHostAndPort();
} else if (machine instanceof WinRmMachineLocation) {
String host = ((WinRmMachineLocation)machine).getAddress().getHostAddress();
int port = ((WinRmMachineLocation)machine).config().get(WinRmMachineLocation.WINRM_PORT);
hostAndPortOverride = HostAndPort.fromParts(host, port);
} else {
LOG.warn("Unexpected machine {} of type {}; expected SSH or WinRM", machine, (machine != null ? machine.getClass() : null));
hostAndPortOverride = null;
}
if (hostAndPortOverride != null) {
final int loginPort = node.get().getLoginPort();
subtasks.put(
"Close port-forward "+hostAndPortOverride+"->"+loginPort,
new Runnable() {
public void run() {
LOG.debug("Closing port-forwarding at {} for machine {}: {}->{}", new Object[] {this, machine, hostAndPortOverride, loginPort});
portForwarder.closePortForwarding(node.get(), loginPort, hostAndPortOverride, Protocol.TCP);
}
});
}
}
// Get all the other port-forwarding mappings for this VM, and release all of those
Set<PortMapping> mappings;
if (portForwardManager != null) {
mappings = Sets.newLinkedHashSet();
mappings.addAll(portForwardManager.getLocationPublicIpIds(machine));
if (nodeId != null) {
mappings.addAll(portForwardManager.getPortMappingWithPublicIpId(nodeId));
}
} else {
mappings = ImmutableSet.of();
}
for (final PortMapping mapping : mappings) {
final HostAndPort publicEndpoint = mapping.getPublicEndpoint();
final int targetPort = mapping.getPrivatePort();
final Protocol protocol = Protocol.TCP;
if (publicEndpoint != null && node.isPresent()) {
subtasks.put(
"Close port-forward "+publicEndpoint+"->"+targetPort,
new Runnable() {
public void run() {
LOG.debug("Closing port-forwarding at {} for machine {}: {}->{}", new Object[] {this, machine, publicEndpoint, targetPort});
portForwarder.closePortForwarding(node.get(), targetPort, publicEndpoint, protocol);
}
});
}
}
if (subtasks.size() > 0) {
final TaskBuilder<Void> builder = TaskBuilder.<Void>builder()
.parallel(true)
.displayName("close port-forwarding at "+machine);
for (Map.Entry<String, Runnable> entry : subtasks.entrySet()) {
builder.add(TaskBuilder.builder().displayName(entry.getKey()).body(entry.getValue()).build());
}
final Task<Void> task = builder.build();
final DynamicTasks.TaskQueueingResult<Void> queueResult = DynamicTasks.queueIfPossible(task);
if(queueResult.isQueuedOrSubmitted()){
final String origDetails = Tasks.setBlockingDetails("waiting for closing port-forwarding of "+machine);
try {
task.blockUntilEnded();
} finally {
Tasks.setBlockingDetails(origDetails);
}
} else {
LOG.warn("Releasing port-forwarding of "+machine+" not executing in execution-context "
+ "(e.g. not invoked inside effector); falling back to executing sequentially");
for (Runnable subtask : subtasks.values()) {
subtask.run();
}
}
}
}
// Forget all port mappings associated with this VM
if (portForwardManager != null) {
portForwardManager.forgetPortMappings(machine);
if (nodeId != null) {
portForwardManager.forgetPortMappings(nodeId);
}
}
}
// ------------ support methods --------------------
/**
* Extracts the user that jclouds tells us about (i.e. from the jclouds node).
*/
protected LoginCredentials extractVmCredentials(ConfigBag setup, NodeMetadata node, LoginCredentials nodeCredentials) {
String user = getUser(setup);
OsCredential localCredentials = LocationConfigUtils.getOsCredential(setup).checkNoErrors();
LOG.debug("Credentials extracted for {}: {}/{} with {}/{}", new Object[] { node,
user, nodeCredentials.getUser(), localCredentials, nodeCredentials });
if (Strings.isNonBlank(nodeCredentials.getUser())) {
if (Strings.isBlank(user)) {
setup.put(USER, user = nodeCredentials.getUser());
} else if (ROOT_USERNAME.equals(user) && ROOT_ALIASES.contains(nodeCredentials.getUser())) {
// deprecated, we used to default username to 'root'; now we leave null, then use autodetected credentials if no user specified
LOG.warn("overriding username 'root' in favour of '"+nodeCredentials.getUser()+"' at {}; this behaviour may be removed in future", node);
setup.put(USER, user = nodeCredentials.getUser());
}
String pkd = Strings.maybeNonBlank(localCredentials.getPrivateKeyData()).or(nodeCredentials.getOptionalPrivateKey().orNull());
String pwd = Strings.maybeNonBlank(localCredentials.getPassword()).or(nodeCredentials.getOptionalPassword().orNull());
if (Strings.isBlank(user) || (Strings.isBlank(pkd) && pwd==null)) {
String missing = (user==null ? "user" : "credential");
LOG.warn("Not able to determine "+missing+" for "+this+" at "+node+"; will likely fail subsequently");
return null;
} else {
LoginCredentials.Builder resultBuilder = LoginCredentials.builder().user(user);
if (pwd!=null && (Strings.isBlank(pkd) || localCredentials.isUsingPassword()))
resultBuilder.password(pwd);
else // pkd guaranteed non-blank due to above
resultBuilder.privateKey(pkd);
return resultBuilder.build();
}
}
LOG.warn("No node-credentials or admin-access available for node "+node+" in "+this+"; will likely fail subsequently");
return null;
}
protected String getFirstReachableAddress(NodeMetadata node, ConfigBag setup) {
String pollForFirstReachable = setup.get(POLL_FOR_FIRST_REACHABLE_ADDRESS);
boolean enabled = !"false".equalsIgnoreCase(pollForFirstReachable);
String result;
if (enabled) {
Duration timeout = "true".equals(pollForFirstReachable) ? Duration.FIVE_MINUTES : Duration.of(pollForFirstReachable);
result = JcloudsUtil.getFirstReachableAddress(node, timeout);
LOG.debug("Using first-reachable address "+result+" for node "+node+" in "+this);
} else {
result = Iterables.getFirst(Iterables.concat(node.getPublicAddresses(), node.getPrivateAddresses()), null);
if (result == null) {
throw new IllegalStateException("No addresses available for node "+node+" in "+this);
}
LOG.debug("Using first address "+result+" for node "+node+" in "+this);
}
return result;
}
protected LoginCredentials waitForWinRmAvailable(final ComputeService computeService, final NodeMetadata node, Optional<HostAndPort> hostAndPortOverride, ConfigBag setup) {
return waitForWinRmAvailable(computeService, node, hostAndPortOverride, ImmutableList.of(node.getCredentials()), setup);
}
protected LoginCredentials waitForWinRmAvailable(final ComputeService computeService, final NodeMetadata node, Optional<HostAndPort> hostAndPortOverride, List<LoginCredentials> credentialsToTry, ConfigBag setup) {
String waitForWinrmAvailable = setup.get(WAIT_FOR_WINRM_AVAILABLE);
checkArgument(!"false".equalsIgnoreCase(waitForWinrmAvailable), "waitForWinRmAvailable called despite waitForWinRmAvailable=%s", waitForWinrmAvailable);
Duration timeout = null;
try {
timeout = Duration.parse(waitForWinrmAvailable);
} catch (Exception e) {
// TODO will this just be a NumberFormatException? If so, catch that specificially
// normal if 'true'; just fall back to default
Exceptions.propagateIfFatal(e);
}
if (timeout == null) {
timeout = Duration.parse(WAIT_FOR_WINRM_AVAILABLE.getDefaultValue());
}
Set<String> users = Sets.newLinkedHashSet();
for (LoginCredentials creds : credentialsToTry) {
users.add(creds.getUser());
}
String user = (users.size() == 1) ? Iterables.getOnlyElement(users) : "{" + Joiner.on(",").join(users) + "}";
String vmIp = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getHostText() : getFirstReachableAddress(node, setup);
if (vmIp==null) LOG.warn("Unable to extract IP for "+node+" ("+setup.getDescription()+"): subsequent connection attempt will likely fail");
int vmPort = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getPortOrDefault(5985) : 5985;
String connectionDetails = user + "@" + vmIp + ":" + vmPort;
final HostAndPort hostAndPort = hostAndPortOverride.isPresent() ? hostAndPortOverride.get() : HostAndPort.fromParts(vmIp, vmPort);
final AtomicReference<LoginCredentials> credsSuccessful = new AtomicReference<LoginCredentials>();
// Don't use config that relates to the final user credentials (those have nothing to do
// with the initial credentials of the VM returned by the cloud provider).
// The createTemporaryWinRmMachineLocation deals with removing that.
ConfigBag winrmProps = ConfigBag.newInstanceCopying(setup);
final Map<WinRmMachineLocation, LoginCredentials> machinesToTry = Maps.newLinkedHashMap();
for (LoginCredentials creds : credentialsToTry) {
machinesToTry.put(createTemporaryWinRmMachineLocation(hostAndPort, creds, winrmProps), creds);
}
try {
Callable<Boolean> checker = new Callable<Boolean>() {
public Boolean call() {
for (Map.Entry<WinRmMachineLocation, LoginCredentials> entry : machinesToTry.entrySet()) {
WinRmMachineLocation machine = entry.getKey();
WinRmToolResponse response = machine.executeCommand(
ImmutableMap.of(WinRmTool.PROP_EXEC_TRIES.getName(), 1),
ImmutableList.of("echo testing"));
boolean success = (response.getStatusCode() == 0);
if (success) {
credsSuccessful.set(entry.getValue());
return true;
}
}
return false;
}};
waitForReachable(checker, connectionDetails, credentialsToTry, setup, timeout);
} finally {
for (WinRmMachineLocation machine : machinesToTry.keySet()) {
getManagementContext().getLocationManager().unmanage(machine);
}
}
return credsSuccessful.get();
}
protected LoginCredentials waitForSshable(final ComputeService computeService, final NodeMetadata node, Optional<HostAndPort> hostAndPortOverride, ConfigBag setup) {
LoginCredentials nodeCreds = node.getCredentials();
String nodeUser = nodeCreds.getUser();
String loginUserOverride = setup.get(LOGIN_USER);
Set<String> users = MutableSet.of();
if (Strings.isNonBlank(nodeUser)) {
users.add(nodeUser);
}
if (Strings.isNonBlank(loginUserOverride)) {
users.add(loginUserOverride);
}
// See https://issues.apache.org/jira/browse/BROOKLYN-186
// Handle where jclouds gives us the wrong login user (!) and both a password + ssh key.
// Try all the permutations to find the one that works.
List<LoginCredentials> credentialsToTry = Lists.newArrayList();
for (String user : users) {
if (nodeCreds.getOptionalPassword().isPresent() && nodeCreds.getOptionalPrivateKey().isPresent()) {
credentialsToTry.add(LoginCredentials.builder(nodeCreds).noPassword().user(user).build());
credentialsToTry.add(LoginCredentials.builder(nodeCreds).noPrivateKey().user(user).build());
} else {
credentialsToTry.add(LoginCredentials.builder(nodeCreds).user(user).build());
}
}
return waitForSshable(computeService, node, hostAndPortOverride, credentialsToTry, setup);
}
protected LoginCredentials waitForSshable(final ComputeService computeService, final NodeMetadata node, Optional<HostAndPort> hostAndPortOverride, List<LoginCredentials> credentialsToTry, ConfigBag setup) {
String waitForSshable = setup.get(WAIT_FOR_SSHABLE);
checkArgument(!"false".equalsIgnoreCase(waitForSshable), "waitForReachable called despite waitForSshable=%s for %s", waitForSshable, node);
checkArgument(credentialsToTry.size() > 0, "waitForReachable called without credentials for %s", node);
Duration timeout = null;
try {
timeout = Duration.parse(waitForSshable);
} catch (Exception e) {
// normal if 'true'; just fall back to default
}
if (timeout == null) {
timeout = Duration.parse(WAIT_FOR_SSHABLE.getDefaultValue());
}
Set<String> users = Sets.newLinkedHashSet();
for (LoginCredentials creds : credentialsToTry) {
users.add(creds.getUser());
}
String user = (users.size() == 1) ? Iterables.getOnlyElement(users) : "{" + Joiner.on(",").join(users) + "}";
String vmIp = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getHostText() : getFirstReachableAddress(node, setup);
if (vmIp==null) LOG.warn("Unable to extract IP for "+node+" ("+setup.getDescription()+"): subsequent connection attempt will likely fail");
int vmPort = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getPortOrDefault(22) : 22;
String connectionDetails = user + "@" + vmIp + ":" + vmPort;
final HostAndPort hostAndPort = hostAndPortOverride.isPresent() ? hostAndPortOverride.get() : HostAndPort.fromParts(vmIp, vmPort);
final AtomicReference<LoginCredentials> credsSuccessful = new AtomicReference<LoginCredentials>();
// Don't use config that relates to the final user credentials (those have nothing to do
// with the initial credentials of the VM returned by the cloud provider).
ConfigBag sshProps = ConfigBag.newInstanceCopying(setup);
sshProps.remove("password");
sshProps.remove("privateKeyData");
sshProps.remove("privateKeyFile");
sshProps.remove("privateKeyPassphrase");
final Map<SshMachineLocation, LoginCredentials> machinesToTry = Maps.newLinkedHashMap();
for (LoginCredentials creds : credentialsToTry) {
machinesToTry.put(createTemporarySshMachineLocation(hostAndPort, creds, sshProps), creds);
}
try {
Callable<Boolean> checker = new Callable<Boolean>() {
public Boolean call() {
for (Map.Entry<SshMachineLocation, LoginCredentials> entry : machinesToTry.entrySet()) {
SshMachineLocation machine = entry.getKey();
int exitstatus = machine.execScript(
ImmutableMap.of(
SshTool.PROP_SSH_TRIES_TIMEOUT.getName(), Duration.THIRTY_SECONDS.toMilliseconds(),
SshTool.PROP_SSH_TRIES.getName(), 1),
"check-connectivity",
ImmutableList.of("true"));
boolean success = (exitstatus == 0);
if (success) {
credsSuccessful.set(entry.getValue());
return true;
}
}
return false;
}};
waitForReachable(checker, connectionDetails, credentialsToTry, setup, timeout);
} finally {
for (SshMachineLocation machine : machinesToTry.keySet()) {
getManagementContext().getLocationManager().unmanage(machine);
Streams.closeQuietly(machine);
}
}
return credsSuccessful.get();
}
protected void waitForReachable(Callable<Boolean> checker, String hostAndPort, List<LoginCredentials> credentialsToLog, ConfigBag setup, Duration timeout) {
if (LOG.isDebugEnabled()) {
List<String> credsToString = Lists.newArrayList();
for (LoginCredentials creds : credentialsToLog) {
String user = creds.getUser();
String password;
String key;
if (Boolean.TRUE.equals(setup.get(LOG_CREDENTIALS))) {
password = creds.getOptionalPassword().or("<absent>");
key = creds.getOptionalPrivateKey().or("<absent>");
} else {
password = creds.getOptionalPassword().isPresent() ? "******" : "<absent>";
key = creds.getOptionalPrivateKey().isPresent() ? "******" : "<absent>";
}
credsToString.add("user="+user+", password="+password+", key="+key);
}
LOG.debug("VM {}: reported online, now waiting {} for it to be contactable on {}; trying {} credential{}: {}",
new Object[] {
setup.getDescription(), timeout,
hostAndPort,
credentialsToLog.size(),
Strings.s(credentialsToLog.size()),
(credsToString.size() == 1) ? credsToString.get(0) : "(multiple!):" + Joiner.on("\n\t").join(credsToString)
});
}
Stopwatch stopwatch = Stopwatch.createStarted();
ReferenceWithError<Boolean> reachable = new Repeater()
.backoff(Duration.ONE_SECOND, 2, Duration.TEN_SECONDS) // exponential backoff, to 10 seconds
.until(checker)
.limitTimeTo(timeout)
.runKeepingError();
if (!reachable.getWithoutError()) {
throw new IllegalStateException("Connection failed for "
+hostAndPort+" ("+setup.getDescription()+") after waiting "
+Time.makeTimeStringRounded(timeout), reachable.getError());
}
LOG.debug("VM {}: connection succeeded after {} on {}",new Object[] {
setup.getDescription(), Time.makeTimeStringRounded(stopwatch),
hostAndPort});
}
// -------------------- hostnames ------------------------
// hostnames are complicated, but irregardless, this code could be cleaned up!
protected void setHostnameUpdatingCredentials(ConfigBag setup, NodeMetadata metadata) {
List<String> usersTried = new ArrayList<String>();
String originalUser = getUser(setup);
if (groovyTruth(originalUser)) {
if (setHostname(setup, metadata, false)) return;
usersTried.add(originalUser);
}
LoginCredentials credentials = metadata.getCredentials();
if (credentials!=null) {
if (Strings.isNonBlank(credentials.getUser())) setup.put(USER, credentials.getUser());
if (Strings.isNonBlank(credentials.getOptionalPrivateKey().orNull())) setup.put(PRIVATE_KEY_DATA, credentials.getOptionalPrivateKey().orNull());
if (setHostname(setup, metadata, false)) {
if (originalUser!=null && !originalUser.equals(getUser(setup))) {
LOG.warn("Switching to cloud-specified user at "+metadata+" as "+getUser(setup)+" (failed to connect using: "+usersTried+")");
}
return;
}
usersTried.add(getUser(setup));
}
for (String u: COMMON_USER_NAMES_TO_TRY) {
setup.put(USER, u);
if (setHostname(setup, metadata, false)) {
LOG.warn("Auto-detected user at "+metadata+" as "+getUser(setup)+" (failed to connect using: "+usersTried+")");
return;
}
usersTried.add(getUser(setup));
}
// just repeat, so we throw exception
LOG.warn("Failed to log in to "+metadata+", tried as users "+usersTried+" (throwing original exception)");
setup.put(USER, originalUser);
setHostname(setup, metadata, true);
}
protected boolean setHostname(ConfigBag setup, NodeMetadata metadata, boolean rethrow) {
try {
setup.put(SshTool.PROP_HOST, getPublicHostname(metadata, Optional.<HostAndPort>absent(), setup));
return true;
} catch (Exception e) {
if (rethrow) {
LOG.warn("couldn't connect to "+metadata+" when trying to discover hostname (rethrowing): "+e);
throw Exceptions.propagate(e);
}
return false;
}
}
protected String getPublicHostname(NodeMetadata node, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) {
return getPublicHostname(node, sshHostAndPort, node.getCredentials(), setup);
}
/**
* Attempts to obtain the hostname or IP of the node, as advertised by the cloud provider.
* Prefers public, reachable IPs.
* For some clouds (e.g. aws-ec2), it will attempt to find the public hostname.
*/
protected String getPublicHostname(NodeMetadata node, Optional<HostAndPort> sshHostAndPort, LoginCredentials userCredentials, ConfigBag setup) {
String provider = (setup != null) ? setup.get(CLOUD_PROVIDER) : null;
if (provider == null) provider= getProvider();
if ("aws-ec2".equals(provider)) {
HostAndPort inferredHostAndPort = null;
if (!sshHostAndPort.isPresent()) {
try {
String vmIp = getFirstReachableAddress(node, setup);
int port = node.getLoginPort();
inferredHostAndPort = HostAndPort.fromParts(vmIp, port);
} catch (Exception e) {
LOG.warn("Error reaching aws-ec2 instance "+node.getId()+"@"+node.getLocation()+" on port "+node.getLoginPort()+"; falling back to jclouds metadata for address", e);
}
}
if (sshHostAndPort.isPresent() || inferredHostAndPort != null) {
if (isWindows(node, setup)) {
if (inferredHostAndPort != null) {
LOG.warn("Cannot querying aws-ec2 Windows instance "+node.getId()+"@"+node.getLocation()+" over ssh for its hostname; falling back to first reachable IP");
return inferredHostAndPort.getHostText();
}
} else {
HostAndPort hostAndPortToUse = sshHostAndPort.isPresent() ? sshHostAndPort.get() : inferredHostAndPort;
try {
return getPublicHostnameAws(hostAndPortToUse, userCredentials, setup);
} catch (Exception e) {
if (inferredHostAndPort != null) {
LOG.warn("Error querying aws-ec2 instance "+node.getId()+"@"+node.getLocation()+" over ssh for its hostname; falling back to first reachable IP", e);
// We've already found a reachable address so settle for that, rather than doing it again
return inferredHostAndPort.getHostText();
} else {
LOG.warn("Error querying aws-ec2 instance "+node.getId()+"@"+node.getLocation()+" over ssh for its hostname; falling back to jclouds metadata for address", e);
}
}
}
}
}
return getPublicHostnameGeneric(node, setup);
}
private String getPublicHostnameGeneric(NodeMetadata node, @Nullable ConfigBag setup) {
// JcloudsUtil.getFirstReachableAddress() (probably) already succeeded so at least one of the provided
// public and private IPs is reachable. Prefer the public IP. Don't use hostname as a fallback
// from the public address - if public address is missing why would hostname resolve to a
// public IP? It is sometimes wrong/abbreviated, resolving to the wrong IP, also e.g. on
// rackspace, the hostname lacks the domain.
//
// TODO If POLL_FOR_FIRST_REACHABLE_ADDRESS=false, then won't have checked if any node is reachable.
// TODO Some of the private addresses might not be reachable, should check connectivity before
// making a choice.
// TODO Choose an IP once and stick to it - multiple places call JcloudsUtil.getFirstReachableAddress(),
// could even get different IP on each call.
if (groovyTruth(node.getPublicAddresses())) {
return node.getPublicAddresses().iterator().next();
} else if (groovyTruth(node.getPrivateAddresses())) {
return node.getPrivateAddresses().iterator().next();
} else {
return null;
}
}
private String getPublicHostnameAws(HostAndPort hostAndPort, LoginCredentials userCredentials, ConfigBag setup) {
SshMachineLocation sshLocByIp = null;
try {
// TODO messy way to get an SSH session
sshLocByIp = createTemporarySshMachineLocation(hostAndPort, userCredentials, setup);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
ByteArrayOutputStream errStream = new ByteArrayOutputStream();
int exitcode = sshLocByIp.execCommands(
MutableMap.of("out", outStream, "err", errStream),
"get public AWS hostname",
ImmutableList.of(
BashCommands.INSTALL_CURL,
"echo `curl --silent --retry 20 http://169.254.169.254/latest/meta-data/public-hostname`; exit"));
String outString = new String(outStream.toByteArray());
String[] outLines = outString.split("\n");
for (String line : outLines) {
if (line.startsWith("ec2-")) return line.trim();
}
throw new IllegalStateException("Could not obtain aws-ec2 hostname for vm "+hostAndPort+"; exitcode="+exitcode+"; stdout="+outString+"; stderr="+new String(errStream.toByteArray()));
} finally {
Streams.closeQuietly(sshLocByIp);
}
}
/**
* Attempts to obtain the private hostname or IP of the node, as advertised by the cloud provider.
*
* For some clouds (e.g. aws-ec2), it will attempt to find the fully qualified hostname (as that works in public+private).
*/
protected String getPrivateHostname(NodeMetadata node, Optional<HostAndPort> sshHostAndPort, ConfigBag setup) {
return getPrivateHostname(node, sshHostAndPort, node.getCredentials(), setup);
}
protected String getPrivateHostname(NodeMetadata node, Optional<HostAndPort> sshHostAndPort, LoginCredentials userCredentials, ConfigBag setup) {
String provider = (setup != null) ? setup.get(CLOUD_PROVIDER) : null;
if (provider == null) provider= getProvider();
// TODO Discouraged to do cloud-specific things; think of this code for aws as an
// exceptional situation rather than a pattern to follow. We need a better way to
// do cloud-specific things.
if ("aws-ec2".equals(provider)) {
Maybe<String> result = getPrivateHostnameAws(node, sshHostAndPort, userCredentials, setup);
if (result.isPresent()) return result.get();
}
return getPrivateHostnameGeneric(node, setup);
}
private Maybe<String> getPrivateHostnameAws(NodeMetadata node, Optional<HostAndPort> sshHostAndPort, LoginCredentials userCredentials, ConfigBag setup) {
// TODO Remove duplication from getPublicHostname.
// TODO Don't like
HostAndPort inferredHostAndPort = null;
if (!sshHostAndPort.isPresent()) {
try {
String vmIp = getFirstReachableAddress(node, setup);
int port = node.getLoginPort();
inferredHostAndPort = HostAndPort.fromParts(vmIp, port);
} catch (Exception e) {
LOG.warn("Error reaching aws-ec2 instance "+node.getId()+"@"+node.getLocation()+" on port "+node.getLoginPort()+"; falling back to jclouds metadata for address", e);
}
}
if (sshHostAndPort.isPresent() || inferredHostAndPort != null) {
HostAndPort hostAndPortToUse = sshHostAndPort.isPresent() ? sshHostAndPort.get() : inferredHostAndPort;
try {
return Maybe.of(getPublicHostnameAws(hostAndPortToUse, userCredentials, setup));
} catch (Exception e) {
LOG.warn("Error querying aws-ec2 instance instance "+node.getId()+"@"+node.getLocation()+" over ssh for its hostname; falling back to jclouds metadata for address", e);
}
}
return Maybe.absent();
}
private String getPrivateHostnameGeneric(NodeMetadata node, @Nullable ConfigBag setup) {
//prefer the private address to the hostname because hostname is sometimes wrong/abbreviated
//(see that javadoc; also e.g. on rackspace/cloudstack, the hostname is not registered with any DNS).
//Don't return local-only address (e.g. never 127.0.0.1)
if (groovyTruth(node.getPrivateAddresses())) {
for (String p : node.getPrivateAddresses()) {
if (Networking.isLocalOnly(p)) continue;
return p;
}
}
if (groovyTruth(node.getPublicAddresses())) {
return node.getPublicAddresses().iterator().next();
} else if (groovyTruth(node.getHostname())) {
return node.getHostname();
} else {
return null;
}
}
// ------------ static converters (could go to a new file) ------------------
public static File asFile(Object o) {
if (o instanceof File) return (File)o;
if (o == null) return null;
return new File(o.toString());
}
public static String fileAsString(Object o) {
if (o instanceof String) return (String)o;
if (o instanceof File) return ((File)o).getAbsolutePath();
if (o==null) return null;
return o.toString();
}
protected static double toDouble(Object v) {
if (v instanceof Number) {
return ((Number)v).doubleValue();
} else {
throw new IllegalArgumentException("Invalid type for double: "+v+" of type "+v.getClass());
}
}
protected static String[] toStringArray(Object v) {
return toListOfStrings(v).toArray(new String[0]);
}
protected static List<String> toListOfStrings(Object v) {
List<String> result = Lists.newArrayList();
if (v instanceof Iterable) {
for (Object o : (Iterable<?>)v) {
result.add(o.toString());
}
} else if (v instanceof Object[]) {
for (int i = 0; i < ((Object[])v).length; i++) {
result.add(((Object[])v)[i].toString());
}
} else if (v instanceof String) {
result.add((String) v);
} else {
throw new IllegalArgumentException("Invalid type for List<String>: "+v+" of type "+v.getClass());
}
return result;
}
protected static byte[] toByteArray(Object v) {
if (v instanceof byte[]) {
return (byte[]) v;
} else if (v instanceof CharSequence) {
return v.toString().getBytes();
} else {
throw new IllegalArgumentException("Invalid type for byte[]: "+v+" of type "+v.getClass());
}
}
@VisibleForTesting
static int[] toIntPortArray(Object v) {
PortRange portRange = PortRanges.fromIterable(Collections.singletonList(v));
int[] portArray = ArrayUtils.toPrimitive(Iterables.toArray(portRange, Integer.class));
return portArray;
}
// Handles GString
protected static Map<String,String> toMapStringString(Object v) {
if (v instanceof Map<?,?>) {
Map<String,String> result = Maps.newLinkedHashMap();
for (Map.Entry<?,?> entry : ((Map<?,?>)v).entrySet()) {
String key = ((CharSequence)entry.getKey()).toString();
String value = ((CharSequence)entry.getValue()).toString();
result.put(key, value);
}
return result;
} else if (v instanceof CharSequence) {
return KeyValueParser.parseMap(v.toString());
} else {
throw new IllegalArgumentException("Invalid type for Map<String,String>: " + v +
(v != null ? " of type "+v.getClass() : ""));
}
}
private List<String> createIptablesRulesForNetworkInterface(Iterable<Integer> ports) {
List<String> iptablesRules = Lists.newArrayList();
for (Integer port : ports) {
iptablesRules.add(IptablesCommands.insertIptablesRule(Chain.INPUT, Protocol.TCP, port, Policy.ACCEPT));
}
return iptablesRules;
}
@Override
public PersistenceObjectStore newPersistenceObjectStore(String container) {
return new JcloudsBlobStoreBasedObjectStore(this, container);
}
}