Add support for one shot action profile programming in PI

A P4 table annotated with @oneshot annotation can be programmed
only with the action profile action set. For these kind of tables
we don't issue read request for action profile groups and members.

Change-Id: I7b6a743f4f4df4190f17d958ebb4807aca5feda5
diff --git a/core/api/src/main/java/org/onosproject/net/pi/model/PiTableModel.java b/core/api/src/main/java/org/onosproject/net/pi/model/PiTableModel.java
index 80e41df..51ba53d 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/model/PiTableModel.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/model/PiTableModel.java
@@ -109,6 +109,14 @@
     boolean isConstantTable();
 
     /**
+     * Returns true if the table supports one shot only when associated to an
+     * action profile.
+     *
+     * @return true if table support one shot only, false otherwise
+     */
+    boolean oneShotOnly();
+
+    /**
      * Returns the action model associated with the given ID, if present. If not
      * present, it means that this table does not support such an action.
      *
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionSet.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionSet.java
new file mode 100644
index 0000000..f2908d4
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionSet.java
@@ -0,0 +1,179 @@
+/*
+ * 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.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ * Instance of an action set of a protocol-independent pipeline used
+ * when doing one-shot action selector programming. Contains a set of weighted
+ * actions, and it is equivalent to the action profile action set from P4Runtime
+ * specifications.
+ */
+@Beta
+public final class PiActionSet implements PiTableAction {
+
+    private final Set<WeightedAction> actionSet;
+
+    private PiActionSet(Set<WeightedAction> actionSet) {
+        this.actionSet = actionSet;
+    }
+
+    /**
+     * Returns the set of actions.
+     *
+     * @return the set of actions
+     */
+    public Set<WeightedAction> actions() {
+        return actionSet;
+    }
+
+    @Override
+    public Type type() {
+        return Type.ACTION_SET;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        PiActionSet that = (PiActionSet) o;
+        return Objects.equal(actionSet, that.actionSet);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(actionSet);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("actionSet", actionSet)
+                .toString();
+    }
+
+    /**
+     * Returns a new builder of an action set.
+     *
+     * @return action set builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder of an action set.
+     */
+    public static final class Builder {
+
+        private final Set<WeightedAction> actionSet = Sets.newHashSet();
+
+        private Builder() {
+            // hides constructor.
+        }
+
+        /**
+         * Adds a weighted action to this action set.
+         *
+         * @param action The action to add
+         * @param weight The weight associated to the action
+         * @return this
+         */
+        public Builder addWeightedAction(
+                PiAction action, int weight) {
+            actionSet.add(new WeightedAction(action, weight));
+            return this;
+        }
+
+        /**
+         * Creates a new action profile action set.
+         *
+         * @return action profile action set
+         */
+        public PiActionSet build() {
+            return new PiActionSet(Set.copyOf(actionSet));
+        }
+    }
+
+    /**
+     * Weighted action used in an actions set.
+     */
+    public static final class WeightedAction {
+        public static final int DEFAULT_WEIGHT = 1;
+
+        private final int weight;
+        private final PiAction action;
+
+        /**
+         * Creates a new weighted action instance that can be used in an action
+         * set, from the given PI action and weight.
+         *
+         * @param action the action
+         * @param weight the weigh
+         */
+        public WeightedAction(PiAction action, int weight) {
+            this.weight = weight;
+            this.action = action;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            WeightedAction that = (WeightedAction) o;
+            return weight == that.weight &&
+                    Objects.equal(action, that.action);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(weight, action);
+        }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                    .add("weight", weight)
+                    .add("action", action)
+                    .toString();
+        }
+
+        public int weight() {
+            return weight;
+        }
+
+        public PiAction action() {
+            return action;
+        }
+    }
+}
+
+
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableAction.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableAction.java
index 284dde6..d803ca6 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableAction.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableAction.java
@@ -43,7 +43,13 @@
          * Executes the action profile member specified by the given
          * identifier.
          */
