Java tutorial
/* * 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("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; } }