blob: e3205c281fdf43caaba06960b34ddd51ea362fa4 [file] [log] [blame]
Jan Kundrát981fe472019-10-15 22:44:19 +02001/*
2 * Copyright 2019-2020 Jan Kundrát, CESNET, <jan.kundrat@cesnet.cz> and 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
17package org.onosproject.drivers.czechlight;
18
19import org.apache.commons.configuration.HierarchicalConfiguration;
20import org.onlab.util.Spectrum;
21import org.onosproject.core.CoreService;
22import org.onosproject.drivers.odtn.impl.DeviceConnectionCache;
23import org.onosproject.net.ChannelSpacing;
24import org.onosproject.net.GridType;
25import org.onosproject.net.Lambda;
26import org.onosproject.net.OchSignal;
27import org.onosproject.net.OchSignalType;
28import org.onosproject.net.PortNumber;
29import org.onosproject.net.device.DeviceService;
30import org.onosproject.net.driver.AbstractHandlerBehaviour;
31import org.onosproject.net.flow.DefaultFlowEntry;
32import org.onosproject.net.flow.DefaultFlowRule;
33import org.onosproject.net.flow.DefaultTrafficSelector;
34import org.onosproject.net.flow.DefaultTrafficTreatment;
35import org.onosproject.net.flow.FlowEntry;
36import org.onosproject.net.flow.FlowRule;
37import org.onosproject.net.flow.FlowRuleProgrammable;
38import org.onosproject.net.flow.FlowRuleService;
39import org.onosproject.net.flow.TrafficSelector;
40import org.onosproject.net.flow.TrafficTreatment;
41import org.onosproject.net.flow.criteria.Criteria;
42import org.onosproject.net.flow.criteria.OchSignalCriterion;
43import org.onosproject.net.flow.criteria.PortCriterion;
44import org.onosproject.net.flow.instructions.Instructions;
45import org.onosproject.net.flow.instructions.L0ModificationInstruction;
46import org.onosproject.netconf.DatastoreId;
47import org.onosproject.netconf.NetconfController;
48import org.onosproject.netconf.NetconfException;
49import org.onosproject.netconf.NetconfSession;
50import org.slf4j.Logger;
51import org.slf4j.LoggerFactory;
52
53import java.util.ArrayList;
54import java.util.Collection;
55import java.util.Map;
56import java.util.Objects;
57import java.util.TreeMap;
58import java.util.stream.Collectors;
59
60import static com.google.common.base.Preconditions.checkNotNull;
61
62/** Modification of the MC by a ROADM device.
63 * The signal might be either attenuated by a specified amount of dB, or its target power can be set
64 * to a specified power in dBm.
65 */
66class MCManipulation {
67 Double attenuation;
68 Double targetPower;
69
70 public MCManipulation(final Double attenuation, final Double targetPower) {
71 this.attenuation = attenuation;
72 this.targetPower = targetPower;
73 }
74
75 public String toString() {
76 if (attenuation != null) {
77 return "attenuation: " + String.valueOf(attenuation);
78 }
79
80 if (targetPower != null) {
81 return "targetPower: " + String.valueOf(targetPower);
82 }
83
84 return "none";
85 }
86};
87
88/** Representation of a ROADM configuration for a given Media Channel.
89 * This contains frequency (`channel`), routing (`leafPort`) and attenuation or power set point (`manipulation`).
90 * */
91class CzechLightRouting {
92 MediaChannelDefinition channel;
93 int leafPort;
94 MCManipulation manipulation;
95
96 public CzechLightRouting(final MediaChannelDefinition channel, final int leafPort, final MCManipulation manip) {
97 this.channel = channel;
98 this.leafPort = leafPort;
99 this.manipulation = manip;
100 }
101
102 public String toString() {
103 return channel.toString() + " -> " + String.valueOf(leafPort) + " (" + manipulation.toString() + ")";
104 }
105};
106
107/**
108 * Implementation of FlowRuleProgrammable interface for CzechLight SDN ROADMs.
109 */
110public class CzechLightFlowRuleProgrammable extends AbstractHandlerBehaviour implements FlowRuleProgrammable {
111
112 private final Logger log =
113 LoggerFactory.getLogger(getClass());
114
115 private static final String NETCONF_OP_MERGE = "merge";
116 private static final String NETCONF_OP_NONE = "none";
117 private static final String ELEMENT_ADD = "add";
118 private static final String ELEMENT_DROP = "drop";
119
120 private enum Direction {
121 ADD,
122 DROP,
123 };
124 // FIXME: can we get this programmaticaly?
125 private static final String DEFAULT_APP = "org.onosproject.drivers.czechlight";
126
127 @Override
128 public Collection<FlowEntry> getFlowEntries() {
129 if (deviceType() == CzechLightDiscovery.DeviceType.INLINE_AMP
130 || deviceType() == CzechLightDiscovery.DeviceType.COHERENT_ADD_DROP) {
131 final var data = getConnectionCache().get(data().deviceId());
132 if (data == null) {
133 return new ArrayList<>();
134 }
135 return data.stream()
136 .map(rule -> new DefaultFlowEntry(rule))
137 .collect(Collectors.toList());
138 }
139
140 HierarchicalConfiguration xml;
141 try {
142 xml = doGetSubtree(CzechLightDiscovery.CHANNEL_DEFS_FILTER + CzechLightDiscovery.MC_ROUTING_FILTER);
143 } catch (NetconfException e) {
144 log.error("Cannot read data from NETCONF: {}", e);
145 return new ArrayList<>();
146 }
147 final var allChannels = MediaChannelDefinition.parseChannelDefinitions(xml);
148
149 Collection<FlowEntry> list = new ArrayList<>();
150
151 final var allMCs = xml.configurationsAt("data.media-channels");
152 allMCs.stream()
153 .map(cfg -> confToMCRouting(ELEMENT_ADD, allChannels, cfg))
154 .filter(Objects::nonNull)
155 .forEach(flow -> {
156 log.debug("{}: found ADD: {}", data().deviceId(), flow.toString());
157 list.add(new DefaultFlowEntry(asFlowRule(Direction.ADD, flow), FlowEntry.FlowEntryState.ADDED));
158 });
159 allMCs.stream()
160 .map(cfg -> confToMCRouting(ELEMENT_DROP, allChannels, cfg))
161 .filter(Objects::nonNull)
162 .forEach(flow -> {
163 log.debug("{}: found DROP: {}", data().deviceId(), flow.toString());
164 list.add(new DefaultFlowEntry(asFlowRule(Direction.DROP, flow), FlowEntry.FlowEntryState.ADDED));
165 });
166 return list;
167 }
168
169 @Override
170 public Collection<FlowRule> applyFlowRules(Collection<FlowRule> rules) {
171 if (deviceType() == CzechLightDiscovery.DeviceType.INLINE_AMP
172 || deviceType() == CzechLightDiscovery.DeviceType.COHERENT_ADD_DROP) {
173 rules.forEach(
174 rule -> {
175 log.debug("{}: asked for {} (whole C-band is always forwarded by the HW)",
176 data().deviceId(), rule);
177 getConnectionCache().add(data().deviceId(), rule.toString(), rule);
178 }
179 );
180 return rules;
181 }
182
183 HierarchicalConfiguration xml;
184 try {
185 xml = doGetSubtree(CzechLightDiscovery.CHANNEL_DEFS_FILTER + CzechLightDiscovery.MC_ROUTING_FILTER);
186 } catch (NetconfException e) {
187 log.error("Cannot read data from NETCONF: {}", e);
188 return new ArrayList<>();
189 }
190 final var allChannels = MediaChannelDefinition.parseChannelDefinitions(xml);
191 var hopefullyAdded = new ArrayList<FlowRule>();
192
193 // temporary store because both ADD and DROP must go into the same <media-channel> list item
194 var changes = new TreeMap<String, String>();
195 rules.forEach(
196 rule -> {
197 log.debug("{}: asked to INSERT rule for:", data().deviceId());
198 rule.selector().criteria().forEach(
199 criteria -> log.debug(" criteria {}", criteria.toString())
200 );
201 rule.treatment().allInstructions().forEach(
202 instruction -> log.debug(" instruction {}", instruction.toString())
203 );
204
205 String element;
206 long leafPort;
207 if (inputPortFromFlow(rule).toLong() == CzechLightDiscovery.PORT_COMMON) {
208 element = ELEMENT_DROP;
209 leafPort = outputPortFromFlow(rule).toLong();
210 } else {
211 element = ELEMENT_ADD;
212 leafPort = inputPortFromFlow(rule).toLong();
213 }
214 final var och = ochSignalFromFlow(rule);
215 final var channel = allChannels.entrySet().stream()
216 .filter(entry -> MediaChannelDefinition.mcMatches(entry, och))
217 .findAny()
218 .orElse(null);
219 if (channel == null) {
220 log.error("No matching channel definition available for the following rule at {}:",
221 data().deviceId());
222 rule.selector().criteria().forEach(
223 criteria -> log.error(" criteria {}", criteria.toString())
224 );
225 rule.treatment().allInstructions().forEach(
226 instruction -> log.error(" instruction {}", instruction.toString())
227 );
228 } else {
229 log.info("{}: Creating \"{}\" MC {}: leaf {}", data().deviceId(),
230 element, channel.getKey(), leafPort);
231 var sb = new StringBuilder();
232 sb.append("<");
233 sb.append(element);
234 sb.append(">");
235 sb.append("<port>");
236 if (deviceType() == CzechLightDiscovery.DeviceType.LINE_DEGREE) {
237 sb.append(CzechLightDiscovery.LINE_EXPRESS_PREFIX);
238 }
239 sb.append(String.valueOf(leafPort));
240 sb.append("</port>");
241 // FIXME: propagate attenuation or power target
242 if (deviceType() == CzechLightDiscovery.DeviceType.LINE_DEGREE) {
243 if (outputPortFromFlow(rule).toLong() == CzechLightDiscovery.PORT_COMMON) {
244 sb.append("<power>-5.0</power>");
245 } else {
246 sb.append("<power>-12.0</power>");
247 }
248 } else {
249 if (outputPortFromFlow(rule).toLong() == CzechLightDiscovery.PORT_COMMON) {
250 sb.append("<power>-12.0</power>");
251 } else {
252 sb.append("<power>-5.0</power>");
253 }
254 }
255 sb.append("</");
256 sb.append(element);
257 sb.append(">");
258 changes.put(channel.getKey(),
259 changes.getOrDefault(channel.getKey(), "") + sb.toString());
260 hopefullyAdded.add(rule);
261 }
262 });
263
264 if (!hopefullyAdded.isEmpty()) {
265 var sb = new StringBuilder();
266 changes.forEach(
267 (channel, data) -> {
268 sb.append(CzechLightDiscovery.XML_MC_OPEN);
269 sb.append("<channel>");
270 sb.append(channel);
271 sb.append("</channel>");
272 sb.append(data);
273 sb.append(CzechLightDiscovery.XML_MC_CLOSE);
274 });
275 doEditConfig(NETCONF_OP_MERGE, sb.toString());
276 }
277 return hopefullyAdded;
278 }
279
280 @Override
281 public Collection<FlowRule> removeFlowRules(Collection<FlowRule> rules) {
282 if (deviceType() == CzechLightDiscovery.DeviceType.INLINE_AMP
283 || deviceType() == CzechLightDiscovery.DeviceType.COHERENT_ADD_DROP) {
284 rules.forEach(
285 rule -> {
286 log.debug("{}: asked to remove {} (whole C-band is always forwarded by the HW)",
287 data().deviceId(), rule);
288 getConnectionCache().remove(data().deviceId(), rule);
289 }
290 );
291 return rules;
292 }
293 HierarchicalConfiguration xml;
294 try {
295 xml = doGetSubtree(CzechLightDiscovery.CHANNEL_DEFS_FILTER + CzechLightDiscovery.MC_ROUTING_FILTER);
296 } catch (NetconfException e) {
297 log.error("Cannot read data from NETCONF: {}", e);
298 return new ArrayList<>();
299 }
300 final var allChannels = MediaChannelDefinition.parseChannelDefinitions(xml);
301
302 var hopefullyRemoved = new ArrayList<FlowRule>();
303
304 // temporary store because both ADD and DROP must go into the same <media-channel> list item
305 var changes = new TreeMap<String, String>();
306
307 rules.forEach(
308 rule -> {
309 final String element = inputPortFromFlow(rule).toLong() == CzechLightDiscovery.PORT_COMMON ?
310 ELEMENT_DROP : ELEMENT_ADD;
311 final var och = ochSignalFromFlow(rule);
312 final var channel = allChannels.entrySet().stream()
313 .filter(entry -> MediaChannelDefinition.mcMatches(entry, och))
314 .findAny()
315 .orElse(null);
316 if (channel == null) {
317 log.error("Cannot find what channel to remove for the following flow rule at {}:",
318 data().deviceId());
319 rule.selector().criteria().forEach(
320 criteria -> log.error(" criteria {}", criteria.toString())
321 );
322 rule.treatment().allInstructions().forEach(
323 instruction -> log.error(" instruction {}", instruction.toString())
324 );
325 } else {
326 log.info("{}: Removing {} MC {}", data().deviceId(), element, channel.getKey());
327 changes.put(channel.getKey(),
328 changes.getOrDefault(channel.getKey(), "")
329 + "<" + element + " nc:operation=\"remove\"/>");
330 hopefullyRemoved.add(rule);
331 }
332 });
333
334 if (!hopefullyRemoved.isEmpty()) {
335 var sb = new StringBuilder();
336 changes.forEach(
337 (channel, data) -> {
338 sb.append(CzechLightDiscovery.XML_MC_OPEN);
339 sb.append("<channel>");
340 sb.append(channel);
341 sb.append("</channel>");
342 sb.append(data);
343 sb.append(CzechLightDiscovery.XML_MC_CLOSE);
344 });
345 doEditConfig(NETCONF_OP_NONE, sb.toString());
346 }
347 return hopefullyRemoved;
348 }
349
350 private static CzechLightRouting confToMCRouting(final String keyPrefix,
351 final Map<String, MediaChannelDefinition> allChannels,
352 final HierarchicalConfiguration item) {
353 if (!item.containsKey(keyPrefix + ".port")) {
354 return null;
355 }
356 // the leaf port is either just a number, or a number prefixed by "E"
357 final var portStr = item.getString(keyPrefix + ".port");
358 final int leafPort = Integer.parseInt(portStr.startsWith(CzechLightDiscovery.LINE_EXPRESS_PREFIX) ?
359 portStr.substring(1) : portStr);
360 return new CzechLightRouting(
361 allChannels.get(item.getString("channel")),
362 leafPort,
363 new MCManipulation(
364 item.getDouble(keyPrefix + ".attenuation", null),
365 item.getDouble(keyPrefix + ".power", null)
366 )
367 );
368 }
369
370 private FlowRule asFlowRule(final Direction direction, final CzechLightRouting routing) {
371 FlowRuleService service = handler().get(FlowRuleService.class);
372 Iterable<FlowEntry> entries = service.getFlowEntries(data().deviceId());
373
374 final var portIn = PortNumber.portNumber(direction == Direction.DROP ?
375 CzechLightDiscovery.PORT_COMMON : routing.leafPort);
376 final var portOut = PortNumber.portNumber(direction == Direction.ADD ?
377 CzechLightDiscovery.PORT_COMMON : routing.leafPort);
378
379 final var channelWidth = routing.channel.highMHz - routing.channel.lowMHz;
380 final var channelCentralFreq = (int) (routing.channel.lowMHz + channelWidth / 2);
381
382 for (FlowEntry entry : entries) {
383 final var och = ochSignalFromFlow(entry);
384 if (och.centralFrequency().asMHz() == channelCentralFreq
385 && och.slotWidth().asMHz() == channelWidth
386 && portIn.equals(inputPortFromFlow(entry))
387 && portOut.equals(outputPortFromFlow(entry))) {
388 return entry;
389 }
390 }
391
392 final var channelSlotWidth = (int) (channelWidth / ChannelSpacing.CHL_12P5GHZ.frequency().asMHz());
393 final var channelMultiplier = (int) ((channelCentralFreq - Spectrum.CENTER_FREQUENCY.asMHz())
394 / ChannelSpacing.CHL_6P25GHZ.frequency().asMHz());
395
396 TrafficSelector selector = DefaultTrafficSelector.builder()
397 .matchInPort(portIn)
398 .add(Criteria.matchOchSignalType(OchSignalType.FLEX_GRID))
399 .add(Criteria.matchLambda(Lambda.ochSignal(GridType.FLEX, ChannelSpacing.CHL_6P25GHZ,
400 channelMultiplier, channelSlotWidth)))
401 .build();
402 TrafficTreatment treatment = DefaultTrafficTreatment.builder()
403 .setOutput(portOut)
404 .build();
405 return DefaultFlowRule.builder()
406 .forDevice(data().deviceId())
407 .withSelector(selector)
408 .withTreatment(treatment)
409 // the concept of priorities does not make sense for a ROADM MC configuration,
410 // but it's mandatory nonetheless
411 .withPriority(666)
412 .makePermanent()
413 .fromApp(handler().get(CoreService.class).getAppId(DEFAULT_APP))
414 .build();
415 }
416
417 private static PortNumber inputPortFromFlow(final Object flow) {
418 return ((flow instanceof FlowEntry) ?
419 ((FlowEntry) flow).selector() : ((FlowRule) flow).selector()).criteria().stream()
420 .filter(c -> c instanceof PortCriterion)
421 .map(c -> ((PortCriterion) c).port())
422 .findAny()
423 .orElse(null);
424 }
425
426 private static PortNumber outputPortFromFlow(final Object flow) {
427 return ((flow instanceof FlowEntry) ?
428 ((FlowEntry) flow).treatment() : ((FlowRule) flow).treatment()).immediate().stream()
429 .filter(c -> c instanceof Instructions.OutputInstruction)
430 .map(c -> ((Instructions.OutputInstruction) c).port())
431 .findAny()
432 .orElse(null);
433 }
434
435 private static OchSignal ochSignalFromFlow(final Object flow) {
436 final var fromCriteria = ((flow instanceof FlowEntry) ?
437 ((FlowEntry) flow).selector() : ((FlowRule) flow).selector()).criteria().stream()
438 .filter(c -> c instanceof OchSignalCriterion)
439 .map(c -> ((OchSignalCriterion) c).lambda())
440 .findAny()
441 .orElse(null);
442 if (fromCriteria != null) {
443 return fromCriteria;
444 }
445 return ((flow instanceof FlowEntry) ?
446 ((FlowEntry) flow).treatment() : ((FlowRule) flow).treatment()).immediate().stream()
447 .filter(c -> c instanceof L0ModificationInstruction.ModOchSignalInstruction)
448 .map(c -> ((L0ModificationInstruction.ModOchSignalInstruction) c).lambda())
449 .findAny()
450 .orElse(null);
451 }
452
453 private CzechLightDiscovery.DeviceType deviceType() {
454 var annotations = this.handler().get(DeviceService.class).getDevice(handler().data().deviceId()).annotations();
455 return CzechLightDiscovery.DeviceType.valueOf(annotations.value(CzechLightDiscovery.DEVICE_TYPE_ANNOTATION));
456 }
457
458 private DeviceConnectionCache getConnectionCache() {
459 return DeviceConnectionCache.init();
460 }
461
462 private HierarchicalConfiguration doGetSubtree(final String subtreeXml) throws NetconfException {
463 NetconfSession session = getNetconfSession();
464 if (session == null) {
465 log.error("Cannot request NETCONF session for {}", data().deviceId());
466 return null;
467 }
468 return CzechLightDiscovery.doGetSubtree(session, subtreeXml);
469 }
470
471 private HierarchicalConfiguration doGetXPath(final String prefix, final String namespace, final String xpathFilter)
472 throws NetconfException {
473 NetconfSession session = getNetconfSession();
474 if (session == null) {
475 log.error("Cannot request NETCONF session for {}", data().deviceId());
476 return null;
477 }
478 return CzechLightDiscovery.doGetXPath(session, prefix, namespace, xpathFilter);
479 }
480
481 public boolean doEditConfig(String mode, String cfg) {
482 NetconfSession session = getNetconfSession();
483 if (session == null) {
484 log.error("Cannot request NETCONF session for {}", data().deviceId());
485 return false;
486 }
487
488 try {
489 return session.editConfig(DatastoreId.RUNNING, mode, cfg);
490 } catch (NetconfException e) {
491 throw new IllegalStateException(new NetconfException("Failed to edit configuration.", e));
492 }
493 }
494
495 private NetconfSession getNetconfSession() {
496 NetconfController controller =
497 checkNotNull(handler().get(NetconfController.class));
498 return controller.getNetconfDevice(data().deviceId()).getSession();
499 }
500}