Major refactoring of the BMv2 protocol module (onos1.6 cherry-pick)

- Created 3 separate sub-modules: API (doesn't depend on
    Thrift), CTL (depends on Thrift), THRIFT-API (to generate Thrift
    sources)
- Implemented 2 new services (for device configuration swapping and
    table entry management) needed to distribute BMv2-specific state
    among ONOS instances.
- Implemented a BMv2 controller (previously other modules where
    using separately a Thrift client and a server)
- Added a default BMv2 JSON configuration (default.json) and interpreter
    to be used for devices that connect for the first time to ONOS.
    This allows for basic services to work (i.e. LLDP link discovery,
    ARP proxy. etc.).
- Changed behavior of the flow rule translator and extension selector,
    now it allows extension to specify only some of the match parameters
    (before extension selectors were expected to describe the whole
    match key, i.e. all fields)
- Various renaming to better represent the API
- Various java doc fixes / improvements

Change-Id: Ida4b5e546b0def97c3552a6c05f7bce76fd32c28
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DeviceThriftClient.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DeviceThriftClient.java
new file mode 100644
index 0000000..ae852c3
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DeviceThriftClient.java
@@ -0,0 +1,428 @@
+/*
+ * 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.
+ * 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.bmv2.ctl;
+
+import com.google.common.collect.Lists;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.thrift.TException;
+import org.apache.thrift.transport.TTransport;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.bmv2.api.runtime.Bmv2Action;
+import org.onosproject.bmv2.api.runtime.Bmv2DeviceAgent;
+import org.onosproject.bmv2.api.runtime.Bmv2ExactMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2LpmMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2MatchKey;
+import org.onosproject.bmv2.api.runtime.Bmv2PortInfo;
+import org.onosproject.bmv2.api.runtime.Bmv2RuntimeException;
+import org.onosproject.bmv2.api.runtime.Bmv2TableEntry;
+import org.onosproject.bmv2.api.runtime.Bmv2TernaryMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2ValidMatchParam;
+import org.onosproject.bmv2.thriftapi.BmAddEntryOptions;
+import org.onosproject.bmv2.thriftapi.BmCounterValue;
+import org.onosproject.bmv2.thriftapi.BmMatchParam;
+import org.onosproject.bmv2.thriftapi.BmMatchParamExact;
+import org.onosproject.bmv2.thriftapi.BmMatchParamLPM;
+import org.onosproject.bmv2.thriftapi.BmMatchParamTernary;
+import org.onosproject.bmv2.thriftapi.BmMatchParamType;
+import org.onosproject.bmv2.thriftapi.BmMatchParamValid;
+import org.onosproject.bmv2.thriftapi.SimpleSwitch;
+import org.onosproject.bmv2.thriftapi.Standard;
+import org.onosproject.net.DeviceId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.onosproject.bmv2.ctl.Bmv2TExceptionParser.parseTException;
+
+/**
+ * Implementation of a Thrift client to control a BMv2 device.
+ */
+public final class Bmv2DeviceThriftClient implements Bmv2DeviceAgent {
+
+    private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+    // FIXME: make context_id arbitrary for each call
+    // See: https://github.com/p4lang/behavioral-model/blob/master/modules/bm_sim/include/bm_sim/context.h
+    private static final int CONTEXT_ID = 0;
+
+    private final Standard.Iface standardClient;
+    private final SimpleSwitch.Iface simpleSwitchClient;
+    private final TTransport transport;
+    private final DeviceId deviceId;
+
+    // ban constructor
+    protected Bmv2DeviceThriftClient(DeviceId deviceId, TTransport transport, Standard.Iface standardClient,
+                                     SimpleSwitch.Iface simpleSwitchClient) {
+        this.deviceId = deviceId;
+        this.transport = transport;
+        this.standardClient = standardClient;
+        this.simpleSwitchClient = simpleSwitchClient;
+    }
+
+    @Override
+    public DeviceId deviceId() {
+        return deviceId;
+    }
+
+    @Override
+    public boolean ping() {
+        try {
+            return this.simpleSwitchClient.ping();
+        } catch (TException e) {
+            return false;
+        }
+    }
+
+    @Override
+    public final long addTableEntry(Bmv2TableEntry entry) throws Bmv2RuntimeException {
+
+        log.debug("Adding table entry... > deviceId={}, entry={}", deviceId, entry);
+
+        long entryId = -1;
+
+        try {
+            BmAddEntryOptions options = new BmAddEntryOptions();
+
+            if (entry.hasPriority()) {
+                options.setPriority(entry.priority());
+            }
+
+            entryId = standardClient.bm_mt_add_entry(
+                    CONTEXT_ID,
+                    entry.tableName(),
+                    buildMatchParamsList(entry.matchKey()),
+                    entry.action().name(),
+                    buildActionParamsList(entry.action()),
+                    options);
+
+            if (entry.hasTimeout()) {
+                /* bmv2 accepts timeouts in milliseconds */
+                int msTimeout = (int) Math.round(entry.timeout() * 1_000);
+                standardClient.bm_mt_set_entry_ttl(
+                        CONTEXT_ID, entry.tableName(), entryId, msTimeout);
+            }
+
+            log.debug("Table entry added! > deviceId={}, entryId={}/{}", deviceId, entry.tableName(), entryId);
+
+            return entryId;
+
+        } catch (TException e) {
+            log.debug("Exception while adding table entry: {} > deviceId={}, tableName={}",
+                      e, deviceId, entry.tableName());
+            if (entryId != -1) {
+                // entry is in inconsistent state (unable to add timeout), remove it
+                try {
+                    deleteTableEntry(entry.tableName(), entryId);
+                } catch (Bmv2RuntimeException e1) {
+                    log.debug("Unable to remove failed table entry: {} > deviceId={}, tableName={}",
+                              e1, deviceId, entry.tableName());
+                }
+            }
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public final void modifyTableEntry(String tableName,
+                                       long entryId, Bmv2Action action)
+            throws Bmv2RuntimeException {
+
+        log.debug("Modifying table entry... > deviceId={}, entryId={}/{}", deviceId, tableName, entryId);
+
+        try {
+            standardClient.bm_mt_modify_entry(
+                    CONTEXT_ID,
+                    tableName,
+                    entryId,
+                    action.name(),
+                    buildActionParamsList(action));
+            log.debug("Table entry modified! > deviceId={}, entryId={}/{}", deviceId, tableName, entryId);
+        } catch (TException e) {
+            log.debug("Exception while modifying table entry: {} > deviceId={}, entryId={}/{}",
+                      e, deviceId, tableName, entryId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public final void deleteTableEntry(String tableName,
+                                       long entryId) throws Bmv2RuntimeException {
+
+        log.debug("Deleting table entry... > deviceId={}, entryId={}/{}", deviceId, tableName, entryId);
+
+        try {
+            standardClient.bm_mt_delete_entry(CONTEXT_ID, tableName, entryId);
+            log.debug("Table entry deleted! > deviceId={}, entryId={}/{}", deviceId, tableName, entryId);
+        } catch (TException e) {
+            log.debug("Exception while deleting table entry: {} > deviceId={}, entryId={}/{}",
+                      e, deviceId, tableName, entryId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public final void setTableDefaultAction(String tableName, Bmv2Action action)
+            throws Bmv2RuntimeException {
+
+        log.debug("Setting table default... > deviceId={}, tableName={}, action={}", deviceId, tableName, action);
+
+        try {
+            standardClient.bm_mt_set_default_action(
+                    CONTEXT_ID,
+                    tableName,
+                    action.name(),
+                    buildActionParamsList(action));
+            log.debug("Table default set! > deviceId={}, tableName={}, action={}", deviceId, tableName, action);
+        } catch (TException e) {
+            log.debug("Exception while setting table default : {} > deviceId={}, tableName={}, action={}",
+                      e, deviceId, tableName, action);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public Collection<Bmv2PortInfo> getPortsInfo() throws Bmv2RuntimeException {
+
+        log.debug("Retrieving port info... > deviceId={}", deviceId);
+
+        try {
+            return standardClient.bm_dev_mgr_show_ports().stream()
+                    .map(p -> new Bmv2PortInfo(p.getIface_name(), p.getPort_num(), p.isIs_up()))
+                    .collect(Collectors.toList());
+        } catch (TException e) {
+            log.debug("Exception while retrieving port info: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public String dumpTable(String tableName) throws Bmv2RuntimeException {
+
+        log.debug("Retrieving table dump... > deviceId={}, tableName={}", deviceId, tableName);
+
+        try {
+            String dump = standardClient.bm_dump_table(CONTEXT_ID, tableName);
+            log.debug("Table dump retrieved! > deviceId={}, tableName={}", deviceId, tableName);
+            return dump;
+        } catch (TException e) {
+            log.debug("Exception while retrieving table dump: {} > deviceId={}, tableName={}",
+                      e, deviceId, tableName);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public void transmitPacket(int portNumber, ImmutableByteSequence packet) throws Bmv2RuntimeException {
+
+        log.debug("Requesting packet transmission... > portNumber={}, packetSize={}", portNumber, packet.size());
+
+        try {
+
+            simpleSwitchClient.push_packet(portNumber, ByteBuffer.wrap(packet.asArray()));
+            log.debug("Packet transmission requested! > portNumber={}, packetSize={}", portNumber, packet.size());
+        } catch (TException e) {
+            log.debug("Exception while requesting packet transmission: {} > portNumber={}, packetSize={}",
+                      e, portNumber, packet.size());
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public void resetState() throws Bmv2RuntimeException {
+
+        log.debug("Resetting device state... > deviceId={}", deviceId);
+
+        try {
+            standardClient.bm_reset_state();
+            log.debug("Device state reset! > deviceId={}", deviceId);
+        } catch (TException e) {
+            log.debug("Exception while resetting device state: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public String dumpJsonConfig() throws Bmv2RuntimeException {
+
+        log.debug("Dumping device config... > deviceId={}", deviceId);
+
+        try {
+            String config = standardClient.bm_get_config();
+            log.debug("Device config dumped! > deviceId={}, configLength={}", deviceId, config.length());
+            return config;
+        } catch (TException e) {
+            log.debug("Exception while dumping device config: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public Pair<Long, Long> readTableEntryCounter(String tableName, long entryId) throws Bmv2RuntimeException {
+
+        log.debug("Reading table entry counters... > deviceId={}, tableName={}, entryId={}",
+                  deviceId, tableName, entryId);
+
+        try {
+            BmCounterValue counterValue = standardClient.bm_mt_read_counter(CONTEXT_ID, tableName, entryId);
+            log.debug("Table entry counters retrieved! > deviceId={}, tableName={}, entryId={}, bytes={}, packets={}",
+                      deviceId, tableName, entryId, counterValue.bytes, counterValue.packets);
+            return Pair.of(counterValue.bytes, counterValue.packets);
+        } catch (TException e) {
+            log.debug("Exception while reading table counters: {} > deviceId={}, tableName={}, entryId={}",
+                      e.toString(), deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public Pair<Long, Long> readCounter(String counterName, int index) throws Bmv2RuntimeException {
+
+        log.debug("Reading table entry counters... > deviceId={}, counterName={}, index={}",
+                  deviceId, counterName, index);
+
+        try {
+            BmCounterValue counterValue = standardClient.bm_counter_read(CONTEXT_ID, counterName, index);
+            log.debug("Table entry counters retrieved! >deviceId={}, counterName={}, index={}, bytes={}, packets={}",
+                      deviceId, counterName, index, counterValue.bytes, counterValue.packets);
+            return Pair.of(counterValue.bytes, counterValue.packets);
+        } catch (TException e) {
+            log.debug("Exception while reading table counters: {} > deviceId={}, counterName={}, index={}",
+                      e.toString(), deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public int getProcessInstanceId() throws Bmv2RuntimeException {
+        log.debug("Getting process instance ID... > deviceId={}", deviceId);
+        try {
+            int instanceId = simpleSwitchClient.get_process_instance_id();
+            log.debug("TProcess instance ID retrieved! > deviceId={}, instanceId={}",
+                      deviceId, instanceId);
+            return instanceId;
+        } catch (TException e) {
+            log.debug("Exception while getting process instance ID: {} > deviceId={}", e.toString(), deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public String getJsonConfigMd5() throws Bmv2RuntimeException {
+
+        log.debug("Getting device config md5... > deviceId={}", deviceId);
+
+        try {
+            String md5 = standardClient.bm_get_config_md5();
+            log.debug("Device config md5 received! > deviceId={}, configMd5={}", deviceId, md5);
+            return md5;
+        } catch (TException e) {
+            log.debug("Exception while getting device config md5: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public void loadNewJsonConfig(String jsonString) throws Bmv2RuntimeException {
+
+        log.debug("Loading new JSON config on device... > deviceId={}, jsonStringLength={}",
+                  deviceId, jsonString.length());
+
+        try {
+            standardClient.bm_load_new_config(jsonString);
+            log.debug("JSON config loaded! > deviceId={}", deviceId);
+        } catch (TException e) {
+            log.debug("Exception while loading JSON config: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public void swapJsonConfig() throws Bmv2RuntimeException {
+
+        log.debug("Swapping JSON config on device... > deviceId={}", deviceId);
+
+        try {
+            standardClient.bm_swap_configs();
+            log.debug("JSON config swapped! > deviceId={}", deviceId);
+        } catch (TException e) {
+            log.debug("Exception while swapping JSON config: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    /**
+     * Builds a list of Bmv2/Thrift compatible match parameters.
+     *
+     * @param matchKey a bmv2 matchKey
+     * @return list of thrift-compatible bm match parameters
+     */
+    private static List<BmMatchParam> buildMatchParamsList(Bmv2MatchKey matchKey) {
+        List<BmMatchParam> paramsList = Lists.newArrayList();
+        matchKey.matchParams().forEach(x -> {
+            ByteBuffer value;
+            ByteBuffer mask;
+            switch (x.type()) {
+                case EXACT:
+                    value = ByteBuffer.wrap(((Bmv2ExactMatchParam) x).value().asArray());
+                    paramsList.add(
+                            new BmMatchParam(BmMatchParamType.EXACT)
+                                    .setExact(new BmMatchParamExact(value)));
+                    break;
+                case TERNARY:
+                    value = ByteBuffer.wrap(((Bmv2TernaryMatchParam) x).value().asArray());
+                    mask = ByteBuffer.wrap(((Bmv2TernaryMatchParam) x).mask().asArray());
+                    paramsList.add(
+                            new BmMatchParam(BmMatchParamType.TERNARY)
+                                    .setTernary(new BmMatchParamTernary(value, mask)));
+                    break;
+                case LPM:
+                    value = ByteBuffer.wrap(((Bmv2LpmMatchParam) x).value().asArray());
+                    int prefixLength = ((Bmv2LpmMatchParam) x).prefixLength();
+                    paramsList.add(
+                            new BmMatchParam(BmMatchParamType.LPM)
+                                    .setLpm(new BmMatchParamLPM(value, prefixLength)));
+                    break;
+                case VALID:
+                    boolean flag = ((Bmv2ValidMatchParam) x).flag();
+                    paramsList.add(
+                            new BmMatchParam(BmMatchParamType.VALID)
+                                    .setValid(new BmMatchParamValid(flag)));
+                    break;
+                default:
+                    // should never be here
+                    throw new RuntimeException("Unknown match param type " + x.type().name());
+            }
+        });
+        return paramsList;
+    }
+
+    /**
+     * Build a list of Bmv2/Thrift compatible action parameters.
+     *
+     * @param action an action object
+     * @return list of ByteBuffers
+     */
+    private static List<ByteBuffer> buildActionParamsList(Bmv2Action action) {
+        List<ByteBuffer> buffers = Lists.newArrayList();
+        action.parameters().forEach(p -> buffers.add(ByteBuffer.wrap(p.asArray())));
+        return buffers;
+    }
+}