-        ACTION_PROFILE_MEMBER_ID
+        ACTION_PROFILE_MEMBER_ID,
+
+        /**
+         * Executes the given action set. Used in one-shot action profile
+         * programming.
+         */
+        ACTION_SET
     }
 
     /**
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
index ee3c3c3..40ad4de 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
@@ -185,6 +185,8 @@
                 return "ACT_PROF_GROUP:" + ((PiActionProfileGroupId) tableAction).id();
             case ACTION_PROFILE_MEMBER_ID:
                 return "ACT_PROF_MEMBER:" + ((PiActionProfileMemberId) tableAction).id();
+            case ACTION_SET:
+                return "ACTION_SET:" + tableAction.toString();
             case ACTION:
             default:
                 return tableAction.toString();
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/DecodeInstructionCodecHelper.java b/core/common/src/main/java/org/onosproject/codec/impl/DecodeInstructionCodecHelper.java
index 7307939..e3efae2 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/DecodeInstructionCodecHelper.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/DecodeInstructionCodecHelper.java
@@ -329,6 +329,7 @@
 
             return Instructions.piTableAction(piActionProfileMemberId);
         }
+        // TODO: implement JSON decoder for ACTION_SET
         throw new IllegalArgumentException("Protocol-independent Instruction subtype "
                                                    + subType + " is not supported");
     }
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/EncodeInstructionCodecHelper.java b/core/common/src/main/java/org/onosproject/codec/impl/EncodeInstructionCodecHelper.java
index 996fa8c..cab335c 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/EncodeInstructionCodecHelper.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/EncodeInstructionCodecHelper.java
@@ -289,6 +289,7 @@
                 final PiActionProfileMemberId memberId = (PiActionProfileMemberId) piInstruction.action();
                 result.put(InstructionCodec.PI_ACTION_PROFILE_MEMBER_ID, memberId.id());
                 break;
+            //TODO: implement JSON encoder for ACTION_SET
             default:
                 throw new IllegalArgumentException("Cannot convert protocol-independent subtype of" +
                                                            piInstruction.action().type().name());
diff --git a/core/net/BUILD b/core/net/BUILD
index fbc6fb6..8b38727 100644
--- a/core/net/BUILD
+++ b/core/net/BUILD
@@ -10,6 +10,7 @@
     "//core/store/dist:onos-core-dist-tests",
     "//utils/osgi:onlab-osgi-tests",
     "//pipelines/basic:onos-pipelines-basic",
+    "//protocols/p4runtime/model:onos-protocols-p4runtime-model-native",
     "@minimal_json//jar",
 ]
 
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java
index f399508..2b4b0f3 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java
@@ -40,6 +40,7 @@
 import org.onosproject.net.pi.model.PiTableType;
 import org.onosproject.net.pi.runtime.PiAction;
 import org.onosproject.net.pi.runtime.PiActionParam;
+import org.onosproject.net.pi.runtime.PiActionSet;
 import org.onosproject.net.pi.runtime.PiExactFieldMatch;
 import org.onosproject.net.pi.runtime.PiFieldMatch;
 import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
@@ -234,19 +235,28 @@
         switch (piTableAction.type()) {
             case ACTION:
                 return checkPiAction((PiAction) piTableAction, table);
+            case ACTION_SET:
+                for (var actProfAct : ((PiActionSet) piTableAction).actions()) {
+                    checkPiAction(actProfAct.action(), table);
+                }
             case ACTION_PROFILE_GROUP_ID:
+                if (table.actionProfile() == null || !table.actionProfile().hasSelector()) {
+                    throw new PiTranslationException(format(
+                            "action is of type '%s', but table '%s' does not" +
+                                    "implement an action profile with dynamic selection",
+                            piTableAction.type(), table.id()));
+                }
             case ACTION_PROFILE_MEMBER_ID:
                 if (!table.tableType().equals(PiTableType.INDIRECT)) {
                     throw new PiTranslationException(format(
                             "action is indirect of type '%s', but table '%s' is of type '%s'",
                             piTableAction.type(), table.id(), table.tableType()));
                 }
-                if (piTableAction.type().equals(PiTableAction.Type.ACTION_PROFILE_GROUP_ID)
-                        && (table.actionProfile() == null || !table.actionProfile().hasSelector())) {
+                if (!piTableAction.type().equals(PiTableAction.Type.ACTION_SET) &&
+                        table.oneShotOnly()) {
                     throw new PiTranslationException(format(
-                            "action is of type '%s', but table '%s' does not" +
-                                    "implement an action profile with dynamic selection",
-                            piTableAction.type(), table.id()));
+                            "table '%s' supports only one shot programming", table.id()
+                    ));
                 }
                 return piTableAction;
             default:
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java
index 42a29a8..e06f54e 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java
@@ -94,6 +94,18 @@
                     actionProfileId));
         }
 
+        // Check if the table associated with the action profile supports only
+        // one-shot action profile programming.
+        boolean isTableOneShot = actionProfileModel.tables().stream()
+                .map(tableId -> pipeconf.pipelineModel().table(tableId))
+                .allMatch(piTableModel -> piTableModel.isPresent() &&
+                        piTableModel.get().oneShotOnly());
+        if (isTableOneShot) {
+            throw new PiTranslationException(format(
+                    "Table associated to action profile '%s' supports only one-shot action profile programming",
+                    actionProfileId));
+        }
+
         // Check group validity.
         if (actionProfileModel.maxGroupSize() > 0
                 && group.buckets().buckets().size() > actionProfileModel.maxGroupSize()) {
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java
index ac9817d..1da3997 100644
--- a/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java
@@ -20,7 +20,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.testing.EqualsTester;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.onlab.util.ImmutableByteSequence;
 import org.onosproject.TestApplicationId;
 import org.onosproject.core.ApplicationId;
@@ -36,7 +38,10 @@
 import org.onosproject.net.group.GroupBucket;
 import org.onosproject.net.group.GroupBuckets;
 import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.pi.model.DefaultPiPipeconf;
 import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiPipeconfId;
+import org.onosproject.net.pi.model.PiPipelineModel;
 import org.onosproject.net.pi.runtime.PiAction;
 import org.onosproject.net.pi.runtime.PiActionParam;
 import org.onosproject.net.pi.runtime.PiActionProfileGroup;
@@ -44,18 +49,24 @@
 import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
 import org.onosproject.net.pi.runtime.PiGroupKey;
 import org.onosproject.net.pi.runtime.PiTableAction;
+import org.onosproject.net.pi.service.PiTranslationException;
+import org.onosproject.p4runtime.model.P4InfoParser;
+import org.onosproject.p4runtime.model.P4InfoParserException;
 import org.onosproject.pipelines.basic.PipeconfLoader;
 
+import java.net.URL;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
+import static java.lang.String.format;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.onlab.util.ImmutableByteSequence.copyFrom;
 import static org.onosproject.net.group.GroupDescription.Type.SELECT;
+import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.P4_INFO_TEXT;
 import static org.onosproject.net.pi.runtime.PiActionProfileGroup.WeightedMember.DEFAULT_WEIGHT;
 import static org.onosproject.pipelines.basic.BasicConstants.INGRESS_WCMP_CONTROL_SET_EGRESS_PORT;
 import static org.onosproject.pipelines.basic.BasicConstants.INGRESS_WCMP_CONTROL_WCMP_SELECTOR;
@@ -88,9 +99,15 @@
 
     private PiPipeconf pipeconf;
 
+    // Derived from basic.p4info, with wcmp_table annotated with @oneshot
+    private static final String PATH_ONESHOT_P4INFO = "oneshot.p4info";
+    private static final PiPipeconfId ONE_SHOT_PIPECONF_ID = new PiPipeconfId("org.onosproject.pipelines.wcmp_oneshot");
+    private PiPipeconf pipeconfOneShot;
+
     @Before
     public void setUp() throws Exception {
         pipeconf = PipeconfLoader.BASIC_PIPECONF;
+        pipeconfOneShot = loadP4InfoPipeconf(ONE_SHOT_PIPECONF_ID, PATH_ONESHOT_P4INFO);
         expectedMemberInstances = ImmutableSet.of(outputMember(1),
                                                   outputMember(2),
                                                   outputMember(3));
@@ -159,4 +176,34 @@
                            && expectedMemberInstances.containsAll(memberInstances));
 
     }
+
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();
+
+    /**
+     * Test add group with buckets.
+     */
+    @Test
+    public void testTranslateGroupsOneShotError() throws Exception {
+        thrown.expect(PiTranslationException.class);
+        thrown.expectMessage(format("Table associated to action profile '%s' " +
+                                            "supports only one-shot action profile programming",
+                                    INGRESS_WCMP_CONTROL_WCMP_SELECTOR.id()));
+        PiGroupTranslatorImpl.translate(SELECT_GROUP, pipeconfOneShot, null);
+    }
+
+    private static PiPipeconf loadP4InfoPipeconf(PiPipeconfId pipeconfId, String p4infoPath) {
+        final URL p4InfoUrl = PiGroupTranslatorImpl.class.getResource(p4infoPath);
+        final PiPipelineModel pipelineModel;
+        try {
+            pipelineModel = P4InfoParser.parse(p4InfoUrl);
+        } catch (P4InfoParserException e) {
+            throw new IllegalStateException(e);
+        }
+        return DefaultPiPipeconf.builder()
+                .withId(pipeconfId)
+                .withPipelineModel(pipelineModel)
+                .addExtension(P4_INFO_TEXT, p4InfoUrl)
+                .build();
+    }
 }
