blob: b65f15c7d780cd363d2d2913e7ab389b1b234289 [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
Ramon Casellas03f194f2018-11-15 16:06:02 +010035
Ramon Casellasfbcd2942018-05-25 14:41:40 +020036import org.apache.commons.configuration.HierarchicalConfiguration;
37import org.apache.commons.configuration.XMLConfiguration;
38import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
39
40import org.onosproject.drivers.utilities.XmlConfigParser;
41
42import org.onosproject.net.Device;
43import org.onosproject.net.DeviceId;
44import org.onosproject.net.device.DeviceService;
45import org.onosproject.net.device.DeviceDescription;
46import org.onosproject.net.device.DeviceDescriptionDiscovery;
47import org.onosproject.net.device.DefaultDeviceDescription;
48import org.onosproject.net.device.DefaultPortDescription;
49import org.onosproject.net.device.DefaultPortDescription.Builder;
50import org.onosproject.net.device.PortDescription;
51
52import org.onosproject.net.driver.AbstractHandlerBehaviour;
53
54import org.onosproject.net.DefaultAnnotations;
55import org.onosproject.net.SparseAnnotations;
56import org.onosproject.net.Port.Type;
57import org.onosproject.net.PortNumber;
Ramon Casellas03f194f2018-11-15 16:06:02 +010058import org.onosproject.net.OchSignal;
59import org.onosproject.net.optical.device.OchPortHelper;
60import org.onosproject.net.OduSignalType;
61import org.onosproject.net.ChannelSpacing;
Ramon Casellasfbcd2942018-05-25 14:41:40 +020062
63import org.onosproject.netconf.NetconfController;
64import org.onosproject.netconf.NetconfDevice;
65import org.onosproject.netconf.NetconfException;
66import org.onosproject.netconf.NetconfSession;
67
68import com.google.common.collect.ImmutableList;
69
70import org.onosproject.odtn.behaviour.OdtnDeviceDescriptionDiscovery;
71
72
73/**
74 * Driver Implementation of the DeviceDescrption discovery for OpenConfig
75 * terminal devices.
76 *
77 */
78public class TerminalDeviceDiscovery
79 extends AbstractHandlerBehaviour
80 implements OdtnDeviceDescriptionDiscovery, DeviceDescriptionDiscovery {
81
82 private static final String RPC_TAG_NETCONF_BASE =
83 "<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">";
84
85 private static final String RPC_CLOSE_TAG = "</rpc>";
86
87 private static final String OC_PLATFORM_TYPES_TRANSCEIVER =
88 "oc-platform-types:TRANSCEIVER";
89
90 private static final String OC_PLATFORM_TYPES_PORT =
91 "oc-platform-types:PORT";
92
93 private static final String OC_TRANSPORT_TYPES_OPTICAL_CHANNEL =
94 "oc-opt-types:OPTICAL_CHANNEL";
95
96 private static final Logger log = getLogger(TerminalDeviceDiscovery.class);
97
98
99 /**
100 * Returns the NetconfSession with the device for which the method was called.
101 *
102 * @param deviceId device indetifier
103 *
104 * @return The netconf session or null
105 */
106 private NetconfSession getNetconfSession(DeviceId deviceId) {
107 NetconfController controller = handler().get(NetconfController.class);
108 NetconfDevice ncdev = controller.getDevicesMap().get(deviceId);
109 if (ncdev == null) {
110 log.trace("No netconf device, returning null session");
111 return null;
112 }
113 return ncdev.getSession();
114 }
115
116
117 /**
118 * Get the deviceId for which the methods apply.
119 *
120 * @return The deviceId as contained in the handler data
121 */
122 private DeviceId did() {
123 return handler().data().deviceId();
124 }
125
126
127 /**
128 * Get the device instance for which the methods apply.
129 *
130 * @return The device instance
131 */
132 private Device getDevice() {
133 DeviceService deviceService = checkNotNull(handler().get(DeviceService.class));
134 Device device = deviceService.getDevice(did());
135 return device;
136 }
137
138
139 /**
140 * Construct a String with a Netconf filtered get RPC Message.
141 *
142 * @param filter A valid XML tree with the filter to apply in the get
143 * @return a String containing the RPC XML Document
144 */
145 private String filteredGetBuilder(String filter) {
146 StringBuilder rpc = new StringBuilder(RPC_TAG_NETCONF_BASE);
147 rpc.append("<get>");
148 rpc.append("<filter type='subtree'>");
149 rpc.append(filter);
150 rpc.append("</filter>");
151 rpc.append("</get>");
152 rpc.append(RPC_CLOSE_TAG);
153 return rpc.toString();
154 }
155
156
157 /**
158 * Construct a String with a Netconf filtered get RPC Message.
159 *
160 * @param filter A valid XPath Expression with the filter to apply in the get
161 * @return a String containing the RPC XML Document
162 *
163 * Note: server must support xpath capability.
164
165 * <select=" /components/component[name='PORT-A-In-1']/properties/...
166 * ...property[name='onos-index']/config/value" type="xpath"/>
167 */
168 private String xpathFilteredGetBuilder(String filter) {
169 StringBuilder rpc = new StringBuilder(RPC_TAG_NETCONF_BASE);
170 rpc.append("<get>");
171 rpc.append("<filter type='xpath' select=\"");
172 rpc.append(filter);
173 rpc.append("\"/>");
174 rpc.append("</get>");
175 rpc.append(RPC_CLOSE_TAG);
176 return rpc.toString();
177 }
178
179
180 /**
181 * Builds a request to get Device details, operational data.
182 *
183 * @return A string with the Netconf RPC for a get with subtree rpcing based on
184 * /components/component/state/type being oc-platform-types:OPERATING_SYSTEM
185 */
186 private String getDeviceDetailsBuilder() {
187 StringBuilder filter = new StringBuilder();
188 filter.append("<components xmlns='http://openconfig.net/yang/platform'>");
189 filter.append(" <component>");
190 filter.append(" <state>");
191 filter.append(" <type xmlns:oc-platform-types='http://openconfig.net/");
192 filter.append("yang/platform-types'>oc-platform-types:OPERATING_SYSTEM</type>");
193 filter.append(" </state>");
194 filter.append(" </component>");
195 filter.append("</components>");
196 return filteredGetBuilder(filter.toString());
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200197 }
198
199
200 /**
201 * Builds a request to get Device Components, config and operational data.
202 *
203 * @return A string with the Netconf RPC for a get with subtree rpcing based on
204 * /components/
205 */
206 private String getDeviceComponentsBuilder() {
Ramon Casellas03f194f2018-11-15 16:06:02 +0100207 return filteredGetBuilder(
208 "<components xmlns='http://openconfig.net/yang/platform'/>");
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200209 }
210
211
212 /**
213 * Builds a request to get Device Ports, config and operational data.
214 *
215 * @return A string with the Netconf RPC for a get with subtree rpcing based on
216 * /components/component/state/type being oc-platform-types:PORT
217 */
218 private String getDevicePortsBuilder() {
219 StringBuilder rpc = new StringBuilder();
220 rpc.append("<components xmlns='http://openconfig.net/yang/platform'>");
221 rpc.append(" <component><state>");
222 rpc.append(" <type xmlns:oc-platform-types='http://openconfig.net/");
223 rpc.append("yang/platform-types'>oc-platform-types:PORT</type>");
224 rpc.append(" </state></component>");
225 rpc.append("</components>");
226 return filteredGetBuilder(rpc.toString());
227 }
228
229
230 /**
231 * Returns a DeviceDescription with Device info.
232 *
233 * @return DeviceDescription or null
234 *
235 * //CHECKSTYLE:OFF
236 * <pre>{@code
237 * <data>
238 * <components xmlns="http://openconfig.net/yang/platform">
239 * <component>
240 * <state>
241 * <name>FIRMWARE</name>
242 * <type>oc-platform-types:OPERATING_SYSTEM</type>
243 * <description>CTTC METRO-HAUL Emulated OpenConfig TerminalDevice</description>
244 * <version>0.0.1</version>
245 * </state>
246 * </component>
247 * </components>
248 * </data>
249 *}</pre>
250 * //CHECKSTYLE:ON
251 */
252 @Override
253 public DeviceDescription discoverDeviceDetails() {
254 log.info("TerminalDeviceDiscovery::discoverDeviceDetails device {}", did());
255 boolean defaultAvailable = true;
256 SparseAnnotations annotations = DefaultAnnotations.builder().build();
257
258 // Other option "OTHER", we use ROADM for now
259 org.onosproject.net.Device.Type type =
260 org.onosproject.net.Device.Type.ROADM;
261
262 // Some defaults
263 String vendor = "NOVENDOR";
Ramon Casellas03f194f2018-11-15 16:06:02 +0100264 String hwVersion = "0.2.1";
265 String swVersion = "0.2.1";
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200266 String serialNumber = "0xCAFEBEEF";
267 String chassisId = "128";
268
269 // Get the session,
270 NetconfSession session = getNetconfSession(did());
271 if (session != null) {
272 try {
273 String reply = session.get(getDeviceDetailsBuilder());
274 // <rpc-reply> as root node
275 XMLConfiguration xconf = (XMLConfiguration) XmlConfigParser.loadXmlString(reply);
276 vendor = xconf.getString("data/components/component/state/mfg-name", vendor);
277 serialNumber = xconf.getString("data/components/component/state/serial-no", serialNumber);
278 // Requires OpenConfig >= 2018
279 swVersion = xconf.getString("data/components/component/state/software-version", swVersion);
280 hwVersion = xconf.getString("data/components/component/state/hardware-version", hwVersion);
281 } catch (Exception e) {
Ray Milkey0fa1c1a2018-05-30 08:27:04 -0700282 throw new IllegalStateException(new NetconfException("Failed to retrieve version info.", e));
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200283 }
284 } else {
285 log.info("TerminalDeviceDiscovery::discoverDeviceDetails - No netconf session for {}", did());
286 }
287 log.info("VENDOR {}", vendor);
288 log.info("HWVERSION {}", hwVersion);
289 log.info("SWVERSION {}", swVersion);
290 log.info("SERIAL {}", serialNumber);
291 log.info("CHASSISID {}", chassisId);
292 ChassisId cid = new ChassisId(Long.valueOf(chassisId, 10));
293 return new DefaultDeviceDescription(did().uri(),
Ramon Casellas03f194f2018-11-15 16:06:02 +0100294 type, vendor, hwVersion, swVersion, serialNumber,
295 cid, defaultAvailable, annotations);
296 }
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200297
298
299
300 /**
301 * Returns a list of PortDescriptions for the device.
302 *
303 * @return a list of descriptions.
304 *
305 * The RPC reply follows the following pattern:
306 * //CHECKSTYLE:OFF
307 * <pre>{@code
308 * <?xml version="1.0" encoding="UTF-8"?>
309 * <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="7">
310 * <data>
311 * <components xmlns="http://openconfig.net/yang/platform">
312 * <component>....
313 * </component>
314 * <component>....
315 * </component>
316 * </components>
317 * </data>
318 * </rpc-reply>
319 * }</pre>
320 * //CHECKSTYLE:ON
321 */
322 @Override
323 public List<PortDescription> discoverPortDetails() {
324 try {
Ramon Casellas03f194f2018-11-15 16:06:02 +0100325 XPathExpressionEngine xpe = new XPathExpressionEngine();
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200326 NetconfSession session = getNetconfSession(did());
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200327 if (session == null) {
328 log.error("discoverPortDetails called with null session for {}", did());
329 return ImmutableList.of();
330 }
331
332 CompletableFuture<String> fut = session.rpc(getDeviceComponentsBuilder());
333 String rpcReply = fut.get();
334
335 XMLConfiguration xconf = (XMLConfiguration) XmlConfigParser.loadXmlString(rpcReply);
Ramon Casellas03f194f2018-11-15 16:06:02 +0100336 xconf.setExpressionEngine(xpe);
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200337
338 HierarchicalConfiguration components = xconf.configurationAt("data/components");
339 return parsePorts(components);
340 } catch (Exception e) {
341 log.error("Exception discoverPortDetails() {}", did(), e);
342 return ImmutableList.of();
343 }
344 }
345
346
347
348
349 /**
350 * Parses port information from OpenConfig XML configuration.
351 *
352 * @param components the XML document with components root.
353 * @return List of ports
354 *
355 * //CHECKSTYLE:OFF
356 * <pre>{@code
357 * <components xmlns="http://openconfig.net/yang/platform">
358 * <component>....
359 * </component>
360 * <component>....
361 * </component>
362 * </components>
363 * }</pre>
364 * //CHECKSTYLE:ON
365 */
366 protected List<PortDescription> parsePorts(HierarchicalConfiguration components) {
367 return components.configurationsAt("component")
368 .stream()
369 .filter(component -> {
Ramon Casellas03f194f2018-11-15 16:06:02 +0100370 return !component.getString("name", "unknown").equals("unknown") &&
371 component.getString("state/type", "unknown").equals(OC_PLATFORM_TYPES_PORT);
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200372 })
373 .map(component -> {
374 try {
375 // Pass the root document for cross-reference
376 return parsePortComponent(component, components);
377 } catch (Exception e) {
378 return null;
379 }
Ramon Casellas03f194f2018-11-15 16:06:02 +0100380 }
381 )
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200382 .filter(Objects::nonNull)
383 .collect(Collectors.toList());
384 }
385
386
387 /**
388 * Checks if a given component has a subcomponent of a given type.
389 *
390 * @param component subtree to parse looking for subcomponents.
391 * @param components the full components tree, to cross-ref in
392 * case we need to check (sub)components' types.
393 *
394 * @return true or false
395 */
Ramon Casellas03f194f2018-11-15 16:06:02 +0100396 private boolean hasSubComponentOfType(
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200397 HierarchicalConfiguration component,
398 HierarchicalConfiguration components,
399 String type) {
400 long count = component.configurationsAt("subcomponents/subcomponent")
401 .stream()
402 .filter(subcomponent -> {
Ramon Casellas03f194f2018-11-15 16:06:02 +0100403 String scName = subcomponent.getString("name");
404 StringBuilder sb = new StringBuilder("component[name='");
405 sb.append(scName);
406 sb.append("']/state/type");
407 String scType = components.getString(sb.toString(), "unknown");
408 return scType.equals(type);
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200409 })
410 .count();
411 return (count > 0);
412 }
413
414
415 /**
416 * Checks if a given component has a subcomponent of type OPTICAL_CHANNEL.
417 *
418 * @param component subtree to parse
419 * @param components the full components tree, to cross-ref in
420 * case we need to check transceivers or optical channels.
421 *
422 * @return true or false
423 */
Ramon Casellas03f194f2018-11-15 16:06:02 +0100424 private boolean hasOpticalChannelSubComponent(
425 HierarchicalConfiguration component,
426 HierarchicalConfiguration components) {
427 return hasSubComponentOfType(component, components,
428 OC_TRANSPORT_TYPES_OPTICAL_CHANNEL);
429 }
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200430
431
Ramon Casellas03f194f2018-11-15 16:06:02 +0100432 /**
433 * Checks if a given component has a subcomponent of type TRANSCEIVER.
434 *
435 * @param component subtree to parse
436 * @param components the full components tree, to cross-ref in
437 * case we need to check transceivers or optical channels.
438 *
439 * @return true or false
440 */
441 private boolean hasTransceiverSubComponent(
442 HierarchicalConfiguration component,
443 HierarchicalConfiguration components) {
444 return hasSubComponentOfType(component, components,
445 OC_PLATFORM_TYPES_TRANSCEIVER);
446 }
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200447
448
Ramon Casellas03f194f2018-11-15 16:06:02 +0100449 /**
450 * Parses a component XML doc into a PortDescription.
451 *
452 * @param component subtree to parse. It must be a component ot type PORT.
453 * @param components the full components tree, to cross-ref in
454 * case we need to check transceivers or optical channels.
455 *
456 * @return PortDescription or null if component does not have onos-index
457 */
458 private PortDescription parsePortComponent(
459 HierarchicalConfiguration component,
460 HierarchicalConfiguration components) {
461 Map<String, String> annotations = new HashMap<>();
462 String name = component.getString("name");
463 String type = component.getString("state/type");
464 log.info("Parsing Component {} type {}", name, type);
465 annotations.put(OdtnDeviceDescriptionDiscovery.OC_NAME, name);
466 annotations.put(OdtnDeviceDescriptionDiscovery.OC_TYPE, type);
467 // Store all properties as port properties
468 component.configurationsAt("properties/property")
469 .forEach(property -> {
470 String pn = property.getString("name");
471 String pv = property.getString("state/value");
472 annotations.put(pn, pv);
473 });
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200474
Ramon Casellas03f194f2018-11-15 16:06:02 +0100475 // Assing an ONOS port number
476 PortNumber portNum;
477 if (annotations.containsKey(ONOS_PORT_INDEX)) {
478 portNum = PortNumber.portNumber(Long.parseLong(annotations.get(ONOS_PORT_INDEX)));
479 } else {
480 log.warn("PORT {} does not include onos-index, hashing...", name);
481 portNum = PortNumber.portNumber(name.hashCode());
482 }
483 log.debug("PORT {} number {}", name, portNum);
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200484
Ramon Casellas03f194f2018-11-15 16:06:02 +0100485 // The heuristic to know if it is client or line side
486 if (!annotations.containsKey(PORT_TYPE)) {
487 if (hasTransceiverSubComponent(component, components)) {
488 annotations.put(PORT_TYPE, OdtnPortType.CLIENT.value());
489 } else if (hasOpticalChannelSubComponent(component, components)) {
490 annotations.put(PORT_TYPE, OdtnPortType.LINE.value());
491 }
492 }
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200493
Ramon Casellas03f194f2018-11-15 16:06:02 +0100494 // Build the port
495 // NOTE: using portNumber(id, name) breaks things. Intent parsing, port resorce management, etc. There seems
496 // to be an issue with resource mapping
497 if (annotations.get(PORT_TYPE)
498 .equals(OdtnPortType.CLIENT.value())) {
499 log.debug("Adding CLIENT port");
500 Builder builder = DefaultPortDescription.builder();
501 builder.type(Type.PACKET);
502 builder.withPortNumber(portNum);
503 builder.annotations(DefaultAnnotations.builder().putAll(annotations).build());
504 return builder.build();
505 }
506 if (annotations.get(PORT_TYPE)
507 .equals(OdtnPortType.LINE.value())) {
508 log.debug("Adding LINE port");
509 // TODO: To be configured
510 OchSignal signalId = OchSignal.newDwdmSlot(ChannelSpacing.CHL_50GHZ, 1);
511 return OchPortHelper.ochPortDescription(
512 portNum, true,
513 OduSignalType.ODU4, // TODO Client signal to be discovered
514 true,
515 signalId,
516 DefaultAnnotations.builder().putAll(annotations).build());
517 }
518 log.error("Unknown port type");
519 return null;
520 }
Ramon Casellasfbcd2942018-05-25 14:41:40 +0200521}