[ONOS-7446] Implemented generic OpenConfig TerminalDevice driver

Change-Id: I9832af7f356338e813aabdd1232e86736ff07e16
diff --git a/drivers/odtn-driver/BUCK b/drivers/odtn-driver/BUCK
index ae771f6..77736e0 100644
--- a/drivers/odtn-driver/BUCK
+++ b/drivers/odtn-driver/BUCK
@@ -1,5 +1,6 @@
 COMPILE_DEPS = [
     '//lib:CORE_DEPS',
+    '//drivers/utilities:onos-drivers-utilities',
     '//protocols/netconf/api:onos-protocols-netconf-api',
     '//lib:commons-jxpath',
     '//apps/odtn/api:onos-apps-odtn-api',
diff --git a/drivers/odtn-driver/pom.xml b/drivers/odtn-driver/pom.xml
index 6f1df76..7049c98 100644
--- a/drivers/odtn-driver/pom.xml
+++ b/drivers/odtn-driver/pom.xml
@@ -46,6 +46,12 @@
         </dependency>
 
         <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-drivers-utilities</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
             <groupId>commons-jxpath</groupId>
             <artifactId>commons-jxpath</artifactId>
             <version>1.3</version>
diff --git a/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/TerminalDeviceDiscovery.java b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/TerminalDeviceDiscovery.java
new file mode 100644
index 0000000..9d60e7b
--- /dev/null
+++ b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/TerminalDeviceDiscovery.java
@@ -0,0 +1,544 @@
+/*
+ * 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.
+ *
+ * This work was partially supported by EC H2020 project METRO-HAUL (761727).
+ */
+
+package org.onosproject.drivers.odtn.openconfig;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import org.slf4j.Logger;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.concurrent.CompletableFuture;
+
+import org.onlab.packet.ChassisId;
+
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.XMLConfiguration;
+import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
+
+import org.onosproject.drivers.utilities.XmlConfigParser;
+
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.device.DeviceDescription;
+import org.onosproject.net.device.DeviceDescriptionDiscovery;
+import org.onosproject.net.device.DefaultDeviceDescription;
+import org.onosproject.net.device.DefaultPortDescription;
+import org.onosproject.net.device.DefaultPortDescription.Builder;
+import org.onosproject.net.device.PortDescription;
+
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.SparseAnnotations;
+import org.onosproject.net.Port.Type;
+import org.onosproject.net.PortNumber;
+
+import org.onosproject.netconf.NetconfController;
+import org.onosproject.netconf.NetconfDevice;
+import org.onosproject.netconf.NetconfException;
+import org.onosproject.netconf.NetconfSession;
+
+import com.google.common.collect.ImmutableList;
+
+import org.onosproject.odtn.behaviour.OdtnDeviceDescriptionDiscovery;
+
+
+/**
+ * Driver Implementation of the DeviceDescrption discovery for OpenConfig
+ * terminal devices.
+ *
+ */
+public class TerminalDeviceDiscovery
+    extends AbstractHandlerBehaviour
+    implements OdtnDeviceDescriptionDiscovery, DeviceDescriptionDiscovery {
+
+    private static final String RPC_TAG_NETCONF_BASE =
+        "<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">";
+
+    private static final String RPC_CLOSE_TAG = "</rpc>";
+
+    private static final String OC_PLATFORM_TYPES_TRANSCEIVER =
+        "oc-platform-types:TRANSCEIVER";
+
+    private static final String OC_PLATFORM_TYPES_PORT =
+        "oc-platform-types:PORT";
+
+    private static final String OC_TRANSPORT_TYPES_OPTICAL_CHANNEL =
+        "oc-opt-types:OPTICAL_CHANNEL";
+
+    private static final Logger log = getLogger(TerminalDeviceDiscovery.class);
+
+
+    /**
+     * Returns the NetconfSession with the device for which the method was called.
+     *
+     * @param deviceId device indetifier
+     *
+     * @return The netconf session or null
+     */
+    private NetconfSession getNetconfSession(DeviceId deviceId) {
+        NetconfController controller = handler().get(NetconfController.class);
+        NetconfDevice ncdev = controller.getDevicesMap().get(deviceId);
+        if (ncdev == null) {
+            log.trace("No netconf device, returning null session");
+            return null;
+        }
+        return ncdev.getSession();
+    }
+
+
+    /**
+     * Get the deviceId for which the methods apply.
+     *
+     * @return The deviceId as contained in the handler data
+     */
+    private DeviceId did() {
+        return handler().data().deviceId();
+    }
+
+
+    /**
+     * Get the device instance for which the methods apply.
+     *
+     * @return The device instance
+     */
+    private Device getDevice() {
+        DeviceService deviceService = checkNotNull(handler().get(DeviceService.class));
+        Device device = deviceService.getDevice(did());
+        return device;
+    }
+
+
+    /**
+     * Construct a String with a Netconf filtered get RPC Message.
+     *
+     * @param filter A valid XML tree with the filter to apply in the get
+     * @return a String containing the RPC XML Document
+     */
+    private String filteredGetBuilder(String filter) {
+        StringBuilder rpc = new StringBuilder(RPC_TAG_NETCONF_BASE);
+        rpc.append("<get>");
+        rpc.append("<filter type='subtree'>");
+        rpc.append(filter);
+        rpc.append("</filter>");
+        rpc.append("</get>");
+        rpc.append(RPC_CLOSE_TAG);
+        return rpc.toString();
+    }
+
+
+    /**
+     * Construct a String with a Netconf filtered get RPC Message.
+     *
+     * @param filter A valid XPath Expression with the filter to apply in the get
+     * @return a String containing the RPC XML Document
+     *
+     * Note: server must support xpath capability.
+
+     * <select=" /components/component[name='PORT-A-In-1']/properties/...
+     * ...property[name='onos-index']/config/value" type="xpath"/>
+     */
+    private String xpathFilteredGetBuilder(String filter) {
+        StringBuilder rpc = new StringBuilder(RPC_TAG_NETCONF_BASE);
+        rpc.append("<get>");
+        rpc.append("<filter type='xpath' select=\"");
+        rpc.append(filter);
+        rpc.append("\"/>");
+        rpc.append("</get>");
+        rpc.append(RPC_CLOSE_TAG);
+        return rpc.toString();
+    }
+
+
+    /**
+     * Builds a request to get Device details, operational data.
+     *
+     * @return A string with the Netconf RPC for a get with subtree rpcing based on
+     *    /components/component/state/type being oc-platform-types:OPERATING_SYSTEM
+     */
+    private String getDeviceDetailsBuilder() {
+        StringBuilder filter = new StringBuilder();
+        filter.append("<components xmlns='http://openconfig.net/yang/platform'>");
+        filter.append(" <component>");
+        filter.append("  <state>");
+        filter.append("   <type xmlns:oc-platform-types='http://openconfig.net/");
+        filter.append("yang/platform-types'>oc-platform-types:OPERATING_SYSTEM</type>");
+        filter.append("  </state>");
+        filter.append(" </component>");
+        filter.append("</components>");
+        return filteredGetBuilder(filter.toString());
+        /* I am not sure the alternative method is more efficient
+           try {
+           DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+           DocumentBuilder db = dbf.newDocumentBuilder();
+           Document doc = db.newDocument();
+           Element rpc = doc.createElementNS("urn:ietf:params:xml:ns:netconf:base:1.0", "rpc");
+           Element get = doc.createElement("get");
+           Element rpc = doc.createElement("rpc");
+           Element components = doc.createElementNS("http://openconfig.net/yang/platform", "components");
+           Element component = doc.createElement("component");
+           Element state = doc.createElement("state");
+           Element type  = doc.createElement("type");
+           type.setAttributeNS("http://www.w3.org/2000/xmlns/",
+           "xmlns:oc-platform-types", "http://openconfig.net/yang/platform-types");
+           type.appendChild(doc.createTextNode("oc-platform-types:OPERATING_SYSTEM"));
+           state.appendChild(type);
+           component.appendChild(state);
+           components.appendChild(component);
+           rpc.appendChild(components);
+           get.appendChild(rpc);
+           rpc.appendChild(get);
+           doc.appendChild(rpc);
+           return NetconfRpcParserUtil.toString(doc);
+           } catch (Exception e) {
+           throw new RuntimeException(new NetconfException("Exception in getDeviceDetailsBuilder", e));
+           }
+         */
+    }
+
+
+    /**
+     * Builds a request to get Device Components, config and operational data.
+     *
+     * @return A string with the Netconf RPC for a get with subtree rpcing based on
+     *    /components/
+     */
+    private String getDeviceComponentsBuilder() {
+        return filteredGetBuilder("<components xmlns='http://openconfig.net/yang/platform'/>");
+    }
+
+
+    /**
+     * Builds a request to get Device Ports, config and operational data.
+     *
+     * @return A string with the Netconf RPC for a get with subtree rpcing based on
+     *    /components/component/state/type being oc-platform-types:PORT
+     */
+    private String getDevicePortsBuilder() {
+        StringBuilder rpc = new StringBuilder();
+        rpc.append("<components xmlns='http://openconfig.net/yang/platform'>");
+        rpc.append(" <component><state>");
+        rpc.append("   <type xmlns:oc-platform-types='http://openconfig.net/");
+        rpc.append("yang/platform-types'>oc-platform-types:PORT</type>");
+        rpc.append(" </state></component>");
+        rpc.append("</components>");
+        return filteredGetBuilder(rpc.toString());
+    }
+
+
+    /**
+     * Returns a DeviceDescription with Device info.
+     *
+     * @return DeviceDescription or null
+     *
+     * //CHECKSTYLE:OFF
+     * <pre>{@code
+     * <data>
+     * <components xmlns="http://openconfig.net/yang/platform">
+     *  <component>
+     *   <state>
+     *     <name>FIRMWARE</name>
+     *     <type>oc-platform-types:OPERATING_SYSTEM</type>
+     *     <description>CTTC METRO-HAUL Emulated OpenConfig TerminalDevice</description>
+     *     <version>0.0.1</version>
+     *   </state>
+     *  </component>
+     * </components>
+     * </data>
+     *}</pre>
+     * //CHECKSTYLE:ON
+     */
+    @Override
+    public DeviceDescription discoverDeviceDetails() {
+        log.info("TerminalDeviceDiscovery::discoverDeviceDetails device {}", did());
+        boolean defaultAvailable = true;
+        SparseAnnotations annotations = DefaultAnnotations.builder().build();
+
+        // Other option "OTHER", we use ROADM for now
+        org.onosproject.net.Device.Type type =
+            org.onosproject.net.Device.Type.ROADM;
+
+        // Some defaults
+        String vendor       = "NOVENDOR";
+        String hwVersion    = "0.1.1";
+        String swVersion    = "0.1.1";
+        String serialNumber = "0xCAFEBEEF";
+        String chassisId    = "128";
+
+        // Get the session,
+        NetconfSession session = getNetconfSession(did());
+        if (session != null) {
+            try {
+                String reply = session.get(getDeviceDetailsBuilder());
+                // <rpc-reply> as root node
+                XMLConfiguration xconf = (XMLConfiguration) XmlConfigParser.loadXmlString(reply);
+                vendor       = xconf.getString("data/components/component/state/mfg-name", vendor);
+                serialNumber = xconf.getString("data/components/component/state/serial-no", serialNumber);
+                // Requires OpenConfig >= 2018
+                swVersion    = xconf.getString("data/components/component/state/software-version", swVersion);
+                hwVersion    = xconf.getString("data/components/component/state/hardware-version", hwVersion);
+            } catch (Exception e) {
+                throw new RuntimeException(new NetconfException("Failed to retrieve version info.", e));
+            }
+        } else {
+            log.info("TerminalDeviceDiscovery::discoverDeviceDetails - No netconf session for {}", did());
+        }
+        log.info("VENDOR    {}", vendor);
+        log.info("HWVERSION {}", hwVersion);
+        log.info("SWVERSION {}", swVersion);
+        log.info("SERIAL    {}", serialNumber);
+        log.info("CHASSISID {}", chassisId);
+        ChassisId cid = new ChassisId(Long.valueOf(chassisId, 10));
+        return new DefaultDeviceDescription(did().uri(),
+                type, vendor, hwVersion, swVersion, serialNumber,
+                cid, defaultAvailable, annotations);
+    }
+
+
+
+    /**
+     * Returns a list of PortDescriptions for the device.
+     *
+     * @return a list of descriptions.
+     *
+     * The RPC reply follows the following pattern:
+     * //CHECKSTYLE:OFF
+     * <pre>{@code
+     * <?xml version="1.0" encoding="UTF-8"?>
+     * <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="7">
+     * <data>
+     *   <components xmlns="http://openconfig.net/yang/platform">
+     *     <component>....
+     *     </component>
+     *     <component>....
+     *     </component>
+     *   </components>
+     * </data>
+     * </rpc-reply>
+     * }</pre>
+     * //CHECKSTYLE:ON
+     */
+    @Override
+    public List<PortDescription> discoverPortDetails() {
+        try {
+            NetconfSession session = getNetconfSession(did());
+            /*
+            Note: the method may get called before the netconf session is established
+            2018-05-24 14:01:43,607 | INFO
+            event NetworkConfigEvent{time=2018-05-24T14:01:43.602Z, type=CONFIG_ADDED, ....
+            configClass=class org.onosproject.netconf.config.NetconfDeviceConfig
+
+            2018-05-24 14:01:43,623 | INFO  | vice-installer-2 | TerminalDeviceDiscovery
+            TerminalDeviceDiscovery::discoverPortDetails netconf:127.0.0.1:830
+
+            2018-05-24 14:01:43,624 | ERROR | vice-installer-2 | TerminalDeviceDiscovery
+            org.onosproject.onos-drivers-metrohaul - 1.14.0.SNAPSHOT | Exception discoverPortDetails()
+
+            2018-05-24 14:01:43,631 | INFO  | vice-installer-1 | NetconfControllerImpl
+            Creating NETCONF session to netconf:127.0.0.1:830 with apache-mina
+             */
+            if (session == null) {
+                log.error("discoverPortDetails called with null session for {}", did());
+                return ImmutableList.of();
+            }
+
+            CompletableFuture<String> fut = session.rpc(getDeviceComponentsBuilder());
+            String rpcReply = fut.get();
+
+            XMLConfiguration xconf = (XMLConfiguration) XmlConfigParser.loadXmlString(rpcReply);
+            xconf.setExpressionEngine(new XPathExpressionEngine());
+
+            HierarchicalConfiguration components = xconf.configurationAt("data/components");
+            return parsePorts(components);
+        } catch (Exception e) {
+            log.error("Exception discoverPortDetails() {}", did(), e);
+            return ImmutableList.of();
+        }
+    }
+
+
+
+
+    /**
+     * Parses port information from OpenConfig XML configuration.
+     *
+     * @param components the XML document with components root.
+     * @return List of ports
+     *
+     * //CHECKSTYLE:OFF
+     * <pre>{@code
+     *   <components xmlns="http://openconfig.net/yang/platform">
+     *     <component>....
+     *     </component>
+     *     <component>....
+     *     </component>
+     *   </components>
+     * }</pre>
+     * //CHECKSTYLE:ON
+     */
+    protected List<PortDescription> parsePorts(HierarchicalConfiguration components) {
+        return components.configurationsAt("component")
+            .stream()
+            .filter(component -> {
+                    return !component.getString("name", "unknown")
+                            .equals("unknown") &&
+                        component.getString("state/type", "unknown")
+                            .equals(OC_PLATFORM_TYPES_PORT);
+                    })
+            .map(component -> {
+                try {
+                    // Pass the root document for cross-reference
+                    return parsePortComponent(component, components);
+                } catch (Exception e) {
+                    return null;
+                }
+                })
+            .filter(Objects::nonNull)
+            .collect(Collectors.toList());
+    }
+
+
+    /**
+     * Checks if a given component has a subcomponent of a given type.
+     *
+     * @param component subtree to parse looking for subcomponents.
+     * @param components the full components tree, to cross-ref in
+     *  case we need to check (sub)components' types.
+     *
+     * @return true or false
+     */
+     private boolean hasSubComponentOfType(
+            HierarchicalConfiguration component,
+            HierarchicalConfiguration components,
+            String type) {
+        long count = component.configurationsAt("subcomponents/subcomponent")
+            .stream()
+            .filter(subcomponent -> {
+                    String scName = subcomponent.getString("name");
+                    StringBuilder sb = new StringBuilder("component[name='");
+                    sb.append(scName);
+                    sb.append("']/state/type");
+                    String scType = components.getString(sb.toString(), "unknown");
+                    return scType.equals(type);
+                    })
+            .count();
+        return (count > 0);
+    }
+
+
+    /**
+     * Checks if a given component has a subcomponent of type OPTICAL_CHANNEL.
+     *
+     * @param component subtree to parse
+     * @param components the full components tree, to cross-ref in
+     *  case we need to check transceivers or optical channels.
+     *
+     * @return true or false
+     */
+     private boolean hasOpticalChannelSubComponent(
+             HierarchicalConfiguration component,
+             HierarchicalConfiguration components) {
+         return hasSubComponentOfType(component, components,
+                 OC_TRANSPORT_TYPES_OPTICAL_CHANNEL);
+     }
+
+
+     /**
+      *  Checks if a given component has a subcomponent of type TRANSCEIVER.
+      *
+      * @param component subtree to parse
+      * @param components the full components tree, to cross-ref in
+      *  case we need to check transceivers or optical channels.
+      *
+      * @return true or false
+      */
+     private boolean hasTransceiverSubComponent(
+             HierarchicalConfiguration component,
+             HierarchicalConfiguration components) {
+         return hasSubComponentOfType(component, components,
+                 OC_PLATFORM_TYPES_TRANSCEIVER);
+     }
+
+
+     /**
+      * Parses a component XML doc into a PortDescription.
+      *
+      * @param component subtree to parse. It must be a component ot type PORT.
+      * @param components the full components tree, to cross-ref in
+      *  case we need to check transceivers or optical channels.
+      *
+      * @return PortDescription or null if component does not have onos-index
+      */
+     private PortDescription parsePortComponent(
+             HierarchicalConfiguration component,
+             HierarchicalConfiguration components) {
+         Map<String, String> annotations = new HashMap<>();
+         String name = component.getString("name");
+         String type = component.getString("state/type");
+         log.info("Parsing Component {} type {}", name, type);
+         annotations.put(OdtnDeviceDescriptionDiscovery.OC_NAME, name);
+         annotations.put(OdtnDeviceDescriptionDiscovery.OC_TYPE, type);
+
+         // Store all properties as port properties
+         component.configurationsAt("properties/property")
+             .forEach(property -> {
+                     String pn = property.getString("name");
+                     String pv = property.getString("state/value");
+                     annotations.put(pn, pv);
+                     });
+
+         if (!annotations.containsKey(ONOS_PORT_INDEX)) {
+             log.warn("DEBUG: PORT {} does not include onos-index, skipping", name);
+             return null;
+         }
+
+         // The heuristic to know if it is client or line side
+         if (!annotations.containsKey(PORT_TYPE)) {
+             if (hasTransceiverSubComponent(component, components)) {
+                 annotations.put(PORT_TYPE, OdtnPortType.CLIENT.value());
+             } else if (hasOpticalChannelSubComponent(component, components)) {
+                 annotations.put(PORT_TYPE, OdtnPortType.LINE.value());
+             }
+         }
+
+         // Build the port
+         Builder builder = DefaultPortDescription.builder();
+         builder.withPortNumber(PortNumber.portNumber(
+                     Long.parseLong(annotations.get(ONOS_PORT_INDEX)), name));
+         if (annotations.get(PORT_TYPE)
+                 .equals(OdtnPortType.CLIENT.value())) {
+             log.info("Adding CLIENT port");
+             builder.type(Type.PACKET);
+         } else if (annotations.get(PORT_TYPE)
+                 .equals(OdtnPortType.LINE.value())) {
+             log.info("Adding LINE port");
+             builder.type(Type.OCH);
+         } else {
+             log.info("Unknown port added as CLIENT port");
+         }
+         builder.annotations(DefaultAnnotations.builder().putAll(annotations).build());
+         return builder.build();
+     }
+}
diff --git a/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/package-info.java b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/package-info.java
new file mode 100644
index 0000000..62d86d7
--- /dev/null
+++ b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * ODTN OpenConfig Driver package.
+ */
+package org.onosproject.drivers.odtn.openconfig;
diff --git a/drivers/odtn-driver/src/main/resources/odtn-drivers.xml b/drivers/odtn-driver/src/main/resources/odtn-drivers.xml
index 8d85e3a..4c44783 100644
--- a/drivers/odtn-driver/src/main/resources/odtn-drivers.xml
+++ b/drivers/odtn-driver/src/main/resources/odtn-drivers.xml
@@ -23,6 +23,10 @@
         <behaviour api="org.onosproject.odtn.behaviour.ConfigurableTransceiver"
                   impl="org.onosproject.odtn.behaviour.PlainTransceiver"/>
     </driver>
+    <driver name="terminal-device" manufacturer="CTTC" hwVersion="" swVersion="">
+        <behaviour api="org.onosproject.net.device.DeviceDescriptionDiscovery"
+                  impl="org.onosproject.drivers.odtn.openconfig.TerminalDeviceDiscovery"/>
+    </driver>
     <driver name="infinera-xt3300" manufacturer="infinera" hwVersion="xt3300" swVersion="18.0">
         <behaviour api="org.onosproject.net.device.DeviceDescriptionDiscovery"
                    impl="org.onosproject.drivers.odtn.InfineraOpenConfigDeviceDiscovery"/>