blob: 26bfbbb78e513b5394f115e130fc0958dc6212a3 [file] [log] [blame]
/*
* 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 IllegalStateException(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();
}
}