Add support for enabling/disabling ports for gNMI devices

This change also includes:
- Refactoring of gNMI protocol+driver to take advantage of the recent
changes to the gRPC protocol subsystem (e.g. no more locking, start RPC
with timeouts, etc.).
- Fixed Stratum driver to work after GeneralDeviceProvider refactoring
- Updated bmv2.py to generate ChassisConfig for stratum_bmv2
- Fixed portstate command to use the same port name as in the store

Change-Id: I0dad3bc73e4b6d907b5cf6b7b9a2852943226be7
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 4ce9f18..3774bfb 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
@@ -17,7 +17,6 @@
 package org.onosproject.drivers.gnmi;
 
 import com.google.common.base.Strings;
-import io.grpc.StatusRuntimeException;
 import org.onosproject.gnmi.api.GnmiClient;
 import org.onosproject.gnmi.api.GnmiClientKey;
 import org.onosproject.gnmi.api.GnmiController;
@@ -32,20 +31,12 @@
 
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 
 /**
  * Abstract implementation of a behaviour handler for a gNMI device.
  */
 public class AbstractGnmiHandlerBehaviour extends AbstractHandlerBehaviour {
 
-    // Default timeout in seconds for device operations.
-    private static final String DEVICE_REQ_TIMEOUT = "deviceRequestTimeout";
-    private static final int DEFAULT_DEVICE_REQ_TIMEOUT = 60;
-
     protected final Logger log = LoggerFactory.getLogger(getClass());
     protected DeviceId deviceId;
     protected DeviceService deviceService;
@@ -95,58 +86,4 @@
             return null;
         }
     }
-
-    /**
-     * Returns the device request timeout driver property, or a default value
-     * if the property is not present or cannot be parsed.
-     *
-     * @return timeout value
-     */
-    private int getDeviceRequestTimeout() {
-        final String timeout = handler().driver()
-                .getProperty(DEVICE_REQ_TIMEOUT);
-        if (timeout == null) {
-            return DEFAULT_DEVICE_REQ_TIMEOUT;
-        } else {
-            try {
-                return Integer.parseInt(timeout);
-            } catch (NumberFormatException e) {
-                log.error("{} driver property '{}' is not a number, using default value {}",
-                        DEVICE_REQ_TIMEOUT, timeout, DEFAULT_DEVICE_REQ_TIMEOUT);
-                return DEFAULT_DEVICE_REQ_TIMEOUT;
-            }
-        }
-    }
-
-    /**
-     * Convenience method to get the result of a completable future while
-     * setting a timeout and checking for exceptions.
-     *
-     * @param future        completable future
-     * @param opDescription operation description to use in log messages. Should
-     *                      be a sentence starting with a verb ending in -ing,
-     *                      e.g. "reading...", "writing...", etc.
-     * @param defaultValue  value to return if operation fails
-     * @param <U>           type of returned value
-     * @return future result or default value
-     */
-    <U> U getFutureWithDeadline(CompletableFuture<U> future, String opDescription,
-                                U defaultValue) {
-        try {
-            return future.get(getDeviceRequestTimeout(), TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            log.error("Exception while {} on {}", opDescription, deviceId);
-        } catch (ExecutionException e) {
-            final Throwable cause = e.getCause();
-            if (cause instanceof StatusRuntimeException) {
-                final StatusRuntimeException grpcError = (StatusRuntimeException) cause;
-                log.warn("Error while {} on {}: {}", opDescription, deviceId, grpcError.getMessage());
-            } else {
-                log.error("Exception while {} on {}", opDescription, deviceId, e.getCause());
-            }
-        } catch (TimeoutException e) {
-            log.error("Operation TIMEOUT while {} on {}", opDescription, deviceId);
-        }
-        return defaultValue;
-    }
 }
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 e56f2f3..ce134ad 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
@@ -18,11 +18,16 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
 import gnmi.Gnmi;
 import gnmi.Gnmi.GetRequest;
 import gnmi.Gnmi.GetResponse;
+import org.onlab.packet.ChassisId;
+import org.onosproject.net.AnnotationKeys;
 import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.Device;
 import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DefaultDeviceDescription;
 import org.onosproject.net.device.DefaultPortDescription;
 import org.onosproject.net.device.DeviceDescription;
 import org.onosproject.net.device.DeviceDescriptionDiscovery;
@@ -49,11 +54,24 @@
     private static final Logger log = LoggerFactory
             .getLogger(OpenConfigGnmiDeviceDescriptionDiscovery.class);
 
