Initial support for multi kubernetes clusters for k8s nodes

Change-Id: I6ca132898f8e157e0583de38a637fdc135f21d6f
(cherry picked from commit e2a04cedde73618ef24575e70cb221e03854de1d)
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/HostNodesInfoJsonMatcher.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/HostNodesInfoJsonMatcher.java
new file mode 100644
index 0000000..5de7a71
--- /dev/null
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/HostNodesInfoJsonMatcher.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2020-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.k8snode.codec;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+import org.onosproject.k8snode.api.HostNodesInfo;
+
+/**
+ * Hamcrest matcher for HostNodesInfo.
+ */
+public final class HostNodesInfoJsonMatcher extends TypeSafeDiagnosingMatcher<JsonNode> {
+
+    private final HostNodesInfo hostNodesInfo;
+
+    private static final String HOST_IP = "hostIp";
+    private static final String NODES = "nodes";
+
+    private HostNodesInfoJsonMatcher(HostNodesInfo hostNodesInfo) {
+        this.hostNodesInfo = hostNodesInfo;
+    }
+
+    @Override
+    protected boolean matchesSafely(JsonNode jsonNode, Description description) {
+
+        // check host IP
+        String jsonHostIp = jsonNode.get(HOST_IP).asText();
+        String hostIp = hostNodesInfo.hostIp().toString();
+        if (!jsonHostIp.equals(hostIp)) {
+            description.appendText("host IP was " + jsonHostIp);
+            return false;
+        }
+
+        // check nodes
+        JsonNode jsonNodes = jsonNode.get(NODES);
+        if (jsonNodes.size() != hostNodesInfo.nodes().size()) {
+            description.appendText("Nodes size was " + jsonNodes.size());
+            return false;
+        }
+
+        boolean nodeFound = true;
+        ArrayNode jsonNodeArray = (ArrayNode) jsonNodes;
+        for (JsonNode jsonNodeTmp : jsonNodeArray) {
+            if (!hostNodesInfo.nodes().contains(jsonNodeTmp.asText())) {
+                nodeFound = false;
+            }
+        }
+
+        if (!nodeFound) {
+            description.appendText("Node not found");
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText(hostNodesInfo.toString());
+    }
+
+    /**
+     * Factory to allocate a hostNodesInfo matcher.
+     *
+     * @param info host IP address to nodes mapping info
+     * @return matcher
+     */
+    public static HostNodesInfoJsonMatcher matchesHostNodesInfo(HostNodesInfo info) {
+        return new HostNodesInfoJsonMatcher(info);
+    }
+}
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigCodecTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigCodecTest.java
index cca918e..b5deb38 100644
--- a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigCodecTest.java
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigCodecTest.java
@@ -18,20 +18,25 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableSet;
 import org.junit.Before;
 import org.junit.Test;
 import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
 import org.onosproject.codec.CodecContext;
 import org.onosproject.codec.JsonCodec;
 import org.onosproject.codec.impl.CodecManager;
 import org.onosproject.core.CoreService;
+import org.onosproject.k8snode.api.DefaultHostNodesInfo;
 import org.onosproject.k8snode.api.DefaultK8sApiConfig;
+import org.onosproject.k8snode.api.HostNodesInfo;
 import org.onosproject.k8snode.api.K8sApiConfig;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 import static junit.framework.TestCase.assertEquals;
 import static org.easymock.EasyMock.createMock;
@@ -51,6 +56,7 @@
     MockCodecContext context;
 
     JsonCodec<K8sApiConfig> k8sApiConfigCodec;
+    JsonCodec<HostNodesInfo> hostNodesInfoCodec;
 
     final CoreService mockCoreService = createMock(CoreService.class);
     private static final String REST_APP_ID = "org.onosproject.rest";
@@ -62,8 +68,10 @@
     public void setUp() {
         context = new MockCodecContext();
         k8sApiConfigCodec = new K8sApiConfigCodec();
+        hostNodesInfoCodec = new HostNodesInfoCodec();
 
         assertThat(k8sApiConfigCodec, notNullValue());
+        assertThat(hostNodesInfoCodec, notNullValue());
 
         expect(mockCoreService.registerApplication(REST_APP_ID))
                 .andReturn(APP_ID).anyTimes();
@@ -76,7 +84,16 @@
      */
     @Test
     public void testK8sApiConfigEncode() {
+        HostNodesInfo info = new DefaultHostNodesInfo.Builder()
+                .hostIp(IpAddress.valueOf("192.168.10.10"))
+                .nodes(ImmutableSet.of("master", "worker"))
+                .build();
+
         K8sApiConfig config = DefaultK8sApiConfig.builder()
+                .clusterName("kubernetes")
+                .segmentId(1)
+                .extNetworkCidr(IpPrefix.valueOf("192.168.200.0/24"))
+                .mode(K8sApiConfig.Mode.NORMAL)
                 .scheme(K8sApiConfig.Scheme.HTTPS)
                 .ipAddress(IpAddress.valueOf("10.10.10.23"))
                 .port(6443)
@@ -85,6 +102,7 @@
                 .caCertData("caCertData")
                 .clientCertData("clientCertData")
                 .clientKeyData("clientKeyData")
+                .infos(ImmutableSet.of(info))
                 .build();
 
         ObjectNode configJson = k8sApiConfigCodec.encode(config, context);
@@ -100,6 +118,10 @@
     public void testK8sApiConfigDecode() throws IOException {
         K8sApiConfig config = getK8sApiConfig("K8sApiConfig.json");
 
+        assertEquals("kubernetes", config.clusterName());
+        assertEquals(1, config.segmentId());
+        assertEquals("192.168.200.0/24", config.extNetworkCidr().toString());
+        assertEquals("NORMAL", config.mode().name());
         assertEquals("HTTPS", config.scheme().name());
         assertEquals("10.134.34.223", config.ipAddress().toString());
         assertEquals(6443, config.port());
@@ -107,6 +129,9 @@
         assertEquals("caCertData", config.caCertData());
         assertEquals("clientCertData", config.clientCertData());
         assertEquals("clientKeyData", config.clientKeyData());
+
+        Set<HostNodesInfo> infos = config.infos();
+        assertEquals(1, infos.size());
     }
 
     private K8sApiConfig getK8sApiConfig(String resourceName) throws IOException {
@@ -141,6 +166,9 @@
             if (entityClass == K8sApiConfig.class) {
                 return (JsonCodec<T>) k8sApiConfigCodec;
             }
+            if (entityClass == HostNodesInfo.class) {
+                return (JsonCodec<T>) hostNodesInfoCodec;
+            }
             return manager.getCodec(entityClass);
         }
 
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigJsonMatcher.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigJsonMatcher.java
index 2eb42b9..7ec3866 100644
--- a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigJsonMatcher.java
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigJsonMatcher.java
@@ -18,6 +18,7 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import org.hamcrest.Description;
 import org.hamcrest.TypeSafeDiagnosingMatcher;
+import org.onosproject.k8snode.api.HostNodesInfo;
 import org.onosproject.k8snode.api.K8sApiConfig;
 
 /**
@@ -27,6 +28,10 @@
 
     private final K8sApiConfig k8sApiConfig;
 
+    private static final String CLUSTER_NAME = "clusterName";
+    private static final String SEGMENT_ID = "segmentId";
+    private static final String EXT_NETWORK_CIDR = "extNetworkCidr";
+    private static final String MODE = "mode";
     private static final String SCHEME = "scheme";
     private static final String IP_ADDRESS = "ipAddress";
     private static final String PORT = "port";
@@ -35,6 +40,7 @@
     private static final String CA_CERT_DATA = "caCertData";
     private static final String CLIENT_CERT_DATA = "clientCertData";
     private static final String CLIENT_KEY_DATA = "clientKeyData";
+    private static final String HOST_NODES_INFO = "hostNodesInfo";
 
     private K8sApiConfigJsonMatcher(K8sApiConfig k8sApiConfig) {
         this.k8sApiConfig = k8sApiConfig;
@@ -43,6 +49,40 @@
     @Override
     protected boolean matchesSafely(JsonNode jsonNode, Description description) {
 
+        // check cluster name
+        String jsonClusterName = jsonNode.get(CLUSTER_NAME).asText();
+        String clusterName = k8sApiConfig.clusterName();
+        if (!jsonClusterName.equals(clusterName)) {
+            description.appendText("cluster name was " + jsonClusterName);
+            return false;
+        }
+
+        // check segment ID
+        int jsonSegmentId = jsonNode.get(SEGMENT_ID).asInt();
+        int segmentId = k8sApiConfig.segmentId();
+        if (jsonSegmentId != segmentId) {
+            description.appendText("Segment ID was " + jsonSegmentId);
+            return false;
+        }
+
+        // check mode
+        String jsonMode = jsonNode.get(MODE).asText();
+        String mode = k8sApiConfig.mode().name();
+        if (!jsonMode.equals(mode)) {
+            description.appendText("mode was " + jsonMode);
+            return false;
+        }
+
+        // check external network CIDR
+        JsonNode jsonCidr = jsonNode.get(EXT_NETWORK_CIDR);
+        String cidr = k8sApiConfig.extNetworkCidr().toString();
+        if (jsonCidr != null) {
+            if (!jsonCidr.asText().equals(cidr)) {
+                description.appendText("External network CIDR was " + jsonCidr);
+                return false;
+            }
+        }
+
         // check scheme
         String jsonScheme = jsonNode.get(SCHEME).asText();
         String scheme = k8sApiConfig.scheme().name();
@@ -119,6 +159,29 @@
             }
         }
 
+        // check hostNodesInfo size
+        JsonNode jsonInfos = jsonNode.get(HOST_NODES_INFO);
+        if (jsonInfos.size() != k8sApiConfig.infos().size()) {
+            description.appendText("Info size was " + jsonInfos.size());
+            return false;
+        }
+
+        // check info
+        for (HostNodesInfo info : k8sApiConfig.infos()) {
+            boolean infoFound = false;
+            for (int infoIndex = 0; infoIndex < jsonInfos.size(); infoIndex++) {
+                HostNodesInfoJsonMatcher infoMatcher = HostNodesInfoJsonMatcher.matchesHostNodesInfo(info);
+                if  (infoMatcher.matches(jsonInfos.get(infoIndex))) {
+                    infoFound = true;
+                    break;
+                }
+            }
+            if (!infoFound) {
+                description.appendText("Info not found " + info.toString());
+                return false;
+            }
+        }
+
         return true;
     }
 
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sHostCodecTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sHostCodecTest.java
new file mode 100644
index 0000000..c7cb041
--- /dev/null
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sHostCodecTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2020-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.k8snode.codec;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableSet;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.codec.impl.CodecManager;
+import org.onosproject.core.CoreService;
+import org.onosproject.k8snode.api.DefaultK8sHost;
+import org.onosproject.k8snode.api.K8sHost;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.onosproject.k8snode.api.K8sHostState.INIT;
+import static org.onosproject.k8snode.codec.K8sHostJsonMatcher.matchesK8sHost;
+import static org.onosproject.net.NetTestTools.APP_ID;
+
+public class K8sHostCodecTest {
+
+    MockCodecContext context;
+
+    JsonCodec<K8sHost> k8sHostCodec;
+
+    final CoreService mockCoreService = createMock(CoreService.class);
+    private static final String REST_APP_ID = "org.onosproject.rest";
+
+    /**
+     * Initial setup for this unit test.
+     */
+    @Before
+    public void setUp() {
+        context = new MockCodecContext();
+        k8sHostCodec = new K8sHostCodec();
+
+        assertThat(k8sHostCodec, notNullValue());
+
+        expect(mockCoreService.registerApplication(REST_APP_ID))
+                .andReturn(APP_ID).anyTimes();
+        replay(mockCoreService);
+        context.registerService(CoreService.class, mockCoreService);
+    }
+
+    /**
+     * Tests the kubernetes host encoding.
+     */
+    @Test
+    public void testK8sHostEncode() {
+        K8sHost host = DefaultK8sHost.builder()
+                .hostIp(IpAddress.valueOf("192.168.200.10"))
+                .state(INIT)
+                .nodeNames(ImmutableSet.of("1", "2"))
+                .build();
+
+        ObjectNode hostJson = k8sHostCodec.encode(host, context);
+        assertThat(hostJson, matchesK8sHost(host));
+    }
+
+    /**
+     * Tests the kubernetes host decoding.
+     */
+    @Test
+    public void testK8sHostDecode() throws IOException {
+        K8sHost host = getK8sHost("K8sHost.json");
+
+        assertEquals("192.168.200.10", host.hostIp().toString());
+        assertEquals("INIT", host.state().name());
+    }
+
+    private K8sHost getK8sHost(String resourceName) throws IOException {
+        InputStream jsonStream = K8sHostCodecTest.class.getResourceAsStream(resourceName);
+        JsonNode json = context.mapper().readTree(jsonStream);
+        assertThat(json, notNullValue());
+        K8sHost host = k8sHostCodec.decode((ObjectNode) json, context);
+        assertThat(host, notNullValue());
+        return host;
+    }
+
+    private class MockCodecContext implements CodecContext {
+        private final ObjectMapper mapper = new ObjectMapper();
+        private final CodecManager manager = new CodecManager();
+        private final Map<Class<?>, Object> services = new HashMap<>();
+
+        /**
+         * Constructs a new mock codec context.
+         */
+        public MockCodecContext() {
+            manager.activate();
+        }
+
+        @Override
+        public ObjectMapper mapper() {
+            return mapper;
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> JsonCodec<T> codec(Class<T> entityClass) {
+            if (entityClass == K8sHost.class) {
+                return (JsonCodec<T>) k8sHostCodec;
+            }
+            return manager.getCodec(entityClass);
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public <T> T getService(Class<T> serviceClass) {
+            return (T) services.get(serviceClass);
+        }
+
+        // for registering mock services
+        public <T> void registerService(Class<T> serviceClass, T impl) {
+            services.put(serviceClass, impl);
+        }
+    }
+}
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sHostJsonMatcher.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sHostJsonMatcher.java
new file mode 100644
index 0000000..4a0cf73
--- /dev/null
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sHostJsonMatcher.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2020-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.k8snode.codec;
+
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+import org.onosproject.k8snode.api.K8sHost;
+
+/**
+ * Hamcrest matcher for kubernetes host.
+ */
+public final class K8sHostJsonMatcher extends TypeSafeDiagnosingMatcher<JsonNode> {
+
+    private final K8sHost host;
+
+    private static final String HOST_IP = "hostIp";
+    private static final String NODE_NAMES = "nodeNames";
+    private static final String STATE = "state";
+
+    private K8sHostJsonMatcher(K8sHost host) {
+        this.host = host;
+    }
+
+    @Override
+    protected boolean matchesSafely(JsonNode jsonNode, Description description) {
+
+        // check host IP
+        String jsonHostIp = jsonNode.get(HOST_IP).asText();
+        String hostIp = host.hostIp().toString();
+        if (!jsonHostIp.equals(hostIp)) {
+            description.appendText("host IP was " + jsonHostIp);
+            return false;
+        }
+
+        // check state
+        String jsonState = jsonNode.get(STATE).asText();
+        String state = host.state().name();
+        if (!jsonState.equals(state)) {
+            description.appendText("state was " + jsonState);
+            return false;
+        }
+
+        // check node names size
+        JsonNode jsonNames = jsonNode.get(NODE_NAMES);
+        if (jsonNames.size() != host.nodeNames().size()) {
+            description.appendText("Node names size was " + jsonNames.size());
+            return false;
+        }
+
+        // check node names
+        for (String name : host.nodeNames()) {
+            boolean nameFound = false;
+            for (int nameIndex = 0; nameIndex < jsonNames.size(); nameIndex++) {
+                if (name.equals(jsonNames.get(nameIndex).asText())) {
+                    nameFound = true;
+                    break;
+                }
+            }
+
+            if (!nameFound) {
+                description.appendText("Name not found " + name);
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText(host.toString());
+    }
+
+    /**
+     * Factory to allocate an k8s host matcher.
+     *
+     * @param host k8s host object we are looking for
+     * @return matcher
+     */
+    public static K8sHostJsonMatcher matchesK8sHost(K8sHost host) {
+        return new K8sHostJsonMatcher(host);
+    }
+}
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sNodeCodecTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sNodeCodecTest.java
index 556e462..c6861be 100644
--- a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sNodeCodecTest.java
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sNodeCodecTest.java
@@ -78,8 +78,10 @@
     @Test
     public void testK8sMinionNodeEncode() {
         K8sNode node = DefaultK8sNode.builder()
+                .clusterName("kubernetes")
                 .hostname("minion")
                 .type(K8sNode.Type.MINION)
+                .segmentId(100)
                 .state(K8sNodeState.INIT)
                 .managementIp(IpAddress.valueOf("10.10.10.1"))
                 .dataIp(IpAddress.valueOf("20.20.20.2"))
@@ -102,8 +104,10 @@
     public void testK8sMinionNodeDecode() throws IOException {
         K8sNode node = getK8sNode("K8sMinionNode.json");
 
+        assertEquals("kubernetes", node.clusterName());
         assertEquals("minion", node.hostname());
         assertEquals("MINION", node.type().name());
+        assertEquals(100, node.segmentId());
         assertEquals("172.16.130.4", node.managementIp().toString());
         assertEquals("172.16.130.4", node.dataIp().toString());
         assertEquals("of:00000000000000a1", node.intgBridge().toString());
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sNodeJsonMatcher.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sNodeJsonMatcher.java
index b953f54..816ca76 100644
--- a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sNodeJsonMatcher.java
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sNodeJsonMatcher.java
@@ -28,8 +28,10 @@
 
     private final K8sNode node;
 
+    private static final String CLUSTER_NAME = "clusterName";
     private static final String HOSTNAME = "hostname";
     private static final String TYPE = "type";
+    private static final String SEGMENT_ID = "segmentId";
     private static final String MANAGEMENT_IP = "managementIp";
     private static final String DATA_IP = "dataIp";
     private static final String INTEGRATION_BRIDGE = "integrationBridge";
@@ -45,6 +47,14 @@
     @Override
     protected boolean matchesSafely(JsonNode jsonNode, Description description) {
 
+        // check cluster name
+        String jsonClusterName = jsonNode.get(CLUSTER_NAME).asText();
+        String clusterName = node.clusterName();
+        if (!jsonClusterName.equals(clusterName)) {
+            description.appendText("cluster name was " + jsonClusterName);
+            return false;
+        }
+
         // check hostname
         String jsonHostname = jsonNode.get(HOSTNAME).asText();
         String hostname = node.hostname();
@@ -61,6 +71,16 @@
             return false;
         }
 
+        // check segment ID
+        JsonNode jsonSegmentId = jsonNode.get(SEGMENT_ID);
+        if (jsonSegmentId != null) {
+            int segmentId = jsonSegmentId.asInt();
+            if (segmentId != node.segmentId()) {
+                description.appendText("segment ID was " + segmentId);
+                return false;
+            }
+        }
+
         // check management IP
         String jsonMgmtIp = jsonNode.get(MANAGEMENT_IP).asText();
         String mgmtIp = node.managementIp().toString();
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/ExternalNetworkManagerTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/ExternalNetworkManagerTest.java
new file mode 100644
index 0000000..f236491
--- /dev/null
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/ExternalNetworkManagerTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.k8snode.impl;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreServiceAdapter;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.store.service.TestStorageService;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Unit tests for external network manager.
+ */
+public class ExternalNetworkManagerTest {
+
+    private static final ApplicationId TEST_APP_ID = new DefaultApplicationId(1, "test");
+
+    private ExternalNetworkManager target;
+
+    /**
+     * Initial setup for this unit test.
+     */
+    @Before
+    public void setUp() {
+        target = new ExternalNetworkManager();
+
+        target.coreService = new TestCoreService();
+        target.storageService = new TestStorageService();
+        target.activate();
+    }
+
+    /**
+     * Clean up unit test.
+     */
+    @After
+    public void tearDown() {
+        target.deactivate();
+        target = null;
+    }
+
+    /**
+     * Checks if creating and removing a config work well with proper events.
+     */
+    @Test
+    public void testObtainGatewayIp() {
+        IpPrefix cidr = IpPrefix.valueOf("192.168.200.0/24");
+        target.registerNetwork(cidr);
+
+        assertEquals(target.getGatewayIp(cidr), IpAddress.valueOf("192.168.200.1"));
+    }
+
+    @Test
+    public void testAllocateReleaseIp() {
+        IpPrefix cidr = IpPrefix.valueOf("192.168.200.0/24");
+        target.registerNetwork(cidr);
+        IpAddress ip = target.allocateIp(cidr);
+        assertEquals(251, target.getAllIps(cidr).size());
+
+        target.releaseIp(cidr, ip);
+        assertEquals(252, target.getAllIps(cidr).size());
+    }
+
+    private static class TestCoreService extends CoreServiceAdapter {
+        @Override
+        public ApplicationId registerApplication(String name) {
+            return TEST_APP_ID;
+        }
+    }
+}
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sApiConfigManagerTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sApiConfigManagerTest.java
index f46feb8..badfced 100644
--- a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sApiConfigManagerTest.java
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sApiConfigManagerTest.java
@@ -72,12 +72,18 @@
     public void setUp() {
 
         apiConfig1 = DefaultK8sApiConfig.builder()
+                .clusterName("kubernetes1")
+                .segmentId(1)
+                .mode(K8sApiConfig.Mode.NORMAL)
                 .scheme(K8sApiConfig.Scheme.HTTP)
                 .ipAddress(IpAddress.valueOf("10.10.10.2"))
                 .port(6443)
                 .state(DISCONNECTED)
                 .build();
         apiConfig2 = DefaultK8sApiConfig.builder()
+                .clusterName("kubernetes2")
+                .segmentId(2)
+                .mode(K8sApiConfig.Mode.NORMAL)
                 .scheme(K8sApiConfig.Scheme.HTTPS)
                 .ipAddress(IpAddress.valueOf("10.10.10.3"))
                 .port(6443)
@@ -88,6 +94,9 @@
                 .clientKeyData("clientKeyData")
                 .build();
         apiConfig3 = DefaultK8sApiConfig.builder()
+                .clusterName("kubernetes3")
+                .segmentId(3)
+                .mode(K8sApiConfig.Mode.PASSTHROUGH)
                 .scheme(K8sApiConfig.Scheme.HTTP)
                 .ipAddress(IpAddress.valueOf("10.10.10.4"))
                 .port(8080)
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sHostManagerTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sHostManagerTest.java
new file mode 100644
index 0000000..3374672
--- /dev/null
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sHostManagerTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2020-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.k8snode.impl;
+
+import com.google.common.collect.ImmutableSet;
+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.k8snode.api.DefaultK8sHost;
+import org.onosproject.k8snode.api.K8sHost;
+import org.onosproject.k8snode.api.K8sHostEvent;
+import org.onosproject.k8snode.api.K8sHostListener;
+import org.onosproject.k8snode.api.K8sHostState;
+import org.onosproject.store.service.TestStorageService;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.onosproject.k8snode.api.K8sHostEvent.Type.K8S_HOST_COMPLETE;
+import static org.onosproject.k8snode.api.K8sHostEvent.Type.K8S_HOST_CREATED;
+import static org.onosproject.k8snode.api.K8sHostEvent.Type.K8S_HOST_REMOVED;
+import static org.onosproject.k8snode.api.K8sHostEvent.Type.K8S_HOST_UPDATED;
+import static org.onosproject.k8snode.api.K8sHostEvent.Type.K8S_NODES_ADDED;
+import static org.onosproject.k8snode.api.K8sHostEvent.Type.K8S_NODES_REMOVED;
+import static org.onosproject.k8snode.api.K8sHostState.COMPLETE;
+import static org.onosproject.k8snode.api.K8sHostState.INIT;
+
+/**
+ * Unit tests for kubernetes host manager.
+ */
+public class K8sHostManagerTest {
+
+    private static final ApplicationId TEST_APP_ID = new DefaultApplicationId(1, "test");
+
+    private static final String ERR_SIZE = "Number of hosts did not match";
+    private static final String ERR_NOT_MATCH = "Host did not match";
+    private static final String ERR_NOT_FOUND = "Host did not exist";
+
+    private static final IpAddress HOST_IP_1 = IpAddress.valueOf("192.168.100.2");
+    private static final IpAddress HOST_IP_2 = IpAddress.valueOf("192.168.101.2");
+    private static final IpAddress HOST_IP_3 = IpAddress.valueOf("192.168.102.2");
+
+    private static final K8sHost HOST_1 = createHost(
+            HOST_IP_1,
+            INIT,
+            ImmutableSet.of("1", "2")
+    );
+
+    private static final K8sHost HOST_2 = createHost(
+            HOST_IP_2,
+            INIT,
+            ImmutableSet.of("3", "4")
+    );
+
+    private static final K8sHost HOST_3 = createHost(
+            HOST_IP_3,
+            COMPLETE,
+            ImmutableSet.of("5", "6")
+    );
+
+    private final TestK8sHostListener testListener = new TestK8sHostListener();
+
+    private K8sHostManager target;
+    private DistributedK8sHostStore hostStore;
+
+    /**
+     * Initial setup for this unit test.
+     */
+    @Before
+    public void setUp() {
+        hostStore = new DistributedK8sHostStore();
+        TestUtils.setField(hostStore, "coreService", new TestCoreService());
+        TestUtils.setField(hostStore, "storageService", new TestStorageService());
+        TestUtils.setField(hostStore, "eventExecutor", MoreExecutors.newDirectExecutorService());
+        hostStore.activate();
+
+        hostStore.createHost(HOST_2);
+        hostStore.createHost(HOST_3);
+
+        target = new K8sHostManager();
+        target.storageService = new TestStorageService();
+        target.coreService = new TestCoreService();
+        target.clusterService = new TestClusterService();
+        target.leadershipService = new TestLeadershipService();
+        target.hostStore = hostStore;
+        target.addListener(testListener);
+        target.activate();
+        testListener.events.clear();
+    }
+
+    /**
+     * Clean up unit test.
+     */
+    @After
+    public void tearDown() {
+        target.removeListener(testListener);
+        target.deactivate();
+        hostStore.deactivate();
+        hostStore = null;
+        target = null;
+    }
+
+    /**
+     * Checks if creating and removing a host work well with proper events.
+     */
+    @Test
+    public void testCreateAndRemoveHost() {
+        target.createHost(HOST_1);
+        assertEquals(ERR_SIZE, 3, target.hosts().size());
+        assertNotNull(target.host(HOST_IP_1));
+
+        target.removeHost(HOST_IP_1);
+        assertEquals(ERR_SIZE, 2, target.hosts().size());
+        assertNull(target.host(HOST_IP_1));
+
+        validateEvents(K8S_HOST_CREATED, K8S_HOST_REMOVED);
+    }
+
+    /**
+     * Checks if creating null host fails with proper exception.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testCreateNullHost() {
+        target.createHost(null);
+    }
+
+    /**
+     * Checks if creating a duplicated host fails with proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateDuplicateHost() {
+        target.createHost(HOST_1);
+        target.createHost(HOST_1);
+    }
+
+    /**
+     * Checks if removing null host fails with proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testRemoveNullHost() {
+        target.removeHost(null);
+    }
+
+    /**
+     * Checks if updating a host works well with proper event.
+     */
+    @Test
+    public void testUpdateHost() {
+        K8sHost updated = HOST_2.updateState(COMPLETE);
+        target.updateHost(updated);
+        validateEvents(K8S_HOST_UPDATED, K8S_HOST_COMPLETE);
+    }
+
+    /**
+     * Checks if updating a null host fails with proper exception.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testUpdateNullHost() {
+        target.updateHost(null);
+    }
+
+    /**
+     * Checks if updating not existing host fails with proper exception.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testUpdateNotExistingHost() {
+        target.updateHost(HOST_1);
+    }
+
+    /**
+     * Checks if adding nodes into host works well with proper event.
+     */
+    @Test
+    public void testAddNodesToHost() {
+        K8sHost updated = HOST_2.updateNodeNames(ImmutableSet.of("3", "4", "5"));
+        target.updateHost(updated);
+        validateEvents(K8S_HOST_UPDATED, K8S_NODES_ADDED);
+    }
+
+    /**
+     * Checks if removing nodes from host works well with proper event.
+     */
+    @Test
+    public void testRemoveNodesFromHost() {
+        K8sHost updated = HOST_2.updateNodeNames(ImmutableSet.of("3"));
+        target.updateHost(updated);
+        validateEvents(K8S_HOST_UPDATED, K8S_NODES_REMOVED);
+    }
+
+    /**
+     * Checks if getting all hosts method returns correct set of nodes.
+     */
+    @Test
+    public void testGetAllHosts() {
+        assertEquals(ERR_SIZE, 2, target.hosts().size());
+        assertTrue(ERR_NOT_FOUND, target.hosts().contains(HOST_2));
+        assertTrue(ERR_NOT_FOUND, target.hosts().contains(HOST_3));
+    }
+
+    /**
+     * Checks if getting complete hosts method returns correct set of nodes.
+     */
+    @Test
+    public void testGetCompleteHosts() {
+        assertEquals(ERR_SIZE, 1, target.completeHosts().size());
+        assertTrue(ERR_NOT_FOUND, target.completeHosts().contains(HOST_3));
+    }
+
+    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 TestK8sHostListener implements K8sHostListener {
+        private List<K8sHostEvent> events = Lists.newArrayList();
+
+        @Override
+        public void event(K8sHostEvent 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 {
+
+    }
+
+    private static K8sHost createHost(IpAddress hostIp, K8sHostState state, Set<String> nodeNames) {
+        return DefaultK8sHost.builder()
+                .hostIp(hostIp)
+                .nodeNames(nodeNames)
+                .state(state)
+                .build();
+    }
+}
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sNodeManagerTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sNodeManagerTest.java
index 50d769f..0bb0258 100644
--- a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sNodeManagerTest.java
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sNodeManagerTest.java
@@ -68,6 +68,8 @@
     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 CLUSTER_NAME = "kubernetes";
+
     private static final String MINION_1_HOSTNAME = "minion_1";
     private static final String MINION_2_HOSTNAME = "minion_2";
     private static final String MINION_3_HOSTNAME = "minion_3";
@@ -84,31 +86,41 @@
     private static final Device MINION_2_LOCAL_DEVICE = createDevice(8);
     private static final Device MINION_3_LOCAL_DEVICE = createDevice(9);
 
+    private static final Device MINION_1_TUN_DEVICE = createDevice(10);
+    private static final Device MINION_2_TUN_DEVICE = createDevice(11);
+    private static final Device MINION_3_TUN_DEVICE = createDevice(12);
+
 
     private static final K8sNode MINION_1 = createNode(
+            CLUSTER_NAME,
             MINION_1_HOSTNAME,
             MINION,
             MINION_1_INTG_DEVICE,
             MINION_1_EXT_DEVICE,
             MINION_1_LOCAL_DEVICE,
+            MINION_1_TUN_DEVICE,
             IpAddress.valueOf("10.100.0.1"),
             INIT
     );
     private static final K8sNode MINION_2 = createNode(
+            CLUSTER_NAME,
             MINION_2_HOSTNAME,
             MINION,
             MINION_2_INTG_DEVICE,
             MINION_2_EXT_DEVICE,
             MINION_2_LOCAL_DEVICE,
+            MINION_2_TUN_DEVICE,
             IpAddress.valueOf("10.100.0.2"),
             INIT
     );
     private static final K8sNode MINION_3 = createNode(
+            CLUSTER_NAME,
             MINION_3_HOSTNAME,
             MINION,
             MINION_3_INTG_DEVICE,
             MINION_3_EXT_DEVICE,
             MINION_3_LOCAL_DEVICE,
+            MINION_3_TUN_DEVICE,
             IpAddress.valueOf("10.100.0.3"),
             COMPLETE
     );
@@ -334,16 +346,18 @@
 
     }
 
-    private static K8sNode createNode(String hostname, K8sNode.Type type,
+    private static K8sNode createNode(String clusterName, String hostname, K8sNode.Type type,
                                       Device intgBridge, Device extBridge,
-                                      Device localBridge, IpAddress ipAddr,
-                                      K8sNodeState state) {
+                                      Device localBridge, Device tunBridge,
+                                      IpAddress ipAddr, K8sNodeState state) {
         return DefaultK8sNode.builder()
                 .hostname(hostname)
+                .clusterName(clusterName)
                 .type(type)
                 .intgBridge(intgBridge.id())
                 .extBridge(extBridge.id())
                 .localBridge(localBridge.id())
+                .tunBridge(tunBridge.id())
                 .managementIp(ipAddr)
                 .dataIp(ipAddr)
                 .state(state)
diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/web/K8sNodeWebResourceTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/web/K8sNodeWebResourceTest.java
index 207bd0e..1005ab2 100644
--- a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/web/K8sNodeWebResourceTest.java
+++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/web/K8sNodeWebResourceTest.java
@@ -25,11 +25,13 @@
 import org.onosproject.codec.impl.CodecManager;
 import org.onosproject.k8snode.api.DefaultK8sApiConfig;
 import org.onosproject.k8snode.api.DefaultK8sNode;
+import org.onosproject.k8snode.api.HostNodesInfo;
 import org.onosproject.k8snode.api.K8sApiConfig;
 import org.onosproject.k8snode.api.K8sApiConfigAdminService;
 import org.onosproject.k8snode.api.K8sNode;
 import org.onosproject.k8snode.api.K8sNodeAdminService;
 import org.onosproject.k8snode.api.K8sNodeState;
+import org.onosproject.k8snode.codec.HostNodesInfoCodec;
 import org.onosproject.k8snode.codec.K8sApiConfigCodec;
 import org.onosproject.k8snode.codec.K8sNodeCodec;
 import org.onosproject.net.DeviceId;
@@ -81,6 +83,7 @@
         codecService.activate();
         codecService.registerCodec(K8sNode.class, new K8sNodeCodec());
         codecService.registerCodec(K8sApiConfig.class, new K8sApiConfigCodec());
+        codecService.registerCodec(HostNodesInfo.class, new HostNodesInfoCodec());
         ServiceDirectory testDirectory =
                 new TestServiceDirectory()
                 .add(K8sNodeAdminService.class, mockK8sNodeAdminService)
@@ -89,6 +92,7 @@
         setServiceDirectory(testDirectory);
 
         k8sNode = DefaultK8sNode.builder()
+                .clusterName("kubernetes")
                 .hostname("minion-node")
                 .type(K8sNode.Type.MINION)
                 .dataIp(IpAddress.valueOf("10.134.34.222"))
@@ -99,6 +103,9 @@
                 .build();
 
         k8sApiConfig = DefaultK8sApiConfig.builder()
+                .clusterName("kubernetes")
+                .segmentId(1)
+                .mode(K8sApiConfig.Mode.NORMAL)
                 .scheme(K8sApiConfig.Scheme.HTTPS)
                 .ipAddress(IpAddress.valueOf("10.134.34.223"))
                 .port(6443)