[ONOS-7752] check east-west VM reachability for troubleshoot purpose

Change-Id: I2922f5bc185a76906da5f12e4d18e640b9ecd8d8
diff --git a/apps/openstacktroubleshoot/api/src/main/java/org/onosproject/openstacktroubleshoot/api/OpenstackTroubleshootService.java b/apps/openstacktroubleshoot/api/src/main/java/org/onosproject/openstacktroubleshoot/api/OpenstackTroubleshootService.java
index 782b3b3..4ad6cfd 100644
--- a/apps/openstacktroubleshoot/api/src/main/java/org/onosproject/openstacktroubleshoot/api/OpenstackTroubleshootService.java
+++ b/apps/openstacktroubleshoot/api/src/main/java/org/onosproject/openstacktroubleshoot/api/OpenstackTroubleshootService.java
@@ -15,13 +15,47 @@
  */
 package org.onosproject.openstacktroubleshoot.api;
 
+import org.onlab.packet.IpAddress;
+
+import java.util.Map;
+
 /**
  * Openstack troubleshoot interface.
  */
 public interface OpenstackTroubleshootService {
 
     /**
-     * A dummy method.
+     * Checks all east-west VMs' connectivity.
+     *
+     * @return reachability map
      */
-    void dummy();
+    Map<String, Reachability> probeEastWestBulk();
+
+    /**
+     * Checks a single VM-to-Vm connectivity.
+     *
+     * @param srcNetId  source network ID
+     * @param srcIp     source IP address
+     * @param dstNetId  destination network ID
+     * @param dstIp     destination IP address
+     * @return reachability
+     */
+    Reachability probeEastWest(String srcNetId, IpAddress srcIp,
+                               String dstNetId, IpAddress dstIp);
+
+    /**
+     * Checks all north-south router to VMs' connectivity.
+     *
+     * @return reachability map
+     */
+    Map<String, Reachability> probeNorthSouth();
+
+    /**
+     * Checks a single router-to-VM connectivity.
+     *
+     * @param netId network ID
+     * @param ip destination VM IP address
+     * @return reachability
+     */
+    Reachability probeNorthSouth(String netId, IpAddress ip);
 }
diff --git a/apps/openstacktroubleshoot/api/src/main/java/org/onosproject/openstacktroubleshoot/api/Reachability.java b/apps/openstacktroubleshoot/api/src/main/java/org/onosproject/openstacktroubleshoot/api/Reachability.java
new file mode 100644
index 0000000..4cb6cad
--- /dev/null
+++ b/apps/openstacktroubleshoot/api/src/main/java/org/onosproject/openstacktroubleshoot/api/Reachability.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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.onosproject.openstacktroubleshoot.api;
+
+import org.onlab.packet.IpAddress;
+
+/**
+ * Reachbility interface which stores VM to VM connectivity.
+ */
+public interface Reachability {
+
+    /**
+     * Source IP address.
+     *
+     * @return source IP address
+     */
+    IpAddress srcIp();
+
+    /**
+     * Destination IP address.
+     *
+     * @return destination IP address
+     */
+    IpAddress dstIp();
+
+    /**
+     * Indicates whether the given source and destination VMs are reachable.
+     *
+     * @return reachability indicator
+     */
+    boolean isReachable();
+
+    /**
+     * Builder of new reachability object.
+     */
+    interface Builder {
+
+        /**
+         * Returns reachability builder with supplied IP address.
+         *
+         * @param srcIp source IP address
+         * @return reachability builder
+         */
+        Builder srcIp(IpAddress srcIp);
+
+        /**
+         * Returns reachability builder with supplied IP address.
+         *
+         * @param dstIp destination IP address
+         * @return reachability builder
+         */
+        Builder dstIp(IpAddress dstIp);
+
+        /**
+         * Returns reachability builder with supplied reachability flag.
+         *
+         * @param isReachable reachability flag
+         * @return reachability builder
+         */
+        Builder isReachable(boolean isReachable);
+
+        /**
+         * Builds an immutable reachability instance.
+         *
+         * @return reachability instance
+         */
+        Reachability build();
+    }
+}
diff --git a/apps/openstacktroubleshoot/app/BUCK b/apps/openstacktroubleshoot/app/BUCK
index 858b63c..b0c072c 100644
--- a/apps/openstacktroubleshoot/app/BUCK
+++ b/apps/openstacktroubleshoot/app/BUCK
@@ -2,10 +2,13 @@
     '//lib:CORE_DEPS',
     '//lib:JACKSON',
     '//lib:KRYO',
+    '//core/store/serializers:onos-core-serializers',
     '//lib:org.apache.karaf.shell.console',
     '//cli:onos-cli',
     '//lib:javax.ws.rs-api',
     '//utils/rest:onlab-rest',
+    '//apps/openstacknode/api:onos-apps-openstacknode-api',
+    '//apps/openstacknetworking/api:onos-apps-openstacknetworking-api',
     '//apps/openstacktroubleshoot/api:onos-apps-openstacktroubleshoot-api',
 ]
 
diff --git a/apps/openstacktroubleshoot/app/BUILD b/apps/openstacktroubleshoot/app/BUILD
index 3c979d2..6e2dd33 100644
--- a/apps/openstacktroubleshoot/app/BUILD
+++ b/apps/openstacktroubleshoot/app/BUILD
@@ -1,4 +1,7 @@
 COMPILE_DEPS = CORE_DEPS + JACKSON + KRYO + CLI + REST + [
+    "//core/store/serializers:onos-core-serializers",
+    "//apps/openstacknode/api:onos-apps-openstacknode-api",
+    "//apps/openstacknetworking/api:onos-apps-openstacknetworking-api",
     "//apps/openstacktroubleshoot/api:onos-apps-openstacktroubleshoot-api",
 ]
 