-    private static final String LAST_CHANGE = "last-change";
+    private static final String LAST_CHANGE = "last-changed";
+
+    private static final String UNKNOWN = "unknown";
 
     @Override
     public DeviceDescription discoverDeviceDetails() {
-        return null;
+        return new DefaultDeviceDescription(
+                data().deviceId().uri(),
+                Device.Type.SWITCH,
+                data().driver().manufacturer(),
+                data().driver().hwVersion(),
+                data().driver().swVersion(),
+                UNKNOWN,
+                new ChassisId(),
+                true,
+                DefaultAnnotations.builder()
+                        .set(AnnotationKeys.PROTOCOL, "gNMI")
+                        .build());
     }
 
     @Override
@@ -63,9 +81,7 @@
         }
         log.debug("Discovering port details on device {}", handler().data().deviceId());
 
-        final GetResponse response = getFutureWithDeadline(
-                client.get(buildPortStateRequest()),
-                "getting port details", GetResponse.getDefaultInstance());
+        final GetResponse response = Futures.getUnchecked(client.get(buildPortStateRequest()));
 
         final Map<String, DefaultPortDescription.Builder> ports = Maps.newHashMap();
         final Map<String, DefaultAnnotations.Builder> annotations = Maps.newHashMap();
diff --git a/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiPortAdminBehaviour.java b/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiPortAdminBehaviour.java
new file mode 100644
index 0000000..4f050b0
--- /dev/null
+++ b/drivers/gnmi/src/main/java/org/onosproject/drivers/gnmi/OpenConfigGnmiPortAdminBehaviour.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2019-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 gnmi.Gnmi;
+import org.onosproject.gnmi.api.GnmiClient;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.behaviour.PortAdmin;
+
+import java.util.concurrent.CompletableFuture;
+
+import static java.util.concurrent.CompletableFuture.completedFuture;
+
+/**
+ * Implementation of PortAdmin for gNMI devices with OpenConfig support.
+ */
+public class OpenConfigGnmiPortAdminBehaviour
+        extends AbstractGnmiHandlerBehaviour
+        implements PortAdmin {
+
+    @Override
+    public CompletableFuture<Boolean> enable(PortNumber number) {
+        doEnable(number, true);
+        // Always returning true is OK assuming this is used only by the
+        // GeneralDeviceProvider, which ignores the return value and instead
+        // waits for a gNMI Update over the Subscribe RPC.
+        return completedFuture(true);
+    }
+
+    @Override
+    public CompletableFuture<Boolean> disable(PortNumber number) {
+        doEnable(number, false);
+        return completedFuture(true);
+    }
+
+    @Override
+    public CompletableFuture<Boolean> isEnabled(PortNumber number) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    private void doEnable(PortNumber portNumber, boolean enabled) {
+        if (portNumber.isLogical()) {
+            log.warn("Cannot update port status for logical port {} on {}",
+                     portNumber, deviceId);
+            return;
+        }
+        final GnmiClient client = getClientByKey();
+        if (client == null) {
+            log.warn("Cannot update ports on {}, missing gNMI client", deviceId);
+            return;
+        }
+        final Gnmi.Path path = Gnmi.Path.newBuilder()
+                .addElem(Gnmi.PathElem.newBuilder().setName("interfaces").build())
+                .addElem(Gnmi.PathElem.newBuilder().setName("interface")
+                                 .putKey("name", portNumber.name()).build())
+                .addElem(Gnmi.PathElem.newBuilder().setName("config").build())
+                .addElem(Gnmi.PathElem.newBuilder().setName("enabled").build())
+                .build();
+        final Gnmi.TypedValue value = Gnmi.TypedValue.newBuilder()
+                .setBoolVal(enabled)
+                .build();
+        final Gnmi.SetRequest request = Gnmi.SetRequest.newBuilder()
+                .addUpdate(Gnmi.Update.newBuilder()
+                                   .setPath(path)
+                                   .setVal(value)
+                                   .build())
+                .build();
+
+        // Async submit request and forget about it. In case of errors, the
+        // client will log them. In case of success, we should receive a gNMI
+        // Update over the Subscribe RPC with the new oper status.
+        client.set(request);
+    }
+}
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 a9481a4..a41144e 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
@@ -17,6 +17,7 @@
 package org.onosproject.drivers.gnmi;
 
 import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
 import gnmi.Gnmi;
 import gnmi.Gnmi.GetRequest;
 import gnmi.Gnmi.GetResponse;
@@ -45,7 +46,7 @@
 
     private static final Map<Pair<DeviceId, PortNumber>, Long> PORT_START_TIMES =
             Maps.newConcurrentMap();
