Implement kubevirt router store, manager and codec with unit tests

Change-Id: Ib93a71326aa35b4817f0e6b6c97d5f57b26fe470
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/cli/KubevirtListRouterCommand.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/cli/KubevirtListRouterCommand.java
new file mode 100644
index 0000000..4ebd21b
--- /dev/null
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/cli/KubevirtListRouterCommand.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021-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.kubevirtnetworking.cli;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.StringUtils;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.kubevirtnetworking.api.KubevirtNetworkService;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouter;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterService;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+import static org.onosproject.kubevirtnetworking.api.Constants.CLI_FLAG_LENGTH;
+import static org.onosproject.kubevirtnetworking.api.Constants.CLI_IP_ADDRESSES_LENGTH;
+import static org.onosproject.kubevirtnetworking.api.Constants.CLI_IP_ADDRESS_LENGTH;
+import static org.onosproject.kubevirtnetworking.api.Constants.CLI_MARGIN_LENGTH;
+import static org.onosproject.kubevirtnetworking.api.Constants.CLI_NAME_LENGTH;
+import static org.onosproject.kubevirtnetworking.util.KubevirtNetworkingUtil.genFormatString;
+import static org.onosproject.kubevirtnetworking.util.KubevirtNetworkingUtil.prettyJson;
+
+@Service
+@Command(scope = "onos", name = "kubevirt-routers",
+        description = "Lists all kubevirt routers")
+public class KubevirtListRouterCommand extends AbstractShellCommand {
+
+    @Override
+    protected void doExecute() throws Exception {
+        KubevirtRouterService service = get(KubevirtRouterService.class);
+        KubevirtNetworkService netService = get(KubevirtNetworkService.class);
+        List<KubevirtRouter> routers = Lists.newArrayList(service.routers());
+        routers.sort(Comparator.comparing(KubevirtRouter::name));
+
+        String format = genFormatString(ImmutableList.of(CLI_NAME_LENGTH,
+                CLI_FLAG_LENGTH, CLI_IP_ADDRESSES_LENGTH, CLI_IP_ADDRESS_LENGTH));
+
+        if (outputJson()) {
+            print("%s", json(routers));
+        } else {
+            print(format, "Name", "SNAT", "Internal", "External");
+
+            for (KubevirtRouter router : routers) {
+                Set<String> internalCidrs = router.internal();
+                Set<String> externalIps = router.external().keySet();
+
+                String internal = internalCidrs.size() == 0 ? "[]" : internalCidrs.toString();
+                String external = externalIps.size() == 0 ? "[]" : externalIps.toString();
+
+                print(format, StringUtils.substring(router.name(), 0,
+                        CLI_NAME_LENGTH - CLI_MARGIN_LENGTH),
+                        StringUtils.substring(String.valueOf(router.enableSnat()), 0,
+                                CLI_FLAG_LENGTH - CLI_MARGIN_LENGTH),
+                        StringUtils.substring(internal, 0,
+                                CLI_IP_ADDRESSES_LENGTH - CLI_MARGIN_LENGTH),
+                        StringUtils.substring(external, 0,
+                                CLI_IP_ADDRESS_LENGTH - CLI_MARGIN_LENGTH)
+                );
+            }
+        }
+    }
+
+    private String json(List<KubevirtRouter> routers) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+
+        for (KubevirtRouter router : routers) {
+            result.add(jsonForEntity(router, KubevirtRouter.class));
+        }
+
+        return prettyJson(mapper, result.toString());
+    }
+}
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/codec/KubevirtRouterCodec.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/codec/KubevirtRouterCodec.java
new file mode 100644
index 0000000..4937535
--- /dev/null
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/codec/KubevirtRouterCodec.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2021-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.kubevirtnetworking.codec;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableMap;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.kubevirtnetworking.api.DefaultKubevirtRouter;
+import org.onosproject.kubevirtnetworking.api.KubevirtPeerRouter;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouter;
+import org.slf4j.Logger;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.nullIsIllegal;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Kubevirt router codec used for serializing and de-serializing JSON string.
+ */
+public final class KubevirtRouterCodec extends JsonCodec<KubevirtRouter> {
+
+    private final Logger log = getLogger(getClass());
+
+    private static final String NAME = "name";
+    private static final String DESCRIPTION = "description";
+    private static final String ENABLE_SNAT = "enableSnat";
+    private static final String INTERNAL = "internal";
+    private static final String EXTERNAL = "external";
+    private static final String PEER_ROUTER = "peerRouter";
+    private static final String IP_ADDRESS = "ip";
+    private static final String MAC_ADDRESS = "mac";
+    private static final String NETWORK = "network";
+
+    private static final String MISSING_MESSAGE = " is required in KubevirtRouter";
+
+    @Override
+    public ObjectNode encode(KubevirtRouter router, CodecContext context) {
+        checkNotNull(router, "Kubevirt router pool cannot be null");
+
+        ObjectNode result = context.mapper().createObjectNode()
+                .put(NAME, router.name())
+                .put(ENABLE_SNAT, router.enableSnat());
+
+        if (router.description() != null) {
+            result.put(DESCRIPTION, router.description());
+        }
+
+        if (router.internal() != null && !router.internal().isEmpty()) {
+            ArrayNode internal = context.mapper().createArrayNode();
+            router.internal().forEach(internal::add);
+
+            result.set(INTERNAL, internal);
+        }
+
+        if (router.external() != null && !router.external().isEmpty()) {
+            ArrayNode external = context.mapper().createArrayNode();
+            router.external().forEach((k, v) -> {
+                ObjectNode item = context.mapper().createObjectNode();
+                item.put(IP_ADDRESS, k);
+                item.put(NETWORK, v);
+                external.add(item);
+            });
+            result.set(EXTERNAL, external);
+        }
+
+        if (router.peerRouter() != null) {
+            ObjectNode peerRouter = context.mapper().createObjectNode();
+            peerRouter.put(IP_ADDRESS, router.peerRouter().ipAddress().toString());
+
+            if (router.peerRouter().macAddress() != null) {
+                peerRouter.put(MAC_ADDRESS, router.peerRouter().macAddress().toString());
+            }
+
+            result.set(PEER_ROUTER, peerRouter);
+        }
+
+        return result;
+    }
+
+    @Override
+    public KubevirtRouter decode(ObjectNode json, CodecContext context) {
+        if (json == null || !json.isObject()) {
+            return null;
+        }
+
+        String name = nullIsIllegal(json.get(NAME).asText(),
+                NAME + MISSING_MESSAGE);
+
+        KubevirtRouter.Builder builder = DefaultKubevirtRouter.builder()
+                .name(name);
+
+        JsonNode descriptionJson = json.get(DESCRIPTION);
+        if (descriptionJson != null) {
+            builder.description(descriptionJson.asText());
+        }
+
+        JsonNode enableSnatJson = json.get(ENABLE_SNAT);
+        if (enableSnatJson != null) {
+            builder.enableSnat(enableSnatJson.asBoolean());
+        }
+
+        ArrayNode internalJson = (ArrayNode) json.get(INTERNAL);
+        Set<String> internal = new HashSet<>();
+        if (internalJson != null) {
+            for (int i = 0; i < internalJson.size(); i++) {
+                internal.add(internalJson.get(i).asText());
+            }
+            builder.internal(internal);
+        }
+
+        ObjectNode externalJson = (ObjectNode) json.get(EXTERNAL);
+        if (externalJson != null) {
+            Map<String, String> external = ImmutableMap.of(
+                    externalJson.get(IP_ADDRESS).asText(),
+                    externalJson.get(NETWORK).asText());
+            builder.external(external);
+        }
+
+        ObjectNode peerRouterJson = (ObjectNode) json.get(PEER_ROUTER);
+        if (peerRouterJson != null) {
+            JsonNode ipJson = peerRouterJson.get(IP_ADDRESS);
+            JsonNode macJson = peerRouterJson.get(MAC_ADDRESS);
+
+            if (ipJson != null && macJson != null) {
+                IpAddress ip = IpAddress.valueOf(ipJson.asText());
+                MacAddress mac = MacAddress.valueOf(macJson.asText());
+                KubevirtPeerRouter peer = new KubevirtPeerRouter(ip, mac);
+                builder.peerRouter(peer);
+            }
+
+            // if mac address is not specified, we will not add mac address to peer router
+            if (ipJson != null && macJson == null) {
+                IpAddress ip = IpAddress.valueOf(ipJson.asText());
+                KubevirtPeerRouter peer = new KubevirtPeerRouter(ip, null);
+                builder.peerRouter(peer);
+            }
+        }
+
+        return builder.build();
+    }
+}
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/DistributedKubevirtPortStore.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/DistributedKubevirtPortStore.java
index 2c5cd3f..5104bc8 100644
--- a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/DistributedKubevirtPortStore.java
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/DistributedKubevirtPortStore.java
@@ -172,8 +172,9 @@
                     // if the event object has invalid port value, we do not
                     // propagate KUBEVIRT_PORT_REMOVED event.
                     if (event.oldValue() != null && event.oldValue().value() != null) {
-                        notifyDelegate(new KubevirtPortEvent(
-                                KUBEVIRT_PORT_REMOVED, event.oldValue().value()));
+                        eventExecutor.execute(() ->
+                                notifyDelegate(new KubevirtPortEvent(
+                                        KUBEVIRT_PORT_REMOVED, event.oldValue().value())));
                     }
                     break;
                 default:
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/DistributedKubevirtRouterStore.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/DistributedKubevirtRouterStore.java
new file mode 100644
index 0000000..537d91c
--- /dev/null
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/DistributedKubevirtRouterStore.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2021-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.kubevirtnetworking.impl;
+
+import com.google.common.collect.ImmutableSet;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.kubevirtnetworking.api.DefaultKubevirtRouter;
+import org.onosproject.kubevirtnetworking.api.KubevirtPeerRouter;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouter;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterEvent;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterStore;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterStoreDelegate;
+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.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+
+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.kubevirtnetworking.api.KubevirtRouterEvent.Type.KUBEVIRT_ROUTER_CREATED;
+import static org.onosproject.kubevirtnetworking.api.KubevirtRouterEvent.Type.KUBEVIRT_ROUTER_REMOVED;
+import static org.onosproject.kubevirtnetworking.api.KubevirtRouterEvent.Type.KUBEVIRT_ROUTER_UPDATED;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of kubevirt router store using consistent map.
+ */
+@Component(immediate = true, service = KubevirtRouterStore.class)
+public class DistributedKubevirtRouterStore
+        extends AbstractStore<KubevirtRouterEvent, KubevirtRouterStoreDelegate>
+        implements KubevirtRouterStore {
+
+    private 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 String APP_ID = "org.onosproject.kubevirtnetwork";
+
+    private static final KryoNamespace
+            SERIALIZER_KUBEVIRT_ROUTER = KryoNamespace.newBuilder()
+            .register(KryoNamespaces.API)
+            .register(KubevirtRouter.class)
+            .register(DefaultKubevirtRouter.class)
+            .register(KubevirtPeerRouter.class)
+            .register(Collection.class)
+            .build();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected StorageService storageService;
+
+    private final ExecutorService eventExecutor = newSingleThreadExecutor(
+            groupedThreads(this.getClass().getSimpleName(), "event-handler", log));
+
+    private final MapEventListener<String, KubevirtRouter> routerMapListener =
+            new KubevirtRouterMapListener();
+
+    private ConsistentMap<String, KubevirtRouter> routerStore;
+
+    @Activate
+    protected void activate() {
+        ApplicationId appId = coreService.registerApplication(APP_ID);
+        routerStore = storageService.<String, KubevirtRouter>consistentMapBuilder()
+                .withSerializer(Serializer.using(SERIALIZER_KUBEVIRT_ROUTER))
+                .withName("kubevirt-routerstore")
+                .withApplicationId(appId)
+                .build();
+        routerStore.addListener(routerMapListener);
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        routerStore.removeListener(routerMapListener);
+        eventExecutor.shutdown();
+        log.info("Stopped");
+    }
+
+    @Override
+    public void createRouter(KubevirtRouter router) {
+        routerStore.compute(router.name(), (name, existing) -> {
+            final String error = router.name() + ERR_DUPLICATE;
+            checkArgument(existing == null, error);
+            return router;
+        });
+    }
+
+    @Override
+    public void updateRouter(KubevirtRouter router) {
+        routerStore.compute(router.name(), (name, existing) -> {
+            final String error = router.name() + ERR_NOT_FOUND;
+            checkArgument(existing != null, error);
+            return router;
+        });
+    }
+
+    @Override
+    public KubevirtRouter removeRouter(String name) {
+        Versioned<KubevirtRouter> router = routerStore.remove(name);
+        if (router == null) {
+            final String error = name + ERR_NOT_FOUND;
+            throw new IllegalArgumentException(error);
+        }
+        return router.value();
+    }
+
+    @Override
+    public KubevirtRouter router(String name) {
+        return routerStore.asJavaMap().get(name);
+    }
+
+    @Override
+    public Set<KubevirtRouter> routers() {
+        return ImmutableSet.copyOf(routerStore.asJavaMap().values());
+    }
+
+    @Override
+    public void clear() {
+        routerStore.clear();
+    }
+
+    private class KubevirtRouterMapListener implements MapEventListener<String, KubevirtRouter> {
+
+        @Override
+        public void event(MapEvent<String, KubevirtRouter> event) {
+            switch (event.type()) {
+                case INSERT:
+                    log.debug("Kubevirt router created");
+                    eventExecutor.execute(() ->
+                            notifyDelegate(new KubevirtRouterEvent(
+                                    KUBEVIRT_ROUTER_CREATED, event.newValue().value())));
+                    break;
+                case UPDATE:
+                    log.debug("Kubevirt router updated");
+                    eventExecutor.execute(() ->
+                            notifyDelegate(new KubevirtRouterEvent(
+                                    KUBEVIRT_ROUTER_UPDATED, event.newValue().value())));
+                    break;
+                case REMOVE:
+                    log.debug("Kubevirt router removed");
+                    eventExecutor.execute(() ->
+                            notifyDelegate(new KubevirtRouterEvent(
+                                    KUBEVIRT_ROUTER_REMOVED, event.oldValue().value())));
+                    break;
+                default:
+                    // do nothing
+                    break;
+            }
+        }
+    }
+}
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/KubevirtPortManager.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/KubevirtPortManager.java
index b8160e2..f7bf516 100644
--- a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/KubevirtPortManager.java
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/KubevirtPortManager.java
@@ -56,7 +56,7 @@
 
     protected final Logger log = getLogger(getClass());
 
