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