-    private static final String LAST_CHANGE = "last-change";
+    private static final String LAST_CHANGE = "last-changed";
 
     @Override
     public Collection<PortStatistics> discoverPortStatistics() {
@@ -67,10 +68,7 @@
             ifacePortNumberMapping.put(portName, port.number());
         });
 
-        GetResponse getResponse =
-                getFutureWithDeadline(client.get(getRequest.build()),
-                         "getting port counters",
-                                      GetResponse.getDefaultInstance());
+        GetResponse getResponse = Futures.getUnchecked(client.get(getRequest.build()));
 
         Map<String, Long> inPkts = Maps.newHashMap();
         Map<String, Long> outPkts = Maps.newHashMap();
diff --git a/drivers/gnmi/src/main/resources/gnmi-drivers.xml b/drivers/gnmi/src/main/resources/gnmi-drivers.xml
index 9af162f..0928284 100644
--- a/drivers/gnmi/src/main/resources/gnmi-drivers.xml
+++ b/drivers/gnmi/src/main/resources/gnmi-drivers.xml
@@ -22,6 +22,8 @@
                    impl="org.onosproject.drivers.gnmi.GnmiHandshaker"/>
         <behaviour api="org.onosproject.net.device.PortStatisticsDiscovery"
                    impl="org.onosproject.drivers.gnmi.OpenConfigGnmiPortStatisticsDiscovery"/>
+        <behaviour api="org.onosproject.net.behaviour.PortAdmin"
+                   impl="org.onosproject.drivers.gnmi.OpenConfigGnmiPortAdminBehaviour"/>
     </driver>
 </drivers>
 
diff --git a/drivers/stratum/src/main/java/org/onosproject/drivers/stratum/AbstractStratumBehaviour.java b/drivers/stratum/src/main/java/org/onosproject/drivers/stratum/AbstractStratumBehaviour.java
new file mode 100644
index 0000000..b7c56e9
--- /dev/null
+++ b/drivers/stratum/src/main/java/org/onosproject/drivers/stratum/AbstractStratumBehaviour.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019-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.stratum;
+
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+import org.onosproject.net.driver.DriverData;
+import org.onosproject.net.driver.DriverHandler;
+import org.onosproject.net.driver.HandlerBehaviour;
+
+/**
+ * Abstract implementation of a driver behaviour for Stratum devices that
+ * provides access to protocol-specific implementations of the same behavior.
+ *
+ * @param <B> type of behaviour
+ */
+public abstract class AbstractStratumBehaviour<B extends HandlerBehaviour>
+        extends AbstractHandlerBehaviour {
+
+    protected B p4runtime;
+    protected B gnmi;
+
+    public AbstractStratumBehaviour(B p4runtime, B gnmi) {
+        this.p4runtime = p4runtime;
+        this.gnmi = gnmi;
+    }
+
+    @Override
+    public void setHandler(DriverHandler handler) {
+        super.setHandler(handler);
+        p4runtime.setHandler(handler);
+        gnmi.setHandler(handler);
+    }
+
+    @Override
+    public void setData(DriverData data) {
+        super.setData(data);
+        p4runtime.setData(data);
+        gnmi.setData(data);
+    }
+}
diff --git a/drivers/stratum/src/main/java/org/onosproject/drivers/stratum/StratumDeviceDescriptionDiscovery.java b/drivers/stratum/src/main/java/org/onosproject/drivers/stratum/StratumDeviceDescriptionDiscovery.java
new file mode 100644
index 0000000..4a65e26
--- /dev/null
+++ b/drivers/stratum/src/main/java/org/onosproject/drivers/stratum/StratumDeviceDescriptionDiscovery.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2019-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.stratum;
+
+import org.onosproject.drivers.gnmi.OpenConfigGnmiDeviceDescriptionDiscovery;
+import org.onosproject.drivers.p4runtime.P4RuntimeDeviceDescriptionDiscovery;
+import org.onosproject.net.AnnotationKeys;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.Device;
+import org.onosproject.net.device.DefaultDeviceDescription;
+import org.onosproject.net.device.DeviceDescription;
+import org.onosproject.net.device.DeviceDescriptionDiscovery;
+import org.onosproject.net.device.PortDescription;
+
+import java.util.List;
+
+import static java.lang.String.format;
+
+/**
+ * Implementation of DeviceDescriptionDiscovery for Stratum devices.
+ */
+public class StratumDeviceDescriptionDiscovery
+        extends AbstractStratumBehaviour<DeviceDescriptionDiscovery>
+        implements DeviceDescriptionDiscovery {
+
+    private static final String UNKNOWN = "unknown";
+
+
+    public StratumDeviceDescriptionDiscovery() {
+        super(new P4RuntimeDeviceDescriptionDiscovery(),
+              new OpenConfigGnmiDeviceDescriptionDiscovery());
+    }
+
+    @Override
+    public DeviceDescription discoverDeviceDetails() {
+        final DeviceDescription p4Descr = p4runtime.discoverDeviceDetails();
+        final DeviceDescription gnmiDescr = gnmi.discoverDeviceDetails();
+        return new DefaultDeviceDescription(
+                data().deviceId().uri(),
+                Device.Type.SWITCH,
+                data().driver().manufacturer(),
+                data().driver().hwVersion(),
+                data().driver().swVersion(),
+                UNKNOWN,
+                p4Descr.chassisId(),
+                // Availability is mandated by P4Runtime.
+                p4Descr.isDefaultAvailable(),
+                DefaultAnnotations.builder()
+                        .set(AnnotationKeys.PROTOCOL, format(
+                                "%s, %s",
+                                p4Descr.annotations().value(AnnotationKeys.PROTOCOL),
+                                gnmiDescr.annotations().value(AnnotationKeys.PROTOCOL)))
+                        .build());
+    }
+
+    @Override
+    public List<PortDescription> discoverPortDetails() {
+        return gnmi.discoverPortDetails();
+    }
+}
diff --git a/drivers/stratum/src/main/java/org/onosproject/drivers/stratum/StratumHandshaker.java b/drivers/stratum/src/main/java/org/onosproject/drivers/stratum/StratumHandshaker.java
index 8762af0..8ce68d7 100644
--- a/drivers/stratum/src/main/java/org/onosproject/drivers/stratum/StratumHandshaker.java
+++ b/drivers/stratum/src/main/java/org/onosproject/drivers/stratum/StratumHandshaker.java
@@ -21,9 +21,6 @@
 import org.onosproject.net.MastershipRole;
 import org.onosproject.net.device.DeviceAgentListener;
 import org.onosproject.net.device.DeviceHandshaker;
