Add gNMI-based ODTN driver

Change-Id: Ic1df82797ee9f60956f46f4c3431e8c74f4548b2
diff --git a/drivers/odtn-driver/BUILD b/drivers/odtn-driver/BUILD
index be11910..bcba0ed 100644
--- a/drivers/odtn-driver/BUILD
+++ b/drivers/odtn-driver/BUILD
@@ -11,6 +11,10 @@
     "//drivers/optical:onos-drivers-optical",
     "//apps/faultmanagement/fmcli:onos-apps-faultmanagement-fmcli",  # Enabling Alarm stuff
     "//apps/faultmanagement/fmmgr:onos-apps-faultmanagement-fmmgr-native",
+    "//drivers/gnmi:onos-drivers-gnmi",
+    "//protocols/gnmi/stub:onos-protocols-gnmi-stub",
+    "//protocols/gnmi/api:onos-protocols-gnmi-api",
+    "//protocols/grpc/utils:onos-protocols-grpc-utils",
 ]
 
 TEST_DEPS = TEST_ADAPTERS + [
@@ -43,6 +47,7 @@
         "org.onosproject.drivers.netconf",
         "org.onosproject.drivers.optical",
         "org.onosproject.optical-model",
+        "org.onosproject.drivers.gnmi",
     ],
     title = "ODTN Driver",
     url = "https://wiki.onosproject.org/display/ODTN/ODTN",