diff --git a/apps/openstacktroubleshoot/app/pom.xml b/apps/openstacktroubleshoot/app/pom.xml
index e1ab1bf..5d1f31f 100644
--- a/apps/openstacktroubleshoot/app/pom.xml
+++ b/apps/openstacktroubleshoot/app/pom.xml
@@ -50,12 +50,30 @@
 
         <dependency>
             <groupId>org.onosproject</groupId>
+            <artifactId>onos-apps-openstacknode-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-apps-openstacknetworking-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
             <artifactId>onos-apps-openstacktroubleshoot-api</artifactId>
             <version>${project.version}</version>
         </dependency>
 
         <dependency>
             <groupId>org.onosproject</groupId>
+            <artifactId>onos-core-serializers</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
             <artifactId>onos-rest</artifactId>
             <version>${project.version}</version>
         </dependency>
@@ -65,6 +83,7 @@
             <artifactId>onlab-rest</artifactId>
             <version>${project.version}</version>
         </dependency>
+
         <dependency>
             <groupId>javax.ws.rs</groupId>
             <artifactId>javax.ws.rs-api</artifactId>
diff --git a/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/cli/OpenstackEastWestProbeCommand.java b/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/cli/OpenstackEastWestProbeCommand.java
new file mode 100644
index 0000000..f9786ac
--- /dev/null
+++ b/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/cli/OpenstackEastWestProbeCommand.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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.onosproject.openstacktroubleshoot.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.openstacktroubleshoot.api.OpenstackTroubleshootService;
+import org.onosproject.openstacktroubleshoot.api.Reachability;
+
+import java.util.Map;
+
+/**
+ * Checks the east-west VMs connectivity.
+ */
+@Command(scope = "onos", name = "openstack-check-east-west",
+        description = "Checks the east-west VMs connectivity")
+public class OpenstackEastWestProbeCommand extends AbstractShellCommand {
+
+    private static final String REACHABLE = "Reachable :)";
+    private static final String UNREACHABLE = "Unreachable :(";
+    private static final String ARROW = "->";
+
+    private static final String FORMAT = "%-20s%-5s%-20s%-20s";
+
+    @Override
+    protected void execute() {
+        OpenstackTroubleshootService troubleshootService =
+                AbstractShellCommand.get(OpenstackTroubleshootService.class);
+
+        if (troubleshootService == null) {
+            error("Failed to troubleshoot openstack networking.");
+            return;
+        }
+
+        print(FORMAT, "Source IP", "", "Destination IP", "Reachability");
+
+        Map<String, Reachability> map = troubleshootService.probeEastWestBulk();
+
+        map.values().forEach(r -> {
+            String result = r.isReachable() ? REACHABLE : UNREACHABLE;
+            print(FORMAT, r.srcIp().toString(), ARROW, r.dstIp().toString(), result);
+        });
+    }
+}
diff --git a/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/impl/DefaultReachability.java b/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/impl/DefaultReachability.java
new file mode 100644
index 0000000..3290b1d
--- /dev/null
+++ b/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/impl/DefaultReachability.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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.onosproject.openstacktroubleshoot.impl;
+
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.IpAddress;
+import org.onosproject.openstacktroubleshoot.api.Reachability;
+
+import java.util.Objects;
+
+/**
+ * Implementation of reachability.
+ */
+public final class DefaultReachability implements Reachability {
+
+    private final IpAddress srcIp;
+    private final IpAddress dstIp;
+    private final boolean isReachable;
+
+    private DefaultReachability(IpAddress srcIp, IpAddress dstIp, boolean isReachable) {
+        this.srcIp = srcIp;
+        this.dstIp = dstIp;
+        this.isReachable = isReachable;
+    }
+
+    @Override
+    public IpAddress srcIp() {
+        return srcIp;
+    }
+
+    @Override
+    public IpAddress dstIp() {
+        return dstIp;
+    }
+
+    @Override
+    public boolean isReachable() {
+        return isReachable;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof DefaultReachability) {
+            DefaultReachability that = (DefaultReachability) obj;
+            return Objects.equals(srcIp, that.srcIp) &&
+                    Objects.equals(dstIp, that.dstIp) &&
+                    Objects.equals(isReachable, that.isReachable);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(srcIp, dstIp, isReachable);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("srcIp", srcIp)
+                .add("dstIp", dstIp)
+                .add("isReachable", isReachable)
+                .toString();
+    }
+
+    /**
+     * Obtains a reachability builder.
+     *
+     * @return reachability builder
+     */
+    public static Builder builder() {
+        return new DefaultBuilder();
+    }
+
+    /**
+     * A builder class for reachability.
+     */
+    public static final class DefaultBuilder implements Builder {
+
+        private IpAddress srcIp;
+        private IpAddress dstIp;
+        private boolean isReachable;
+
+        @Override
+        public Builder srcIp(IpAddress srcIp) {
+            this.srcIp = srcIp;
+            return this;
+        }
+
+        @Override
+        public Builder dstIp(IpAddress dstIp) {
+            this.dstIp = dstIp;
+            return this;
+        }
+
+        @Override
+        public Builder isReachable(boolean isReachable) {
+            this.isReachable = isReachable;
+            return this;
+        }
+
+        @Override
+        public Reachability build() {
+            return new DefaultReachability(srcIp, dstIp, isReachable);
+        }
+    }
+}
diff --git a/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/impl/OpenstackTroubleshootManager.java b/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/impl/OpenstackTroubleshootManager.java
index 9378c55..3124da6 100644
--- a/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/impl/OpenstackTroubleshootManager.java
+++ b/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/impl/OpenstackTroubleshootManager.java
@@ -15,13 +15,755 @@
  */
 package org.onosproject.openstacktroubleshoot.impl;
 
