/* * 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 org.jclouds.compute.options.RunScriptOptions.Builder.overrideLoginCredentials; import static org.jclouds.compute.util.ComputeServiceUtils.execHttpResponse; import static org.jclouds.scriptbuilder.domain.Statements.appendFile; import static org.jclouds.scriptbuilder.domain.Statements.exec; import static org.jclouds.scriptbuilder.domain.Statements.interpret; import static org.jclouds.scriptbuilder.domain.Statements.newStatementList; import java.io.File; import java.io.IOException; import java.net.URI; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; import org.apache.brooklyn.core.config.Sanitizer; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.core.config.ConfigBag; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.net.Protocol; import org.apache.brooklyn.util.net.ReachableSocketFinder; 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.time.Duration; import org.jclouds.Constants; import org.jclouds.ContextBuilder; import org.jclouds.aws.ec2.AWSEC2Api; import org.jclouds.blobstore.BlobStoreContext; import org.jclouds.compute.ComputeService; import org.jclouds.compute.ComputeServiceContext; import org.jclouds.compute.RunScriptOnNodesException; import org.jclouds.compute.domain.ExecResponse; import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.OperatingSystem; import org.jclouds.compute.options.RunScriptOptions; import org.jclouds.compute.predicates.OperatingSystemPredicates; import org.jclouds.docker.DockerApi; import org.jclouds.docker.domain.Container; import org.jclouds.domain.LoginCredentials; import org.jclouds.ec2.compute.domain.PasswordDataAndPrivateKey; import org.jclouds.ec2.compute.functions.WindowsLoginCredentialsFromEncryptedData; import org.jclouds.ec2.domain.PasswordData; import org.jclouds.ec2.features.WindowsApi; import org.jclouds.encryption.bouncycastle.config.BouncyCastleCryptoModule; import org.jclouds.logging.slf4j.config.SLF4JLoggingModule; import org.jclouds.scriptbuilder.domain.Statement; import org.jclouds.scriptbuilder.domain.Statements; import org.jclouds.sshj.config.SshjSshClientModule; import org.jclouds.util.Predicates2; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.Beta; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.io.Files; import com.google.common.net.HostAndPort; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.inject.Module; public class JcloudsUtil implements JcloudsLocationConfig { // TODO Review what utility methods are needed, and what is now supported in jclouds 1.1 private static final Logger LOG = LoggerFactory.getLogger(JcloudsUtil.class); /** * @deprecated since 0.7; see {@link BashCommands} */ @Deprecated public static String APT_INSTALL = "apt-get install -f -y -qq --force-yes"; /** * @deprecated since 0.7; see {@link BashCommands} */ @Deprecated public static String installAfterUpdatingIfNotPresent(String cmd) { String aptInstallCmd = APT_INSTALL + " " + cmd; return String.format("which %s || (%s || (apt-get update && %s))", cmd, aptInstallCmd, aptInstallCmd); } /** * @deprecated since 0.7 */ @Deprecated public static Predicate<NodeMetadata> predicateMatchingById(final NodeMetadata node) { return predicateMatchingById(node.getId()); } /** * @deprecated since 0.7 */ @Deprecated public static Predicate<NodeMetadata> predicateMatchingById(final String id) { Predicate<NodeMetadata> nodePredicate = new Predicate<NodeMetadata>() { @Override public boolean apply(NodeMetadata arg0) { return id.equals(arg0.getId()); } @Override public String toString() { return "node.id=="+id; } }; return nodePredicate; } /** * @deprecated since 0.7; see {@link IptablesCommands} */ @Deprecated public static Statement authorizePortInIpTables(int port) { // TODO gogrid rules only allow ports 22, 3389, 80 and 443. // the first rule will be ignored, so we have to apply this // directly return Statements.newStatementList(// just in case iptables are being used, try to open 8080 exec("iptables -I INPUT 1 -p tcp --dport " + port + " -j ACCEPT"),// exec("iptables -I RH-Firewall-1-INPUT 1 -p tcp --dport " + port + " -j ACCEPT"),// exec("iptables-save")); } /** * @throws RunScriptOnNodesException * @throws IllegalStateException If do not find exactly one matching node * * @deprecated since 0.7 */ @Deprecated public static ExecResponse runScriptOnNode(ComputeService computeService, NodeMetadata node, Statement statement, String scriptName) throws RunScriptOnNodesException { // TODO Includes workaround for NodeMetadata's equals/hashcode method being wrong. Map<? extends NodeMetadata, ExecResponse> scriptResults = computeService.runScriptOnNodesMatching( JcloudsUtil.predicateMatchingById(node), statement, new RunScriptOptions().nameTask(scriptName)); if (scriptResults.isEmpty()) { throw new IllegalStateException("No matching node found when executing script "+scriptName+": expected="+node); } else if (scriptResults.size() > 1) { throw new IllegalStateException("Multiple nodes matched predicate: id="+node.getId()+"; expected="+node+"; actual="+scriptResults.keySet()); } else { return Iterables.getOnlyElement(scriptResults.values()); } } /** * @deprecated since 0.7; see {@link #installJavaAndCurl(OperatingSystem)} */ @Deprecated public static final Statement APT_RUN_SCRIPT = newStatementList(// exec(installAfterUpdatingIfNotPresent("curl")),// exec("(which java && java -fullversion 2>&1|egrep -q 1.6 ) ||"),// execHttpResponse(URI.create("http://whirr.s3.amazonaws.com/0.2.0-incubating-SNAPSHOT/sun/java/install")),// exec(new StringBuilder()// .append("echo nameserver 208.67.222.222 >> /etc/resolv.conf\n")// // jeos hasn't enough room! .append("rm -rf /var/cache/apt /usr/lib/vmware-tools\n")// .append("echo \"export PATH=\\\"$JAVA_HOME/bin/:$PATH\\\"\" >> /root/.bashrc")// .toString())); /** * @deprecated since 0.7; see {@link #installJavaAndCurl(OperatingSystem)} */ @Deprecated public static final Statement YUM_RUN_SCRIPT = newStatementList( exec("which curl ||yum --nogpgcheck -y install curl"),// exec("(which java && java -fullversion 2>&1|egrep -q 1.6 ) ||"),// execHttpResponse(URI.create("http://whirr.s3.amazonaws.com/0.2.0-incubating-SNAPSHOT/sun/java/install")),// exec(new StringBuilder()// .append("echo nameserver 208.67.222.222 >> /etc/resolv.conf\n") // .append("echo \"export PATH=\\\"$JAVA_HOME/bin/:$PATH\\\"\" >> /root/.bashrc")// .toString())); /** * @deprecated since 0.7; {@link #installJavaAndCurl(OperatingSystem)} */ @Deprecated public static final Statement ZYPPER_RUN_SCRIPT = exec(new StringBuilder()// .append("echo nameserver 208.67.222.222 >> /etc/resolv.conf\n")// .append("which curl || zypper install curl\n")// .append("(which java && java -fullversion 2>&1|egrep -q 1.6 ) || zypper install java-1.6.0-openjdk\n")// .toString()); // Code taken from RunScriptData /** * @deprecated since 0.7; see {@link BashCommands#installJava7()} and {@link BashCommands#INSTALL_CURL} */ @Deprecated public static Statement installJavaAndCurl(OperatingSystem os) { if (os == null || OperatingSystemPredicates.supportsApt().apply(os)) return APT_RUN_SCRIPT; else if (OperatingSystemPredicates.supportsYum().apply(os)) return YUM_RUN_SCRIPT; else if (OperatingSystemPredicates.supportsZypper().apply(os)) return ZYPPER_RUN_SCRIPT; else throw new IllegalArgumentException("don't know how to handle" + os.toString()); } /** * @deprecated since 0.7; see {@link ComputeServiceRegistry#findComputeService(ConfigBag, boolean)} */ @Deprecated public static ComputeService findComputeService(ConfigBag conf) { return ComputeServiceRegistryImpl.INSTANCE.findComputeService(conf, true); } /** * @deprecated since 0.7; see {@link ComputeServiceRegistry#findComputeService(ConfigBag, boolean)} */ @Deprecated public static ComputeService findComputeService(ConfigBag conf, boolean allowReuse) { return ComputeServiceRegistryImpl.INSTANCE.findComputeService(conf, allowReuse); } /** * Returns the jclouds modules we typically install * * @deprecated since 0.7; see {@link ComputeServiceRegistry} */ @Deprecated public static ImmutableSet<Module> getCommonModules() { return ImmutableSet.<Module> of( new SshjSshClientModule(), new SLF4JLoggingModule(), new BouncyCastleCryptoModule()); } /** * Temporary constructor to address https://issues.apache.org/jira/browse/JCLOUDS-615. * <p> * See https://issues.apache.org/jira/browse/BROOKLYN-6 . * When https://issues.apache.org/jira/browse/JCLOUDS-615 is fixed in the jclouds we use, * we can remove the useSoftlayerFix argument. * <p> * (Marked Beta as that argument will likely be removed.) * * @since 0.7.0 */ @Beta public static BlobStoreContext newBlobstoreContext(String provider, @Nullable String endpoint, String identity, String credential) { Properties overrides = new Properties(); // * Java 7,8 bug workaround - sockets closed by GC break the internal bookkeeping // of HttpUrlConnection, leading to invalid handling of the "HTTP/1.1 100 Continue" // response. Coupled with a bug when using SSL sockets reads will block // indefinitely even though a read timeout is explicitly set. // * Java 6 ignores the header anyways as it is included in its restricted headers black list. // * Also there's a bug in SL object store which still expects Content-Length bytes // even when it responds with a 408 timeout response, leading to incorrectly // interpreting the next request (triggered by above problem). overrides.setProperty(Constants.PROPERTY_STRIP_EXPECT_HEADER, "true"); ContextBuilder contextBuilder = ContextBuilder.newBuilder(provider).credentials(identity, credential); contextBuilder.modules(MutableList.copyOf(JcloudsUtil.getCommonModules())); if (!org.apache.brooklyn.util.text.Strings.isBlank(endpoint)) { contextBuilder.endpoint(endpoint); } contextBuilder.overrides(overrides); BlobStoreContext context = contextBuilder.buildView(BlobStoreContext.class); return context; } /** * @deprecated since 0.7 */ @Deprecated protected static String getDeprecatedProperty(ConfigBag conf, String key) { if (conf.containsKey(key)) { LOG.warn("Jclouds using deprecated brooklyn-jclouds property "+key+": "+Sanitizer.sanitize(conf.getAllConfig())); return (String) conf.getStringKey(key); } else { return null; } } /** * @deprecated since 0.7 */ @Deprecated // Do this so that if there's a problem with our USERNAME's ssh key, we can still get in to check // TODO Once we're really confident there are not going to be regular problems, then delete this public static Statement addAuthorizedKeysToRoot(File publicKeyFile) throws IOException { String publicKey = Files.toString(publicKeyFile, Charsets.UTF_8); return addAuthorizedKeysToRoot(publicKey); } /** * @deprecated since 0.7 */ @Deprecated public static Statement addAuthorizedKeysToRoot(String publicKey) { return newStatementList( appendFile("/root/.ssh/authorized_keys", Splitter.on('\n').split(publicKey)), interpret("chmod 600 /root/.ssh/authorized_keys")); } /** * @deprecated since 0.9.0; use {@link #getFirstReachableAddress(NodeMetadata, Duration)} */ public static String getFirstReachableAddress(ComputeServiceContext context, NodeMetadata node) { // Previously this called jclouds `sshForNode().apply(Node)` to check all IPs of node (private+public), // to find one that is reachable. It does `openSocketFinder.findOpenSocketOnNode(node, node.getLoginPort(), ...)`. // This keeps trying for time org.jclouds.compute.reference.ComputeServiceConstants.Timeouts.portOpen. // TODO Want to configure this timeout here. // // TODO We could perhaps instead just set `templateOptions.blockOnPort(loginPort, 120)`, but need // to be careful to only set that if config WAIT_FOR_SSHABLE is true. For some advanced networking examples // (e.g. using DNAT on CloudStack), the brooklyn machine won't be able to reach the VM until some additional // setup steps have been done. See links from Andrea: // https://github.com/jclouds/jclouds/pull/895 // https://issues.apache.org/jira/browse/WHIRR-420 // jclouds.ssh.max-retries // jclouds.ssh.retry-auth // // With `sshForNode`, we'd seen exceptions: // java.lang.IllegalStateException: Optional.get() cannot be called on an absent value // from org.jclouds.crypto.ASN1Codec.createASN1Sequence(ASN1Codec.java:86), if the ssh key has a passphrase, against AWS. // And others reported: // java.lang.IllegalArgumentException: DER length more than 4 bytes // when using a key with a passphrase (perhaps from other clouds?); not sure if that's this callpath or a different one. return getFirstReachableAddress(node, Duration.FIVE_MINUTES); } public static String getFirstReachableAddress(NodeMetadata node, Duration timeout) { final int port = node.getLoginPort(); List<HostAndPort> sockets = FluentIterable .from(Iterables.concat(node.getPublicAddresses(), node.getPrivateAddresses())) .transform(new Function<String, HostAndPort>() { @Override public HostAndPort apply(String input) { return HostAndPort.fromParts(input, port); }}) .toList(); ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); try { ReachableSocketFinder finder = new ReachableSocketFinder(executor); HostAndPort result = finder.findOpenSocketOnNode(sockets, timeout); return result.getHostText(); } catch (Exception e) { Exceptions.propagateIfFatal(e); throw new IllegalStateException("Unable to connect SshClient to "+node+"; check that the node is accessible and that the SSH key exists and is correctly configured, including any passphrase defined", e); } finally { executor.shutdownNow(); } } // Suggest at least 15 minutes for timeout public static String waitForPasswordOnAws(ComputeService computeService, final NodeMetadata node, long timeout, TimeUnit timeUnit) throws TimeoutException { ComputeServiceContext computeServiceContext = computeService.getContext(); AWSEC2Api ec2Client = computeServiceContext.unwrapApi(AWSEC2Api.class); final WindowsApi client = ec2Client.getWindowsApi().get(); final String region = node.getLocation().getParent().getId(); // The Administrator password will take some time before it is ready - Amazon says sometimes 15 minutes. // So we create a predicate that tests if the password is ready, and wrap it in a retryable predicate. Predicate<String> passwordReady = new Predicate<String>() { @Override public boolean apply(String s) { if (Strings.isNullOrEmpty(s)) return false; PasswordData data = client.getPasswordDataInRegion(region, s); if (data == null) return false; return !Strings.isNullOrEmpty(data.getPasswordData()); } }; LOG.info("Waiting for password, for "+node.getProviderId()+":"+node.getId()); Predicate<String> passwordReadyRetryable = Predicates2.retry(passwordReady, timeUnit.toMillis(timeout), 10*1000, TimeUnit.MILLISECONDS); boolean ready = passwordReadyRetryable.apply(node.getProviderId()); if (!ready) throw new TimeoutException("Password not available for "+node+" in region "+region+" after "+timeout+" "+timeUnit.name()); // Now pull together Amazon's encrypted password blob, and the private key that jclouds generated PasswordDataAndPrivateKey dataAndKey = new PasswordDataAndPrivateKey( client.getPasswordDataInRegion(region, node.getProviderId()), node.getCredentials().getPrivateKey()); // And apply it to the decryption function WindowsLoginCredentialsFromEncryptedData f = computeServiceContext.utils().injector().getInstance(WindowsLoginCredentialsFromEncryptedData.class); LoginCredentials credentials = f.apply(dataAndKey); return credentials.getPassword(); } public static Map<Integer, Integer> dockerPortMappingsFor(JcloudsLocation docker, String containerId) { ComputeServiceContext context = null; try { Properties properties = new Properties(); properties.setProperty(Constants.PROPERTY_TRUST_ALL_CERTS, Boolean.toString(true)); properties.setProperty(Constants.PROPERTY_RELAX_HOSTNAME, Boolean.toString(true)); context = ContextBuilder.newBuilder("docker") .endpoint(docker.getEndpoint()) .credentials(docker.getIdentity(), docker.getCredential()) .overrides(properties) .modules(ImmutableSet.<Module>of(new SLF4JLoggingModule(), new SshjSshClientModule())) .build(ComputeServiceContext.class); DockerApi api = context.unwrapApi(DockerApi.class); Container container = api.getContainerApi().inspectContainer(containerId); Map<Integer, Integer> portMappings = Maps.newLinkedHashMap(); Map<String, List<Map<String, String>>> ports = container.networkSettings().ports(); if (ports == null) ports = ImmutableMap.<String, List<Map<String,String>>>of(); LOG.debug("Docker will forward these ports {}", ports); for (Map.Entry<String, List<Map<String, String>>> entrySet : ports.entrySet()) { String containerPort = Iterables.get(Splitter.on("/").split(entrySet.getKey()), 0); String hostPort = Iterables.getOnlyElement(Iterables.transform(entrySet.getValue(), new Function<Map<String, String>, String>() { @Override public String apply(Map<String, String> hostIpAndPort) { return hostIpAndPort.get("HostPort"); } })); portMappings.put(Integer.parseInt(containerPort), Integer.parseInt(hostPort)); } return portMappings; } finally { if (context != null) { context.close(); } } } /** * @deprecated since 0.7 */ @Deprecated public static void mapSecurityGroupRuleToIpTables(ComputeService computeService, NodeMetadata node, LoginCredentials credentials, String networkInterface, Iterable<Integer> ports) { for (Integer port : ports) { String insertIptableRule = IptablesCommands.insertIptablesRule(Chain.INPUT, networkInterface, Protocol.TCP, port, Policy.ACCEPT); Statement statement = Statements.newStatementList(exec(insertIptableRule)); ExecResponse response = computeService.runScriptOnNode(node.getId(), statement, overrideLoginCredentials(credentials).runAsRoot(false)); if (response.getExitStatus() != 0) { String msg = String.format("Cannot insert the iptables rule for port %d. Error: %s", port, response.getError()); LOG.error(msg); throw new RuntimeException(msg); } } } }