/* * 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.entity.java; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import java.net.MalformedURLException; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.Certificate; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import org.apache.brooklyn.api.entity.EntityLocal; import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.sensor.SensorEvent; import org.apache.brooklyn.api.sensor.SensorEventListener; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; import org.apache.brooklyn.core.location.PortRanges; import org.apache.brooklyn.core.test.entity.TestApplication; import org.apache.brooklyn.entity.java.JavaAppUtils; import org.apache.brooklyn.entity.java.UsesJava; import org.apache.brooklyn.entity.java.UsesJmx; import org.apache.brooklyn.entity.java.VanillaJavaApp; import org.apache.brooklyn.feed.jmx.JmxHelper; import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.util.core.crypto.FluentKeySigner; import org.apache.brooklyn.util.core.crypto.SecureKeys; import org.apache.brooklyn.util.crypto.SslTrustUtils; import org.apache.brooklyn.util.jmx.jmxmp.JmxmpAgent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; public class VanillaJavaAppTest { private static final Logger LOG = LoggerFactory.getLogger(VanillaJavaAppTest.class); private static final long TIMEOUT_MS = 10*1000; // Static attributes such as number of processors and start time are only polled every 60 seconds // so if they are not immediately available, it will be 60 seconds before they are polled again private static final Object LONG_TIMEOUT_MS = 61*1000; private static String BROOKLYN_THIS_CLASSPATH = null; private static Class<?> MAIN_CLASS = ExampleVanillaMain.class; private static Class<?> MAIN_CPU_HUNGRY_CLASS = ExampleVanillaMainCpuHungry.class; private TestApplication app; private LocalhostMachineProvisioningLocation loc; @BeforeMethod(alwaysRun = true) public void setUp() throws Exception { if (BROOKLYN_THIS_CLASSPATH==null) { BROOKLYN_THIS_CLASSPATH = ResourceUtils.create(MAIN_CLASS).getClassLoaderDir(); } app = TestApplication.Factory.newManagedInstanceForTests(); loc = app.newLocalhostProvisioningLocation(MutableMap.of("address", "localhost")); } @AfterMethod(alwaysRun = true) public void tearDown() throws Exception { if (app != null) Entities.destroyAll(app.getManagementContext()); } @Test public void testReadsConfigFromFlags() throws Exception { final VanillaJavaApp javaProcess = app.createAndManageChild(EntitySpec.create(VanillaJavaApp.class) .configure("main", "my.Main").configure("classpath", ImmutableList.of("c1", "c2")) .configure("args", ImmutableList.of("a1", "a2"))); assertEquals(javaProcess.getMainClass(), "my.Main"); assertEquals(javaProcess.getClasspath(), ImmutableList.of("c1","c2")); assertEquals(javaProcess.getConfig(VanillaJavaApp.ARGS), ImmutableList.of("a1", "a2")); } @Test(groups={"WIP", "Integration"}) public void testJavaSystemProperties() throws Exception { final VanillaJavaApp javaProcess = app.createAndManageChild(EntitySpec.create(VanillaJavaApp.class) .configure("main", "my.Main").configure("classpath", ImmutableList.of("c1", "c2")) .configure("args", ImmutableList.of("a1", "a2"))); ((EntityLocal)javaProcess).config().set(UsesJava.JAVA_SYSPROPS, ImmutableMap.of("fooKey", "fooValue", "barKey", "barValue")); // TODO: how to test: launch standalone app that outputs system properties to stdout? Probe via JMX? } @Test(groups={"Integration"}) public void testStartsAndStops() throws Exception { String main = MAIN_CLASS.getCanonicalName(); final VanillaJavaApp javaProcess = app.createAndManageChild(EntitySpec.create(VanillaJavaApp.class) .configure("main", main).configure("classpath", ImmutableList.of(BROOKLYN_THIS_CLASSPATH)) .configure("args", ImmutableList.of())); app.start(ImmutableList.of(loc)); assertEquals(javaProcess.getAttribute(VanillaJavaApp.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING); javaProcess.stop(); assertEquals(javaProcess.getAttribute(VanillaJavaApp.SERVICE_STATE_ACTUAL), Lifecycle.STOPPED); } @Test(groups={"Integration"}) public void testHasJvmMXBeanSensorVals() throws Exception { String main = MAIN_CLASS.getCanonicalName(); final VanillaJavaApp javaProcess = app.createAndManageChild(EntitySpec.create(VanillaJavaApp.class) .configure("main", main).configure("classpath", ImmutableList.of(BROOKLYN_THIS_CLASSPATH)) .configure("args", ImmutableList.of())); app.start(ImmutableList.of(loc)); // Memory MXBean Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() { public void run() { assertNotNull(javaProcess.getAttribute(VanillaJavaApp.NON_HEAP_MEMORY_USAGE)); long init = javaProcess.getAttribute(VanillaJavaApp.INIT_HEAP_MEMORY); long used = javaProcess.getAttribute(VanillaJavaApp.USED_HEAP_MEMORY); long committed = javaProcess.getAttribute(VanillaJavaApp.COMMITTED_HEAP_MEMORY); long max = javaProcess.getAttribute(VanillaJavaApp.MAX_HEAP_MEMORY); assertNotNull(used); assertNotNull(init); assertNotNull(committed); assertNotNull(max); assertTrue(init <= max, String.format("init %d > max %d heap memory", init, max)); assertTrue(used <= committed, String.format("used %d > committed %d heap memory", used, committed)); assertTrue(committed <= max, String.format("committed %d > max %d heap memory", committed, max)); }}); // Threads MX Bean Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() { public void run() { long current = javaProcess.getAttribute(VanillaJavaApp.CURRENT_THREAD_COUNT); long peak = javaProcess.getAttribute(VanillaJavaApp.PEAK_THREAD_COUNT); assertNotNull(current); assertNotNull(peak); assertTrue(current <= peak, String.format("current %d > peak %d thread count", current, peak)); }}); // Runtime MX Bean Asserts.succeedsEventually(MutableMap.of("timeout", LONG_TIMEOUT_MS), new Runnable() { public void run() { assertNotNull(javaProcess.getAttribute(VanillaJavaApp.START_TIME)); assertNotNull(javaProcess.getAttribute(VanillaJavaApp.UP_TIME)); }}); // Operating System MX Bean Asserts.succeedsEventually(MutableMap.of("timeout", LONG_TIMEOUT_MS), new Runnable() { public void run() { assertNotNull(javaProcess.getAttribute(VanillaJavaApp.PROCESS_CPU_TIME)); assertNotNull(javaProcess.getAttribute(VanillaJavaApp.SYSTEM_LOAD_AVERAGE)); assertNotNull(javaProcess.getAttribute(VanillaJavaApp.AVAILABLE_PROCESSORS)); assertNotNull(javaProcess.getAttribute(VanillaJavaApp.TOTAL_PHYSICAL_MEMORY_SIZE)); assertNotNull(javaProcess.getAttribute(VanillaJavaApp.FREE_PHYSICAL_MEMORY_SIZE)); }}); // TODO work on providing useful metrics from garbage collector MX Bean // assertNotNull(javaProcess.getAttribute(VanillaJavaApp.GARBAGE_COLLECTION_TIME)) TODO: work on providing this } @Test(groups={"Integration"}) public void testJvmMXBeanProcessCpuTimeGivesNonZeroPercentage() throws Exception { String main = MAIN_CPU_HUNGRY_CLASS.getCanonicalName(); final VanillaJavaApp javaProcess = app.createAndManageChild(EntitySpec.create(VanillaJavaApp.class) .configure("main", main).configure("classpath", ImmutableList.of(BROOKLYN_THIS_CLASSPATH)) .configure("args", ImmutableList.of())); app.start(ImmutableList.of(loc)); JavaAppUtils.connectJavaAppServerPolicies((EntityLocal)javaProcess); final List<Double> fractions = new CopyOnWriteArrayList<Double>(); app.getManagementContext().getSubscriptionManager().subscribe(javaProcess, VanillaJavaApp.PROCESS_CPU_TIME_FRACTION_LAST, new SensorEventListener<Double>() { public void onEvent(SensorEvent<Double> event) { fractions.add(event.getValue()); }}); // Expect non-trivial load to be generated by the process. // Expect load to be in the right order of magnitude (to ensure we haven't got a decimal point in the wrong place etc); // But with multi-core could get big number; and on jenkins@releng3 we once saw [11.9, 0.6, 0.5]! Asserts.succeedsEventually(new Runnable() { public void run() { Iterable<Double> nonTrivialFractions = Iterables.filter(fractions, new Predicate<Double>() { public boolean apply(Double input) { return input > 0.01; }}); assertTrue(Iterables.size(nonTrivialFractions) > 3, "fractions="+fractions); }}); Iterable<Double> tooBigFractions = Iterables.filter(fractions, new Predicate<Double>() { public boolean apply(Double input) { return input > 50; }}); assertTrue(Iterables.isEmpty(tooBigFractions), "fractions="+fractions); Iterable<Double> ballparkRightFractions = Iterables.filter(fractions, new Predicate<Double>() { public boolean apply(Double input) { return input > 0.01 && input < 4; }}); assertTrue(Iterables.size(ballparkRightFractions) >= (fractions.size() / 2), "fractions="+fractions); LOG.info("VanillaJavaApp->ExampleVanillaMainCpuHuntry: ProcessCpuTime fractions="+fractions); } @Test(groups={"Integration"}) public void testStartsWithJmxPortSpecifiedInConfig() throws Exception { int port = 53405; String main = MAIN_CLASS.getCanonicalName(); VanillaJavaApp javaProcess = app.createAndManageChild(EntitySpec.create(VanillaJavaApp.class) .configure("main", main).configure("classpath", ImmutableList.of(BROOKLYN_THIS_CLASSPATH)) .configure("args", ImmutableList.of())); ((EntityLocal)javaProcess).config().set(UsesJmx.JMX_PORT, PortRanges.fromInteger(port)); app.start(ImmutableList.of(loc)); assertEquals(javaProcess.getAttribute(UsesJmx.JMX_PORT), (Integer)port); } // FIXME Way test was written requires JmxSensorAdapter; need to rewrite... @Test(groups={"Integration", "WIP"}) public void testStartsWithSecureJmxPortSpecifiedInConfig() throws Exception { int port = 53406; String main = MAIN_CLASS.getCanonicalName(); final VanillaJavaApp javaProcess = app.createAndManageChild(EntitySpec.create(VanillaJavaApp.class) .configure("main", main).configure("classpath", ImmutableList.of(BROOKLYN_THIS_CLASSPATH)) .configure("args", ImmutableList.of())); ((EntityLocal)javaProcess).config().set(UsesJmx.JMX_PORT, PortRanges.fromInteger(port)); ((EntityLocal)javaProcess).config().set(UsesJmx.JMX_SSL_ENABLED, true); app.start(ImmutableList.of(loc)); // will fail above if JMX can't connect, but also do some add'l checks assertEquals(javaProcess.getAttribute(UsesJmx.JMX_PORT), (Integer)port); // good key+cert succeeds new AsserterForJmxConnection(javaProcess) .customizeSocketFactory(null, null) .connect(); // bad cert fails Asserts.assertFails(new Callable<Void>() { public Void call() throws Exception { new AsserterForJmxConnection(javaProcess) .customizeSocketFactory(null, new FluentKeySigner("cheater").newCertificateFor("jmx-access-key", SecureKeys.newKeyPair())) .connect(); return null; }}); // bad key fails Asserts.assertFails(new Callable<Void>() { public Void call() throws Exception { new AsserterForJmxConnection(javaProcess) .customizeSocketFactory(SecureKeys.newKeyPair().getPrivate(), null) .connect(); return null; }}); // bad profile fails Asserts.assertFails(new Callable<Void>() { public Void call() throws Exception { AsserterForJmxConnection asserter = new AsserterForJmxConnection(javaProcess); asserter.putEnv("jmx.remote.profiles", JmxmpAgent.TLS_JMX_REMOTE_PROFILES); asserter.customizeSocketFactory(SecureKeys.newKeyPair().getPrivate(), null) .connect(); return null; }}); } private static class AsserterForJmxConnection { final VanillaJavaApp entity; final JMXServiceURL url; final Map<String,Object> env; @SuppressWarnings("unchecked") public AsserterForJmxConnection(VanillaJavaApp e) throws MalformedURLException { this.entity = e; JmxHelper jmxHelper = new JmxHelper((EntityLocal)entity); this.url = new JMXServiceURL(jmxHelper.getUrl()); this.env = Maps.newLinkedHashMap(jmxHelper.getConnectionEnvVars()); } public JMXServiceURL getJmxUrl() throws MalformedURLException { return url; } public void putEnv(String key, Object val) { env.put(key, val); } public AsserterForJmxConnection customizeSocketFactory(PrivateKey customKey, Certificate customCert) throws Exception { PrivateKey key = (customKey == null) ? entity.getConfig(UsesJmx.JMX_SSL_ACCESS_KEY) : customKey; Certificate cert = (customCert == null) ? entity.getConfig(UsesJmx.JMX_SSL_ACCESS_CERT) : customCert; KeyStore ks = SecureKeys.newKeyStore(); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); if (key!=null) { ks.setKeyEntry("brooklyn-jmx-access", key, "".toCharArray(), new Certificate[] {cert}); } kmf.init(ks, "".toCharArray()); TrustManager tms = // TODO use root cert for trusting server //trustStore!=null ? SecureKeys.getTrustManager(trustStore) : SslTrustUtils.TRUST_ALL; SSLContext ctx = SSLContext.getInstance("TLSv1"); ctx.init(kmf.getKeyManagers(), new TrustManager[] {tms}, null); SSLSocketFactory ssf = ctx.getSocketFactory(); env.put(JmxmpAgent.TLS_SOCKET_FACTORY_PROPERTY, ssf); return this; } public JMXConnector connect() throws Exception { return JMXConnectorFactory.connect(getJmxUrl(), env); } } }