diff --git a/core/net/src/test/resources/org/onosproject/net/pi/impl/oneshot.p4info b/core/net/src/test/resources/org/onosproject/net/pi/impl/oneshot.p4info
new file mode 100644
index 0000000..334398b
--- /dev/null
+++ b/core/net/src/test/resources/org/onosproject/net/pi/impl/oneshot.p4info
@@ -0,0 +1,94 @@
+pkg_info {
+  arch: "v1model"
+}
+tables {
+  preamble {
+    id: 33594717
+    name: "ingress.wcmp_control.wcmp_table"
+    alias: "wcmp_table"
+    annotations: "@oneshot"
+  }
+  match_fields {
+    id: 1
+    name: "local_metadata.next_hop_id"
+    bitwidth: 16
+    match_type: EXACT
+  }
+  action_refs {
+    id: 16796092
+  }
+  action_refs {
+    id: 16800567
+    annotations: "@defaultonly"
+    scope: DEFAULT_ONLY
+  }
+  implementation_id: 285253634
+  size: 1024
+}
+actions {
+  preamble {
+    id: 16800567
+    name: "NoAction"
+    alias: "NoAction"
+  }
+}
+actions {
+  preamble {
+    id: 16796092
+    name: "ingress.wcmp_control.set_egress_port"
+    alias: "wcmp_control.set_egress_port"
+  }
+  params {
+    id: 1
+    name: "port"
+    bitwidth: 9
+  }
+}
+action_profiles {
+  preamble {
+    id: 285253634
+    name: "ingress.wcmp_control.wcmp_selector"
+    alias: "wcmp_selector"
+  }
+  table_ids: 33594717
+  with_selector: true
+  size: 64
+}
+controller_packet_metadata {
+  preamble {
+    id: 67146229
+    name: "packet_in"
+    alias: "packet_in"
+    annotations: "@controller_header(\"packet_in\")"
+  }
+  metadata {
+    id: 1
+    name: "ingress_port"
+    bitwidth: 9
+  }
+  metadata {
+    id: 2
+    name: "_padding"
+    bitwidth: 7
+  }
+}
+controller_packet_metadata {
+  preamble {
+    id: 67121543
+    name: "packet_out"
+    alias: "packet_out"
+    annotations: "@controller_header(\"packet_out\")"
+  }
+  metadata {
+    id: 1
+    name: "egress_port"
+    bitwidth: 9
+  }
+  metadata {
+    id: 2
+    name: "_padding"
+    bitwidth: 7
+  }
+}
+type_info {
+}