Translator-based Bmv2 flow rule driver

Removed old parsing logic. Now it uses Bmv2FlowRuleTranslator to
translate ONOS flow rule into Bmv2 model-dependent table entries.

Change-Id: I1febc23b334acade027e806c8a8c266acc061277
diff --git a/drivers/bmv2/src/main/java/org/onosproject/drivers/bmv2/Bmv2FlowRuleDriver.java b/drivers/bmv2/src/main/java/org/onosproject/drivers/bmv2/Bmv2FlowRuleDriver.java
index c226e60..3c6acf5 100644
--- a/drivers/bmv2/src/main/java/org/onosproject/drivers/bmv2/Bmv2FlowRuleDriver.java
+++ b/drivers/bmv2/src/main/java/org/onosproject/drivers/bmv2/Bmv2FlowRuleDriver.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014-2016 Open Networking Laboratory
+ * Copyright 2016-present 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.
@@ -16,221 +16,146 @@
 
 package org.onosproject.drivers.bmv2;
 
-import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import org.onosproject.bmv2.api.runtime.Bmv2ExtensionSelector;
-import org.onosproject.bmv2.api.runtime.Bmv2ExtensionTreatment;
-import org.onosproject.bmv2.api.runtime.Bmv2TableEntry;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.commons.lang3.tuple.Triple;
+import org.onosproject.bmv2.api.runtime.Bmv2MatchKey;
 import org.onosproject.bmv2.api.runtime.Bmv2RuntimeException;
+import org.onosproject.bmv2.api.runtime.Bmv2TableEntry;
 import org.onosproject.bmv2.ctl.Bmv2ThriftClient;
+import org.onosproject.drivers.bmv2.translators.Bmv2DefaultFlowRuleTranslator;
+import org.onosproject.drivers.bmv2.translators.Bmv2FlowRuleTranslator;
+import org.onosproject.drivers.bmv2.translators.Bmv2FlowRuleTranslatorException;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.driver.AbstractHandlerBehaviour;
 import org.onosproject.net.flow.DefaultFlowEntry;
 import org.onosproject.net.flow.FlowEntry;
 import org.onosproject.net.flow.FlowRule;
 import org.onosproject.net.flow.FlowRuleProgrammable;