-    private static final String MSG_PORT = "Kubernetes port %s %s";
+    private static final String MSG_PORT = "Kubevirt port %s %s";
     private static final String MSG_CREATED = "created";
     private static final String MSG_UPDATED = "updated";
     private static final String MSG_REMOVED = "removed";
@@ -158,7 +158,7 @@
         @Override
         public void notify(KubevirtPortEvent event) {
             if (event != null) {
-                log.trace("send kubevirt networking event {}", event);
+                log.trace("send kubevirt port event {}", event);
                 process(event);
             }
         }
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/KubevirtRouterManager.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/KubevirtRouterManager.java
new file mode 100644
index 0000000..bae3ff9
--- /dev/null
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/KubevirtRouterManager.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2021-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.kubevirtnetworking.impl;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.MacAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.event.ListenerRegistry;
+import org.onosproject.kubevirtnetworking.api.KubevirtPeerRouter;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouter;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterAdminService;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterEvent;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterListener;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterService;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterStore;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterStoreDelegate;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.kubevirtnetworking.api.Constants.KUBEVIRT_NETWORKING_APP_ID;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provides implementation of administering and interfacing kubevirt router.
+ */
+@Component(
+        immediate = true,
+        service = {KubevirtRouterAdminService.class, KubevirtRouterService.class }
+)
+public class KubevirtRouterManager
+        extends ListenerRegistry<KubevirtRouterEvent, KubevirtRouterListener>
+        implements KubevirtRouterAdminService, KubevirtRouterService {
+
+    protected final Logger log = getLogger(getClass());
+
+    private static final String MSG_ROUTER = "Kubevirt router %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_ROUTER = "Kubevirt router cannot be null";
+    private static final String ERR_NULL_ROUTER_NAME = "Kubevirt router name cannot be null";
+    private static final String ERR_IN_USE = " still in use";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected KubevirtRouterStore kubevirtRouterStore;
+
+    private final InternalRouterStorageDelegate delegate = new InternalRouterStorageDelegate();
+
+    private ApplicationId appId;
+
+    @Activate
+    protected void activate() {
+        appId = coreService.registerApplication(KUBEVIRT_NETWORKING_APP_ID);
+
+        kubevirtRouterStore.setDelegate(delegate);
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        kubevirtRouterStore.unsetDelegate(delegate);
+        log.info("Stopped");
+    }
+
+    @Override
+    public void createRouter(KubevirtRouter router) {
+        checkNotNull(router, ERR_NULL_ROUTER);
+        checkArgument(!Strings.isNullOrEmpty(router.name()), ERR_NULL_ROUTER_NAME);
+
+        kubevirtRouterStore.createRouter(router);
+        log.info(String.format(MSG_ROUTER, router.name(), MSG_CREATED));
+    }
+
+    @Override
+    public void updateRouter(KubevirtRouter router) {
+        checkNotNull(router, ERR_NULL_ROUTER);
+        checkArgument(!Strings.isNullOrEmpty(router.name()), ERR_NULL_ROUTER_NAME);
+
+        kubevirtRouterStore.updateRouter(router);
+        log.info(String.format(MSG_ROUTER, router.name(), MSG_UPDATED));
+    }
+
+    @Override
+    public void removeRouter(String name) {
+        checkArgument(name != null, ERR_NULL_ROUTER_NAME);
+        synchronized (this) {
+            if (isRouterInUse(name)) {
+                final String error = String.format(MSG_ROUTER, name, ERR_IN_USE);
+                throw new IllegalStateException(error);
+            }
+
+            KubevirtRouter router = kubevirtRouterStore.removeRouter(name);
+            if (router != null) {
+                log.info(String.format(MSG_ROUTER, router.name(), MSG_REMOVED));
+            }
+        }
+    }
+
+    @Override
+    public void updatePeerRouterMac(String name, MacAddress mac) {
+        KubevirtRouter router = kubevirtRouterStore.router(name);
+        if (router == null) {
+            log.warn("The router is not found with the given name {}", name);
+            return;
+        }
+
+        KubevirtPeerRouter existing = router.peerRouter();
+        if (existing == null) {
+            log.warn("The peer router is not found with the given name {}", name);
+            return;
+        }
+
+        KubevirtPeerRouter updated = new KubevirtPeerRouter(existing.ipAddress(), mac);
+        kubevirtRouterStore.updateRouter(router.updatePeerRouter(updated));
+    }
+
+    @Override
+    public void clear() {
+        kubevirtRouterStore.clear();
+    }
+
+    @Override
+    public KubevirtRouter router(String name) {
+        checkArgument(name != null, ERR_NULL_ROUTER_NAME);
+        return kubevirtRouterStore.router(name);
+    }
+
+    @Override
+    public Set<KubevirtRouter> routers() {
+        return ImmutableSet.copyOf(kubevirtRouterStore.routers());
+    }
+
+    private boolean isRouterInUse(String name) {
+        return false;
+    }
+
+    private class InternalRouterStorageDelegate implements KubevirtRouterStoreDelegate {
+
+        @Override
+        public void notify(KubevirtRouterEvent event) {
+            log.trace("send kubevirt router event {}", event);
+            process(event);
+        }
+    }
+}
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/KubevirtRouterWatcher.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/KubevirtRouterWatcher.java
new file mode 100644
index 0000000..501099f
--- /dev/null
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/KubevirtRouterWatcher.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2021-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.kubevirtnetworking.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.Watcher;
+import io.fabric8.kubernetes.client.WatcherException;
+import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext;
+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.kubevirtnetworking.api.AbstractWatcher;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouter;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterAdminService;
+import org.onosproject.kubevirtnode.api.KubevirtApiConfigEvent;
+import org.onosproject.kubevirtnode.api.KubevirtApiConfigListener;
+import org.onosproject.kubevirtnode.api.KubevirtApiConfigService;
+import org.onosproject.mastership.MastershipService;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.kubevirtnetworking.api.Constants.KUBEVIRT_NETWORKING_APP_ID;
+import static org.onosproject.kubevirtnetworking.util.KubevirtNetworkingUtil.k8sClient;
+import static org.onosproject.kubevirtnetworking.util.KubevirtNetworkingUtil.parseResourceName;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Kubevirt virtual router watcher used for feeding kubevirt router information.
+ */
+@Component(immediate = true)
+public class KubevirtRouterWatcher extends AbstractWatcher {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected MastershipService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected LeadershipService leadershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected KubevirtRouterAdminService adminService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected KubevirtApiConfigService configService;
+
+    private final ExecutorService eventExecutor = newSingleThreadExecutor(
+            groupedThreads(this.getClass().getSimpleName(), "event-handler"));
+
+    private final InternalVirtualRouterWatcher
+            watcher = new InternalVirtualRouterWatcher();
+    private final InternalKubevirtApiConfigListener
+            configListener = new InternalKubevirtApiConfigListener();
+
+    CustomResourceDefinitionContext routerCrdCxt = new CustomResourceDefinitionContext
+            .Builder()
+            .withGroup("kubevirt.io")
+            .withScope("Cluster")
+            .withVersion("v1")
+            .withPlural("virtualrouters")
+            .build();
+
+    private ApplicationId appId;
+    private NodeId localNodeId;
+
+    @Activate
+    protected void activate() {
+        appId = coreService.registerApplication(KUBEVIRT_NETWORKING_APP_ID);
+        localNodeId = clusterService.getLocalNode().id();
+        leadershipService.runForLeadership(appId.name());
+        configService.addListener(configListener);
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        configService.removeListener(configListener);
+        leadershipService.withdraw(appId.name());
+        eventExecutor.shutdown();
+
+        log.info("Stopped");
+    }
+
+    private void instantiateWatcher() {
+        KubernetesClient client = k8sClient(configService);
+
+        if (client != null) {
+            try {
+                client.customResource(routerCrdCxt).watch(watcher);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private KubevirtRouter parseKubevirtRouter(String resource) {
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            JsonNode json = mapper.readTree(resource);
+            ObjectNode spec = (ObjectNode) json.get("spec");
+            return codec(KubevirtRouter.class).decode(spec, this);
+        } catch (IOException e) {
+            log.error("Failed to parse kubevirt router object");
+        }
+
+        return null;
+    }
+
+    private class InternalKubevirtApiConfigListener implements KubevirtApiConfigListener {
+
+        private boolean isRelevantHelper() {
+            return Objects.equals(localNodeId, leadershipService.getLeader(appId.name()));
+        }
+
+        @Override
+        public void event(KubevirtApiConfigEvent event) {
+
+            switch (event.type()) {
+                case KUBEVIRT_API_CONFIG_UPDATED:
+                    eventExecutor.execute(this::processConfigUpdate);
+                    break;
+                case KUBEVIRT_API_CONFIG_CREATED:
+                case KUBEVIRT_API_CONFIG_REMOVED:
+                default:
+                    // do nothing
+                    break;
+            }
+        }
+
+        private void processConfigUpdate() {
+            if (!isRelevantHelper()) {
+                return;
+            }
+
+            instantiateWatcher();
+        }
+    }
+
+    private class InternalVirtualRouterWatcher implements Watcher<String> {
+
+        @Override
+        public void eventReceived(Action action, String resource) {
+            switch (action) {
+                case ADDED:
+                    eventExecutor.execute(() -> processAddition(resource));
+                    break;
+                case MODIFIED:
+                    eventExecutor.execute(() -> processModification(resource));
+                    break;
+                case DELETED:
+                    eventExecutor.execute(() -> processDeletion(resource));
+                    break;
+                case ERROR:
+                    log.warn("Failures processing virtual router manipulation.");
+                    break;
+                default:
+                    // do nothing
+                    break;
+            }
+        }
+
+        @Override
+        public void onClose(WatcherException e) {
+            // due to the bugs in fabric8, the watcher might be closed,
+            // we will re-instantiate the watcher in this case
+            // FIXME: https://github.com/fabric8io/kubernetes-client/issues/2135
+            log.warn("Virtual Router watcher OnClose, re-instantiate the watcher...");
+
+            instantiateWatcher();
+        }
+
+        private void processAddition(String resource) {
+            if (!isMaster()) {
+                return;
+            }
+
+            String name = parseResourceName(resource);
+
+            log.trace("Process Virtual Router {} creating event from API server.",
+                    name);
+
+            KubevirtRouter router = parseKubevirtRouter(resource);
+            if (router != null) {
+                if (adminService.router(router.name()) == null) {
+                    adminService.createRouter(router);
+                }
+            }
+        }
+
+        private void processModification(String resource) {
+            if (!isMaster()) {
+                return;
+            }
+
+            String name = parseResourceName(resource);
+
+            log.trace("Process Virtual Router {} updating event from API server.",
+                    name);
+
+            KubevirtRouter router = parseKubevirtRouter(resource);
+            if (router != null) {
+                adminService.updateRouter(router);
+            }
+        }
+
+        private void processDeletion(String resource) {
+            if (!isMaster()) {
+                return;
+            }
+
+            String name = parseResourceName(resource);
+
+            log.trace("Process Virtual Router {} removal event from API server.",
+                    name);
+
+            adminService.removeRouter(name);
+        }
+
+        private boolean isMaster() {
+            return Objects.equals(localNodeId, leadershipService.getLeader(appId.name()));
+        }
+    }
+}
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/NetworkAttachmentDefinitionWatcher.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/NetworkAttachmentDefinitionWatcher.java
index 32bed58..d3a8ee4 100644
--- a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/NetworkAttachmentDefinitionWatcher.java
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/impl/NetworkAttachmentDefinitionWatcher.java
@@ -58,6 +58,7 @@
 import static org.onosproject.kubevirtnetworking.api.Constants.KUBEVIRT_NETWORKING_APP_ID;
 import static org.onosproject.kubevirtnetworking.api.KubevirtNetwork.Type.FLAT;
 import static org.onosproject.kubevirtnetworking.util.KubevirtNetworkingUtil.k8sClient;
+import static org.onosproject.kubevirtnetworking.util.KubevirtNetworkingUtil.parseResourceName;
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
@@ -218,7 +219,7 @@
                 return;
             }
 
-            String name = parseName(resource);
+            String name = parseResourceName(resource);
 
             log.trace("Process NetworkAttachmentDefinition {} creating event from API server.",
                     name);
@@ -236,7 +237,7 @@
                 return;
             }
 
