/*
* 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.ssh;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.OutputStream;
import java.net.InetAddress;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import org.apache.brooklyn.api.effector.Effector;
import org.apache.brooklyn.api.entity.EntityInitializer;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.location.MachineDetails;
import org.apache.brooklyn.api.location.MachineLocation;
import org.apache.brooklyn.api.location.PortRange;
import org.apache.brooklyn.core.effector.EffectorBody;
import org.apache.brooklyn.core.effector.EffectorTaskTest;
import org.apache.brooklyn.core.effector.Effectors;
import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.factory.ApplicationBuilder;
import org.apache.brooklyn.core.location.BasicHardwareDetails;
import org.apache.brooklyn.core.location.BasicMachineDetails;
import org.apache.brooklyn.core.location.BasicOsDetails;
import org.apache.brooklyn.core.location.Machines;
import org.apache.brooklyn.core.location.PortRanges;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.file.ArchiveUtils;
import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool;
import org.apache.brooklyn.util.core.internal.ssh.SshException;
import org.apache.brooklyn.util.core.task.BasicExecutionContext;
import org.apache.brooklyn.util.core.task.BasicExecutionManager;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.net.Networking;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.time.Duration;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Files;
/**
* Test the {@link SshMachineLocation} implementation of the {@link Location} interface.
*/
public class SshMachineLocationTest extends BrooklynAppUnitTestSupport {
private SshMachineLocation host;
@BeforeMethod(alwaysRun=true)
public void setUp() throws Exception {
super.setUp();
host = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
.configure("address", Networking.getLocalHost()));
RecordingSshTool.clear();
}
@AfterMethod(alwaysRun=true)
public void tearDown() throws Exception {
try {
if (host != null) Streams.closeQuietly(host);
} finally {
RecordingSshTool.clear();
super.tearDown();
}
}
@Test(groups = "Integration")
public void testGetMachineDetails() throws Exception {
BasicExecutionManager execManager = new BasicExecutionManager("mycontextid");
BasicExecutionContext execContext = new BasicExecutionContext(execManager);
try {
MachineDetails details = execContext.submit(new Callable<MachineDetails>() {
public MachineDetails call() {
return host.getMachineDetails();
}}).get();
assertNotNull(details);
} finally {
execManager.shutdownNow();
}
}
@Test
public void testSupplyingMachineDetails() throws Exception {
MachineDetails machineDetails = new BasicMachineDetails(new BasicHardwareDetails(1, 1024), new BasicOsDetails("myname", "myarch", "myversion"));
SshMachineLocation host2 = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
.configure(SshMachineLocation.MACHINE_DETAILS, machineDetails));
assertSame(host2.getMachineDetails(), machineDetails);
}
@Test
public void testConfigurePrivateAddresses() throws Exception {
SshMachineLocation host2 = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
.configure("address", Networking.getLocalHost())
.configure(SshMachineLocation.PRIVATE_ADDRESSES, ImmutableList.of("1.2.3.4"))
.configure(BrooklynConfigKeys.SKIP_ON_BOX_BASE_DIR_RESOLUTION, true));
assertEquals(host2.getPrivateAddresses(), ImmutableSet.of("1.2.3.4"));
}
// Wow, this is hard to test (until I accepted creating the entity + effector)! Code smell?
// Need to call getMachineDetails in a DynamicSequentialTask so that the "innessential" takes effect,
// to not fail its caller. But to get one of those outside of an effector is non-obvious.
@Test(groups = "Integration")
public void testGetMachineIsInessentialOnFailure() throws Exception {
SshMachineLocation host2 = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
.configure("address", Networking.getLocalHost())
.configure(SshMachineLocation.SSH_TOOL_CLASS, FailingSshTool.class.getName()));
final Effector<MachineDetails> GET_MACHINE_DETAILS = Effectors.effector(MachineDetails.class, "getMachineDetails")
.impl(new EffectorBody<MachineDetails>() {
public MachineDetails call(ConfigBag parameters) {
Maybe<MachineLocation> machine = Machines.findUniqueMachineLocation(entity().getLocations());
try {
machine.get().getMachineDetails();
throw new IllegalStateException("Expected failure in ssh");
} catch (RuntimeException e) {
return null;
}
}})
.build();
EntitySpec<TestApplication> appSpec = EntitySpec.create(TestApplication.class)
.configure(BrooklynConfigKeys.SKIP_ON_BOX_BASE_DIR_RESOLUTION, true)
.addInitializer(new EntityInitializer() {
public void apply(EntityLocal entity) {
((EntityInternal)entity).getMutableEntityType().addEffector(EffectorTaskTest.DOUBLE_1);
}});
TestApplication app = ApplicationBuilder.newManagedApp(appSpec, mgmt);
app.start(ImmutableList.of(host2));
MachineDetails details = app.invoke(GET_MACHINE_DETAILS, ImmutableMap.<String, Object>of()).get();
assertNull(details);
}
public static class FailingSshTool extends RecordingSshTool {
public FailingSshTool(Map<?, ?> props) {
super(props);
}
@Override public int execScript(Map<String, ?> props, List<String> commands, Map<String, ?> env) {
throw new RuntimeException("Simulating failure of ssh: cmds="+commands);
}
@Override public int execCommands(Map<String, ?> props, List<String> commands, Map<String, ?> env) {
throw new RuntimeException("Simulating failure of ssh: cmds="+commands);
}
}
// Note: requires `ssh localhost` to be setup such that no password is required
@Test(groups = "Integration")
public void testSshExecScript() throws Exception {
OutputStream outStream = new ByteArrayOutputStream();
String expectedName = Os.user();
host.execScript(MutableMap.of("out", outStream), "mysummary", ImmutableList.of("whoami; exit"));
String outString = outStream.toString();
assertTrue(outString.contains(expectedName), outString);
}
// Note: requires `ssh localhost` to be setup such that no password is required
@Test(groups = "Integration")
public void testSshExecCommands() throws Exception {
OutputStream outStream = new ByteArrayOutputStream();
String expectedName = Os.user();
host.execCommands(MutableMap.of("out", outStream), "mysummary", ImmutableList.of("whoami; exit"));
String outString = outStream.toString();
assertTrue(outString.contains(expectedName), outString);
}
// For issue #230
@Test(groups = "Integration")
public void testOverridingPropertyOnExec() throws Exception {
SshMachineLocation host = new SshMachineLocation(MutableMap.of("address", Networking.getLocalHost(), "sshPrivateKeyData", "wrongdata"));
OutputStream outStream = new ByteArrayOutputStream();
String expectedName = Os.user();
host.execCommands(MutableMap.of("sshPrivateKeyData", null, "out", outStream), "my summary", ImmutableList.of("whoami"));
String outString = outStream.toString();
assertTrue(outString.contains(expectedName), "outString="+outString);
}
@Test(groups = "Integration", expectedExceptions={IllegalStateException.class, SshException.class})
public void testSshRunWithInvalidUserFails() throws Exception {
SshMachineLocation badHost = new SshMachineLocation(MutableMap.of("user", "doesnotexist", "address", Networking.getLocalHost()));
badHost.execScript("mysummary", ImmutableList.of("whoami; exit"));
}
// Note: requires `ssh localhost` to be setup such that no password is required
@Test(groups = "Integration")
public void testCopyFileTo() throws Exception {
File dest = Os.newTempFile(getClass(), ".dest.tmp");
File src = Os.newTempFile(getClass(), ".src.tmp");
try {
Files.write("abc", src, Charsets.UTF_8);
host.copyTo(src, dest);
assertEquals("abc", Files.readFirstLine(dest, Charsets.UTF_8));
} finally {
src.delete();
dest.delete();
}
}
// Note: requires `ssh localhost` to be setup such that no password is required
@Test(groups = "Integration")
public void testCopyStreamTo() throws Exception {
String contents = "abc";
File dest = new File(Os.tmp(), "sssMachineLocationTest_dest.tmp");
try {
host.copyTo(Streams.newInputStreamWithContents(contents), dest.getAbsolutePath());
assertEquals("abc", Files.readFirstLine(dest, Charsets.UTF_8));
} finally {
dest.delete();
}
}
@Test(groups = "Integration")
public void testInstallUrlTo() throws Exception {
File dest = new File(Os.tmp(), "sssMachineLocationTest_dir/");
dest.mkdir();
try {
int result = host.installTo("https://raw.github.com/brooklyncentral/brooklyn/master/README.md", Urls.mergePaths(dest.getAbsolutePath(), "README.md"));
assertEquals(result, 0);
String contents = ArchiveUtils.readFullyString(new File(dest, "README.md"));
assertTrue(contents.contains("http://brooklyncentral.github.com"), "contents missing expected phrase; contains:\n"+contents);
} finally {
dest.delete();
}
}
@Test(groups = "Integration")
public void testInstallClasspathCopyTo() throws Exception {
File dest = new File(Os.tmp(), "sssMachineLocationTest_dir/");
dest.mkdir();
try {
int result = host.installTo("classpath://brooklyn/config/sample.properties", Urls.mergePaths(dest.getAbsolutePath(), "sample.properties"));
assertEquals(result, 0);
String contents = ArchiveUtils.readFullyString(new File(dest, "sample.properties"));
assertTrue(contents.contains("Property 1"), "contents missing expected phrase; contains:\n"+contents);
} finally {
dest.delete();
}
}
// Note: requires `ssh localhost` to be setup such that no password is required
@Test(groups = "Integration")
public void testIsSshableWhenTrue() throws Exception {
assertTrue(host.isSshable());
}
// Note: on some (home/airport) networks, `ssh 123.123.123.123` hangs seemingly forever.
// Make sure we fail, waiting for longer than the 70 second TCP timeout.
//
// Times out in 2m7s on Ubuntu Vivid (syn retries set to 6)
@Test(groups = "Integration")
public void testIsSshableWhenFalse() throws Exception {
byte[] unreachableIp = new byte[] {123,123,123,123};
final SshMachineLocation unreachableHost = new SshMachineLocation(MutableMap.of("address", InetAddress.getByAddress("unreachablename", unreachableIp)));
Asserts.assertReturnsEventually(new Runnable() {
public void run() {
assertFalse(unreachableHost.isSshable());
}},
Duration.minutes(3));
}
@Test
public void obtainSpecificPortGivesOutPortOnlyOnce() {
int port = 2345;
assertTrue(host.obtainSpecificPort(port));
assertFalse(host.obtainSpecificPort(port));
host.releasePort(port);
assertTrue(host.obtainSpecificPort(port));
}
@Test
public void obtainPortInRangeGivesBackRequiredPortOnlyIfAvailable() {
int port = 2345;
assertEquals(host.obtainPort(new PortRanges.LinearPortRange(port, port)), port);
assertEquals(host.obtainPort(new PortRanges.LinearPortRange(port, port)), -1);
host.releasePort(port);
assertEquals(host.obtainPort(new PortRanges.LinearPortRange(port, port)), port);
}
@Test
public void obtainPortInWideRange() {
int lowerPort = 2345;
int upperPort = 2350;
PortRange range = new PortRanges.LinearPortRange(lowerPort, upperPort);
for (int i = lowerPort; i <= upperPort; i++) {
assertEquals(host.obtainPort(range), i);
}
assertEquals(host.obtainPort(range), -1);
host.releasePort(lowerPort);
assertEquals(host.obtainPort(range), lowerPort);
assertEquals(host.obtainPort(range), -1);
}
@Test
public void testObtainPortDoesNotUsePreReservedPorts() {
host = new SshMachineLocation(MutableMap.of("address", Networking.getLocalHost(), "usedPorts", ImmutableSet.of(8000)));
assertEquals(host.obtainPort(PortRanges.fromString("8000")), -1);
assertEquals(host.obtainPort(PortRanges.fromString("8000+")), 8001);
}
}