-import org.onosproject.net.flow.criteria.Criterion;
-import org.onosproject.net.flow.criteria.ExtensionCriterion;
-import org.onosproject.net.flow.criteria.ExtensionSelector;
-import org.onosproject.net.flow.criteria.ExtensionSelectorType;
-import org.onosproject.net.flow.instructions.ExtensionTreatment;
-import org.onosproject.net.flow.instructions.ExtensionTreatmentType;
-import org.onosproject.net.flow.instructions.Instruction;
-import org.onosproject.net.flow.instructions.Instructions;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
 
 public class Bmv2FlowRuleDriver extends AbstractHandlerBehaviour
         implements FlowRuleProgrammable {
 
-    private final Logger log =
-            LoggerFactory.getLogger(this.getClass());
-
-    // Bmv2 doesn't support proper table dump, use a local store
-    // FIXME: synchronize entries with device
-    private final Map<FlowRule, FlowEntry> deviceEntriesMap = Maps.newHashMap();
-    private final Map<Integer, Set<FlowRule>> tableRulesMap = Maps.newHashMap();
-    private final Map<FlowRule, Long> tableEntryIdsMap = Maps.newHashMap();
+    private static final Logger LOG =
+            LoggerFactory.getLogger(Bmv2FlowRuleDriver.class);
+    // There's no Bmv2 client method to poll flow entries from the device device. gitNeed a local store.
+    private static final ConcurrentMap<Triple<DeviceId, String, Bmv2MatchKey>, Pair<Long, FlowEntry>>
+            ENTRIES_MAP = Maps.newConcurrentMap();
+    private static final Bmv2FlowRuleTranslator TRANSLATOR = new Bmv2DefaultFlowRuleTranslator();
 
     @Override
     public Collection<FlowEntry> getFlowEntries() {
-        return Collections.unmodifiableCollection(
-                deviceEntriesMap.values());
+
+        DeviceId deviceId = handler().data().deviceId();
+
+        List<FlowEntry> entryList = Lists.newArrayList();
+
+        // FIXME: improve this, e.g. might store a separate Map<DeviceId, Collection<FlowEntry>>
+        ENTRIES_MAP.forEach((key, value) -> {
+            if (key.getLeft() == deviceId && value != null) {
+                entryList.add(value.getRight());
+            }
+        });
+
+        return Collections.unmodifiableCollection(entryList);
     }
 
     @Override
     public Collection<FlowRule> applyFlowRules(Collection<FlowRule> rules) {
-        Bmv2ThriftClient deviceClient;
-        try {
-            deviceClient = getDeviceClient();
-        } catch (Bmv2RuntimeException e) {
-            return Collections.emptyList();
-        }
 
-        List<FlowRule> appliedFlowRules = Lists.newArrayList();
-
-        for (FlowRule rule : rules) {
-
-            Bmv2TableEntry entry;
-
-            try {
-                entry = parseFlowRule(rule);
-            } catch (IllegalStateException e) {
-                log.error("Unable to parse flow rule", e);
-                continue;
-            }
-
-            // Instantiate flowrule set for table if it does not exist
-            if (!tableRulesMap.containsKey(rule.tableId())) {
-                tableRulesMap.put(rule.tableId(), Sets.newHashSet());
-            }
-
-            if (tableRulesMap.get(rule.tableId()).contains(rule)) {
-                /* Rule is already installed in the table */
-                long entryId = tableEntryIdsMap.get(rule);
-
-                try {
-                    deviceClient.modifyTableEntry(
-                            entry.tableName(), entryId, entry.action());
-
-                    // Replace stored rule as treatment, etc. might have changed
-                    // Java Set doesn't replace on add, remove first
-                    tableRulesMap.get(rule.tableId()).remove(rule);
-                    tableRulesMap.get(rule.tableId()).add(rule);
-                    tableEntryIdsMap.put(rule, entryId);
-                    deviceEntriesMap.put(rule, new DefaultFlowEntry(
-                            rule, FlowEntry.FlowEntryState.ADDED, 0, 0, 0));
-                } catch (Bmv2RuntimeException e) {
-                    log.error("Unable to update flow rule", e);
-                    continue;
-                }
-
-            } else {
-                /* Rule is new */
-                try {
-                    long entryId = deviceClient.addTableEntry(entry);
-
-                    tableRulesMap.get(rule.tableId()).add(rule);
-                    tableEntryIdsMap.put(rule, entryId);
-                    deviceEntriesMap.put(rule, new DefaultFlowEntry(
-                            rule, FlowEntry.FlowEntryState.ADDED, 0, 0, 0));
-                } catch (Bmv2RuntimeException e) {
-                    log.error("Unable to add flow rule", e);
-                    continue;
-                }
-            }
-
-            appliedFlowRules.add(rule);
-        }
-
-        return Collections.unmodifiableCollection(appliedFlowRules);
+        return processFlowRules(rules, Operation.APPLY);
     }
 
     @Override
     public Collection<FlowRule> removeFlowRules(Collection<FlowRule> rules) {
+
+        return processFlowRules(rules, Operation.REMOVE);
+    }
+
+    private Collection<FlowRule> processFlowRules(Collection<FlowRule> rules, Operation operation) {
+
+        DeviceId deviceId = handler().data().deviceId();
+
         Bmv2ThriftClient deviceClient;
         try {
-            deviceClient = getDeviceClient();
+            deviceClient = Bmv2ThriftClient.of(deviceId);
         } catch (Bmv2RuntimeException e) {
+            LOG.error("Failed to connect to Bmv2 device", e);
             return Collections.emptyList();
         }
 
-        List<FlowRule> removedFlowRules = Lists.newArrayList();
+        List<FlowRule> processedFlowRules = Lists.newArrayList();
 
         for (FlowRule rule : rules) {
 
-            if (tableEntryIdsMap.containsKey(rule)) {
-                long entryId = tableEntryIdsMap.get(rule);
-                String tableName = parseTableName(rule.tableId());
+            Bmv2TableEntry bmv2Entry;
 
-                try {
-                    deviceClient.deleteTableEntry(tableName, entryId);
-                } catch (Bmv2RuntimeException e) {
-                    log.error("Unable to delete flow rule", e);
-                    continue;
-                }
-
-                /* remove from local store */
-                tableEntryIdsMap.remove(rule);
-                tableRulesMap.get(rule.tableId()).remove(rule);
-                deviceEntriesMap.remove(rule);
-
-                removedFlowRules.add(rule);
+            try {
+                bmv2Entry = TRANSLATOR.translate(rule);
+            } catch (Bmv2FlowRuleTranslatorException e) {
+                LOG.error("Unable to translate flow rule: {}", e.getMessage());
+                continue;
             }
+
+            String tableName = bmv2Entry.tableName();
+            Triple<DeviceId, String, Bmv2MatchKey> entryKey = Triple.of(deviceId, tableName, bmv2Entry.matchKey());
+
+            /*
+            From here on threads are synchronized over entryKey, i.e. serialize operations
+            over the same matchKey of a specific table and device.
+             */
+            ENTRIES_MAP.compute(entryKey, (key, value) -> {
+                try {
+                    if (operation == Operation.APPLY) {
+                        // Apply entry
+                        long entryId;
+                        if (value == null) {
+                            // New entry
+                            entryId = deviceClient.addTableEntry(bmv2Entry);
+                        } else {
+                            // Existing entry
+                            entryId = value.getKey();
+                            // FIXME: check if priority or timeout changed
+                            // In this case we should to re-add the entry (not modify)
+                            deviceClient.modifyTableEntry(tableName, entryId, bmv2Entry.action());
+                        }
+                        // TODO: evaluate flow entry life, bytes and packets
+                        FlowEntry flowEntry = new DefaultFlowEntry(
+                                rule, FlowEntry.FlowEntryState.ADDED, 0, 0, 0);
+                        value = Pair.of(entryId, flowEntry);
+                    } else {
+                        // Remove entry
+                        if (value == null) {
+                            // Entry not found in map, how come?
+                            LOG.debug("Trying to remove entry, but entry ID not found: " + entryKey);
+                        } else {
+                            deviceClient.deleteTableEntry(tableName, value.getKey());
+                            value = null;
+                        }
+                    }
+                    // If here, no exceptions... things went well :)
+                    processedFlowRules.add(rule);
+                } catch (Bmv2RuntimeException e) {
+                    LOG.error("Unable to " + operation.name().toLowerCase() + " flow rule", e);
+                } catch (Exception e) {
+                    LOG.error("Uncaught exception while processing flow rule", e);
+                }
+                return value;
+            });
         }
 
-        return Collections.unmodifiableCollection(removedFlowRules);
+        return processedFlowRules;
     }
 
-    private Bmv2TableEntry parseFlowRule(FlowRule flowRule) {
-
-        // TODO make it pipeline dependant, i.e. implement mapping
-
-        Bmv2TableEntry.Builder entryBuilder = Bmv2TableEntry.builder();
-
-        // Check selector
-        ExtensionCriterion ec =
-                (ExtensionCriterion) flowRule
-                        .selector().getCriterion(Criterion.Type.EXTENSION);
-        Preconditions.checkState(
-                flowRule.selector().criteria().size() == 1
-                        && ec != null,
-                "Selector must have only 1 criterion of type EXTENSION");
-        ExtensionSelector es = ec.extensionSelector();
-        Preconditions.checkState(
-                es.type() == ExtensionSelectorType.ExtensionSelectorTypes.P4_BMV2_MATCH_KEY.type(),
-                "ExtensionSelectorType must be P4_BMV2_MATCH_KEY");
-
-        // Selector OK, get Bmv2MatchKey
-        entryBuilder.withMatchKey(((Bmv2ExtensionSelector) es).matchKey());
-
-        // Check treatment
-        Instruction inst = flowRule.treatment().allInstructions().get(0);
-        Preconditions.checkState(
-                flowRule.treatment().allInstructions().size() == 1
-                        && inst.type() == Instruction.Type.EXTENSION,
-                "Treatment must have only 1 instruction of type EXTENSION");
-        ExtensionTreatment et =
-                ((Instructions.ExtensionInstructionWrapper) inst)
-                        .extensionInstruction();
-
-        Preconditions.checkState(
-                et.type() == ExtensionTreatmentType.ExtensionTreatmentTypes.P4_BMV2_ACTION.type(),
-                "ExtensionTreatmentType must be P4_BMV2_ACTION");
-
-        // Treatment OK, get Bmv2Action
-        entryBuilder.withAction(((Bmv2ExtensionTreatment) et).getAction());
-
-        // Table name
-        entryBuilder.withTableName(parseTableName(flowRule.tableId()));
-
-        if (!flowRule.isPermanent()) {
-            entryBuilder.withTimeout(flowRule.timeout());
-        }
-
-        entryBuilder.withPriority(flowRule.priority());
-
-        return entryBuilder.build();
+    private enum Operation {
+        APPLY, REMOVE
     }
-
-    private String parseTableName(int tableId) {
-        // TODO: map tableId with tableName according to P4 JSON
-        return "table" + String.valueOf(tableId);
-    }
-
-    private Bmv2ThriftClient getDeviceClient() throws Bmv2RuntimeException {
-        try {
-            return Bmv2ThriftClient.of(handler().data().deviceId());
-        } catch (Bmv2RuntimeException e) {
-            log.error("Failed to connect to Bmv2 device", e);
-            throw e;
-        }
-    }
-}
+}
\ No newline at end of file