-import org.onosproject.net.driver.AbstractHandlerBehaviour;
-import org.onosproject.net.driver.DriverData;
-import org.onosproject.net.driver.DriverHandler;
 import org.onosproject.net.provider.ProviderId;
 
 import java.util.concurrent.CompletableFuture;
@@ -31,33 +28,20 @@
 /**
  * Implementation of DeviceHandshaker for Stratum device.
  */
-public class StratumHandshaker extends AbstractHandlerBehaviour implements DeviceHandshaker {
-
-    private P4RuntimeHandshaker p4runtime;
-    private GnmiHandshaker gnmi;
+public class StratumHandshaker
+        extends AbstractStratumBehaviour<DeviceHandshaker>
+        implements DeviceHandshaker {
 
     public StratumHandshaker() {
-        p4runtime = new P4RuntimeHandshaker();
-        gnmi = new GnmiHandshaker();
-    }
-
-    @Override
-    public void setHandler(DriverHandler handler) {
-        super.setHandler(handler);
-        p4runtime.setHandler(handler);
-        gnmi.setHandler(handler);
-    }
-
-    @Override
-    public void setData(DriverData data) {
-        super.setData(data);
-        p4runtime.setData(data);
-        gnmi.setData(data);
+        super(new P4RuntimeHandshaker(), new GnmiHandshaker());
     }
 
     @Override
     public boolean isReachable() {
-        return p4runtime.isReachable() && gnmi.isReachable();
+        // Reachability is mainly used for mastership contests and it's a
+        // prerequisite for availability. We can probably live without gNMI and
+        // gNOI, but we will always need P4Runtime.
+        return p4runtime.isReachable();
     }
 
     @Override
diff --git a/drivers/stratum/src/main/resources/stratum-drivers.xml b/drivers/stratum/src/main/resources/stratum-drivers.xml
index fa0c0c4..f854d5f 100644
--- a/drivers/stratum/src/main/resources/stratum-drivers.xml
+++ b/drivers/stratum/src/main/resources/stratum-drivers.xml
@@ -19,6 +19,8 @@
             hwVersion="master" swVersion="Stratum" extends="p4runtime,gnmi">
         <behaviour api="org.onosproject.net.device.DeviceHandshaker"
                    impl="org.onosproject.drivers.stratum.StratumHandshaker"/>
+        <behaviour api="org.onosproject.net.device.DeviceDescriptionDiscovery"
+                   impl="org.onosproject.drivers.stratum.StratumDeviceDescriptionDiscovery"/>
     </driver>
 
     <driver name="stratum-dummy" manufacturer="Open Networking Foundation"