-            String name = parseName(resource);
+            String name = parseResourceName(resource);
 
             log.trace("Process NetworkAttachmentDefinition {} updating event from API server.",
                     name);
@@ -252,7 +253,7 @@
                 return;
             }
 
-            String name = parseName(resource);
+            String name = parseResourceName(resource);
 
             log.trace("Process NetworkAttachmentDefinition {} removal event from API server.",
                     name);
@@ -264,20 +265,10 @@
             return Objects.equals(localNodeId, leadershipService.getLeader(appId.name()));
         }
 
-        private String parseName(String resource) {
-            try {
-                JSONObject json = new JSONObject(resource);
-                return json.getJSONObject("metadata").getString("name");
-            } catch (JSONException e) {
-                log.error("");
-            }
-            return "";
-        }
-
         private KubevirtNetwork parseKubevirtNetwork(String resource) {
             try {
                 JSONObject json = new JSONObject(resource);
-                String name = parseName(resource);
+                String name = parseResourceName(resource);
                 JSONObject annots = json.getJSONObject("metadata").getJSONObject("annotations");
                 String networkConfig = annots.getString(NETWORK_CONFIG);
                 if (networkConfig != null) {
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/util/KubevirtNetworkingUtil.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/util/KubevirtNetworkingUtil.java
index 04b6dba..d852ca4 100644
--- a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/util/KubevirtNetworkingUtil.java
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/util/KubevirtNetworkingUtil.java
@@ -413,6 +413,16 @@
         }
     }
 
+    public static String parseResourceName(String resource) {
+        try {
+            JSONObject json = new JSONObject(resource);
+            return json.getJSONObject("metadata").getString("name");
+        } catch (JSONException e) {
+            log.error("");
+        }
+        return "";
+    }
+
     private static PortNumber portNumber(DeviceId deviceId, String portName) {
         DeviceService deviceService = DefaultServiceDirectory.getService(DeviceService.class);
         Port port = deviceService.getPorts(deviceId).stream()
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtNetworkingCodecRegister.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtNetworkingCodecRegister.java
index 4691b8b..2539dff 100644
--- a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtNetworkingCodecRegister.java
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtNetworkingCodecRegister.java
@@ -20,10 +20,12 @@
 import org.onosproject.kubevirtnetworking.api.KubevirtIpPool;
 import org.onosproject.kubevirtnetworking.api.KubevirtNetwork;
 import org.onosproject.kubevirtnetworking.api.KubevirtPort;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouter;
 import org.onosproject.kubevirtnetworking.codec.KubevirtHostRouteCodec;
 import org.onosproject.kubevirtnetworking.codec.KubevirtIpPoolCodec;
 import org.onosproject.kubevirtnetworking.codec.KubevirtNetworkCodec;
 import org.onosproject.kubevirtnetworking.codec.KubevirtPortCodec;
+import org.onosproject.kubevirtnetworking.codec.KubevirtRouterCodec;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
@@ -51,6 +53,7 @@
         codecService.registerCodec(KubevirtIpPool.class, new KubevirtIpPoolCodec());
         codecService.registerCodec(KubevirtNetwork.class, new KubevirtNetworkCodec());
         codecService.registerCodec(KubevirtPort.class, new KubevirtPortCodec());
+        codecService.registerCodec(KubevirtRouter.class, new KubevirtRouterCodec());
 
         log.info("Started");
     }
@@ -62,6 +65,7 @@
         codecService.unregisterCodec(KubevirtIpPool.class);
         codecService.unregisterCodec(KubevirtNetwork.class);
         codecService.unregisterCodec(KubevirtPort.class);
+        codecService.unregisterCodec(KubevirtRouter.class);
 
         log.info("Stopped");
     }
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtNetworkingWebApplication.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtNetworkingWebApplication.java
index 29fe7bc..d2a45af 100644
--- a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtNetworkingWebApplication.java
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtNetworkingWebApplication.java
@@ -27,7 +27,8 @@
     public Set<Class<?>> getClasses() {
         return getClasses(
                 KubevirtNetworkWebResource.class,
-                KubevirtManagementWebResource.class
+                KubevirtManagementWebResource.class,
+                KubevirtRouterWebResource.class
         );
     }
 }
diff --git a/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtRouterWebResource.java b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtRouterWebResource.java
new file mode 100644
index 0000000..731b0d4
--- /dev/null
+++ b/apps/kubevirt-networking/app/src/main/java/org/onosproject/kubevirtnetworking/web/KubevirtRouterWebResource.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021-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.kubevirtnetworking.web;
+
+import org.onosproject.kubevirtnetworking.api.KubevirtRouter;
+import org.onosproject.kubevirtnetworking.api.KubevirtRouterService;
+import org.onosproject.rest.AbstractWebResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * Handles REST API call for kubevirt router.
+ */
+@Path("router")
+public class KubevirtRouterWebResource extends AbstractWebResource {
+
+    protected final Logger log = LoggerFactory.getLogger(getClass());
+
+    /**
+     * Returns set of all routers.
+     *
+     * @return 200 OK with set of all routers
+     * @onos.rsModel KubevirtRouters
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getRouters() {
+        KubevirtRouterService service = get(KubevirtRouterService.class);
+        final Iterable<KubevirtRouter> routers = service.routers();
+        return ok(encodeArray(KubevirtRouter.class, "routers", routers)).build();
+    }
+}