ONOS-7828 ODTN OpenConfig FlowRule and LambdaQuery.

Change-Id: I03212d2b8b544bae95f9f085eb1cadf883e5611d
diff --git a/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/TerminalDeviceFlowRuleProgrammable.java b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/TerminalDeviceFlowRuleProgrammable.java
new file mode 100644
index 0000000..d6f8aa2
--- /dev/null
+++ b/drivers/odtn-driver/src/main/java/org/onosproject/drivers/odtn/openconfig/TerminalDeviceFlowRuleProgrammable.java
@@ -0,0 +1,385 @@
+/*
+ * 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 com.google.common.collect.ImmutableList;
+import org.onlab.util.Frequency;
+import org.onosproject.drivers.odtn.impl.FlowRuleParser;
+import org.onosproject.drivers.odtn.impl.OpenConfigConnectionCache;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+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.onosproject.netconf.DatastoreId;
+import org.onosproject.netconf.NetconfController;
+import org.onosproject.netconf.NetconfException;
+import org.onosproject.netconf.NetconfSession;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathFactory;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Implementation of FlowRuleProgrammable interface for
+ * OpenConfig terminal devices.
+ */
+public class TerminalDeviceFlowRuleProgrammable
+        extends AbstractHandlerBehaviour implements FlowRuleProgrammable {
+
+    private static final Logger log =
+            LoggerFactory.getLogger(TerminalDeviceFlowRuleProgrammable.class);
+
+    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>";
+
+
+    /**
+     * Apply the flow entries specified in the collection rules.
+     *
+     * @param rules A collection of Flow Rules to be applied
+     * @return The collection of added Flow Entries
+     */
+    @Override
+    public Collection<FlowRule> applyFlowRules(Collection<FlowRule> rules) {
+        NetconfSession session = getNetconfSession();
+        if (session == null) {
+            openConfigError("null session");
+            return ImmutableList.of();
+        }
+        List<FlowRule> added = new ArrayList<>();
+        for (FlowRule r : rules) {
+            try {
+                applyFlowRule(session, r);
+            } catch (Exception e) {
+                openConfigError("Error {}", e);
+                continue;
+            }
+            getConnectionCache().add(did(), r);
+            added.add(r);
+        }
+        openConfigLog("applyFlowRules added {}", added.size());
+        return added;
+    }
+
+    /**
+     * Get the flow entries that are present on the device.
+     *
+     * @return A collection of Flow Entries
+     */
+    @Override
+    public Collection<FlowEntry> getFlowEntries() {
+        OpenConfigConnectionCache cache = getConnectionCache();
+        if (cache.get(did()) == null) {
+            return ImmutableList.of();
+        }
+
+        List<FlowEntry> entries = new ArrayList<>();
+        for (FlowRule r : cache.get(did())) {
+            entries.add(
+                    new DefaultFlowEntry(r, FlowEntry.FlowEntryState.ADDED, 0, 0, 0));
+        }
+        return entries;
+    }
+
+    /**
+     * Remove the specified flow rules.
+     *
+     * @param rules A collection of Flow Rules to be removed
+     * @return The collection of removed Flow Entries
+     */
+    @Override
+    public Collection<FlowRule> removeFlowRules(Collection<FlowRule> rules) {
+        NetconfSession session = getNetconfSession();
+        if (session == null) {
+            openConfigError("null session");
+            return ImmutableList.of();
+        }
+        List<FlowRule> removed = new ArrayList<>();
+        for (FlowRule r : rules) {
+            try {
+                removeFlowRule(session, r);
+            } catch (Exception e) {
+                openConfigError("Error {}", e);
+                continue;
+            }
+            getConnectionCache().add(did(), r);
+            removed.add(r);
+        }
+        openConfigLog("removedFlowRules removed {}", removed.size());
+        return removed;
+    }
+
+    private OpenConfigConnectionCache getConnectionCache() {
+        return OpenConfigConnectionCache.init();
+    }
+
+    // Context so XPath expressions are aware of XML namespaces
+    private static final NamespaceContext NS_CONTEXT = new NamespaceContext() {
+        @Override
+        public String getNamespaceURI(String prefix) {
+            if (prefix.equals("oc-platform-types")) {
+                return "http://openconfig.net/yang/platform-types";
+            }
+            if (prefix.equals("oc-opt-term")) {
+                return "http://openconfig.net/yang/terminal-device";
+            }
+            return null;
+        }
+
+        @Override
+        public Iterator getPrefixes(String val) {
+            return null;
+        }
+
+        @Override
+        public String getPrefix(String uri) {
+            return null;
+        }
+    };
+
+
+    /**
+     * Helper method to get the device id.
+     */
+    private DeviceId did() {
+        return data().deviceId();
+    }
+
+    /**
+     * Helper method to log from this class adding DeviceId.
+     */
+    private void openConfigLog(String format, Object... arguments) {
+        log.info("OPENCONFIG {}: " + format, did(), arguments);
+    }
+
+    /**
+     * Helper method to log an error from this class adding DeviceId.
+     */
+    private void openConfigError(String format, Object... arguments) {
+        log.error("OPENCONFIG {}: " + format, did(), arguments);
+    }
+
+
+    /**
+     * Helper method to get the Netconf Session.
+     */
+    private NetconfSession getNetconfSession() {
+        NetconfController controller =
+                checkNotNull(handler().get(NetconfController.class));
+        return controller.getNetconfDevice(did()).getSession();
+    }
+
+
+    /**
+     * 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 get request to retrieve Components and their
+     * properties (for the ONOS port, index).
+     *
+     * @return The filt content to send to the device.
+     */
+    private String getComponents() {
+        StringBuilder filt = new StringBuilder();
+        filt.append("<components xmlns='http://openconfig.net/yang/platform'>");
+        filt.append(" <component>");
+        filt.append("  <name/>");
+        filt.append("  <properties/>");
+        filt.append(" </component>");
+        filt.append("</components>");
+        return filteredGetBuilder(filt.toString());
+    }
+
+
+    /**
+     * Construct a get request to retrieve Optical Channels and
+     * the line port they are using.
+     * <p>
+     * This method is used to query the device so we can find the
+     * OpticalChannel component name that used a given line port.
+     *
+     * @return The filt content to send to the device.
+     */
+    private String getOpticalChannels() {
+        StringBuilder filt = new StringBuilder();
+        filt.append("<components xmlns='http://openconfig.net/yang/platform'>");
+        filt.append(" <component>");
+        filt.append("  <name/>");
+        filt.append("  <state/>");
+        filt.append("  <oc-opt-term:optical-channel xmlns:oc-opt-term"
+                + " = 'http://openconfig.net/yang/terminal-device'>");
+        filt.append("    <oc-opt-term:config>");
+        filt.append("     <oc-opt-term:line-port/>");
+        filt.append("    </oc-opt-term:config>");
+        filt.append("  </oc-opt-term:optical-channel>");
+        filt.append(" </component>");
+        filt.append("</components>");
+        return filteredGetBuilder(filt.toString());
+    }
+
+
+    /**
+     * Get the OpenConfig component name for the OpticalChannel component
+     * associated to the passed port number (typically a line side port, already
+     * mapped to ONOS port).
+     *
+     * @param session    The netconf session to the device.
+     * @param portNumber ONOS port number of the Line port ().
+     * @return the channel component name or null
+     */
+    private String getOpticalChannel(NetconfSession session,
+                                     PortNumber portNumber) {
+        try {
+            checkNotNull(session);
+            checkNotNull(portNumber);
+            XPath xp = XPathFactory.newInstance().newXPath();
+            xp.setNamespaceContext(NS_CONTEXT);
+
+            // Get the port name for a given port number
+            // We could iterate the port annotations too, no need to
+            // interact with device.
+            String xpGetPortName =
+                    "/rpc-reply/data/components/"
+                            +
+                            "component[./properties/property[name='onos-index']/config/value ='" +
+                            portNumber.toLong() + "']/"
+                            + "name/text()";
+
+            // Get all the components and their properties
+            String compReply = session.rpc(getComponents()).get();
+            DocumentBuilderFactory builderFactory =
+                    DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = builderFactory.newDocumentBuilder();
+            Document document =
+                    builder.parse(new InputSource(new StringReader(compReply)));
+            String portName = xp.evaluate(xpGetPortName, document);
+            String xpGetOptChannelName =
+                    "/rpc-reply/data/components/"
+                            + "component[./optical-channel/config/line-port='" + portName +
+                            "']/name/text()";
+
+            String optChannelReply = session.rpc(getOpticalChannels()).get();
+            document =
+                    builder.parse(new InputSource(new StringReader(optChannelReply)));
+            return xp.evaluate(xpGetOptChannelName, document);
+        } catch (Exception e) {
+            openConfigError("Exception {}", e);
+            return null;
+        }
+    }
+
+
+    private void setOpticalChannelFrequency(NetconfSession session,
+                                            String optChannel, Frequency freq)
+            throws NetconfException {
+        StringBuilder sb = new StringBuilder();
+        sb.append(
+                "<components xmlns='http://openconfig.net/yang/platform'>"
+                        + "<component operation='merge'>"
+                        + "<name>" + optChannel + "</name>"
+                        + "<oc-opt-term:optical-channel "
+                        +
+                        "    xmlns:oc-opt-term='http://openconfig.net/yang/terminal-device'>"
+                        + "  <oc-opt-term:config>"
+                        + "  <oc-opt-term:frequency>" + (long) freq.asMHz() +
+                        "</oc-opt-term:frequency>"
+                        + "    </oc-opt-term:config>"
+                        + " </oc-opt-term:optical-channel>"
+                        + "</component>"
+                        + "</components>");
+
+        boolean ok =
+                session.editConfig(DatastoreId.RUNNING, null, sb.toString());
+        if (!ok) {
+            throw new NetconfException("error writing channel frequency");
+        }
+    }
+
+
+    /**
+     * Apply the flowrule.
+     *
+     * Note: only bidirectional are supported as of now,
+     * given OpenConfig note (below). In consequence, only the
+     * TX rules are actually mapped to netconf ops.
+     * <p>
+     * https://github.com/openconfig/public/blob/master/release/models
+     * /optical-transport/openconfig-terminal-device.yang
+     * <p>
+     * Directionality:
+     * To maintain simplicity in the model, the configuration is
+     * described from client-to-line direction.  The assumption is that
+     * equivalent reverse configuration is implicit, resulting in
+     * the same line-to-client configuration.
+     *
+     * @param session The Netconf session.
+     * @param r       Flow Rules to be applied.
+     * @throws NetconfException if exchange goes wrong
+     */
+    protected void applyFlowRule(NetconfSession session, FlowRule r) throws NetconfException {
+        FlowRuleParser frp = new FlowRuleParser(r);
+        if (!frp.isReceiver()) {
+            String optChannel = getOpticalChannel(session, frp.getPortNumber());
+            setOpticalChannelFrequency(session, optChannel,
+                    frp.getCentralFrequency());
+        }
+    }
+
+
+    protected void removeFlowRule(NetconfSession session, FlowRule r)
+            throws NetconfException {
+        FlowRuleParser frp = new FlowRuleParser(r);
+        if (!frp.isReceiver()) {
+            String optChannel = getOpticalChannel(session, frp.getPortNumber());
+            setOpticalChannelFrequency(session, optChannel, Frequency.ofMHz(0));
+        }
+    }
+}