[SDFAB-189] UpfProgrammable implementation for fabric v1model

Change-Id: I4ea7980830d761a0da8a78943c08229c2da9410d
diff --git a/pipelines/fabric/impl/src/main/java/org/onosproject/pipelines/fabric/impl/behaviour/upf/FabricUpfProgrammable.java b/pipelines/fabric/impl/src/main/java/org/onosproject/pipelines/fabric/impl/behaviour/upf/FabricUpfProgrammable.java
new file mode 100644
index 0000000..77ad732
--- /dev/null
+++ b/pipelines/fabric/impl/src/main/java/org/onosproject/pipelines/fabric/impl/behaviour/upf/FabricUpfProgrammable.java
@@ -0,0 +1,668 @@
+/*
+ * Copyright 2021-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.pipelines.fabric.impl.behaviour.upf;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip4Prefix;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.drivers.p4runtime.AbstractP4RuntimeHandlerBehaviour;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.behaviour.upf.ForwardingActionRule;
+import org.onosproject.net.behaviour.upf.GtpTunnel;
+import org.onosproject.net.behaviour.upf.PacketDetectionRule;
+import org.onosproject.net.behaviour.upf.PdrStats;
+import org.onosproject.net.behaviour.upf.UpfInterface;
+import org.onosproject.net.behaviour.upf.UpfProgrammable;
+import org.onosproject.net.behaviour.upf.UpfProgrammableException;
+import org.onosproject.net.flow.DefaultFlowRule;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.criteria.PiCriterion;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketService;
+import org.onosproject.net.pi.model.PiCounterId;
+import org.onosproject.net.pi.model.PiCounterModel;
+import org.onosproject.net.pi.model.PiTableId;
+import org.onosproject.net.pi.model.PiTableModel;
+import org.onosproject.net.pi.runtime.PiCounterCell;
+import org.onosproject.net.pi.runtime.PiCounterCellHandle;
+import org.onosproject.net.pi.runtime.PiCounterCellId;
+import org.onosproject.pipelines.fabric.impl.FabricPipeconfLoader;
+import org.onosproject.pipelines.fabric.impl.behaviour.FabricCapabilities;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.onosproject.net.behaviour.upf.UpfProgrammableException.Type.UNSUPPORTED_OPERATION;
+import static org.onosproject.net.pi.model.PiCounterType.INDIRECT;
+import static org.onosproject.pipelines.fabric.FabricConstants.FABRIC_EGRESS_SPGW_PDR_COUNTER;
+import static org.onosproject.pipelines.fabric.FabricConstants.FABRIC_INGRESS_SPGW_DOWNLINK_PDRS;
+import static org.onosproject.pipelines.fabric.FabricConstants.FABRIC_INGRESS_SPGW_FARS;
+import static org.onosproject.pipelines.fabric.FabricConstants.FABRIC_INGRESS_SPGW_INTERFACES;
+import static org.onosproject.pipelines.fabric.FabricConstants.FABRIC_INGRESS_SPGW_PDR_COUNTER;
+import static org.onosproject.pipelines.fabric.FabricConstants.FABRIC_INGRESS_SPGW_UPLINK_PDRS;
+import static org.onosproject.pipelines.fabric.FabricConstants.HDR_FAR_ID;
+import static org.onosproject.pipelines.fabric.FabricConstants.HDR_GTPU_IS_VALID;
+import static org.onosproject.pipelines.fabric.FabricConstants.HDR_IPV4_DST_ADDR;
+import static org.onosproject.pipelines.fabric.FabricConstants.HDR_TEID;
+import static org.onosproject.pipelines.fabric.FabricConstants.HDR_TUNNEL_IPV4_DST;
+import static org.onosproject.pipelines.fabric.FabricConstants.HDR_UE_ADDR;
+
+
+/**
+ * Implementation of a UPF programmable device behavior.
+ */
+public class FabricUpfProgrammable extends AbstractP4RuntimeHandlerBehaviour
+        implements UpfProgrammable {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private static final int DEFAULT_PRIORITY = 128;
+    private static final long DEFAULT_P4_DEVICE_ID = 1;
+
+    protected FlowRuleService flowRuleService;
+    protected PacketService packetService;
+    protected FabricUpfStore fabricUpfStore;
+    protected FabricUpfTranslator upfTranslator;
+
+    private long farTableSize;
+    private long encappedPdrTableSize;
+    private long unencappedPdrTableSize;
+    private long pdrCounterSize;
+
+    private ApplicationId appId;
+
+    // FIXME: remove, buffer drain should be triggered by Up4Service
+    private BufferDrainer bufferDrainer;
+
+    // FIXME: dbuf tunnel should be managed by Up4Service
+    //  Up4Service should be responsible of setting up such tunnel, then transforming FARs for this
+    //  device accordingly. When the tunnel endpoint change, it should be up to Up4Service to update
+    //  the FAR on the device.
+    private GtpTunnel dbufTunnel;
+
+    @Override
+    protected boolean setupBehaviour(String opName) {
+        if (!super.setupBehaviour(opName)) {
+            return false;
+        }
+        flowRuleService = handler().get(FlowRuleService.class);
+        packetService = handler().get(PacketService.class);
+        fabricUpfStore = handler().get(FabricUpfStore.class);
+        upfTranslator = new FabricUpfTranslator(fabricUpfStore);
+        final CoreService coreService = handler().get(CoreService.class);
+        appId = coreService.getAppId(FabricPipeconfLoader.PIPELINE_APP_NAME);
+        if (appId == null) {
+            log.warn("Application ID is null. Cannot initialize behaviour.");
+            return false;
+        }
+
+        var capabilities = new FabricCapabilities(pipeconf);
+        if (!capabilities.supportUpf()) {
+            log.warn("Pipeconf {} on {} does not support UPF capabilities, " +
+                             "cannot perform {}",
+                     pipeconf.id(), deviceId, opName);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean init() {
+        if (setupBehaviour("init()")) {
+            if (!computeHardwareResourceSizes()) {
+                // error message will be printed by computeHardwareResourceSizes()
+                return false;
+            }
+            log.info("UpfProgrammable initialized for appId {} and deviceId {}", appId, deviceId);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Grab the capacities for the PDR and FAR tables from the pipeconf. Runs only once, on initialization.
+     *
+     * @return true if resource is fetched successfully, false otherwise.
+     * @throws IllegalStateException when FAR or PDR table can't be found in the pipeline model.
+     */
+    private boolean computeHardwareResourceSizes() {
+        long farTableSize = 0;
+        long encappedPdrTableSize = 0;
+        long unencappedPdrTableSize = 0;
+
+        // Get table sizes of interest
+        for (PiTableModel piTable : pipeconf.pipelineModel().tables()) {
+            if (piTable.id().equals(FABRIC_INGRESS_SPGW_UPLINK_PDRS)) {
+                encappedPdrTableSize = piTable.maxSize();
+            } else if (piTable.id().equals(FABRIC_INGRESS_SPGW_DOWNLINK_PDRS)) {
+                unencappedPdrTableSize = piTable.maxSize();
+            } else if (piTable.id().equals(FABRIC_INGRESS_SPGW_FARS)) {
+                farTableSize = piTable.maxSize();
+            }
+        }
+        if (encappedPdrTableSize == 0) {
+            throw new IllegalStateException("Unable to find uplink PDR table in pipeline model.");
+        }
+        if (unencappedPdrTableSize == 0) {
+            throw new IllegalStateException("Unable to find downlink PDR table in pipeline model.");
+        }
+        if (encappedPdrTableSize != unencappedPdrTableSize) {
+            log.warn("The uplink and downlink PDR tables don't have equal sizes! Using the minimum of the two.");
+        }
+        if (farTableSize == 0) {
+            throw new IllegalStateException("Unable to find FAR table in pipeline model.");
+        }
+        // Get counter sizes of interest
+        long ingressCounterSize = 0;
+        long egressCounterSize = 0;
+        for (PiCounterModel piCounter : pipeconf.pipelineModel().counters()) {
+            if (piCounter.id().equals(FABRIC_INGRESS_SPGW_PDR_COUNTER)) {
+                ingressCounterSize = piCounter.size();
+            } else if (piCounter.id().equals(FABRIC_EGRESS_SPGW_PDR_COUNTER)) {
+                egressCounterSize = piCounter.size();
+            }
+        }
+        if (ingressCounterSize != egressCounterSize) {
+            log.warn("PDR ingress and egress counter sizes are not equal! Using the minimum of the two.");
+        }
+        this.farTableSize = farTableSize;
+        this.encappedPdrTableSize = encappedPdrTableSize;
+        this.unencappedPdrTableSize = unencappedPdrTableSize;
+        this.pdrCounterSize = Math.min(ingressCounterSize, egressCounterSize);
+        return true;
+    }
+
+    @Override
+    public void setBufferDrainer(BufferDrainer drainer) {
+        if (!setupBehaviour("setBufferDrainer()")) {
+            return;
+        }
+        this.bufferDrainer = drainer;
+    }
+
+    @Override
+    public void unsetBufferDrainer() {
+        if (!setupBehaviour("unsetBufferDrainer()")) {
+            return;
+        }
+        this.bufferDrainer = null;
+    }
+
+    @Override
+    public void enablePscEncap(int defaultQfi) throws UpfProgrammableException {
+        throw new UpfProgrammableException("PSC encap is not supported in fabric-v1model",
+                                           UNSUPPORTED_OPERATION);
+    }
+
+    @Override
+    public void disablePscEncap() throws UpfProgrammableException {
+        throw new UpfProgrammableException("PSC encap is not supported in fabric-v1model",
+                                           UNSUPPORTED_OPERATION);
+    }
+
+    @Override
+    public void sendPacketOut(ByteBuffer data) {
+        if (!setupBehaviour("sendPacketOut()")) {
+            return;
+        }
+        final OutboundPacket pkt = new DefaultOutboundPacket(
+                deviceId,
+                // Use TABLE logical port to have pkt routed via pipeline tables.
+                DefaultTrafficTreatment.builder()
+                        .setOutput(PortNumber.TABLE)
+                        .build(),
+                data);
+        packetService.emit(pkt);
+    }
+
+    @Override
+    public void setDbufTunnel(Ip4Address switchAddr, Ip4Address dbufAddr) {
+        if (!setupBehaviour("setDbufTunnel()")) {
+            return;
+        }
+        this.dbufTunnel = GtpTunnel.builder()
+                .setSrc(switchAddr)
+                .setDst(dbufAddr)
+                .setSrcPort((short) 2152)
+                .setTeid(0)
+                .build();
+    }
+
+    @Override
+    public void unsetDbufTunnel() {
+        if (!setupBehaviour("unsetDbufTunnel()")) {
+            return;
+        }
+        this.dbufTunnel = null;
+    }
+
+    /**
+     * Convert the given buffering FAR to a FAR that tunnels the packet to dbuf.
+     *
+     * @param far the FAR to convert
+     * @return the converted FAR
+     */
+    private ForwardingActionRule convertToDbufFar(ForwardingActionRule far) {
+        if (!far.buffers()) {
+            throw new IllegalArgumentException("Converting a non-buffering FAR to a dbuf FAR! This shouldn't happen.");
+        }
+        return ForwardingActionRule.builder()
+                .setFarId(far.farId())
+                .withSessionId(far.sessionId())
+                .setNotifyFlag(far.notifies())
+                .setBufferFlag(true)
+                .setTunnel(dbufTunnel)
+                .build();
+    }
+
+    @Override
+    public void cleanUp() {
+        if (!setupBehaviour("cleanUp()")) {
+            return;
+        }
+        log.info("Clearing all UPF-related table entries.");
+        flowRuleService.removeFlowRulesById(appId);
+        fabricUpfStore.reset();
+    }
+
+    @Override
+    public void clearInterfaces() {
+        if (!setupBehaviour("clearInterfaces()")) {
+            return;
+        }
+        log.info("Clearing all UPF interfaces.");
+        for (FlowRule entry : flowRuleService.getFlowEntriesById(appId)) {
+            if (upfTranslator.isFabricInterface(entry)) {
+                flowRuleService.removeFlowRules(entry);
+            }
+        }
+    }
+
+    @Override
+    public void clearFlows() {
+        if (!setupBehaviour("clearFlows()")) {
+            return;
+        }
+        log.info("Clearing all UE sessions.");
+        int pdrsCleared = 0;
+        int farsCleared = 0;
+        for (FlowRule entry : flowRuleService.getFlowEntriesById(appId)) {
+            if (upfTranslator.isFabricPdr(entry)) {
+                pdrsCleared++;
+                flowRuleService.removeFlowRules(entry);
+            } else if (upfTranslator.isFabricFar(entry)) {
+                farsCleared++;
+                flowRuleService.removeFlowRules(entry);
+            }
+        }
+        log.info("Cleared {} PDRs and {} FARS.", pdrsCleared, farsCleared);
+    }
+
+
+    @Override
+    public Collection<PdrStats> readAllCounters(long maxCounterId) {
+        if (!setupBehaviour("readAllCounters()")) {
+            return null;
+        }
+
+        long counterSize = pdrCounterSize();
+        if (maxCounterId != -1) {
+            counterSize = Math.min(maxCounterId, counterSize);
+        }
+
+        // Prepare PdrStats object builders, one for each counter ID currently in use
+        Map<Integer, PdrStats.Builder> pdrStatBuilders = Maps.newHashMap();
+        for (int cellId = 0; cellId < counterSize; cellId++) {
+            pdrStatBuilders.put(cellId, PdrStats.builder().withCellId(cellId));
+        }
+
+        // Generate the counter cell IDs.
+        Set<PiCounterId> counterIds = ImmutableSet.of(
+                FABRIC_INGRESS_SPGW_PDR_COUNTER,
+                FABRIC_EGRESS_SPGW_PDR_COUNTER
+        );
+
+        // Query the device.
+        Collection<PiCounterCell> counterEntryResponse = client.read(
+                DEFAULT_P4_DEVICE_ID, pipeconf)
+                .counterCells(counterIds)
+                .submitSync()
+                .all(PiCounterCell.class);
+
+        // Process response.
+        counterEntryResponse.forEach(counterCell -> {
+            if (counterCell.cellId().counterType() != INDIRECT) {
+                log.warn("Invalid counter data type {}, skipping", counterCell.cellId().counterType());
+                return;
+            }
+            if (!pdrStatBuilders.containsKey((int) counterCell.cellId().index())) {
+                // Most likely Up4config.maxUes() is set to a value smaller than what the switch
+                // pipeline can hold.
+                log.debug("Unrecognized index {} when reading all counters, " +
+                                  "that's expected if we are manually limiting maxUes", counterCell);
+                return;
+            }
+            PdrStats.Builder statsBuilder = pdrStatBuilders.get((int) counterCell.cellId().index());
+            if (counterCell.cellId().counterId().equals(FABRIC_INGRESS_SPGW_PDR_COUNTER)) {
+                statsBuilder.setIngress(counterCell.data().packets(),
+                                        counterCell.data().bytes());
+            } else if (counterCell.cellId().counterId().equals(FABRIC_EGRESS_SPGW_PDR_COUNTER)) {
+                statsBuilder.setEgress(counterCell.data().packets(),
+                                       counterCell.data().bytes());
+            } else {
+                log.warn("Unrecognized counter ID {}, skipping", counterCell);
+            }
+        });
+
+        return pdrStatBuilders
+                .values()
+                .stream()
+                .map(PdrStats.Builder::build)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public long pdrCounterSize() {
+        if (!setupBehaviour("pdrCounterSize()")) {
+            return -1;
+        }
+        computeHardwareResourceSizes();
+        return pdrCounterSize;
+    }
+
+    @Override
+    public long farTableSize() {
+        if (!setupBehaviour("farTableSize()")) {
+            return -1;
+        }
+        computeHardwareResourceSizes();
+        return farTableSize;
+    }
+
+    @Override
+    public long pdrTableSize() {
+        if (!setupBehaviour("pdrTableSize()")) {
+            return -1;
+        }
+        computeHardwareResourceSizes();
+        return Math.min(encappedPdrTableSize, unencappedPdrTableSize) * 2;
+    }
+
+    @Override
+    public PdrStats readCounter(int cellId) throws UpfProgrammableException {
+        if (!setupBehaviour("readCounter()")) {
+            return null;
+        }
+        if (cellId >= pdrCounterSize() || cellId < 0) {
+            throw new UpfProgrammableException("Requested PDR counter cell index is out of bounds.",
+                                               UpfProgrammableException.Type.COUNTER_INDEX_OUT_OF_RANGE);
+        }
+        PdrStats.Builder stats = PdrStats.builder().withCellId(cellId);
+
+        // Make list of cell handles we want to read.
+        List<PiCounterCellHandle> counterCellHandles = List.of(
+                PiCounterCellHandle.of(deviceId,
+                                       PiCounterCellId.ofIndirect(FABRIC_INGRESS_SPGW_PDR_COUNTER, cellId)),
+                PiCounterCellHandle.of(deviceId,
+                                       PiCounterCellId.ofIndirect(FABRIC_EGRESS_SPGW_PDR_COUNTER, cellId)));
+
+        // Query the device.
+        Collection<PiCounterCell> counterEntryResponse = client.read(
+                DEFAULT_P4_DEVICE_ID, pipeconf)
+                .handles(counterCellHandles).submitSync()
+                .all(PiCounterCell.class);
+
+        // Process response.
+        counterEntryResponse.forEach(counterCell -> {
+            if (counterCell.cellId().counterType() != INDIRECT) {
+                log.warn("Invalid counter data type {}, skipping", counterCell.cellId().counterType());
+                return;
+            }
+            if (cellId != counterCell.cellId().index()) {
+                log.warn("Unrecognized counter index {}, skipping", counterCell);
+                return;
+            }
+            if (counterCell.cellId().counterId().equals(FABRIC_INGRESS_SPGW_PDR_COUNTER)) {
+                stats.setIngress(counterCell.data().packets(), counterCell.data().bytes());
+            } else if (counterCell.cellId().counterId().equals(FABRIC_EGRESS_SPGW_PDR_COUNTER)) {
+                stats.setEgress(counterCell.data().packets(), counterCell.data().bytes());
+            } else {
+                log.warn("Unrecognized counter ID {}, skipping", counterCell);
+            }
+        });
+        return stats.build();
+    }
+
+
+    @Override
+    public void addPdr(PacketDetectionRule pdr) throws UpfProgrammableException {
+        if (!setupBehaviour("addPdr()")) {
+            return;
+        }
+        if (pdr.counterId() >= pdrCounterSize() || pdr.counterId() < 0) {
+            throw new UpfProgrammableException("Counter cell index referenced by PDR is out of bounds.",
+                                               UpfProgrammableException.Type.COUNTER_INDEX_OUT_OF_RANGE);
+        }
+        FlowRule fabricPdr = upfTranslator.pdrToFabricEntry(pdr, deviceId, appId, DEFAULT_PRIORITY);
+        log.info("Installing {}", pdr.toString());
+        flowRuleService.applyFlowRules(fabricPdr);
+        log.debug("PDR added with flowID {}", fabricPdr.id().value());
+
+        // If the flow rule was applied and the PDR is downlink, add the PDR to the farID->PDR mapping
+        if (pdr.matchesUnencapped()) {
+            fabricUpfStore.learnFarIdToUeAddrs(pdr);
+        }
+    }
+
+
+    @Override
+    public void addFar(ForwardingActionRule far) throws UpfProgrammableException {
+        if (!setupBehaviour("addFar()")) {
+            return;
+        }
+        UpfRuleIdentifier ruleId = UpfRuleIdentifier.of(far.sessionId(), far.farId());
+        if (far.buffers()) {
+            // If the far has the buffer flag, modify its tunnel so it directs to dbuf
+            far = convertToDbufFar(far);
+            fabricUpfStore.learBufferingFarId(ruleId);
+        }
+        FlowRule fabricFar = upfTranslator.farToFabricEntry(far, deviceId, appId, DEFAULT_PRIORITY);
+        log.info("Installing {}", far.toString());
+        flowRuleService.applyFlowRules(fabricFar);
+        log.debug("FAR added with flowID {}", fabricFar.id().value());
+        if (!far.buffers() && fabricUpfStore.isFarIdBuffering(ruleId)) {
+            // If this FAR does not buffer but used to, then drain the buffer for every UE address
+            // that hits this FAR.
+            fabricUpfStore.forgetBufferingFarId(ruleId);
+            for (var ueAddr : fabricUpfStore.ueAddrsOfFarId(ruleId)) {
+                if (bufferDrainer == null) {
+                    log.warn("Unable to drain downlink buffer for UE {}, bufferDrainer is null", ueAddr);
+                } else {
+                    bufferDrainer.drain(ueAddr);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void addInterface(UpfInterface upfInterface) throws UpfProgrammableException {
+        if (!setupBehaviour("addInterface()")) {
+            return;
+        }
+        FlowRule flowRule = upfTranslator.interfaceToFabricEntry(upfInterface, deviceId, appId, DEFAULT_PRIORITY);
+        log.info("Installing {}", upfInterface);
+        flowRuleService.applyFlowRules(flowRule);
+        log.debug("Interface added with flowID {}", flowRule.id().value());
+        // By default we enable UE-to-UE communication on the UE subnet identified by the CORE interface.
+        // TODO: allow enabling/disabling UE-to-UE via netcfg or other API.
+        log.warn("UE-to-UE traffic is not supported in fabric-v1model");
+    }
+
+    private boolean removeEntry(PiCriterion match, PiTableId tableId, boolean failSilent)
+            throws UpfProgrammableException {
+        if (!setupBehaviour("removeEntry()")) {
+            return false;
+        }
+        FlowRule entry = DefaultFlowRule.builder()
+                .forDevice(deviceId).fromApp(appId).makePermanent()
+                .forTable(tableId)
+                .withSelector(DefaultTrafficSelector.builder().matchPi(match).build())
+                .withPriority(DEFAULT_PRIORITY)
+                .build();
+
+        /*
+         *  FIXME: Stupid stupid slow hack, needed because removeFlowRules expects FlowRule objects
+         *   with correct and complete actions and parameters, but P4Runtime deletion requests
+         *   will not have those.
+         */
+        for (FlowEntry installedEntry : flowRuleService.getFlowEntriesById(appId)) {
+            if (installedEntry.selector().equals(entry.selector())) {
+                log.info("Found matching entry to remove, it has FlowID {}", installedEntry.id());
+                flowRuleService.removeFlowRules(installedEntry);
+                return true;
+            }
+        }
+        if (!failSilent) {
+            throw new UpfProgrammableException("Match criterion " + match.toString() +
+                                                       " not found in table " + tableId.toString());
+        }
+        return false;
+    }
+
+    @Override
+    public Collection<PacketDetectionRule> getPdrs() throws UpfProgrammableException {
+        if (!setupBehaviour("getPdrs()")) {
+            return null;
+        }
+        ArrayList<PacketDetectionRule> pdrs = new ArrayList<>();
+        for (FlowRule flowRule : flowRuleService.getFlowEntriesById(appId)) {
+            if (upfTranslator.isFabricPdr(flowRule)) {
+                pdrs.add(upfTranslator.fabricEntryToPdr(flowRule));
+            }
+        }
+        return pdrs;
+    }
+
+    @Override
+    public Collection<ForwardingActionRule> getFars() throws UpfProgrammableException {
+        if (!setupBehaviour("getFars()")) {
+            return null;
+        }
+        ArrayList<ForwardingActionRule> fars = new ArrayList<>();
+        for (FlowRule flowRule : flowRuleService.getFlowEntriesById(appId)) {
+            if (upfTranslator.isFabricFar(flowRule)) {
+                fars.add(upfTranslator.fabricEntryToFar(flowRule));
+            }
+        }
+        return fars;
+    }
+
+    @Override
+    public Collection<UpfInterface> getInterfaces() throws UpfProgrammableException {
+        if (!setupBehaviour("getInterfaces()")) {
+            return null;
+        }
+        ArrayList<UpfInterface> ifaces = new ArrayList<>();
+        for (FlowRule flowRule : flowRuleService.getFlowEntriesById(appId)) {
+            if (upfTranslator.isFabricInterface(flowRule)) {
+                ifaces.add(upfTranslator.fabricEntryToInterface(flowRule));
+            }
+        }
+        return ifaces;
+    }
+
+    @Override
+    public void removePdr(PacketDetectionRule pdr) throws UpfProgrammableException {
+        if (!setupBehaviour("removePdr()")) {
+            return;
+        }
+        final PiCriterion match;
+        final PiTableId tableId;
+        if (pdr.matchesEncapped()) {
+            match = PiCriterion.builder()
+                    .matchExact(HDR_TEID, pdr.teid().asArray())
+                    .matchExact(HDR_TUNNEL_IPV4_DST, pdr.tunnelDest().toInt())
+                    .build();
+            tableId = FABRIC_INGRESS_SPGW_UPLINK_PDRS;
+        } else {
+            match = PiCriterion.builder()
+                    .matchExact(HDR_UE_ADDR, pdr.ueAddress().toInt())
+                    .build();
+            tableId = FABRIC_INGRESS_SPGW_DOWNLINK_PDRS;
+        }
+        log.info("Removing {}", pdr.toString());
+        removeEntry(match, tableId, false);
+
+        // Remove the PDR from the farID->PDR mapping
+        // This is an inefficient hotfix FIXME: remove UE addrs from the mapping in sublinear time
+        if (pdr.matchesUnencapped()) {
+            // Should we remove just from the map entry with key == far ID?
+            fabricUpfStore.forgetUeAddr(pdr.ueAddress());
+        }
+    }
+
+    @Override
+    public void removeFar(ForwardingActionRule far) throws UpfProgrammableException {
+        log.info("Removing {}", far.toString());
+
+        PiCriterion match = PiCriterion.builder()
+                .matchExact(HDR_FAR_ID, fabricUpfStore.globalFarIdOf(far.sessionId(), far.farId()))
+                .build();
+
+        removeEntry(match, FABRIC_INGRESS_SPGW_FARS, false);
+    }
+
+    @Override
+    public void removeInterface(UpfInterface upfInterface) throws UpfProgrammableException {
+        if (!setupBehaviour("removeInterface()")) {
+            return;
+        }
+        Ip4Prefix ifacePrefix = upfInterface.getPrefix();
+        // If it isn't a core interface (so it is either access or unknown), try removing core
+        if (!upfInterface.isCore()) {
+            PiCriterion match1 = PiCriterion.builder()
+                    .matchLpm(HDR_IPV4_DST_ADDR, ifacePrefix.address().toInt(),
+                              ifacePrefix.prefixLength())
+                    .matchExact(HDR_GTPU_IS_VALID, 1)
+                    .build();
+            if (removeEntry(match1, FABRIC_INGRESS_SPGW_INTERFACES, true)) {
+                return;
+            }
+        }
+        // If that didn't work or didn't execute, try removing access
+        PiCriterion match2 = PiCriterion.builder()
+                .matchLpm(HDR_IPV4_DST_ADDR, ifacePrefix.address().toInt(),
+                          ifacePrefix.prefixLength())
+                .matchExact(HDR_GTPU_IS_VALID, 0)
+                .build();
+        removeEntry(match2, FABRIC_INGRESS_SPGW_INTERFACES, false);
+    }
+}