[ONOS-6632] Add REST APIs for configuring compute and gateway nodes.

 - network configuration is not working any more.

Change-Id: I24e4d8b7b8cc33d839328887b9c6190d714baeba
diff --git a/apps/openstacknode/app/BUCK b/apps/openstacknode/app/BUCK
new file mode 100644
index 0000000..821cb71
--- /dev/null
+++ b/apps/openstacknode/app/BUCK
@@ -0,0 +1,28 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:org.apache.karaf.shell.console',
+    '//cli:onos-cli',
+    '//lib:javax.ws.rs-api',
+    '//utils/rest:onlab-rest',
+    '//protocols/ovsdb/api:onos-protocols-ovsdb-api',
+    '//protocols/ovsdb/rfc:onos-protocols-ovsdb-rfc',
+    '//core/store/serializers:onos-core-serializers',
+    '//apps/openstacknode/api:onos-apps-openstacknode-api',
+]
+
+TEST_DEPS = [
+    '//lib:TEST_ADAPTERS',
+    '//core/api:onos-api-tests',
+    '//core/common:onos-core-common-tests',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+    test_deps = TEST_DEPS,
+    web_context = '/onos/openstacknode',
+    api_title = 'OpenStack Node App API',
+    api_version = '1.0',
+    api_description = 'REST API for OpenStack Node App',
+    api_package = 'org.onosproject.openstacknode.web',
+)
+
diff --git a/apps/openstacknode/app/pom.xml b/apps/openstacknode/app/pom.xml
new file mode 100644
index 0000000..33bbfee
--- /dev/null
+++ b/apps/openstacknode/app/pom.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onosproject</groupId>
+        <artifactId>onos-apps-openstacknode</artifactId>
+        <version>1.11.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>onos-apps-openstacknode-app</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>SONA Openstack Node Bootstrap Application</description>
+
+    <properties>
+        <onos.app.name>org.onosproject.openstacknode</onos.app.name>
+        <onos.app.title>OpenStack Node Bootstrap App</onos.app.title>
+        <onos.app.category>Utility</onos.app.category>
+        <onos.app.url>http://onosproject.org</onos.app.url>
+        <onos.app.readme>SONA Openstack Node Bootstrap Application</onos.app.readme>
+        <onos.app.requires>
+            org.onosproject.ovsdb-base,
+            org.onosproject.drivers.ovsdb
+        </onos.app.requires>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-rest</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-apps-openstacknode-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-rest</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.ws.rs</groupId>
+            <artifactId>javax.ws.rs-api</artifactId>
+            <version>2.0.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-core-serializers</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-cli</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-protocols-ovsdb-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf.shell</groupId>
+            <artifactId>org.apache.karaf.shell.console</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+            <classifier>tests</classifier>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava-testlib</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.easymock</groupId>
+            <artifactId>easymock</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+</project>
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/OpenstackNodeCheckCommand.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/OpenstackNodeCheckCommand.java
new file mode 100644
index 0000000..6a27c0e
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/OpenstackNodeCheckCommand.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.openstacknode.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.Device;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupService;
+import org.onosproject.openstacknode.api.OpenstackNode;
+import org.onosproject.openstacknode.api.OpenstackNodeService;
+
+import static org.onosproject.net.AnnotationKeys.PORT_NAME;
+import static org.onosproject.openstacknode.api.Constants.*;
+import static org.onosproject.openstacknode.api.OpenstackNode.NetworkMode.VLAN;
+import static org.onosproject.openstacknode.api.OpenstackNode.NetworkMode.VXLAN;
+import static org.onosproject.openstacknode.api.OpenstackNode.NodeType.GATEWAY;
+
+/**
+ * Checks detailed node init state.
+ */
+@Command(scope = "onos", name = "openstack-node-check",
+        description = "Shows detailed node init state")
+public class OpenstackNodeCheckCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "hostname", description = "Hostname",
+            required = true, multiValued = false)
+    private String hostname = null;
+
+    private static final String MSG_OK = "OK";
+    private static final String MSG_NO = "NO";
+    private static final String BUCKET_FORMAT =
+            "   bucket=%s, bytes=%s, packets=%s, actions=%s";
+
+    @Override
+    protected void execute() {
+        OpenstackNodeService osNodeService = AbstractShellCommand.get(OpenstackNodeService.class);
+        DeviceService deviceService = AbstractShellCommand.get(DeviceService.class);
+        GroupService groupService = AbstractShellCommand.get(GroupService.class);
+
+        OpenstackNode osNode = osNodeService.node(hostname);
+        if (osNode == null) {
+            print("Cannot find %s from registered nodes", hostname);
+            return;
+        }
+
+        print("[Integration Bridge Status]");
+        Device device = deviceService.getDevice(osNode.intgBridge());
+        if (device != null) {
+            print("%s %s=%s available=%s %s",
+                    deviceService.isAvailable(device.id()) ? MSG_OK : MSG_NO,
+                    INTEGRATION_BRIDGE,
+                    device.id(),
+                    deviceService.isAvailable(device.id()),
+                    device.annotations());
+            if (osNode.dataIp() != null) {
+                printPortState(deviceService, osNode.intgBridge(), DEFAULT_TUNNEL);
+            }
+            if (osNode.vlanIntf() != null) {
+                printPortState(deviceService, osNode.intgBridge(), osNode.vlanIntf());
+            }
+            printGatewayGroupState(osNodeService, groupService, osNode);
+        } else {
+            print("%s %s=%s is not available",
+                    MSG_NO,
+                    INTEGRATION_BRIDGE,
+                    osNode.intgBridge());
+        }
+    }
+
+    private void printPortState(DeviceService deviceService, DeviceId deviceId, String portName) {
+        Port port = deviceService.getPorts(deviceId).stream()
+                .filter(p -> p.annotations().value(PORT_NAME).equals(portName) &&
+                        p.isEnabled())
+                .findAny().orElse(null);
+
+        if (port != null) {
+            print("%s %s portNum=%s enabled=%s %s",
+                    port.isEnabled() ? MSG_OK : MSG_NO,
+                    portName,
+                    port.number(),
+                    port.isEnabled() ? Boolean.TRUE : Boolean.FALSE,
+                    port.annotations());
+        } else {
+            print("%s %s does not exist", MSG_NO, portName);
+        }
+    }
+
+    private void printGatewayGroupState(OpenstackNodeService osNodeService,
+                                        GroupService groupService, OpenstackNode osNode) {
+        if (osNode.type() == GATEWAY) {
+            return;
+        }
+        if (osNodeService.completeNodes(GATEWAY).isEmpty()) {
+            print("N/A No complete state gateway nodes exist");
+            return;
+        }
+        if (osNode.dataIp() != null) {
+            Group osGroup = groupService.getGroup(osNode.intgBridge(),
+                    osNode.gatewayGroupKey(VXLAN));
+            if (osGroup == null || osGroup.state() != Group.GroupState.ADDED) {
+                print("%s VXLAN gateway group does not exist", MSG_NO);
+            } else {
+                print("%s VXLAN group 0x%s added", MSG_OK, Integer.toHexString(osGroup.id().id()));
+                int i = 0;
+                for (GroupBucket bucket : osGroup.buckets().buckets()) {
+                    print(BUCKET_FORMAT, ++i, bucket.bytes(), bucket.packets(),
+                            bucket.treatment().allInstructions());
+                }
+            }
+        }
+        if (osNode.vlanIntf() != null) {
+            Group osGroup = groupService.getGroup(osNode.intgBridge(),
+                    osNode.gatewayGroupKey(VLAN));
+            if (osGroup == null || osGroup.state() != Group.GroupState.ADDED) {
+                print("\n%s VLAN gateway group does not exist", MSG_NO);
+            } else {
+                print("\n%s VLAN group 0x%s added", MSG_OK, Integer.toHexString(osGroup.id().id()));
+                int i = 0;
+                for (GroupBucket bucket : osGroup.buckets().buckets()) {
+                    print(BUCKET_FORMAT, ++i, bucket.bytes(), bucket.packets(),
+                            bucket.treatment().allInstructions());
+                }
+            }
+        }
+    }
+}
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/OpenstackNodeInitCommand.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/OpenstackNodeInitCommand.java
new file mode 100644
index 0000000..34f4a99
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/OpenstackNodeInitCommand.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.openstacknode.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.Option;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.openstacknode.api.NodeState;
+import org.onosproject.openstacknode.api.OpenstackNode;
+import org.onosproject.openstacknode.api.OpenstackNodeAdminService;
+import org.onosproject.openstacknode.api.OpenstackNodeService;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Initializes nodes for OpenStack node service.
+ */
+@Command(scope = "onos", name = "openstack-node-init",
+        description = "Initializes nodes for OpenStack node service")
+public class OpenstackNodeInitCommand extends AbstractShellCommand {
+
+    @Option(name = "-a", aliases = "--all", description = "Apply this command to all nodes",
+            required = false, multiValued = false)
+    private boolean isAll = false;
+
+    @Option(name = "-i", aliases = "--incomplete",
+            description = "Apply this command to incomplete nodes",
+            required = false, multiValued = false)
+    private boolean isIncomplete = false;
+
+    @Argument(index = 0, name = "hostnames", description = "Hostname(s) to apply this command",
+            required = false, multiValued = true)
+    private String[] hostnames = null;
+
+    @Override
+    protected void execute() {
+        OpenstackNodeService osNodeService =
+                AbstractShellCommand.get(OpenstackNodeService.class);
+        OpenstackNodeAdminService osNodeAdminService =
+                AbstractShellCommand.get(OpenstackNodeAdminService.class);
+
+        if ((!isAll && !isIncomplete && hostnames == null) ||
+                (isAll && isIncomplete) ||
+                (isIncomplete && hostnames != null) ||
+                (hostnames != null && isAll)) {
+            print("Please specify one of hostname, --all, and --incomplete options.");
+            return;
+        }
+
+        if (isAll) {
+            List<String> osNodes = osNodeService.nodes().stream()
+                    .map(OpenstackNode::hostname)
+                    .collect(Collectors.toList());
+            hostnames = osNodes.toArray(new String[osNodes.size()]);
+        } else if (isIncomplete) {
+            List<String> osNodes = osNodeService.nodes().stream()
+                    .filter(osNode -> osNode.state() != NodeState.COMPLETE)
+                    .map(OpenstackNode::hostname)
+                    .collect(Collectors.toList());
+            hostnames = osNodes.toArray(new String[osNodes.size()]);
+        }
+
+        for (String hostname : hostnames) {
+            OpenstackNode osNode = osNodeService.node(hostname);
+            if (osNode == null) {
+                print("Unable to find %s", hostname);
+                continue;
+            }
+            print("Initializing %s", hostname);
+            OpenstackNode updated = osNode.updateState(NodeState.INIT);
+            osNodeAdminService.updateNode(updated);
+        }
+        print("Done.");
+    }
+}
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/OpenstackNodeListCommand.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/OpenstackNodeListCommand.java
new file mode 100644
index 0000000..ceed2a7
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/OpenstackNodeListCommand.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.openstacknode.cli;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.google.common.collect.Lists;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.openstacknode.api.OpenstackNode;
+import org.onosproject.openstacknode.api.OpenstackNodeService;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Lists all nodes registered to the service.
+ */
+@Command(scope = "onos", name = "openstack-nodes",
+        description = "Lists all nodes registered in OpenStack node service")
+public class OpenstackNodeListCommand extends AbstractShellCommand {
+
+    private static final String FORMAT = "%-20s%-15s%-24s%-24s%-20s%-20s%-15s%s";
+
+    @Override
+    protected void execute() {
+        OpenstackNodeService osNodeService = AbstractShellCommand.get(OpenstackNodeService.class);
+        List<OpenstackNode> osNodes = Lists.newArrayList(osNodeService.nodes());
+        osNodes.sort(Comparator.comparing(OpenstackNode::hostname));
+
+        if (outputJson()) {
+            print("%s", json(osNodes));
+        } else {
+            print(FORMAT, "Hostname", "Type", "Integration Bridge", "Router Bridge",
+                    "Management IP", "Data IP", "VLAN Intf", "State");
+            for (OpenstackNode osNode : osNodes) {
+                print(FORMAT,
+                        osNode.hostname(),
+                        osNode.type(),
+                        osNode.intgBridge(),
+                        osNode.routerBridge() != null ? osNode.routerBridge() : "",
+                        osNode.managementIp(),
+                        osNode.dataIp() != null ? osNode.dataIp() : "",
+                        osNode.vlanIntf() != null ? osNode.vlanIntf() : "",
+                        osNode.state());
+            }
+            print("Total %s nodes", osNodeService.nodes().size());
+        }
+    }
+
+    private JsonNode json(List<OpenstackNode> osNodes) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        for (OpenstackNode osNode : osNodes) {
+            result.add(mapper.createObjectNode()
+                    .put("hostname", osNode.hostname())
+                    .put("type", osNode.type().name())
+                    .put("integrationBridge", osNode.intgBridge().toString())
+                    .put("routerBridge", osNode.routerBridge().toString())
+                    .put("managementIp", osNode.managementIp().toString())
+                    .put("dataIp", osNode.dataIp().toString())
+                    .put("vlanIntf", osNode.vlanIntf())
+                    .put("tunnelPortNum", osNode.tunnelPortNum().toString())
+                    .put("vlanPortNum", osNode.vlanPortNum().toString())
+                    .put("state", osNode.state().name()));
+        }
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/package-info.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/package-info.java
new file mode 100644
index 0000000..6a6e636
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/**
+ * Console commands to manage OpenStack nodes.
+ */
+package org.onosproject.openstacknode.cli;
\ No newline at end of file
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/DefaultOpenstackNode.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/DefaultOpenstackNode.java
new file mode 100644
index 0000000..7313667
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/DefaultOpenstackNode.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.openstacknode.impl;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.group.DefaultGroupKey;
+import org.onosproject.net.group.GroupKey;
+import org.onosproject.openstacknode.api.NodeState;
+import org.onosproject.openstacknode.api.OpenstackNode;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.onosproject.net.AnnotationKeys.PORT_MAC;
+import static org.onosproject.net.AnnotationKeys.PORT_NAME;
+import static org.onosproject.openstacknode.api.Constants.DEFAULT_TUNNEL;
+import static org.onosproject.openstacknode.api.Constants.PATCH_INTG_BRIDGE;
+
+/**
+ * Representation of a openstack node.
+ */
+public class DefaultOpenstackNode implements OpenstackNode {
+
+    private final String hostname;
+    private final NodeType type;
+    private final DeviceId intgBridge;
+    private final DeviceId routerBridge;
+    private final IpAddress managementIp;
+    private final IpAddress dataIp;
+    private final String vlanIntf;
+    private final NodeState state;
+
+    protected DefaultOpenstackNode(String hostname,
+                                 NodeType type,
+                                 DeviceId intgBridge,
+                                 DeviceId routerBridge,
+                                 IpAddress managementIp,
+                                 IpAddress dataIp,
+                                 String vlanIntf,
+                                 NodeState state) {
+        this.hostname = hostname;
+        this.type = type;
+        this.intgBridge = intgBridge;
+        this.routerBridge = routerBridge;
+        this.managementIp = managementIp;
+        this.dataIp = dataIp;
+        this.vlanIntf = vlanIntf;
+        this.state = state;
+    }
+
+    @Override
+    public String hostname() {
+        return hostname;
+    }
+
+    @Override
+    public NodeType type() {
+        return type;
+    }
+
+    @Override
+    public DeviceId ovsdb() {
+        return DeviceId.deviceId("ovsdb:" + managementIp().toString());
+    }
+
+    @Override
+    public DeviceId intgBridge() {
+        return intgBridge;
+    }
+
+    @Override
+    public DeviceId routerBridge() {
+        return routerBridge;
+    }
+
+    @Override
+    public IpAddress managementIp() {
+        return managementIp;
+    }
+
+    @Override
+    public IpAddress dataIp() {
+        return dataIp;
+    }
+
+    @Override
+    public String vlanIntf() {
+        return vlanIntf;
+    }
+
+    @Override
+    public NodeState state() {
+        return state;
+    }
+
+    @Override
+    public GroupKey gatewayGroupKey(NetworkMode mode) {
+        return new DefaultGroupKey(intgBridge.toString().concat(mode.name()).getBytes());
+    }
+
+    @Override
+    public PortNumber tunnelPortNum() {
+        if (dataIp == null) {
+            return null;
+        }
+        DeviceService deviceService = DefaultServiceDirectory.getService(DeviceService.class);
+        Port port = deviceService.getPorts(intgBridge).stream()
+                .filter(p -> p.isEnabled() &&
+                        Objects.equals(p.annotations().value(PORT_NAME), DEFAULT_TUNNEL))
+                .findAny().orElse(null);
+        return port != null ? port.number() : null;
+    }
+
+    @Override
+    public PortNumber vlanPortNum() {
+        if (vlanIntf == null) {
+            return null;
+        }
+        DeviceService deviceService = DefaultServiceDirectory.getService(DeviceService.class);
+        Port port = deviceService.getPorts(intgBridge).stream()
+                .filter(p -> p.isEnabled() &&
+                        Objects.equals(p.annotations().value(PORT_NAME), vlanIntf))
+                .findAny().orElse(null);
+        return port != null ? port.number() : null;
+    }
+
+    @Override
+    public PortNumber patchPortNum() {
+        if (type == NodeType.COMPUTE) {
+            return null;
+        }
+        DeviceService deviceService = DefaultServiceDirectory.getService(DeviceService.class);
+        Port port = deviceService.getPorts(intgBridge).stream()
+                .filter(p -> p.isEnabled() &&
+                        Objects.equals(p.annotations().value(PORT_NAME), PATCH_INTG_BRIDGE))
+                .findAny().orElse(null);
+        return port != null ? port.number() : null;
+    }
+
+    @Override
+    public MacAddress vlanPortMac() {
+        if (vlanIntf == null) {
+            return null;
+        }
+        DeviceService deviceService = DefaultServiceDirectory.getService(DeviceService.class);
+        Port port = deviceService.getPorts(intgBridge).stream()
+                .filter(p -> p.annotations().value(PORT_NAME).equals(vlanIntf))
+                .findAny().orElse(null);
+        return port != null ? MacAddress.valueOf(port.annotations().value(PORT_MAC)) : null;
+    }
+
+    @Override
+    public GroupId gatewayGroupId(NetworkMode mode) {
+        return new GroupId(intgBridge.toString().concat(mode.name()).hashCode());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof DefaultOpenstackNode) {
+            DefaultOpenstackNode that = (DefaultOpenstackNode) obj;
+            if (Objects.equals(hostname, that.hostname) &&
+                    Objects.equals(type, that.type) &&
+                    Objects.equals(intgBridge, that.intgBridge) &&
+                    Objects.equals(routerBridge, that.routerBridge) &&
+                    Objects.equals(managementIp, that.managementIp) &&
+                    Objects.equals(dataIp, that.dataIp) &&
+                    Objects.equals(vlanIntf, that.vlanIntf)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(hostname,
+                type,
+                intgBridge,
+                routerBridge,
+                managementIp,
+                dataIp,
+                vlanIntf);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("hostname", hostname)
+                .add("type", type)
+                .add("integrationBridge", intgBridge)
+                .add("routerBridge", routerBridge)
+                .add("managementIp", managementIp)
+                .add("dataIp", dataIp)
+                .add("vlanIntf", vlanIntf)
+                .add("state", state)
+                .toString();
+    }
+
+    @Override
+    public OpenstackNode updateState(NodeState newState) {
+        return new Builder()
+                .type(type)
+                .hostname(hostname)
+                .intgBridge(intgBridge)
+                .routerBridge(routerBridge)
+                .managementIp(managementIp)
+                .dataIp(dataIp)
+                .vlanIntf(vlanIntf)
+                .state(newState)
+                .build();
+    }
+
+    /**
+     * Returns new builder instance.
+     *
+     * @return openstack node builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns new builder instance with the given node as a default value.
+     *
+     * @param osNode openstack node
+     * @return openstack node builder
+     */
+    public static Builder from(OpenstackNode osNode) {
+        return new Builder()
+                .hostname(osNode.hostname())
+                .type(osNode.type())
+                .intgBridge(osNode.intgBridge())
+                .routerBridge(osNode.routerBridge())
+                .managementIp(osNode.managementIp())
+                .dataIp(osNode.dataIp())
+                .vlanIntf(osNode.vlanIntf())
+                .state(osNode.state());
+    }
+
+    public static final class Builder implements OpenstackNode.Builder {
+
+        private String hostname;
+        private NodeType type;
+        private DeviceId intgBridge;
+        private DeviceId routerBridge;
+        private IpAddress managementIp;
+        private IpAddress dataIp;
+        private String vlanIntf;
+        private NodeState state;
+
+        private Builder() {
+        }
+
+        @Override
+        public DefaultOpenstackNode build() {
+            checkArgument(hostname != null, "Node hostname cannot be null");
+            checkArgument(type != null, "Node type cannot be null");
+            checkArgument(intgBridge != null, "Node integration bridge cannot be null");
+            checkArgument(managementIp != null, "Node management IP cannot be null");
+            checkArgument(state != null, "Node state cannot be null");
+
+            if (type == NodeType.GATEWAY && routerBridge == null) {
+                throw new IllegalArgumentException("Router bridge is required for gateway node");
+            }
+            if (dataIp == null && Strings.isNullOrEmpty(vlanIntf)) {
+                throw new IllegalArgumentException("Either data IP or VLAN interface is required");
+            }
+
+            return new DefaultOpenstackNode(hostname,
+                    type,
+                    intgBridge,
+                    routerBridge,
+                    managementIp,
+                    dataIp,
+                    vlanIntf,
+                    state);
+        }
+
+        @Override
+        public Builder hostname(String hostname) {
+            if (!Strings.isNullOrEmpty(hostname)) {
+                this.hostname = hostname;
+            }
+            return this;
+        }
+
+        @Override
+        public Builder type(NodeType type) {
+            this.type = type;
+            return this;
+        }
+
+        @Override
+        public Builder intgBridge(DeviceId intgBridge) {
+            this.intgBridge = intgBridge;
+            return this;
+        }
+
+        @Override
+        public Builder routerBridge(DeviceId routerBridge) {
+            this.routerBridge = routerBridge;
+            return this;
+        }
+
+        @Override
+        public Builder managementIp(IpAddress managementIp) {
+            this.managementIp = managementIp;
+            return this;
+        }
+
+        @Override
+        public Builder dataIp(IpAddress dataIp) {
+            this.dataIp = dataIp;
+            return this;
+        }
+
+        @Override
+        public Builder vlanIntf(String vlanIntf) {
+            this.vlanIntf = vlanIntf;
+            return this;
+        }
+
+        @Override
+        public Builder state(NodeState state) {
+            this.state = state;
+            return this;
+        }
+    }
+}
+
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/DefaultOpenstackNodeHandler.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/DefaultOpenstackNodeHandler.java
new file mode 100644
index 0000000..d3597ee
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/DefaultOpenstackNodeHandler.java
@@ -0,0 +1,804 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.openstacknode.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+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.Modified;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.packet.IpAddress;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.LeadershipService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.behaviour.BridgeConfig;
+import org.onosproject.net.behaviour.BridgeDescription;
+import org.onosproject.net.behaviour.BridgeName;
+import org.onosproject.net.behaviour.ControllerInfo;
+import org.onosproject.net.behaviour.DefaultBridgeDescription;
+import org.onosproject.net.behaviour.DefaultPatchDescription;
+import org.onosproject.net.behaviour.DefaultTunnelDescription;
+import org.onosproject.net.behaviour.ExtensionTreatmentResolver;
+import org.onosproject.net.behaviour.InterfaceConfig;
+import org.onosproject.net.behaviour.PatchDescription;
+import org.onosproject.net.behaviour.TunnelDescription;
+import org.onosproject.net.behaviour.TunnelEndPoints;
+import org.onosproject.net.behaviour.TunnelKeys;
+import org.onosproject.net.device.DeviceAdminService;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.instructions.ExtensionPropertyException;
+import org.onosproject.net.flow.instructions.ExtensionTreatment;
+import org.onosproject.net.group.DefaultGroupDescription;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.group.GroupEvent;
+import org.onosproject.net.group.GroupListener;
+import org.onosproject.net.group.GroupService;
+import org.onosproject.openstacknode.api.NodeState;
+import org.onosproject.openstacknode.api.OpenstackNode;
+import org.onosproject.openstacknode.api.OpenstackNode.NetworkMode;
+import org.onosproject.openstacknode.api.OpenstackNodeAdminService;
+import org.onosproject.openstacknode.api.OpenstackNodeEvent;
+import org.onosproject.openstacknode.api.OpenstackNodeHandler;
+import org.onosproject.openstacknode.api.OpenstackNodeListener;
+import org.onosproject.openstacknode.api.OpenstackNodeService;
+import org.onosproject.ovsdb.controller.OvsdbClientService;
+import org.onosproject.ovsdb.controller.OvsdbController;
+import org.onosproject.ovsdb.controller.OvsdbNodeId;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+import static org.onlab.packet.TpPort.tpPort;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.net.AnnotationKeys.PORT_NAME;
+import static org.onosproject.net.flow.instructions.ExtensionTreatmentType.ExtensionTreatmentTypes.NICIRA_SET_TUNNEL_DST;
+import static org.onosproject.net.group.DefaultGroupBucket.createSelectGroupBucket;
+import static org.onosproject.openstacknode.api.Constants.*;
+import static org.onosproject.openstacknode.api.Constants.PATCH_INTG_BRIDGE;
+import static org.onosproject.openstacknode.api.NodeState.*;
+import static org.onosproject.openstacknode.api.OpenstackNode.NetworkMode.VLAN;
+import static org.onosproject.openstacknode.api.OpenstackNode.NetworkMode.VXLAN;
+import static org.onosproject.openstacknode.api.OpenstackNode.NodeType.COMPUTE;
+import static org.onosproject.openstacknode.api.OpenstackNode.NodeType.GATEWAY;
+import static org.onosproject.openstacknode.api.OpenstackNodeService.APP_ID;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Service bootstraps openstack node based on its type.
+ */
+@Component(immediate = true)
+public class DefaultOpenstackNodeHandler implements OpenstackNodeHandler {
+
+    protected final Logger log = getLogger(getClass());
+
+    private static final String OVSDB_PORT = "ovsdbPortNum";
+    private static final int DEFAULT_OVSDB_PORT = 6640;
+    private static final String DEFAULT_OF_PROTO = "tcp";
+    private static final int DEFAULT_OFPORT = 6653;
+    private static final int DPID_BEGIN = 3;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LeadershipService leadershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceAdminService deviceAdminService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected OvsdbController ovsdbController;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected GroupService groupService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected OpenstackNodeService osNodeService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected OpenstackNodeAdminService osNodeAdminService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ComponentConfigService componentConfigService;
+
+    @Property(name = OVSDB_PORT, intValue = DEFAULT_OVSDB_PORT,
+            label = "OVSDB server listen port")
+    private int ovsdbPort = DEFAULT_OVSDB_PORT;
+
+    private final ExecutorService eventExecutor = newSingleThreadExecutor(
+            groupedThreads(this.getClass().getSimpleName(), "event-handler", log));
+
+    private final DeviceListener ovsdbListener = new InternalOvsdbListener();
+    private final DeviceListener bridgeListener = new InternalBridgeListener();
+    private final GroupListener groupListener = new InternalGroupListener();
+    private final OpenstackNodeListener osNodeListener = new InternalOpenstackNodeListener();
+
+    private ApplicationId appId;
+    private NodeId localNode;
+
+    @Activate
+    protected void activate() {
+        appId = coreService.getAppId(APP_ID);
+        localNode = clusterService.getLocalNode().id();
+
+        componentConfigService.registerProperties(getClass());
+        leadershipService.runForLeadership(appId.name());
+        groupService.addListener(groupListener);
+        deviceService.addListener(ovsdbListener);
+        deviceService.addListener(bridgeListener);
+        osNodeService.addListener(osNodeListener);
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        osNodeService.removeListener(osNodeListener);
+        deviceService.removeListener(bridgeListener);
+        deviceService.removeListener(ovsdbListener);
+        groupService.removeListener(groupListener);
+        componentConfigService.unregisterProperties(getClass(), false);
+        leadershipService.withdraw(appId.name());
+        eventExecutor.shutdown();
+
+        log.info("Stopped");
+    }
+
+    @Modified
+    protected void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context.getProperties();
+        int updatedOvsdbPort = Tools.getIntegerProperty(properties, OVSDB_PORT);
+        if (!Objects.equals(updatedOvsdbPort, ovsdbPort)) {
+            ovsdbPort = updatedOvsdbPort;
+        }
+
+        log.info("Modified");
+    }
+
+    @Override
+    public void processInitState(OpenstackNode osNode) {
+        if (!isOvsdbConnected(osNode)) {
+            ovsdbController.connect(osNode.managementIp(), tpPort(ovsdbPort));
+            return;
+        }
+        if (!deviceService.isAvailable(osNode.intgBridge())) {
+            createBridge(osNode, INTEGRATION_BRIDGE, osNode.intgBridge());
+        }
+        if (osNode.type() == GATEWAY &&
+                !isBridgeCreated(osNode.ovsdb(), ROUTER_BRIDGE)) {
+            createBridge(osNode, ROUTER_BRIDGE, osNode.routerBridge());
+        }
+    }
+
+    @Override
+    public void processDeviceCreatedState(OpenstackNode osNode) {
+        if (!isOvsdbConnected(osNode)) {
+            ovsdbController.connect(osNode.managementIp(), tpPort(ovsdbPort));
+            return;
+        }
+        if (osNode.type() == GATEWAY && (
+                !isIntfEnabled(osNode, PATCH_INTG_BRIDGE) ||
+                        !isIntfCreated(osNode, PATCH_ROUT_BRIDGE)
+        )) {
+            createPatchInterface(osNode);
+        }
+        if (osNode.dataIp() != null &&
+                !isIntfEnabled(osNode, DEFAULT_TUNNEL)) {
+            createTunnelInterface(osNode);
+        }
+        if (osNode.vlanIntf() != null &&
+                !isIntfEnabled(osNode, osNode.vlanIntf())) {
+            addSystemInterface(osNode, INTEGRATION_BRIDGE, osNode.vlanIntf());
+        }
+    }
+
+    @Override
+    public void processPortCreatedState(OpenstackNode osNode) {
+        switch (osNode.type()) {
+            case COMPUTE:
+                if (osNode.dataIp() != null) {
+                    addOrUpdateGatewayGroup(osNode,
+                            osNodeService.completeNodes(GATEWAY),
+                            VXLAN);
+                }
+                if (osNode.vlanIntf() != null) {
+                    addOrUpdateGatewayGroup(osNode,
+                            osNodeService.completeNodes(GATEWAY),
+                            VLAN);
+                }
+                break;
+            case GATEWAY:
+                Set<OpenstackNode> gateways =
+                        Sets.newHashSet(osNodeService.completeNodes(GATEWAY));
+                gateways.add(osNode);
+                osNodeService.completeNodes(COMPUTE).forEach(n -> {
+                    if (n.dataIp() != null) {
+                        addOrUpdateGatewayGroup(n, gateways, VXLAN);
+                    }
+                    if (n.vlanIntf() != null) {
+                        addOrUpdateGatewayGroup(n, gateways, VLAN);
+                    }
+                });
+                break;
+            default:
+                break;
+        }
+    }
+
+    @Override
+    public void processCompleteState(OpenstackNode osNode) {
+        OvsdbClientService ovsdbClient = ovsdbController.getOvsdbClient(
+                new OvsdbNodeId(osNode.managementIp(), DEFAULT_OVSDB_PORT));
+        if (ovsdbClient != null && ovsdbClient.isConnected()) {
+            ovsdbClient.disconnect();
+        }
+    }
+
+    @Override
+    public void processIncompleteState(OpenstackNode osNode) {
+        if (osNode.type() == COMPUTE) {
+            if (osNode.dataIp() != null) {
+                groupService.removeGroup(osNode.intgBridge(), osNode.gatewayGroupKey(VXLAN), appId);
+            }
+            if (osNode.vlanIntf() != null) {
+                groupService.removeGroup(osNode.intgBridge(), osNode.gatewayGroupKey(VLAN), appId);
+            }
+        }
+        if (osNode.type() == GATEWAY) {
+            osNodeService.completeNodes(COMPUTE).forEach(n -> {
+                if (n.dataIp() != null) {
+                    addOrUpdateGatewayGroup(n,
+                            osNodeService.completeNodes(GATEWAY),
+                            VXLAN);
+                }
+                if (n.vlanIntf() != null) {
+                    addOrUpdateGatewayGroup(n,
+                            osNodeService.completeNodes(GATEWAY),
+                            VLAN);
+                }
+            });
+        }
+    }
+
+    private boolean isOvsdbConnected(OpenstackNode osNode) {
+        OvsdbNodeId ovsdb = new OvsdbNodeId(osNode.managementIp(), ovsdbPort);
+        OvsdbClientService client = ovsdbController.getOvsdbClient(ovsdb);
+        return deviceService.isAvailable(osNode.ovsdb()) &&
+                client != null &&
+                client.isConnected();
+    }
+
+    private void createBridge(OpenstackNode osNode, String bridgeName, DeviceId deviceId) {
+        Device device = deviceService.getDevice(osNode.ovsdb());
+        if (device == null || !device.is(BridgeConfig.class)) {
+            log.error("Failed to create integration bridge on {}", osNode.ovsdb());
+            return;
+        }
+
+        // TODO fix this when we use single ONOS cluster for both openstackNode and vRouter
+        Set<IpAddress> controllerIps;
+        if (bridgeName.equals(ROUTER_BRIDGE)) {
+            // TODO checks if empty controller does not break anything
+            controllerIps = ImmutableSet.of();
+        } else {
+            controllerIps = clusterService.getNodes().stream()
+                    .map(ControllerNode::ip)
+                    .collect(Collectors.toSet());
+        }
+
+        List<ControllerInfo> controllers = controllerIps.stream()
+                .map(ip -> new ControllerInfo(ip, DEFAULT_OFPORT, DEFAULT_OF_PROTO))
+                .collect(Collectors.toList());
+
+        String dpid = deviceId.toString().substring(DPID_BEGIN);
+        BridgeDescription bridgeDesc = DefaultBridgeDescription.builder()
+                .name(bridgeName)
+                .failMode(BridgeDescription.FailMode.SECURE)
+                .datapathId(dpid)
+                .disableInBand()
+                .controllers(controllers)
+                .build();
+
+        BridgeConfig bridgeConfig = device.as(BridgeConfig.class);
+        bridgeConfig.addBridge(bridgeDesc);
+    }
+
+    private void addSystemInterface(OpenstackNode osNode, String bridgeName, String intfName) {
+        Device device = deviceService.getDevice(osNode.ovsdb());
+        if (device == null || !device.is(BridgeConfig.class)) {
+            return;
+        }
+        BridgeConfig bridgeConfig =  device.as(BridgeConfig.class);
+        bridgeConfig.addPort(BridgeName.bridgeName(bridgeName), intfName);
+    }
+
+    private void createTunnelInterface(OpenstackNode osNode) {
+        if (isIntfEnabled(osNode, DEFAULT_TUNNEL)) {
+            return;
+        }
+
+        Device device = deviceService.getDevice(osNode.ovsdb());
+        if (device == null || !device.is(InterfaceConfig.class)) {
+            log.error("Failed to create tunnel interface on {}", osNode.ovsdb());
+            return;
+        }
+
+        TunnelDescription tunnelDesc = DefaultTunnelDescription.builder()
+                .deviceId(INTEGRATION_BRIDGE)
+                .ifaceName(DEFAULT_TUNNEL)
+                .type(TunnelDescription.Type.VXLAN)
+                .remote(TunnelEndPoints.flowTunnelEndpoint())
+                .key(TunnelKeys.flowTunnelKey())
+                .build();
+
+        InterfaceConfig ifaceConfig = device.as(InterfaceConfig.class);
+        ifaceConfig.addTunnelMode(DEFAULT_TUNNEL, tunnelDesc);
+    }
+
+    private void createPatchInterface(OpenstackNode osNode) {
+        checkArgument(osNode.type().equals(OpenstackNode.NodeType.GATEWAY));
+        if (isIntfEnabled(osNode, PATCH_INTG_BRIDGE) &&
+                isIntfCreated(osNode, PATCH_ROUT_BRIDGE)) {
+            return;
+        }
+
+        Device device = deviceService.getDevice(osNode.ovsdb());
+        if (device == null || !device.is(InterfaceConfig.class)) {
+            log.error("Failed to create patch interfaces on {}", osNode.hostname());
+            return;
+        }
+
+        PatchDescription patchIntg = DefaultPatchDescription.builder()
+                .deviceId(INTEGRATION_BRIDGE)
+                .ifaceName(PATCH_INTG_BRIDGE)
+                .peer(PATCH_ROUT_BRIDGE)
+                .build();
+
+        PatchDescription patchRout = DefaultPatchDescription.builder()
+                .deviceId(ROUTER_BRIDGE)
+                .ifaceName(PATCH_ROUT_BRIDGE)
+                .peer(PATCH_INTG_BRIDGE)
+                .build();
+
+        InterfaceConfig ifaceConfig = device.as(InterfaceConfig.class);
+        ifaceConfig.addPatchMode(PATCH_INTG_BRIDGE, patchIntg);
+        ifaceConfig.addPatchMode(PATCH_ROUT_BRIDGE, patchRout);
+    }
+
+    private void addOrUpdateGatewayGroup(OpenstackNode osNode,
+                                         Set<OpenstackNode> gatewayNodes,
+                                         NetworkMode mode) {
+        GroupBuckets buckets = gatewayGroupBuckets(osNode, gatewayNodes, mode);
+        if (groupService.getGroup(osNode.intgBridge(), osNode.gatewayGroupKey(mode)) == null) {
+            GroupDescription groupDescription = new DefaultGroupDescription(
+                    osNode.intgBridge(),
+                    GroupDescription.Type.SELECT,
+                    buckets,
+                    osNode.gatewayGroupKey(mode),
+                    osNode.gatewayGroupId(mode).id(),
+                    appId);
+            groupService.addGroup(groupDescription);
+            log.debug("Created gateway group for {}", osNode.hostname());
+        } else {
+            groupService.setBucketsForGroup(
+                    osNode.intgBridge(),
+                    osNode.gatewayGroupKey(mode),
+                    buckets,
+                    osNode.gatewayGroupKey(mode),
+                    appId);
+            log.debug("Updated gateway group for {}", osNode.hostname());
+        }
+    }
+
+    private GroupBuckets gatewayGroupBuckets(OpenstackNode osNode,
+                                             Set<OpenstackNode> gatewayNodes,
+                                             NetworkMode mode) {
+        List<GroupBucket> bucketList = Lists.newArrayList();
+        switch (mode) {
+            case VXLAN:
+                gatewayNodes.stream().filter(n -> n.dataIp() != null).forEach(n -> {
+                    TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                            .extension(tunnelDstTreatment(osNode.intgBridge(),
+                                    n.dataIp()),
+                                    osNode.intgBridge())
+                            .setOutput(osNode.tunnelPortNum())
+                            .build();
+                    bucketList.add(createSelectGroupBucket(treatment));
+                });
+                return new GroupBuckets(bucketList);
+            case VLAN:
+                gatewayNodes.stream().filter(n -> n.vlanIntf() != null).forEach(n -> {
+                    TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                            .setEthDst(n.vlanPortMac())
+                            .setOutput(osNode.vlanPortNum())
+                            .build();
+                    bucketList.add(createSelectGroupBucket(treatment));
+                });
+                return new GroupBuckets(bucketList);
+            default:
+                return null;
+        }
+    }
+
+    private ExtensionTreatment tunnelDstTreatment(DeviceId deviceId, IpAddress remoteIp) {
+        Device device = deviceService.getDevice(deviceId);
+        if (device != null && !device.is(ExtensionTreatmentResolver.class)) {
+            log.error("The extension treatment is not supported");
+            return null;
+        }
+
+        ExtensionTreatmentResolver resolver = device.as(ExtensionTreatmentResolver.class);
+        ExtensionTreatment treatment = resolver.getExtensionInstruction(NICIRA_SET_TUNNEL_DST.type());
+        try {
+            treatment.setPropertyValue("tunnelDst", remoteIp.getIp4Address());
+            return treatment;
+        } catch (ExtensionPropertyException e) {
+            log.warn("Failed to get tunnelDst extension treatment for {}", deviceId);
+            return null;
+        }
+    }
+
+    private boolean isBridgeCreated(DeviceId ovsdbId, String bridgeName) {
+        Device device = deviceService.getDevice(ovsdbId);
+        if (device == null || !deviceService.isAvailable(device.id()) ||
+                !device.is(BridgeConfig.class)) {
+            return false;
+        }
+        BridgeConfig bridgeConfig = device.as(BridgeConfig.class);
+        return bridgeConfig.getBridges().stream()
+                .anyMatch(bridge -> bridge.name().equals(bridgeName));
+    }
+
+    private boolean isIntfEnabled(OpenstackNode osNode, String intf) {
+        if (!deviceService.isAvailable(osNode.intgBridge())) {
+            return false;
+        }
+        return deviceService.getPorts(osNode.intgBridge()).stream()
+                .anyMatch(port -> Objects.equals(
+                        port.annotations().value(PORT_NAME), intf) &&
+                        port.isEnabled());
+    }
+
+    private boolean isIntfCreated(OpenstackNode osNode, String intf) {
+        Device device = deviceService.getDevice(osNode.ovsdb());
+        if (device == null || !deviceService.isAvailable(osNode.ovsdb()) ||
+                !device.is(BridgeConfig.class)) {
+            return false;
+        }
+
+        BridgeConfig bridgeConfig =  device.as(BridgeConfig.class);
+        return bridgeConfig.getPorts().stream()
+                .anyMatch(port -> port.annotations().value(PORT_NAME).equals(intf));
+    }
+
+    private boolean isGroupCreated(OpenstackNode osNode) {
+        for (OpenstackNode gNode : osNodeService.completeNodes(GATEWAY)) {
+            if (!isGatewayBucketAdded(osNode, gNode)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isGatewayBucketAdded(OpenstackNode cNode, OpenstackNode gNode) {
+        if (cNode.dataIp() != null) {
+            Group osGroup = groupService.getGroup(cNode.intgBridge(),
+                    cNode.gatewayGroupKey(VXLAN));
+            TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                    .extension(tunnelDstTreatment(gNode.intgBridge(),
+                            gNode.dataIp()),
+                            cNode.intgBridge())
+                    .setOutput(cNode.tunnelPortNum())
+                    .build();
+            GroupBucket bucket = createSelectGroupBucket(treatment);
+            if (osGroup == null || !osGroup.buckets().buckets().contains(bucket)) {
+                return false;
+            }
+        }
+        if (cNode.vlanIntf() != null) {
+            Group osGroup = groupService.getGroup(cNode.intgBridge(),
+                    cNode.gatewayGroupKey(VLAN));
+            TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                    .setEthDst(gNode.vlanPortMac())
+                    .setOutput(cNode.vlanPortNum())
+                    .build();
+            GroupBucket bucket = createSelectGroupBucket(treatment);
+            if (osGroup == null || !osGroup.buckets().buckets().contains(bucket)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isCurrentStateDone(OpenstackNode osNode) {
+        switch (osNode.state()) {
+            case INIT:
+                if (!deviceService.isAvailable(osNode.intgBridge())) {
+                    return false;
+                }
+                if (osNode.type() == GATEWAY &&
+                        !isBridgeCreated(osNode.ovsdb(), ROUTER_BRIDGE)) {
+                    return false;
+                }
+                return true;
+            case DEVICE_CREATED:
+                if (osNode.dataIp() != null &&
+                        !isIntfEnabled(osNode, DEFAULT_TUNNEL)) {
+                    return false;
+                }
+                if (osNode.vlanIntf() != null &&
+                        !isIntfEnabled(osNode, osNode.vlanIntf())) {
+                    return false;
+                }
+                if (osNode.type() == GATEWAY && (
+                        !isIntfEnabled(osNode, PATCH_INTG_BRIDGE) ||
+                        !isIntfCreated(osNode, PATCH_ROUT_BRIDGE))) {
+                    return false;
+                }
+                return true;
+            case PORT_CREATED:
+                if (osNode.type() == COMPUTE) {
+                    return isGroupCreated(osNode);
+                } else {
+                    for (OpenstackNode cNode : osNodeService.completeNodes(COMPUTE)) {
+                        if (!isGatewayBucketAdded(cNode, osNode)) {
+                            return false;
+                        }
+                    }
+                    return true;
+                }
+            case COMPLETE:
+                return false;
+            case INCOMPLETE:
+                // always return false
+                // run init CLI to re-trigger node bootstrap
+                return false;
+            default:
+                return true;
+        }
+    }
+
+    private void setState(OpenstackNode osNode, NodeState newState) {
+        if (osNode.state() == newState) {
+            return;
+        }
+        OpenstackNode updated = osNode.updateState(newState);
+        osNodeAdminService.updateNode(updated);
+        log.info("Changed {} state: {}", osNode.hostname(), newState);
+    }
+
+    private void bootstrapNode(OpenstackNode osNode) {
+        if (isCurrentStateDone(osNode)) {
+            setState(osNode, osNode.state().nextState());
+        } else {
+            log.trace("Processing {} state for {}", osNode.state(), osNode.hostname());
+            osNode.state().process(this, osNode);
+        }
+    }
+
+    private class InternalOvsdbListener implements DeviceListener {
+
+        @Override
+        public boolean isRelevant(DeviceEvent event) {
+            NodeId leader = leadershipService.getLeader(appId.name());
+            return Objects.equals(localNode, leader) &&
+                    event.subject().type() == Device.Type.CONTROLLER &&
+                    osNodeService.node(event.subject().id()) != null;
+        }
+
+        @Override
+        public void event(DeviceEvent event) {
+            Device device = event.subject();
+            OpenstackNode osNode = osNodeService.node(device.id());
+
+            switch (event.type()) {
+                case DEVICE_AVAILABILITY_CHANGED:
+                case DEVICE_ADDED:
+                    eventExecutor.execute(() -> {
+                        if (deviceService.isAvailable(device.id())) {
+                            log.debug("OVSDB {} detected", device.id());
+                            bootstrapNode(osNode);
+                        } else if (osNode.state() == COMPLETE) {
+                            log.debug("Removing OVSDB {}", device.id());
+                            deviceAdminService.removeDevice(device.id());
+                        }
+                    });
+                    break;
+                case PORT_ADDED:
+                case PORT_REMOVED:
+                case DEVICE_REMOVED:
+                default:
+                    // do nothing
+                    break;
+            }
+        }
+    }
+
+    private class InternalBridgeListener implements DeviceListener {
+
+        @Override
+        public boolean isRelevant(DeviceEvent event) {
+            NodeId leader = leadershipService.getLeader(appId.name());
+            return Objects.equals(localNode, leader) &&
+                    event.subject().type() == Device.Type.SWITCH &&
+                    osNodeService.node(event.subject().id()) != null;
+        }
+
+        @Override
+        public void event(DeviceEvent event) {
+            Device device = event.subject();
+            OpenstackNode osNode = osNodeService.node(device.id());
+
+            switch (event.type()) {
+                case DEVICE_AVAILABILITY_CHANGED:
+                case DEVICE_ADDED:
+                    eventExecutor.execute(() -> {
+                        if (deviceService.isAvailable(device.id())) {
+                            log.debug("Integration bridge created on {}", osNode.hostname());
+                            bootstrapNode(osNode);
+                        } else if (osNode.state() == COMPLETE) {
+                            log.warn("Device {} disconnected", device.id());
+                            setState(osNode, INCOMPLETE);
+                        }
+                    });
+                    break;
+                case PORT_ADDED:
+                    eventExecutor.execute(() -> {
+                        Port port = event.port();
+                        String portName = port.annotations().value(PORT_NAME);
+                        if (osNode.state() == DEVICE_CREATED && (
+                                Objects.equals(portName, DEFAULT_TUNNEL) ||
+                                Objects.equals(portName, osNode.vlanIntf()) ||
+                                Objects.equals(portName, PATCH_INTG_BRIDGE) ||
+                                Objects.equals(portName, PATCH_ROUT_BRIDGE))) {
+                            // FIXME we never gets PATCH_ROUTE_BRIDGE port added events as of now
+                            log.debug("Interface {} added to {}", portName, event.subject().id());
+                            bootstrapNode(osNode);
+                        }
+                    });
+                    break;
+                case PORT_REMOVED:
+                    eventExecutor.execute(() -> {
+                        Port port = event.port();
+                        String portName = port.annotations().value(PORT_NAME);
+                        if (osNode.state() == COMPLETE && (
+                                Objects.equals(portName, DEFAULT_TUNNEL) ||
+                                Objects.equals(portName, osNode.vlanIntf()) ||
+                                Objects.equals(portName, PATCH_INTG_BRIDGE) ||
+                                Objects.equals(portName, PATCH_ROUT_BRIDGE))) {
+                            log.warn("Interface {} removed from {}", portName, event.subject().id());
+                            setState(osNode, INCOMPLETE);
+                        }
+                    });
+                    break;
+                case PORT_UPDATED:
+                case DEVICE_REMOVED:
+                default:
+                    // do nothing
+                    break;
+            }
+        }
+    }
+
+    private class InternalGroupListener implements GroupListener {
+
+        @Override
+        public boolean isRelevant(GroupEvent event) {
+            NodeId leader = leadershipService.getLeader(appId.name());
+            return Objects.equals(localNode, leader);
+        }
+
+        @Override
+        public void event(GroupEvent event) {
+            switch (event.type()) {
+                case GROUP_ADDED:
+                    eventExecutor.execute(() -> {
+                        log.trace("Group added, ID:{} state:{}", event.subject().id(),
+                                event.subject().state());
+                        processGroup(event.subject());
+                    });
+                    break;
+                case GROUP_UPDATED:
+                    eventExecutor.execute(() -> {
+                        log.trace("Group updated, ID:{} state:{}", event.subject().id(),
+                                event.subject().state());
+                        processGroup(event.subject());
+                    });
+                    break;
+                case GROUP_REMOVED:
+                    // TODO handle group removed
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        private void processGroup(Group group) {
+            OpenstackNode osNode = osNodeService.nodes(COMPUTE).stream()
+                    .filter(n -> n.state() == PORT_CREATED &&
+                            (n.gatewayGroupId(VXLAN).equals(group.id()) ||
+                            n.gatewayGroupId(VLAN).equals(group.id())))
+                    .findAny().orElse(null);
+            if (osNode != null) {
+                bootstrapNode(osNode);
+            }
+            osNodeService.nodes(GATEWAY).stream()
+                    .filter(gNode -> gNode.state() == PORT_CREATED)
+                    .forEach(DefaultOpenstackNodeHandler.this::bootstrapNode);
+        }
+    }
+
+    private class InternalOpenstackNodeListener implements OpenstackNodeListener {
+
+        @Override
+        public boolean isRelevant(OpenstackNodeEvent event) {
+            NodeId leader = leadershipService.getLeader(appId.name());
+            return Objects.equals(localNode, leader);
+        }
+
+        @Override
+        public void event(OpenstackNodeEvent event) {
+            switch (event.type()) {
+                case OPENSTACK_NODE_CREATED:
+                case OPENSTACK_NODE_UPDATED:
+                    eventExecutor.execute(() -> {
+                        bootstrapNode(event.subject());
+                    });
+                    break;
+                case OPENSTACK_NODE_COMPLETE:
+                    break;
+                case OPENSTACK_NODE_REMOVED:
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+}
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/DistributedOpenstackNodeStore.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/DistributedOpenstackNodeStore.java
new file mode 100644
index 0000000..f248367
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/DistributedOpenstackNodeStore.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.openstacknode.impl;
+
+import com.google.common.collect.ImmutableSet;
+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.util.KryoNamespace;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.openstacknode.api.NodeState;
+import org.onosproject.openstacknode.api.OpenstackNode;
+import org.onosproject.openstacknode.api.OpenstackNodeEvent;
+import org.onosproject.openstacknode.api.OpenstackNodeStore;
+import org.onosproject.openstacknode.api.OpenstackNodeStoreDelegate;
+import org.onosproject.store.AbstractStore;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.MapEvent;
+import org.onosproject.store.service.MapEventListener;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.Versioned;
+import org.slf4j.Logger;
+
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.openstacknode.api.NodeState.COMPLETE;
+import static org.onosproject.openstacknode.api.NodeState.INCOMPLETE;
+import static org.onosproject.openstacknode.api.OpenstackNodeEvent.Type.*;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of openstack node store using consistent map.
+ */
+@Service
+@Component(immediate = true)
+public class DistributedOpenstackNodeStore
+        extends AbstractStore<OpenstackNodeEvent, OpenstackNodeStoreDelegate>
+        implements OpenstackNodeStore {
+
+    protected final Logger log = getLogger(getClass());
+
+    private static final String ERR_NOT_FOUND = " does not exist";
+    private static final String ERR_DUPLICATE = " already exists";
+
+    private static final KryoNamespace SERIALIZER_OPENSTACK_NODE = KryoNamespace.newBuilder()
+            .register(KryoNamespaces.API)
+            .register(OpenstackNode.class)
+            .register(DefaultOpenstackNode.class)
+            .register(OpenstackNode.NodeType.class)
+            .register(NodeState.class)
+            .build();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StorageService storageService;
+
+    private final ExecutorService eventExecutor = newSingleThreadExecutor(
+            groupedThreads(this.getClass().getSimpleName(), "event-handler", log));
+
+    private final MapEventListener<String, OpenstackNode> osNodeMapListener =
+            new OpenstackNodeMapListener();
+    private ConsistentMap<String, OpenstackNode> osNodeStore;
+
+    @Activate
+    protected void activate() {
+        ApplicationId appId = coreService.registerApplication("org.onosproject.openstacknode");
+        osNodeStore = storageService.<String, OpenstackNode>consistentMapBuilder()
+                .withSerializer(Serializer.using(SERIALIZER_OPENSTACK_NODE))
+                .withName("openstack-nodestore")
+                .withApplicationId(appId)
+                .build();
+        osNodeStore.addListener(osNodeMapListener);
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        osNodeStore.removeListener(osNodeMapListener);
+        eventExecutor.shutdown();
+        log.info("Stopped");
+    }
+
+    @Override
+    public void createNode(OpenstackNode osNode) {
+        osNodeStore.compute(osNode.hostname(), (hostname, existing) -> {
+            final String error = osNode.hostname() + ERR_DUPLICATE;
+            checkArgument(existing == null, error);
+            return osNode;
+        });
+    }
+
+    @Override
+    public void updateNode(OpenstackNode osNode) {
+        osNodeStore.compute(osNode.hostname(), (hostname, existing) -> {
+            final String error = osNode.hostname() + ERR_NOT_FOUND;
+            checkArgument(existing != null, error);
+            return osNode;
+        });
+    }
+
+    @Override
+    public OpenstackNode removeNode(String hostname) {
+        Versioned<OpenstackNode> osNode = osNodeStore.remove(hostname);
+        if (osNode == null) {
+            final String error = hostname + ERR_NOT_FOUND;
+            throw new IllegalArgumentException(error);
+        }
+        return osNode.value();
+    }
+
+    @Override
+    public Set<OpenstackNode> nodes() {
+        Set<OpenstackNode> osNodes = osNodeStore.values().stream()
+                .map(Versioned::value)
+                .collect(Collectors.toSet());
+        return ImmutableSet.copyOf(osNodes);
+    }
+
+    @Override
+    public OpenstackNode node(String hostname) {
+        Versioned<OpenstackNode> osNode = osNodeStore.get(hostname);
+        return osNode == null ? null : osNode.value();
+    }
+
+    private class OpenstackNodeMapListener implements MapEventListener<String, OpenstackNode> {
+
+        @Override
+        public void event(MapEvent<String, OpenstackNode> event) {
+            switch (event.type()) {
+                case INSERT:
+                    log.debug("OpenStack node created {}", event.newValue());
+                    eventExecutor.execute(() -> {
+                        notifyDelegate(new OpenstackNodeEvent(
+                                OPENSTACK_NODE_CREATED,
+                                event.newValue().value()
+                        ));
+                    });
+                    break;
+                case UPDATE:
+                    log.debug("OpenStack node updated {}", event.newValue());
+                    eventExecutor.execute(() -> {
+                        notifyDelegate(new OpenstackNodeEvent(
+                                OPENSTACK_NODE_UPDATED,
+                                event.newValue().value()
+                        ));
+                        if (event.newValue().value().state() == COMPLETE) {
+                            notifyDelegate(new OpenstackNodeEvent(
+                                    OPENSTACK_NODE_COMPLETE,
+                                    event.newValue().value()
+                            ));
+                        } else if (event.newValue().value().state() == INCOMPLETE) {
+                            notifyDelegate(new OpenstackNodeEvent(
+                                    OPENSTACK_NODE_INCOMPLETE,
+                                    event.newValue().value()
+                            ));
+                        }
+                    });
+                    break;
+                case REMOVE:
+                    log.debug("OpenStack node removed {}", event.oldValue());
+                    eventExecutor.execute(() -> {
+                        notifyDelegate(new OpenstackNodeEvent(
+                                OPENSTACK_NODE_REMOVED,
+                                event.oldValue().value()
+                        ));
+                    });
+                    break;
+                default:
+                    // do nothing
+                    break;
+            }
+        }
+    }
+}
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/OpenstackNodeManager.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/OpenstackNodeManager.java
new file mode 100644
index 0000000..ddd583e
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/OpenstackNodeManager.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.openstacknode.impl;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+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.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.event.ListenerRegistry;
+import org.onosproject.net.DeviceId;
+import org.onosproject.openstacknode.api.OpenstackNode;
+import org.onosproject.openstacknode.api.OpenstackNodeAdminService;
+import org.onosproject.openstacknode.api.OpenstackNodeEvent;
+import org.onosproject.openstacknode.api.OpenstackNodeListener;
+import org.onosproject.openstacknode.api.OpenstackNodeService;
+import org.onosproject.openstacknode.api.OpenstackNodeStore;
+import org.onosproject.openstacknode.api.OpenstackNodeStoreDelegate;
+import org.slf4j.Logger;
+
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.openstacknode.api.NodeState.COMPLETE;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Service administering the inventory of openstack nodes.
+ */
+@Service
+@Component(immediate = true)
+public class OpenstackNodeManager extends ListenerRegistry<OpenstackNodeEvent, OpenstackNodeListener>
+        implements OpenstackNodeService, OpenstackNodeAdminService {
+
+    protected final Logger log = getLogger(getClass());
+
+    private static final String MSG_NODE = "OpenStack node %s %s";
+    private static final String MSG_CREATED = "created";
+    private static final String MSG_UPDATED = "updated";
+    private static final String MSG_REMOVED = "removed";
+
+    private static final String ERR_NULL_NODE = "OpenStack node cannot be null";
+    private static final String ERR_NULL_HOSTNAME = "OpenStack node hostname cannot be null";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected OpenstackNodeStore osNodeStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LeadershipService leadershipService;
+
+    private final ExecutorService eventExecutor = newSingleThreadExecutor(
+            groupedThreads(this.getClass().getSimpleName(), "event-handler", log));
+
+    private final OpenstackNodeStoreDelegate delegate = new InternalNodeStoreDelegate();
+
+    private ApplicationId appId;
+    private NodeId localNode;
+
+    @Activate
+    protected void activate() {
+        appId = coreService.registerApplication(APP_ID);
+        osNodeStore.setDelegate(delegate);
+
+        localNode = clusterService.getLocalNode().id();
+        leadershipService.runForLeadership(appId.name());
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        osNodeStore.unsetDelegate(delegate);
+
+        leadershipService.withdraw(appId.name());
+        eventExecutor.shutdown();
+
+        log.info("Stopped");
+    }
+
+    @Override
+    public void createNode(OpenstackNode osNode) {
+        checkNotNull(osNode, ERR_NULL_NODE);
+        osNodeStore.createNode(osNode);
+        log.info(String.format(MSG_NODE, osNode.hostname(), MSG_CREATED));
+    }
+
+    @Override
+    public void updateNode(OpenstackNode osNode) {
+        checkNotNull(osNode, ERR_NULL_NODE);
+        osNodeStore.updateNode(osNode);
+        log.info(String.format(MSG_NODE, osNode.hostname(), MSG_UPDATED));
+    }
+
+    @Override
+    public OpenstackNode removeNode(String hostname) {
+        checkArgument(!Strings.isNullOrEmpty(hostname), ERR_NULL_HOSTNAME);
+        OpenstackNode osNode = osNodeStore.removeNode(hostname);
+        log.info(String.format(MSG_NODE, hostname, MSG_REMOVED));
+        return osNode;
+    }
+
+    @Override
+    public Set<OpenstackNode> nodes() {
+        return osNodeStore.nodes();
+    }
+
+    @Override
+    public Set<OpenstackNode> nodes(OpenstackNode.NodeType type) {
+        Set<OpenstackNode> osNodes = osNodeStore.nodes().stream()
+                .filter(osNode -> Objects.equals(osNode.type(), type))
+                .collect(Collectors.toSet());
+        return ImmutableSet.copyOf(osNodes);
+    }
+
+    @Override
+    public Set<OpenstackNode> completeNodes() {
+        Set<OpenstackNode> osNodes = osNodeStore.nodes().stream()
+                .filter(osNode -> Objects.equals(osNode.state(), COMPLETE))
+                .collect(Collectors.toSet());
+        return ImmutableSet.copyOf(osNodes);
+    }
+
+    @Override
+    public Set<OpenstackNode> completeNodes(OpenstackNode.NodeType type) {
+        Set<OpenstackNode> osNodes = osNodeStore.nodes().stream()
+                .filter(osNode -> osNode.type() == type &&
+                        Objects.equals(osNode.state(), COMPLETE))
+                .collect(Collectors.toSet());
+        return ImmutableSet.copyOf(osNodes);
+    }
+
+    @Override
+    public OpenstackNode node(String hostname) {
+        return osNodeStore.node(hostname);
+    }
+
+    @Override
+    public OpenstackNode node(DeviceId deviceId) {
+        OpenstackNode result = osNodeStore.nodes().stream()
+                .filter(osNode -> Objects.equals(osNode.intgBridge(), deviceId) ||
+                        Objects.equals(osNode.ovsdb(), deviceId) ||
+                        Objects.equals(osNode.routerBridge(), deviceId))
+                .findFirst().orElse(null);
+        return result;
+    }
+
+    private class InternalNodeStoreDelegate implements OpenstackNodeStoreDelegate {
+
+        @Override
+        public void notify(OpenstackNodeEvent event) {
+            if (event != null) {
+                log.trace("send openstack node event {}", event);
+                process(event);
+            }
+        }
+    }
+}
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/package-info.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/package-info.java
new file mode 100644
index 0000000..4982c44
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/**
+ * Application for bootstrapping Compute/Gateway Node in OpenStack.
+ */
+package org.onosproject.openstacknode.impl;
\ No newline at end of file
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/web/OpenstackNodeWebResource.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/web/OpenstackNodeWebResource.java
new file mode 100644
index 0000000..cce5134
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/web/OpenstackNodeWebResource.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.openstacknode.web;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.google.common.collect.Sets;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onlab.packet.IpAddress;
+import org.onosproject.net.DeviceId;
+import org.onosproject.openstacknode.api.NodeState;
+import org.onosproject.openstacknode.api.OpenstackNode;
+import org.onosproject.openstacknode.api.OpenstackNodeAdminService;
+import org.onosproject.openstacknode.api.OpenstackNodeService;
+import org.onosproject.openstacknode.impl.DefaultOpenstackNode;
+import org.onosproject.rest.AbstractWebResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.io.InputStream;
+import java.util.Set;
+
+import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
+import static javax.ws.rs.core.Response.created;
+
+@Path("configure")
+public class OpenstackNodeWebResource extends AbstractWebResource {
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final String MESSAGE_NODE = "Received node %s request";
+    private static final String NODES = "nodes";
+
+    private final OpenstackNodeAdminService osNodeAdminService =
+            DefaultServiceDirectory.getService(OpenstackNodeAdminService.class);
+    private final OpenstackNodeService osNodeService =
+            DefaultServiceDirectory.getService(OpenstackNodeService.class);
+
+    @Context
+    private UriInfo uriInfo;
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response createNodes(InputStream input) {
+        log.trace(String.format(MESSAGE_NODE, "CREATE"));
+
+        readNodeConfiguration(input).forEach(osNode -> {
+            OpenstackNode existing = osNodeService.node(osNode.hostname());
+            if (existing == null) {
+                osNodeAdminService.createNode(osNode);
+            }
+        });
+
+        UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+                .path(NODES)
+                .path("NODE_ID");
+
+        return created(locationBuilder.build()).build();
+    }
+
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response updateNodes(InputStream input) {
+        log.trace(String.format(MESSAGE_NODE, "UPDATE"));
+
+        Set<OpenstackNode> nodes = readNodeConfiguration(input);
+        for (OpenstackNode osNode: nodes) {
+            OpenstackNode existing = osNodeService.node(osNode.hostname());
+            if (existing == null) {
+                log.warn("There is no node configuration to update : {}", osNode.hostname());
+                return Response.notModified().build();
+            } else if (!existing.equals(osNode)) {
+                osNodeAdminService.updateNode(osNode);
+            }
+        }
+
+        return Response.ok().build();
+    }
+
+    @DELETE
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response deleteNodes(InputStream input) {
+        log.trace(String.format(MESSAGE_NODE, "DELETE"));
+
+        Set<OpenstackNode> nodes = readNodeConfiguration(input);
+        for (OpenstackNode osNode: nodes) {
+            OpenstackNode existing = osNodeService.node(osNode.hostname());
+            if (existing == null) {
+                log.warn("There is no node configuration to delete : {}", osNode.hostname());
+                return Response.notModified().build();
+            } else {
+                osNodeAdminService.removeNode(osNode.hostname());
+            }
+        }
+
+        return Response.ok().build();
+    }
+
+    private Set<OpenstackNode> readNodeConfiguration(InputStream input) {
+        Set<OpenstackNode> nodeSet = Sets.newHashSet();
+        try {
+             JsonNode jsonTree = mapper().enable(INDENT_OUTPUT).readTree(input);
+             ArrayNode nodes = (ArrayNode) jsonTree.path("nodes");
+             nodes.forEach(node -> {
+                 try {
+                     String hostname = node.get("hostname").asText();
+                     String type = node.get("type").asText();
+                     String mIp = node.get("managementIp").asText();
+                     String dIp = node.get("dataIp").asText();
+                     String iBridge = node.get("integrationBridge").asText();
+                     String rBridge = null;
+                     if (node.get("routerBridge") != null) {
+                         rBridge = node.get("routerBridge").asText();
+                     }
+                     DefaultOpenstackNode.Builder nodeBuilder = DefaultOpenstackNode.builder()
+                             .hostname(hostname)
+                             .type(OpenstackNode.NodeType.valueOf(type))
+                             .managementIp(IpAddress.valueOf(mIp))
+                             .dataIp(IpAddress.valueOf(dIp))
+                             .intgBridge(DeviceId.deviceId(iBridge))
+                             .state(NodeState.INIT);
+                     if (rBridge != null) {
+                         nodeBuilder.routerBridge(DeviceId.deviceId(rBridge));
+                     }
+                     log.trace("node is {}", nodeBuilder.build().toString());
+                     nodeSet.add(nodeBuilder.build());
+                 } catch (Exception e) {
+                     log.error(e.toString());
+                     throw  new IllegalArgumentException();
+                 }
+             });
+        } catch (Exception e) {
+            throw new IllegalArgumentException(e);
+        }
+
+        return nodeSet;
+    }
+}
diff --git a/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/web/package-info.java b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/web/package-info.java
new file mode 100644
index 0000000..85d6a61
--- /dev/null
+++ b/apps/openstacknode/app/src/main/java/org/onosproject/openstacknode/web/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/**
+ * Application for bootstrapping Compute/Gateway Node in OpenStack.
+ */
+package org.onosproject.openstacknode.web;
\ No newline at end of file
diff --git a/apps/openstacknode/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/openstacknode/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..f6bf14b
--- /dev/null
+++ b/apps/openstacknode/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,29 @@
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.openstacknode.cli.OpenstackNodeListCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.openstacknode.cli.OpenstackNodeCheckCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.openstacknode.cli.OpenstackNodeInitCommand"/>
+        </command>
+    </command-bundle>
+</blueprint>
diff --git a/apps/openstacknode/app/src/main/webapp/WEB-INF/web.xml b/apps/openstacknode/app/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..583cd55
--- /dev/null
+++ b/apps/openstacknode/app/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2017-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         id="ONOS" version="2.5">
+    <display-name>Openstack Node REST API v1.0</display-name>
+
+    <servlet>
+        <servlet-name>JAX-RS Service</servlet-name>
+        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
+        <init-param>
+            <param-name>jersey.config.server.provider.classnames</param-name>
+            <param-value>
+                org.onosproject.openstacknode.web.OpenstackNodeWebResource
+            </param-value>
+        </init-param>
+        <load-on-startup>1</load-on-startup>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>JAX-RS Service</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+</web-app>
diff --git a/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/DefaultOpenstackNodeHandlerTest.java b/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/DefaultOpenstackNodeHandlerTest.java
new file mode 100644
index 0000000..aadec13
--- /dev/null
+++ b/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/DefaultOpenstackNodeHandlerTest.java
@@ -0,0 +1,1062 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.openstacknode.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.junit.TestUtils;
+import org.onlab.packet.ChassisId;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.cfg.ComponentConfigAdapter;
+import org.onosproject.cluster.ClusterServiceAdapter;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.DefaultControllerNode;
+import org.onosproject.cluster.LeadershipServiceAdapter;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreServiceAdapter;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.Annotations;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.DefaultDevice;
+import org.onosproject.net.DefaultPort;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.behaviour.BridgeConfig;
+import org.onosproject.net.behaviour.BridgeDescription;
+import org.onosproject.net.behaviour.BridgeName;
+import org.onosproject.net.behaviour.ControllerInfo;
+import org.onosproject.net.behaviour.DefaultBridgeDescription;
+import org.onosproject.net.behaviour.ExtensionTreatmentResolver;
+import org.onosproject.net.behaviour.InterfaceConfig;
+import org.onosproject.net.behaviour.PatchDescription;
+import org.onosproject.net.behaviour.TunnelDescription;
+import org.onosproject.net.device.DefaultPortDescription;
+import org.onosproject.net.device.DeviceAdminService;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceInterfaceDescription;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceServiceAdapter;
+import org.onosproject.net.device.PortDescription;
+import org.onosproject.net.driver.Behaviour;
+import org.onosproject.net.driver.DriverData;
+import org.onosproject.net.driver.DriverHandler;
+import org.onosproject.net.flow.instructions.ExtensionPropertyException;
+import org.onosproject.net.flow.instructions.ExtensionTreatment;
+import org.onosproject.net.flow.instructions.ExtensionTreatmentType;
+import org.onosproject.net.group.DefaultGroup;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.group.GroupEvent;
+import org.onosproject.net.group.GroupKey;
+import org.onosproject.net.group.GroupListener;
+import org.onosproject.net.group.GroupService;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.openstacknode.api.NodeState;
+import org.onosproject.openstacknode.api.OpenstackNode;
+import org.onosproject.openstacknode.api.OpenstackNodeAdminService;
+import org.onosproject.openstacknode.api.OpenstackNodeListener;
+import org.onosproject.openstacknode.api.OpenstackNodeService;
+import org.onosproject.ovsdb.controller.OvsdbClientService;
+import org.onosproject.ovsdb.controller.OvsdbController;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.easymock.EasyMock.*;
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertEquals;
+import static org.onosproject.net.AnnotationKeys.PORT_NAME;
+import static org.onosproject.net.Device.Type.CONTROLLER;
+import static org.onosproject.net.Device.Type.SWITCH;
+import static org.onosproject.net.device.DeviceEvent.Type.*;
+import static org.onosproject.openstacknode.api.Constants.*;
+import static org.onosproject.openstacknode.api.NodeState.*;
+import static org.onosproject.openstacknode.api.OpenstackNode.NodeType.COMPUTE;
+import static org.onosproject.openstacknode.api.OpenstackNode.NodeType.GATEWAY;
+
+/**
+ * Unit test for DefaultOpenstackNodeHandler.
+ */
+public class DefaultOpenstackNodeHandlerTest {
+
+    private static final ApplicationId TEST_APP_ID = new DefaultApplicationId(1, "test");
+    private static final String ERR_STATE_NOT_MATCH = "Node state did not match";
+    private static final NodeId LOCAL_NODE_ID = new NodeId("local");
+    private static final ControllerNode LOCAL_CTRL =
+            new DefaultControllerNode(LOCAL_NODE_ID, IpAddress.valueOf("127.0.0.1"));
+
+    private static final BridgeDescription ROUT_BRIDGE = DefaultBridgeDescription.builder()
+            .name(ROUTER_BRIDGE)
+            .failMode(BridgeDescription.FailMode.SECURE)
+            .disableInBand()
+            .build();
+
+    private static final PortDescription PATCH_ROUT = new DefaultPortDescription(
+            PortNumber.portNumber(1),
+            true,
+            DefaultAnnotations.builder()
+                    .set(PORT_NAME, PATCH_ROUT_BRIDGE)
+                    .build()
+    );
+
+    private static final String COMPUTE_1_HOSTNAME = "compute_1";
+    private static final String COMPUTE_2_HOSTNAME = "compute_2";
+    private static final String COMPUTE_3_HOSTNAME = "compute_3";
+    private static final String COMPUTE_4_HOSTNAME = "compute_4";
+    private static final String GATEWAY_1_HOSTNAME = "gateway_1";
+    private static final String GATEWAY_2_HOSTNAME = "gateway_2";
+    private static final String GATEWAY_3_HOSTNAME = "gateway_3";
+    private static final String GATEWAY_4_HOSTNAME = "gateway_4";
+
+    private static final IpAddress COMPUTE_1_IP = IpAddress.valueOf("10.100.0.1");
+    private static final IpAddress COMPUTE_2_IP = IpAddress.valueOf("10.100.0.2");
+    private static final IpAddress COMPUTE_3_IP = IpAddress.valueOf("10.100.0.3");
+    private static final IpAddress COMPUTE_4_IP = IpAddress.valueOf("10.100.0.4");
+    private static final IpAddress GATEWAY_1_IP = IpAddress.valueOf("10.100.0.5");
+    private static final IpAddress GATEWAY_2_IP = IpAddress.valueOf("10.100.0.6");
+    private static final IpAddress GATEWAY_3_IP = IpAddress.valueOf("10.100.0.7");
+    private static final IpAddress GATEWAY_4_IP = IpAddress.valueOf("10.100.0.8");
+
+    private static final Device COMPUTE_1_INTG_DEVICE = createOpenFlowDevice(1, INTEGRATION_BRIDGE);
+    private static final Device COMPUTE_2_INTG_DEVICE = createOpenFlowDevice(2, INTEGRATION_BRIDGE);
+    private static final Device COMPUTE_3_INTG_DEVICE = createOpenFlowDevice(3, INTEGRATION_BRIDGE);
+    private static final Device COMPUTE_4_INTG_DEVICE = createOpenFlowDevice(4, INTEGRATION_BRIDGE);
+    private static final Device GATEWAY_1_INTG_DEVICE = createOpenFlowDevice(5, INTEGRATION_BRIDGE);
+    private static final Device GATEWAY_1_ROUT_DEVICE = createOpenFlowDevice(6, ROUTER_BRIDGE);
+    private static final Device GATEWAY_2_INTG_DEVICE = createOpenFlowDevice(7, INTEGRATION_BRIDGE);
+    private static final Device GATEWAY_2_ROUT_DEVICE = createOpenFlowDevice(8, ROUTER_BRIDGE);
+    private static final Device GATEWAY_3_INTG_DEVICE = createOpenFlowDevice(9, INTEGRATION_BRIDGE);
+    private static final Device GATEWAY_3_ROUT_DEVICE = createOpenFlowDevice(10, ROUTER_BRIDGE);
+    private static final Device GATEWAY_4_INTG_DEVICE = createOpenFlowDevice(11, INTEGRATION_BRIDGE);
+    private static final Device GATEWAY_4_ROUT_DEVICE = createOpenFlowDevice(12, ROUTER_BRIDGE);
+
+    private static final Device COMPUTE_1_OVSDB_DEVICE = createOvsdbDevice(COMPUTE_1_IP);
+    private static final Device COMPUTE_2_OVSDB_DEVICE = createOvsdbDevice(COMPUTE_2_IP);
+    private static final Device COMPUTE_3_OVSDB_DEVICE = createOvsdbDevice(COMPUTE_3_IP);
+    private static final Device COMPUTE_4_OVSDB_DEVICE = createOvsdbDevice(COMPUTE_4_IP);
+    private static final Device GATEWAY_1_OVSDB_DEVICE = createOvsdbDevice(GATEWAY_1_IP);
+    private static final Device GATEWAY_2_OVSDB_DEVICE = createOvsdbDevice(GATEWAY_2_IP);
+
+    private static final OpenstackNode COMPUTE_1 = createNode(
+            COMPUTE_1_HOSTNAME,
+            COMPUTE,
+            COMPUTE_1_INTG_DEVICE,
+            COMPUTE_1_IP,
+            INIT
+    );
+
+    private static final OpenstackNode COMPUTE_2 = createNode(
+            COMPUTE_2_HOSTNAME,
+            COMPUTE,
+            COMPUTE_2_INTG_DEVICE,
+            COMPUTE_2_IP,
+            DEVICE_CREATED
+    );
+
+    private static final OpenstackNode COMPUTE_3 = createNode(
+            COMPUTE_3_HOSTNAME,
+            COMPUTE,
+            COMPUTE_3_INTG_DEVICE,
+            COMPUTE_3_IP,
+            PORT_CREATED
+    );
+
+    private static final OpenstackNode COMPUTE_4 = createNode(
+            COMPUTE_4_HOSTNAME,
+            COMPUTE,
+            COMPUTE_4_INTG_DEVICE,
+            COMPUTE_4_IP,
+            COMPLETE
+    );
+
+    private static final OpenstackNode GATEWAY_1 = createNode(
+            GATEWAY_1_HOSTNAME,
+            GATEWAY,
+            GATEWAY_1_INTG_DEVICE,
+            GATEWAY_1_ROUT_DEVICE,
+            GATEWAY_1_IP,
+            INIT
+    );
+
+    private static final OpenstackNode GATEWAY_2 = createNode(
+            GATEWAY_2_HOSTNAME,
+            GATEWAY,
+            GATEWAY_2_INTG_DEVICE,
+            GATEWAY_2_ROUT_DEVICE,
+            GATEWAY_2_IP,
+            DEVICE_CREATED
+    );
+
+    private static final OpenstackNode GATEWAY_3 = createNode(
+            GATEWAY_3_HOSTNAME,
+            GATEWAY,
+            GATEWAY_3_INTG_DEVICE,
+            GATEWAY_3_ROUT_DEVICE,
+            GATEWAY_3_IP,
+            PORT_CREATED
+    );
+
+    private static final OpenstackNode GATEWAY_4 = createNode(
+            GATEWAY_4_HOSTNAME,
+            GATEWAY,
+            GATEWAY_4_INTG_DEVICE,
+            GATEWAY_4_ROUT_DEVICE,
+            GATEWAY_4_IP,
+            COMPLETE
+    );
+
+    private static final TestDeviceService TEST_DEVICE_SERVICE = new TestDeviceService();
+
+    private TestOpenstackNodeManager testNodeManager;
+    private DefaultOpenstackNodeHandler target;
+
+    @Before
+    public void setUp() throws Exception {
+        DeviceAdminService mockDeviceAdminService = createMock(DeviceAdminService.class);
+        mockDeviceAdminService.removeDevice(anyObject());
+        replay(mockDeviceAdminService);
+
+        OvsdbClientService mockOvsdbClient = createMock(OvsdbClientService.class);
+        expect(mockOvsdbClient.isConnected())
+                .andReturn(true)
+                .anyTimes();
+        replay(mockOvsdbClient);
+
+        OvsdbController mockOvsdbController = createMock(OvsdbController.class);
+        expect(mockOvsdbController.getOvsdbClient(anyObject()))
+                .andReturn(mockOvsdbClient)
+                .anyTimes();
+        replay(mockOvsdbController);
+
+        testNodeManager = new TestOpenstackNodeManager();
+        target = new DefaultOpenstackNodeHandler();
+
+        target.coreService = new TestCoreService();
+        target.leadershipService = new TestLeadershipService();
+        target.clusterService = new TestClusterService();
+        target.deviceService = TEST_DEVICE_SERVICE;
+        target.deviceAdminService = mockDeviceAdminService;
+        target.ovsdbController = mockOvsdbController;
+        target.groupService = new TestGroupService();
+        target.osNodeService = testNodeManager;
+        target.osNodeAdminService = testNodeManager;
+        target.componentConfigService = new TestComponentConfigService();
+        TestUtils.setField(target, "eventExecutor", MoreExecutors.newDirectExecutorService());
+        target.activate();
+    }
+
+    @After
+    public void tearDown() {
+        TEST_DEVICE_SERVICE.clear();
+        target.deactivate();
+        target = null;
+        testNodeManager = null;
+    }
+
+    /**
+     * Checks if the compute node state changes from INIT to DEVICE_CREATED
+     * after processing INIT state.
+     */
+    @Test
+    public void testComputeNodeProcessNodeInitState() {
+        testNodeManager.createNode(COMPUTE_1);
+        TEST_DEVICE_SERVICE.devMap.put(COMPUTE_1_OVSDB_DEVICE.id(), COMPUTE_1_OVSDB_DEVICE);
+
+        assertEquals(ERR_STATE_NOT_MATCH, INIT,
+                testNodeManager.node(COMPUTE_1_HOSTNAME).state());
+        target.processInitState(COMPUTE_1);
+        assertEquals(ERR_STATE_NOT_MATCH, DEVICE_CREATED,
+                testNodeManager.node(COMPUTE_1_HOSTNAME).state());
+    }
+
+    /**
+     * Checks if the gateway node state changes from INIT to DEVICE_CREATED
+     * after processing INIT state.
+     */
+    @Test
+    public void testGatewayNodeProcessNodeInitState() {
+        testNodeManager.createNode(GATEWAY_1);
+        TEST_DEVICE_SERVICE.devMap.put(GATEWAY_1_OVSDB_DEVICE.id(), GATEWAY_1_OVSDB_DEVICE);
+
+        assertEquals(ERR_STATE_NOT_MATCH, INIT,
+                testNodeManager.node(GATEWAY_1_HOSTNAME).state());
+        target.processInitState(GATEWAY_1);
+        assertEquals(ERR_STATE_NOT_MATCH, DEVICE_CREATED,
+                testNodeManager.node(GATEWAY_1_HOSTNAME).state());
+    }
+
+    /**
+     * Checks if the compute node state changes from DEVICE_CREATED to
+     * PORT_CREATED after processing DEVICE_CREATED state.
+     */
+    @Test
+    public void testComputeNodeProcessDeviceCreatedState() {
+        testNodeManager.createNode(COMPUTE_2);
+        TEST_DEVICE_SERVICE.devMap.put(COMPUTE_2_OVSDB_DEVICE.id(), COMPUTE_2_OVSDB_DEVICE);
+        TEST_DEVICE_SERVICE.devMap.put(COMPUTE_2_INTG_DEVICE.id(), COMPUTE_2_INTG_DEVICE);
+
+        assertEquals(ERR_STATE_NOT_MATCH, DEVICE_CREATED,
+                testNodeManager.node(COMPUTE_2_HOSTNAME).state());
+        target.processDeviceCreatedState(COMPUTE_2);
+        assertEquals(ERR_STATE_NOT_MATCH, PORT_CREATED,
+                testNodeManager.node(COMPUTE_2_HOSTNAME).state());
+    }
+
+    /**
+     * Checks if the gateway node state changes from DEVICE_CREATED to
+     * PORT_CREATED after processing DEVICE_CREATED state.
+     */
+    @Test
+    public void testGatewayNodeProcessDeviceCreatedState() {
+        testNodeManager.createNode(GATEWAY_2);
+        TEST_DEVICE_SERVICE.devMap.put(GATEWAY_2_OVSDB_DEVICE.id(), GATEWAY_2_OVSDB_DEVICE);
+        TEST_DEVICE_SERVICE.devMap.put(GATEWAY_2_INTG_DEVICE.id(), GATEWAY_2_INTG_DEVICE);
+
+        assertEquals(ERR_STATE_NOT_MATCH, DEVICE_CREATED,
+                testNodeManager.node(GATEWAY_2_HOSTNAME).state());
+        target.processDeviceCreatedState(GATEWAY_2);
+        assertEquals(ERR_STATE_NOT_MATCH, PORT_CREATED,
+                testNodeManager.node(GATEWAY_2_HOSTNAME).state());
+    }
+
+    /**
+     * Checks if the compute node state changes from PORT_CREATED to
+     * COMPLETE after processing PORT_CREATED state.
+     */
+    @Test
+    public void testComputeNodeProcessPortCreatedState() {
+        testNodeManager.createNode(COMPUTE_3);
+        TEST_DEVICE_SERVICE.devMap.put(COMPUTE_3_OVSDB_DEVICE.id(), COMPUTE_3_OVSDB_DEVICE);
+        TEST_DEVICE_SERVICE.devMap.put(COMPUTE_3_INTG_DEVICE.id(), COMPUTE_3_INTG_DEVICE);
+        TEST_DEVICE_SERVICE.portList.add(createPort(COMPUTE_3_INTG_DEVICE, DEFAULT_TUNNEL));
+
+        testNodeManager.createNode(GATEWAY_4);
+        TEST_DEVICE_SERVICE.devMap.put(GATEWAY_4_INTG_DEVICE.id(), GATEWAY_4_INTG_DEVICE);
+
+        assertEquals(ERR_STATE_NOT_MATCH, PORT_CREATED,
+                testNodeManager.node(COMPUTE_3_HOSTNAME).state());
+        target.processPortCreatedState(COMPUTE_3);
+        assertEquals(ERR_STATE_NOT_MATCH, COMPLETE,
+                testNodeManager.node(COMPUTE_3_HOSTNAME).state());
+    }
+
+    /**
+     * Checks if the gateway node state changes from PORT_CREATED to
+     * COMPLETE after processing PORT_CREATED state.
+     */
+    @Test
+    public void testGatewayNodeProcessPortCreatedState() {
+        testNodeManager.createNode(COMPUTE_4);
+        TEST_DEVICE_SERVICE.devMap.put(COMPUTE_4_OVSDB_DEVICE.id(), COMPUTE_4_OVSDB_DEVICE);
+        TEST_DEVICE_SERVICE.devMap.put(COMPUTE_4_INTG_DEVICE.id(), COMPUTE_4_INTG_DEVICE);
+        TEST_DEVICE_SERVICE.portList.add(createPort(COMPUTE_4_INTG_DEVICE, DEFAULT_TUNNEL));
+
+        testNodeManager.createNode(GATEWAY_3);
+        TEST_DEVICE_SERVICE.devMap.put(GATEWAY_3_INTG_DEVICE.id(), GATEWAY_4_INTG_DEVICE);
+
+        assertEquals(ERR_STATE_NOT_MATCH, PORT_CREATED,
+                testNodeManager.node(GATEWAY_3_HOSTNAME).state());
+        target.processPortCreatedState(GATEWAY_3);
+        assertEquals(ERR_STATE_NOT_MATCH, COMPLETE,
+                testNodeManager.node(GATEWAY_3_HOSTNAME).state());
+    }
+
+    /**
+     * Checks if the compute node state changes from COMPLETE to INCOMPLETE
+     * when integration bridge is disconnected.
+     */
+    @Test
+    public void testBackToIncompleteWhenBrIntDisconnected() {
+        testNodeManager.createNode(COMPUTE_4);
+
+        assertEquals(ERR_STATE_NOT_MATCH, COMPLETE,
+                testNodeManager.node(COMPUTE_4_HOSTNAME).state());
+        TEST_DEVICE_SERVICE.removeDevice(COMPUTE_4_INTG_DEVICE);
+        assertEquals(ERR_STATE_NOT_MATCH, INCOMPLETE,
+                testNodeManager.node(COMPUTE_4_HOSTNAME).state());
+    }
+
+    /**
+     * Checks if the compute node state changes from COMPLETE to INCOMPLETE
+     * when vxlan port is removed from integration bridge.
+     */
+    @Test
+    public void testBackToIncompleteWhenVxlanRemoved() {
+        testNodeManager.createNode(COMPUTE_4);
+
+        assertEquals(ERR_STATE_NOT_MATCH, COMPLETE,
+                testNodeManager.node(COMPUTE_4_HOSTNAME).state());
+        TEST_DEVICE_SERVICE.removePort(COMPUTE_4_INTG_DEVICE, createPort(
+                COMPUTE_4_INTG_DEVICE, DEFAULT_TUNNEL));
+        assertEquals(ERR_STATE_NOT_MATCH, INCOMPLETE,
+                testNodeManager.node(COMPUTE_4_HOSTNAME).state());
+
+    }
+
+    private static Device createOvsdbDevice(IpAddress ovsdbIp) {
+        return new TestDevice(new ProviderId("of", "foo"),
+                DeviceId.deviceId("ovsdb:" + ovsdbIp.toString()),
+                CONTROLLER,
+                "manufacturer",
+                "hwVersion",
+                "swVersion",
+                "serialNumber",
+                new ChassisId(1));
+    }
+
+    private static Device createOpenFlowDevice(long devIdNum, String type) {
+        return new TestDevice(new ProviderId("of", "foo"),
+                DeviceId.deviceId(String.format("of:%016d", devIdNum)),
+                SWITCH,
+                type,
+                "hwVersion",
+                "swVersion",
+                "serialNumber",
+                new ChassisId(1));
+    }
+
+    private static Port createPort(Device device, String portName) {
+        return new DefaultPort(device,
+                PortNumber.portNumber(1),
+                true,
+                DefaultAnnotations.builder().set(PORT_NAME, portName).build());
+    }
+
+    private static OpenstackNode createNode(String hostname,
+                                            OpenstackNode.NodeType type,
+                                            Device intgBridge,
+                                            IpAddress ipAddr,
+                                            NodeState state) {
+        return new TestOpenstackNode(
+                hostname,
+                type,
+                intgBridge.id(),
+                null,
+                ipAddr,
+                ipAddr,
+                null, state);
+    }
+
+    private static OpenstackNode createNode(String hostname,
+                                            OpenstackNode.NodeType type,
+                                            Device intgBridge,
+                                            Device routerBridge,
+                                            IpAddress ipAddr,
+                                            NodeState state) {
+        return new TestOpenstackNode(
+                hostname,
+                type,
+                intgBridge.id(),
+                routerBridge.id(),
+                ipAddr,
+                ipAddr,
+                null, state);
+    }
+
+    private static final class TestDevice extends DefaultDevice {
+        private TestDevice(ProviderId providerId,
+                           DeviceId id,
+                           Type type,
+                           String manufacturer,
+                           String hwVersion,
+                           String swVersion,
+                           String serialNumber,
+                           ChassisId chassisId,
+                           Annotations... annotations) {
+            super(providerId,
+                    id,
+                    type,
+                    manufacturer,
+                    hwVersion,
+                    swVersion,
+                    serialNumber,
+                    chassisId,
+                    annotations);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <B extends Behaviour> B as(Class<B> projectionClass) {
+            if (projectionClass.equals(BridgeConfig.class)) {
+                return (B) new TestBridgeConfig();
+            } else if (projectionClass.equals(InterfaceConfig.class)) {
+                return (B) new TestInterfaceConfig();
+            } else if (projectionClass.equals(ExtensionTreatmentResolver.class)) {
+                ExtensionTreatmentResolver treatmentResolver = createMock(ExtensionTreatmentResolver.class);
+                expect(treatmentResolver.getExtensionInstruction(anyObject()))
+                        .andReturn(new TestExtensionTreatment())
+                        .anyTimes();
+                replay(treatmentResolver);
+                return (B) treatmentResolver;
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public <B extends Behaviour> boolean is(Class<B> projectionClass) {
+            return true;
+        }
+    }
+
+    private static final class TestOpenstackNode extends DefaultOpenstackNode {
+        private TestOpenstackNode(String hostname,
+                                  NodeType type,
+                                  DeviceId intgBridge,
+                                  DeviceId routerBridge,
+                                  IpAddress managementIp,
+                                  IpAddress dataIp,
+                                  String vlanIntf,
+                                  NodeState state) {
+            super(hostname,
+                    type,
+                    intgBridge,
+                    routerBridge,
+                    managementIp,
+                    dataIp,
+                    vlanIntf,
+                    state);
+        }
+
+        @Override
+        public PortNumber tunnelPortNum() {
+            return PortNumber.portNumber(1);
+        }
+
+        @Override
+        public PortNumber vlanPortNum() {
+            return PortNumber.portNumber(1);
+        }
+
+        @Override
+        public PortNumber patchPortNum() {
+            return PortNumber.portNumber(1);
+        }
+
+        @Override
+        public MacAddress vlanPortMac() {
+            return MacAddress.NONE;
+        }
+    }
+
+    private static class TestOpenstackNodeManager implements OpenstackNodeService, OpenstackNodeAdminService {
+        Map<String, OpenstackNode> osNodeMap = Maps.newHashMap();
+        List<OpenstackNodeListener> listeners = Lists.newArrayList();
+
+        @Override
+        public Set<OpenstackNode> nodes() {
+            return ImmutableSet.copyOf(osNodeMap.values());
+        }
+
+        @Override
+        public Set<OpenstackNode> nodes(OpenstackNode.NodeType type) {
+            return osNodeMap.values().stream()
+                    .filter(osNode -> osNode.type() == type)
+                    .collect(Collectors.toSet());
+        }
+
+        @Override
+        public Set<OpenstackNode> completeNodes() {
+            return osNodeMap.values().stream()
+                    .filter(osNode -> osNode.state() == COMPLETE)
+                    .collect(Collectors.toSet());
+        }
+
+        @Override
+        public Set<OpenstackNode> completeNodes(OpenstackNode.NodeType type) {
+            return osNodeMap.values().stream()
+                    .filter(osNode -> osNode.type() == type && osNode.state() == COMPLETE)
+                    .collect(Collectors.toSet());
+        }
+
+        @Override
+        public OpenstackNode node(String hostname) {
+            return osNodeMap.get(hostname);
+        }
+
+        @Override
+        public OpenstackNode node(DeviceId deviceId) {
+            return osNodeMap.values().stream()
+                    .filter(osNode -> Objects.equals(osNode.intgBridge(), deviceId) ||
+                            Objects.equals(osNode.ovsdb(), deviceId) ||
+                            Objects.equals(osNode.routerBridge(), deviceId))
+                    .findFirst().orElse(null);
+        }
+
+        @Override
+        public void addListener(OpenstackNodeListener listener) {
+            listeners.add(listener);
+        }
+
+        @Override
+        public void removeListener(OpenstackNodeListener listener) {
+            listeners.remove(listener);
+        }
+
+        @Override
+        public void createNode(OpenstackNode osNode) {
+            osNodeMap.put(osNode.hostname(), osNode);
+        }
+
+        @Override
+        public void updateNode(OpenstackNode osNode) {
+            osNodeMap.put(osNode.hostname(), osNode);
+        }
+
+        @Override
+        public OpenstackNode removeNode(String hostname) {
+            return null;
+        }
+    }
+
+    private static class TestDeviceService extends DeviceServiceAdapter {
+        Map<DeviceId, Device> devMap = Maps.newHashMap();
+        List<Port> portList = Lists.newArrayList();
+        List<DeviceListener> listeners = Lists.newArrayList();
+
+        @Override
+        public void addListener(DeviceListener listener) {
+            listeners.add(listener);
+        }
+
+        @Override
+        public void removeListener(DeviceListener listener) {
+            listeners.remove(listener);
+        }
+
+        @Override
+        public Device getDevice(DeviceId deviceId) {
+            return devMap.get(deviceId);
+        }
+
+        @Override
+        public List<Port> getPorts(DeviceId deviceId) {
+            return this.portList.stream()
+                    .filter(p -> p.element().id().equals(deviceId))
+                    .collect(Collectors.toList());
+        }
+
+        @Override
+        public boolean isAvailable(DeviceId deviceId) {
+            return devMap.containsKey(deviceId);
+        }
+
+        void addDevice(Device device) {
+            devMap.put(device.id(), device);
+            DeviceEvent event = new DeviceEvent(DEVICE_ADDED, device);
+            listeners.stream().filter(l -> l.isRelevant(event)).forEach(l -> l.event(event));
+        }
+
+        void removeDevice(Device device) {
+            devMap.remove(device.id());
+            DeviceEvent event = new DeviceEvent(DEVICE_AVAILABILITY_CHANGED, device);
+            listeners.stream().filter(l -> l.isRelevant(event)).forEach(l -> l.event(event));
+        }
+
+        void addPort(Device device, Port port) {
+            portList.add(port);
+            DeviceEvent event = new DeviceEvent(PORT_ADDED, device, port);
+            listeners.stream().filter(l -> l.isRelevant(event)).forEach(l -> l.event(event));
+        }
+
+        void removePort(Device device, Port port) {
+            portList.remove(port);
+            DeviceEvent event = new DeviceEvent(PORT_REMOVED, device, port);
+            listeners.stream().filter(l -> l.isRelevant(event)).forEach(l -> l.event(event));
+        }
+
+        void clear() {
+            this.listeners.clear();
+            this.devMap.clear();
+            this.portList.clear();
+        }
+    }
+
+    private static class TestBridgeConfig implements BridgeConfig {
+
+        @Override
+        public DriverData data() {
+            return null;
+        }
+
+        @Override
+        public void setData(DriverData data) {
+
+        }
+
+        @Override
+        public DriverHandler handler() {
+            return null;
+        }
+
+        @Override
+        public void setHandler(DriverHandler handler) {
+
+        }
+
+        @Override
+        public void addBridge(BridgeName bridgeName) {
+
+        }
+
+        @Override
+        public void addBridge(BridgeName bridgeName, String dpid, String exPortName) {
+
+        }
+
+        @Override
+        public boolean addBridge(BridgeName bridgeName, String dpid, List<ControllerInfo> controllers) {
+            return false;
+        }
+
+        @Override
+        public boolean addBridge(BridgeDescription bridge) {
+            TEST_DEVICE_SERVICE.addDevice(new DefaultDevice(new ProviderId("of", "foo"),
+                    DeviceId.deviceId("of:" + bridge.datapathId().get()),
+                    SWITCH,
+                    bridge.name(),
+                    "hwVersion",
+                    "swVersion",
+                    "serialNumber",
+                    new ChassisId(1)));
+            return true;
+        }
+
+        @Override
+        public void deleteBridge(BridgeName bridgeName) {
+
+        }
+
+        @Override
+        public Collection<BridgeDescription> getBridges() {
+            return ImmutableSet.of(ROUT_BRIDGE);
+        }
+
+        @Override
+        public void addPort(BridgeName bridgeName, String portName) {
+
+        }
+
+        @Override
+        public void deletePort(BridgeName bridgeName, String portName) {
+
+        }
+
+        @Override
+        public Collection<PortDescription> getPorts() {
+            return ImmutableSet.of(PATCH_ROUT);
+        }
+
+        @Override
+        public Set<PortNumber> getPortNumbers() {
+            return null;
+        }
+
+        @Override
+        public List<PortNumber> getLocalPorts(Iterable<String> ifaceIds) {
+            return null;
+        }
+    }
+
+    private static class TestInterfaceConfig implements InterfaceConfig {
+
+        @Override
+        public DriverData data() {
+            return null;
+        }
+
+        @Override
+        public void setData(DriverData data) {
+
+        }
+
+        @Override
+        public DriverHandler handler() {
+            return null;
+        }
+
+        @Override
+        public void setHandler(DriverHandler handler) {
+
+        }
+
+        @Override
+        public boolean addAccessInterface(DeviceId deviceId, String intf, VlanId vlanId) {
+            return false;
+        }
+
+        @Override
+        public boolean addAccessMode(String intf, VlanId vlanId) {
+            return false;
+        }
+
+        @Override
+        public boolean removeAccessInterface(DeviceId deviceId, String intf) {
+            return false;
+        }
+
+        @Override
+        public boolean removeAccessMode(String intf) {
+            return false;
+        }
+
+        @Override
+        public boolean addTrunkInterface(DeviceId deviceId, String intf, List<VlanId> vlanIds) {
+            return false;
+        }
+
+        @Override
+        public boolean addTrunkMode(String intf, List<VlanId> vlanIds) {
+            return false;
+        }
+
+        @Override
+        public boolean removeTrunkInterface(DeviceId deviceId, String intf) {
+            return false;
+        }
+
+        @Override
+        public boolean removeTrunkMode(String intf) {
+            return false;
+        }
+
+        @Override
+        public boolean addRateLimit(String intf, short limit) {
+            return false;
+        }
+
+        @Override
+        public boolean removeRateLimit(String intf) {
+            return false;
+        }
+
+        @Override
+        public boolean addTunnelMode(String intf, TunnelDescription tunnelDesc) {
+            TEST_DEVICE_SERVICE.devMap.values().stream()
+                    .filter(device -> device.type() == SWITCH &&
+                            device.manufacturer().equals(INTEGRATION_BRIDGE))
+                    .forEach(device -> {
+                        TEST_DEVICE_SERVICE.addPort(device, createPort(device, intf));
+                    });
+            return true;
+        }
+
+        @Override
+        public boolean removeTunnelMode(String intf) {
+            return false;
+        }
+
+        @Override
+        public boolean addPatchMode(String ifaceName, PatchDescription patchInterface) {
+            if (ifaceName.equals(PATCH_INTG_BRIDGE)) {
+                TEST_DEVICE_SERVICE.devMap.values().stream()
+                        .filter(device -> device.type() == SWITCH &&
+                                device.manufacturer().equals(INTEGRATION_BRIDGE))
+                        .forEach(device -> {
+                            TEST_DEVICE_SERVICE.addPort(device, createPort(device, ifaceName));
+                        });
+            } else if (ifaceName.equals(PATCH_ROUT_BRIDGE)) {
+                TEST_DEVICE_SERVICE.devMap.values().stream()
+                        .filter(device -> device.type() == SWITCH &&
+                                device.manufacturer().equals(ROUTER_BRIDGE))
+                        .forEach(device -> {
+                            TEST_DEVICE_SERVICE.addPort(device, createPort(device, ifaceName));
+                        });
+            }
+            return true;
+        }
+
+        @Override
+        public boolean removePatchMode(String ifaceName) {
+            return false;
+        }
+
+        @Override
+        public List<DeviceInterfaceDescription> getInterfaces(DeviceId deviceId) {
+            return null;
+        }
+
+        @Override
+        public List<DeviceInterfaceDescription> getInterfaces() {
+            return null;
+        }
+    }
+
+    private static class TestGroupService implements GroupService {
+        Map<GroupKey, Group> groupMap = Maps.newHashMap();
+        Map<GroupKey, GroupBuckets> groupBucketsMap = Maps.newHashMap();
+        List<GroupListener> listeners = Lists.newArrayList();
+
+        @Override
+        public void addListener(GroupListener listener) {
+            listeners.add(listener);
+        }
+
+        @Override
+        public void removeListener(GroupListener listener) {
+            listeners.remove(listener);
+        }
+
+        @Override
+        public void addGroup(GroupDescription groupDesc) {
+            DefaultGroup group = new DefaultGroup(GroupId.valueOf(groupDesc.givenGroupId()), groupDesc);
+            group.setState(Group.GroupState.ADDED);
+            groupMap.put(groupDesc.appCookie(), group);
+            groupBucketsMap.put(groupDesc.appCookie(), groupDesc.buckets());
+
+            GroupEvent event = new GroupEvent(GroupEvent.Type.GROUP_ADDED, group);
+            listeners.stream().filter(listener -> listener.isRelevant(event))
+                    .forEach(listener -> listener.event(event));
+        }
+
+        @Override
+        public Group getGroup(DeviceId deviceId, GroupKey appCookie) {
+            return groupMap.get(appCookie);
+        }
+
+        @Override
+        public void addBucketsToGroup(DeviceId deviceId, GroupKey oldCookie, GroupBuckets buckets,
+                                      GroupKey newCookie, ApplicationId appId) {
+
+        }
+
+        @Override
+        public void removeBucketsFromGroup(DeviceId deviceId, GroupKey oldCookie, GroupBuckets buckets,
+                                           GroupKey newCookie, ApplicationId appId) {
+
+        }
+
+        @Override
+        public void purgeGroupEntries(DeviceId deviceId) {
+
+        }
+
+        @Override
+        public void removeGroup(DeviceId deviceId, GroupKey appCookie, ApplicationId appId) {
+
+        }
+
+        @Override
+        public Iterable<Group> getGroups(DeviceId deviceId, ApplicationId appId) {
+            return null;
+        }
+
+        @Override
+        public Iterable<Group> getGroups(DeviceId deviceId) {
+            return null;
+        }
+
+        @Override
+        public void setBucketsForGroup(DeviceId deviceId, GroupKey oldCookie, GroupBuckets buckets,
+                                       GroupKey newCookie, ApplicationId appId) {
+            groupBucketsMap.put(newCookie, buckets);
+            GroupEvent event = new GroupEvent(GroupEvent.Type.GROUP_UPDATED, groupMap.get(newCookie));
+            listeners.stream().filter(listener -> listener.isRelevant(event))
+                    .forEach(listener -> listener.event(event));
+        }
+
+    }
+
+    private static class TestExtensionTreatment implements ExtensionTreatment {
+        Ip4Address tunnelDst;
+
+        @Override
+        public ExtensionTreatmentType type() {
+            return null;
+        }
+
+        @Override
+        public <T> void setPropertyValue(String key, T value) throws ExtensionPropertyException {
+            tunnelDst = (Ip4Address) value;
+        }
+
+        @Override
+        public <T> T getPropertyValue(String key) throws ExtensionPropertyException {
+            return null;
+        }
+
+        @Override
+        public List<String> getProperties() {
+            return null;
+        }
+
+        @Override
+        public byte[] serialize() {
+            return new byte[0];
+        }
+
+        @Override
+        public void deserialize(byte[] data) {
+
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            TestExtensionTreatment that = (TestExtensionTreatment) obj;
+            return Objects.equals(tunnelDst, that.tunnelDst);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(tunnelDst);
+        }
+    }
+
+    private static class TestCoreService extends CoreServiceAdapter {
+
+        @Override
+        public ApplicationId getAppId(String name) {
+            return TEST_APP_ID;
+        }
+    }
+
+    private static class TestLeadershipService extends LeadershipServiceAdapter {
+
+        @Override
+        public NodeId getLeader(String path) {
+            return LOCAL_NODE_ID;
+        }
+    }
+
+    private static class TestClusterService extends ClusterServiceAdapter {
+
+        @Override
+        public ControllerNode getLocalNode() {
+            return LOCAL_CTRL;
+        }
+    }
+
+    private class TestComponentConfigService extends ComponentConfigAdapter {
+
+    }
+}
diff --git a/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/DefaultOpenstackNodeTest.java b/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/DefaultOpenstackNodeTest.java
new file mode 100644
index 0000000..fdc7028
--- /dev/null
+++ b/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/DefaultOpenstackNodeTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.openstacknode.impl;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onosproject.net.Device;
+import org.onosproject.openstacknode.api.NodeState;
+import org.onosproject.openstacknode.api.OpenstackNode;
+
+/**
+ * Unit tests for DefaultOpenstackNode.
+ */
+public class DefaultOpenstackNodeTest extends OpenstackNodeTest {
+
+    private static final IpAddress TEST_IP = IpAddress.valueOf("10.100.0.3");
+
+    private static final String HOSTNAME_1 = "hostname_1";
+    private static final String HOSTNAME_2 = "hostname_2";
+    private static final Device DEVICE_1 = createDevice(1);
+    private static final Device DEVICE_2 = createDevice(2);
+    private static final OpenstackNode OS_NODE_1 = createNode(
+            HOSTNAME_1,
+            OpenstackNode.NodeType.COMPUTE,
+            DEVICE_1,
+            TEST_IP,
+            NodeState.INIT);
+    private static final OpenstackNode OS_NODE_2 = createNode(
+            HOSTNAME_1,
+            OpenstackNode.NodeType.COMPUTE,
+            DEVICE_1,
+            TEST_IP,
+            NodeState.COMPLETE);
+    private static final OpenstackNode OS_NODE_3 = createNode(
+            HOSTNAME_2,
+            OpenstackNode.NodeType.COMPUTE,
+            DEVICE_2,
+            TEST_IP,
+            NodeState.INIT);
+
+    /**
+     * Checks equals method works as expected.
+     */
+    @Test
+    public void testEquality() {
+        new EqualsTester().addEqualityGroup(OS_NODE_1, OS_NODE_2)
+                .addEqualityGroup(OS_NODE_3)
+                .testEquals();
+    }
+
+    /**
+     * Checks building a node without hostname fails with proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testBuildWithoutHostname() {
+        DefaultOpenstackNode.builder()
+                .type(OpenstackNode.NodeType.COMPUTE)
+                .intgBridge(DEVICE_1.id())
+                .managementIp(TEST_IP)
+                .dataIp(TEST_IP)
+                .state(NodeState.INIT)
+                .build();
+    }
+
+    /**
+     * Checks building a node without type fails with proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testBuildWithoutType() {
+        DefaultOpenstackNode.builder()
+                .hostname(HOSTNAME_1)
+                .intgBridge(DEVICE_1.id())
+                .managementIp(TEST_IP)
+                .dataIp(TEST_IP)
+                .state(NodeState.INIT)
+                .build();
+    }
+
+    /**
+     * Checks building a node without integration bridge ID fails with
+     * proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testBuildWithoutIntegrationBridgeId() {
+        DefaultOpenstackNode.builder()
+                .hostname(HOSTNAME_1)
+                .type(OpenstackNode.NodeType.COMPUTE)
+                .managementIp(TEST_IP)
+                .dataIp(TEST_IP)
+                .state(NodeState.INIT)
+                .build();
+    }
+
+    /**
+     * Checks building a node without management IP address fails with
+     * proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testBuildWithoutManagementIp() {
+        DefaultOpenstackNode.builder()
+                .hostname(HOSTNAME_1)
+                .type(OpenstackNode.NodeType.COMPUTE)
+                .intgBridge(DEVICE_1.id())
+                .dataIp(TEST_IP)
+                .state(NodeState.INIT)
+                .build();
+    }
+
+    /**
+     * Checks building a node without data IP nor VLAN interface name
+     * fails with proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testBuildWithoutDataIpNorVlanIntf() {
+        DefaultOpenstackNode.builder()
+                .hostname(HOSTNAME_1)
+                .type(OpenstackNode.NodeType.COMPUTE)
+                .intgBridge(DEVICE_1.id())
+                .state(NodeState.INIT)
+                .build();
+    }
+
+    /**
+     * Checks building a gateway type node without router bridge ID
+     * fails with proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testGatewayWithoutRouterBridgeId() {
+        DefaultOpenstackNode.builder()
+                .hostname(HOSTNAME_1)
+                .type(OpenstackNode.NodeType.GATEWAY)
+                .intgBridge(DEVICE_1.id())
+                .managementIp(TEST_IP)
+                .dataIp(TEST_IP)
+                .state(NodeState.INIT)
+                .build();
+    }
+}
diff --git a/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/OpenstackNodeManagerTest.java b/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/OpenstackNodeManagerTest.java
new file mode 100644
index 0000000..69a0f9d
--- /dev/null
+++ b/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/OpenstackNodeManagerTest.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.openstacknode.impl;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.junit.TestUtils;
+import org.onlab.packet.IpAddress;
+import org.onosproject.cluster.ClusterServiceAdapter;
+import org.onosproject.cluster.LeadershipServiceAdapter;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreServiceAdapter;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.event.Event;
+import org.onosproject.net.Device;
+import org.onosproject.openstacknode.api.NodeState;
+import org.onosproject.openstacknode.api.OpenstackNode;
+import org.onosproject.openstacknode.api.OpenstackNodeEvent;
+import org.onosproject.openstacknode.api.OpenstackNodeListener;
+import org.onosproject.store.service.TestStorageService;
+
+import java.util.List;
+import java.util.Objects;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.onosproject.openstacknode.api.OpenstackNode.NodeType.COMPUTE;
+import static org.onosproject.openstacknode.api.OpenstackNode.NodeType.GATEWAY;
+import static org.onosproject.openstacknode.api.OpenstackNodeEvent.Type.OPENSTACK_NODE_COMPLETE;
+import static org.onosproject.openstacknode.api.OpenstackNodeEvent.Type.OPENSTACK_NODE_CREATED;
+import static org.onosproject.openstacknode.api.OpenstackNodeEvent.Type.OPENSTACK_NODE_INCOMPLETE;
+import static org.onosproject.openstacknode.api.OpenstackNodeEvent.Type.OPENSTACK_NODE_REMOVED;
+import static org.onosproject.openstacknode.api.OpenstackNodeEvent.Type.OPENSTACK_NODE_UPDATED;
+
+/**
+ * Unit tests for OpenStack node manager.
+ */
+public class OpenstackNodeManagerTest extends OpenstackNodeTest {
+
+    private static final ApplicationId TEST_APP_ID = new DefaultApplicationId(1, "test");
+
+    private static final String ERR_SIZE = "Number of nodes did not match";
+    private static final String ERR_NOT_MATCH = "Node did not match";
+    private static final String ERR_NOT_FOUND = "Node did not exist";
+
+    private static final String COMPUTE_1_HOSTNAME = "compute_1";
+    private static final String COMPUTE_2_HOSTNAME = "compute_2";
+    private static final String COMPUTE_3_HOSTNAME = "compute_3";
+    private static final String GATEWAY_1_HOSTNAME = "gateway_1";
+
+    private static final Device COMPUTE_1_INTG_DEVICE = createDevice(1);
+    private static final Device COMPUTE_2_INTG_DEVICE = createDevice(2);
+    private static final Device COMPUTE_3_INTG_DEVICE = createDevice(3);
+    private static final Device GATEWAY_1_INTG_DEVICE = createDevice(4);
+    private static final Device GATEWAY_1_ROUT_DEVICE = createDevice(5);
+
+    private static final OpenstackNode COMPUTE_1 = createNode(
+            COMPUTE_1_HOSTNAME,
+            COMPUTE,
+            COMPUTE_1_INTG_DEVICE,
+            IpAddress.valueOf("10.100.0.1"),
+            NodeState.INIT
+    );
+    private static final OpenstackNode COMPUTE_2 = createNode(
+            COMPUTE_2_HOSTNAME,
+            COMPUTE,
+            COMPUTE_2_INTG_DEVICE,
+            IpAddress.valueOf("10.100.0.2"),
+            NodeState.INIT
+    );
+    private static final OpenstackNode COMPUTE_3 = createNode(
+            COMPUTE_3_HOSTNAME,
+            COMPUTE,
+            COMPUTE_3_INTG_DEVICE,
+            IpAddress.valueOf("10.100.0.3"),
+            NodeState.COMPLETE
+    );
+    private static final OpenstackNode GATEWAY_1 = createNode(
+            GATEWAY_1_HOSTNAME,
+            OpenstackNode.NodeType.GATEWAY,
+            GATEWAY_1_INTG_DEVICE,
+            GATEWAY_1_ROUT_DEVICE,
+            IpAddress.valueOf("10.100.0.4"),
+            NodeState.COMPLETE
+    );
+
+    private final TestOpenstackNodeListener testListener = new TestOpenstackNodeListener();
+
+    private OpenstackNodeManager target;
+    private DistributedOpenstackNodeStore osNodeStore;
+
+    @Before
+    public void setUp() {
+        osNodeStore = new DistributedOpenstackNodeStore();
+        TestUtils.setField(osNodeStore, "coreService", new TestCoreService());
+        TestUtils.setField(osNodeStore, "storageService", new TestStorageService());
+        TestUtils.setField(osNodeStore, "eventExecutor", MoreExecutors.newDirectExecutorService());
+        osNodeStore.activate();
+
+        osNodeStore.createNode(COMPUTE_2);
+        osNodeStore.createNode(COMPUTE_3);
+        osNodeStore.createNode(GATEWAY_1);
+
+        target = new org.onosproject.openstacknode.impl.OpenstackNodeManager();
+        target.coreService = new TestCoreService();
+        target.clusterService = new TestClusterService();
+        target.leadershipService = new TestLeadershipService();
+        target.osNodeStore = osNodeStore;
+        target.addListener(testListener);
+        target.activate();
+        testListener.events.clear();
+    }
+
+    @After
+    public void tearDown() {
+        target.removeListener(testListener);
+        target.deactivate();
+        osNodeStore.deactivate();
+        osNodeStore = null;
+        target = null;
+    }
+
+    /**
+     * Checks if creating and removing a node work well with proper events.
+     */
+    @Test
+    public void testCreateAndRemoveNode() {
+        target.createNode(COMPUTE_1);
+        assertEquals(ERR_SIZE, 4, target.nodes().size());
+        assertTrue(target.node(COMPUTE_1_HOSTNAME) != null);
+
+        target.removeNode(COMPUTE_1_HOSTNAME);
+        assertEquals(ERR_SIZE, 3, target.nodes().size());
+        assertTrue(target.node(COMPUTE_1_HOSTNAME) == null);
+
+        validateEvents(OPENSTACK_NODE_CREATED, OPENSTACK_NODE_REMOVED);
+    }
+
+    /**
+     * Checks if creating null node fails with proper exception.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testCreateNullNode() {
+        target.createNode(null);
+    }
+
+    /**
+     * Checks if creating a duplicated node fails with proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateDuplicateNode() {
+        target.createNode(COMPUTE_1);
+        target.createNode(COMPUTE_1);
+    }
+
+    /**
+     * Checks if removing null node fails with proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testRemoveNullNode() {
+        target.removeNode(null);
+    }
+
+    /**
+     * Checks if updating a node works well with proper event.
+     */
+    @Test
+    public void testUpdateNode() {
+        OpenstackNode updated = DefaultOpenstackNode.from(COMPUTE_2)
+                .dataIp(IpAddress.valueOf("10.200.0.100"))
+                .build();
+        target.updateNode(updated);
+        assertEquals(ERR_NOT_MATCH, updated, target.node(COMPUTE_2_INTG_DEVICE.id()));
+        validateEvents(OPENSTACK_NODE_UPDATED);
+    }
+
+    /**
+     * Checks if updating a node state to complete generates proper events.
+     */
+    @Test
+    public void testUpdateNodeStateComplete() {
+        OpenstackNode updated = DefaultOpenstackNode.from(COMPUTE_2)
+                .state(NodeState.COMPLETE)
+                .build();
+        target.updateNode(updated);
+        assertEquals(ERR_NOT_MATCH, updated, target.node(COMPUTE_2_HOSTNAME));
+        validateEvents(OPENSTACK_NODE_UPDATED, OPENSTACK_NODE_COMPLETE);
+    }
+
+    /**
+     * Checks if updating a node state to incomplete generates proper events.
+     */
+    @Test
+    public void testUpdateNodeStateIncomplete() {
+        OpenstackNode updated = DefaultOpenstackNode.from(COMPUTE_3)
+                .state(NodeState.INCOMPLETE)
+                .build();
+        target.updateNode(updated);
+        assertEquals(ERR_NOT_MATCH, updated, target.node(COMPUTE_3_HOSTNAME));
+        validateEvents(OPENSTACK_NODE_UPDATED, OPENSTACK_NODE_INCOMPLETE);
+    }
+
+    /**
+     * Checks if updating a null node fails with proper exception.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testUpdateNullNode() {
+        target.updateNode(null);
+    }
+
+    /**
+     * Checks if updating not existing node fails with proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testUpdateNotExistingNode() {
+        target.updateNode(COMPUTE_1);
+    }
+
+    /**
+     * Checks if getting all nodes method returns correct set of nodes.
+     */
+    @Test
+    public void testGetAllNodes() {
+        assertEquals(ERR_SIZE, 3, target.nodes().size());
+        assertTrue(ERR_NOT_FOUND, target.nodes().contains(COMPUTE_2));
+        assertTrue(ERR_NOT_FOUND, target.nodes().contains(COMPUTE_3));
+        assertTrue(ERR_NOT_FOUND, target.nodes().contains(GATEWAY_1));
+    }
+
+    /**
+     * Checks if getting complete nodes method returns correct set of nodes.
+     */
+    @Test
+    public void testGetCompleteNodes() {
+        assertEquals(ERR_SIZE, 2, target.completeNodes().size());
+        assertTrue(ERR_NOT_FOUND, target.completeNodes().contains(COMPUTE_3));
+        assertTrue(ERR_NOT_FOUND, target.completeNodes().contains(GATEWAY_1));
+    }
+
+    /**
+     * Checks if getting nodes by type method returns correct set of nodes.
+     */
+    @Test
+    public void testGetNodesByType() {
+        assertEquals(ERR_SIZE, 2, target.nodes(COMPUTE).size());
+        assertTrue(ERR_NOT_FOUND, target.nodes(COMPUTE).contains(COMPUTE_2));
+        assertTrue(ERR_NOT_FOUND, target.nodes(COMPUTE).contains(COMPUTE_3));
+
+        assertEquals(ERR_SIZE, 1, target.nodes(GATEWAY).size());
+        assertTrue(ERR_NOT_FOUND, target.nodes(GATEWAY).contains(GATEWAY_1));
+    }
+
+    /**
+     * Checks if getting a node by hostname returns correct node.
+     */
+    @Test
+    public void testGetNodeByHostname() {
+        assertTrue(ERR_NOT_FOUND, Objects.equals(
+                target.node(COMPUTE_2_HOSTNAME), COMPUTE_2));
+        assertTrue(ERR_NOT_FOUND, Objects.equals(
+                target.node(COMPUTE_3_HOSTNAME), COMPUTE_3));
+        assertTrue(ERR_NOT_FOUND, Objects.equals(
+                target.node(GATEWAY_1_HOSTNAME), GATEWAY_1));
+    }
+
+    /**
+     * Checks if getting a node by device ID returns correct node.
+     */
+    @Test
+    public void testGetNodeByDeviceId() {
+        assertTrue(ERR_NOT_FOUND, Objects.equals(
+                target.node(GATEWAY_1_INTG_DEVICE.id()), GATEWAY_1));
+        assertTrue(ERR_NOT_FOUND, Objects.equals(
+                target.node(GATEWAY_1.ovsdb()), GATEWAY_1));
+        assertTrue(ERR_NOT_FOUND, Objects.equals(
+                target.node(GATEWAY_1.routerBridge()), GATEWAY_1));
+    }
+
+    private void validateEvents(Enum... types) {
+        int i = 0;
+        assertEquals("Number of events did not match", types.length, testListener.events.size());
+        for (Event event : testListener.events) {
+            assertEquals("Incorrect event received", types[i], event.type());
+            i++;
+        }
+        testListener.events.clear();
+    }
+
+    private static class TestOpenstackNodeListener implements OpenstackNodeListener {
+        private List<OpenstackNodeEvent> events = Lists.newArrayList();
+
+        @Override
+        public void event(OpenstackNodeEvent event) {
+            events.add(event);
+        }
+    }
+
+    private static class TestCoreService extends CoreServiceAdapter {
+
+        @Override
+        public ApplicationId registerApplication(String name) {
+            return TEST_APP_ID;
+        }
+    }
+
+    private class TestClusterService extends ClusterServiceAdapter {
+
+    }
+
+    private static class TestLeadershipService extends LeadershipServiceAdapter {
+
+    }
+}
\ No newline at end of file
diff --git a/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/OpenstackNodeTest.java b/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/OpenstackNodeTest.java
new file mode 100644
index 0000000..3cb336a
--- /dev/null
+++ b/apps/openstacknode/app/src/test/java/org/onosproject/openstacknode/impl/OpenstackNodeTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.openstacknode.impl;
+
+import org.onlab.packet.ChassisId;
+import org.onlab.packet.IpAddress;
+import org.onosproject.net.DefaultDevice;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.openstacknode.api.NodeState;
+import org.onosproject.openstacknode.api.OpenstackNode;
+import org.onosproject.openstacknode.api.OpenstackNode.NodeType;
+
+import static org.onosproject.net.Device.Type.SWITCH;
+
+/**
+ * Provides a set of test OpenstackNode parameters for use with OpenstackNode related tests.
+ */
+abstract class OpenstackNodeTest {
+
+    protected static Device createDevice(long devIdNum) {
+        return new DefaultDevice(new ProviderId("of", "foo"),
+                DeviceId.deviceId(String.format("of:%016d", devIdNum)),
+                SWITCH,
+                "manufacturer",
+                "hwVersion",
+                "swVersion",
+                "serialNumber",
+                new ChassisId(1));
+    }
+
+    protected static OpenstackNode createNode(String hostname, NodeType type,
+                                              Device intgBridge, IpAddress ipAddr,
+                                              NodeState state) {
+        return org.onosproject.openstacknode.impl.DefaultOpenstackNode.builder()
+                .hostname(hostname)
+                .type(type)
+                .intgBridge(intgBridge.id())
+                .managementIp(ipAddr)
+                .dataIp(ipAddr)
+                .state(state)
+                .build();
+    }
+
+    protected static OpenstackNode createNode(String hostname, NodeType type,
+                                              Device intgBridge, Device routerBridge,
+                                              IpAddress ipAddr, NodeState state) {
+        return org.onosproject.openstacknode.impl.DefaultOpenstackNode.builder()
+                .hostname(hostname)
+                .type(type)
+                .intgBridge(intgBridge.id())
+                .routerBridge(routerBridge.id())
+                .managementIp(ipAddr)
+                .dataIp(ipAddr)
+                .state(state)
+                .build();
+    }
+}