blob: 075d3dc4c6fa746983a36138ab7c0208f7d83ac2 [file] [log] [blame]
Michele Santuarice256492017-06-23 14:44:01 +02001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2016 Open Networking Foundation
Michele Santuarice256492017-06-23 14:44:01 +02003 *
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.juniper;
18
19import com.google.common.annotations.Beta;
20import com.google.common.base.Strings;
21import org.apache.commons.lang.StringUtils;
22import org.onlab.packet.Ip4Address;
23import org.onosproject.net.AnnotationKeys;
24import org.onosproject.net.ConnectPoint;
25import org.onosproject.net.DeviceId;
26import org.onosproject.net.Link;
27import org.onosproject.net.Port;
28import org.onosproject.net.PortNumber;
29import org.onosproject.net.device.DeviceService;
30import org.onosproject.net.driver.AbstractHandlerBehaviour;
31import org.onosproject.net.flow.FlowEntry;
32import org.onosproject.net.flow.FlowRule;
33import org.onosproject.net.flow.FlowRuleProgrammable;
34import org.onosproject.net.flow.FlowRuleService;
35import org.onosproject.net.flow.criteria.Criterion;
36import org.onosproject.net.flow.criteria.IPCriterion;
37import org.onosproject.net.flow.instructions.Instruction;
38import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
39import org.onosproject.net.link.LinkService;
40import org.onosproject.netconf.DatastoreId;
41import org.onosproject.netconf.NetconfController;
42import org.onosproject.netconf.NetconfException;
43import org.onosproject.netconf.NetconfSession;
44
Michele Santuarice256492017-06-23 14:44:01 +020045import java.util.Collection;
46import java.util.Collections;
47import java.util.HashSet;
48import java.util.Map;
49import java.util.Optional;
50import java.util.Set;
51import java.util.stream.Collectors;
52
53import static com.google.common.base.Preconditions.checkNotNull;
54import static org.onosproject.drivers.juniper.JuniperUtils.OperationType;
55import static org.onosproject.drivers.juniper.JuniperUtils.OperationType.ADD;
56import static org.onosproject.drivers.juniper.JuniperUtils.OperationType.REMOVE;
57import static org.onosproject.drivers.juniper.JuniperUtils.commitBuilder;
58import static org.onosproject.drivers.juniper.JuniperUtils.rollbackBuilder;
59import static org.onosproject.drivers.juniper.JuniperUtils.routeAddBuilder;
60import static org.onosproject.drivers.juniper.JuniperUtils.routeDeleteBuilder;
61import static org.onosproject.drivers.utilities.XmlConfigParser.loadXmlString;
62import static org.onosproject.net.flow.FlowEntry.FlowEntryState.PENDING_REMOVE;
63import static org.onosproject.net.flow.FlowEntry.FlowEntryState.REMOVED;
64import static org.slf4j.LoggerFactory.getLogger;
65
66/**
67 * Conversion of FlowRules into static routes and retrieve of installed
68 * static routes as FlowRules.
69 * The selector of the FlowRule must contains the IPv4 address
70 * {@link org.onosproject.net.flow.TrafficSelector.Builder#matchIPDst(org.onlab.packet.IpPrefix)}
71 * of the host to connect and the treatment must include an
72 * output port {@link org.onosproject.net.flow.TrafficTreatment.Builder#setOutput(PortNumber)}
73 * All other instructions in the selector and treatment are ignored.
74 * <p>
75 * This implementation requires an IP adjacency
76 * (e.g., IP link discovered by {@link LinkDiscoveryJuniperImpl}) between the routers
77 * to find the next hop IP address.
78 */
79@Beta
80public class FlowRuleJuniperImpl extends AbstractHandlerBehaviour
81 implements FlowRuleProgrammable {
82
83 private static final String OK = "<ok/>";
Michele Santuarice256492017-06-23 14:44:01 +020084 private final org.slf4j.Logger log = getLogger(getClass());
85
86 @Override
87 public Collection<FlowEntry> getFlowEntries() {
88
89 DeviceId devId = checkNotNull(this.data().deviceId());
90 NetconfController controller = checkNotNull(handler().get(NetconfController.class));
91 NetconfSession session = controller.getDevicesMap().get(devId).getSession();
92 if (session == null) {
93 log.warn("Device {} is not registered in netconf", devId);
Kieran McPeake201fe1f2019-06-03 12:47:22 +010094 return Collections.emptyList();
Michele Santuarice256492017-06-23 14:44:01 +020095 }
96
97 //Installed static routes
98 String reply;
99 try {
100 reply = session.get(routingTableBuilder());
Yuta HIGUCHI234eaf32017-09-06 13:45:05 -0700101 } catch (NetconfException e) {
Ray Milkey986a47a2018-01-25 11:38:51 -0800102 throw new IllegalStateException(new NetconfException("Failed to retrieve configuration.",
Michele Santuarice256492017-06-23 14:44:01 +0200103 e));
104 }
105 Collection<StaticRoute> devicesStaticRoutes =
106 JuniperUtils.parseRoutingTable(loadXmlString(reply));
107
108 //Expected FlowEntries installed
109 FlowRuleService flowRuleService = this.handler().get(FlowRuleService.class);
110 Iterable<FlowEntry> flowEntries = flowRuleService.getFlowEntries(devId);
111
112 Collection<FlowEntry> installedRules = new HashSet<>();
113 flowEntries.forEach(flowEntry -> {
114 Optional<IPCriterion> ipCriterion = getIpCriterion(flowEntry);
115 if (!ipCriterion.isPresent()) {
116 return;
117 }
118
119 Optional<OutputInstruction> output = getOutput(flowEntry);
120 if (!output.isPresent()) {
121 return;
122 }
123 //convert FlowRule into static route
124 getStaticRoute(devId, ipCriterion.get(), output.get(), flowEntry.priority()).ifPresent(staticRoute -> {
125 //Two type of FlowRules:
126 //1. FlowRules to forward to a remote subnet: they are translated into static route
127 // configuration. So a removal request will be processed.
128 //2. FlowRules to forward on a subnet directly attached to the router (Generally speaking called local):
129 // those routes do not require any configuration because the router is already able to forward on
130 // directly attached subnet. In this case, when the driver receive the request to remove,
131 // it will report as removed.
132
133 if (staticRoute.isLocalRoute()) {
134 //if the FlowRule is in PENDING_REMOVE or REMOVED, it is not reported.
135 if (flowEntry.state() == PENDING_REMOVE || flowEntry.state() == REMOVED) {
136 devicesStaticRoutes.remove(staticRoute);
137 } else {
138 //FlowRule is reported installed
139 installedRules.add(flowEntry);
140 devicesStaticRoutes.remove(staticRoute);
141 }
142
143 } else {
144 //if the route is found in the device, the FlowRule is reported installed.
145 if (devicesStaticRoutes.contains(staticRoute)) {
146 installedRules.add(flowEntry);
147 devicesStaticRoutes.remove(staticRoute);
148 }
149 }
150 });
151 });
152
153 if (!devicesStaticRoutes.isEmpty()) {
154 log.info("Found static routes on device {} not installed by ONOS: {}",
155 devId, devicesStaticRoutes);
156// FIXME: enable configuration to purge already installed flows.
157// It cannot be allowed by default because it may remove needed management routes
158// log.warn("Removing from device {} the FlowEntries not expected {}", deviceId, devicesStaticRoutes);
159// devicesStaticRoutes.forEach(staticRoute -> editRoute(session, REMOVE, staticRoute));
160 }
161 return installedRules;
162 }
163
164 @Override
165 public Collection<FlowRule> applyFlowRules(Collection<FlowRule> rules) {
166 return manageRules(rules, ADD);
167 }
168
169 @Override
170 public Collection<FlowRule> removeFlowRules(Collection<FlowRule> rules) {
171 return manageRules(rules, REMOVE);
172 }
173
174 private Collection<FlowRule> manageRules(Collection<FlowRule> rules, OperationType type) {
175
176 DeviceId deviceId = this.data().deviceId();
177
178 log.debug("{} flow entries to NETCONF device {}", type, deviceId);
179 NetconfController controller = checkNotNull(handler()
180 .get(NetconfController.class));
181 NetconfSession session = controller.getDevicesMap().get(deviceId)
182 .getSession();
183 Collection<FlowRule> managedRules = new HashSet<>();
184
185 for (FlowRule flowRule : rules) {
186
187 Optional<IPCriterion> ipCriterion = getIpCriterion(flowRule);
188 if (!ipCriterion.isPresent()) {
189 log.error("Currently not supported: IPv4 destination match must be used");
190 continue;
191 }
192
193 Optional<OutputInstruction> output = getOutput(flowRule);
194 if (!output.isPresent()) {
195 log.error("Currently not supported: the output action is needed");
196 continue;
197 }
198
199 getStaticRoute(deviceId, ipCriterion.get(), output.get(), flowRule.priority()).ifPresent(
200 staticRoute -> {
201 //If the static route is not local, the driver tries to install
202 if (!staticRoute.isLocalRoute()) {
203 //Only if the installation is successful, the driver report the
204 // FlowRule as installed.
205 if (editRoute(session, type, staticRoute)) {
206 managedRules.add(flowRule);
207 }
208 //If static route are local, they are not installed
209 // because are not required. Directly connected routes
210 //are automatically forwarded.
211 } else {
212 managedRules.add(flowRule);
213 }
214 }
215 );
216 }
217 return rules;
218 }
219
220 private boolean editRoute(NetconfSession session, OperationType type,
221 StaticRoute staticRoute) {
222 try {
223 boolean reply = false;
224 if (type == ADD) {
225 reply = session
226 .editConfig(DatastoreId.CANDIDATE, "merge",
227 routeAddBuilder(staticRoute));
228 } else if (type == REMOVE) {
229 reply = session
230 .editConfig(DatastoreId.CANDIDATE, "none", routeDeleteBuilder(staticRoute));
231 }
232 if (reply && commit()) {
233 return true;
234 } else {
235 if (!rollback()) {
236 log.error("Something went wrong in the configuration and impossible to rollback");
237 } else {
238 log.error("Something went wrong in the configuration: a static route has not been {} {}",
239 type == ADD ? "added" : "removed", staticRoute);
240 }
241 }
Yuta HIGUCHI234eaf32017-09-06 13:45:05 -0700242 } catch (NetconfException e) {
Ray Milkey986a47a2018-01-25 11:38:51 -0800243 throw new IllegalStateException(new NetconfException("Failed to retrieve configuration.",
Michele Santuarice256492017-06-23 14:44:01 +0200244 e));
245 }
246 return false;
247 }
248
249 /**
250 * Helper method to convert FlowRule into an abstraction of static route
251 * {@link StaticRoute}.
252 *
253 * @param devId the device id
254 * @param criteria the IP destination criteria
255 * @param output the output instruction
256 * @return optional of Static Route
257 */
258 private Optional<StaticRoute> getStaticRoute(DeviceId devId,
259 IPCriterion criteria,
260 OutputInstruction output,
261 int priority) {
262
263 DeviceService deviceService = this.handler().get(DeviceService.class);
264 Collection<Port> ports = deviceService.getPorts(devId);
265 Optional<Port> port = ports.stream().filter(x -> x.number().equals(output.port())).findAny();
266 if (!port.isPresent()) {
267 log.error("The port {} does not exist in the device",
268 output.port());
269 return Optional.empty();
270 }
271
272 //Find if the route refers to a local interface.
273 Optional<Port> local = deviceService.getPorts(devId).stream().filter(this::isIp)
274 .filter(p -> criteria.ip().getIp4Prefix().contains(
Kieran McPeake201fe1f2019-06-03 12:47:22 +0100275 Ip4Address.valueOf(p.annotations().value(JuniperUtils.AK_IP)))).findAny();
Michele Santuarice256492017-06-23 14:44:01 +0200276
277 if (local.isPresent()) {
278 return Optional.of(new StaticRoute(criteria.ip().getIp4Prefix(),
279 criteria.ip().getIp4Prefix().address(), true, priority));
280 }
281
282 Optional<Ip4Address> nextHop = findIpDst(devId, port.get());
283 if (nextHop.isPresent()) {
284 return Optional.of(
285 new StaticRoute(criteria.ip().getIp4Prefix(), nextHop.get(), false, priority));
286 } else {
287 log.error("The destination interface has not an IP {}", port.get());
288 return Optional.empty();
289 }
290
291 }
292
293 /**
294 * Helper method to get the IP destination criterion given a flow rule.
295 *
296 * @param flowRule the flow rule
297 * @return optional of IP destination criterion
298 */
299 private Optional<IPCriterion> getIpCriterion(FlowRule flowRule) {
300
301 Criterion ip = flowRule.selector().getCriterion(Criterion.Type.IPV4_DST);
302 return Optional.ofNullable((IPCriterion) ip);
303 }
304
305 /**
306 * Helper method to get the output instruction given a flow rule.
307 *
308 * @param flowRule the flow rule
309 * @return the output instruction
310 */
311 private Optional<OutputInstruction> getOutput(FlowRule flowRule) {
Kieran McPeake201fe1f2019-06-03 12:47:22 +0100312 return flowRule
Michele Santuarice256492017-06-23 14:44:01 +0200313 .treatment().allInstructions().stream()
314 .filter(instruction -> instruction
315 .type() == Instruction.Type.OUTPUT)
Kieran McPeake201fe1f2019-06-03 12:47:22 +0100316 .map(OutputInstruction.class::cast).findFirst();
Michele Santuarice256492017-06-23 14:44:01 +0200317 }
318
319 private String routingTableBuilder() {
320 StringBuilder rpc = new StringBuilder("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">");
321 rpc.append("<get-route-information/>");
322 rpc.append("</rpc>");
323 return rpc.toString();
324 }
325
326 private boolean commit() {
327 NetconfController controller = checkNotNull(handler()
328 .get(NetconfController.class));
329 NetconfSession session = controller.getDevicesMap()
330 .get(handler().data().deviceId()).getSession();
331
332 String replay;
333 try {
334 replay = session.get(commitBuilder());
Yuta HIGUCHI234eaf32017-09-06 13:45:05 -0700335 } catch (NetconfException e) {
Ray Milkey986a47a2018-01-25 11:38:51 -0800336 throw new IllegalStateException(new NetconfException("Failed to retrieve configuration.",
Michele Santuarice256492017-06-23 14:44:01 +0200337 e));
338 }
339
Kieran McPeake201fe1f2019-06-03 12:47:22 +0100340 return replay != null && replay.contains(OK);
Michele Santuarice256492017-06-23 14:44:01 +0200341 }
342
343 private boolean rollback() {
344 NetconfController controller = checkNotNull(handler()
345 .get(NetconfController.class));
346 NetconfSession session = controller.getDevicesMap()
347 .get(handler().data().deviceId()).getSession();
348
349 String replay;
350 try {
351 replay = session.get(rollbackBuilder(0));
Yuta HIGUCHI234eaf32017-09-06 13:45:05 -0700352 } catch (NetconfException e) {
Ray Milkey986a47a2018-01-25 11:38:51 -0800353 throw new IllegalStateException(new NetconfException("Failed to retrieve configuration.",
Michele Santuarice256492017-06-23 14:44:01 +0200354 e));
355 }
356
Kieran McPeake201fe1f2019-06-03 12:47:22 +0100357 return replay != null && replay.contains(OK);
Michele Santuarice256492017-06-23 14:44:01 +0200358 }
359
360 /**
361 * Helper method to find the next hop IP address.
362 * The logic is to check if the destination ports have an IP address
363 * by checking the logical interface (e.g., for port physical ge-2/0/1,
364 * a logical interface may be ge-2/0/1.0
365 *
366 * @param deviceId the device id of the flow rule to be installed
367 * @param port output port of the flow rule treatment
368 * @return optional IPv4 address of a next hop
369 */
370 private Optional<Ip4Address> findIpDst(DeviceId deviceId, Port port) {
371 LinkService linkService = this.handler().get(LinkService.class);
372 Set<Link> links = linkService.getEgressLinks(new ConnectPoint(deviceId, port.number()));
373 DeviceService deviceService = this.handler().get(DeviceService.class);
374 //Using only links with adjacency discovered by the LLDP protocol (see LinkDiscoveryJuniperImpl)
375 Map<DeviceId, Port> dstPorts = links.stream().filter(l ->
Kieran McPeake201fe1f2019-06-03 12:47:22 +0100376 JuniperUtils.AK_IP.toUpperCase().equals(l.annotations().value(AnnotationKeys.LAYER)))
Michele Santuarice256492017-06-23 14:44:01 +0200377 .collect(Collectors.toMap(
378 l -> l.dst().deviceId(),
379 l -> deviceService.getPort(l.dst().deviceId(), l.dst().port())));
380 for (Map.Entry<DeviceId, Port> entry : dstPorts.entrySet()) {
381 String portName = entry.getValue().annotations().value(AnnotationKeys.PORT_NAME);
382
383 Optional<Port> childPort = deviceService.getPorts(entry.getKey()).stream()
384 .filter(p -> Strings.nullToEmpty(
385 p.annotations().value(AnnotationKeys.PORT_NAME)).contains(portName.trim()))
Kieran McPeake201fe1f2019-06-03 12:47:22 +0100386 .filter(this::isIp)
Michele Santuarice256492017-06-23 14:44:01 +0200387 .findAny();
388 if (childPort.isPresent()) {
Kieran McPeake201fe1f2019-06-03 12:47:22 +0100389 return Optional.ofNullable(Ip4Address.valueOf(childPort.get().annotations().value(JuniperUtils.AK_IP)));
Michele Santuarice256492017-06-23 14:44:01 +0200390 }
391 }
392
393 return Optional.empty();
394 }
395
396 /**
397 * Helper method to find if an interface has an IP address.
398 * It will check the annotations of the port.
399 *
400 * @param port the port
401 * @return true if the IP address is present. Otherwise false.
402 */
403 private boolean isIp(Port port) {
Kieran McPeake201fe1f2019-06-03 12:47:22 +0100404 final String ipv4 = port.annotations().value(JuniperUtils.AK_IP);
405 if (StringUtils.isEmpty(ipv4)) {
Michele Santuarice256492017-06-23 14:44:01 +0200406 return false;
407 }
408 try {
Kieran McPeake201fe1f2019-06-03 12:47:22 +0100409 Ip4Address.valueOf(ipv4);
Michele Santuarice256492017-06-23 14:44:01 +0200410 } catch (IllegalArgumentException e) {
411 return false;
412 }
413 return true;
414 }
415}