Implement L2 load balancer support in XConnectManager

Change-Id: Ib310a1dde72db38abb60273ce66b5f72768bf4ca
diff --git a/apps/segmentrouting/BUCK b/apps/segmentrouting/BUCK
index 1b3990f..f56834e 100644
--- a/apps/segmentrouting/BUCK
+++ b/apps/segmentrouting/BUCK
@@ -9,5 +9,9 @@
     url = 'http://onosproject.org',
     included_bundles = BUNDLES,
     description = 'Segment routing application.',
-    required_apps = [ 'org.onosproject.route-service', 'org.onosproject.mcast' ],
+    required_apps = [
+        'org.onosproject.route-service',
+        'org.onosproject.mcast',
+        'org.onosproject.l2lb',
+    ],
 )
diff --git a/apps/segmentrouting/BUILD b/apps/segmentrouting/BUILD
index a6c8aca..d3846f3 100644
--- a/apps/segmentrouting/BUILD
+++ b/apps/segmentrouting/BUILD
@@ -10,6 +10,7 @@
     required_apps = [
         "org.onosproject.route-service",
         "org.onosproject.mcast",
+        "org.onosproject.l2lb",
     ],
     title = "Segment Routing",
     url = "http://onosproject.org",
diff --git a/apps/segmentrouting/app/BUCK b/apps/segmentrouting/app/BUCK
index 7deda13..9579ffe 100644
--- a/apps/segmentrouting/app/BUCK
+++ b/apps/segmentrouting/app/BUCK
@@ -10,6 +10,7 @@
     '//apps/route-service/api:onos-apps-route-service-api',
     '//apps/mcast/api:onos-apps-mcast-api',
     '//apps/mcast/cli:onos-apps-mcast-cli',