+import com.google.common.collect.Sets;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.packet.DeserializationException;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.ICMP;
+import org.onlab.packet.ICMPEcho;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.LeadershipService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.IPCriterion;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.InboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketService;
+import org.onosproject.openstacknetworking.api.InstancePort;
+import org.onosproject.openstacknetworking.api.InstancePortService;
+import org.onosproject.openstacknetworking.api.OpenstackFlowRuleService;
+import org.onosproject.openstacknetworking.api.OpenstackNetworkService;
+import org.onosproject.openstacknode.api.OpenstackNodeService;
 import org.onosproject.openstacktroubleshoot.api.OpenstackTroubleshootService;
+import org.onosproject.openstacktroubleshoot.api.Reachability;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.AtomicCounter;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.function.BooleanSupplier;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
+import static org.onlab.packet.Ethernet.TYPE_IPV4;
+import static org.onlab.packet.ICMP.TYPE_ECHO_REPLY;
+import static org.onlab.packet.ICMP.TYPE_ECHO_REQUEST;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.net.PortNumber.TABLE;
+import static org.onosproject.net.flow.FlowEntry.FlowEntryState.ADDED;
+import static org.onosproject.net.flow.criteria.Criterion.Type.IPV4_DST;
+import static org.onosproject.net.flow.criteria.Criterion.Type.IPV4_SRC;
+import static org.onosproject.openstacknetworking.api.Constants.ACL_TABLE;
+import static org.onosproject.openstacknetworking.api.Constants.DEFAULT_GATEWAY_MAC;
+import static org.onosproject.openstacknetworking.api.Constants.FORWARDING_TABLE;
+import static org.onosproject.openstacknetworking.api.Constants.OPENSTACK_NETWORKING_APP_ID;
+import static org.onosproject.openstacknetworking.api.Constants.PRIORITY_ICMP_PROBE_RULE;
+import static org.onosproject.openstacknetworking.api.Constants.VTAG_TABLE;
+import static org.onosproject.openstacknetworking.api.InstancePort.State.ACTIVE;
+import static org.onosproject.openstacknode.api.OpenstackNode.NodeType.COMPUTE;
+import static org.onosproject.openstacktroubleshoot.util.OpenstackTroubleshootUtil.getSegId;
 
 /**
  * Implementation of openstack troubleshoot app.
  */
