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();
+ }
+
+ }
}