[SDFAB-199] Add the support for last-change

- Add parsing in OpenConfigGnmiDeviceDescriptionDiscovery and
  defaults to 0 for the devices not providing last-change
- Remove hack in OpenConfigGnmiPortStatisticsDiscovery and set
  the duration to 0 for the devices that do not support last-change
- Subscribe to the state changes of a given port and add parsing of
  last-change timestamp in GnmiDeviceStateSubscribe

Note that if the device does not aggregate updates into a single notification
two PORT_UPDATED events will be generated. The first as consequence of the
operational status change and the second caused by the reconciliation which
updates correctly last-change

Change-Id: I6b2cb3652b306358bd9e701780946864a1ed324b
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 210a237..eb41f4d 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
@@ -57,7 +57,7 @@
     private static final Logger log = LoggerFactory
             .getLogger(OpenConfigGnmiDeviceDescriptionDiscovery.class);
 
-    private static final String LAST_CHANGE = "last-changed";
+    private static final String LAST_CHANGE = "last-change";
 
     private static final String UNKNOWN = "unknown";
 
@@ -96,7 +96,6 @@
         // Creates port descriptions with port name and port number
         response.getNotificationList()
                 .forEach(notification -> {
-                    long timestamp = notification.getTimestamp();
                     notification.getUpdateList().forEach(update -> {
                         // /interfaces/interface[name=ifName]/state/...
                         final String ifName = update.getPath().getElem(1)
@@ -107,12 +106,17 @@
                         }
                         final DefaultPortDescription.Builder builder = ports.get(ifName);
                         final DefaultAnnotations.Builder annotationsBuilder = annotations.get(ifName);
-                        parseInterfaceInfo(update, ifName, builder, annotationsBuilder, timestamp);
+                        parseInterfaceInfo(update, ifName, builder, annotationsBuilder);
                     });
                 });
 
         final List<PortDescription> portDescriptionList = Lists.newArrayList();
         ports.forEach((key, value) -> {
+            // For devices not providing last-change, we set it to 0
+            final DefaultAnnotations.Builder annotationsBuilder = annotations.get(key);
+            if (!annotationsBuilder.build().keys().contains(LAST_CHANGE)) {
+                annotationsBuilder.set(LAST_CHANGE, String.valueOf(0));
+            }
             DefaultAnnotations annotation = annotations.get(key).build();
             portDescriptionList.add(value.annotations(annotation).build());
         });
