| /* |
| * Copyright 2016 Open Networking Foundation |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.onosproject.drivers.juniper; |
| |
| import com.google.common.annotations.Beta; |
| import com.google.common.base.Strings; |
| import org.apache.commons.lang.StringUtils; |
| import org.onlab.packet.Ip4Address; |
| import org.onosproject.net.AnnotationKeys; |
| import org.onosproject.net.ConnectPoint; |
| import org.onosproject.net.DeviceId; |
| import org.onosproject.net.Link; |
| import org.onosproject.net.Port; |
| import org.onosproject.net.PortNumber; |
| import org.onosproject.net.device.DeviceService; |
| import org.onosproject.net.driver.AbstractHandlerBehaviour; |
| import org.onosproject.net.flow.FlowEntry; |
| import org.onosproject.net.flow.FlowRule; |
| import org.onosproject.net.flow.FlowRuleProgrammable; |
| import org.onosproject.net.flow.FlowRuleService; |
| import org.onosproject.net.flow.criteria.Criterion; |
| import org.onosproject.net.flow.criteria.IPCriterion; |
| import org.onosproject.net.flow.instructions.Instruction; |
| import org.onosproject.net.flow.instructions.Instructions.OutputInstruction; |
| import org.onosproject.net.link.LinkService; |
| import org.onosproject.netconf.DatastoreId; |
| import org.onosproject.netconf.NetconfController; |
| import org.onosproject.netconf.NetconfException; |
| import org.onosproject.netconf.NetconfSession; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static org.onosproject.drivers.juniper.JuniperUtils.OperationType; |
| import static org.onosproject.drivers.juniper.JuniperUtils.OperationType.ADD; |
| import static org.onosproject.drivers.juniper.JuniperUtils.OperationType.REMOVE; |
| import static org.onosproject.drivers.juniper.JuniperUtils.commitBuilder; |
| import static org.onosproject.drivers.juniper.JuniperUtils.rollbackBuilder; |
| import static org.onosproject.drivers.juniper.JuniperUtils.routeAddBuilder; |
| import static org.onosproject.drivers.juniper.JuniperUtils.routeDeleteBuilder; |
| import static org.onosproject.drivers.utilities.XmlConfigParser.loadXmlString; |
| import static org.onosproject.net.flow.FlowEntry.FlowEntryState.PENDING_REMOVE; |
| import static org.onosproject.net.flow.FlowEntry.FlowEntryState.REMOVED; |
| import static org.slf4j.LoggerFactory.getLogger; |
| |
| /** |
| * Conversion of FlowRules into static routes and retrieve of installed |
| * static routes as FlowRules. |
| * The selector of the FlowRule must contains the IPv4 address |
| * {@link org.onosproject.net.flow.TrafficSelector.Builder#matchIPDst(org.onlab.packet.IpPrefix)} |
| * of the host to connect and the treatment must include an |
| * output port {@link org.onosproject.net.flow.TrafficTreatment.Builder#setOutput(PortNumber)} |
| * All other instructions in the selector and treatment are ignored. |
| * <p> |
| * This implementation requires an IP adjacency |
| * (e.g., IP link discovered by {@link LinkDiscoveryJuniperImpl}) between the routers |
| * to find the next hop IP address. |
| */ |
| @Beta |
| public class FlowRuleJuniperImpl extends AbstractHandlerBehaviour |
| implements FlowRuleProgrammable { |
| |
| private static final String OK = "<ok/>"; |
| public static final String IP_STRING = "ip"; |
| private final org.slf4j.Logger log = getLogger(getClass()); |
| |
| @Override |
| public Collection<FlowEntry> getFlowEntries() { |
| |
| DeviceId devId = checkNotNull(this.data().deviceId()); |
| NetconfController controller = checkNotNull(handler().get(NetconfController.class)); |
| NetconfSession session = controller.getDevicesMap().get(devId).getSession(); |
| if (session == null) { |
| log.warn("Device {} is not registered in netconf", devId); |
| return Collections.EMPTY_LIST; |
| } |
| |
| //Installed static routes |
| String reply; |
| try { |
| reply = session.get(routingTableBuilder()); |
| } catch (NetconfException e) { |
| throw new IllegalStateException(new NetconfException("Failed to retrieve configuration.", |
| e)); |
| } |
| Collection<StaticRoute> devicesStaticRoutes = |
| JuniperUtils.parseRoutingTable(loadXmlString(reply)); |
| |
| //Expected FlowEntries installed |
| FlowRuleService flowRuleService = this.handler().get(FlowRuleService.class); |
| Iterable<FlowEntry> flowEntries = flowRuleService.getFlowEntries(devId); |
| |
| Collection<FlowEntry> installedRules = new HashSet<>(); |
| flowEntries.forEach(flowEntry -> { |
| Optional<IPCriterion> ipCriterion = getIpCriterion(flowEntry); |
| if (!ipCriterion.isPresent()) { |
| return; |
| } |
| |
| Optional<OutputInstruction> output = getOutput(flowEntry); |
| if (!output.isPresent()) { |
| return; |
| } |
| //convert FlowRule into static route |
| getStaticRoute(devId, ipCriterion.get(), output.get(), flowEntry.priority()).ifPresent(staticRoute -> { |
| //Two type of FlowRules: |
| //1. FlowRules to forward to a remote subnet: they are translated into static route |
| // configuration. So a removal request will be processed. |
| //2. FlowRules to forward on a subnet directly attached to the router (Generally speaking called local): |
| // those routes do not require any configuration because the router is already able to forward on |
| // directly attached subnet. In this case, when the driver receive the request to remove, |
| // it will report as removed. |
| |
| if (staticRoute.isLocalRoute()) { |
| //if the FlowRule is in PENDING_REMOVE or REMOVED, it is not reported. |
| if (flowEntry.state() == PENDING_REMOVE || flowEntry.state() == REMOVED) { |
| devicesStaticRoutes.remove(staticRoute); |
| } else { |
| //FlowRule is reported installed |
| installedRules.add(flowEntry); |
| devicesStaticRoutes.remove(staticRoute); |
| } |
| |
| } else { |
| //if the route is found in the device, the FlowRule is reported installed. |
| if (devicesStaticRoutes.contains(staticRoute)) { |
| installedRules.add(flowEntry); |
| devicesStaticRoutes.remove(staticRoute); |
| } |
| } |
| }); |
| }); |
| |
| if (!devicesStaticRoutes.isEmpty()) { |
| log.info("Found static routes on device {} not installed by ONOS: {}", |
| devId, devicesStaticRoutes); |
| // FIXME: enable configuration to purge already installed flows. |
| // It cannot be allowed by default because it may remove needed management routes |
| // log.warn("Removing from device {} the FlowEntries not expected {}", deviceId, devicesStaticRoutes); |
| // devicesStaticRoutes.forEach(staticRoute -> editRoute(session, REMOVE, staticRoute)); |
| } |
| return installedRules; |
| } |
| |
| @Override |
| public Collection<FlowRule> applyFlowRules(Collection<FlowRule> rules) { |
| return manageRules(rules, ADD); |
| } |
| |
| @Override |
| public Collection<FlowRule> removeFlowRules(Collection<FlowRule> rules) { |
| return manageRules(rules, REMOVE); |
| } |
| |
| private Collection<FlowRule> manageRules(Collection<FlowRule> rules, OperationType type) { |
| |
| DeviceId deviceId = this.data().deviceId(); |
| |
| log.debug("{} flow entries to NETCONF device {}", type, deviceId); |
| NetconfController controller = checkNotNull(handler() |
| .get(NetconfController.class)); |
| NetconfSession session = controller.getDevicesMap().get(deviceId) |
| .getSession(); |
| Collection<FlowRule> managedRules = new HashSet<>(); |
| |
| for (FlowRule flowRule : rules) { |
| |
| Optional<IPCriterion> ipCriterion = getIpCriterion(flowRule); |
| if (!ipCriterion.isPresent()) { |
| log.error("Currently not supported: IPv4 destination match must be used"); |
| continue; |
| } |
| |
| Optional<OutputInstruction> output = getOutput(flowRule); |
| if (!output.isPresent()) { |
| log.error("Currently not supported: the output action is needed"); |
| continue; |
| } |
| |
| getStaticRoute(deviceId, ipCriterion.get(), output.get(), flowRule.priority()).ifPresent( |
| staticRoute -> { |
| //If the static route is not local, the driver tries to install |
| if (!staticRoute.isLocalRoute()) { |
| //Only if the installation is successful, the driver report the |
| // FlowRule as installed. |
| if (editRoute(session, type, staticRoute)) { |
| managedRules.add(flowRule); |
| } |
| //If static route are local, they are not installed |
| // because are not required. Directly connected routes |
| //are automatically forwarded. |
| } else { |
| managedRules.add(flowRule); |
| } |
| } |
| ); |
| } |
| return rules; |
| } |
| |
| private boolean editRoute(NetconfSession session, OperationType type, |
| StaticRoute staticRoute) { |
| try { |
| boolean reply = false; |
| if (type == ADD) { |
| reply = session |
| .editConfig(DatastoreId.CANDIDATE, "merge", |
| routeAddBuilder(staticRoute)); |
| } else if (type == REMOVE) { |
| reply = session |
| .editConfig(DatastoreId.CANDIDATE, "none", routeDeleteBuilder(staticRoute)); |
| } |
| if (reply && commit()) { |
| return true; |
| } else { |
| if (!rollback()) { |
| log.error("Something went wrong in the configuration and impossible to rollback"); |
| } else { |
| log.error("Something went wrong in the configuration: a static route has not been {} {}", |
| type == ADD ? "added" : "removed", staticRoute); |
| } |
| } |
| } catch (NetconfException e) { |
| throw new IllegalStateException(new NetconfException("Failed to retrieve configuration.", |
| e)); |
| } |
| return false; |
| } |
| |
| /** |
| * Helper method to convert FlowRule into an abstraction of static route |
| * {@link StaticRoute}. |
| * |
| * @param devId the device id |
| * @param criteria the IP destination criteria |
| * @param output the output instruction |
| * @return optional of Static Route |
| */ |
| private Optional<StaticRoute> getStaticRoute(DeviceId devId, |
| IPCriterion criteria, |
| OutputInstruction output, |
| int priority) { |
| |
| DeviceService deviceService = this.handler().get(DeviceService.class); |
| Collection<Port> ports = deviceService.getPorts(devId); |
| Optional<Port> port = ports.stream().filter(x -> x.number().equals(output.port())).findAny(); |
| if (!port.isPresent()) { |
| log.error("The port {} does not exist in the device", |
| output.port()); |
| return Optional.empty(); |
| } |
| |
| //Find if the route refers to a local interface. |
| Optional<Port> local = deviceService.getPorts(devId).stream().filter(this::isIp) |
| .filter(p -> criteria.ip().getIp4Prefix().contains( |
| Ip4Address.valueOf(p.annotations().value(IP_STRING)))).findAny(); |
| |
| if (local.isPresent()) { |
| return Optional.of(new StaticRoute(criteria.ip().getIp4Prefix(), |
| criteria.ip().getIp4Prefix().address(), true, priority)); |
| } |
| |
| Optional<Ip4Address> nextHop = findIpDst(devId, port.get()); |
| if (nextHop.isPresent()) { |
| return Optional.of( |
| new StaticRoute(criteria.ip().getIp4Prefix(), nextHop.get(), false, priority)); |
| } else { |
| log.error("The destination interface has not an IP {}", port.get()); |
| return Optional.empty(); |
| } |
| |
| } |
| |
| /** |
| * Helper method to get the IP destination criterion given a flow rule. |
| * |
| * @param flowRule the flow rule |
| * @return optional of IP destination criterion |
| */ |
| private Optional<IPCriterion> getIpCriterion(FlowRule flowRule) { |
| |
| Criterion ip = flowRule.selector().getCriterion(Criterion.Type.IPV4_DST); |
| return Optional.ofNullable((IPCriterion) ip); |
| } |
| |
| /** |
| * Helper method to get the output instruction given a flow rule. |
| * |
| * @param flowRule the flow rule |
| * @return the output instruction |
| */ |
| private Optional<OutputInstruction> getOutput(FlowRule flowRule) { |
| Optional<OutputInstruction> output = flowRule |
| .treatment().allInstructions().stream() |
| .filter(instruction -> instruction |
| .type() == Instruction.Type.OUTPUT) |
| .map(x -> (OutputInstruction) x).findFirst(); |
| return output; |
| } |
| |
| private String routingTableBuilder() { |
| StringBuilder rpc = new StringBuilder("<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">"); |
| rpc.append("<get-route-information/>"); |
| rpc.append("</rpc>"); |
| return rpc.toString(); |
| } |
| |
| private boolean commit() { |
| NetconfController controller = checkNotNull(handler() |
| .get(NetconfController.class)); |
| NetconfSession session = controller.getDevicesMap() |
| .get(handler().data().deviceId()).getSession(); |
| |
| String replay; |
| try { |
| replay = session.get(commitBuilder()); |
| } catch (NetconfException e) { |
| throw new IllegalStateException(new NetconfException("Failed to retrieve configuration.", |
| e)); |
| } |
| |
| if (replay != null && replay.indexOf(OK) >= 0) { |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean rollback() { |
| NetconfController controller = checkNotNull(handler() |
| .get(NetconfController.class)); |
| NetconfSession session = controller.getDevicesMap() |
| .get(handler().data().deviceId()).getSession(); |
| |
| String replay; |
| try { |
| replay = session.get(rollbackBuilder(0)); |
| } catch (NetconfException e) { |
| throw new IllegalStateException(new NetconfException("Failed to retrieve configuration.", |
| e)); |
| } |
| |
| if (replay != null && replay.indexOf(OK) >= 0) { |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Helper method to find the next hop IP address. |
| * The logic is to check if the destination ports have an IP address |
| * by checking the logical interface (e.g., for port physical ge-2/0/1, |
| * a logical interface may be ge-2/0/1.0 |
| * |
| * @param deviceId the device id of the flow rule to be installed |
| * @param port output port of the flow rule treatment |
| * @return optional IPv4 address of a next hop |
| */ |
| private Optional<Ip4Address> findIpDst(DeviceId deviceId, Port port) { |
| LinkService linkService = this.handler().get(LinkService.class); |
| Set<Link> links = linkService.getEgressLinks(new ConnectPoint(deviceId, port.number())); |
| DeviceService deviceService = this.handler().get(DeviceService.class); |
| //Using only links with adjacency discovered by the LLDP protocol (see LinkDiscoveryJuniperImpl) |
| Map<DeviceId, Port> dstPorts = links.stream().filter(l -> |
| IP_STRING.toUpperCase().equals(l.annotations().value(AnnotationKeys.LAYER))) |
| .collect(Collectors.toMap( |
| l -> l.dst().deviceId(), |
| l -> deviceService.getPort(l.dst().deviceId(), l.dst().port()))); |
| for (Map.Entry<DeviceId, Port> entry : dstPorts.entrySet()) { |
| String portName = entry.getValue().annotations().value(AnnotationKeys.PORT_NAME); |
| |
| Optional<Port> childPort = deviceService.getPorts(entry.getKey()).stream() |
| .filter(p -> Strings.nullToEmpty( |
| p.annotations().value(AnnotationKeys.PORT_NAME)).contains(portName.trim())) |
| .filter(p -> isIp(p)) |
| .findAny(); |
| if (childPort.isPresent()) { |
| return Optional.ofNullable(Ip4Address.valueOf(childPort.get().annotations().value("ip"))); |
| } |
| } |
| |
| return Optional.empty(); |
| } |
| |
| /** |
| * Helper method to find if an interface has an IP address. |
| * It will check the annotations of the port. |
| * |
| * @param port the port |
| * @return true if the IP address is present. Otherwise false. |
| */ |
| private boolean isIp(Port port) { |
| String ip4 = port.annotations().value(IP_STRING); |
| if (StringUtils.isEmpty(ip4)) { |
| return false; |
| } |
| try { |
| |
| Ip4Address.valueOf(port.annotations().value(IP_STRING)); |
| } catch (IllegalArgumentException e) { |
| return false; |
| } |
| return true; |
| } |
| } |