diff --git a/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDeviceDiscovery.java b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDeviceDiscovery.java
new file mode 100644
index 0000000..b64aaea
--- /dev/null
+++ b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDeviceDiscovery.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2020-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.odtn.openconfig;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Streams;
+import gnmi.Gnmi;
+import org.onosproject.drivers.gnmi.OpenConfigGnmiDeviceDescriptionDiscovery;
+import org.onosproject.gnmi.api.GnmiUtils.GnmiPathBuilder;
+import org.onosproject.net.AnnotationKeys;
+import org.onosproject.net.ChannelSpacing;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.Device;
+import org.onosproject.net.OchSignal;
+import org.onosproject.net.OduSignalType;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DefaultDeviceDescription;
+import org.onosproject.net.device.DeviceDescription;
+import org.onosproject.net.device.PortDescription;
+import org.onosproject.net.optical.device.OchPortHelper;
+import org.onosproject.odtn.behaviour.OdtnDeviceDescriptionDiscovery;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.onosproject.gnmi.api.GnmiUtils.pathToString;
+
+/**
+ * A ODTN device discovery behaviour based on gNMI and OpenConfig model.
+ *
+ * This behavior is based on the origin gNMI OpenConfig device description discovery
+ * with additional logic to discover optical ports for this device.
+ *
+ * To find all optical port name and info, it queries all components with path:
+ *  /components/component[name=*]
+ * And it uses components with type "OPTICAL_CHANNEL" to find optical ports
+ *
+ */
+public class GnmiTerminalDeviceDiscovery
+        extends OpenConfigGnmiDeviceDescriptionDiscovery
+        implements OdtnDeviceDescriptionDiscovery {
+
+    private static final Logger log = LoggerFactory.getLogger(GnmiTerminalDeviceDiscovery.class);
+    private static final String COMPONENT_TYPE_PATH_TEMPLATE =
+            "/components/component[name=%s]/state/type";
+    private static final String LINE_PORT_PATH_TEMPLATE =
+            "/components/component[name=%s]/optical-channel/config/line-port";
+
+    @Override
+    public DeviceDescription discoverDeviceDetails() {
+        return new DefaultDeviceDescription(super.discoverDeviceDetails(),
+                                            Device.Type.TERMINAL_DEVICE);
+    }
+
+    @Override
+    public List<PortDescription> discoverPortDetails() {
+        if (!setupBehaviour("discoverPortDetails()")) {
+            return Collections.emptyList();
+        }
+
+        // Get all components
+        Gnmi.Path path = GnmiPathBuilder.newBuilder()
+                .addElem("components")
+                .addElem("component").withKeyValue("name", "*")
+                .build();
+
+        Gnmi.GetRequest req = Gnmi.GetRequest.newBuilder()
+                .addPath(path)
+                .setEncoding(Gnmi.Encoding.PROTO)
+                .build();
+        Gnmi.GetResponse resp;
+        try {
+            resp = client.get(req).get();
+        } catch (ExecutionException | InterruptedException e) {
+            log.warn("unable to get components via gNMI: {}", e.getMessage());
+            return Collections.emptyList();
+        }
+
+        Multimap<String, Gnmi.Update> componentUpdates = HashMultimap.create();
+        resp.getNotificationList().stream()
+                .map(Gnmi.Notification::getUpdateList)
+                .flatMap(List::stream)
+                .forEach(u -> {
+                    // Get component name
+                    // /components/component[name=?]
+                    Gnmi.Path p = u.getPath();
+                    if (p.getElemCount() < 2) {
+                        // Invalid path
+                        return;
+                    }
+                    String name = p.getElem(1)
+                            .getKeyOrDefault("name", null);
+
+                    // Collect gNMI updates for the component.
+                    // name -> a set of gNMI updates
+                    if (name != null) {
+                        componentUpdates.put(name, u);
+                    }
+                });
+
+        Stream<PortDescription> normalPorts = super.discoverPortDetails().stream();
+        Stream<PortDescription> opticalPorts = componentUpdates.keySet().stream()
+                .map(name -> convertComponentToOdtnPortDesc(name, componentUpdates.get(name)))
+                .filter(Objects::nonNull);
+        return Streams.concat(normalPorts, opticalPorts)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Converts gNMI updates to ODTN port description.
+     *
+     * Paths we expected per optical port component:
+     * /components/component/state/type
+     * /components/component/optical-channel/config/line-port
+     *
+     * @param name component name
+     * @param updates gNMI updates
+     * @return port description, null if it is not a valid component config/state
+     */
+    private PortDescription
+        convertComponentToOdtnPortDesc(String name, Collection<Gnmi.Update> updates) {
+        Map<String, Gnmi.TypedValue> pathValue = Maps.newHashMap();
+        updates.forEach(u -> pathValue.put(pathToString(u.getPath()), u.getVal()));
+
+        String componentTypePathStr =
+                String.format(COMPONENT_TYPE_PATH_TEMPLATE, name);
+        Gnmi.TypedValue componentType =
+                pathValue.get(componentTypePathStr);
+
+        if (componentType == null ||
+                !componentType.getStringVal().equals("OPTICAL_CHANNEL")) {
+            // Ignore the component which is not a optical channel type.
+            return null;
+        }
+
+        Map<String, String> annotations = Maps.newHashMap();
+        annotations.put(OC_NAME, name);
+        annotations.put(OC_TYPE, componentType.getStringVal());
+
+        String linePortPathStr =
+                String.format(LINE_PORT_PATH_TEMPLATE, name);
+        Gnmi.TypedValue linePort = pathValue.get(linePortPathStr);
+
+        // Invalid optical port
+        if (linePort == null) {
+            return null;
+        }
+
+        // According to CassiniTerminalDevice class, we expected to received a string with
+        // this format: port-[port id].
+        // And we use "port id" from the string as the port number.
+        // However, if we can't get port id from line port value, we will use
+        // hash number of the port name. (According to TerminalDeviceDiscovery class)
+        String linePortString = linePort.getStringVal();
+        long portId = name.hashCode();
+        if (linePortString.contains("-") && !linePortString.endsWith("-")) {
+            try {
+                portId = Long.parseUnsignedLong(linePortString.split("-")[1]);
+            } catch (NumberFormatException e) {
+                log.warn("Invalid line port string: {}, use {}", linePortString, portId);
+            }
+        }
+
+        annotations.put(AnnotationKeys.PORT_NAME, linePortString);
+        annotations.putIfAbsent(PORT_TYPE,
+                OdtnDeviceDescriptionDiscovery.OdtnPortType.LINE.value());
+        annotations.putIfAbsent(ONOS_PORT_INDEX, Long.toString(portId));
+        annotations.putIfAbsent(CONNECTION_ID, "connection-" + portId);
+
+        OchSignal signalId = OchSignal.newDwdmSlot(ChannelSpacing.CHL_50GHZ, 1);
+        return OchPortHelper.ochPortDescription(
+                PortNumber.portNumber(portId, name),
+                true,
+                OduSignalType.ODU4, // TODO: discover type via gNMI if possible
+                true,
+                signalId,
+                DefaultAnnotations.builder().putAll(annotations).build());
+    }
+}
diff --git a/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDeviceFlowRuleProgrammable.java b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDeviceFlowRuleProgrammable.java
new file mode 100644
index 0000000..7d9cae5
--- /dev/null
+++ b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDeviceFlowRuleProgrammable.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2020-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.odtn.openconfig;
+
+import com.google.common.collect.ImmutableList;
+import gnmi.Gnmi;
+import org.onlab.util.Frequency;
+import org.onosproject.drivers.odtn.impl.DeviceConnectionCache;
+import org.onosproject.drivers.odtn.impl.FlowRuleParser;
+import org.onosproject.gnmi.api.GnmiClient;
+import org.onosproject.gnmi.api.GnmiController;
+import org.onosproject.gnmi.api.GnmiUtils.GnmiPathBuilder;
+import org.onosproject.grpc.utils.AbstractGrpcHandlerBehaviour;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultFlowEntry;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleProgrammable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static org.onosproject.odtn.behaviour.OdtnDeviceDescriptionDiscovery.OC_NAME;
+
+/**
+ * A FlowRuleProgrammable behavior which converts flow rules to gNMI calls that sets
+ * frequency for the optical component.
+ */
+public class GnmiTerminalDeviceFlowRuleProgrammable
+        extends AbstractGrpcHandlerBehaviour<GnmiClient, GnmiController>
+        implements FlowRuleProgrammable {
+
+    private static final Logger log =
+            LoggerFactory.getLogger(GnmiTerminalDeviceFlowRuleProgrammable.class);
+
+    public GnmiTerminalDeviceFlowRuleProgrammable() {
+        super(GnmiController.class);
+    }
+
+    @Override
+    public Collection<FlowEntry> getFlowEntries() {
+        // TODO: currently, we store flow rules in a cluster store. Should check if rule/config exists via gNMI.
+        if (!setupBehaviour("getFlowEntries")) {
+            return Collections.emptyList();
+        }
+        DeviceConnectionCache cache = getConnectionCache();
+        Set<FlowRule> cachedRules = cache.get(deviceId);
+        if (cachedRules == null) {
+            return ImmutableList.of();
+        }
+
+        return cachedRules.stream()
+                .filter(Objects::nonNull)
+                .map(r -> new DefaultFlowEntry(r, FlowEntry.FlowEntryState.ADDED, 0, 0, 0))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Collection<FlowRule> applyFlowRules(Collection<FlowRule> rules) {
+        if (!setupBehaviour("applyFlowRules")) {
+            return Collections.emptyList();
+        }
+        List<FlowRule> added = new ArrayList<>();
+        for (FlowRule r : rules) {
+            String connectionId = applyFlowRule(r);
+            if (connectionId != null) {
+                getConnectionCache().add(deviceId, connectionId, r);
+                added.add(r);
+            }
+        }
+        return added;
+    }
+
+    @Override
+    public Collection<FlowRule> removeFlowRules(Collection<FlowRule> rules) {
+        if (!setupBehaviour("removeFlowRules")) {
+            return Collections.emptyList();
+        }
+        List<FlowRule> removed = new ArrayList<>();
+        for (FlowRule r : rules) {
+            String connectionId = removeFlowRule(r);
+            if (connectionId != null) {
+                getConnectionCache().remove(deviceId, connectionId);
+                removed.add(r);
+            }
+        }
+        return removed;
+    }
+
+    private String applyFlowRule(FlowRule r) {
+        FlowRuleParser frp = new FlowRuleParser(r);
+        if (!frp.isReceiver()) {
+            String opticalPortName = getOpticalPortName(frp.getPortNumber());
+            if (opticalPortName == null) {
+                log.warn("[Apply] No optical port name found from port {}, skipped",
+                        frp.getPortNumber());
+                return null;
+            }
+            if (!setOpticalPortFrequency(opticalPortName, frp.getCentralFrequency())) {
+                // Already logged in setOpticalChannelFrequency function
+                return null;
+            }
+            return opticalPortName + ":" + frp.getCentralFrequency().asGHz();
+        }
+        return String.valueOf(frp.getCentralFrequency().asGHz());
+
+    }
+
+    private String removeFlowRule(FlowRule r) {
+        FlowRuleParser frp = new FlowRuleParser(r);
+        if (!frp.isReceiver()) {
+            String opticalPortName = getOpticalPortName(frp.getPortNumber());
+            if (opticalPortName == null) {
+                log.warn("[Remove] No optical port name found from port {}, skipped",
+                         frp.getPortNumber());
+                return null;
+            }
+            if (!setOpticalPortFrequency(opticalPortName, Frequency.ofMHz(0))) {
+                // Already logged in setOpticalChannelFrequency function
+                return null;
+            }
+            return opticalPortName + ":" + frp.getCentralFrequency().asGHz();
+        }
+        return String.valueOf(frp.getCentralFrequency().asGHz());
+    }
+
+    private boolean setOpticalPortFrequency(String opticalPortName, Frequency freq) {
+        // gNMI set
+        // /components/component[name=opticalPortName]/optical-channel/config/frequency
+        Gnmi.Path path = GnmiPathBuilder.newBuilder()
+                .addElem("components")
+                .addElem("component").withKeyValue("name", opticalPortName)
+                .addElem("optical-channel")
+                .addElem("config")
+                .addElem("frequency")
+                .build();
+        Gnmi.TypedValue val = Gnmi.TypedValue.newBuilder()
+                .setUintVal((long) freq.asMHz())
+                .build();
+        Gnmi.Update update = Gnmi.Update.newBuilder()
+                .setPath(path)
+                .setVal(val)
+                .build();
+        Gnmi.SetRequest req = Gnmi.SetRequest.newBuilder()
+                .addUpdate(update)
+                .build();
+        try {
+            client.set(req).get();
+            return true;
+        } catch (ExecutionException | InterruptedException e) {
+            log.warn("Got exception when performing gNMI set operation: {}", e.getMessage());
+            log.warn("{}", req);
+        }
+        return false;
+    }
+
+    private String getOpticalPortName(PortNumber portNumber) {
+        Port clientPort = handler().get(DeviceService.class).getPort(deviceId, portNumber);
+        if (clientPort == null) {
+            log.warn("Unable to get port from device {}, port {}", deviceId, portNumber);
+            return null;
+        }
+        return clientPort.annotations().value(OC_NAME);
+    }
+
+    private DeviceConnectionCache getConnectionCache() {
+        return DeviceConnectionCache.init();
+    }
+}
diff --git a/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDeviceModulationConfig.java b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDeviceModulationConfig.java
new file mode 100644
index 0000000..5911293
--- /dev/null
+++ b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDeviceModulationConfig.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2020-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.odtn.openconfig;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
+import gnmi.Gnmi;
+import org.onosproject.gnmi.api.GnmiClient;
+import org.onosproject.gnmi.api.GnmiController;
+import org.onosproject.gnmi.api.GnmiUtils.GnmiPathBuilder;
+import org.onosproject.grpc.utils.AbstractGrpcHandlerBehaviour;
+import org.onosproject.net.ModulationScheme;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.behaviour.ModulationConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Modulation Config behavior for gNMI and OpenConfig model based device.
+ */
+public class GnmiTerminalDeviceModulationConfig<T>
+        extends AbstractGrpcHandlerBehaviour<GnmiClient, GnmiController>
+        implements ModulationConfig<T> {
+
+    public static Logger log = LoggerFactory.getLogger(GnmiTerminalDeviceModulationConfig.class);
+
+    private static final BiMap<Long, ModulationScheme> OPERATIONAL_MODE_TO_MODULATION =
+            ImmutableBiMap.<Long, ModulationScheme>builder()
+                    .put(1L, ModulationScheme.DP_QPSK)
+                    .put(2L, ModulationScheme.DP_16QAM)
+                    .put(3L, ModulationScheme.DP_8QAM)
+                    .build();
+
+    public GnmiTerminalDeviceModulationConfig() {
+        super(GnmiController.class);
+    }
+
+    @Override
+    public Optional<ModulationScheme> getModulationScheme(PortNumber portNumber, T component) {
+        if (!setupBehaviour("getModulationScheme")) {
+            return Optional.empty();
+        }
+        // Get value from path
+        //  /components/component[name=]/optical-channel/state/operational-mode
+        // And convert operational mode (uint64 bit mask) to ModulationScheme enum
+
+        // First we need to find component name (from port annotation)
+        String ocName = getOcName(portNumber);
+
+        // Query operational mode from device
+        Gnmi.Path path = GnmiPathBuilder.newBuilder()
+                .addElem("components")
+                .addElem("component").withKeyValue("name", ocName)
+                .addElem("optical-channel")
+                .addElem("state")
+                .addElem("operational-mode")
+                .build();
+
+        Gnmi.GetRequest req = Gnmi.GetRequest.newBuilder()
+                .addPath(path)
+                .setEncoding(Gnmi.Encoding.PROTO)
+                .build();
+
+        Gnmi.GetResponse resp;
+        try {
+            resp = client.get(req).get();
+        } catch (ExecutionException | InterruptedException e) {
+            log.warn("Unable to get operational mode from device {}, port {}: {}",
+                     deviceId, portNumber, e.getMessage());
+            return Optional.empty();
+        }
+
+        // Get operational mode value from gNMI get response
+        // Here we assume we get only one response
+        if (resp.getNotificationCount() == 0 || resp.getNotification(0).getUpdateCount() == 0) {
+            log.warn("No update message found");
+            return Optional.empty();
+        }
+
+        Gnmi.Update update = resp.getNotification(0).getUpdate(0);
+        Gnmi.TypedValue operationalModeVal = update.getVal();
+
+        if (operationalModeVal == null) {
+            log.warn("No operational mode found");
+            return Optional.empty();
+        }
+
+        return Optional.ofNullable(
+                OPERATIONAL_MODE_TO_MODULATION.getOrDefault(operationalModeVal.getUintVal(), null));
+    }
+
+    @Override
+    public void setModulationScheme(PortNumber portNumber, T component, long bitRate) {
+        if (!setupBehaviour("getModulationScheme")) {
+            return;
+        }
+        // Sets value to path
+        //  /components/component[name]/optical-channel/config/operational-mode
+
+        // First we convert bit rate to modulation scheme to operational mode
+        ModulationScheme modulationScheme = ModulationScheme.DP_16QAM;
+        // Use DP_QPSK if bit rate is less or equals to 100 Gbps
+        if (bitRate <= 100) {
+            modulationScheme = ModulationScheme.DP_QPSK;
+        }
+        long operationalMode = OPERATIONAL_MODE_TO_MODULATION.inverse().get(modulationScheme);
+
+        // Build gNMI set request
+        String ocName = getOcName(portNumber);
+        Gnmi.Path path = GnmiPathBuilder.newBuilder()
+                .addElem("components")
+                .addElem("component").withKeyValue("name", ocName)
+                .addElem("optical-channel")
+                .addElem("config")
+                .addElem("operational-mode")
+                .build();
+
+        Gnmi.TypedValue val = Gnmi.TypedValue.newBuilder()
+                .setUintVal(operationalMode)
+                .build();
+
+        Gnmi.Update update = Gnmi.Update.newBuilder()
+                .setPath(path)
+                .setVal(val)
+                .build();
+
+        Gnmi.SetRequest req = Gnmi.SetRequest.newBuilder()
+                .addUpdate(update)
+                .build();
+
+        try {
+            client.set(req).get();
+        } catch (ExecutionException | InterruptedException e) {
+            log.warn("Unable to set operational mode to device {}, port {}, mode: {}: {}",
+                    deviceId, portNumber, operationalMode, e.getMessage());
+        }
+    }
+
+    private String getOcName(PortNumber portNumber) {
+        return deviceService.getPort(deviceId, portNumber).annotations().value("oc-name");
+    }
+}
diff --git a/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDevicePowerConfig.java b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDevicePowerConfig.java
new file mode 100644
index 0000000..792b46c
--- /dev/null
+++ b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/GnmiTerminalDevicePowerConfig.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2020-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.
+ *
+ * This work was partially supported by EC H2020 project METRO-HAUL (761727).
+ */
+
+package org.onosproject.drivers.odtn.openconfig;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Range;
+import gnmi.Gnmi;
+import org.onosproject.gnmi.api.GnmiClient;
+import org.onosproject.gnmi.api.GnmiController;
+import org.onosproject.gnmi.api.GnmiUtils.GnmiPathBuilder;
+import org.onosproject.grpc.utils.AbstractGrpcHandlerBehaviour;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.behaviour.PowerConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * PowerConfig behaviour for gNMI and OpenConfig model based device.
+ */
+public class GnmiTerminalDevicePowerConfig<T>
+        extends AbstractGrpcHandlerBehaviour<GnmiClient, GnmiController>
+        implements PowerConfig<T> {
+
+    private static final Logger log = LoggerFactory.getLogger(GnmiTerminalDevicePowerConfig.class);
+    private static final int DEFAULT_OC_POWER_PRECISION = 2;
+    private static final Collection<Port.Type> OPTICAL_TYPES = ImmutableSet.of(Port.Type.FIBER,
+                    Port.Type.PACKET,
+                    Port.Type.ODUCLT,
+                    Port.Type.OCH,
+                    Port.Type.OMS,
+                    Port.Type.OTU);
+
+    public GnmiTerminalDevicePowerConfig() {
+        super(GnmiController.class);
+    }
+
+    @Override
+    public Optional<Double> getTargetPower(PortNumber port, T component) {
+        if (!setupBehaviour("getTargetPower")) {
+            return Optional.empty();
+        }
+        if (!isOpticalPort(port)) {
+            return Optional.empty();
+        }
+        // path: /components/component[name=<name>]/optical-channel/config/target-output-power
+        return getValueFromPath(getOcName(port), "config/target-output-power");
+    }
+
+    @Override
+    public void setTargetPower(PortNumber port, T component, double power) {
+        if (!setupBehaviour("setTargetPower")) {
+            return;
+        }
+        if (!isOpticalPort(port)) {
+            return;
+        }
+        setValueToPath(getOcName(port), "config/target-output-power", power);
+    }
+
+    @Override
+    public Optional<Double> currentPower(PortNumber port, T component) {
+        if (!setupBehaviour("currentPower")) {
+            return Optional.empty();
+        }
+        if (!isOpticalPort(port)) {
+            return Optional.empty();
+        }
+        // path: /components/component[name=<name>]/optical-channel/state/output-power/instant
+        return getValueFromPath(getOcName(port), "state/output-power/instant");
+    }
+
+    @Override
+    public Optional<Double> currentInputPower(PortNumber port, T component) {
+        if (!setupBehaviour("currentInputPower")) {
+            return Optional.empty();
+        }
+        if (!isOpticalPort(port)) {
+            return Optional.empty();
+        }
+        // path: /components/component[name=<name>]/optical-channel/state/input-power/instant
+        return getValueFromPath(getOcName(port), "state/input-power/instant");
+    }
+
+    @Override
+    public Optional<Range<Double>> getTargetPowerRange(PortNumber port, Object component) {
+        if (!isOpticalPort(port)) {
+            return Optional.empty();
+        }
+
+        // From CassiniTerminalDevicePowerConfig
+        double targetMin = -30;
+        double targetMax = 1;
+        return Optional.of(Range.open(targetMin, targetMax));
+    }
+
+    @Override
+    public Optional<Range<Double>> getInputPowerRange(PortNumber port, Object component) {
+        if (!isOpticalPort(port)) {
+            return Optional.empty();
+        }
+
+        // From CassiniTerminalDevicePowerConfig
+        double targetMin = -30;
+        double targetMax = 1;
+        return Optional.of(Range.open(targetMin, targetMax));
+    }
+
+    private String getOcName(PortNumber portNumber) {
+        if (!setupBehaviour("getOcName")) {
+            return null;
+        }
+        return deviceService.getPort(deviceId, portNumber).annotations().value("oc-name");
+    }
+
+    private boolean isOpticalPort(PortNumber portNumber) {
+        if (!setupBehaviour("isOpticalPort")) {
+            return false;
+        }
+        return OPTICAL_TYPES.contains(deviceService.getPort(deviceId, portNumber).type());
+    }
+
+    private Optional<Double> getValueFromPath(String ocName, String subPath) {
+        Gnmi.GetRequest req = Gnmi.GetRequest.newBuilder()
+                .addPath(buildPathWithSubPath(ocName, subPath))
+                .setEncoding(Gnmi.Encoding.PROTO)
+                .build();
+        try {
+            Gnmi.GetResponse resp = client.get(req).get();
+            // Here we assume we have only one response
+            if (resp.getNotificationCount() == 0 || resp.getNotification(0).getUpdateCount() == 0) {
+                log.warn("Empty response for sub-path {}, component {}", subPath, ocName);
+                return Optional.empty();
+            }
+            Gnmi.Update update = resp.getNotification(0).getUpdate(0);
+            Gnmi.Decimal64 value = update.getVal().getDecimalVal();
+            return Optional.of(decimal64ToDouble(value));
+        } catch (ExecutionException | InterruptedException e) {
+            log.warn("Unable to get value from optical sub-path {} for component {}: {}",
+                     subPath, ocName, e.getMessage());
+            return Optional.empty();
+        }
+    }
+
+    private void setValueToPath(String ocName, String subPath, Double value) {
+        Gnmi.TypedValue val = Gnmi.TypedValue.newBuilder()
+                .setDecimalVal(doubleToDecimal64(value, DEFAULT_OC_POWER_PRECISION))
+                .build();
+        Gnmi.Update update = Gnmi.Update.newBuilder()
+                .setPath(buildPathWithSubPath(ocName, subPath))
+                .setVal(val)
+                .build();
+        Gnmi.SetRequest req = Gnmi.SetRequest.newBuilder()
+                .addUpdate(update)
+                .build();
+        try {
+            client.set(req).get();
+        } catch (ExecutionException | InterruptedException e) {
+            log.warn("Unable to set optical sub-path {}, component {}, value {}: {}",
+                     subPath, ocName, value, e.getMessage());
+        }
+    }
+
+    private Gnmi.Path buildPathWithSubPath(String ocName, String subPath) {
+        String[] elems = subPath.split("/");
+        GnmiPathBuilder pathBuilder = GnmiPathBuilder.newBuilder()
+                .addElem("components")
+                .addElem("component").withKeyValue("name", ocName)
+                .addElem("optical-channel");
+        for (String elem : elems) {
+            pathBuilder.addElem(elem);
+        }
+        return pathBuilder.build();
+    }
+
+    private Double decimal64ToDouble(Gnmi.Decimal64 value) {
+        double result = value.getDigits();
+        if (value.getPrecision() != 0) {
+            result = result / Math.pow(10, value.getPrecision());
+        }
+        return result;
+    }
+
+    private Gnmi.Decimal64 doubleToDecimal64(Double value, int precision) {
+        return Gnmi.Decimal64.newBuilder()
+                .setDigits((long) (value * Math.pow(10, precision)))
+                .setPrecision(precision)
+                .build();
+    }
+}
\ No newline at end of file
diff --git a/drivers/odtn-driver/src/main/resources/odtn-drivers.xml b/drivers/odtn-driver/src/main/resources/odtn-drivers.xml
index dfc418a..a9e110f 100644
--- a/drivers/odtn-driver/src/main/resources/odtn-drivers.xml
+++ b/drivers/odtn-driver/src/main/resources/odtn-drivers.xml
@@ -227,5 +227,20 @@
     <behaviour api="org.onosproject.net.behaviour.ModulationConfig"
                impl="org.onosproject.drivers.odtn.CassiniModulationOpenConfig"/>
     </driver>
+
+    <driver name="gnmi-openconfig-terminal-device" manufacturer="OpenConfig" hwVersion="Unknown" swVersion="gNMI">
+        <behaviour api="org.onosproject.net.device.DeviceDescriptionDiscovery"
+                   impl="org.onosproject.drivers.odtn.openconfig.GnmiTerminalDeviceDiscovery"/>
+        <behaviour api="org.onosproject.odtn.behaviour.OdtnDeviceDescriptionDiscovery"
+                   impl="org.onosproject.drivers.odtn.openconfig.GnmiTerminalDeviceDiscovery"/>
+        <behaviour api="org.onosproject.net.flow.FlowRuleProgrammable"
+                   impl="org.onosproject.drivers.odtn.openconfig.GnmiTerminalDeviceFlowRuleProgrammable"/>
+        <behaviour api="org.onosproject.net.behaviour.PowerConfig"
+                   impl="org.onosproject.drivers.odtn.openconfig.GnmiTerminalDevicePowerConfig" />
+        <behaviour api="org.onosproject.net.behaviour.ModulationConfig"
+                   impl="org.onosproject.drivers.odtn.openconfig.GnmiTerminalDeviceModulationConfig" />
+        <behaviour api="org.onosproject.net.device.DeviceHandshaker"
+                   impl="org.onosproject.drivers.gnmi.GnmiHandshakerStandalone" />
+    </driver>
 </drivers>
 
diff --git a/protocols/gnmi/api/src/main/java/org/onosproject/gnmi/api/GnmiUtils.java b/protocols/gnmi/api/src/main/java/org/onosproject/gnmi/api/GnmiUtils.java
index 6a71a19..22f9a5a 100644
--- a/protocols/gnmi/api/src/main/java/org/onosproject/gnmi/api/GnmiUtils.java
+++ b/protocols/gnmi/api/src/main/java/org/onosproject/gnmi/api/GnmiUtils.java
@@ -16,6 +16,8 @@
 
 package org.onosproject.gnmi.api;
 
+import com.google.common.collect.Lists;
+import gnmi.Gnmi;
 import gnmi.Gnmi.Path;
 
 import java.util.List;
@@ -52,4 +54,57 @@
         });
         return pathStringBuilder.toString();
     }
