[SDFAB-831] Add UPF meters to UpfProgrammable APIs

UPF meters can be of type session or application.
Also, add meter index to sessions and terminations UPF entities.

Change-Id: I8babfca35341a21b234d8eb6edaa2e1c02684210
(cherry picked from commit b25299afaf824a8d352297224e5b9a1285901d00)
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfEntity.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfEntity.java
index 81dd28a..e54ab34 100644
--- a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfEntity.java
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfEntity.java
@@ -29,6 +29,16 @@
     byte DEFAULT_APP_ID = 0;
 
     /**
+     * Default session index, to be used if no session metering is performed.
+     */
+    int DEFAULT_SESSION_INDEX = 0;
+
+    /**
+     * Default app index, to be used if no app metering is performed.
+     */
+    int DEFAULT_APP_INDEX = 0;
+
+    /**
      * Returns the type of this entity.
      *
      * @return entity type
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfEntityType.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfEntityType.java
index 2e9a26a..c5d5590 100644
--- a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfEntityType.java
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfEntityType.java
@@ -30,7 +30,9 @@
     SESSION_UPLINK("session_downlink"),
     TUNNEL_PEER("tunnel_peer"),
     COUNTER("counter"),
-    APPLICATION("application");
+    APPLICATION("application"),
+    SESSION_METER("session_meter"),
+    APPLICATION_METER("application_meter");
 
     private final String humanReadableName;
 
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfMeter.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfMeter.java
new file mode 100644
index 0000000..1a3e799
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfMeter.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2022-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.behaviour.upf;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import org.onosproject.net.meter.Band;
+import org.onosproject.net.meter.DefaultBand;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.behaviour.upf.UpfEntityType.APPLICATION_METER;
+import static org.onosproject.net.behaviour.upf.UpfEntityType.SESSION_METER;
+import static org.onosproject.net.meter.Band.Type.MARK_RED;
+import static org.onosproject.net.meter.Band.Type.MARK_YELLOW;
+
+/**
+ * A structure representing a UPF meter, either for metering session (UE) or
+ * application traffic.
+ * UPF meters represent PFCP QER MBR and GBR information.
+ * UPF meters of type session support only the peak band.
+ * UPF meters of type application support both peak and committed bands.
+ */
+public final class UpfMeter implements UpfEntity {
+    private final int cellId;
+    private final ImmutableMap<Band.Type, Band> meterBands;
+    private final UpfEntityType type;
+
+    private UpfMeter(int cellId, Map<Band.Type, Band> meterBands, UpfEntityType type) {
+        this.cellId = cellId;
+        this.meterBands = ImmutableMap.copyOf(meterBands);
+        this.type = type;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UpfMeter(type=%s, index=%d, committed=%s, peak=%s)",
+                             type, cellId, committedBand().orElse(null),
+                             peakBand().orElse(null));
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (this == object) {
+            return true;
+        }
+
+        if (object == null) {
+            return false;
+        }
+
+        if (getClass() != object.getClass()) {
+            return false;
+        }
+
+        UpfMeter that = (UpfMeter) object;
+        return this.type.equals(that.type) &&
+                this.cellId == that.cellId &&
+                this.meterBands.equals(that.meterBands);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(type, cellId, meterBands);
+    }
+
+    @Override
+    public UpfEntityType type() {
+        return this.type;
+    }
+
+    /**
+     * Get the meter cell index of this meter.
+     *
+     * @return the cell index
+     */
+    public int cellId() {
+        return this.cellId;
+    }
+
+    /**
+     * Get the committed band of this meter.
+     *
+     * @return the committed band, Empty if none
+     */
+    public Optional<Band> committedBand() {
+        return Optional.ofNullable(meterBands.getOrDefault(MARK_YELLOW, null));
+    }
+
+    /**
+     * Get the peak band of this meter.
+     *
+     * @return the peak band, Empty if none
+     */
+    public Optional<Band> peakBand() {
+        return Optional.ofNullable(meterBands.getOrDefault(MARK_RED, null));
+    }
+
+    /**
+     * Check if this UPF meter is for sessions (UE) traffic.
+     *
+     * @return true if the meter is for session traffic
+     */
+    public boolean isSession() {
+        return type.equals(SESSION_METER);
+    }
+
+    /**
+     * Check if this UPF meter is for application traffic.
+     *
+     * @return true if the meter is for application traffic
+     */
+    public boolean isApplication() {
+        return type.equals(APPLICATION_METER);
+    }
+
+    /**
+     * Check if this UPF meter is a reset.
+     *
+     * @return true if this represents a meter reset.
+     */
+    public boolean isReset() {
+        return meterBands.isEmpty();
+    }
+
+    /**
+     * Return a session UPF meter with no bands. Used to reset the meter.
+     *
+     * @param cellId the meter cell index of this meter
+     * @return a UpfMeter of type session with no bands
+     */
+    public static UpfMeter resetSession(int cellId) {
+        return new UpfMeter(cellId, Maps.newHashMap(), SESSION_METER);
+    }
+
+    /**
+     * Return an application UPF meter with no bands. Used to reset the meter.
+     *
+     * @param cellId the meter cell index of this meter
+     * @return a UpfMeter of type application with no bands
+     */
+    public static UpfMeter resetApplication(int cellId) {
+        return new UpfMeter(cellId, Maps.newHashMap(), APPLICATION_METER);
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder of UpfMeter object. Use {@link #resetApplication(int)} and
+     * {@link #resetSession(int)} to reset the meter config.
+     */
+    public static class Builder {
+        private Integer cellId = null;
+        private Map<Band.Type, Band> bands = Maps.newHashMap();
+        private UpfEntityType type;
+
+        public Builder() {
+
+        }
+
+        /**
+         * Set the meter cell index of this meter.
+         *
+         * @param cellId the meter cell index
+         * @return this builder object
+         */
+        public Builder setCellId(int cellId) {
+            this.cellId = cellId;
+            return this;
+        }
+
+        /**
+         * Set the committed band of this meter.
+         * Valid only for meter of type application.
+         *
+         * @param cir    the Committed Information Rate in bytes/s
+         * @param cburst the Committed Burst in bytes
+         * @return this builder object
+         */
+        public Builder setCommittedBand(long cir, long cburst) {
+            this.bands.put(MARK_YELLOW,
+                           DefaultBand.builder()
+                                   .ofType(MARK_YELLOW)
+                                   .withRate(cir)
+                                   .burstSize(cburst)
+                                   .build()
+            );
+            return this;
+        }
+
+        /**
+         * Set the peak band of this meter.
+         *
+         * @param pir    the Peak Information Rate in bytes/s
+         * @param pburst the Peak Burst in bytes
+         * @return this builder object
+         */
+        public Builder setPeakBand(long pir, long pburst) {
+            this.bands.put(MARK_RED,
+                           DefaultBand.builder()
+                                   .ofType(MARK_RED)
+                                   .withRate(pir)
+                                   .burstSize(pburst)
+                                   .build()
+            );
+            return this;
+        }
+
+        /**
+         * Make this meter a session meter.
+         *
+         * @return this builder object
+         */
+        public Builder setSession() {
+            this.type = SESSION_METER;
+            return this;
+        }
+
+        /**
+         * Make this meter an application meter.
+         *
+         * @return this builder object
+         */
+        public Builder setApplication() {
+            this.type = APPLICATION_METER;
+            return this;
+        }
+
+        public UpfMeter build() {
+            checkNotNull(type, "A meter type must be assigned");
+            switch (type) {
+                case SESSION_METER:
+                    checkArgument(!bands.containsKey(MARK_YELLOW),
+                                  "Committed band can not be provided for session meter!");
+                    break;
+                case APPLICATION_METER:
+                    checkArgument((bands.containsKey(MARK_YELLOW) && bands.containsKey(MARK_RED)) || bands.isEmpty(),
+                                  "Bands (committed and peak) must be provided together or not at all!");
+                    break;
+                default:
+                    // I should never reach this point
+                    throw new IllegalArgumentException("Invalid meter type, I should never reach this point");
+            }
+            checkNotNull(cellId, "Meter cell ID must be provided!");
+            return new UpfMeter(cellId, bands, type);
+        }
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfProgrammable.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfProgrammable.java
index 052084e..2fd718c 100644
--- a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfProgrammable.java
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfProgrammable.java
@@ -19,6 +19,7 @@
 import com.google.common.annotations.Beta;
 import org.onosproject.net.driver.HandlerBehaviour;
 import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.meter.Meter;
 
 /**
  * Provides means to update the device forwarding state to implement a 3GPP
@@ -43,4 +44,12 @@
      * @return True if the given flow rule has been created by this UPF behaviour, False otherwise.
      */
     boolean fromThisUpf(FlowRule flowRule);
+
+    /**
+     * Checks if the given meter has been generated by this UPF behaviour.
+     *
+     * @param meter the meter to check
+     * @return True if the given meter has been created by this UPF behaviour, False otherwise.
+     */
+    boolean fromThisUpf(Meter meter);
 }
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfSessionDownlink.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfSessionDownlink.java
index 413c7ed..02a8a54 100644
--- a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfSessionDownlink.java
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfSessionDownlink.java
@@ -33,14 +33,17 @@
     private final Ip4Address ueAddress;
     // Action parameters
     private final Byte tunPeerId;
+    private final int sessionMeterIdx;
     private final boolean buffering;
     private final boolean dropping;
 
     private UpfSessionDownlink(Ip4Address ipv4Address,
                                Byte tunPeerId,
+                               int sessionMeterIdx,
                                boolean buffering,
                                boolean drop) {
         this.ueAddress = ipv4Address;
+        this.sessionMeterIdx = sessionMeterIdx;
         this.tunPeerId = tunPeerId;
         this.buffering = buffering;
         this.dropping = drop;
@@ -66,13 +69,14 @@
 
         return this.buffering == that.buffering &&
                 this.dropping == that.dropping &&
+                this.sessionMeterIdx == that.sessionMeterIdx &&
                 Objects.equals(ueAddress, that.ueAddress) &&
                 Objects.equals(tunPeerId, that.tunPeerId);
     }
 
     @Override
     public int hashCode() {
-        return java.util.Objects.hash(ueAddress, tunPeerId, buffering, dropping);
+        return java.util.Objects.hash(ueAddress, sessionMeterIdx, tunPeerId, buffering, dropping);
     }
 
     @Override
@@ -95,7 +99,8 @@
         } else {
             actionStrBuilder.append("FWD, ");
         }
-       return actionStrBuilder.append(" tun_peer=").append(this.tunPeerId()).append(")")
+       return actionStrBuilder.append(" tun_peer=").append(this.tunPeerId())
+               .append(", session_meter_idx=").append(this.sessionMeterIdx()).append(")")
                .toString();
     }
 
@@ -135,6 +140,15 @@
         return tunPeerId;
     }
 
