ONOS-7739 Support for P4Runtime multicast programming

Design doc: https://docs.google.com/document/d/13rkQlwr49M-uxQQEuxCMP7BFEPY2gtQ850Hn3gUfesU/edit#heading=h.lzdayib259sq

Change-Id: Ief00bec89fe5a9784b0ee13fdaafa3ae58ab654f
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImpl.java
new file mode 100644
index 0000000..3d524c7
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImpl.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2018-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.impl;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiPreReplica;
+import org.onosproject.net.pi.service.PiTranslationException;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+
+/**
+ * Implementation of multicast group translation logic.
+ */
+final class PiMulticastGroupTranslatorImpl {
+
+    private PiMulticastGroupTranslatorImpl() {
+        // Hides constructor.
+    }
+
+    /**
+     * Returns a PI PRE multicast group entry equivalent to the given group, for
+     * the given pipeconf and device.
+     * <p>
+     * The passed group is expected to have type {@link GroupDescription.Type#ALL}.
+     *
+     * @param group    group
+     * @return PI PRE entry
+     * @throws PiTranslationException if the group cannot be translated
+     */
+    static PiMulticastGroupEntry translate(Group group)
+            throws PiTranslationException {
+
+        checkNotNull(group);
+
+        if (!group.type().equals(GroupDescription.Type.ALL)) {
+            throw new PiTranslationException(format(
+                    "group type %s not supported", group.type()));
+        }
+
+        final List<Instruction> instructions = group.buckets().buckets().stream()
+                .flatMap(b -> b.treatment().allInstructions().stream())
+                .collect(Collectors.toList());
+
+        final boolean hasNonOutputInstr = instructions.stream()
+                .anyMatch(i -> !i.type().equals(Instruction.Type.OUTPUT));
+
+        if (instructions.size() != group.buckets().buckets().size()
+                || hasNonOutputInstr) {
+            throw new PiTranslationException(
+                    "support only groups with just one OUTPUT instruction per bucket");
+        }
+
+        final List<OutputInstruction> outInstructions = instructions.stream()
+                .map(i -> (OutputInstruction) i)
+                .collect(Collectors.toList());
+
+        return PiMulticastGroupEntry.builder()
+                .withGroupId(group.id().id())
+                .addReplicas(getReplicas(outInstructions))
+                .build();
+    }
+
+    private static Set<PiPreReplica> getReplicas(Collection<OutputInstruction> instructions) {
+        // Account for multiple replicas for the same port.
+        final Map<PortNumber, Set<PiPreReplica>> replicaMap = Maps.newHashMap();
+        final List<PortNumber> ports = instructions.stream()
+                .map(OutputInstruction::port)
+                .collect(Collectors.toList());
+        for (PortNumber port : ports) {
+            replicaMap.putIfAbsent(port, Sets.newHashSet());
+            // Use incremental instance IDs for replicas of the same port.
+            replicaMap.get(port).add(
+                    new PiPreReplica(port, replicaMap.get(port).size() + 1));
+        }
+        return replicaMap.values().stream()
+                .flatMap(Collection::stream)
+                .collect(Collectors.toSet());
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
index c69ced9..1ee53cf 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
@@ -31,6 +31,7 @@
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.runtime.PiActionGroup;
 import org.onosproject.net.pi.runtime.PiMeterCellConfig;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.service.PiFlowRuleTranslationStore;
 import org.onosproject.net.pi.service.PiFlowRuleTranslator;
@@ -38,6 +39,8 @@
 import org.onosproject.net.pi.service.PiGroupTranslator;
 import org.onosproject.net.pi.service.PiMeterTranslationStore;
 import org.onosproject.net.pi.service.PiMeterTranslator;
+import org.onosproject.net.pi.service.PiMulticastGroupTranslationStore;
+import org.onosproject.net.pi.service.PiMulticastGroupTranslator;
 import org.onosproject.net.pi.service.PiTranslationException;
 import org.onosproject.net.pi.service.PiTranslationService;
 import org.slf4j.Logger;
@@ -64,16 +67,21 @@
     private PiGroupTranslationStore groupTranslationStore;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private PiMulticastGroupTranslationStore mcGroupTranslationStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     private PiMeterTranslationStore meterTranslationStore;
 
     private PiFlowRuleTranslator flowRuleTranslator;
     private PiGroupTranslator groupTranslator;
+    private PiMulticastGroupTranslator mcGroupTranslator;
     private PiMeterTranslator meterTranslator;
 
     @Activate
     public void activate() {
         flowRuleTranslator = new InternalFlowRuleTranslator(flowRuleTranslationStore);
         groupTranslator = new InternalGroupTranslator(groupTranslationStore);
+        mcGroupTranslator = new InternalMulticastGroupTranslator(mcGroupTranslationStore);
         meterTranslator = new InternalMeterTranslator(meterTranslationStore);
         log.info("Started");
     }
@@ -101,6 +109,11 @@
         return meterTranslator;
     }
 
+    @Override
+    public PiMulticastGroupTranslator multicastGroupTranslator() {
+        return mcGroupTranslator;
+    }
+
     private Device getDevice(DeviceId deviceId) throws PiTranslationException {
         final Device device = deviceService.getDevice(deviceId);
         if (device == null) {
@@ -141,6 +154,21 @@
         }
     }
 
+    private final class InternalMulticastGroupTranslator
+            extends AbstractPiTranslatorImpl<Group, PiMulticastGroupEntry>
+            implements PiMulticastGroupTranslator {
+
+        private InternalMulticastGroupTranslator(PiMulticastGroupTranslationStore store) {
+            super(store);
+        }
+
+        @Override
+        public PiMulticastGroupEntry translate(Group original, PiPipeconf pipeconf)
+                throws PiTranslationException {
+            return PiMulticastGroupTranslatorImpl.translate(original);
+        }
+    }
+
     private final class InternalMeterTranslator
             extends AbstractPiTranslatorImpl<Meter, PiMeterCellConfig>
             implements PiMeterTranslator {
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImplTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImplTest.java
new file mode 100644
index 0000000..15774ac
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImplTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2018-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.impl;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.MacAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultFlowRule;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiMatchKey;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
+import org.onosproject.pipelines.basic.PipeconfLoader;
+
+import java.util.Optional;
+import java.util.Random;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_DST_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_SRC_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_TYPE_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.HDR_IN_PORT_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.TBL_TABLE0_ID;
+
+/**
+ * Test for {@link PiFlowRuleTranslatorImpl}.
+ */
+@SuppressWarnings("ConstantConditions")
+public class PiFlowRuleTranslatorImplTest {
+    private static final short IN_PORT_MASK = 0x01ff; // 9-bit mask
+    private static final short ETH_TYPE_MASK = (short) 0xffff;
+    private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:dummy:1");
+
+    private Random random = new Random();
+    private PiPipeconf pipeconf;
+
+    @Before
+    public void setUp() {
+        pipeconf = PipeconfLoader.BASIC_PIPECONF;
+    }
+
+    @Test
+    public void testTranslateFlowRules() throws Exception {
+
+        ApplicationId appId = new DefaultApplicationId(1, "test");
+        int tableId = 0;
+        MacAddress ethDstMac = MacAddress.valueOf(random.nextLong());
+        MacAddress ethSrcMac = MacAddress.valueOf(random.nextLong());
+        short ethType = (short) (0x0000FFFF & random.nextInt());
+        short outPort = (short) random.nextInt(65);
+        short inPort = (short) random.nextInt(65);
+        int timeout = random.nextInt(100);
+        int priority = random.nextInt(100);
+
+        TrafficSelector matchInPort1 = DefaultTrafficSelector
+                .builder()
+                .matchInPort(PortNumber.portNumber(inPort))
+                .matchEthDst(ethDstMac)
+                .matchEthSrc(ethSrcMac)
+                .matchEthType(ethType)
+                .build();
+
+        TrafficSelector emptySelector = DefaultTrafficSelector
+                .builder().build();
+
+        TrafficTreatment outPort2 = DefaultTrafficTreatment
+                .builder()
+                .setOutput(PortNumber.portNumber(outPort))
+                .build();
+
+        FlowRule rule1 = DefaultFlowRule.builder()
+                .forDevice(DEVICE_ID)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withSelector(matchInPort1)
+                .withTreatment(outPort2)
+                .makeTemporary(timeout)
+                .withPriority(priority)
+                .build();
+
+        FlowRule rule2 = DefaultFlowRule.builder()
+                .forDevice(DEVICE_ID)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withSelector(matchInPort1)
+                .withTreatment(outPort2)
+                .makeTemporary(timeout)
+                .withPriority(priority)
+                .build();
+
+        FlowRule defActionRule = DefaultFlowRule.builder()
+                .forDevice(DEVICE_ID)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withSelector(emptySelector)
+                .withTreatment(outPort2)
+                .makeTemporary(timeout)
+                .withPriority(priority)
+                .build();
+
+        PiTableEntry entry1 = PiFlowRuleTranslatorImpl.translate(rule1, pipeconf, null);
+        PiTableEntry entry2 = PiFlowRuleTranslatorImpl.translate(rule2, pipeconf, null);
+        PiTableEntry defActionEntry = PiFlowRuleTranslatorImpl.translate(defActionRule, pipeconf, null);
+
+        // check equality, i.e. same rules must produce same entries
+        new EqualsTester()
+                .addEqualityGroup(rule1, rule2)
+                .addEqualityGroup(entry1, entry2)
+                .testEquals();
+
+        // parse values stored in entry1
+        PiTernaryFieldMatch inPortParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_IN_PORT_ID).get();
+        PiTernaryFieldMatch ethDstParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_DST_ID).get();
+        PiTernaryFieldMatch ethSrcParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_SRC_ID).get();
+        PiTernaryFieldMatch ethTypeParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_TYPE_ID).get();
+        Optional<Double> expectedTimeout = pipeconf.pipelineModel().table(TBL_TABLE0_ID).get().supportsAging()
+                ? Optional.of((double) rule1.timeout()) : Optional.empty();
+
+        // check that values stored in entry are the same used for the flow rule
+        assertThat("Incorrect inPort match param value",
+                   inPortParam.value().asReadOnlyBuffer().getShort(), is(equalTo(inPort)));
+        assertThat("Incorrect inPort match param mask",
+                   inPortParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(IN_PORT_MASK)));
+        assertThat("Incorrect ethDestMac match param value",
+                   ethDstParam.value().asArray(), is(equalTo(ethDstMac.toBytes())));
+        assertThat("Incorrect ethDestMac match param mask",
+                   ethDstParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
+        assertThat("Incorrect ethSrcMac match param value",
+                   ethSrcParam.value().asArray(), is(equalTo(ethSrcMac.toBytes())));
+        assertThat("Incorrect ethSrcMac match param mask",
+                   ethSrcParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
+        assertThat("Incorrect ethType match param value",
+                   ethTypeParam.value().asReadOnlyBuffer().getShort(), is(equalTo(ethType)));
+        assertThat("Incorrect ethType match param mask",
+                   ethTypeParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(ETH_TYPE_MASK)));
+        // FIXME: re-enable when P4Runtime priority handling will be moved out of transltion service
+        // see PiFlowRuleTranslatorImpl
+        // assertThat("Incorrect priority value",
+        //            entry1.priority().get(), is(equalTo(MAX_PI_PRIORITY - rule1.priority())));
+        assertThat("Incorrect timeout value",
+                   entry1.timeout(), is(equalTo(expectedTimeout)));
+        assertThat("Match key should be empty",
+                   defActionEntry.matchKey(), is(equalTo(PiMatchKey.EMPTY)));
+        assertThat("Priority should not be set", !defActionEntry.priority().isPresent());
+    }
+}
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
new file mode 100644
index 0000000..aeaa553
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2018-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.impl;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.testing.EqualsTester;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.group.DefaultGroup;
+import org.onosproject.net.group.DefaultGroupBucket;
+import org.onosproject.net.group.DefaultGroupDescription;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionGroup;
+import org.onosproject.net.pi.runtime.PiActionGroupMember;
+import org.onosproject.net.pi.runtime.PiActionGroupMemberId;
+import org.onosproject.net.pi.runtime.PiActionParam;
+import org.onosproject.net.pi.runtime.PiGroupKey;
+import org.onosproject.net.pi.runtime.PiTableAction;
+import org.onosproject.pipelines.basic.PipeconfLoader;
+
+import java.util.Collection;
+import java.util.List;
+
+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.pipelines.basic.BasicConstants.ACT_PRF_WCMP_SELECTOR_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRM_PORT_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.ACT_SET_EGRESS_PORT_WCMP_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.PORT_BITWIDTH;
+import static org.onosproject.pipelines.basic.BasicConstants.TBL_WCMP_TABLE_ID;
+
+/**
+ * Test for {@link PiGroupTranslatorImpl}.
+ */
+public class PiGroupTranslatorImplTest {
+
+    private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:dummy:1");
+    private static final ApplicationId APP_ID = TestApplicationId.create("dummy");
+    private static final GroupId GROUP_ID = GroupId.valueOf(1);
+    private static final PiGroupKey GROUP_KEY = new PiGroupKey(
+            TBL_WCMP_TABLE_ID, ACT_PRF_WCMP_SELECTOR_ID, GROUP_ID.id());
+    private static final List<GroupBucket> BUCKET_LIST = ImmutableList.of(
+            selectOutputBucket(1),
+            selectOutputBucket(2),
+            selectOutputBucket(3));
+    private static final GroupBuckets BUCKETS = new GroupBuckets(BUCKET_LIST);
+    private static final GroupDescription SELECT_GROUP_DESC = new DefaultGroupDescription(
+            DEVICE_ID, SELECT, BUCKETS, GROUP_KEY, GROUP_ID.id(), APP_ID);
+    private static final Group SELECT_GROUP = new DefaultGroup(GROUP_ID, SELECT_GROUP_DESC);
+    private static final int DEFAULT_MEMBER_WEIGHT = 1;
+    private static final int BASE_MEM_ID = 65535;
+    private Collection<PiActionGroupMember> expectedMembers;
+
+    private PiPipeconf pipeconf;
+
+    @Before
+    public void setUp() throws Exception {
+        pipeconf = PipeconfLoader.BASIC_PIPECONF;
+        expectedMembers = ImmutableSet.of(outputMember(1),
+                                          outputMember(2),
+                                          outputMember(3));
+    }
+
+    private static GroupBucket selectOutputBucket(int portNum) {
+        ImmutableByteSequence paramVal = copyFrom(portNum);
+        PiActionParam param = new PiActionParam(ACT_PRM_PORT_ID, paramVal);
+        PiTableAction action = PiAction.builder().withId(ACT_SET_EGRESS_PORT_WCMP_ID).withParameter(param).build();
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .add(Instructions.piTableAction(action))
+                .build();
+        return DefaultGroupBucket.createSelectGroupBucket(treatment);
+    }
+
+    private static PiActionGroupMember outputMember(int portNum)
+            throws ImmutableByteSequence.ByteSequenceTrimException {
+        PiActionParam param = new PiActionParam(ACT_PRM_PORT_ID, copyFrom(portNum).fit(PORT_BITWIDTH));
+        PiAction piAction = PiAction.builder()
+                .withId(ACT_SET_EGRESS_PORT_WCMP_ID)
+                .withParameter(param).build();
+        return PiActionGroupMember.builder()
+                .withAction(piAction)
+                .withId(PiActionGroupMemberId.of(BASE_MEM_ID + portNum))
+                .withWeight(DEFAULT_MEMBER_WEIGHT)
+                .build();
+    }
+
+    /**
+     * Test add group with buckets.
+     */
+    @Test
+    public void testTranslateGroups() throws Exception {
+
+        PiActionGroup piGroup1 = PiGroupTranslatorImpl.translate(SELECT_GROUP, pipeconf, null);
+        PiActionGroup piGroup2 = PiGroupTranslatorImpl.translate(SELECT_GROUP, pipeconf, null);
+
+        new EqualsTester()
+                .addEqualityGroup(piGroup1, piGroup2)
+                .testEquals();
+
+        assertThat("Group ID must be equal",
+                   piGroup1.id().id(), is(equalTo(GROUP_ID.id())));
+        assertThat("Action profile ID must be equal",
+                   piGroup1.actionProfileId(), is(equalTo(ACT_PRF_WCMP_SELECTOR_ID)));
+
+        // members installed
+        Collection<PiActionGroupMember> members = piGroup1.members();
+        assertThat("The number of group members must be equal",
+                   piGroup1.members().size(), is(expectedMembers.size()));
+        assertThat("Group members must be equal",
+                   members.containsAll(expectedMembers) && expectedMembers.containsAll(members));
+    }
+}
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImplTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImplTest.java
new file mode 100644
index 0000000..475b7d8
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImplTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2018-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.impl;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.group.DefaultGroup;
+import org.onosproject.net.group.DefaultGroupBucket;
+import org.onosproject.net.group.DefaultGroupDescription;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.pi.runtime.PiGroupKey;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiPreReplica;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.onosproject.net.group.GroupDescription.Type.ALL;
+import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRF_WCMP_SELECTOR_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.TBL_WCMP_TABLE_ID;
+
+/**
+ * Test for {@link PiMulticastGroupTranslatorImpl}.
+ */
+public class PiMulticastGroupTranslatorImplTest {
+    private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:dummy:1");
+    private static final ApplicationId APP_ID = TestApplicationId.create("dummy");
+    private static final GroupId GROUP_ID = GroupId.valueOf(1);
+    private static final PiGroupKey GROUP_KEY = new PiGroupKey(
+            TBL_WCMP_TABLE_ID, ACT_PRF_WCMP_SELECTOR_ID, GROUP_ID.id());
+
+    private static final List<GroupBucket> BUCKET_LIST = ImmutableList.of(
+            allOutputBucket(1),
+            allOutputBucket(2),
+            allOutputBucket(3));
+    private static final List<GroupBucket> BUCKET_LIST_2 = ImmutableList.of(
+            allOutputBucket(1),
+            allOutputBucket(2),
+            allOutputBucket(2),
+            allOutputBucket(3),
+            allOutputBucket(3));
+
+    private static final Set<PiPreReplica> REPLICAS = ImmutableSet.of(
+            new PiPreReplica(PortNumber.portNumber(1), 1),
+            new PiPreReplica(PortNumber.portNumber(2), 1),
+            new PiPreReplica(PortNumber.portNumber(3), 1));
+    private static final Set<PiPreReplica> REPLICAS_2 = ImmutableSet.of(
+            new PiPreReplica(PortNumber.portNumber(1), 1),
+            new PiPreReplica(PortNumber.portNumber(2), 1),
+            new PiPreReplica(PortNumber.portNumber(2), 2),
+            new PiPreReplica(PortNumber.portNumber(3), 1),
+            new PiPreReplica(PortNumber.portNumber(3), 2));
+
+    private static final PiMulticastGroupEntry PI_MULTICAST_GROUP =
+            PiMulticastGroupEntry.builder()
+                    .withGroupId(GROUP_ID.id())
+                    .addReplicas(REPLICAS)
+                    .build();
+    private static final PiMulticastGroupEntry PI_MULTICAST_GROUP_2 =
+            PiMulticastGroupEntry.builder()
+                    .withGroupId(GROUP_ID.id())
+                    .addReplicas(REPLICAS_2)
+                    .build();
+
+    private static final GroupBuckets BUCKETS = new GroupBuckets(BUCKET_LIST);
+    private static final GroupBuckets BUCKETS_2 = new GroupBuckets(BUCKET_LIST_2);
+
+    private static final GroupDescription ALL_GROUP_DESC = new DefaultGroupDescription(
+            DEVICE_ID, ALL, BUCKETS, GROUP_KEY, GROUP_ID.id(), APP_ID);
+    private static final Group ALL_GROUP = new DefaultGroup(GROUP_ID, ALL_GROUP_DESC);
+
+    private static final GroupDescription ALL_GROUP_DESC_2 = new DefaultGroupDescription(
+            DEVICE_ID, ALL, BUCKETS_2, GROUP_KEY, GROUP_ID.id(), APP_ID);
+    private static final Group ALL_GROUP_2 = new DefaultGroup(GROUP_ID, ALL_GROUP_DESC_2);
+
+
+    private static GroupBucket allOutputBucket(int portNum) {
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .setOutput(PortNumber.portNumber(portNum))
+                .build();
+        return DefaultGroupBucket.createAllGroupBucket(treatment);
+    }
+
+    @Test
+    public void testTranslatePreGroups() throws Exception {
+
+        PiMulticastGroupEntry multicastGroup = PiMulticastGroupTranslatorImpl
+                .translate(ALL_GROUP);
+        PiMulticastGroupEntry multicastGroup2 = PiMulticastGroupTranslatorImpl
+                .translate(ALL_GROUP_2);
+
+        new EqualsTester()
+                .addEqualityGroup(multicastGroup, PI_MULTICAST_GROUP)
+                .addEqualityGroup(multicastGroup2, PI_MULTICAST_GROUP_2)
+                .testEquals();
+    }
+}
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java
deleted file mode 100644
index 9faeaba..0000000
--- a/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright 2017-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.impl;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.testing.EqualsTester;
-import org.junit.Before;
-import org.junit.Test;
-import org.onlab.packet.MacAddress;
-import org.onlab.util.ImmutableByteSequence;
-import org.onosproject.TestApplicationId;
-import org.onosproject.core.ApplicationId;
-import org.onosproject.core.DefaultApplicationId;
-import org.onosproject.core.GroupId;
-import org.onosproject.net.DeviceId;
-import org.onosproject.net.PortNumber;
-import org.onosproject.net.flow.DefaultFlowRule;
-import org.onosproject.net.flow.DefaultTrafficSelector;
-import org.onosproject.net.flow.DefaultTrafficTreatment;
-import org.onosproject.net.flow.FlowRule;
-import org.onosproject.net.flow.TrafficSelector;
-import org.onosproject.net.flow.TrafficTreatment;
-import org.onosproject.net.flow.instructions.Instructions;
-import org.onosproject.net.group.DefaultGroup;
-import org.onosproject.net.group.DefaultGroupBucket;
-import org.onosproject.net.group.DefaultGroupDescription;
-import org.onosproject.net.group.Group;
-import org.onosproject.net.group.GroupBucket;
-import org.onosproject.net.group.GroupBuckets;
-import org.onosproject.net.group.GroupDescription;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiAction;
-import org.onosproject.net.pi.runtime.PiActionGroup;
-import org.onosproject.net.pi.runtime.PiActionGroupMember;
-import org.onosproject.net.pi.runtime.PiActionGroupMemberId;
-import org.onosproject.net.pi.runtime.PiActionParam;
-import org.onosproject.net.pi.runtime.PiGroupKey;
-import org.onosproject.net.pi.runtime.PiMatchKey;
-import org.onosproject.net.pi.runtime.PiTableAction;
-import org.onosproject.net.pi.runtime.PiTableEntry;
-import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
-import org.onosproject.pipelines.basic.PipeconfLoader;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.Random;
-
-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.pipelines.basic.BasicConstants.ACT_PRF_WCMP_SELECTOR_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRM_PORT_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.ACT_SET_EGRESS_PORT_WCMP_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_DST_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_SRC_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_TYPE_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.HDR_IN_PORT_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.PORT_BITWIDTH;
-import static org.onosproject.pipelines.basic.BasicConstants.TBL_TABLE0_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.TBL_WCMP_TABLE_ID;
-
-/**
- * Tests for {@link PiFlowRuleTranslatorImpl}.
- */
-@SuppressWarnings("ConstantConditions")
-public class PiTranslatorServiceTest {
-
-    private static final short IN_PORT_MASK = 0x01ff; // 9-bit mask
-    private static final short ETH_TYPE_MASK = (short) 0xffff;
-    private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:dummy:1");
-    private static final ApplicationId APP_ID = TestApplicationId.create("dummy");
-    private static final GroupId GROUP_ID = GroupId.valueOf(1);
-    private static final List<GroupBucket> BUCKET_LIST = ImmutableList.of(outputBucket(1),
-                                                                          outputBucket(2),
-                                                                          outputBucket(3)
-    );
-    private static final PiGroupKey GROUP_KEY = new PiGroupKey(TBL_WCMP_TABLE_ID, ACT_PRF_WCMP_SELECTOR_ID,
-                                                               GROUP_ID.id());
-    private static final GroupBuckets BUCKETS = new GroupBuckets(BUCKET_LIST);
-    private static final GroupDescription GROUP_DESC =
-            new DefaultGroupDescription(DEVICE_ID, SELECT, BUCKETS, GROUP_KEY, GROUP_ID.id(), APP_ID);
-    private static final Group GROUP = new DefaultGroup(GROUP_ID, GROUP_DESC);
-    private static final int DEFAULT_MEMBER_WEIGHT = 1;
-    private static final int BASE_MEM_ID = 65535;
-    private Collection<PiActionGroupMember> expectedMembers;
-
-    private Random random = new Random();
-    private PiPipeconf pipeconf;
-
-    @Before
-    public void setUp() throws Exception {
-        pipeconf = PipeconfLoader.BASIC_PIPECONF;
-        expectedMembers = ImmutableSet.of(outputMember(1),
-                                          outputMember(2),
-                                          outputMember(3));
-    }
-
-    @Test
-    public void testTranslateFlowRules() throws Exception {
-
-        ApplicationId appId = new DefaultApplicationId(1, "test");
-        int tableId = 0;
-        MacAddress ethDstMac = MacAddress.valueOf(random.nextLong());
-        MacAddress ethSrcMac = MacAddress.valueOf(random.nextLong());
-        short ethType = (short) (0x0000FFFF & random.nextInt());
-        short outPort = (short) random.nextInt(65);
-        short inPort = (short) random.nextInt(65);
-        int timeout = random.nextInt(100);
-        int priority = random.nextInt(100);
-
-        TrafficSelector matchInPort1 = DefaultTrafficSelector
-                .builder()
-                .matchInPort(PortNumber.portNumber(inPort))
-                .matchEthDst(ethDstMac)
-                .matchEthSrc(ethSrcMac)
-                .matchEthType(ethType)
-                .build();
-
-        TrafficSelector emptySelector = DefaultTrafficSelector
-                .builder().build();
-
-        TrafficTreatment outPort2 = DefaultTrafficTreatment
-                .builder()
-                .setOutput(PortNumber.portNumber(outPort))
-                .build();
-
-        FlowRule rule1 = DefaultFlowRule.builder()
-                .forDevice(DEVICE_ID)
-                .forTable(tableId)
-                .fromApp(appId)
-                .withSelector(matchInPort1)
-                .withTreatment(outPort2)
-                .makeTemporary(timeout)
-                .withPriority(priority)
-                .build();
-
-        FlowRule rule2 = DefaultFlowRule.builder()
-                .forDevice(DEVICE_ID)
-                .forTable(tableId)
-                .fromApp(appId)
-                .withSelector(matchInPort1)
-                .withTreatment(outPort2)
-                .makeTemporary(timeout)
-                .withPriority(priority)
-                .build();
-
-        FlowRule defActionRule = DefaultFlowRule.builder()
-                .forDevice(DEVICE_ID)
-                .forTable(tableId)
-                .fromApp(appId)
-                .withSelector(emptySelector)
-                .withTreatment(outPort2)
-                .makeTemporary(timeout)
-                .withPriority(priority)
-                .build();
-
-        PiTableEntry entry1 = PiFlowRuleTranslatorImpl.translate(rule1, pipeconf, null);
-        PiTableEntry entry2 = PiFlowRuleTranslatorImpl.translate(rule2, pipeconf, null);
-        PiTableEntry defActionEntry = PiFlowRuleTranslatorImpl.translate(defActionRule, pipeconf, null);
-
-        // check equality, i.e. same rules must produce same entries
-        new EqualsTester()
-                .addEqualityGroup(rule1, rule2)
-                .addEqualityGroup(entry1, entry2)
-                .testEquals();
-
-        // parse values stored in entry1
-        PiTernaryFieldMatch inPortParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_IN_PORT_ID).get();
-        PiTernaryFieldMatch ethDstParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_DST_ID).get();
-        PiTernaryFieldMatch ethSrcParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_SRC_ID).get();
-        PiTernaryFieldMatch ethTypeParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_TYPE_ID).get();
-        Optional<Double> expectedTimeout = pipeconf.pipelineModel().table(TBL_TABLE0_ID).get().supportsAging()
-                ? Optional.of((double) rule1.timeout()) : Optional.empty();
-
-        // check that values stored in entry are the same used for the flow rule
-        assertThat("Incorrect inPort match param value",
-                   inPortParam.value().asReadOnlyBuffer().getShort(), is(equalTo(inPort)));
-        assertThat("Incorrect inPort match param mask",
-                   inPortParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(IN_PORT_MASK)));
-        assertThat("Incorrect ethDestMac match param value",
-                   ethDstParam.value().asArray(), is(equalTo(ethDstMac.toBytes())));
-        assertThat("Incorrect ethDestMac match param mask",
-                   ethDstParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
-        assertThat("Incorrect ethSrcMac match param value",
-                   ethSrcParam.value().asArray(), is(equalTo(ethSrcMac.toBytes())));
-        assertThat("Incorrect ethSrcMac match param mask",
-                   ethSrcParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
-        assertThat("Incorrect ethType match param value",
-                   ethTypeParam.value().asReadOnlyBuffer().getShort(), is(equalTo(ethType)));
-        assertThat("Incorrect ethType match param mask",
-                   ethTypeParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(ETH_TYPE_MASK)));
-        // FIXME: re-enable when P4Runtime priority handling will be moved out of transltion service
-        // see PiFlowRuleTranslatorImpl
-        // assertThat("Incorrect priority value",
-        //            entry1.priority().get(), is(equalTo(MAX_PI_PRIORITY - rule1.priority())));
-        assertThat("Incorrect timeout value",
-                   entry1.timeout(), is(equalTo(expectedTimeout)));
-        assertThat("Match key should be empty",
-                   defActionEntry.matchKey(), is(equalTo(PiMatchKey.EMPTY)));
-        assertThat("Priority should not be set", !defActionEntry.priority().isPresent());
-
-    }
-
-    private static GroupBucket outputBucket(int portNum) {
-        ImmutableByteSequence paramVal = copyFrom(portNum);
-        PiActionParam param = new PiActionParam(ACT_PRM_PORT_ID, paramVal);
-        PiTableAction action = PiAction.builder().withId(ACT_SET_EGRESS_PORT_WCMP_ID).withParameter(param).build();
-        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
-                .add(Instructions.piTableAction(action))
-                .build();
-        return DefaultGroupBucket.createSelectGroupBucket(treatment);
-    }
-
-    private static PiActionGroupMember outputMember(int portNum)
-            throws ImmutableByteSequence.ByteSequenceTrimException {
-        PiActionParam param = new PiActionParam(ACT_PRM_PORT_ID, copyFrom(portNum).fit(PORT_BITWIDTH));
-        PiAction piAction = PiAction.builder()
-                .withId(ACT_SET_EGRESS_PORT_WCMP_ID)
-                .withParameter(param).build();
-        return PiActionGroupMember.builder()
-                .withAction(piAction)
-                .withId(PiActionGroupMemberId.of(BASE_MEM_ID + portNum))
-                .withWeight(DEFAULT_MEMBER_WEIGHT)
-                .build();
-    }
-
-    /**
-     * Test add group with buckets.
-     */
-    @Test
-    public void testTranslateGroups() throws Exception {
-
-        PiActionGroup piGroup1 = PiGroupTranslatorImpl.translate(GROUP, pipeconf, null);
-        PiActionGroup piGroup2 = PiGroupTranslatorImpl.translate(GROUP, pipeconf, null);
-
-        new EqualsTester()
-                .addEqualityGroup(piGroup1, piGroup2)
-                .testEquals();
-
-        assertThat("Group ID must be equal",
-                   piGroup1.id().id(), is(equalTo(GROUP_ID.id())));
-        assertThat("Action profile ID must be equal",
-                   piGroup1.actionProfileId(), is(equalTo(ACT_PRF_WCMP_SELECTOR_ID)));
-
-        // members installed
-        Collection<PiActionGroupMember> members = piGroup1.members();
-        assertThat("The number of group members must be equal",
-                   piGroup1.members().size(), is(expectedMembers.size()));
-        assertThat("Group members must be equal",
-                   members.containsAll(expectedMembers) && expectedMembers.containsAll(members));
-    }
-}