+@Component(immediate = true)
+@Service
 public class OpenstackTroubleshootManager implements OpenstackTroubleshootService {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final int VID_TAG_RULE_INSTALL_TIMEOUT_MS = 1000;
+    private static final int ICMP_RULE_INSTALL_TIMEOUT_MS = 1000;
+    private static final int ICMP_REPLY_TIMEOUT_MS = 3000;
+    private static final String SERIALIZER_NAME = "openstack-troubleshoot";
+    private static final byte TTL = 64;
+    private static final short INITIAL_SEQ = 1;
+    private static final short MAX_ICMP_GEN = 3;
+    private static final int PREFIX_LENGTH = 32;
+    private static final int ICMP_PROCESSOR_PRIORITY = 99;
+
+    private static final String ICMP_COUNTER_NAME = "icmp-id-counter";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected PacketService packetService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected FlowRuleService flowRuleService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StorageService storageService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LeadershipService leadershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MastershipService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected OpenstackNodeService osNodeService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected OpenstackNetworkService osNetworkService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected OpenstackFlowRuleService osFlowRuleService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected InstancePortService instancePortService;
+
+    private final ExecutorService eventExecutor = newSingleThreadScheduledExecutor(
+            groupedThreads(this.getClass().getSimpleName(), "event-handler", log));
+    private final InternalPacketProcessor packetProcessor = new InternalPacketProcessor();
+    private ConsistentMap<String, Reachability> icmpReachabilityMap;
+    private AtomicCounter icmpIdCounter;
+
+    private static final KryoNamespace SERIALIZER_DEFAULT_MAP = KryoNamespace.newBuilder()
+            .register(KryoNamespaces.API)
+            .register(Reachability.class)
+            .register(DefaultReachability.class)
+            .build();
+
+    private Set<String> icmpIds = Sets.newConcurrentHashSet();
+
+    private ApplicationId appId;
+    private NodeId localNodeId;
+
+    @Activate
+    protected void activate() {
+
+        appId = coreService.registerApplication(OPENSTACK_NETWORKING_APP_ID);
+        packetService.addProcessor(packetProcessor,
+                            PacketProcessor.director(ICMP_PROCESSOR_PRIORITY));
+
+        localNodeId = clusterService.getLocalNode().id();
+        leadershipService.runForLeadership(appId.name());
+
+        icmpReachabilityMap = storageService.<String, Reachability>consistentMapBuilder()
+                .withSerializer(Serializer.using(SERIALIZER_DEFAULT_MAP))
+                .withName(SERIALIZER_NAME)
+                .withApplicationId(appId)
+                .build();
+
+        icmpIdCounter = storageService.getAtomicCounter(ICMP_COUNTER_NAME);
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+
+        packetService.removeProcessor(packetProcessor);
+        leadershipService.withdraw(appId.name());
+        eventExecutor.shutdown();
+
+        log.info("Stopped");
+    }
+
     @Override
-    public void dummy() {
+    public Map<String, Reachability> probeEastWestBulk() {
+
+        // install flow rules to enforce ICMP_REQUEST to be tagged and direct to ACL table
+        eventExecutor.execute(() -> setAllVidTagRule(true));
+
+        // install flow rules to enforce forwarding ICMP_REPLY to controller
+        eventExecutor.execute(() -> setAllIcmpReplyRule(true));
+
+        icmpReachabilityMap.clear();
+
+        // send ICMP PACKET_OUT to all connect VMs whose instance port state is ACTIVE
+        Set<InstancePort> activePorts = instancePortService.instancePorts().stream()
+                                                .filter(p -> p.state() == ACTIVE)
+                                                .collect(Collectors.toSet());
+
+        timeoutSupplier(activePorts.size(), VID_TAG_RULE_INSTALL_TIMEOUT_MS, this::checkAllVidTagRules);
+        timeoutSupplier(activePorts.size(), ICMP_RULE_INSTALL_TIMEOUT_MS, this::checkAllIcmpReplyRules);
+
+        for (InstancePort srcPort : activePorts) {
+
+            // we only let the master of the switch where the source host
+            // is attached to send out ICMP request packet
+            if (!mastershipService.isLocalMaster(srcPort.deviceId())) {
+                continue;
+            }
+
+            for (InstancePort dstPort : activePorts) {
+                // if the source and destination ports are identical, we do
+                // not probe the reachability
+                if (srcPort.equals(dstPort)) {
+                    continue;
+                }
+
+                // if the two ports are located in different types of networks,
+                // we do not probe the reachability
+                if (!osNetworkService.networkType(srcPort.networkId())
+                        .equals(osNetworkService.networkType(dstPort.networkId()))) {
+                    continue;
+                }
+
+                sendIcmpEchoRequest(srcPort, dstPort);
+            }
+        }
+
+        long count = icmpReachabilityMap.asJavaMap().values().stream()
+                                        .filter(r -> !r.isReachable()).count();
+
+        BooleanSupplier checkReachability = () -> icmpReachabilityMap.asJavaMap()
+                .values().stream().allMatch(Reachability::isReachable);
+
+        timeoutSupplier(count, ICMP_REPLY_TIMEOUT_MS, checkReachability);
+
+        // uninstall ICMP_REQUEST VID tagging rules
+        eventExecutor.execute(() -> setAllVidTagRule(false));
+
+        // uninstall ICMP_REPLY enforcing rules
+        eventExecutor.execute(() -> setAllIcmpReplyRule(false));
+
+        return icmpReachabilityMap.asJavaMap();
+    }
+
+    @Override
+    public Reachability probeEastWest(String srcNetId, IpAddress srcIp,
+                                      String dstNetId, IpAddress dstIp) {
+
+        Reachability.Builder rBuilder = DefaultReachability.builder()
+                                                        .srcIp(srcIp)
+                                                        .dstIp(dstIp);
+
+        if (srcIp.equals(dstIp)) {
+            // self probing should always return true
+            rBuilder.isReachable(true);
+            return rBuilder.build();
+        }  else {
+            InstancePort srcPort = instancePortService.instancePort(srcIp, srcNetId);
+            InstancePort dstPort = instancePortService.instancePort(dstIp, dstNetId);
+
+            if (srcPort.state() == ACTIVE && dstPort.state() == ACTIVE) {
+
+                // install flow rules to enforce ICMP_REQUEST to be tagged and direct to ACL table
+                eventExecutor.execute(() -> setVidTagRule(srcPort, true));
+
+                // install flow rules to enforce forwarding ICMP_REPLY to controller
+                eventExecutor.execute(() -> setIcmpReplyRule(srcPort, true));
+
+                timeoutPredicate(1, VID_TAG_RULE_INSTALL_TIMEOUT_MS,
+                        this::checkVidTagRule, srcPort.ipAddress().toString());
+
+                timeoutPredicate(1, ICMP_RULE_INSTALL_TIMEOUT_MS,
+                        this::checkIcmpReplyRule, srcPort.ipAddress().toString());
+
+                // send out ICMP ECHO request
+                sendIcmpEchoRequest(srcPort, dstPort);
+
+                BooleanSupplier checkReachability = () -> icmpReachabilityMap.asJavaMap()
+                        .values().stream().allMatch(Reachability::isReachable);
+
+                timeoutSupplier(1, ICMP_REPLY_TIMEOUT_MS, checkReachability);
+
+                // uninstall ICMP_REQUEST VID tagging rules
+                eventExecutor.execute(() -> setVidTagRule(srcPort, false));
+
+                // uninstall ICMP_REPLY enforcing rules
+                eventExecutor.execute(() -> setIcmpReplyRule(srcPort, false));
+
+                return icmpReachabilityMap.asJavaMap()
+                                          .get(String.valueOf(icmpIdCounter.get()));
+
+            } else {
+                rBuilder.isReachable(false);
+                return rBuilder.build();
+            }
+        }
+    }
+
+    @Override
+    public Map<String, Reachability> probeNorthSouth() {
+        // TODO: require implementation
+        return null;
+    }
+
+    @Override
+    public Reachability probeNorthSouth(String netId, IpAddress ip) {
+        // TODO: require implementation
+        return null;
+    }
+
+    /**
+     * Checks whether all of ICMP reply rules are added or not.
+     *
+     * @return true if all of ICMP reply rules are added, false otherwise
+     */
+    private boolean checkAllIcmpReplyRules() {
+
+        Set<InstancePort> activePorts = instancePortService.instancePorts().stream()
+                .filter(p -> p.state() == ACTIVE).collect(Collectors.toSet());
+
+        for (InstancePort port : activePorts) {
+            if (!checkIcmpReplyRule(port.ipAddress().toString())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks whether ICMP reply rule is added or not.
+     *
+     * @param dstIp destination IP address
+     * @return true if ICMP reply rule is added, false otherwise
+     */
+    private boolean checkIcmpReplyRule(String dstIp) {
+        for (FlowEntry entry : flowRuleService.getFlowEntriesById(appId)) {
+            TrafficSelector selector = entry.selector();
+
+            IPCriterion dstIpCriterion = (IPCriterion) selector.getCriterion(IPV4_DST);
+
+            if (dstIpCriterion != null &&
+                    dstIp.equals(dstIpCriterion.ip().address().toString()) &&
+                    entry.state() == ADDED) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks whether all of ICMP request VID tagging rules are added or not.
+     *
+     * @return true if the rule is added, false otherwise
+     */
+    private boolean checkAllVidTagRules() {
+        Set<InstancePort> activePorts = instancePortService.instancePorts().stream()
+                .filter(p -> p.state() == ACTIVE).collect(Collectors.toSet());
+
+        for (InstancePort port : activePorts) {
+            if (!checkVidTagRule(port.ipAddress().toString())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks whether ICMP request VID tagging rule is added or not.
+     *
+     * @param srcIp source IP address
+     * @return true if the rule is added, false otherwise
+     */
+    private boolean checkVidTagRule(String srcIp) {
+        for (FlowEntry entry : flowRuleService.getFlowEntriesById(appId)) {
+            TrafficSelector selector = entry.selector();
+
+            IPCriterion srcIpCriterion = (IPCriterion) selector.getCriterion(IPV4_SRC);
+
+            if (srcIpCriterion != null &&
+                    srcIp.equals(srcIpCriterion.ip().address().toString()) &&
+                    entry.state() == ADDED) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Installs/uninstalls all of the flow rules to match ingress fake ICMP requests.
+     *
+     * @param install   installation flag
+     */
+    private void setAllVidTagRule(boolean install) {
+        osNodeService.nodes(COMPUTE).forEach(n ->
+                instancePortService.instancePorts().stream()
+                        .filter(p -> p.deviceId().equals(n.intgBridge()))
+                        .forEach(p -> setVidTagRule(p, install))
+        );
+    }
+
+    /**
+     * Installs/uninstalls a flow rule to match ingress fake ICMP request packets,
+     * and tags VNI/VID, direct the tagged packet to ACL table.
+     *
+     * @param port      instance port
+     * @param install   installation flag
+     */
+    private void setVidTagRule(InstancePort port, boolean install) {
+        TrafficSelector selector = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV4)
+                .matchIPSrc(IpPrefix.valueOf(port.ipAddress(), PREFIX_LENGTH))
+                .build();
+
+        TrafficTreatment.Builder tb = DefaultTrafficTreatment.builder()
+                .setTunnelId(getSegId(osNetworkService, port))
+                .transition(ACL_TABLE);
+
+        osFlowRuleService.setRule(
+                appId,
+                port.deviceId(),
+                selector,
+                tb.build(),
+                PRIORITY_ICMP_PROBE_RULE,
+                VTAG_TABLE,
+                install);
+    }
+
+    /**
+     * Installs/uninstalls all of the flow rules to match ICMP reply packets.
+     *
+     * @param install   installation flag
+     */
+    private void setAllIcmpReplyRule(boolean install) {
+        osNodeService.nodes(COMPUTE).forEach(n ->
+            instancePortService.instancePorts().stream()
+                    .filter(p -> p.deviceId().equals(n.intgBridge()))
+                    .forEach(p -> setIcmpReplyRule(p, install))
+        );
+    }
+
+    /**
+     * Installs/uninstalls a flow rule to match ICMP reply packets, direct all
+     * ICMP reply packets to the controller.
+     *
+     * @param install   installation flag
+     */
+    private void setIcmpReplyRule(InstancePort port, boolean install) {
+        TrafficSelector selector = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV4)
+                .matchIPDst(IpPrefix.valueOf(port.ipAddress(), PREFIX_LENGTH))
+                .matchIPProtocol(IPv4.PROTOCOL_ICMP)
+                .matchIcmpType(ICMP.TYPE_ECHO_REPLY)
+                .build();
+
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .punt()
+                .build();
+
+        osFlowRuleService.setRule(
+                appId,
+                port.deviceId(),
+                selector,
+                treatment,
+                PRIORITY_ICMP_PROBE_RULE,
+                FORWARDING_TABLE,
+                install);
+    }
+
+    /**
+     * Sends out ICMP ECHO REQUEST to destined VM.
+     *
+     * @param srcPort   source instance port
+     * @param dstPort   destination instance port
+     */
+    private void sendIcmpEchoRequest(InstancePort srcPort, InstancePort dstPort) {
+
+        short icmpSeq = INITIAL_SEQ;
+
+        short icmpId = (short) icmpIdCounter.incrementAndGet();
+
+        for (int i = 0; i < MAX_ICMP_GEN; i++) {
+            packetService.emit(buildIcmpOutputPacket(srcPort, dstPort, icmpId, icmpSeq));
+            icmpSeq++;
+        }
+    }
+
+    /**
+     * Builds ICMP Outbound packet.
+     *
+     * @param srcPort   source instance port
+     * @param dstPort   destination instance port
+     * @param icmpId    ICMP identifier
+     * @param icmpSeq   ICMP sequence number
+     */
+    private OutboundPacket buildIcmpOutputPacket(InstancePort srcPort,
+                                                 InstancePort dstPort,
+                                                 short icmpId,
+                                                 short icmpSeq) {
+
+        // TODO: need to encapsulate the frame into VXLAN/VLAN and transit the
+        // packet to TABLE 0 in order to force the packet to go through all pipelines
+        Ethernet ethFrame = constructIcmpPacket(srcPort, dstPort, icmpId, icmpSeq);
+
+        TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+
+        // we send out the packet to ingress table (index is 0) of source OVS
+        // to enforce the Outbound packet to go through the ingress and egress
+        // pipeline
+        tBuilder.setOutput(TABLE);
+
+        Reachability reachability = DefaultReachability.builder()
+                                            .srcIp(srcPort.ipAddress())
+                                            .dstIp(dstPort.ipAddress())
+                                            .isReachable(false)
+                                            .build();
+
+        icmpReachabilityMap.put(String.valueOf(icmpId), reachability);
+        icmpIds.add(String.valueOf(icmpId));
+
+        return new DefaultOutboundPacket(
+                        srcPort.deviceId(),
+                        tBuilder.build(),
+                        ByteBuffer.wrap(ethFrame.serialize()));
+    }
+
+    /**
+     * Constructs an ICMP packet with given source and destination IP/MAC.
+     *
+     * @param srcPort   source instance port
+     * @param dstPort   destination instance port
+     * @param icmpId    ICMP identifier
+     * @param icmpSeq   ICMP sequence number
+     * @return an ethernet frame which contains ICMP payload
+     */
+    private Ethernet constructIcmpPacket(InstancePort srcPort,
+                                         InstancePort dstPort,
+                                         short icmpId, short icmpSeq) {
+        // Ethernet frame
+        Ethernet ethFrame = new Ethernet();
+
+        ethFrame.setEtherType(TYPE_IPV4);
+        ethFrame.setSourceMACAddress(srcPort.macAddress());
+
+        boolean isRemote = !srcPort.deviceId().equals(dstPort.deviceId());
+
+        if (isRemote) {
+            // if the source and destination VMs are located in different OVS,
+            // we will assign fake gateway MAC as the destination MAC
+            ethFrame.setDestinationMACAddress(DEFAULT_GATEWAY_MAC);
+        } else {
+            ethFrame.setDestinationMACAddress(dstPort.macAddress());
+        }
+
+        // IP packet
+        IPv4 iPacket = new IPv4();
+        iPacket.setDestinationAddress(dstPort.ipAddress().toString());
+        iPacket.setSourceAddress(srcPort.ipAddress().toString());
+        iPacket.setTtl(TTL);
+        iPacket.setProtocol(IPv4.PROTOCOL_ICMP);
+
+        // ICMP packet
+        ICMP icmp = new ICMP();
+        icmp.setIcmpType(TYPE_ECHO_REQUEST)
+                .setIcmpCode(TYPE_ECHO_REQUEST)
+                .resetChecksum();
+
+        // ICMP ECHO packet
+        ICMPEcho icmpEcho = new ICMPEcho();
+        icmpEcho.setIdentifier(icmpId)
+                .setSequenceNum(icmpSeq);
+
+        ByteBuffer byteBufferIcmpEcho = ByteBuffer.wrap(icmpEcho.serialize());
+
+        try {
+            icmp.setPayload(ICMPEcho.deserializer().deserialize(byteBufferIcmpEcho.array(),
+                    0, ICMPEcho.ICMP_ECHO_HEADER_LENGTH));
+        } catch (DeserializationException e) {
+            log.warn("Failed to deserialize ICMP ECHO REQUEST packet");
+        }
+
+        ByteBuffer byteBufferIcmp = ByteBuffer.wrap(icmp.serialize());
+
+        try {
+            iPacket.setPayload(ICMP.deserializer().deserialize(byteBufferIcmp.array(),
+                    0,
+                    byteBufferIcmp.array().length));
+        } catch (DeserializationException e) {
+            log.warn("Failed to deserialize ICMP packet");
+        }
+
+        ethFrame.setPayload(iPacket);
+
+        return ethFrame;
+    }
+
+    /**
+     * Handles ICMP ECHO REPLY packets.
+     *
+     * @param ipPacket  IP packet
+     * @param icmp ICMP packet
+     */
+    private void handleIcmpEchoReply(IPv4 ipPacket, ICMP icmp) {
+
+        String icmpKey = icmpId(icmp);
+
+        String srcIp = IPv4.fromIPv4Address(ipPacket.getDestinationAddress());
+        String dstIp = IPv4.fromIPv4Address(ipPacket.getSourceAddress());
+
+        Reachability reachability = DefaultReachability.builder()
+                                                .srcIp(IpAddress.valueOf(srcIp))
+                                                .dstIp(IpAddress.valueOf(dstIp))
+                                                .isReachable(false)
+                                                .build();
+
+        icmpReachabilityMap.computeIfPresent(icmpKey, (key, value) -> {
+            if (value.equals(reachability)) {
+
+                log.debug("src: {}, dst: {} is reachable!", value.dstIp(), value.srcIp());
+
+                return DefaultReachability.builder()
+                        .srcIp(IpAddress.valueOf(srcIp))
+                        .dstIp(IpAddress.valueOf(dstIp))
+                        .isReachable(true)
+                        .build();
+            }
+            return reachability;
+        });
+    }
+
+    /**
+     * Obtains an unique ICMP key.
+     *
+     * @param icmp ICMP packet
+     * @return  ICMP key
+     */
+    private String icmpId(ICMP icmp) {
+        ICMPEcho echo = (ICMPEcho) icmp.getPayload();
+        checkNotNull(echo);
+
+        short icmpId = echo.getIdentifier();
+
+        return String.valueOf(icmpId);
+    }
+
+    /**
+     * Holds the current thread unit the timeout expires, during the hold the
+     * thread periodically execute the given method.
+     *
+     * @param count         count of unit
+     * @param unit          unit
+     * @param predicate     predicate
+     * @param predicateArg  predicate argument
+     */
+    private void timeoutPredicate(long count, int unit,
+                                  Predicate<String> predicate, String predicateArg) {
+        long timeoutExpiredMs = System.currentTimeMillis() + unit * count;
+
+        while (true) {
+
+            long waitMs = timeoutExpiredMs - System.currentTimeMillis();
+
+            if (predicate.test(predicateArg)) {
+                break;
+            }
+
+            if (waitMs <= 0) {
+                break;
+            }
+        }
+    }
+
+    /**
+     * Holds the current thread unit the timeout expires, during the hold the
+     * thread periodically execute the given method.
+     *
+     * @param count     count of unit
+     * @param unit      unit
+     * @param supplier  boolean supplier
+     */
+    private void timeoutSupplier(long count, int unit, BooleanSupplier supplier) {
+        long timeoutExpiredMs = System.currentTimeMillis() + unit * count;
+
+        while (true) {
+
+            long waitMs = timeoutExpiredMs - System.currentTimeMillis();
+
+            if (supplier.getAsBoolean()) {
+                break;
+            }
+
+            if (waitMs <= 0) {
+                break;
+            }
+        }
+    }
+
+    private class InternalPacketProcessor implements PacketProcessor {
+
+        @Override
+        public void process(PacketContext context) {
+            if (context.isHandled()) {
+                return;
+            }
+
+            InboundPacket pkt = context.inPacket();
+            Ethernet ethernet = pkt.parsed();
+            if (ethernet == null || ethernet.getEtherType() == Ethernet.TYPE_ARP) {
+                return;
+            }
+
+            IPv4 iPacket = (IPv4) ethernet.getPayload();
+            if (iPacket.getProtocol() == IPv4.PROTOCOL_ICMP) {
+                eventExecutor.execute(() -> processIcmpPacket(context, ethernet));
+            }
+        }
+
+        /**
+         * Processes the received ICMP packet.
+         *
+         * @param context   packet context
+         * @param ethernet  ethernet
+         */
+        private void processIcmpPacket(PacketContext context, Ethernet ethernet) {
+            IPv4 ipPacket = (IPv4) ethernet.getPayload();
+            ICMP icmp = (ICMP) ipPacket.getPayload();
+            log.trace("Processing ICMP packet source MAC:{}, source IP:{}," +
+                            "dest MAC:{}, dest IP:{}",
+                    ethernet.getSourceMAC(),
+                    IpAddress.valueOf(ipPacket.getSourceAddress()),
+                    ethernet.getDestinationMAC(),
+                    IpAddress.valueOf(ipPacket.getDestinationAddress()));
+
+            String icmpId = icmpId(icmp);
+
+            // if the ICMP ID is not contained in ICMP ID set, we do not handle it
+            if (!icmpIds.contains(icmpId)) {
+                return;
+            }
+
+            switch (icmp.getIcmpType()) {
+                case TYPE_ECHO_REPLY:
+                    handleIcmpEchoReply(ipPacket, icmp);
+                    context.block();
+                    icmpIds.remove(icmpId);
+                    break;
+                default:
+                    break;
+            }
+        }
     }
 }
diff --git a/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/util/OpenstackTroubleshootUtil.java b/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/util/OpenstackTroubleshootUtil.java
new file mode 100644
index 0000000..c78b160
--- /dev/null
+++ b/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/util/OpenstackTroubleshootUtil.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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.onosproject.openstacktroubleshoot.util;
+
+import org.onlab.packet.Ip4Address;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.behaviour.ExtensionTreatmentResolver;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.instructions.ExtensionPropertyException;
+import org.onosproject.net.flow.instructions.ExtensionTreatment;
+import org.onosproject.openstacknetworking.api.InstancePort;
+import org.onosproject.openstacknetworking.api.OpenstackNetworkService;
+import org.slf4j.Logger;
+
+import static org.onosproject.net.flow.instructions.ExtensionTreatmentType.ExtensionTreatmentTypes.NICIRA_SET_TUNNEL_DST;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provides common methods to help populating flow rules for troubleshoot app.
+ */
+public final class OpenstackTroubleshootUtil {
+
+    private static final Logger log = getLogger(OpenstackTroubleshootUtil.class);
+
+    private static final String TUNNEL_DST = "tunnelDst";
+
+    private OpenstackTroubleshootUtil() {
+    }
+
+    /**
+     * Returns tunnel destination extension treatment object.
+     *
+     * @param deviceService driver service
+     * @param deviceId device id to apply this treatment
+     * @param remoteIp tunnel destination ip address
+     * @return extension treatment
+     */
+    public static ExtensionTreatment buildExtension(DeviceService deviceService,
+                                                    DeviceId deviceId,
+                                                    Ip4Address remoteIp) {
+        Device device = deviceService.getDevice(deviceId);
+        if (device != null && !device.is(ExtensionTreatmentResolver.class)) {
+            log.error("The extension treatment is not supported");
+            return null;
+        }
+
+        if (device == null) {
+            return null;
+        }
+
+        ExtensionTreatmentResolver resolver = device.as(ExtensionTreatmentResolver.class);
+        ExtensionTreatment treatment = resolver.getExtensionInstruction(NICIRA_SET_TUNNEL_DST.type());
+        try {
+            treatment.setPropertyValue(TUNNEL_DST, remoteIp);
+            return treatment;
+        } catch (ExtensionPropertyException e) {
+            log.warn("Failed to get tunnelDst extension treatment for {}", deviceId);
+            return null;
+        }
+    }
+
+    /**
+     * Returns segment ID of the given instance port where a VM is attached.
+     *
+     * @param service   openstack network service
+     * @param port      instance port
+     * @return segment ID
+     */
+    public static long getSegId(OpenstackNetworkService service, InstancePort port) {
+        return Long.parseLong(service.segmentId(port.networkId()));
+    }
+}
diff --git a/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/util/package-info.java b/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/util/package-info.java
new file mode 100644
index 0000000..f1587c8
--- /dev/null
+++ b/apps/openstacktroubleshoot/app/src/main/java/org/onosproject/openstacktroubleshoot/util/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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.
+ */
+
+/**
+ * Openstack troubleshoot utility package.
+ */
+package org.onosproject.openstacktroubleshoot.util;
\ No newline at end of file
diff --git a/apps/openstacktroubleshoot/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/openstacktroubleshoot/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..67a1364
--- /dev/null
+++ b/apps/openstacktroubleshoot/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,22 @@
+<!--
+~ Copyright 2017-present Open Networking Foundation
+~
+~ Licensed 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.
+-->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+    <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
+        <command>
+            <action class="org.onosproject.openstacktroubleshoot.cli.OpenstackEastWestProbeCommand"/>
+        </command>
+    </command-bundle>
+</blueprint>
diff --git a/apps/openstacktroubleshoot/app/src/test/java/org/onosproject/openstacktroubleshoot/impl/DefaultReachabilityTest.java b/apps/openstacktroubleshoot/app/src/test/java/org/onosproject/openstacktroubleshoot/impl/DefaultReachabilityTest.java
new file mode 100644
index 0000000..3926da7
--- /dev/null
+++ b/apps/openstacktroubleshoot/app/src/test/java/org/onosproject/openstacktroubleshoot/impl/DefaultReachabilityTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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.onosproject.openstacktroubleshoot.impl;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onosproject.openstacktroubleshoot.api.Reachability;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Unit tests for DefaultReachability class.
+ */
+public final class DefaultReachabilityTest {
+
+    private static final IpAddress IP_ADDRESS_1_1 = IpAddress.valueOf("1.2.3.4");
+    private static final IpAddress IP_ADDRESS_1_2 = IpAddress.valueOf("2.3.4.5");
+    private static final IpAddress IP_ADDRESS_2_1 = IpAddress.valueOf("5.6.7.8");
+    private static final IpAddress IP_ADDRESS_2_2 = IpAddress.valueOf("6.7.8.9");
+
+    private Reachability reachability1;
+    private Reachability sameAsReachability1;
+    private Reachability reachability2;
+
+    /**
+     * Initial setup for this unit test.
+     */
+    @Before
+    public void setUp() {
+        reachability1 = DefaultReachability.builder()
+                                .srcIp(IP_ADDRESS_1_1)
+                                .dstIp(IP_ADDRESS_1_2)
+                                .isReachable(true)
+                                .build();
+
+        sameAsReachability1 = DefaultReachability.builder()
+                                .srcIp(IP_ADDRESS_1_1)
+                                .dstIp(IP_ADDRESS_1_2)
+                                .isReachable(true)
+                                .build();
+
+        reachability2 = DefaultReachability.builder()
+                                .srcIp(IP_ADDRESS_2_1)
+                                .dstIp(IP_ADDRESS_2_2)
+                                .isReachable(false)
+                                .build();
+    }
+
+    /**
+     * Checks the class is immutable.
+     */
+    @Test
+    public void testImmutability() {
+        assertThatClassIsImmutable(DefaultReachability.class);
+    }
+
+    /**
+     * Tests object equality.
+     */
+    @Test
+    public void testEquality() {
+        new EqualsTester().addEqualityGroup(reachability1, sameAsReachability1)
+                .addEqualityGroup(reachability2)
+                .testEquals();
+    }
+
+    /**
+     * Test object construction.
+     */
+    @Test
+    public void testConstruction() {
+        Reachability reachability = reachability1;
+
+        assertEquals(reachability.srcIp(), IP_ADDRESS_1_1);
+        assertEquals(reachability.dstIp(), IP_ADDRESS_1_2);
+        assertTrue(reachability.isReachable());
+    }
+}