+    '//apps/l2lb:onos-apps-l2lb',
 ]
 
 TEST_DEPS = [
diff --git a/apps/segmentrouting/app/BUILD b/apps/segmentrouting/app/BUILD
index 9edce3e..081c645 100644
--- a/apps/segmentrouting/app/BUILD
+++ b/apps/segmentrouting/app/BUILD
@@ -5,6 +5,7 @@
     "//apps/route-service/api:onos-apps-route-service-api",
     "//apps/mcast/api:onos-apps-mcast-api",
     "//apps/mcast/cli:onos-apps-mcast-cli",
+    "//apps/l2lb:onos-apps-l2lb",
 ]
 
 TEST_DEPS = TEST_ADAPTERS + [
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java
index a1ad929..0ff6195 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java
@@ -21,11 +21,12 @@
 import org.onlab.packet.VlanId;
 import org.onosproject.cli.AbstractShellCommand;
 import org.onosproject.net.DeviceId;
-import org.onosproject.net.PortNumber;
 import org.onosproject.segmentrouting.xconnect.api.XconnectService;
 
 import java.util.Set;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 /**
  * Creates Xconnect.
  */
@@ -42,23 +43,25 @@
     private String vlanIdStr;
 
     @Argument(index = 2, name = "port1",
-            description = "Port 1",
+            description = "Port 1. Can also specify L2 load balancer by L2LB(<key>)",
             required = true, multiValued = false)
     private String port1Str;
 
     @Argument(index = 3, name = "port2",
-            description = "Port 2",
+            description = "Port 2. Can also specify L2 load balancer by L2LB(<key>)",
             required = true, multiValued = false)
     private String port2Str;
 
+    private static final String L2LB_PATTERN = "^(\\d*|L2LB\\(\\d*\\))$";
 
     @Override
     protected void execute() {
         DeviceId deviceId = DeviceId.deviceId(deviceIdStr);
         VlanId vlanId = VlanId.vlanId(vlanIdStr);
-        PortNumber port1 = PortNumber.portNumber(port1Str);
-        PortNumber port2 = PortNumber.portNumber(port2Str);
-        Set<PortNumber> ports = Sets.newHashSet(port1, port2);
+        Set<String> ports = Sets.newHashSet(port1Str, port2Str);
+
+        checkArgument(port1Str.matches(L2LB_PATTERN), "Wrong L2 load balancer format " + port1Str);
+        checkArgument(port2Str.matches(L2LB_PATTERN), "Wrong L2 load balancer format " + port2Str);
 
         XconnectService xconnectService = get(XconnectService.class);
         xconnectService.addOrUpdateXconnect(deviceId, vlanId, ports);
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java
index 77824e8..08558a8 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java
@@ -23,7 +23,6 @@
 import org.onosproject.codec.CodecContext;
 import org.onosproject.codec.JsonCodec;
 import org.onosproject.net.DeviceId;
-import org.onosproject.net.PortNumber;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -52,10 +51,10 @@
         DeviceId deviceId = DeviceId.deviceId(json.path(DEVICE_ID).asText());
         VlanId vlanId = VlanId.vlanId(json.path(VLAN_ID).asText());
 
-        Set<PortNumber> ports = Sets.newHashSet();
+        Set<String> ports = Sets.newHashSet();
         JsonNode portNodes = json.get(PORTS);
         if (portNodes != null) {
-            portNodes.forEach(portNode -> ports.add(PortNumber.portNumber(portNode.asInt())));
+            portNodes.forEach(portNode -> ports.add(portNode.asText()));
         }
 
         XconnectKey key = new XconnectKey(deviceId, vlanId);
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java
index 0eead31..e94c1db 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java
@@ -16,7 +16,6 @@
 package org.onosproject.segmentrouting.xconnect.api;
 
 import com.google.common.base.MoreObjects;
-import org.onosproject.net.PortNumber;
 
 import java.util.Objects;
 import java.util.Set;
@@ -26,7 +25,7 @@
  */
 public class XconnectDesc {
     private XconnectKey key;
-    private Set<PortNumber> ports;
+    private Set<String> ports;
 
     /**
      * Constructs new Xconnect description with given device ID and VLAN ID.
@@ -34,7 +33,7 @@
      * @param key Xconnect key
      * @param ports set of ports
      */
-    public XconnectDesc(XconnectKey key, Set<PortNumber> ports) {
+    public XconnectDesc(XconnectKey key, Set<String> ports) {
         this.key = key;
         this.ports = ports;
     }
@@ -53,7 +52,7 @@
      *
      * @return set of ports
      */
-    public Set<PortNumber> ports() {
+    public Set<String> ports() {
         return ports;
     }
 
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
index 4e82524..ec47180 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
@@ -48,7 +48,7 @@
      * @param vlanId VLAN ID
      * @param ports set of ports
      */
-    void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<PortNumber> ports);
+    void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<String> ports);
 
     /**
      * Deletes Xconnect.
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
index 1f41fda..ea34ff2 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
@@ -31,6 +31,7 @@
 import org.onosproject.codec.CodecService;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
+import org.onosproject.l2lb.api.L2LbService;
 import org.onosproject.mastership.MastershipService;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
@@ -49,11 +50,14 @@
 import org.onosproject.net.flowobjective.DefaultFilteringObjective;
 import org.onosproject.net.flowobjective.DefaultForwardingObjective;
 import org.onosproject.net.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.DefaultNextTreatment;
 import org.onosproject.net.flowobjective.DefaultObjectiveContext;
 import org.onosproject.net.flowobjective.FilteringObjective;
 import org.onosproject.net.flowobjective.FlowObjectiveService;
 import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.IdNextTreatment;
 import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.NextTreatment;
 import org.onosproject.net.flowobjective.Objective;
 import org.onosproject.net.flowobjective.ObjectiveContext;
 import org.onosproject.net.flowobjective.ObjectiveError;
@@ -126,19 +130,24 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     HostService hostService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    L2LbService l2LbService;
+
     private static final String APP_NAME = "org.onosproject.xconnect";
     private static final String ERROR_NOT_MASTER = "Not master controller";
+    private static final String ERROR_NEXT_OBJ_BUILDER = "Unable to construct next objective builder";
+    private static final String ERROR_NEXT_ID = "Unable to get next id";
 
     private static Logger log = LoggerFactory.getLogger(XconnectManager.class);
 
     private ApplicationId appId;
-    private ConsistentMap<XconnectKey, Set<PortNumber>> xconnectStore;
+    private ConsistentMap<XconnectKey, Set<String>> xconnectStore;
     private ConsistentMap<XconnectKey, Integer> xconnectNextObjStore;
 
     private ConsistentMap<VlanNextObjectiveStoreKey, Integer> xconnectMulticastNextStore;
     private ConsistentMap<VlanNextObjectiveStoreKey, List<PortNumber>> xconnectMulticastPortsStore;
 
-    private final MapEventListener<XconnectKey, Set<PortNumber>> xconnectListener = new XconnectMapListener();
+    private final MapEventListener<XconnectKey, Set<String>> xconnectListener = new XconnectMapListener();
     private final DeviceListener deviceListener = new InternalDeviceListener();
 
     private ExecutorService deviceEventExecutor;
@@ -158,7 +167,7 @@
                 .register(XconnectKey.class)
                 .register(VlanNextObjectiveStoreKey.class);
 
-        xconnectStore = storageService.<XconnectKey, Set<PortNumber>>consistentMapBuilder()
+        xconnectStore = storageService.<XconnectKey, Set<String>>consistentMapBuilder()
                 .withName("onos-sr-xconnect")
                 .withRelaxedReadConsistency()
                 .withSerializer(Serializer.using(serializer.build()))
@@ -207,7 +216,7 @@
     }
 
     @Override
-    public void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<PortNumber> ports) {
+    public void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<String> ports) {
         log.info("Adding or updating xconnect. deviceId={}, vlanId={}, ports={}",
                  deviceId, vlanId, ports);
         final XconnectKey key = new XconnectKey(deviceId, vlanId);
@@ -238,15 +247,14 @@
     @Override
     public boolean hasXconnect(ConnectPoint cp) {
         return getXconnects().stream().anyMatch(desc ->
-                                                        desc.key().deviceId().equals(cp.deviceId())
-                                                                && desc.ports().contains(cp.port())
+                desc.key().deviceId().equals(cp.deviceId()) && desc.ports().contains(cp.port().toString())
         );
     }
 
     @Override
     public List<VlanId> getXconnectVlans(DeviceId deviceId, PortNumber port) {
         return getXconnects().stream()
-                .filter(desc -> desc.key().deviceId().equals(deviceId) && desc.ports().contains(port))
+                .filter(desc -> desc.key().deviceId().equals(deviceId) && desc.ports().contains(port.toString()))
                 .map(XconnectDesc::key)
                 .map(XconnectKey::vlanId)
                 .collect(Collectors.toList());
@@ -285,22 +293,22 @@
         });
     }
 
-    private class XconnectMapListener implements MapEventListener<XconnectKey, Set<PortNumber>> {
+    private class XconnectMapListener implements MapEventListener<XconnectKey, Set<String>> {
         @Override
-        public void event(MapEvent<XconnectKey, Set<PortNumber>> event) {
+        public void event(MapEvent<XconnectKey, Set<String>> event) {
             XconnectKey key = event.key();
-            Versioned<Set<PortNumber>> ports = event.newValue();
-            Versioned<Set<PortNumber>> oldPorts = event.oldValue();
+            Set<String> ports = Versioned.valueOrNull(event.newValue());
+            Set<String> oldPorts = Versioned.valueOrNull(event.oldValue());
 
             switch (event.type()) {
                 case INSERT:
-                    populateXConnect(key, ports.value());
+                    populateXConnect(key, ports);
                     break;
                 case UPDATE:
-                    updateXConnect(key, oldPorts.value(), ports.value());
+                    updateXConnect(key, oldPorts, ports);
                     break;
                 case REMOVE:
-                    revokeXConnect(key, oldPorts.value());
+                    revokeXConnect(key, oldPorts);
                     break;
                 default:
                     break;
@@ -445,14 +453,19 @@
      * @param key   XConnect key
      * @param ports a set of ports to be cross-connected
      */
-    private void populateXConnect(XconnectKey key, Set<PortNumber> ports) {
+    private void populateXConnect(XconnectKey key, Set<String> ports) {
         if (!mastershipService.isLocalMaster(key.deviceId())) {
             log.info("Abort populating XConnect {}: {}", key, ERROR_NOT_MASTER);
             return;
         }
 
+        int nextId = populateNext(key, ports);
+        if (nextId == -1) {
+            log.warn("Fail to populateXConnect {}: {}", key, ERROR_NEXT_ID);
+            return;
+        }
         populateFilter(key, ports);
-        populateFwd(key, populateNext(key, ports));
+        populateFwd(key, nextId);
         populateAcl(key);
     }
 
@@ -462,15 +475,24 @@
      * @param key   XConnect store key
      * @param ports XConnect ports
      */
-    private void populateFilter(XconnectKey key, Set<PortNumber> ports) {
-        ports.forEach(port -> {
-            FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port);
+    private void populateFilter(XconnectKey key, Set<String> ports) {
+        // FIXME Improve the logic
+        //       If L2 load balancer is not involved, use filtered port. Otherwise, use unfiltered port.
+        //       The purpose is to make sure existing XConnect logic can still work on a configured port.
+        boolean filtered = ports.stream()
+                .map(p -> getNextTreatment(key.deviceId(), p))
+                .allMatch(t -> t.type().equals(NextTreatment.Type.TREATMENT));
+
+        ports.stream()
+                .map(p -> getPhysicalPorts(key.deviceId(), p))
+                .flatMap(Set::stream).forEach(port -> {
+            FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port, filtered);
             ObjectiveContext context = new DefaultObjectiveContext(
                     (objective) -> log.debug("XConnect FilterObj for {} on port {} populated",
-                                             key, port),
+                            key, port),
                     (objective, error) ->
                             log.warn("Failed to populate XConnect FilterObj for {} on port {}: {}",
-                                     key, port, error));
+                                    key, port, error));
             flowObjectiveService.filter(key.deviceId(), filtObjBuilder.add(context));
         });
     }
@@ -480,14 +502,19 @@
      *
      * @param key   XConnect store key
      * @param ports XConnect ports
+     * @return next id
      */
-    private int populateNext(XconnectKey key, Set<PortNumber> ports) {
+    private int populateNext(XconnectKey key, Set<String> ports) {
         if (xconnectNextObjStore.containsKey(key)) {
             int nextId = xconnectNextObjStore.get(key).value();
             log.debug("NextObj for {} found, id={}", key, nextId);
             return nextId;
         } else {
             NextObjective.Builder nextObjBuilder = nextObjBuilder(key, ports);
+            if (nextObjBuilder == null) {
+                log.warn("Fail to populate {}: {}", key, ERROR_NEXT_OBJ_BUILDER);
+                return -1;
+            }
             ObjectiveContext nextContext = new DefaultObjectiveContext(
                     // To serialize this with kryo
                     (Serializable & Consumer<Objective>) (objective) ->
@@ -539,13 +566,12 @@
      * @param key   XConnect key
      * @param ports XConnect ports
      */
-    private void revokeXConnect(XconnectKey key, Set<PortNumber> ports) {
+    private void revokeXConnect(XconnectKey key, Set<String> ports) {
         if (!mastershipService.isLocalMaster(key.deviceId())) {
             log.info("Abort populating XConnect {}: {}", key, ERROR_NOT_MASTER);
             return;
         }
 
-        revokeFilter(key, ports);
         if (xconnectNextObjStore.containsKey(key)) {
             int nextId = xconnectNextObjStore.get(key).value();
             revokeFwd(key, nextId, null);
@@ -553,6 +579,7 @@
         } else {
             log.warn("NextObj for {} does not exist in the store.", key);
         }
+        revokeFilter(key, ports);
         revokeAcl(key);
     }
 
@@ -562,9 +589,15 @@
      * @param key   XConnect store key
      * @param ports XConnect ports
      */
-    private void revokeFilter(XconnectKey key, Set<PortNumber> ports) {
-        ports.forEach(port -> {
-            FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port);
+    private void revokeFilter(XconnectKey key, Set<String> ports) {
+        // FIXME Improve the logic
+        //       If L2 load balancer is not involved, use filtered port. Otherwise, use unfiltered port.
+        //       The purpose is to make sure existing XConnect logic can still work on a configured port.
+        Set<Set<PortNumber>> portsSet = ports.stream()
+                .map(p -> getPhysicalPorts(key.deviceId(), p)).collect(Collectors.toSet());
+        boolean filtered = portsSet.stream().allMatch(s -> s.size() == 1);
+        portsSet.stream().flatMap(Set::stream).forEach(port -> {
+            FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port, filtered);
             ObjectiveContext context = new DefaultObjectiveContext(
                     (objective) -> log.debug("XConnect FilterObj for {} on port {} revoked",
                                              key, port),
@@ -583,7 +616,7 @@
      * @param nextId     next objective id
      * @param nextFuture completable future for this next objective operation
      */
-    private void revokeNext(XconnectKey key, Set<PortNumber> ports, int nextId,
+    private void revokeNext(XconnectKey key, Set<String> ports, int nextId,
                             CompletableFuture<ObjectiveError> nextFuture) {
         ObjectiveContext context = new ObjectiveContext() {
             @Override
@@ -603,7 +636,13 @@
                 srService.invalidateNextObj(objective.id());
             }
         };
-        flowObjectiveService.next(key.deviceId(), nextObjBuilder(key, ports, nextId).remove(context));
+
+        NextObjective.Builder nextObjBuilder = nextObjBuilder(key, ports, nextId);
+        if (nextObjBuilder == null) {
+            log.warn("Fail to revokeNext {}: {}", key, ERROR_NEXT_OBJ_BUILDER);
+            return;
+        }
+        flowObjectiveService.next(key.deviceId(), nextObjBuilder.remove(context));
         xconnectNextObjStore.remove(key);
     }
 
@@ -657,19 +696,17 @@
      * @param prevPorts previous XConnect ports
      * @param ports     new XConnect ports
      */
-    private void updateXConnect(XconnectKey key, Set<PortNumber> prevPorts,
-                                Set<PortNumber> ports) {
+    private void updateXConnect(XconnectKey key, Set<String> prevPorts,
+                                Set<String> ports) {
         // NOTE: ACL flow doesn't include port information. No need to update it.
         //       Pair port is built-in and thus not going to change. No need to update it.
 
         // remove old filter
         prevPorts.stream().filter(port -> !ports.contains(port)).forEach(port ->
-                                                                                 revokeFilter(key,
-                                                                                              ImmutableSet.of(port)));
+                revokeFilter(key, ImmutableSet.of(port)));
         // install new filter
         ports.stream().filter(port -> !prevPorts.contains(port)).forEach(port ->
-                                                                                 populateFilter(key,
-                                                                                                ImmutableSet.of(port)));
+                populateFilter(key, ImmutableSet.of(port)));
 
         CompletableFuture<ObjectiveError> fwdFuture = new CompletableFuture<>();
         CompletableFuture<ObjectiveError> nextFuture = new CompletableFuture<>();
@@ -688,7 +725,12 @@
             nextFuture.thenAcceptAsync(nextStatus -> {
                 if (nextStatus == null) {
                     log.debug("Installing new group and flow for {}", key);
-                    populateFwd(key, populateNext(key, ports));
+                    int newNextId = populateNext(key, ports);
+                    if (newNextId == -1) {
+                        log.warn("Fail to updateXConnect {}: {}", key, ERROR_NEXT_ID);
+                        return;
+                    }
+                    populateFwd(key, newNextId);
                 }
             });
         } else {
@@ -699,23 +741,28 @@
     /**
      * Creates a next objective builder for XConnect with given nextId.
      *
-     * @param key    XConnect key
-     * @param ports  set of XConnect ports
-     * @param nextId next objective id
+     * @param key     XConnect key
+     * @param ports   ports or L2 load balancer key
+     * @param nextId  next objective id
      * @return next objective builder
      */
-    private NextObjective.Builder nextObjBuilder(XconnectKey key, Set<PortNumber> ports, int nextId) {
+    private NextObjective.Builder nextObjBuilder(XconnectKey key, Set<String> ports, int nextId) {
         TrafficSelector metadata =
                 DefaultTrafficSelector.builder().matchVlanId(key.vlanId()).build();
         NextObjective.Builder nextObjBuilder = DefaultNextObjective
                 .builder().withId(nextId)
                 .withType(NextObjective.Type.BROADCAST).fromApp(appId)
                 .withMeta(metadata);
-        ports.forEach(port -> {
-            TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
-            tBuilder.setOutput(port);
-            nextObjBuilder.addTreatment(tBuilder.build());
-        });
+
+        for (String port : ports) {
+            NextTreatment nextTreatment = getNextTreatment(key.deviceId(), port);
+            if (nextTreatment == null) {
+                log.warn("Unable to create nextObj. Null NextTreatment");
+                return null;
+            }
+            nextObjBuilder.addTreatment(nextTreatment);
+        }
+
         return nextObjBuilder;
     }
 
@@ -726,7 +773,7 @@
      * @param ports set of XConnect ports
      * @return next objective builder
      */
-    private NextObjective.Builder nextObjBuilder(XconnectKey key, Set<PortNumber> ports) {
+    private NextObjective.Builder nextObjBuilder(XconnectKey key, Set<String> ports) {
         int nextId = flowObjectiveService.allocateNextId();
         return nextObjBuilder(key, ports, nextId);
     }
@@ -785,14 +832,19 @@
      *
      * @param key  XConnect key
      * @param port XConnect ports
+     * @param filtered true if this is a filtered port
      * @return next objective builder
      */
-    private FilteringObjective.Builder filterObjBuilder(XconnectKey key, PortNumber port) {
+    private FilteringObjective.Builder filterObjBuilder(XconnectKey key, PortNumber port, boolean filtered) {
         FilteringObjective.Builder fob = DefaultFilteringObjective.builder();
         fob.withKey(Criteria.matchInPort(port))
-                .addCondition(Criteria.matchVlanId(key.vlanId()))
                 .addCondition(Criteria.matchEthDst(MacAddress.NONE))
                 .withPriority(XCONNECT_PRIORITY);
+        if (filtered) {
+            fob.addCondition(Criteria.matchVlanId(key.vlanId()));
+        } else {
+            fob.addCondition(Criteria.matchVlanId(VlanId.ANY));
+        }
         return fob.permit().fromApp(appId);
     }
 
@@ -1113,4 +1165,46 @@
         return ports.stream().anyMatch(p -> !p.equals(pairPort));
     }
 
+    private Set<PortNumber> getPhysicalPorts(DeviceId deviceId, String port) {
+        // If port is numeric, treat it as regular port.
+        // Otherwise try to parse it as load balancer key and get the physical port the LB maps to.
+        try {
+            return Sets.newHashSet(PortNumber.portNumber(Integer.parseInt(port)));
+        } catch (NumberFormatException e) {
+            log.debug("Port {} is not numeric. Try to parse it as load balancer key", port);
+        }
+
+        String l2LbKey = port.substring("L2LB(".length(), port.length() - 1);
+        try {
+            return Sets.newHashSet(l2LbService.getL2Lb(deviceId, Integer.parseInt(l2LbKey)).ports());
+        } catch (NumberFormatException e) {
+            log.debug("Port {} is not load balancer key either. Ignore", port);
+        } catch (NullPointerException e) {
+            log.debug("L2 load balancer {} not found. Ignore", l2LbKey);
+        }
+
+        return Sets.newHashSet();
+    }
+
+    private NextTreatment getNextTreatment(DeviceId deviceId, String port) {
+        // If port is numeric, treat it as regular port.
+        // Otherwise try to parse it as load balancer key and get the physical port the LB maps to.
+        try {
+            PortNumber portNumber = PortNumber.portNumber(Integer.parseInt(port));
+            return DefaultNextTreatment.of(DefaultTrafficTreatment.builder().setOutput(portNumber).build());
+        } catch (NumberFormatException e) {
+            log.debug("Port {} is not numeric. Try to parse it as load balancer key", port);
+        }
+
+        String l2LbKey = port.substring("L2LB(".length(), port.length() - 1);
+        try {
+            return IdNextTreatment.of(l2LbService.getL2LbNexts(deviceId, Integer.parseInt(l2LbKey)));
+        } catch (NumberFormatException e) {
+            log.debug("Port {} is not load balancer key either. Ignore", port);
+        } catch (NullPointerException e) {
+            log.debug("L2 load balancer {} not found. Ignore", l2LbKey);
+        }
+
+        return null;
+    }
 }