blob: 26bfbbb78e513b5394f115e130fc0958dc6212a3 [file] [log] [blame]
Ramon Casellasfbcd2942018-05-25 14:41:40 +02001/*
2 * Copyright 2018-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 * This work was partially supported by EC H2020 project METRO-HAUL (761727).
17 */
18
19package org.onosproject.drivers.odtn.openconfig;
20
21import static com.google.common.base.Preconditions.checkNotNull;
22import static org.slf4j.LoggerFactory.getLogger;
23
24import org.slf4j.Logger;
25
26import java.util.HashMap;
27import java.util.List;
28import java.util.Map;
29import java.util.Objects;
30import java.util.stream.Collectors;
31import java.util.concurrent.CompletableFuture;
32
33import org.onlab.packet.ChassisId;
34
35import org.apache.commons.configuration.HierarchicalConfiguration;
36import org.apache.commons.configuration.XMLConfiguration;
37import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
38
39import org.onosproject.drivers.utilities.XmlConfigParser;
40
41import org.onosproject.net.Device;
42import org.onosproject.net.DeviceId;
43import org.onosproject.net.device.DeviceService;
44import org.onosproject.net.device.DeviceDescription;
45import org.onosproject.net.device.DeviceDescriptionDiscovery;
46import org.onosproject.net.device.DefaultDeviceDescription;
47import org.onosproject.net.device.DefaultPortDescription;
48import org.onosproject.net.device.DefaultPortDescription.Builder;
49import org.onosproject.net.device.PortDescription;
50
51import org.onosproject.net.driver.AbstractHandlerBehaviour;
52
53import org.onosproject.net.DefaultAnnotations;
54import org.onosproject.net.SparseAnnotations;
55import org.onosproject.net.Port.Type;
56import org.onosproject.net.PortNumber;
57
58import org.onosproject.netconf.NetconfController;
59import org.onosproject.netconf.NetconfDevice;
60import org.onosproject.netconf.NetconfException;
61import org.onosproject.netconf.NetconfSession;
62
63import com.google.common.collect.ImmutableList;
64
65import org.onosproject.odtn.behaviour.OdtnDeviceDescriptionDiscovery;
66
67
68/**
69 * Driver Implementation of the DeviceDescrption discovery for OpenConfig
70 * terminal devices.
71 *
72 */
73public class TerminalDeviceDiscovery
74 extends AbstractHandlerBehaviour
75 implements OdtnDeviceDescriptionDiscovery, DeviceDescriptionDiscovery {
76
77 private static final String RPC_TAG_NETCONF_BASE =
78 "<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">";
79
80 private static final String RPC_CLOSE_TAG = "</rpc>";
81
82 private static final String OC_PLATFORM_TYPES_TRANSCEIVER =
83 "oc-platform-types:TRANSCEIVER";
84
85 private static final String OC_PLATFORM_TYPES_PORT =
86 "oc-platform-types:PORT";
87
88 private static final String OC_TRANSPORT_TYPES_OPTICAL_CHANNEL =
89 "oc-opt-types:OPTICAL_CHANNEL";
90
91 private static final Logger log = getLogger(TerminalDeviceDiscovery.class);
92
93
94 /**
95 * Returns the NetconfSession with the device for which the method was called.
96 *
97 * @param deviceId device indetifier
98 *
99 * @return The netconf session or null
100 */
101 private NetconfSession getNetconfSession(DeviceId deviceId) {
102 NetconfController controller = handler().get(NetconfController.class);
103 NetconfDevice ncdev = controller.getDevicesMap().get(deviceId);
104 if (ncdev == null) {
105 log.trace("No netconf device, returning null session");
106 return null;
107 }
108 return ncdev.getSession();
109 }
110
111
112 /**
113 * Get the deviceId for which the methods apply.
114 *
115 * @return The deviceId as contained in the handler data
116 */
117 private DeviceId did() {
118 return handler().data().deviceId();
119 }
120
121
122 /**
123 * Get the device instance for which the methods apply.
124 *
125 * @return The device instance
126 */
127 private Device getDevice() {
128 DeviceService deviceService = checkNotNull(handler().get(DeviceService.class));
129 Device device = deviceService.getDevice(did());
130 return device;
131 }
132
133
134 /**
135 * Construct a String with a Netconf filtered get RPC Message.
136 *
137 * @param filter A valid XML tree with the filter to apply in the get
138 * @return a String containing the RPC XML Document
139 */
140 private String filteredGetBuilder(String filter) {
141 StringBuilder rpc = new StringBuilder(RPC_TAG_NETCONF_BASE);
142 rpc.append("<get>");
143 rpc.append("<filter type='subtree'>");
144 rpc.append(filter);
145 rpc.append("</filter>");
146 rpc.append("</get>");
147 rpc.append(RPC_CLOSE_TAG);
148 return rpc.toString();
149 }
150
151
152 /**
153 * Construct a String with a Netconf filtered get RPC Message.
154 *
155 * @param filter A valid XPath Expression with the filter to apply in the get
156 * @return a String containing the RPC XML Document
157 *
158 * Note: server must support xpath capability.
159
160 * <select=" /components/component[name='PORT-A-In-1']/properties/...
161 * ...property[name='onos-index']/config/value" type="xpath"/>
162 */
163 private String xpathFilteredGetBuilder(String filter) {
164 StringBuilder rpc = new StringBuilder(RPC_TAG_NETCONF_BASE);
165 rpc.append("<get>");
166 rpc.append("<filter type='xpath' select=\"");
167 rpc.append(filter);
168 rpc.append("\"/>");
169 rpc.append("</get>");
170 rpc.append(RPC_CLOSE_TAG);
171 return rpc.toString();
172 }
173
174
175 /**
176 * Builds a request to get Device details, operational data.
177 *
178 * @return A string with the Netconf RPC for a get with subtree rpcing based on
179 * /components/component/state/type being oc-platform-types:OPERATING_SYSTEM
180 */
181 private String getDeviceDetailsBuilder() {
182 StringBuilder filter = new StringBuilder();
183 filter.append("<components xmlns='http://openconfig.net/yang/platform'>");
184 filter.append(" <component>");
185 filter.append(" <state>");
186 filter.append(" <type xmlns:oc-platform-types='http://openconfig.net/");
187 filter.append("yang/platform-types'>oc-platform-types:OPERATING_SYSTEM</type>");
188 filter.append(" </state>");
189 filter.append(" </component>");
190 filter.append("</components>");
191 return filteredGetBuilder(filter.toString());
192 /* I am not sure the alternative method is more efficient
193 try {
194 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
195 DocumentBuilder db = dbf.newDocumentBuilder();
196 Document doc = db.newDocument();
197 Element rpc = doc.createElementNS("urn:ietf:params:xml:ns:netconf:base:1.0", "rpc");
198 Element get = doc.createElement("get");
199 Element rpc = doc.createElement("rpc");
200 Element components = doc.createElementNS("http://openconfig.net/yang/platform", "components");
201 Element component = doc.createElement("component");
202 Element state = doc.createElement("state");
203 Element type = doc.createElement("type");
204 type.setAttributeNS("http://www.w3.org/2000/xmlns/",
205 "xmlns:oc-platform-types", "http://openconfig.net/yang/platform-types");
206 type.appendChild(doc.createTextNode("oc-platform-types:OPERATING_SYSTEM"));
207 state.appendChild(type);
208 component.appendChild(state);
209 components.appendChild(component);
210 rpc.appendChild(components);
211 get.appendChild(rpc);
212 rpc.appendChild(get);
213 doc.appendChild(rpc);
214 return NetconfRpcParserUtil.toString(doc);
215 } catch (Exception e) {
Ray Milkeydbd38212018-07-02 09:18:09 -0700216 throw new IllegalStateException(new NetconfException("Exception in getDeviceDetailsBuilder", e));
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200217 }
218 */
219 }
220
221
222 /**
223 * Builds a request to get Device Components, config and operational data.
224 *
225 * @return A string with the Netconf RPC for a get with subtree rpcing based on
226 * /components/
227 */
228 private String getDeviceComponentsBuilder() {
229 return filteredGetBuilder("<components xmlns='http://openconfig.net/yang/platform'/>");
230 }
231
232
233 /**
234 * Builds a request to get Device Ports, config and operational data.
235 *
236 * @return A string with the Netconf RPC for a get with subtree rpcing based on
237 * /components/component/state/type being oc-platform-types:PORT
238 */
239 private String getDevicePortsBuilder() {
240 StringBuilder rpc = new StringBuilder();
241 rpc.append("<components xmlns='http://openconfig.net/yang/platform'>");
242 rpc.append(" <component><state>");
243 rpc.append(" <type xmlns:oc-platform-types='http://openconfig.net/");
244 rpc.append("yang/platform-types'>oc-platform-types:PORT</type>");
245 rpc.append(" </state></component>");
246 rpc.append("</components>");
247 return filteredGetBuilder(rpc.toString());
248 }
249
250
251 /**
252 * Returns a DeviceDescription with Device info.
253 *
254 * @return DeviceDescription or null
255 *
256 * //CHECKSTYLE:OFF
257 * <pre>{@code
258 * <data>
259 * <components xmlns="http://openconfig.net/yang/platform">
260 * <component>
261 * <state>
262 * <name>FIRMWARE</name>
263 * <type>oc-platform-types:OPERATING_SYSTEM</type>
264 * <description>CTTC METRO-HAUL Emulated OpenConfig TerminalDevice</description>
265 * <version>0.0.1</version>
266 * </state>
267 * </component>
268 * </components>
269 * </data>
270 *}</pre>
271 * //CHECKSTYLE:ON
272 */
273 @Override
274 public DeviceDescription discoverDeviceDetails() {
275 log.info("TerminalDeviceDiscovery::discoverDeviceDetails device {}", did());
276 boolean defaultAvailable = true;
277 SparseAnnotations annotations = DefaultAnnotations.builder().build();
278
279 // Other option "OTHER", we use ROADM for now
280 org.onosproject.net.Device.Type type =
281 org.onosproject.net.Device.Type.ROADM;
282
283 // Some defaults
284 String vendor = "NOVENDOR";
285 String hwVersion = "0.1.1";
286 String swVersion = "0.1.1";
287 String serialNumber = "0xCAFEBEEF";
288 String chassisId = "128";
289
290 // Get the session,
291 NetconfSession session = getNetconfSession(did());
292 if (session != null) {
293 try {
294 String reply = session.get(getDeviceDetailsBuilder());
295 // <rpc-reply> as root node
296 XMLConfiguration xconf = (XMLConfiguration) XmlConfigParser.loadXmlString(reply);
297 vendor = xconf.getString("data/components/component/state/mfg-name", vendor);
298 serialNumber = xconf.getString("data/components/component/state/serial-no", serialNumber);
299 // Requires OpenConfig >= 2018
300 swVersion = xconf.getString("data/components/component/state/software-version", swVersion);
301 hwVersion = xconf.getString("data/components/component/state/hardware-version", hwVersion);
302 } catch (Exception e) {
Ray Milkey0fa1c1a2018-05-30 08:27:04 -0700303 throw new IllegalStateException(new NetconfException("Failed to retrieve version info.", e));
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200304 }
305 } else {
306 log.info("TerminalDeviceDiscovery::discoverDeviceDetails - No netconf session for {}", did());
307 }
308 log.info("VENDOR {}", vendor);
309 log.info("HWVERSION {}", hwVersion);
310 log.info("SWVERSION {}", swVersion);
311 log.info("SERIAL {}", serialNumber);
312 log.info("CHASSISID {}", chassisId);
313 ChassisId cid = new ChassisId(Long.valueOf(chassisId, 10));
314 return new DefaultDeviceDescription(did().uri(),
315 type, vendor, hwVersion, swVersion, serialNumber,
316 cid, defaultAvailable, annotations);
317 }
318
319
320
321 /**
322 * Returns a list of PortDescriptions for the device.
323 *
324 * @return a list of descriptions.
325 *
326 * The RPC reply follows the following pattern:
327 * //CHECKSTYLE:OFF
328 * <pre>{@code
329 * <?xml version="1.0" encoding="UTF-8"?>
330 * <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="7">
331 * <data>
332 * <components xmlns="http://openconfig.net/yang/platform">
333 * <component>....
334 * </component>
335 * <component>....
336 * </component>
337 * </components>
338 * </data>
339 * </rpc-reply>
340 * }</pre>
341 * //CHECKSTYLE:ON
342 */
343 @Override
344 public List<PortDescription> discoverPortDetails() {
345 try {
346 NetconfSession session = getNetconfSession(did());
347 /*
348 Note: the method may get called before the netconf session is established
349 2018-05-24 14:01:43,607 | INFO
350 event NetworkConfigEvent{time=2018-05-24T14:01:43.602Z, type=CONFIG_ADDED, ....
351 configClass=class org.onosproject.netconf.config.NetconfDeviceConfig
352
353 2018-05-24 14:01:43,623 | INFO | vice-installer-2 | TerminalDeviceDiscovery
354 TerminalDeviceDiscovery::discoverPortDetails netconf:127.0.0.1:830
355
356 2018-05-24 14:01:43,624 | ERROR | vice-installer-2 | TerminalDeviceDiscovery
357 org.onosproject.onos-drivers-metrohaul - 1.14.0.SNAPSHOT | Exception discoverPortDetails()
358
359 2018-05-24 14:01:43,631 | INFO | vice-installer-1 | NetconfControllerImpl
360 Creating NETCONF session to netconf:127.0.0.1:830 with apache-mina
361 */
362 if (session == null) {
363 log.error("discoverPortDetails called with null session for {}", did());
364 return ImmutableList.of();
365 }
366
367 CompletableFuture<String> fut = session.rpc(getDeviceComponentsBuilder());
368 String rpcReply = fut.get();
369
370 XMLConfiguration xconf = (XMLConfiguration) XmlConfigParser.loadXmlString(rpcReply);
371 xconf.setExpressionEngine(new XPathExpressionEngine());
372
373 HierarchicalConfiguration components = xconf.configurationAt("data/components");
374 return parsePorts(components);
375 } catch (Exception e) {
376 log.error("Exception discoverPortDetails() {}", did(), e);
377 return ImmutableList.of();
378 }
379 }
380
381
382
383
384 /**
385 * Parses port information from OpenConfig XML configuration.
386 *
387 * @param components the XML document with components root.
388 * @return List of ports
389 *
390 * //CHECKSTYLE:OFF
391 * <pre>{@code
392 * <components xmlns="http://openconfig.net/yang/platform">
393 * <component>....
394 * </component>
395 * <component>....
396 * </component>
397 * </components>
398 * }</pre>
399 * //CHECKSTYLE:ON
400 */
401 protected List<PortDescription> parsePorts(HierarchicalConfiguration components) {
402 return components.configurationsAt("component")
403 .stream()
404 .filter(component -> {
405 return !component.getString("name", "unknown")
406 .equals("unknown") &&
407 component.getString("state/type", "unknown")
408 .equals(OC_PLATFORM_TYPES_PORT);
409 })
410 .map(component -> {
411 try {
412 // Pass the root document for cross-reference
413 return parsePortComponent(component, components);
414 } catch (Exception e) {
415 return null;
416 }
417 })
418 .filter(Objects::nonNull)
419 .collect(Collectors.toList());
420 }
421
422
423 /**
424 * Checks if a given component has a subcomponent of a given type.
425 *
426 * @param component subtree to parse looking for subcomponents.
427 * @param components the full components tree, to cross-ref in
428 * case we need to check (sub)components' types.
429 *
430 * @return true or false
431 */
432 private boolean hasSubComponentOfType(
433 HierarchicalConfiguration component,
434 HierarchicalConfiguration components,
435 String type) {
436 long count = component.configurationsAt("subcomponents/subcomponent")
437 .stream()
438 .filter(subcomponent -> {
439 String scName = subcomponent.getString("name");
440 StringBuilder sb = new StringBuilder("component[name='");
441 sb.append(scName);
442 sb.append("']/state/type");
443 String scType = components.getString(sb.toString(), "unknown");
444 return scType.equals(type);
445 })
446 .count();
447 return (count > 0);
448 }
449
450
451 /**
452 * Checks if a given component has a subcomponent of type OPTICAL_CHANNEL.
453 *
454 * @param component subtree to parse
455 * @param components the full components tree, to cross-ref in
456 * case we need to check transceivers or optical channels.
457 *
458 * @return true or false
459 */
460 private boolean hasOpticalChannelSubComponent(
461 HierarchicalConfiguration component,
462 HierarchicalConfiguration components) {
463 return hasSubComponentOfType(component, components,
464 OC_TRANSPORT_TYPES_OPTICAL_CHANNEL);
465 }
466
467
468 /**
469 * Checks if a given component has a subcomponent of type TRANSCEIVER.
470 *
471 * @param component subtree to parse
472 * @param components the full components tree, to cross-ref in
473 * case we need to check transceivers or optical channels.
474 *
475 * @return true or false
476 */
477 private boolean hasTransceiverSubComponent(
478 HierarchicalConfiguration component,
479 HierarchicalConfiguration components) {
480 return hasSubComponentOfType(component, components,
481 OC_PLATFORM_TYPES_TRANSCEIVER);
482 }
483
484
485 /**
486 * Parses a component XML doc into a PortDescription.
487 *
488 * @param component subtree to parse. It must be a component ot type PORT.
489 * @param components the full components tree, to cross-ref in
490 * case we need to check transceivers or optical channels.
491 *
492 * @return PortDescription or null if component does not have onos-index
493 */
494 private PortDescription parsePortComponent(
495 HierarchicalConfiguration component,
496 HierarchicalConfiguration components) {
497 Map<String, String> annotations = new HashMap<>();
498 String name = component.getString("name");
499 String type = component.getString("state/type");
500 log.info("Parsing Component {} type {}", name, type);
501 annotations.put(OdtnDeviceDescriptionDiscovery.OC_NAME, name);
502 annotations.put(OdtnDeviceDescriptionDiscovery.OC_TYPE, type);
503
504 // Store all properties as port properties
505 component.configurationsAt("properties/property")
506 .forEach(property -> {
507 String pn = property.getString("name");
508 String pv = property.getString("state/value");
509 annotations.put(pn, pv);
510 });
511
512 if (!annotations.containsKey(ONOS_PORT_INDEX)) {
513 log.warn("DEBUG: PORT {} does not include onos-index, skipping", name);
514 return null;
515 }
516
517 // The heuristic to know if it is client or line side
518 if (!annotations.containsKey(PORT_TYPE)) {
519 if (hasTransceiverSubComponent(component, components)) {
520 annotations.put(PORT_TYPE, OdtnPortType.CLIENT.value());
521 } else if (hasOpticalChannelSubComponent(component, components)) {
522 annotations.put(PORT_TYPE, OdtnPortType.LINE.value());
523 }
524 }
525
526 // Build the port
527 Builder builder = DefaultPortDescription.builder();
528 builder.withPortNumber(PortNumber.portNumber(
529 Long.parseLong(annotations.get(ONOS_PORT_INDEX)), name));
530 if (annotations.get(PORT_TYPE)
531 .equals(OdtnPortType.CLIENT.value())) {
532 log.info("Adding CLIENT port");
533 builder.type(Type.PACKET);
534 } else if (annotations.get(PORT_TYPE)
535 .equals(OdtnPortType.LINE.value())) {
536 log.info("Adding LINE port");
537 builder.type(Type.OCH);
538 } else {
539 log.info("Unknown port added as CLIENT port");
540 }
541 builder.annotations(DefaultAnnotations.builder().putAll(annotations).build());
542 return builder.build();
543 }
544}