+    /**
+     * Get the session meter index that is set by this UPF UE Session rule.
+     *
+     * @return Session meter index
+     */
+    public int sessionMeterIdx() {
+        return this.sessionMeterIdx;
+    }
+
     @Override
     public UpfEntityType type() {
         return UpfEntityType.SESSION_DOWNLINK;
@@ -143,6 +157,7 @@
     public static class Builder {
         private Ip4Address ueAddress = null;
         private Byte tunPeerId = null;
+        private int sessionMeterIdx = DEFAULT_SESSION_INDEX;
         private boolean buffer = false;
         private boolean drop = false;
 
@@ -151,7 +166,7 @@
         }
 
         /**
-         * Set the UE IP address that this downlink UPF UE session rule matches on.
+         * Sets the UE IP address that this downlink UPF UE session rule matches on.
          *
          * @param ueAddress UE IP address
          * @return This builder object
@@ -162,7 +177,7 @@
         }
 
         /**
-         * Set the GTP tunnel peer ID that is set by this UPF UE Session rule.
+         * Sets the GTP tunnel peer ID that is set by this UPF UE Session rule.
          *
          * @param tunnelPeerId GTP tunnel peer ID
          * @return This builder object
@@ -173,7 +188,7 @@
         }
 
         /**
-         * Set whether to buffer downlink UPF UE session traffic or not.
+         * Sets whether to buffer downlink UPF UE session traffic or not.
          *
          * @param buffer True if request to buffer, false otherwise
          * @return This builder object
@@ -184,7 +199,7 @@
         }
 
         /**
-         * Set whether to drop downlink UPF UE session traffic or not.
+         * Sets whether to drop downlink UPF UE session traffic or not.
          *
          * @param drop True if request to buffer, false otherwise
          * @return This builder object
@@ -194,10 +209,22 @@
             return this;
         }
 
+        /**
+         * Sets the meter index associated with this UE session.
+         * If not set, default to {@link UpfEntity#DEFAULT_SESSION_INDEX}.
+         *
+         * @param sessionMeterIdx Session meter index
+         * @return This builder object
+         */
+        public Builder withSessionMeterIdx(int sessionMeterIdx) {
+            this.sessionMeterIdx = sessionMeterIdx;
+            return this;
+        }
+
         public UpfSessionDownlink build() {
             // Match fields are required
             checkNotNull(ueAddress, "UE address must be provided");
-            return new UpfSessionDownlink(ueAddress, tunPeerId, buffer, drop);
+            return new UpfSessionDownlink(ueAddress, tunPeerId, sessionMeterIdx, buffer, drop);
         }
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfSessionUplink.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfSessionUplink.java
index 0a6bfa4..7cdff7e 100644
--- a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfSessionUplink.java
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfSessionUplink.java
@@ -35,12 +35,15 @@
 
     // Action parameters
     private final boolean dropping; // Used to convey dropping information
+    private final int sessionMeterIdx;
 
     private UpfSessionUplink(Ip4Address tunDestAddr,
                              Integer teid,
+                             int sessionMeterIdx,
                              boolean drop) {
         this.tunDestAddr = tunDestAddr;
         this.teid = teid;
+        this.sessionMeterIdx = sessionMeterIdx;
         this.dropping = drop;
     }
 
@@ -63,13 +66,14 @@
         UpfSessionUplink that = (UpfSessionUplink) object;
 
         return this.dropping == that.dropping &&
+                this.sessionMeterIdx == that.sessionMeterIdx &&
                 Objects.equals(tunDestAddr, that.tunDestAddr) &&
                 Objects.equals(teid, that.teid);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(tunDestAddr, teid, dropping);
+        return Objects.hash(tunDestAddr, teid, sessionMeterIdx, dropping);
     }
 
     @Override
