Add OpenConfig based port statistics discovery

Change-Id: I3e7d5683f8a51d06db18b644963044d204911346
diff --git a/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/AbstractGnmiHandlerBehaviour.java b/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/AbstractGnmiHandlerBehaviour.java
index 1e06c95..3bfca46 100644
--- a/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/AbstractGnmiHandlerBehaviour.java
+++ b/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/AbstractGnmiHandlerBehaviour.java
@@ -53,7 +53,7 @@
 
     protected boolean setupBehaviour() {
         deviceId = handler().data().deviceId();
-
+        deviceService = handler().get(DeviceService.class);
         controller = handler().get(GnmiController.class);
         client = controller.getClient(deviceId);
 
diff --git a/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiDeviceDescriptionDiscovery.java b/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiDeviceDescriptionDiscovery.java
index 526c333..e56f2f3 100644
--- a/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiDeviceDescriptionDiscovery.java
+++ b/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiDeviceDescriptionDiscovery.java
@@ -49,6 +49,8 @@
     private static final Logger log = LoggerFactory
             .getLogger(OpenConfigGnmiDeviceDescriptionDiscovery.class);
 
+    private static final String LAST_CHANGE = "last-change";
+
     @Override
     public DeviceDescription discoverDeviceDetails() {
         return null;
@@ -70,19 +72,20 @@
 
         // Creates port descriptions with port name and port number
         response.getNotificationList()
-                .stream()
-                .flatMap(notification -> notification.getUpdateList().stream())
-                .forEach(update -> {
-                    // /interfaces/interface[name=ifName]/state/...
-                    final String ifName = update.getPath().getElem(1)
-                            .getKeyMap().get("name");
-                    if (!ports.containsKey(ifName)) {
-                        ports.put(ifName, DefaultPortDescription.builder());
-                        annotations.put(ifName, DefaultAnnotations.builder());
-                    }
-                    final DefaultPortDescription.Builder builder = ports.get(ifName);
-                    final DefaultAnnotations.Builder annotationsBuilder = annotations.get(ifName);
-                    parseInterfaceInfo(update, ifName, builder, annotationsBuilder);
+                .forEach(notification -> {
+                    long timestamp = notification.getTimestamp();
+                    notification.getUpdateList().forEach(update -> {
+                        // /interfaces/interface[name=ifName]/state/...
+                        final String ifName = update.getPath().getElem(1)
+                                .getKeyMap().get("name");
+                        if (!ports.containsKey(ifName)) {
+                            ports.put(ifName, DefaultPortDescription.builder());
+                            annotations.put(ifName, DefaultAnnotations.builder());
+                        }
+                        final DefaultPortDescription.Builder builder = ports.get(ifName);
+                        final DefaultAnnotations.Builder annotationsBuilder = annotations.get(ifName);
+                        parseInterfaceInfo(update, ifName, builder, annotationsBuilder, timestamp);
+                    });
                 });
 
         final List<PortDescription> portDescriptionList = Lists.newArrayList();
@@ -114,7 +117,8 @@
     private void parseInterfaceInfo(Update update,
                                     String ifName,
                                     DefaultPortDescription.Builder builder,
-                                    DefaultAnnotations.Builder annotationsBuilder) {
+                                    DefaultAnnotations.Builder annotationsBuilder,
+                                    long timestamp) {
 
 
         final Path path = update.getPath();
@@ -130,6 +134,7 @@
                     return;
                 case "oper-status":
                     builder.isEnabled(parseOperStatus(val.getStringVal()));
+                    annotationsBuilder.set(LAST_CHANGE, String.valueOf(timestamp));
                     return;
                 default:
                     break;
diff --git a/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiPortStatisticsDiscovery.java b/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiPortStatisticsDiscovery.java
new file mode 100644
index 0000000..a9481a4
--- /dev/null
+++ b/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiPortStatisticsDiscovery.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.gnmi;
+
+import com.google.common.collect.Maps;
+import gnmi.Gnmi;
+import gnmi.Gnmi.GetRequest;
+import gnmi.Gnmi.GetResponse;
+import gnmi.Gnmi.Path;
+import org.apache.commons.lang3.tuple.Pair;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DefaultPortStatistics;
+import org.onosproject.net.device.PortStatistics;
+import org.onosproject.net.device.PortStatisticsDiscovery;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Behaviour to get port statistics from device via gNMI.
+ */
+public class OpenConfigGnmiPortStatisticsDiscovery extends AbstractGnmiHandlerBehaviour
+        implements PortStatisticsDiscovery {
+
+    private static final Map<Pair<DeviceId, PortNumber>, Long> PORT_START_TIMES =
+            Maps.newConcurrentMap();
+    private static final String LAST_CHANGE = "last-change";
+
+    @Override
+    public Collection<PortStatistics> discoverPortStatistics() {
+        if (!setupBehaviour()) {
+            return Collections.emptyList();
+        }
+
+        Map<String, PortNumber> ifacePortNumberMapping = Maps.newHashMap();
+        List<Port> ports = deviceService.getPorts(deviceId);
+        GetRequest.Builder getRequest = GetRequest.newBuilder();
+        getRequest.setEncoding(Gnmi.Encoding.PROTO);
+
+        // Use this path to get all counters from specific interface(port)
+        // /interfaces/interface[port-name]/state/counters/[counter name]
+        ports.forEach(port -> {
+            String portName = port.number().name();
+            Path path = interfaceCounterPath(portName);
+            getRequest.addPath(path);
+            ifacePortNumberMapping.put(portName, port.number());
+        });
+
+        GetResponse getResponse =
+                getFutureWithDeadline(client.get(getRequest.build()),
+                         "getting port counters",
+                                      GetResponse.getDefaultInstance());
+
+        Map<String, Long> inPkts = Maps.newHashMap();
+        Map<String, Long> outPkts = Maps.newHashMap();
+        Map<String, Long> inBytes = Maps.newHashMap();
+        Map<String, Long> outBytes = Maps.newHashMap();
+        Map<String, Long> inDropped = Maps.newHashMap();
+        Map<String, Long> outDropped = Maps.newHashMap();
+        Map<String, Long> inErrors = Maps.newHashMap();
+        Map<String, Long> outErrors = Maps.newHashMap();
+        Map<String, Duration> timestamps = Maps.newHashMap();
+
+        // Collect responses and sum {in,out,dropped} packets
+        getResponse.getNotificationList().forEach(notification -> {
+            notification.getUpdateList().forEach(update -> {
+                Path path = update.getPath();
+                String ifName = interfaceNameFromPath(path);
+                timestamps.putIfAbsent(ifName, Duration.ofNanos(notification.getTimestamp()));
+
+                // Last element is the counter name
+                String counterName = path.getElem(path.getElemCount() - 1).getName();
+                long counterValue = update.getVal().getUintVal();
+
+
+                switch (counterName) {
+                    case "in-octets":
+                        inBytes.put(ifName, counterValue);
+                        break;
+                    case "out-octets":
+                        outBytes.put(ifName, counterValue);
+                        break;
+                    case "in-discards":
+                    case "in-fcs-errors":
+                        inDropped.compute(ifName, (k, v) -> v == null ? counterValue : v + counterValue);
+                        break;
+                    case "out-discards":
+                        outDropped.put(ifName, counterValue);
+                        break;
+                    case "in-errors":
+                        inErrors.put(ifName, counterValue);
+                        break;
+                    case "out-errors":
+                        outErrors.put(ifName, counterValue);
+                        break;
+                    case "in-unicast-pkts":
+                    case "in-broadcast-pkts":
+                    case "in-multicast-pkts":
+                    case "in-unknown-protos":
+                        inPkts.compute(ifName, (k, v) -> v == null ? counterValue : v + counterValue);
+                        break;
+                    case "out-unicast-pkts":
+                    case "out-broadcast-pkts":
+                    case "out-multicast-pkts":
+                        outPkts.compute(ifName, (k, v) -> v == null ? counterValue : v + counterValue);
+                        break;
+                    default:
+                        log.warn("Unsupported counter name {}, ignored", counterName);
+                        break;
+                }
+            });
+        });
+
+        // Build ONOS port stats map
+        return ifacePortNumberMapping.entrySet().stream()
+            .map(e -> {
+                String ifName = e.getKey();
+                PortNumber portNumber = e.getValue();
+                Duration portActive = getDurationActive(portNumber, timestamps.get(ifName));
+                return DefaultPortStatistics.builder()
+                        .setDeviceId(deviceId)
+                        .setPort(portNumber)
+                        .setDurationSec(portActive.getSeconds())
+                        .setDurationNano(portActive.getNano())
+                        .setPacketsSent(outPkts.getOrDefault(ifName, 0L))
+                        .setPacketsReceived(inPkts.getOrDefault(ifName, 0L))
+                        .setPacketsTxDropped(outDropped.getOrDefault(ifName, 0L))
+                        .setPacketsRxDropped(inDropped.getOrDefault(ifName, 0L))
+                        .setBytesSent(outBytes.getOrDefault(ifName, 0L))
+                        .setBytesReceived(inBytes.getOrDefault(ifName, 0L))
+                        .setPacketsTxErrors(outErrors.getOrDefault(ifName, 0L))
+                        .setPacketsRxErrors(inErrors.getOrDefault(ifName, 0L))
+                        .build();
+            })
+            .collect(Collectors.toList());
+
+    }
+
+    private String interfaceNameFromPath(Path path) {
+        // /interfaces/interface[name=iface-name]
+        return path.getElem(1).getKeyOrDefault("name", null);
+    }
+
+    private Path interfaceCounterPath(String portName) {
+        // /interfaces/interface[name=port-name]/state/counters
+        return Path.newBuilder()
+                .addElem(Gnmi.PathElem.newBuilder().setName("interfaces").build())
+                .addElem(Gnmi.PathElem.newBuilder().setName("interface")
+                        .putKey("name", portName).build())
+                .addElem(Gnmi.PathElem.newBuilder().setName("state").build())
+                .addElem(Gnmi.PathElem.newBuilder().setName("counters").build())
+                .build();
+    }
+
+    private Duration getDurationActive(PortNumber portNumber, Duration timestamp) {
+        Port port = deviceService.getPort(deviceId, portNumber);
+        if (port == null || !port.isEnabled()) {
+            //FIXME log
+            return Duration.ZERO;
+        }
+        String lastChangedStr = port.annotations().value(LAST_CHANGE);
+        if (lastChangedStr == null) {
+            //FIXME log
+            // Falling back to the hack...
+            // FIXME: This is a workaround since we cannot determine the port
+            // duration from gNMI now
+            final long now = System.currentTimeMillis() / 1000;
+            final Long startTime = PORT_START_TIMES.putIfAbsent(
+                    Pair.of(deviceId, portNumber), now);
+            return Duration.ofSeconds(startTime == null ? now : now - startTime);
+        }
+
+        try {
+            long lastChanged = Long.parseLong(lastChangedStr);
+            return timestamp.minus(lastChanged, ChronoUnit.NANOS);
+        } catch (NullPointerException | NumberFormatException ex) {
+            //FIXME log
+            return Duration.ZERO;
+        }
+    }
+}
diff --git a/drivers/gnmi/src/main/resources/gnmi-drivers.xml b/drivers/gnmi/src/main/resources/gnmi-drivers.xml
index 70c0b20..9af162f 100644
--- a/drivers/gnmi/src/main/resources/gnmi-drivers.xml
+++ b/drivers/gnmi/src/main/resources/gnmi-drivers.xml
@@ -20,6 +20,8 @@
                    impl="org.onosproject.drivers.gnmi.OpenConfigGnmiDeviceDescriptionDiscovery"/>
         <behaviour api="org.onosproject.net.device.DeviceHandshaker"
                    impl="org.onosproject.drivers.gnmi.GnmiHandshaker"/>
+        <behaviour api="org.onosproject.net.device.PortStatisticsDiscovery"
+                   impl="org.onosproject.drivers.gnmi.OpenConfigGnmiPortStatisticsDiscovery"/>
     </driver>
 </drivers>
 
diff --git a/drivers/stratum/src/main/resources/stratum-drivers.xml b/drivers/stratum/src/main/resources/stratum-drivers.xml
index 83fea1d..fa0c0c4 100644
--- a/drivers/stratum/src/main/resources/stratum-drivers.xml
+++ b/drivers/stratum/src/main/resources/stratum-drivers.xml
@@ -29,4 +29,3 @@
         <property name="actionGroupReadFromMirror">true</property>
     </driver>
 </drivers>
-
diff --git a/protocols/gnmi/ctl/src/main/java/org/onosproject/gnmi/ctl/GnmiClientImpl.java b/protocols/gnmi/ctl/src/main/java/org/onosproject/gnmi/ctl/GnmiClientImpl.java
index 117b27e..ce9c5f8 100644
--- a/protocols/gnmi/ctl/src/main/java/org/onosproject/gnmi/ctl/GnmiClientImpl.java
+++ b/protocols/gnmi/ctl/src/main/java/org/onosproject/gnmi/ctl/GnmiClientImpl.java
@@ -106,7 +106,7 @@
             return blockingStub.capabilities(request);
         } catch (StatusRuntimeException e) {
             log.warn("Unable to get capability from {}: {}", deviceId, e.getMessage());
-            return null;
+            return CapabilityResponse.getDefaultInstance();
         }
     }
 
@@ -115,7 +115,7 @@
             return blockingStub.get(request);
         } catch (StatusRuntimeException e) {
             log.warn("Unable to get data from {}: {}", deviceId, e.getMessage());
-            return null;
+            return GetResponse.getDefaultInstance();
         }
     }
 
@@ -124,7 +124,7 @@
             return blockingStub.set(request);
         } catch (StatusRuntimeException e) {
             log.warn("Unable to set data to {}: {}", deviceId, e.getMessage());
-            return null;
+            return SetResponse.getDefaultInstance();
         }
     }
 
