From FlowRule to static routes for Juniper
Conversion of FlowRules into static routes and retrieve of
installed static routes as FlowRules.
Change-Id: If960e4d15e431ae8f5ea75eff83913056a51c852
diff --git a/drivers/juniper/src/main/java/org/onosproject/drivers/juniper/FlowRuleJuniperImpl.java b/drivers/juniper/src/main/java/org/onosproject/drivers/juniper/FlowRuleJuniperImpl.java
new file mode 100644
index 0000000..ae9d7e9
--- /dev/null
+++ b/drivers/juniper/src/main/java/org/onosproject/drivers/juniper/FlowRuleJuniperImpl.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * 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.io.IOException;
+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 (IOException e) {
+ throw new RuntimeException(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 (IOException e) {
+ throw new RuntimeException(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 (IOException e) {
+ throw new RuntimeException(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 (IOException e) {
+ throw new RuntimeException(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;
+ }
+}