Add OpenConfig based port statistics discovery
Change-Id: I3e7d5683f8a51d06db18b644963044d204911346
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;
+ }
+ }
+}