diff --git a/providers/general/device/src/main/java/org/onosproject/provider/general/device/impl/GnmiDeviceStateSubscriber.java b/providers/general/device/src/main/java/org/onosproject/provider/general/device/impl/GnmiDeviceStateSubscriber.java
index b253f3e..028c5fb 100644
--- a/providers/general/device/src/main/java/org/onosproject/provider/general/device/impl/GnmiDeviceStateSubscriber.java
+++ b/providers/general/device/src/main/java/org/onosproject/provider/general/device/impl/GnmiDeviceStateSubscriber.java
@@ -37,9 +37,9 @@
 import org.onosproject.mastership.MastershipEvent;
 import org.onosproject.mastership.MastershipListener;
 import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.DefaultAnnotations;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.Port;
-import org.onosproject.net.SparseAnnotations;
 import org.onosproject.net.device.DefaultPortDescription;
 import org.onosproject.net.device.DeviceEvent;
 import org.onosproject.net.device.DeviceListener;
@@ -61,6 +61,8 @@
 @Beta
 class GnmiDeviceStateSubscriber {
 
+    private static final String LAST_CHANGE = "last-change";
+
     private static Logger log = LoggerFactory.getLogger(GnmiDeviceStateSubscriber.class);
 
     private final GnmiController gnmiController;
@@ -199,14 +201,15 @@
 
             // Use last element to identify which state updated
             if ("oper-status".equals(lastElem.getName())) {
-                handleOperStatusUpdate(eventSubject.deviceId(), update);
+                handleOperStatusUpdate(eventSubject.deviceId(), update,
+                                       notification.getTimestamp());
             } else {
                 log.debug("Unrecognized update {}", GnmiUtils.pathToString(path));
             }
         });
     }
 
-    private void handleOperStatusUpdate(DeviceId deviceId, Update update) {
+    private void handleOperStatusUpdate(DeviceId deviceId, Update update, long timestamp) {
         Path path = update.getPath();
         // first element should be "interface"
         String interfaceName = path.getElem(1).getKeyOrDefault("name", null);
@@ -222,6 +225,11 @@
                 return;
             }
 
+            DefaultAnnotations portAnnotations = DefaultAnnotations.builder()
+                    .putAll(port.annotations())
+                    .set(LAST_CHANGE, String.valueOf(timestamp))
+                    .build();
+
             // Port/Interface name is identical in OpenConfig model, but not in ONOS
             // This might cause some problem if we use one name to different port
             PortDescription portDescription = DefaultPortDescription.builder()
@@ -229,7 +237,7 @@
                     .withPortNumber(port.number())
                     .isEnabled(update.getVal().getStringVal().equals("UP"))
                     .type(port.type())
-                    .annotations((SparseAnnotations) port.annotations())
+                    .annotations(portAnnotations)
                     .build();
             providerService.portStatusChanged(deviceId, portDescription);
         });