IT for provider network
[netvirt.git] / vpnservice / it / impl / src / test / java / org / opendaylight / netvirt / it / NetvirtIT.java
index 0b0f31fac1c9393cccdfee04084f1e5a1c63b85b..57bc2d7f8db96551bac5e45bb02eea7b177c17b3 100644 (file)
@@ -7,16 +7,18 @@
  */
 package org.opendaylight.netvirt.it;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.ops4j.pax.exam.CoreOptions.composite;
 import static org.ops4j.pax.exam.CoreOptions.maven;
 import static org.ops4j.pax.exam.CoreOptions.vmOption;
+import static org.ops4j.pax.exam.CoreOptions.when;
+import static org.ops4j.pax.exam.OptionUtils.combine;
 import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureConsole;
 import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.editConfigurationFilePut;
 import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.keepRuntimeFolder;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.logLevel;
 import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.replaceConfigurationFile;
 import static org.ops4j.pax.exam.karaf.options.LogLevelOption.LogLevel.DEBUG;
 import static org.ops4j.pax.exam.karaf.options.LogLevelOption.LogLevel.ERROR;
@@ -26,6 +28,8 @@ import static org.ops4j.pax.exam.karaf.options.LogLevelOption.LogLevel.WARN;
 
 import com.google.common.collect.Maps;
 import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -40,9 +44,15 @@ import org.opendaylight.netvirt.it.NetvirtITConstants.DefaultFlow;
 import org.opendaylight.ovsdb.utils.mdsal.utils.MdsalUtils;
 import org.opendaylight.ovsdb.utils.mdsal.utils.NotifyingDataChangeListener;
 import org.opendaylight.ovsdb.utils.ovsdb.it.utils.DockerOvs;
+import org.opendaylight.ovsdb.utils.ovsdb.it.utils.ItConstants;
 import org.opendaylight.ovsdb.utils.ovsdb.it.utils.NodeInfo;
 import org.opendaylight.ovsdb.utils.ovsdb.it.utils.OvsdbItUtils;
 import org.opendaylight.ovsdb.utils.southbound.utils.SouthboundUtils;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.types.rev130715.Uuid;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.genius.itm.op.rev160406.TunnelsState;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netvirt.neutronvpn.rev150602.VpnMaps;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netvirt.neutronvpn.rev150602.vpnmaps.VpnMap;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netvirt.neutronvpn.rev150602.vpnmaps.VpnMapKey;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.ovsdb.rev150105.ovsdb.node.attributes.ConnectionInfo;
 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology;
 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.TopologyId;
@@ -53,6 +63,7 @@ import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
 import org.ops4j.pax.exam.Configuration;
 import org.ops4j.pax.exam.Option;
 import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.karaf.options.LogLevelOption;
 import org.ops4j.pax.exam.options.MavenUrlReference;
 import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
 import org.ops4j.pax.exam.spi.reactors.PerClass;
