+/*
+ * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.distribution.test;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.Socket;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+
+import javax.management.JMX;
+import javax.management.MBeanServerConnection;
+import javax.management.Notification;
+import javax.management.NotificationListener;
+import javax.management.ObjectName;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.ow2.chameleon.management.beans.BundleMXBean;
+import org.ow2.chameleon.management.beans.OSGiPlatformMXBean;
+
+/**
+ * This integration test assumes a running local controller. The test does the
+ * following:
+ * 1) Wait for HTTP, JMX and OF ports to open
+ * 2) Establishes a JMX connection and registers a bundle notification listener
+ * 3) Waits till all bundles reach expected states or the timeout period elapses
+ */
+public class SanityIT {
+ private static final int OF_PORT = Integer.getInteger("ctrl.of.port", 6633);
+ private static final int HTTP_PORT = Integer.getInteger("ctrl.http.port", 8080);
+ private static final int JMX_PORT = Integer.getInteger("ctrl.jmx.port", 1088);
+ private static final int RMI_PORT = Integer.getInteger("ctrl.rmi.port", 1099);
+ private static final String CTRL_HOST = System.getProperty("ctrl.host", "127.0.0.1");
+ private static final String CTRL_HOME = System.getProperty("ctrl.home");
+ private static final long TIMEOUT_MILLIS =
+ Integer.getInteger("ctrl.start.timeout", 3*60) * 1000L;
+ private static final String JMX_URL =
+ "service:jmx:rmi:///jndi/rmi://" + CTRL_HOST + ":" + JMX_PORT + "/jmxrmi";
+
+ private static final Set<String> bundles =
+ Collections.synchronizedSet(new HashSet<String>());
+ private static final Set<String> fragments =
+ Collections.synchronizedSet(new HashSet<String>());
+
+ @BeforeClass
+ public static void loadBundles() throws IOException {
+ log(" ctrl.home: " + CTRL_HOME);
+ log(" ctrl.host: " + CTRL_HOST);
+ log("ctrl.start.timeout: " + TIMEOUT_MILLIS);
+ log(" jmx.url: " + JMX_URL);
+
+ Assert.assertNotNull(CTRL_HOME);
+ File ctrlHome = new File(CTRL_HOME);
+ Assert.assertTrue(ctrlHome.exists() && ctrlHome.isDirectory());
+ File configDir = new File(ctrlHome, "configuration");
+ Assert.assertTrue(configDir.exists() && configDir.isDirectory());
+ File configIni = new File(configDir, "config.ini");
+ Assert.assertTrue(configIni.exists());
+ Properties config = new Properties();
+ config.load(new FileInputStream(configIni));
+ processBundles(configDir, config.getProperty("osgi.bundles"));
+ processBundles(new File(ctrlHome, "plugins"));
+ log("Bundles found in installation: " + bundles.size());
+ log("Fragments found in installation: " + fragments.size());
+ }
+
+@Test
+ public void sanityTest() throws Exception {
+ // wait for http, jmx & of ports to open
+ long startTime = System.currentTimeMillis();
+ waitForListening(OF_PORT, false, startTime);
+ waitForListening(JMX_PORT, false, startTime);
+ waitForListening(HTTP_PORT, false, startTime);
+
+ // open jmx connection
+ JMXServiceURL serviceUrl = new JMXServiceURL(JMX_URL);
+ JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, null);
+ final MBeanServerConnection conn = jmxConnector.getMBeanServerConnection();
+
+ ObjectName fmkName = new ObjectName("org.ow2.chameleon:type=framework");
+ OSGiPlatformMXBean fmkBean= JMX.newMBeanProxy(conn, fmkName,
+ OSGiPlatformMXBean.class, true);
+ conn.addNotificationListener(fmkName, new NotificationListener() {
+
+ @Override
+ public void handleNotification(Notification n, Object handback) {
+ try {
+ //log("Notification: source: " + n.getSource());
+ ObjectName bundleName = new ObjectName(
+ "org.ow2.chameleon:type=bundle,id=" + n.getUserData());
+ BundleMXBean bundleBean = JMX.newMXBeanProxy(conn,
+ bundleName, BundleMXBean.class, true);
+ log("Bundle state change: " + bundleBean.getSymbolicName() +
+ " : " + stateToString(bundleBean.getState()));
+ handleBundleEvent(bundleBean);
+ // if its a system bundle, notify the main thread
+ if (bundleBean.getBundleId() == 0 &&
+ bundleBean.getState() == Bundle.ACTIVE)
+ {
+ synchronized(SanityIT.this) {
+ SanityIT.this.notify();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }, null, null);
+
+ if (checkAllBundles(conn) > 0) {
+ log("Waiting for system bundle to start... (times out in: " + TIMEOUT_MILLIS + "ms)");
+ long timeElapsed = System.currentTimeMillis() - startTime;
+ synchronized(this) {
+ this.wait(TIMEOUT_MILLIS - timeElapsed);
+ }
+ log("System bundle started. Revalidating bundle states for all bundles...");
+
+ // Sometimes, the system bundle starts earlier than other bundles(?). The
+ // following loop will cover that case.
+ do {
+ Thread.sleep(2000); // 2s seems appropriate given the default timeout
+ if (checkAllBundles(conn) == 0) {
+ break;
+ }
+ } while(System.currentTimeMillis() - startTime < TIMEOUT_MILLIS);
+ }
+ try {
+ jmxConnector.close();
+ } catch (Exception ignore) {
+ // dont want the tests to fail in case we can't close jmx connection
+ ignore.printStackTrace();
+ }
+ if (bundles.size() + fragments.size() == 0) {
+ log("All bundles have reached expected state");
+ } else {
+ log("The following bundles did not reach expected state: ");
+ for (String s : bundles) log("Bundle: " + s);
+ for (String s : fragments) log("Fragment: " + s);
+
+ }
+ Assert.assertTrue(bundles.size() == 0 && fragments.size() == 0);
+ }
+
+ private static int checkAllBundles(MBeanServerConnection conn) throws Exception {
+ ObjectName allBundlesName = new ObjectName("org.ow2.chameleon:*");
+ Set<ObjectName> bundleNames = conn.queryNames(allBundlesName, null);
+ for (ObjectName bundleName : bundleNames) {
+ if ("bundle".equals(bundleName.getKeyProperty("type"))) {
+ BundleMXBean bundleBean = JMX.newMBeanProxy(conn, bundleName,
+ BundleMXBean.class, true);
+ handleBundleEvent(bundleBean);
+ }
+ }
+ int remaining = bundles.size() + fragments.size();
+ if (remaining > 0) {
+ log("Bundles not in expected states: " + remaining + " Waiting...");
+ }
+ return remaining;
+ }
+
+ private synchronized static void handleBundleEvent(BundleMXBean bundleBean) {
+ String name = bundleBean.getSymbolicName();
+ int state = bundleBean.getState();
+ // BUG in BundleMXBeanImpl - can't get bundle headers :(
+ // String fragHost = bundleBean.getBundleHeaders().get(Constants.FRAGMENT_HOST);
+ if (bundles.contains(name) && (state == Bundle.RESOLVED || state == Bundle.ACTIVE)) {
+ bundles.remove(name);
+ } else if (fragments.contains(name) && state == Bundle.RESOLVED) {
+ fragments.remove(name);
+ }
+ //log("Bundles to be started: " + bundles.size());
+ }
+
+
+ // reference\:file\:../lib/org.apache.felix.fileinstall-3.1.6.jar@1:start,\
+ private static void processBundles(File dir, String property) {
+ Assert.assertTrue(property == null || property.length() > 0);
+ for(String s : property.split(",")) {
+ int idx = s.indexOf("@");
+ s = s.substring(15, (idx == -1 ? s.length() : idx));
+ if (!s.endsWith(".jar")) s = s + ".jar";
+ processJar(new File(dir, s));
+ }
+ }
+
+ public static void processBundles(File dir) throws IOException {
+ if (!dir.exists()) throw new FileNotFoundException("Cannot find dir:" + dir);
+ dir.listFiles(new FileFilter() {
+
+ @Override
+ public boolean accept(File file) {
+ //p("---- " + file);
+ if (file.getName().endsWith(".jar")) {
+ return processJar(file);
+ }
+ return false;
+ }
+ });
+ }
+
+ public static boolean processJar(File file) {
+ try {
+ //log("----" + file);
+ JarFile jar = new JarFile(file, false);
+ Attributes jarAttributes = jar.getManifest().getMainAttributes();
+ String bundleName = jarAttributes.getValue(Constants.BUNDLE_SYMBOLICNAME);
+ String fragHost = jarAttributes.getValue(Constants.FRAGMENT_HOST);
+ if (bundleName == null) {
+ log("Found a non bundle file:" + file);
+ return false;
+ } else {
+ int idx = bundleName.indexOf(';');
+ if (idx > -1) {
+ bundleName = bundleName.substring(0, idx);
+ }
+ }
+
+ if (fragHost == null) {
+ if (!bundles.add(bundleName)) {
+ throw new IllegalStateException(
+ "Found duplicate bundles with same symbolic name: "
+ + bundleName);
+ }
+ } else {
+ // fragments attaching to framework can't be detected
+ if (fragHost.contains("extension:=\"framework\"")) return false;
+ if (!fragments.add(bundleName)) {
+ throw new IllegalStateException(
+ "Found duplicate fragments with same symbolic name: "
+ + bundleName);
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Error processing jar: " + file , e);
+ }
+ return true;
+ }
+
+ public static long waitForListening(int port, boolean isHTTP, long beginTime)
+ throws InterruptedException {
+ long timeElapsedMillis = System.currentTimeMillis() - beginTime;
+ long sleepTimeMillis = 500L; // 0.5 secs
+
+ while (timeElapsedMillis < TIMEOUT_MILLIS) {
+ long timeRemaining = TIMEOUT_MILLIS - timeElapsedMillis;
+ sleepTimeMillis *= 2; // exponential backoff
+ long toSleep = (sleepTimeMillis > timeRemaining)
+ ? timeRemaining : sleepTimeMillis;
+ Thread.sleep(toSleep);
+ timeElapsedMillis = System.currentTimeMillis() - beginTime;
+ if (isHTTP ? connectHTTP(port) : connectTCP(port)) {
+ log("Port is open: " + port);
+ return timeElapsedMillis;
+ }
+ }
+ throw new IllegalStateException("Timeout waiting for port: " + port);
+ }
+
+ private static void log(String msg) {
+ System.out.format("[SanityIT] [%s] %s %s", new Date().toString(), msg,
+ System.lineSeparator());
+ }
+
+ public static boolean connectTCP(int port) {
+ String host = CTRL_HOST.length() == 0 ? null : CTRL_HOST;
+ try {
+ Socket sock = new Socket(host, port);
+ sock.getPort();
+ try {
+ sock.close();
+ } catch (IOException ignore) {
+ // Can't close socket. Ingore and let downstream validate health
+ }
+ return true;
+ } catch (IOException ioe) {
+ return false;
+ }
+ }
+
+ public static boolean connectHTTP(int port) {
+ String host = CTRL_HOST.length() == 0 ? "localhost" : CTRL_HOST;
+ try {
+ URL url = new URL("http", host, port, "/");
+ HttpURLConnection con;
+ con = (HttpURLConnection) url.openConnection();
+ return (con.getResponseCode() > 0);
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ private String stateToString(int state) {
+ switch (state) {
+ case Bundle.ACTIVE: return "ACTIVE";
+ case Bundle.INSTALLED: return "INSTALLED";
+ case Bundle.RESOLVED: return "RESOLVED";
+ case Bundle.UNINSTALLED: return "UNINSTALLED";
+ case Bundle.STARTING: return "STARTING";
+ case Bundle.STOPPING: return "STOPPING";
+ default: return "UNKNOWN: " + state;
+ }
+ }
+
+
+
+
+}