[AETHER-38] Extract pipeline-dependent code from current T3 implementation
- Exposes some ofdpa specific tables and types
- Introduces a new driver behavior PipelineTraceable
- OfdpaPipelineTraceable is the first implementation of the
new driver behavior
- New abstractions are introduced to encapsulate the input/output
of the traceables processing
- Implements some basic unit tests for Ofdpa implementation
Change-Id: I89d3fdeda445983ec7ebfa9ebb78afb1c6d3fd8f
diff --git a/drivers/default/src/main/java/org/onosproject/driver/traceable/OfdpaPipelineTraceable.java b/drivers/default/src/main/java/org/onosproject/driver/traceable/OfdpaPipelineTraceable.java
new file mode 100644
index 0000000..c269022
--- /dev/null
+++ b/drivers/default/src/main/java/org/onosproject/driver/traceable/OfdpaPipelineTraceable.java
@@ -0,0 +1,785 @@
+/*
+ * Copyright 2020-present 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.driver.traceable;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.onlab.packet.EthType;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.GroupId;
+import org.onosproject.driver.pipeline.ofdpa.Ofdpa2Pipeline;
+import org.onosproject.driver.pipeline.ofdpa.OvsOfdpaPipeline;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PipelineTraceableHitChain;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DataPlaneEntity;
+import org.onosproject.net.PipelineTraceableInput;
+import org.onosproject.net.PipelineTraceableOutput;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.behaviour.PipelineTraceable;
+import org.onosproject.net.behaviour.Pipeliner;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.IndexTableId;
+import org.onosproject.net.flow.TableId;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.EthCriterion;
+import org.onosproject.net.flow.criteria.EthTypeCriterion;
+import org.onosproject.net.flow.criteria.IPCriterion;
+import org.onosproject.net.flow.criteria.MetadataCriterion;
+import org.onosproject.net.flow.criteria.VlanIdCriterion;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+import org.onosproject.net.flow.instructions.L2ModificationInstruction;
+import org.onosproject.net.flow.instructions.L2ModificationInstruction.ModVlanIdInstruction;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaGroupHandlerUtility.L2_FLOOD_TYPE;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaGroupHandlerUtility.L2_INTERFACE_TYPE;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaGroupHandlerUtility.L2_MULTICAST_TYPE;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaGroupHandlerUtility.L3_MULTICAST_TYPE;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaPipelineUtility.ACL_TABLE;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaPipelineUtility.BRIDGING_TABLE;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaPipelineUtility.MPLS_L3_TYPE_TABLE;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaPipelineUtility.MULTICAST_ROUTING_TABLE;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaPipelineUtility.TMAC_TABLE;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaPipelineUtility.VLAN_TABLE;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implements a driver behavior that enables a logical probe packet to traverse the device pipeline
+ * and to return dataplane entities that matched against the logical probe packet.
+ */
+public class OfdpaPipelineTraceable extends AbstractHandlerBehaviour implements PipelineTraceable {
+
+ private static final Logger log = getLogger(OfdpaPipelineTraceable.class);
+ // Behavior context
+ private DeviceId deviceId;
+ private String driverName;
+ // Utility
+ private final Comparator<FlowEntry> comparatorById = Comparator.comparing(
+ (FlowEntry f) -> ((IndexTableId) f.table()).id());
+ private final Comparator<FlowEntry> comparatorByPriority = Comparator.comparing(
+ FlowRule::priority);
+
+ @Override
+ public void init() {
+ this.deviceId = this.data().deviceId();
+ this.driverName = this.data().driver().name();
+ }
+
+ @Override
+ public PipelineTraceableOutput apply(PipelineTraceableInput input) {
+ PipelineTraceableOutput.Builder outputBuilder = PipelineTraceableOutput.builder();
+ log.debug("Current packet {} - applying flow tables", input.ingressPacket());
+ List<FlowEntry> outputFlows = new ArrayList<>();
+ List<Instruction> deferredInstructions = new ArrayList<>();
+ PipelineTraceableHitChain currentHitChain = PipelineTraceableHitChain.emptyHitChain();
+ TrafficSelector currentPacket = DefaultTrafficSelector.builder(input.ingressPacket()).build();
+
+ // Init step - find out the first table
+ int initialTableId = -1;
+ FlowEntry nextTableIdEntry = findNextTableIdEntry(initialTableId, input.flows());
+ if (nextTableIdEntry == null) {
+ currentHitChain.setEgressPacket(currentPacket);
+ currentHitChain.dropped();
+ return outputBuilder.appendToLog("No flow rules for device " + deviceId + ". Aborting")
+ .noFlows()
+ .addHitChain(currentHitChain)
+ .build();
+ }
+
+ // Iterates over the flow tables until the end of the pipeline
+ TableId tableId = nextTableIdEntry.table();
+ FlowEntry flowEntry;
+ boolean lastTable = false;
+ while (!lastTable) {
+ log.debug("Searching a Flow Entry on table {} for packet {}", tableId, currentPacket);
+
+ // Gets the rule that matches the incoming packet
+ flowEntry = matchHighestPriority(currentPacket, tableId, input.flows());
+ log.debug("Found Flow Entry {}", flowEntry);
+
+ // If the flow entry on a table is null and we are on hardware we treat as table miss, with few exceptions
+ if (flowEntry == null && isHardwareSwitch()) {
+ log.debug("Ofdpa Hw setup, no flow rule means table miss");
+
+ if (((IndexTableId) tableId).id() == MPLS_L3_TYPE_TABLE) {
+ // Apparently a miss but Table 27 on OFDPA is a fixed table
+ currentPacket = handleOfdpa27FixedTable(input.ingressPacket(), currentPacket);
+ // The nextTable should be ACL
+ tableId = IndexTableId.of(ACL_TABLE - 1);
+ }
+
+ // Finding next table to go In case of miss
+ nextTableIdEntry = findNextTableIdEntry(((IndexTableId) tableId).id(), input.flows());
+ log.debug("Next table id entry {}", nextTableIdEntry);
+ // FIXME Find better solution that enable granularity greater than 0 or all rules
+ // (another possibility is max tableId)
+ if (nextTableIdEntry == null && currentHitChain.getHitChain().size() == 0) {
+ currentHitChain.setEgressPacket(currentPacket);
+ currentHitChain.dropped();
+ return outputBuilder.appendToLog("No flow rules for device " + deviceId + ". Aborting")
+ .noFlows()
+ .addHitChain(currentHitChain)
+ .build();
+
+ } else if (nextTableIdEntry == null) {
+ // Means that no more flow rules are present
+ lastTable = true;
+
+ } else if (((IndexTableId) tableId).id() == TMAC_TABLE) {
+ // If the table is 20 OFDPA skips to table 50
+ log.debug("A miss on Table 20 on OFDPA means that we skip directly to table 50");
+ tableId = IndexTableId.of(BRIDGING_TABLE);
+
+ } else if (((IndexTableId) tableId).id() == MULTICAST_ROUTING_TABLE) {
+ // If the table is 40 OFDPA skips to table 60
+ log.debug("A miss on Table 40 on OFDPA means that we skip directly to table 60");
+ tableId = IndexTableId.of(ACL_TABLE);
+ } else {
+ tableId = nextTableIdEntry.table();
+ }
+
+ } else if (flowEntry == null) {
+ currentHitChain.setEgressPacket(currentPacket);
+ currentHitChain.dropped();
+ return outputBuilder.appendToLog("Packet has no match on table " + tableId
+ + " in device " + deviceId + ". Dropping")
+ .noFlows()
+ .addHitChain(currentHitChain)
+ .build();
+ } else {
+
+ // If the table has a transition
+ if (flowEntry.treatment().tableTransition() != null) {
+ // Updates the next table we transitions to
+ tableId = IndexTableId.of(flowEntry.treatment().tableTransition().tableId());
+ log.debug("Flow Entry has transition to table Id {}", tableId);
+ currentHitChain.addDataPlaneEntity(new DataPlaneEntity(flowEntry));
+ } else {
+ // Table has no transition so it means that it's an output rule if on the last table
+ log.debug("Flow Entry has no transition to table, treating as last rule {}", flowEntry);
+ currentHitChain.addDataPlaneEntity(new DataPlaneEntity(flowEntry));
+ outputFlows.add(flowEntry);
+ lastTable = true;
+ }
+
+ // Updates the packet according to the immediate actions of this flow rule.
+ currentPacket = updatePacket(currentPacket, flowEntry.treatment().immediate()).build();
+
+ // Saves the deferred rules for later maintaining the order
+ deferredInstructions.addAll(flowEntry.treatment().deferred());
+
+ // If the flow requires to clear deferred actions we do so for all the ones we encountered.
+ if (flowEntry.treatment().clearedDeferred()) {
+ deferredInstructions.clear();
+ }
+
+ // On table 10 OFDPA needs two rules to apply the vlan if none and then to transition to the next table.
+ if (shouldMatchSecondVlanFlow(flowEntry)) {
+
+ // Let's get the packet vlanId instruction
+ VlanIdCriterion packetVlanIdCriterion =
+ (VlanIdCriterion) currentPacket.getCriterion(Criterion.Type.VLAN_VID);
+
+ // Let's get the flow entry vlan mod instructions
+ ModVlanIdInstruction entryModVlanIdInstruction = (ModVlanIdInstruction) flowEntry.treatment()
+ .immediate().stream()
+ .filter(instruction -> instruction instanceof ModVlanIdInstruction)
+ .findFirst().orElse(null);
+
+ // If the entry modVlan is not null we need to make sure that the packet has been updated and there
+ // is a flow rule that matches on same criteria and with updated vlanId
+ if (entryModVlanIdInstruction != null) {
+
+ FlowEntry secondVlanFlow = getSecondFlowEntryOnTable10(currentPacket,
+ packetVlanIdCriterion, entryModVlanIdInstruction, input.flows());
+
+ // We found the flow that we expected
+ if (secondVlanFlow != null) {
+ currentHitChain.addDataPlaneEntity(new DataPlaneEntity(secondVlanFlow));
+ } else {
+ currentHitChain.setEgressPacket(currentPacket);
+ currentHitChain.dropped();
+ return outputBuilder.appendToLog("Missing forwarding rule for tagged"
+ + " packet on " + deviceId)
+ .noFlows()
+ .addHitChain(currentHitChain)
+ .build();
+ }
+ }
+ }
+ }
+ }
+
+ // Creating a modifiable builder for the egress packet
+ TrafficSelector.Builder egressPacket = DefaultTrafficSelector.builder(currentPacket);
+
+ log.debug("Current packet {} - applying output flows", currentPacket);
+ // Handling output flows which basically means handling output to controller.
+ // OVS and OFDPA have both immediate -> OUTPUT:CONTROLLER. Theoretically there is no
+ // need to reflect the updates performed on the packets and on the chain.
+ List<PortNumber> outputPorts = new ArrayList<>();
+ handleOutputFlows(currentPacket, outputFlows, egressPacket, outputPorts, currentHitChain,
+ outputBuilder, input.ingressPacket());
+
+ // Immediate instructions
+ log.debug("Current packet {} - applying immediate instructions", currentPacket);
+ // Handling immediate instructions which basically means handling output to controller.
+ // OVS has immediate -> group -> OUTPUT:CONTROLLER.
+ List<DataPlaneEntity> entries = ImmutableList.copyOf(currentHitChain.getHitChain());
+ // Go to the next step - using a copy of the egress packet and of the hit chain
+ PipelineTraceableHitChain newHitChain = PipelineTraceableHitChain.emptyHitChain();
+ currentHitChain.getHitChain().forEach(newHitChain::addDataPlaneEntity);
+ TrafficSelector.Builder newEgressPacket = DefaultTrafficSelector.builder(egressPacket.build());
+ for (DataPlaneEntity entry : entries) {
+ flowEntry = entry.getFlowEntry();
+ if (flowEntry != null) {
+ getGroupsFromInstructions(input.groups(), flowEntry.treatment().immediate(), newEgressPacket,
+ outputPorts, newHitChain, outputBuilder, input, false);
+ }
+ }
+
+ // Deferred instructions
+ log.debug("Current packet {} - applying deferred instructions", egressPacket.build());
+ // If we have deferred instructions at this point we handle them.
+ // Here, we are basically handling the normal forwarding scenarios that
+ // always happen through deferred:group. Here we don't care about the
+ // egress packet and of the hit chain. This is the last step.
+ if (deferredInstructions.size() > 0) {
+ handleDeferredActions(egressPacket.build(), input.groups(), deferredInstructions, outputPorts,
+ currentHitChain, outputBuilder, input);
+ }
+
+ // If there are no outputs - packet is dropped
+ // Let's store the partial hit chain and set a message
+ if (outputPorts.isEmpty()) {
+ currentHitChain.setEgressPacket(egressPacket.build());
+ currentHitChain.dropped();
+ outputBuilder.appendToLog("Packet has no output in device " + deviceId + ". Dropping")
+ .dropped()
+ .addHitChain(currentHitChain);
+ }
+
+ // Done!
+ return outputBuilder.build();
+ }
+
+ // Finds the flow entry with the minimum next table Id.
+ private FlowEntry findNextTableIdEntry(int currentId, List<FlowEntry> flows) {
+ return flows.stream()
+ .filter(f -> ((IndexTableId) f.table()).id() > currentId)
+ .min(comparatorById).orElse(null);
+ }
+
+ // Finds the rule in the device that matches the input packet and has the highest priority.
+ // TODO Candidate for an AbstractBehavior implementation
+ private FlowEntry matchHighestPriority(TrafficSelector packet, TableId tableId, List<FlowEntry> flows) {
+ //Computing the possible match rules.
+ return flows.stream()
+ .filter(flowEntry -> flowEntry.table().equals(tableId))
+ .filter(flowEntry -> match(packet, flowEntry))
+ .max(comparatorByPriority).orElse(null);
+ }
+
+ // Matches the packet with the given flow entry
+ // TODO Candidate for an AbstractBehavior implementation
+ private boolean match(TrafficSelector packet, FlowEntry flowEntry) {
+ return flowEntry.selector().criteria().stream().allMatch(criterion -> {
+ Criterion.Type type = criterion.type();
+ //If the criterion has IP we need to do LPM to establish matching.
+ if (type.equals(Criterion.Type.IPV4_SRC) || type.equals(Criterion.Type.IPV4_DST) ||
+ type.equals(Criterion.Type.IPV6_SRC) || type.equals(Criterion.Type.IPV6_DST)) {
+ return matchIp(packet, (IPCriterion) criterion);
+ //we check that the packet contains the criterion provided by the flow rule.
+ } else if (type.equals(Criterion.Type.ETH_SRC_MASKED)) {
+ return matchMac(packet, (EthCriterion) criterion, false);
+ } else if (type.equals(Criterion.Type.ETH_DST_MASKED)) {
+ return matchMac(packet, (EthCriterion) criterion, true);
+ } else {
+ return packet.criteria().contains(criterion);
+ }
+ });
+ }
+
+ // Checks if the packet has an dst or src IP and if that IP matches the subnet of the ip criterion
+ // TODO Candidate for an AbstractBehavior implementation
+ private boolean matchIp(TrafficSelector packet, IPCriterion criterion) {
+ IPCriterion matchCriterion = (IPCriterion) packet.getCriterion(criterion.type());
+ // if the packet does not have an IPv4 or IPv6 criterion we return true
+ if (matchCriterion == null) {
+ return false;
+ }
+ log.debug("Checking if {} is under {}", matchCriterion.ip(), criterion.ip());
+ IpPrefix subnet = criterion.ip();
+ return subnet.contains(matchCriterion.ip().address());
+ }
+
+ // Checks if the packet has a dst or src MAC and if that Mac matches the mask of the mac criterion
+ // TODO Candidate for an AbstractBehavior implementation
+ private boolean matchMac(TrafficSelector packet, EthCriterion hitCriterion, boolean dst) {
+ //Packet can have only one EthCriterion
+ EthCriterion matchCriterion;
+ if (dst) {
+ matchCriterion = (EthCriterion) packet.criteria().stream().filter(criterion1 ->
+ criterion1.type().equals(Criterion.Type.ETH_DST_MASKED) ||
+ criterion1.type().equals(Criterion.Type.ETH_DST))
+ .findFirst().orElse(null);
+ } else {
+ matchCriterion = (EthCriterion) packet.criteria().stream().filter(criterion1 ->
+ criterion1.type().equals(Criterion.Type.ETH_SRC_MASKED) ||
+ criterion1.type().equals(Criterion.Type.ETH_SRC))
+ .findFirst().orElse(null);
+ }
+ //if the packet does not have an ETH criterion we return true
+ if (matchCriterion == null) {
+ return true;
+ }
+ log.debug("Checking if {} is under {}/{}", matchCriterion.mac(), hitCriterion.mac(), hitCriterion.mask());
+ return matchCriterion.mac().inRange(hitCriterion.mac(), hitCriterion.mask());
+ }
+
+ // Handles table 27 in Ofpda which is a fixed table not visible to any controller that handles Mpls Labels.
+ private TrafficSelector handleOfdpa27FixedTable(TrafficSelector initialPacket, TrafficSelector packet) {
+ log.debug("Handling table 27 on OFDPA, removing mpls ETH Type and change mpls label");
+
+ Criterion mplsCriterion = packet.getCriterion(Criterion.Type.ETH_TYPE);
+ // T3 was using the initial packet of the trace - using the metadata in the packet to carry this info
+ Criterion metadataCriterion = initialPacket.getCriterion(Criterion.Type.METADATA);
+ ImmutableList.Builder<Instruction> builder = ImmutableList.builder();
+
+ // If the packet comes in with the expected elements we update it as per OFDPA spec.
+ if (mplsCriterion != null && ((EthTypeCriterion) mplsCriterion).ethType()
+ .equals(EthType.EtherType.MPLS_UNICAST.ethType()) && metadataCriterion != null) {
+
+ // Get the metadata to restore the original ethertype
+ long ethType = ((MetadataCriterion) metadataCriterion).metadata();
+ //TODO update with parsing with eth MPLS pop Instruction for treating label an bos
+ Instruction ethInstruction = Instructions.popMpls(EthType.EtherType.lookup((short) ethType).ethType());
+ //FIXME what do we use as L3_Unicast mpls Label ?
+ //translateInstruction(builder, ethInstruction);
+ builder.add(ethInstruction);
+
+ // Filtering out metadata
+ TrafficSelector.Builder currentPacketBuilder = DefaultTrafficSelector.builder();
+ packet.criteria().stream()
+ .filter(criterion -> criterion.type() != Criterion.Type.METADATA)
+ .forEach(currentPacketBuilder::add);
+ packet = currentPacketBuilder.build();
+ }
+ packet = updatePacket(packet, builder.build()).build();
+ return packet;
+ }
+
+ // Applies all give instructions to the input packet
+ private TrafficSelector.Builder updatePacket(TrafficSelector packet, List<Instruction> instructions) {
+ TrafficSelector.Builder newSelector = DefaultTrafficSelector.builder(packet);
+
+ //FIXME optimize
+ for (Instruction instruction : instructions) {
+ newSelector = translateInstruction(newSelector, instruction);
+ }
+ return newSelector;
+ }
+
+ // Applies an instruction to the packet in the form of a selector
+ private TrafficSelector.Builder translateInstruction(TrafficSelector.Builder newSelector, Instruction instruction) {
+ log.debug("Translating instruction {}", instruction);
+ log.debug("New Selector {}", newSelector.build());
+ //TODO add as required
+ Criterion criterion = null;
+ if (instruction.type() == Instruction.Type.L2MODIFICATION) {
+ L2ModificationInstruction l2Instruction = (L2ModificationInstruction) instruction;
+ switch (l2Instruction.subtype()) {
+ case VLAN_ID:
+ ModVlanIdInstruction vlanIdInstruction =
+ (ModVlanIdInstruction) instruction;
+ VlanId id = vlanIdInstruction.vlanId();
+ criterion = Criteria.matchVlanId(id);
+ break;
+ case VLAN_POP:
+ criterion = Criteria.matchVlanId(VlanId.NONE);
+ break;
+ case MPLS_PUSH:
+ L2ModificationInstruction.ModMplsHeaderInstruction mplsEthInstruction =
+ (L2ModificationInstruction.ModMplsHeaderInstruction) instruction;
+ criterion = Criteria.matchEthType(mplsEthInstruction.ethernetType().toShort());
+
+ // When pushing MPLS adding metadata to remember the original ethtype
+ if (isHardwareSwitch()) {
+ TrafficSelector temporaryPacket = newSelector.build();
+ Criterion ethCriterion = temporaryPacket.getCriterion(Criterion.Type.ETH_TYPE);
+ if (ethCriterion != null) {
+ TrafficSelector.Builder tempSelector = DefaultTrafficSelector.builder(temporaryPacket);
+ // Store the old ether type for the
+ tempSelector.matchMetadata(((EthTypeCriterion) ethCriterion).ethType().toShort());
+ newSelector = tempSelector;
+ }
+ }
+
+ break;
+ case MPLS_POP:
+ L2ModificationInstruction.ModMplsHeaderInstruction mplsPopInstruction =
+ (L2ModificationInstruction.ModMplsHeaderInstruction) instruction;
+ criterion = Criteria.matchEthType(mplsPopInstruction.ethernetType().toShort());
+
+ //When popping MPLS we remove label and BOS
+ TrafficSelector temporaryPacket = newSelector.build();
+ if (temporaryPacket.getCriterion(Criterion.Type.MPLS_LABEL) != null) {
+ TrafficSelector.Builder noMplsSelector = DefaultTrafficSelector.builder();
+ temporaryPacket.criteria().stream().filter(c ->
+ !c.type().equals(Criterion.Type.MPLS_LABEL) &&
+ !c.type().equals(Criterion.Type.MPLS_BOS))
+ .forEach(noMplsSelector::add);
+ newSelector = noMplsSelector;
+ }
+
+ break;
+ case MPLS_LABEL:
+ L2ModificationInstruction.ModMplsLabelInstruction mplsLabelInstruction =
+ (L2ModificationInstruction.ModMplsLabelInstruction) instruction;
+ criterion = Criteria.matchMplsLabel(mplsLabelInstruction.label());
+ newSelector.matchMplsBos(true);
+ break;
+ case ETH_DST:
+ L2ModificationInstruction.ModEtherInstruction modEtherDstInstruction =
+ (L2ModificationInstruction.ModEtherInstruction) instruction;
+ criterion = Criteria.matchEthDst(modEtherDstInstruction.mac());
+ break;
+ case ETH_SRC:
+ L2ModificationInstruction.ModEtherInstruction modEtherSrcInstruction =
+ (L2ModificationInstruction.ModEtherInstruction) instruction;
+ criterion = Criteria.matchEthSrc(modEtherSrcInstruction.mac());
+ break;
+ default:
+ log.debug("Unsupported L2 Instruction");
+ break;
+ }
+ } else {
+ log.debug("Unsupported Instruction");
+ }
+ if (criterion != null) {
+ log.debug("Adding criterion {}", criterion);
+ newSelector.add(criterion);
+ }
+ return newSelector;
+ }
+
+ // Method that finds a flow rule on table 10 that matches the packet and the VLAN of the already
+ // found rule on table 10. This is because OFDPA needs two rules on table 10, first to apply the rule,
+ // second to transition to following table
+ private FlowEntry getSecondFlowEntryOnTable10(TrafficSelector packet, VlanIdCriterion packetVlanIdCriterion,
+ ModVlanIdInstruction entryModVlanIdInstruction,
+ List<FlowEntry> flows) {
+ FlowEntry secondVlanFlow = null;
+ // Check the packet has been update from the first rule.
+ if (packetVlanIdCriterion.vlanId().equals(entryModVlanIdInstruction.vlanId())) {
+ // find a rule on the same table that matches the vlan and
+ // also all the other elements of the flow such as input port
+ secondVlanFlow = flows.stream()
+ .filter(entry -> entry.table().equals(IndexTableId.of(VLAN_TABLE)))
+ .filter(entry -> {
+ VlanIdCriterion criterion = (VlanIdCriterion) entry.selector()
+ .getCriterion(Criterion.Type.VLAN_VID);
+ return criterion != null && match(packet, entry)
+ && criterion.vlanId().equals(entryModVlanIdInstruction.vlanId());
+ }).findFirst().orElse(null);
+
+ }
+ return secondVlanFlow;
+ }
+
+ // Handles output flows
+ private List<FlowEntry> handleOutputFlows(TrafficSelector currentPacket, List<FlowEntry> outputFlows,
+ TrafficSelector.Builder egressPacket, List<PortNumber> outputPorts,
+ PipelineTraceableHitChain currentHitChain,
+ PipelineTraceableOutput.Builder outputBuilder,
+ TrafficSelector initialPacket) {
+ // TODO optimization
+ // outputFlows contains also last rule of device, so we need filtering for OUTPUT instructions.
+ List<FlowEntry> outputFlowEntries = outputFlows.stream().filter(flow -> flow.treatment()
+ .allInstructions().stream().filter(instruction -> instruction.type()
+ .equals(Instruction.Type.OUTPUT)).count() > 0).collect(Collectors.toList());
+
+ if (outputFlowEntries.size() > 1) {
+ outputBuilder.appendToLog("More than one flow rule with OUTPUT instruction");
+ log.warn("There cannot be more than one flow entry with OUTPUT instruction for {}", currentPacket);
+ }
+
+ if (outputFlowEntries.size() == 1) {
+ OutputInstruction outputInstruction = (OutputInstruction) outputFlowEntries.get(0).treatment()
+ .allInstructions().stream()
+ .filter(instruction -> instruction.type().equals(Instruction.Type.OUTPUT))
+ .findFirst().get();
+ buildOutputFromDevice(egressPacket, outputPorts, outputInstruction, currentHitChain,
+ outputBuilder, initialPacket, false);
+ }
+
+ return outputFlowEntries;
+ }
+
+ // Builds a possible output from this device
+ private void buildOutputFromDevice(TrafficSelector.Builder egressPacket,
+ List<PortNumber> outputPorts,
+ OutputInstruction outputInstruction,
+ PipelineTraceableHitChain currentHitChain,
+ PipelineTraceableOutput.Builder outputBuilder,
+ TrafficSelector initialPacket,
+ boolean dropped) {
+ // Store the output port for further processing
+ outputPorts.add(outputInstruction.port());
+ // Create the final hit chain from the current one (deep copy)
+ ConnectPoint outputPort = new ConnectPoint(deviceId, outputInstruction.port());
+ PipelineTraceableHitChain finalHitChain = new PipelineTraceableHitChain(outputPort,
+ Lists.newArrayList(currentHitChain.getHitChain()),
+ egressPacket.build());
+ // Dropped early
+ if (dropped) {
+ log.debug("Packet {} has been dropped", egressPacket.build());
+ } else {
+ finalHitChain.pass();
+ }
+ if (outputPort.port().equals(PortNumber.CONTROLLER)) {
+ handleVlanToController(finalHitChain, initialPacket);
+ }
+ // If there is already a chain do not add a copy
+ outputBuilder.addHitChain(finalHitChain);
+ }
+
+ // If the initial packet comes tagged with a Vlan we output it with that to ONOS.
+ // If ONOS applied a vlan we remove it.
+ // TODO Candidate for an AbstractBehavior implementation
+ private void handleVlanToController(PipelineTraceableHitChain currentHitChain, TrafficSelector initialPacket) {
+
+ VlanIdCriterion initialVid = (VlanIdCriterion) initialPacket
+ .getCriterion(Criterion.Type.VLAN_VID);
+ VlanIdCriterion finalVid = (VlanIdCriterion) currentHitChain.getEgressPacket()
+ .getCriterion(Criterion.Type.VLAN_VID);
+
+ if (initialVid != null && !initialVid.equals(finalVid) && initialVid.vlanId().equals(VlanId.NONE)) {
+ Set<Criterion> finalCriteria = new HashSet<>(currentHitChain.getEgressPacket().criteria());
+ //removing the final vlanId
+ finalCriteria.remove(finalVid);
+ TrafficSelector.Builder packetUpdated = DefaultTrafficSelector.builder();
+ finalCriteria.forEach(packetUpdated::add);
+ //Initial was none so we set it to that
+ packetUpdated.add(Criteria.matchVlanId(VlanId.NONE));
+ //Update final packet
+ currentHitChain.setEgressPacket(packetUpdated.build());
+ }
+ }
+
+ // Gets group information from instructions.
+ private void getGroupsFromInstructions(Map<GroupId, Group> groups, List<Instruction> instructions,
+ TrafficSelector.Builder egressPacket, List<PortNumber> outputPorts,
+ PipelineTraceableHitChain currentHitChain,
+ PipelineTraceableOutput.Builder outputBuilder,
+ PipelineTraceableInput input,
+ boolean dropped) {
+
+ List<Instruction> groupInstructionlist = new ArrayList<>();
+ // sort instructions according to priority (larger Instruction.Type ENUM constant first)
+ // which enables to treat other actions before the OUTPUT action
+ // TODO improve the priority scheme according to the OpenFlow ActionSet spec
+ List<Instruction> instructionsSorted = new ArrayList<>();
+ instructionsSorted.addAll(instructions);
+ instructionsSorted.sort((instr1, instr2) ->
+ Integer.compare(instr2.type().ordinal(), instr1.type().ordinal()));
+
+ // Handles first all non-group instructions
+ for (Instruction instruction : instructionsSorted) {
+ log.debug("Considering Instruction {}", instruction);
+ // if the instruction is not group we need to update the packet or add the output
+ // to the possible outputs for this packet
+ if (!instruction.type().equals(Instruction.Type.GROUP)) {
+ // FIXME ?? if the instruction is not group we need to update the packet
+ // or add the output to the possible outputs for this packet
+ if (instruction.type().equals(Instruction.Type.OUTPUT)) {
+ buildOutputFromDevice(egressPacket, outputPorts, (OutputInstruction) instruction,
+ currentHitChain, outputBuilder, input.ingressPacket(), dropped);
+ } else {
+ egressPacket = translateInstruction(egressPacket, instruction);
+ }
+ } else {
+ // Store for later if the instruction is pointing to a group
+ groupInstructionlist.add(instruction);
+ }
+ }
+
+ // handle all the internal instructions pointing to a group.
+ for (Instruction instr : groupInstructionlist) {
+ Instructions.GroupInstruction groupInstruction = (Instructions.GroupInstruction) instr;
+ Group group = groups.get(groupInstruction.groupId());
+
+ // group does not exist in the dataplane
+ if (group == null) {
+ currentHitChain.setEgressPacket(egressPacket.build());
+ currentHitChain.dropped();
+ outputBuilder.appendToLog("Null group for Instruction " + instr)
+ .noGroups()
+ .addHitChain(currentHitChain);
+ break;
+ }
+
+ log.debug("Analyzing group {}", group.id());
+
+ // group is there but there are no members/buckets
+ if (group.buckets().buckets().size() == 0) {
+ // add the group to the traversed groups
+ currentHitChain.addDataPlaneEntity(new DataPlaneEntity(group));
+ currentHitChain.setEgressPacket(egressPacket.build());
+ currentHitChain.dropped();
+ outputBuilder.appendToLog("Group " + group.id() + " has no buckets")
+ .noMembers()
+ .addHitChain(currentHitChain);
+ break;
+ }
+
+ PipelineTraceableHitChain newHitChain;
+ TrafficSelector.Builder newEgressPacket;
+ // Cycle in each of the group's buckets and add them to the groups for this Device.
+ for (GroupBucket bucket : group.buckets().buckets()) {
+
+ // add the group to the traversed groups
+ currentHitChain.addDataPlaneEntity(new DataPlaneEntity(group));
+
+ // Go to the next step - using a copy of the egress packet and of the hit chain
+ newHitChain = PipelineTraceableHitChain.emptyHitChain();
+ currentHitChain.getHitChain().forEach(newHitChain::addDataPlaneEntity);
+ newEgressPacket = DefaultTrafficSelector.builder(egressPacket.build());
+ getGroupsFromInstructions(groups, bucket.treatment().allInstructions(), newEgressPacket,
+ outputPorts, newHitChain, outputBuilder, input,
+ dropped | isDropped(group.id(), bucket, input.ingressPort()));
+ }
+ }
+ }
+
+ private boolean isDropped(GroupId groupId, GroupBucket bucket, ConnectPoint ingressPort) {
+ log.debug("Verify if the packet has to be dropped by the input port {}",
+ ingressPort);
+ // if It is not a l2 flood group and l2/l3 mcast skip
+ int maskedId = groupId.id() & 0xF0000000;
+ if (maskedId != L2_FLOOD_TYPE && maskedId != L2_MULTICAST_TYPE &&
+ maskedId != L3_MULTICAST_TYPE) {
+ return false;
+ }
+ // Verify if the bucket points to the ingress port
+ Instructions.GroupInstruction groupInstruction;
+ for (Instruction instr : bucket.treatment().allInstructions()) {
+ if (instr.type().equals(Instruction.Type.GROUP)) {
+ groupInstruction = (Instructions.GroupInstruction) instr;
+ // FIXME According to the OFDPA spec for L3 MCAST if the VLAN is changed packet is not dropped
+ if ((groupInstruction.groupId().id() & 0xF0000000) == L2_INTERFACE_TYPE) {
+ return (groupInstruction.groupId().id() & 0x0000FFFF) == ingressPort.port().toLong();
+ }
+ }
+ }
+ return false;
+ }
+
+ // Handles deferred instructions taken from the flows
+ private void handleDeferredActions(TrafficSelector egressPacket, Map<GroupId, Group> groups,
+ List<Instruction> deferredInstructions, List<PortNumber> outputPorts,
+ PipelineTraceableHitChain currentHitChain,
+ PipelineTraceableOutput.Builder outputBuilder,
+ PipelineTraceableInput input) {
+ // Update the packet with the deferred instructions
+ TrafficSelector.Builder newEgressPacket = updatePacket(egressPacket, deferredInstructions);
+
+ //Gather any output instructions from the deferred instruction
+ List<Instruction> outputFlowInstruction = deferredInstructions.stream().filter(instruction ->
+ instruction.type().equals(Instruction.Type.OUTPUT))
+ .collect(Collectors.toList());
+
+ //We are considering deferred instructions from flows, there can only be one output.
+ if (outputFlowInstruction.size() > 1) {
+ outputBuilder.appendToLog("More than one flow rule with OUTPUT instruction");
+ log.warn("There cannot be more than one flow entry with OUTPUT instruction for {}", egressPacket);
+ }
+
+ // If there is one output let's go through that. No need to make a copy
+ // of the egress packet and of the current hit chain.
+ if (outputFlowInstruction.size() == 1) {
+ buildOutputFromDevice(newEgressPacket, outputPorts, (OutputInstruction) outputFlowInstruction.get(0),
+ currentHitChain, outputBuilder, input.ingressPacket(), false);
+ }
+
+ // If there is no output let's see if there any deferred instruction point to groups.
+ // No need to make a copy of the egress packet and of the current chain.
+ if (outputFlowInstruction.size() == 0) {
+ getGroupsFromInstructions(groups, deferredInstructions, newEgressPacket, outputPorts,
+ currentHitChain, outputBuilder, input, false);
+ }
+ }
+
+ // Checks whether it is an hw device based on different means.
+ // throws an exception if the behavior has been used with wrong drivers
+ private boolean isHardwareSwitch() {
+ // Check if we are using ofdpa hw device by looking at the pipeliner
+ // if we need to support a device that does not have a pipeliner
+ // we can add an exclusion rules before this
+ if (!this.handler().hasBehaviour(Pipeliner.class)) {
+ throw new UnsupportedOperationException("Not supported device");
+ }
+ Pipeliner pipeliner = this.handler().behaviour(Pipeliner.class);
+ if (pipeliner instanceof OvsOfdpaPipeline) {
+ return false;
+ } else if (pipeliner instanceof Ofdpa2Pipeline) {
+ return true;
+ }
+ throw new UnsupportedOperationException("Not supported device");
+ }
+
+ // OF-DPA hardware requires one VLAN filtering rule and one VLAN assignment flow in the VLAN table.
+ // This method is used to determine whether there is a need to match a second VLAN flow after
+ // matching the given flowEntry.
+ private boolean shouldMatchSecondVlanFlow(FlowEntry flowEntry) {
+ // if we need to support a device that does not have a pipeliner
+ // we can add an exclusion rules before this
+ if (!this.handler().hasBehaviour(Pipeliner.class)) {
+ throw new UnsupportedOperationException("Not supported device");
+ }
+ Pipeliner pipeliner = this.handler().behaviour(Pipeliner.class);
+ if (!(pipeliner instanceof Ofdpa2Pipeline)) {
+ return false;
+ }
+ return ((Ofdpa2Pipeline) pipeliner).requireSecondVlanTableEntry() &&
+ flowEntry.table().equals(IndexTableId.of(VLAN_TABLE)) &&
+ flowEntry.selector().getCriterion(Criterion.Type.VLAN_VID) != null &&
+ ((VlanIdCriterion) flowEntry.selector().getCriterion(Criterion.Type.VLAN_VID))
+ .vlanId().equals(VlanId.NONE);
+ }
+
+}