+
+    /**
+     * Helper class which builds gNMI path.
+     *
+     * Example usage:
+     * Path: /interfaces/interface[name=if1]/state/oper-status
+     * Java code:
+     * <code>
+     * Gnmi.Path path = GnmiPathBuilder.newBuilder()
+     *     .addElem("interfaces")
+     *     .addElem("interface").withKeyValue("name", "if1")
+     *     .addElem("state")
+     *     .addElem("oper-status")
+     *     .build();
+     * </code>
+     */
+    public static final class GnmiPathBuilder {
+        List<Gnmi.PathElem> elemList;
+        private GnmiPathBuilder() {
+            elemList = Lists.newArrayList();
+        }
+
+        public static GnmiPathBuilder newBuilder() {
+            return new GnmiPathBuilder();
+        }
+
+        public GnmiPathBuilder addElem(String elemName) {
+            Gnmi.PathElem elem =
+                    Gnmi.PathElem.newBuilder()
+                            .setName(elemName)
+                            .build();
+            elemList.add(elem);
+            return this;
+        }
+        public GnmiPathBuilder withKeyValue(String key, String value) {
+            if (elemList.isEmpty()) {
+                // Invalid case. ignore it
+                return this;
+            }
+            Gnmi.PathElem lastElem = elemList.remove(elemList.size() - 1);
+            Gnmi.PathElem newElem =
+                    Gnmi.PathElem.newBuilder(lastElem)
+                            .putKey(key, value)
+                            .build();
+            elemList.add(newElem);
+            return this;
+        }
+
+        public Gnmi.Path build() {
+            return Gnmi.Path.newBuilder().addAllElem(elemList).build();
+        }
+
+    }
 }