@@ -89,7 +93,9 @@
         } else {
             actionStrBuilder.append("FWD");
         }
-        return actionStrBuilder.append(")").toString();
+        return actionStrBuilder
+                .append(", session_meter_idx=").append(this.sessionMeterIdx())
+                .append(")").toString();
     }
 
     /**
@@ -119,6 +125,15 @@
         return teid;
     }
 
+    /**
+     * Get the session meter index that is set by this UPF UE Session rule.
+     *
+     * @return Session meter index
+     */
+    public int sessionMeterIdx() {
+        return this.sessionMeterIdx;
+    }
+
     @Override
     public UpfEntityType type() {
         return UpfEntityType.SESSION_UPLINK;
@@ -127,6 +142,7 @@
     public static class Builder {
         private Ip4Address tunDstAddr = null;
         private Integer teid = null;
+        public int sessionMeterIdx = DEFAULT_SESSION_INDEX;
         private boolean drop = false;
 
         public Builder() {
@@ -134,7 +150,7 @@
         }
 
         /**
-         * Set the tunnel destination IP address (N3/S1U address) that this UPF UE Session rule matches on.
+         * Sets the tunnel destination IP address (N3/S1U address) that this UPF UE Session rule matches on.
          *
          * @param tunDstAddr The tunnel destination IP address
          * @return This builder object
@@ -145,7 +161,7 @@
         }
 
         /**
-         * Set the identifier of the GTP tunnel that this UPF UE Session rule matches on.
+         * Sets the identifier of the GTP tunnel that this UPF UE Session rule matches on.
          *
          * @param teid GTP tunnel ID
          * @return This builder object
@@ -167,11 +183,23 @@
             return this;
         }
 
+        /**
+         * Sets the meter index associated with this UE session.
+         * If not set, default to {@link UpfEntity#DEFAULT_SESSION_INDEX}.
+         *
+         * @param sessionMeterIdx Session meter index
+         * @return This builder object
+         */
+        public Builder withSessionMeterIdx(int sessionMeterIdx) {
+            this.sessionMeterIdx = sessionMeterIdx;
+            return this;
+        }
+
         public UpfSessionUplink build() {
             // Match keys are required.
             checkNotNull(tunDstAddr, "Tunnel destination must be provided");
             checkNotNull(teid, "TEID must be provided");
-            return new UpfSessionUplink(tunDstAddr, teid, drop);
+            return new UpfSessionUplink(tunDstAddr, teid, sessionMeterIdx, drop);
         }
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfTerminationDownlink.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfTerminationDownlink.java
index edab0ac..dd66724 100644
--- a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfTerminationDownlink.java
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfTerminationDownlink.java
@@ -38,16 +38,18 @@
     private final Byte trafficClass;
     private final Integer teid;  // Tunnel Endpoint Identifier
     private final Byte qfi; // QoS Flow Identifier
+    private final int appMeterIdx;
     private final boolean dropping;
 
     private UpfTerminationDownlink(Ip4Address ueSessionId, byte applicationId, Integer ctrId, Byte trafficClass,
-                                   Integer teid, Byte qfi, boolean dropping) {
+                                   Integer teid, Byte qfi, int appMeterIdx, boolean dropping) {
         this.ueSessionId = ueSessionId;
         this.applicationId = applicationId;
         this.ctrId = ctrId;
         this.trafficClass = trafficClass;
         this.teid = teid;
         this.qfi = qfi;
+        this.appMeterIdx = appMeterIdx;
         this.dropping = dropping;
     }
 
@@ -75,12 +77,13 @@
                 Objects.equals(this.ctrId, that.ctrId) &&
                 Objects.equals(this.trafficClass, that.trafficClass) &&
                 Objects.equals(this.teid, that.teid) &&
+                this.appMeterIdx == that.appMeterIdx &&
                 Objects.equals(this.qfi, that.qfi);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(ueSessionId, applicationId, ctrId, trafficClass, teid, qfi, dropping);
+        return Objects.hash(ueSessionId, applicationId, ctrId, trafficClass, teid, qfi, appMeterIdx, dropping);
     }
 
     /**
@@ -146,6 +149,15 @@
         return dropping;
     }
 
+    /**
+     * Get the app meter index set by this UPF Termination rule.
+     *
+     * @return App meter index
+     */
+    public int appMeterIdx() {
+        return appMeterIdx;
+    }
+
     @Override
     public UpfEntityType type() {
         return UpfEntityType.TERMINATION_DOWNLINK;
@@ -170,6 +182,7 @@
                 ", ctr_id=" + this.counterId() +
                 ", qfi=" + this.qfi() +
                 ", tc=" + this.trafficClass() +
+                ", app_meter_idx=" + this.appMeterIdx() +
                 ")";
     }
 
@@ -180,6 +193,7 @@
         private Byte trafficClass = null;
         private Integer teid = null;
         private Byte qfi = null;
+        private int appMeterIdx = DEFAULT_APP_INDEX;
         private boolean drop = false;
 
         public Builder() {
@@ -199,6 +213,7 @@
 
         /**
          * Set the ID of the application.
+         * If not set, default to {@link UpfEntity#DEFAULT_APP_ID}.
          *
          * @param applicationId Application ID
          * @return This builder object
@@ -263,6 +278,18 @@
             return this;
         }
 
+        /**
+         * Sets the app meter index.
+         * If not set, default to {@link UpfEntity#DEFAULT_APP_INDEX}.
+         *
+         * @param appMeterIdx App meter index
+         * @return This builder object
+         */
+        public Builder withAppMeterIdx(int appMeterIdx) {
+            this.appMeterIdx = appMeterIdx;
+            return this;
+        }
+
         public UpfTerminationDownlink build() {
             // Match fields must be provided
             checkNotNull(ueSessionId, "UE session ID must be provided");
@@ -273,7 +300,7 @@
             // TODO: should we verify that when dropping no other fields are provided
             return new UpfTerminationDownlink(
                     this.ueSessionId, this.applicationId, this.ctrId, this.trafficClass,
-                    this.teid, this.qfi, this.drop
+                    this.teid, this.qfi, this.appMeterIdx, this.drop
             );
         }
 
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfTerminationUplink.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfTerminationUplink.java
index 93ac0b7..ddec2e4 100644
--- a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfTerminationUplink.java
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfTerminationUplink.java
@@ -36,14 +36,17 @@
     // Action parameters
     private final Integer ctrId;  // Counter ID unique to this UPF Termination Rule
     private final Byte trafficClass;
+    private final int appMeterIdx;
     private final boolean dropping;
 
-    private UpfTerminationUplink(Ip4Address ueSessionId, byte applicationId, Integer ctrId, Byte trafficClass,
-                                 boolean dropping) {
+    private UpfTerminationUplink(Ip4Address ueSessionId, byte applicationId,
+                                 Integer ctrId, Byte trafficClass,
+                                 int appMeterIdx, boolean dropping) {
         this.ueSessionId = ueSessionId;
         this.applicationId = applicationId;
         this.ctrId = ctrId;
         this.trafficClass = trafficClass;
+        this.appMeterIdx = appMeterIdx;
         this.dropping = dropping;
     }
 
@@ -69,12 +72,13 @@
                 Objects.equals(this.ueSessionId, that.ueSessionId) &&
                 Objects.equals(this.applicationId, that.applicationId) &&
                 Objects.equals(this.ctrId, that.ctrId) &&
+                this.appMeterIdx == that.appMeterIdx &&
                 Objects.equals(this.trafficClass, that.trafficClass);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(ueSessionId, applicationId, ctrId, trafficClass, dropping);
+        return Objects.hash(ueSessionId, applicationId, ctrId, trafficClass, appMeterIdx, dropping);
     }
 
     /**
@@ -122,6 +126,15 @@
         return dropping;
     }
 
+    /**
+     * Get the app meter index set by this UPF Termination rule.
+     *
+     * @return App meter index
+     */
+    public int appMeterIdx() {
+        return appMeterIdx;
+    }
+
     @Override
     public UpfEntityType type() {
         return UpfEntityType.TERMINATION_UPLINK;
@@ -144,6 +157,7 @@
         return "Action(" + fwd +
                 ", ctr_id=" + this.counterId() +
                 ", tc=" + this.trafficClass() +
+                ", app_meter_idx=" + this.appMeterIdx() +
                 ")";
     }
 
@@ -152,6 +166,7 @@
         private Byte applicationId = null;
         private Integer ctrId = null;
         private Byte trafficClass = null;
+        private int appMeterIdx = DEFAULT_APP_INDEX;
         private boolean dropping = false;
 
         public Builder() {
@@ -171,6 +186,7 @@
 
         /**
          * Set the ID of the application.
+         * If not set, default to {@link UpfEntity#DEFAULT_APP_ID}.
          *
          * @param applicationId Application ID
          * @return This builder object
@@ -213,6 +229,18 @@
             return this;
         }
 
+        /**
+         * Sets the app meter index.
+         * If not set, default to {@link UpfEntity#DEFAULT_APP_INDEX}.
+         *
+         * @param appMeterIdx App meter index
+         * @return This builder object
+         */
+        public Builder withAppMeterIdx(int appMeterIdx) {
+            this.appMeterIdx = appMeterIdx;
+            return this;
+        }
+
         public UpfTerminationUplink build() {
             // Match fields must be provided
             checkNotNull(ueSessionId, "UE session ID must be provided");
@@ -223,7 +251,7 @@
             // TODO: should we verify that when dropping no other fields are provided
             return new UpfTerminationUplink(
                     this.ueSessionId, this.applicationId, this.ctrId,
-                    this.trafficClass, this.dropping
+                    this.trafficClass, this.appMeterIdx, this.dropping
             );
         }
 
diff --git a/core/api/src/main/java/org/onosproject/net/meter/DefaultBand.java b/core/api/src/main/java/org/onosproject/net/meter/DefaultBand.java
index e0069a5..85e6c70 100644
--- a/core/api/src/main/java/org/onosproject/net/meter/DefaultBand.java
+++ b/core/api/src/main/java/org/onosproject/net/meter/DefaultBand.java
@@ -15,6 +15,8 @@
  */
 package org.onosproject.net.meter;
 
+import com.google.common.base.Objects;
+
 import static com.google.common.base.MoreObjects.toStringHelper;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -96,6 +98,26 @@
         return new Builder();
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        DefaultBand that = (DefaultBand) o;
+        return rate == that.rate &&
+                type == that.type &&
+                Objects.equal(burstSize, that.burstSize) &&
+                Objects.equal(prec, that.prec);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(type, rate, burstSize, prec);
+    }
+
     public static final class Builder implements Band.Builder {
 
         private long rate;