@@ -67,6 +78,7 @@ import org.slf4j.LoggerFactory;
 @ExamReactorStrategy(PerClass.class)
 public class NetvirtIT extends AbstractMdsalTestBase {
     private static final Logger LOG = LoggerFactory.getLogger(NetvirtIT.class);
+    private static final String PHYSNET = "physnet";
     private static OvsdbItUtils itUtils;
     private static MdsalUtils mdsalUtils = null;
     private static SouthboundUtils southboundUtils;
@@ -77,6 +89,9 @@ public class NetvirtIT extends AbstractMdsalTestBase {
     @Inject @Filter(timeout = 60000)
     private static DataBroker dataBroker = null;
     private static String userSpaceEnabled;
+    private static final String OVS_ONE_NODE_YML = "ovs-2.5.0-hwvtep.yml";
+    private static final String OVS_TWO_NODE_YML = "two_" + OVS_ONE_NODE_YML;
+    private static NeutronSecurityGroupUtils neutronSecurityGroupUtils;
 
     @Override
     public MavenUrlReference getFeatureRepo() {
@@ -96,38 +111,55 @@ public class NetvirtIT extends AbstractMdsalTestBase {
     @Configuration
     @Override
     public Option[] config() {
-        Option[] ovsProps = super.config();
-        Option[] propertiesOptions = DockerOvs.getSysPropOptions();
-        Option[] otherOptions = getOtherOptions();
-        Option[] options = new Option[ovsProps.length + propertiesOptions.length + otherOptions.length];
-        System.arraycopy(ovsProps, 0, options, 0, ovsProps.length);
-        System.arraycopy(propertiesOptions, 0, options, ovsProps.length, propertiesOptions.length);
-        System.arraycopy(otherOptions, 0, options, ovsProps.length + propertiesOptions.length,
-                otherOptions.length);
-        return options;
+        Option[] tempOptions = combine(super.config(), DockerOvs.getSysPropOptions());
+        return combine(tempOptions, getOtherOptions());
     }
 
     private Option[] getOtherOptions() {
         return new Option[] {
                 configureConsole().startLocalConsole(),
-                //when("transparent".equals(System.getProperty("sgm"))).useOptions(
+                // Use transparent as the default
+                when("transparent".equals(System.getProperty("sgm", "transparent"))).useOptions(
                         replaceConfigurationFile(
                                 "etc/opendaylight/datastore/initial/config/netvirt-aclservice-config.xml",
-                                new File("src/test/resources/initial/netvirt-aclservice-config.xml")),//),
+                                new File("src/test/resources/initial/netvirt-aclservice-config-transparent.xml"))),
+                when("learn".equals(System.getProperty("sgm"))).useOptions(
+                        replaceConfigurationFile(
+                                "etc/opendaylight/datastore/initial/config/netvirt-aclservice-config.xml",
+                                new File("src/test/resources/initial/netvirt-aclservice-config-learn.xml"))),
+                when("stateful".equals(System.getProperty("sgm"))).useOptions(
+                        replaceConfigurationFile(
+                                "etc/opendaylight/datastore/initial/config/netvirt-aclservice-config.xml",
+                                new File("src/test/resources/initial/netvirt-aclservice-config-stateful.xml"))),
+                when("stateless".equals(System.getProperty("sgm"))).useOptions(
+                        replaceConfigurationFile(
+                                "etc/opendaylight/datastore/initial/config/netvirt-aclservice-config.xml",
+                                new File("src/test/resources/initial/netvirt-aclservice-config-stateless.xml"))),
+                // Add our own logging.cfg so we can log to a single karaf.log file
+                replaceConfigurationFile("etc/org.ops4j.pax.logging.cfg",
+                        new File("src/test/resources/org.ops4j.pax.logging.cfg")),
                 vmOption("-javaagent:../jars/org.jacoco.agent.jar=destfile=../../jacoco-it.exec"),
+                vmOption("-Xmx2048m"),
+                //vmOption("-XX:MaxPermSize=m"),
                 keepRuntimeFolder()
         };
     }
 
+    // This won't get used when we use our own logging.cfg file set in getOtherOptions
+    // but we keep it for reference.
     @Override
     public Option getLoggingOption() {
         return composite(
+                logLevel(LogLevelOption.LogLevel.INFO),
                 editConfigurationFilePut(ORG_OPS4J_PAX_LOGGING_CFG,
                         logConfiguration(NetvirtIT.class),
                         INFO.name()),
                 editConfigurationFilePut(ORG_OPS4J_PAX_LOGGING_CFG,
                         "log4j.logger.org.opendaylight.netvirt",
                         TRACE.name()),
+                editConfigurationFilePut(ORG_OPS4J_PAX_LOGGING_CFG,
+                        "log4j.logger.org.opendaylight.genius",
+                        TRACE.name()),
                 editConfigurationFilePut(ORG_OPS4J_PAX_LOGGING_CFG,
                         "log4j.logger.org.opendaylight.ovsdb.utils.southbound.utils.SouthboundUtils",
                         TRACE.name()),
@@ -150,6 +182,10 @@ public class NetvirtIT extends AbstractMdsalTestBase {
                         "log4j.logger.org.opendaylight.netvirt.fibmanager.FibNodeCapableListener",
                         DEBUG.name()),
                 super.getLoggingOption());
+                // TODO trying to get console logged to karaf.log, but doesn't work.
+                // wondering if the test stops and the log isn't flushed?
+                //editConfigurationFilePut(ORG_OPS4J_PAX_LOGGING_CFG,
+                //        "log4j.rootLogger", "INFO, async, stdout, osgi:*"));
     }
 
     @Before
@@ -179,6 +215,7 @@ public class NetvirtIT extends AbstractMdsalTestBase {
         nvSouthboundUtils = new org.opendaylight.netvirt.it.SouthboundUtils(mdsalUtils);
         assertTrue("Did not find " + NETVIRT_TOPOLOGY_ID, getNetvirtTopology());
         flowITUtil = new FlowITUtil(dataBroker);
+        neutronSecurityGroupUtils = new NeutronSecurityGroupUtils(mdsalUtils);
 
         setup.set(true);
     }
@@ -230,9 +267,10 @@ public class NetvirtIT extends AbstractMdsalTestBase {
         }
     }
 
-    private void addLocalIp(NodeInfo nodeInfo) {
+    private void addLocalIp(NodeInfo nodeInfo, String ip) {
+        LOG.info("addlocalIp: nodeinfo: {}, local_ip: {}", nodeInfo.ovsdbNode.getNodeId(), ip);
         Map<String, String> otherConfigs = Maps.newHashMap();
-        otherConfigs.put("local_ip", "10.1.1.1");
+        otherConfigs.put("local_ip", ip);
         assertTrue(nvSouthboundUtils.addOpenVSwitchOtherConfig(nodeInfo.ovsdbNode, otherConfigs));
     }
 
@@ -249,25 +287,31 @@ public class NetvirtIT extends AbstractMdsalTestBase {
     @Test
     @SuppressWarnings("checkstyle:IllegalCatch")
     public void testNetVirt() throws InterruptedException {
+        int ovs1 = 1;
+        System.getProperties().setProperty(ItConstants.DOCKER_COMPOSE_FILE_NAME, OVS_ONE_NODE_YML);
         try (DockerOvs ovs = new DockerOvs()) {
-            ovs.logState(0, "idle");
-            ConnectionInfo connectionInfo =
-                    SouthboundUtils.getConnectionInfo(ovs.getOvsdbAddress(0), ovs.getOvsdbPort(0));
-            NodeInfo nodeInfo = itUtils.createNodeInfo(connectionInfo, null);
-            nodeInfo.connect();
-            LOG.info("testNetVirt: should be connected: {}", nodeInfo.ovsdbNode.getNodeId());
-            addLocalIp(nodeInfo);
-
-            validateDefaultFlows(nodeInfo.datapathId, 2 * 60 * 1000);
-            ovs.logState(0, "default flows");
-
-            nodeInfo.disconnect();
+            Boolean isUserSpace = userSpaceEnabled.equals("yes");
+            LOG.info("isUserSpace: {}, usingExternalDocker: {}", isUserSpace, ovs.usingExternalDocker());
+            NetOvs netOvs = getNetOvs(ovs, isUserSpace);
+
+            NodeInfo nodeInfo = connectOvs(netOvs, ovs1, ovs);
+
+            disconnectOvs(nodeInfo);
         } catch (Exception e) {
             LOG.error("testNetVirt: Exception thrown by OvsDocker.OvsDocker()", e);
             fail("testNetVirt: Exception thrown by OvsDocker.OvsDocker() : " + e.getMessage());
         }
     }
 
+    private static final String NETWORK1_NAME = "net1";
+    private static final String NETWORK1_SEGID = "101";
+    private static final String NETWORK1_IPPFX = "10.1.1.";
+
+    private static final String NETWORK2_NAME = "net2";
+    private static final String NETWORK2_SEGID = "201";
+    private static final String NETWORK2_IPPFX = "20.1.1.";
+
+    private static final String ROUTER1_NAME = "router1";
 
     /**
      * Test a basic neutron use case. This test constructs a Neutron network, subnet, and two "vm" ports
@@ -277,58 +321,340 @@ public class NetvirtIT extends AbstractMdsalTestBase {
     @Test
     @SuppressWarnings("checkstyle:IllegalCatch")
     public void testNeutronNet() throws InterruptedException {
-        LOG.warn("testNeutronNet: starting test");
+        int ovs1 = 1;
+        System.getProperties().setProperty(ItConstants.DOCKER_COMPOSE_FILE_NAME, OVS_ONE_NODE_YML);
+        try (DockerOvs ovs = new DockerOvs()) {
+            Boolean isUserSpace = userSpaceEnabled.equals("yes");
+            LOG.info("isUserSpace: {}, usingExternalDocker: {}", isUserSpace, ovs.usingExternalDocker());
+            NetOvs netOvs = getNetOvs(ovs, isUserSpace);
+            netOvs.createNetwork(NETWORK1_NAME, NETWORK1_SEGID, NETWORK1_IPPFX);
+
+            //Creating default SG
+            LOG.info("Installing default SG");
+            List<Uuid> sgList = new ArrayList<>();
+            sgList.add(neutronSecurityGroupUtils.createDefaultSG());
+
+            NodeInfo nodeInfo = connectOvs(netOvs, ovs1, ovs);
+            String port1 = addPort(netOvs, nodeInfo, ovs1, NETWORK1_NAME, sgList);
+            String port2 = addPort(netOvs, nodeInfo, ovs1, NETWORK1_NAME, sgList);
+
+            int rc = netOvs.ping(port1, port2);
+            LOG.info("Ping status rc: {}, ignored for isUserSpace: {}", rc, isUserSpace);
+            netOvs.logState(ovs1, "node 1 after ping");
+            if (!isUserSpace) {
+                LOG.info("Ping status rc: {}", rc);
+            }
+
+            destroyOvs(netOvs);
+            disconnectOvs(nodeInfo);
+        } catch (Exception e) {
+            LOG.error("testNeutronNet: Exception thrown by OvsDocker.OvsDocker()", e);
+            fail("testNeutronNet: Exception thrown by OvsDocker.OvsDocker() : " + e.getMessage());
+        }
+    }
+
+    @Test
+    @SuppressWarnings("checkstyle:IllegalCatch")
+    public void testNeutronNetL3() throws InterruptedException {
+        int ovs1 = 1;
+        System.getProperties().setProperty(ItConstants.DOCKER_COMPOSE_FILE_NAME, OVS_ONE_NODE_YML);
+        try (DockerOvs ovs = new DockerOvs()) {
+            Boolean isUserSpace = userSpaceEnabled.equals("yes");
+            LOG.info("isUserSpace: {}, usingExternalDocker: {}", isUserSpace, ovs.usingExternalDocker());
+            NetOvs netOvs = getNetOvs(ovs, isUserSpace);
+
+            //create 2 networks
+            netOvs.createNetwork(NETWORK1_NAME, NETWORK1_SEGID, NETWORK1_IPPFX);
+            netOvs.createNetwork(NETWORK2_NAME, NETWORK2_SEGID, NETWORK2_IPPFX);
+
+            //Creating default SG
+            LOG.info("Installing default SG");
+            List<Uuid> sgList = new ArrayList<>();
+            sgList.add(neutronSecurityGroupUtils.createDefaultSG());
+
+            NodeInfo nodeInfo = connectOvs(netOvs, ovs1, ovs);
+            //create 2 "vms" ports
+            String port1 = addPort(netOvs, nodeInfo, ovs1, NETWORK1_NAME, sgList);
+            String port2 = addPort(netOvs, nodeInfo, ovs1, NETWORK2_NAME, sgList);
+
+            int rc = netOvs.ping(port1, port2);
+            netOvs.logState(ovs1, "after ping without router");
+            assertTrue("Ping should fail without router", rc != 0);
+
+            //create neutron router and add the networks
+            addRouter(netOvs, ROUTER1_NAME);
+            netOvs.createRouterInterface(ROUTER1_NAME, NETWORK1_NAME);
+            netOvs.createRouterInterface(ROUTER1_NAME, NETWORK2_NAME);
+
+            rc = netOvs.ping(port1, port2);
+            netOvs.logState(ovs1, "after ping with router");
+            assertTrue("Ping with router", rc == 0);
+
+            destroyOvs(netOvs);
+            disconnectOvs(nodeInfo);
+        } catch (Exception e) {
+            LOG.error("testNeutronNetL3: Exception thrown by OvsDocker.OvsDocker()", e);
+            fail("testNeutronNetL3: Exception thrown by OvsDocker.OvsDocker() : " + e.getMessage());
+        }
+    }
+
+    // This test requires ovs kernel modules to be loaded which is not in jenkins yet.
+    @Test
+    @SuppressWarnings("checkstyle:IllegalCatch")
+    public void testNeutronNetTwoNodes() throws InterruptedException {
+        int ovs1 = 1;
+        int ovs2 = 2;
+        System.getProperties().setProperty(ItConstants.DOCKER_COMPOSE_FILE_NAME, OVS_TWO_NODE_YML);
         try (DockerOvs ovs = new DockerOvs()) {
-            Neutron neutron = new Neutron(mdsalUtils);
-            NetOvs netOvs;
             Boolean isUserSpace = userSpaceEnabled.equals("yes");
             LOG.info("isUserSpace: {}, usingExternalDocker: {}", isUserSpace, ovs.usingExternalDocker());
+            NetOvs netOvs = getNetOvs(ovs, isUserSpace);
+
+            netOvs.createNetwork(NETWORK1_NAME, NETWORK1_SEGID, NETWORK1_IPPFX);
+
+            //Creating default SG
+            LOG.info("Installing default SG");
+            List<Uuid> sgList = new ArrayList<>();
+            sgList.add(neutronSecurityGroupUtils.createDefaultSG());
+
+            NodeInfo nodeInfo = connectOvs(netOvs, ovs1, ovs);
+            NodeInfo nodeInfo2 = connectOvs(netOvs, ovs2, ovs);
+            String port1 = addPort(netOvs, nodeInfo, ovs1, NETWORK1_NAME, sgList);
+            String port2 = addPort(netOvs, nodeInfo2, ovs2, NETWORK1_NAME, sgList);
+
+            int rc = netOvs.ping(port1, port2);
+            LOG.info("Ping status rc: {}, ignored for isUserSpace: {}", rc, isUserSpace);
+            netOvs.logState(ovs1, "node 1 after ping");
+            netOvs.logState(ovs2, "node 2 after ping");
+            if (!isUserSpace) {
+                LOG.info("Ping status rc: {}", rc);
+            }
+
+            destroyOvs(netOvs);
+            disconnectOvs(nodeInfo);
+            disconnectOvs(nodeInfo2);
+        } catch (Exception e) {
+            LOG.error("testNeutronNetTwoNodes: Exception thrown by OvsDocker.OvsDocker()", e);
+            fail("testNeutronNetTwoNodes: Exception thrown by OvsDocker.OvsDocker() : " + e.getMessage());
+        }
+    }
+
+    @Test
+    @SuppressWarnings("checkstyle:IllegalCatch")
+    public void testProviderNetTwoNodes() throws InterruptedException {
+        int ovs1 = 1;
+        int ovs2 = 2;
+        Properties props = System.getProperties();
+        props.setProperty(ItConstants.DOCKER_COMPOSE_FILE_NAME, "two_ovs-2.5.1-dual-nic.yml");
+        props.setProperty(ItConstants.DOCKER_WAIT_FOR_PING_SECS, "20");
+
+        //Remove the ovsdb.controller.ipaddress to force DockerOvs to create it's own network
+        //since that is the only way this docker compose file works (it uses the "odl" network)
+        //We reset the env. in the finally clause
+        String controllerIpAddress = props.getProperty(ItConstants.CONTROLLER_IPADDRESS);
+        props.remove(ItConstants.CONTROLLER_IPADDRESS);
+        try (DockerOvs ovs = new DockerOvs()) {
             if (ovs.usingExternalDocker()) {
-                netOvs = new RealNetOvsImpl(ovs, isUserSpace, mdsalUtils, neutron, southboundUtils);
-            } else {
-                netOvs = new DockerNetOvsImpl(ovs, isUserSpace, mdsalUtils, neutron, southboundUtils);
+                LOG.debug("testProviderNetTwoNodes - Not configured to run docker, skipping this test");
+                return;
             }
+            Boolean isUserSpace = userSpaceEnabled.equals("yes");
+            LOG.info("isUserSpace: {}, usingExternalDocker: {}", isUserSpace, ovs.usingExternalDocker());
 
-            netOvs.logState(0, "idle");
-            ConnectionInfo connectionInfo =
-                    SouthboundUtils.getConnectionInfo(ovs.getOvsdbAddress(0), ovs.getOvsdbPort(0));
-            NodeInfo nodeInfo = itUtils.createNodeInfo(connectionInfo, null);
-            nodeInfo.connect();
-            LOG.info("testNeutronNet: should be connected: {}", nodeInfo.ovsdbNode.getNodeId());
-            addLocalIp(nodeInfo);
+            NetOvs netOvs = getNetOvs(ovs, isUserSpace);
 
-            validateDefaultFlows(nodeInfo.datapathId, 2 * 60 * 1000);
-            netOvs.logState(0, "default flows");
+            NodeInfo nodeInfo = connectOvs(netOvs, ovs1, ovs);
+            NodeInfo nodeInfo2 = connectOvs(netOvs, ovs2, ovs);
 
-            neutron.createNetwork();
-            neutron.createSubnet();
+            netOvs.createFlatNetwork(NETWORK1_NAME, NETWORK1_SEGID, NETWORK1_IPPFX, PHYSNET);
 
-            String port1 = netOvs.createPort(nodeInfo.bridgeNode);
-            String port2 = netOvs.createPort(nodeInfo.bridgeNode);
+            String port1 = addPort(netOvs, nodeInfo, ovs1, NETWORK1_NAME, null);
+            String port2 = addPort(netOvs, nodeInfo2, ovs2, NETWORK1_NAME, null);
 
-            InstanceIdentifier<TerminationPoint> tpIid =
-                    southboundUtils.createTerminationPointInstanceIdentifier(nodeInfo.bridgeNode, port2);
-            final NotifyingDataChangeListener portOperationalListener =
-                    new NotifyingDataChangeListener(LogicalDatastoreType.OPERATIONAL,
-                            NotifyingDataChangeListener.BIT_CREATE, tpIid, null);
-            portOperationalListener.registerDataChangeListener(dataBroker);
+            int rc = netOvs.ping(port1, port2);
+            LOG.info("Ping status rc: {}, ignored for isUserSpace: {}", rc, isUserSpace);
+            netOvs.logState(ovs1, "node 1 after ping");
+            netOvs.logState(ovs2, "node 2 after ping");
+            if (!isUserSpace) {
+                LOG.info("Ping status rc: {}", rc);
+            }
 
-            netOvs.preparePortForPing(port1);
-            netOvs.preparePortForPing(port2);
+            destroyOvs(netOvs);
+            disconnectOvs(nodeInfo);
+            disconnectOvs(nodeInfo2);
+        } catch (Exception e) {
+            LOG.error("testProviderNet: Exception thrown by OvsDocker.OvsDocker()", e);
+            fail("testProviderNet: Exception thrown by OvsDocker.OvsDocker() : " + e.getMessage());
+        } finally {
+            if (controllerIpAddress != null) {
+                props.setProperty(ItConstants.CONTROLLER_IPADDRESS, controllerIpAddress);
+            }
+        }
+    }
+
+    // This test requires ovs kernel modules to be loaded which is not in jenkins yet.
+    @Test
+    @SuppressWarnings("checkstyle:IllegalCatch")
+    public void testNeutronNetL3TwoNodes() throws InterruptedException {
+        int ovs1 = 1;
+        int ovs2 = 2;
+        System.getProperties().setProperty(ItConstants.DOCKER_COMPOSE_FILE_NAME, OVS_TWO_NODE_YML);
+        try (DockerOvs ovs = new DockerOvs()) {
+            Boolean isUserSpace = userSpaceEnabled.equals("yes");
+            LOG.info("isUserSpace: {}, usingExternalDocker: {}", isUserSpace, ovs.usingExternalDocker());
+            NetOvs netOvs = getNetOvs(ovs, isUserSpace);
 
-            portOperationalListener.waitForCreation(10000);
-            Thread.sleep(30000);
-            netOvs.logState(0, "after ports");
+            //Creating default SG
+            LOG.info("Installing default SG");
+            List<Uuid> sgList = new ArrayList<>();
+            sgList.add(neutronSecurityGroupUtils.createDefaultSG());
+
+            //create 2 networks
+            netOvs.createNetwork(NETWORK1_NAME, NETWORK1_SEGID, NETWORK1_IPPFX);
+            netOvs.createNetwork(NETWORK2_NAME, NETWORK2_SEGID, NETWORK2_IPPFX);
+
+            NodeInfo nodeInfo = connectOvs(netOvs, ovs1, ovs);
+            NodeInfo nodeInfo2 = connectOvs(netOvs, ovs2, ovs);
+            //create 2 "vms" ports
+            String port1 = addPort(netOvs, nodeInfo, ovs1, NETWORK1_NAME, sgList);
+            String port2 = addPort(netOvs, nodeInfo2, ovs2, NETWORK2_NAME, sgList);
 
             int rc = netOvs.ping(port1, port2);
-            netOvs.logState(0, "after ping");
-            assertEquals("Ping failed rc: " + rc, 0, rc);
+            netOvs.logState(ovs1, "node 1 after ping without router");
+            netOvs.logState(ovs2, "node 2 after ping without router");
+            assertTrue("Ping should fail without router", rc != 0);
+
+            //create neutron router and add the networks
+            addRouter(netOvs, ROUTER1_NAME);
+            netOvs.createRouterInterface(ROUTER1_NAME, NETWORK1_NAME);
+            netOvs.createRouterInterface(ROUTER1_NAME, NETWORK2_NAME);
+
+            waitForTunnels();
+            rc = netOvs.ping(port1, port2);
+            netOvs.logState(ovs1, "node 1 after ping with router");
+            netOvs.logState(ovs2, "node 2 after ping with router");
+            if (!isUserSpace) {
+                assertTrue("Ping with router", rc == 0);
+            }
 
-            netOvs.destroy();
-            nodeInfo.disconnect();
+            destroyOvs(netOvs);
+            disconnectOvs(nodeInfo);
+            disconnectOvs(nodeInfo2);
         } catch (Exception e) {
-            LOG.error("testNeutronNet: Exception thrown by OvsDocker.OvsDocker()", e);
-            fail("testNeutronNet: Exception thrown by OvsDocker.OvsDocker() : " + e.getMessage());
+            LOG.error("testNeutronNetL3TwoNodes: Exception thrown by OvsDocker.OvsDocker()", e);
+            fail("testNeutronNetL3TwoNodes: Exception thrown by OvsDocker.OvsDocker() : " + e.getMessage());
+        }
+    }
+
+    private NetOvs getNetOvs(DockerOvs ovs, Boolean isUserSpace) {
+        NetOvs netOvs;
+        if (ovs.usingExternalDocker()) {
+            netOvs = new RealNetOvsImpl(ovs, isUserSpace, mdsalUtils, southboundUtils);
+        } else {
+            netOvs = new DockerNetOvsImpl(ovs, isUserSpace, mdsalUtils, southboundUtils);
+        }
+        return netOvs;
+    }
+
+    private NodeInfo connectOvs(NetOvs netOvs, int ovsInstance, DockerOvs ovs) throws Exception {
+        LOG.info("connectOvs enter: netOvs {}", ovsInstance);
+        netOvs.logState(ovsInstance, "node " + ovsInstance + " idle");
+        ConnectionInfo connectionInfo =
+                SouthboundUtils.getConnectionInfo(ovs.getOvsdbAddress(ovsInstance), ovs.getOvsdbPort(ovsInstance));
+        NodeInfo nodeInfo = itUtils.createNodeInfo(connectionInfo, null);
+        nodeInfo.connect();
+        LOG.info("connectOvs: node {} should be connected: {}",
+                ovsInstance, nodeInfo.ovsdbNode.getNodeId());
+        String localIp = netOvs.getInstanceIp(ovsInstance);
+        addLocalIp(nodeInfo, localIp);
+
+        validateDefaultFlows(nodeInfo.datapathId, 2 * 60 * 1000);
+        netOvs.logState(ovsInstance, "node " + ovsInstance + " default flows");
+        LOG.info("connectOvs exit: netOvs {}", ovsInstance);
+        return nodeInfo;
+    }
+
+    private void disconnectOvs(NodeInfo nodeInfo) throws Exception {
+        LOG.info("disconnectOvs enter: {}", nodeInfo.ovsdbNode.getNodeId().getValue());
+        nodeInfo.disconnect();
+        Thread.sleep(5000);
+        LOG.info("disconnectOvs exit: {}", nodeInfo.ovsdbNode.getNodeId().getValue());
+    }
+
+    private void destroyOvs(NetOvs netOvs) throws InterruptedException {
+        LOG.info("destroyOvs enter");
+        netOvs.destroy();
+        // This sleep allows netvirt and genius to run properly cleanup of neutron ports
+        // and networks deleted by destroy()
+        Thread.sleep(5000);
+        LOG.info("destroyOvs exit");
+    }
+
+    private String addPort(NetOvs netOvs, NodeInfo nodeInfo, int ovsInstance, String networkName,
+            List<Uuid> securityGroupList) throws Exception {
+        String port = netOvs.createPort(ovsInstance, nodeInfo.bridgeNode, networkName, securityGroupList);
+        LOG.info("addPort enter: Bridge node: {}, Created port: {} on network: {}",
+                nodeInfo.bridgeNode.getNodeId().getValue(), netOvs.getPortInfo(port), networkName);
+
+        InstanceIdentifier<TerminationPoint> tpIid =
+                southboundUtils.createTerminationPointInstanceIdentifier(nodeInfo.bridgeNode, port);
+        final NotifyingDataChangeListener portOperationalListener =
+                new NotifyingDataChangeListener(LogicalDatastoreType.OPERATIONAL,
+                        NotifyingDataChangeListener.BIT_CREATE, tpIid, null);
+        portOperationalListener.registerDataChangeListener(dataBroker);
+
+        netOvs.preparePortForPing(port);
+
+        portOperationalListener.waitForCreation(10000);
+        portOperationalListener.clear();
+        portOperationalListener.close();
+        // TODO: find better wait condition, what event indicates the port is added
+        // in the models and is ready for use
+        Thread.sleep(30000);
+        netOvs.logState(ovsInstance, "node " + ovsInstance + " " + nodeInfo.bridgeNode.getNodeId().getValue()
+                + " after port " + netOvs.getPortInfo(port));
+        LOG.info("addPort exit: Bridge node: {}, Created port: {} on network: {}",
+                nodeInfo.bridgeNode.getNodeId().getValue(), netOvs.getPortInfo(port), networkName);
+        return port;
+    }
+
+    private void addRouter(NetOvs netOvs, String routerName) throws Exception {
+        LOG.info("addRouter enter: {}", routerName);
+        String routerId = netOvs.createRouter(routerName);
+
+        //wait for VpnMap update before starting to use the router
+        InstanceIdentifier<VpnMap> vpnIid = InstanceIdentifier.builder(VpnMaps.class)
+                .child(VpnMap.class, new VpnMapKey(new Uuid(routerId)))
+                .build();
+        final NotifyingDataChangeListener vpnMapListener =
+                new NotifyingDataChangeListener(LogicalDatastoreType.CONFIGURATION,
+                        NotifyingDataChangeListener.BIT_CREATE, vpnIid, null);
+        vpnMapListener.registerDataChangeListener(dataBroker);
+        vpnMapListener.waitForCreation(10000);
+        vpnMapListener.close();
+        LOG.info("addRouter exit: {}", routerName);
+    }
+
+    private void waitForTunnels() throws InterruptedException {
+        LOG.info("waitForTunnels enter");
+        InstanceIdentifier<TunnelsState> tunIid = InstanceIdentifier.builder(TunnelsState.class).build();
+        for (int i = 0; i < 10; i++) {
+            TunnelsState tunnelsState = mdsalUtils.read(LogicalDatastoreType.OPERATIONAL, tunIid);
+            LOG.info("waitForTunnels try {}, {}", i, tunnelsState);
+            if (tunnelsState != null && tunnelsState.getStateTunnelList() != null) {
+                // TODO: add more verification to validate the two tunnels are the right ones
+                // i.e. check host ips or other parts of the model
+                if (tunnelsState.getStateTunnelList().size() == 2) {
+                    LOG.info("waitForTunnels found both tunnels");
+                    break;
+                } else {
+                    LOG.info("waitForTunnels try {}, size: {}", i, tunnelsState.getStateTunnelList().size());
+                }
+            } else {
+                Thread.sleep(1000);
+            }
         }
+        Thread.sleep(3000);
+        LOG.info("waitForTunnels exit");
     }
 }