@@ -140,9 +144,7 @@
     private void parseInterfaceInfo(Update update,
                                     String ifName,
                                     DefaultPortDescription.Builder builder,
-                                    DefaultAnnotations.Builder annotationsBuilder,
-                                    long timestamp) {
-
+                                    DefaultAnnotations.Builder annotationsBuilder) {
 
         final Path path = update.getPath();
         final List<PathElem> elems = path.getElemList();
@@ -150,6 +152,7 @@
         if (elems.size() == 4) {
             // /interfaces/interface/state/ifindex
             // /interfaces/interface/state/oper-status
+            // /interfaces/interface/state/last-change
             final String pathElemName = elems.get(3).getName();
             switch (pathElemName) {
                 case "ifindex": // port number
@@ -157,7 +160,9 @@
                     return;
                 case "oper-status":
                     builder.isEnabled(parseOperStatus(val.getStringVal()));
-                    annotationsBuilder.set(LAST_CHANGE, String.valueOf(timestamp));
+                    return;
+                case "last-change":
+                    annotationsBuilder.set(LAST_CHANGE, String.valueOf(val.getUintVal()));
                     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
index 6e361ff..bae396b 100644
--- a/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiPortStatisticsDiscovery.java
+++ b/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiPortStatisticsDiscovery.java
@@ -22,11 +22,9 @@
 import gnmi.Gnmi.GetRequest;
 import gnmi.Gnmi.GetResponse;
 import gnmi.Gnmi.Path;
-import org.apache.commons.lang3.tuple.Pair;
 import org.onosproject.gnmi.api.GnmiClient;
 import org.onosproject.gnmi.api.GnmiController;
 import org.onosproject.grpc.utils.AbstractGrpcHandlerBehaviour;
-import org.onosproject.net.DeviceId;
 import org.onosproject.net.Port;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.device.DefaultPortStatistics;
@@ -48,9 +46,7 @@
         extends AbstractGrpcHandlerBehaviour<GnmiClient, GnmiController>
         implements PortStatisticsDiscovery {
 
-    private static final Map<Pair<DeviceId, PortNumber>, Long> PORT_START_TIMES =
-            Maps.newConcurrentMap();
-    private static final String LAST_CHANGE = "last-changed";
+    private static final String LAST_CHANGE = "last-change";
 
     public OpenConfigGnmiPortStatisticsDiscovery() {
         super(GnmiController.class);
@@ -185,21 +181,16 @@
             //FIXME log
             return Duration.ZERO;
         }
+
+        // Set duration 0 for devices that do not support reporting last-change
         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);
+            return Duration.ZERO;
         }
 
         try {
             long lastChanged = Long.parseLong(lastChangedStr);
-            return timestamp.minus(lastChanged, ChronoUnit.NANOS);
+            return lastChanged == 0 ? Duration.ZERO : timestamp.minus(lastChanged, ChronoUnit.NANOS);
         } catch (NullPointerException | NumberFormatException ex) {
             //FIXME log
             return Duration.ZERO;
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 b7804bb..d55ca7f 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
@@ -62,7 +62,7 @@
 @Beta
 class GnmiDeviceStateSubscriber {
 
-    private static final String LAST_CHANGE = "last-changed";
+    private static final String LAST_CHANGE = "last-change";
 
     private static Logger log = LoggerFactory.getLogger(GnmiDeviceStateSubscriber.class);
 
@@ -126,13 +126,12 @@
                 && !deviceService.getPorts(deviceId).isEmpty();
     }
 
-    private Path interfaceOperStatusPath(String interfaceName) {
+    private Path interfaceStatePath(String interfaceName) {
         return Path.newBuilder()
                 .addElem(PathElem.newBuilder().setName("interfaces").build())
                 .addElem(PathElem.newBuilder()
                                  .setName("interface").putKey("name", interfaceName).build())
                 .addElem(PathElem.newBuilder().setName("state").build())
-                .addElem(PathElem.newBuilder().setName("oper-status").build())
                 .build();
     }
 
@@ -163,7 +162,7 @@
                 .setUpdatesOnly(true)
                 .addAllSubscription(ports.stream().map(
                         port -> Subscription.newBuilder()
-                                .setPath(interfaceOperStatusPath(port.name()))
+                                .setPath(interfaceStatePath(port.name()))
                                 .setMode(SubscriptionMode.ON_CHANGE)
                                 .build()).collect(Collectors.toList()))
                 .build();
@@ -183,19 +182,32 @@
             return;
         }
 
-        List<Update> updateList = notification.getUpdateList();
-        updateList.forEach(update -> {
-            Path path = update.getPath();
-            PathElem lastElem = path.getElem(path.getElemCount() - 1);
+        long lastChange = 0;
+        Update statusUpdate = null;
+        Path path;
+        PathElem lastElem;
+        // The assumption is that the notification contains all the updates:
+        // last-change, oper-status, counters, and so on. Otherwise, we need
+        // to put in place the aggregation logic in ONOS
+        for (Update update : notification.getUpdateList()) {
+            path = update.getPath();
+            lastElem = path.getElem(path.getElemCount() - 1);
 
             // Use last element to identify which state updated
             if ("oper-status".equals(lastElem.getName())) {
-                handleOperStatusUpdate(eventSubject.deviceId(), update,
-                                       notification.getTimestamp());
-            } else {
+                statusUpdate = update;
+            } else if ("last-change".equals(lastElem.getName())) {
+                lastChange = update.getVal().getUintVal();
+            } else if (log.isDebugEnabled()) {
                 log.debug("Unrecognized update {}", GnmiUtils.pathToString(path));
             }
-        });
+        }
+
+        // Last-change could be not supported by the device
+        // Cannot proceed without the status update.
+        if (statusUpdate != null) {
+            handleOperStatusUpdate(eventSubject.deviceId(), statusUpdate, lastChange);
+        }
     }
 
     private void handleOperStatusUpdate(DeviceId deviceId, Update update, long timestamp) {