org.onosproject.drivers.odtn.openconfig.TerminalDeviceDiscovery.java Source code

Java tutorial

Introduction

Here is the source code for org.onosproject.drivers.odtn.openconfig.TerminalDeviceDiscovery.java

Source

/*
 * 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 IllegalStateException(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();
    }
}