Implement L2 load balancing service

Including event/listener, CLI support

Change-Id: I26f1da578a72f5b3ead413aa5155233fbf9ab2b6
diff --git a/apps/l2lb/BUCK b/apps/l2lb/BUCK
new file mode 100644
index 0000000..d293b2d
--- /dev/null
+++ b/apps/l2lb/BUCK
@@ -0,0 +1,23 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:KRYO',
+    '//cli:onos-cli',
+    '//core/store/serializers:onos-core-serializers',
+]
+
+TEST_DEPS = [
+    '//lib:TEST_ADAPTERS',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+    test_deps = TEST_DEPS,
+)
+
+onos_app (
+    app_name = 'org.onosproject.l2lb',
+    category = 'Utilities',
+    description = 'L2 Load Balance Service',
+    title = 'L2 Load Balance Service',
+    url = 'http://onosproject.org',
+)
diff --git a/apps/l2lb/BUILD b/apps/l2lb/BUILD
new file mode 100644
index 0000000..f02e2f8
--- /dev/null
+++ b/apps/l2lb/BUILD
@@ -0,0 +1,19 @@
+COMPILE_DEPS = CORE_DEPS + KRYO + CLI + [
+    "//core/store/serializers:onos-core-serializers",
+]
+
+TEST_DEPS = TEST_ADAPTERS
+
+osgi_jar_with_tests(
+    karaf_command_packages = ["org.onosproject.l2lb.cli"],
+    test_deps = TEST_DEPS,
+    deps = COMPILE_DEPS,
+)
+
+onos_app(
+    app_name = "org.onosproject.l2lb",
+    category = "Utilities",
+    description = "L2 Load Balance Service",
+    title = "L2 Load Balance Service",
+    url = "http://onosproject.org",
+)
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2Lb.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2Lb.java
new file mode 100644
index 0000000..3c377ff
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2Lb.java
@@ -0,0 +1,101 @@
+/*
+ * 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.l2lb.api;
+
+import org.onosproject.net.PortNumber;
+
+import java.util.Objects;
+import java.util.Set;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Represents L2 load balancer information.
+ */
+public class L2Lb {
+    private L2LbId l2LbId;
+    private Set<PortNumber> ports;
+    private L2LbMode mode;
+
+    /**
+     * Constructs a L2 load balancer.
+     *
+     * @param l2LbId L2 load balancer ID
+     * @param ports Set of member ports
+     * @param mode L2 load balancer mode
+     */
+    public L2Lb(L2LbId l2LbId, Set<PortNumber> ports, L2LbMode mode) {
+        this.l2LbId = l2LbId;
+        this.ports = ports;
+        this.mode = mode;
+    }
+
+    /**
+     * Gets L2 load balancer ID.
+     *
+     * @return L2 load balancer ID
+     */
+    public L2LbId l2LbId() {
+        return l2LbId;
+    }
+
+    /**
+     * Gets set of member ports.
+     *
+     * @return Set of member ports
+     */
+    public Set<PortNumber> ports() {
+        return ports;
+    }
+
+    /**
+     * Gets L2 load balancer mode.
+     *
+     * @return L2 load balancer mode.
+     */
+    public L2LbMode mode() {
+        return mode;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(l2LbId, ports, mode);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof L2Lb)) {
+            return false;
+        }
+        final L2Lb other = (L2Lb) obj;
+
+        return Objects.equals(this.l2LbId, other.l2LbId) &&
+                Objects.equals(this.ports, other.ports) &&
+                this.mode == other.mode;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(getClass())
+                .add("l2LbId", l2LbId)
+                .add("ports", ports)
+                .add("mode", mode)
+                .toString();
+    }
+}
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbAdminService.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbAdminService.java
new file mode 100644
index 0000000..076ed36
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbAdminService.java
@@ -0,0 +1,47 @@
+/*
+ * 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.l2lb.api;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Set;
+
+/**
+ * LACP admin service.
+ */
+public interface L2LbAdminService {
+    /**
+     * Creates or updates a L2 load balancer.
+     *
+     * @param deviceId Device ID
+     * @param key L2 load balancer key
+     * @param ports physical ports in the L2 load balancer
+     * @param mode L2 load balancer mode
+     * @return L2 load balancer that is created or updated
+     */
+    L2Lb createOrUpdate(DeviceId deviceId, int key, Set<PortNumber> ports, L2LbMode mode);
+
+    /**
+     * Removes a L2 load balancer.
+     *
+     * @param deviceId Device ID
+     * @param key L2 load balancer key
+     * @return L2 load balancer that is removed
+     */
+    L2Lb remove(DeviceId deviceId, int key);
+
+}
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbEvent.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbEvent.java
new file mode 100644
index 0000000..f0ac847
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbEvent.java
@@ -0,0 +1,89 @@
+/*
+ * 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.l2lb.api;
+
+import org.onlab.util.Tools;
+import org.onosproject.event.AbstractEvent;
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+public class L2LbEvent extends AbstractEvent<L2LbEvent.Type, L2Lb> {
+
+    private L2Lb prevSubject;
+
+    /**
+     * L2 load balancer event type.
+     */
+    public enum Type {
+        ADDED,
+        REMOVED,
+        UPDATED
+    }
+
+    /**
+     * Constructs a L2 load balancer event.
+     *
+     * @param type event type
+     * @param subject current L2 load balancer information
+     * @param prevSubject previous L2 load balancer information
+     */
+    public L2LbEvent(Type type, L2Lb subject, L2Lb prevSubject) {
+        super(type, subject);
+        this.prevSubject = prevSubject;
+    }
+
+    /**
+     * Gets previous L2 load balancer information.
+     *
+     * @return previous subject
+     */
+    public L2Lb prevSubject() {
+        return prevSubject;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(subject(), time(), prevSubject);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (!(other instanceof L2LbEvent)) {
+            return false;
+        }
+
+        L2LbEvent that = (L2LbEvent) other;
+        return Objects.equals(this.subject(), that.subject()) &&
+                Objects.equals(this.type(), that.type()) &&
+                Objects.equals(this.prevSubject, that.prevSubject);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("type", type())
+                .add("subject", subject())
+                .add("prevSubject", prevSubject)
+                .add("time", Tools.defaultOffsetDataTime(time()))
+                .toString();
+    }
+}
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbId.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbId.java
new file mode 100644
index 0000000..65aecef
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbId.java
@@ -0,0 +1,78 @@
+/*
+ * 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.l2lb.api;
+
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+public class L2LbId {
+    private final DeviceId deviceId;
+    private final int key;
+
+    /**
+     * Constructs L2 load balancer ID.
+     *
+     * @param deviceId device ID
+     * @param key L2 load balancer key
+     */
+    public L2LbId(DeviceId deviceId, int key) {
+        this.deviceId = deviceId;
+        this.key = key;
+    }
+
+    /**
+     * Returns L2 load balancer device ID.
+     *
+     * @return L2 load balancer device ID
+     */
+    public DeviceId deviceId() {
+        return deviceId;
+    }
+
+    /**
+     * Returns L2 load balancer key.
+     *
+     * @return L2 load balancer key
+     */
+    public int key() {
+        return key;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(deviceId, key);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof L2LbId)) {
+            return false;
+        }
+        final L2LbId other = (L2LbId) obj;
+
+        return Objects.equals(this.deviceId, other.deviceId) &&
+                Objects.equals(this.key, other.key);
+    }
+
+    @Override
+    public String toString() {
+        return deviceId.toString() + " (" + key + ")";
+    }
+}
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbListener.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbListener.java
new file mode 100644
index 0000000..f04ca47
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbListener.java
@@ -0,0 +1,25 @@
+/*
+ * 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.l2lb.api;
+
+import org.onosproject.event.EventListener;
+
+/**
+ * L2 load balancer event listener.
+ */
+public interface L2LbListener extends EventListener<L2LbEvent> {
+}
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbMode.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbMode.java
new file mode 100644
index 0000000..9c38bd6
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbMode.java
@@ -0,0 +1,31 @@
+/*
+ * 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.l2lb.api;
+
+/**
+ * L2 load balancer mode.
+ */
+public enum L2LbMode {
+    /**
+     * Static L2 load balancer.
+     */
+    STATIC,
+
+    /**
+     * L2 load balancer based on LACP.
+     */
+    LACP
+}
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbService.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbService.java
new file mode 100644
index 0000000..8779192
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/L2LbService.java
@@ -0,0 +1,59 @@
+/*
+ * 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.l2lb.api;
+
+import org.onosproject.event.ListenerService;
+import org.onosproject.net.DeviceId;
+
+import java.util.Map;
+
+/**
+ * L2 load balance service.
+ */
+public interface L2LbService extends ListenerService<L2LbEvent, L2LbListener> {
+    /**
+     * Gets all L2 load balancers from the store.
+     *
+     * @return L2 load balancer ID and L2 load balancer information mapping
+     */
+    Map<L2LbId, L2Lb> getL2Lbs();
+
+    /**
+     * Gets L2 load balancer that matches given device ID and key, or null if not found.
+     *
+     * @param deviceId Device ID
+     * @param key L2 load balancer key
+     * @return L2 load balancer information
+     */
+    L2Lb getL2Lb(DeviceId deviceId, int key);
+
+    /**
+     * Gets L2 load balancer next ids from the store.
+     *
+     * @return L2 load balancer id and next id mapping
+     */
+    Map<L2LbId, Integer> getL2LbNexts();
+
+    /**
+     * Gets L2 load balancer next id that matches given device Id and key, or null if not found.
+     *
+     * @param deviceId Device ID
+     * @param key L2 load balancer key
+     * @return next ID
+     */
+    int getL2LbNexts(DeviceId deviceId, int key);
+}
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/api/package-info.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/package-info.java
new file mode 100644
index 0000000..862428b
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/api/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * L2 load balancer API.
+ */
+package org.onosproject.l2lb.api;
\ No newline at end of file
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/app/L2LbManager.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/app/L2LbManager.java
new file mode 100644
index 0000000..1f58960
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/app/L2LbManager.java
@@ -0,0 +1,337 @@
+/*
+ * 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.l2lb.app;
+
+
+import com.google.common.collect.Sets;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.l2lb.api.L2Lb;
+import org.onosproject.l2lb.api.L2LbEvent;
+import org.onosproject.l2lb.api.L2LbAdminService;
+import org.onosproject.l2lb.api.L2LbId;
+import org.onosproject.l2lb.api.L2LbListener;
+import org.onosproject.l2lb.api.L2LbMode;
+import org.onosproject.l2lb.api.L2LbService;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.net.packet.PacketService;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.MapEvent;
+import org.onosproject.store.service.MapEventListener;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.Versioned;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.onlab.util.Tools.groupedThreads;
+import static org.slf4j.LoggerFactory.getLogger;
+
+@Component(
+    immediate = true,
+    service = {
+        L2LbService.class,
+        L2LbAdminService.class
+    }
+)
+public class L2LbManager implements L2LbService, L2LbAdminService {
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    private CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    private PacketService packetService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    private InterfaceService interfaceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    private StorageService storageService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    private FlowObjectiveService flowObjService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    private MastershipService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    private DeviceService deviceService;
+
+    private static final Logger log = getLogger(L2LbManager.class);
+    private static final String APP_NAME = "org.onosproject.l2lb";
+
+    private ApplicationId appId;
+    private ConsistentMap<L2LbId, L2Lb> l2LbStore;
+    private ConsistentMap<L2LbId, Integer> l2LbNextStore;
+    private Set<L2LbListener> listeners = Sets.newConcurrentHashSet();
+
+    private ExecutorService l2LbEventExecutor;
+    private ExecutorService l2LbProvExecutor;
+    private MapEventListener<L2LbId, L2Lb> l2LbStoreListener;
+    // TODO build CLI to view and clear the next store
+    private MapEventListener<L2LbId, Integer> l2LbNextStoreListener;
+
+    @Activate
+    public void activate() {
+        appId = coreService.registerApplication(APP_NAME);
+
+        l2LbEventExecutor = Executors.newSingleThreadExecutor(groupedThreads("l2lb-event", "%d", log));
+        l2LbProvExecutor = Executors.newSingleThreadExecutor(groupedThreads("l2lb-prov", "%d", log));
+        l2LbStoreListener = new L2LbStoreListener();
+        l2LbNextStoreListener = new L2LbNextStoreListener();
+
+        KryoNamespace serializer = KryoNamespace.newBuilder()
+                .register(KryoNamespaces.API)
+                .register(L2Lb.class)
+                .register(L2LbId.class)
+                .register(L2LbMode.class)
+                .build();
+        l2LbStore = storageService.<L2LbId, L2Lb>consistentMapBuilder()
+                .withName("onos-l2lb-store")
+                .withRelaxedReadConsistency()
+                .withSerializer(Serializer.using(serializer))
+                .build();
+        l2LbStore.addListener(l2LbStoreListener);
+        l2LbNextStore = storageService.<L2LbId, Integer>consistentMapBuilder()
+                .withName("onos-l2lb-next-store")
+                .withRelaxedReadConsistency()
+                .withSerializer(Serializer.using(serializer))
+                .build();
+        l2LbNextStore.addListener(l2LbNextStoreListener);
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        l2LbStore.removeListener(l2LbStoreListener);
+        l2LbNextStore.removeListener(l2LbNextStoreListener);
+
+        l2LbEventExecutor.shutdown();
+
+        log.info("Stopped");
+    }
+
+    @Override
+    public void addListener(L2LbListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(L2LbListener listener) {
+        listeners.remove(listener);
+    }
+
+    @Override
+    public L2Lb createOrUpdate(DeviceId deviceId, int key, Set<PortNumber> ports, L2LbMode mode) {
+        L2LbId l2LbId = new L2LbId(deviceId, key);
+        log.debug("Putting {} -> {} {} into L2 load balancer store", l2LbId, mode, ports);
+        return Versioned.valueOrNull(l2LbStore.put(l2LbId, new L2Lb(l2LbId, ports, mode)));
+    }
+
+    @Override
+    public L2Lb remove(DeviceId deviceId, int key) {
+        L2LbId l2LbId = new L2LbId(deviceId, key);
+        log.debug("Removing {} from L2 load balancer store", l2LbId);
+        return Versioned.valueOrNull(l2LbStore.remove(l2LbId));
+    }
+
+    @Override
+    public Map<L2LbId, L2Lb> getL2Lbs() {
+        return l2LbStore.asJavaMap();
+    }
+
+    @Override
+    public L2Lb getL2Lb(DeviceId deviceId, int key) {
+        return Versioned.valueOrNull(l2LbStore.get(new L2LbId(deviceId, key)));
+    }
+
+    @Override
+    public Map<L2LbId, Integer> getL2LbNexts() {
+        return l2LbNextStore.asJavaMap();
+    }
+
+    @Override
+    public int getL2LbNexts(DeviceId deviceId, int key) {
+        return Versioned.valueOrNull(l2LbNextStore.get(new L2LbId(deviceId, key)));
+    }
+
+    private class L2LbStoreListener implements MapEventListener<L2LbId, L2Lb> {
+        public void event(MapEvent<L2LbId, L2Lb> event) {
+            switch (event.type()) {
+                case INSERT:
+                    log.debug("L2Lb {} insert new={}, old={}", event.key(), event.newValue(), event.oldValue());
+                    post(new L2LbEvent(L2LbEvent.Type.ADDED, event.newValue().value(), null));
+                    populateL2Lb(event.newValue().value());
+                    break;
+                case REMOVE:
+                    log.debug("L2Lb {} remove new={}, old={}", event.key(), event.newValue(), event.oldValue());
+                    post(new L2LbEvent(L2LbEvent.Type.REMOVED, null, event.oldValue().value()));
+                    revokeL2Lb(event.oldValue().value());
+                    break;
+                case UPDATE:
+                    log.debug("L2Lb {} update new={}, old={}", event.key(), event.newValue(), event.oldValue());
+                    post(new L2LbEvent(L2LbEvent.Type.UPDATED, event.newValue().value(),
+                            event.oldValue().value()));
+                    updateL2Lb(event.newValue().value(), event.oldValue().value());
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private class L2LbNextStoreListener implements MapEventListener<L2LbId, Integer> {
+        public void event(MapEvent<L2LbId, Integer> event) {
+            switch (event.type()) {
+                case INSERT:
+                    log.debug("L2Lb next {} insert new={}, old={}", event.key(), event.newValue(), event.oldValue());
+                    break;
+                case REMOVE:
+                    log.debug("L2Lb next {} remove new={}, old={}", event.key(), event.newValue(), event.oldValue());
+                    break;
+                case UPDATE:
+                    log.debug("L2Lb next {} update new={}, old={}", event.key(), event.newValue(), event.oldValue());
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private void post(L2LbEvent l2LbEvent) {
+        l2LbEventExecutor.execute(() -> {
+            for (L2LbListener l : listeners) {
+                l.event(l2LbEvent);
+            }
+        });
+    }
+
+    // TODO repopulate when device reconnect
+    private void populateL2Lb(L2Lb l2Lb) {
+        DeviceId deviceId = l2Lb.l2LbId().deviceId();
+        if (!mastershipService.isLocalMaster(deviceId)) {
+            log.debug("Not the master of {}. Skip populateL2Lb {}", deviceId, l2Lb.l2LbId());
+            return;
+        }
+
+        l2LbProvExecutor.execute(() -> {
+            L2LbObjectiveContext context = new L2LbObjectiveContext(l2Lb.l2LbId());
+            NextObjective nextObj = nextObjBuilder(l2Lb.l2LbId(), l2Lb.ports()).add(context);
+
+            flowObjService.next(deviceId, nextObj);
+            l2LbNextStore.put(l2Lb.l2LbId(), nextObj.id());
+        });
+    }
+
+    private void revokeL2Lb(L2Lb l2Lb) {
+        DeviceId deviceId = l2Lb.l2LbId().deviceId();
+        if (!mastershipService.isLocalMaster(deviceId)) {
+            log.debug("Not the master of {}. Skip revokeL2Lb {}", deviceId, l2Lb.l2LbId());
+            return;
+        }
+
+        l2LbProvExecutor.execute(() -> {
+            l2LbNextStore.remove(l2Lb.l2LbId());
+            // NOTE group is not removed and we rely on the garbage collection mechanism
+        });
+    }
+
+    private void updateL2Lb(L2Lb newL2Lb, L2Lb oldL2Lb) {
+        DeviceId deviceId = newL2Lb.l2LbId().deviceId();
+        if (!mastershipService.isLocalMaster(deviceId)) {
+            log.debug("Not the master of {}. Skip updateL2Lb {}", deviceId, newL2Lb.l2LbId());
+            return;
+        }
+
+        l2LbProvExecutor.execute(() -> {
+            L2LbObjectiveContext context = new L2LbObjectiveContext(newL2Lb.l2LbId());
+            Set<PortNumber> portsToBeAdded = Sets.difference(newL2Lb.ports(), oldL2Lb.ports());
+            Set<PortNumber> portsToBeRemoved = Sets.difference(oldL2Lb.ports(), newL2Lb.ports());
+
+            flowObjService.next(deviceId, nextObjBuilder(newL2Lb.l2LbId(), portsToBeAdded).addToExisting(context));
+            flowObjService.next(deviceId, nextObjBuilder(newL2Lb.l2LbId(), portsToBeRemoved)
+                    .removeFromExisting(context));
+        });
+    }
+
+    private NextObjective.Builder nextObjBuilder(L2LbId l2LbId, Set<PortNumber> ports) {
+        return nextObjBuilder(l2LbId, ports, flowObjService.allocateNextId());
+    }
+
+    private NextObjective.Builder nextObjBuilder(L2LbId l2LbId, Set<PortNumber> ports, int nextId) {
+        // TODO replace logical l2lb port
+        TrafficSelector meta = DefaultTrafficSelector.builder()
+                .matchInPort(PortNumber.portNumber(l2LbId.key())).build();
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective.builder()
+                .withId(nextId)
+                .withMeta(meta)
+                .withType(NextObjective.Type.HASHED)
+                .fromApp(appId);
+        ports.forEach(port -> {
+            TrafficTreatment treatment = DefaultTrafficTreatment.builder().setOutput(port).build();
+            nextObjBuilder.addTreatment(treatment);
+        });
+        return nextObjBuilder;
+    }
+
+    private final class L2LbObjectiveContext implements ObjectiveContext {
+        private final L2LbId l2LbId;
+
+        private L2LbObjectiveContext(L2LbId l2LbId) {
+            this.l2LbId = l2LbId;
+        }
+
+        @Override
+        public void onSuccess(Objective objective) {
+            NextObjective nextObj = (NextObjective) objective;
+            log.debug("Added nextobj {} for L2 load balancer {}", nextObj, l2LbId);
+        }
+
+        @Override
+        public void onError(Objective objective, ObjectiveError error) {
+            NextObjective nextObj = (NextObjective) objective;
+            log.debug("Failed to add nextobj {} for L2 load balancer {}", nextObj, l2LbId);
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/app/package-info.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/app/package-info.java
new file mode 100644
index 0000000..6e155f6
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/app/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * L2 load balancer app.
+ */
+package org.onosproject.l2lb.app;
\ No newline at end of file
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/L2LbAddCommand.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/L2LbAddCommand.java
new file mode 100644
index 0000000..4afa1fe
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/L2LbAddCommand.java
@@ -0,0 +1,70 @@
+/*
+ * 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.l2lb.cli;
+
+import com.google.common.collect.Sets;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.l2lb.api.L2LbAdminService;
+import org.onosproject.l2lb.api.L2LbMode;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Command to add a L2 load balancer.
+ */
+@Service
+@Command(scope = "onos", name = "l2lb-add", description = "Create or update L2 load balancer")
+public class L2LbAddCommand extends AbstractShellCommand {
+    @Argument(index = 0, name = "deviceId",
+            description = "Device ID",
+            required = true, multiValued = false)
+    private String deviceIdStr;
+
+    @Argument(index = 1, name = "key",
+            description = "L2 load balancer key",
+            required = true, multiValued = false)
+    private String keyStr;
+
+    @Argument(index = 2, name = "mode",
+            description = "L2 load balancer mode. STATIC or LACP",
+            required = true, multiValued = false)
+    private String modeStr;
+
+    @Argument(index = 3, name = "ports",
+            description = "L2 load balancer physical ports",
+            required = true, multiValued = true)
+    private String[] portsStr;
+
+    @Override
+    protected void doExecute() {
+        DeviceId deviceId = DeviceId.deviceId(deviceIdStr);
+        int l2LbPort = Integer.parseInt(keyStr);
+
+        L2LbMode mode = L2LbMode.valueOf(modeStr.toUpperCase());
+        Set<PortNumber> ports = Sets.newHashSet(portsStr).stream()
+                .map(PortNumber::fromString).collect(Collectors.toSet());
+
+        L2LbAdminService l2LbAdminService = get(L2LbAdminService.class);
+        l2LbAdminService.createOrUpdate(deviceId, l2LbPort, ports, mode);
+
+    }
+}
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/L2LbListCommand.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/L2LbListCommand.java
new file mode 100644
index 0000000..e1504f5
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/L2LbListCommand.java
@@ -0,0 +1,40 @@
+/*
+ * 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.l2lb.cli;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.l2lb.api.L2Lb;
+import org.onosproject.l2lb.api.L2LbId;
+import org.onosproject.l2lb.api.L2LbService;
+
+import java.util.Map;
+
+/**
+ * Command to show all L2 load balancers.
+ */
+@Service
+@Command(scope = "onos", name = "l2lbs", description = "Lists L2 load balancers")
+public class L2LbListCommand extends AbstractShellCommand {
+    @Override
+    public void doExecute() {
+        L2LbService service = get(L2LbService.class);
+        Map<L2LbId, L2Lb> l2LbStore = service.getL2Lbs();
+
+        l2LbStore.forEach((l2LbId, l2Lb) -> print("%s -> %s, %s", l2LbId, l2Lb.ports(), l2Lb.mode()));
+    }
+}
\ No newline at end of file
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/L2LbRemoveCommand.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/L2LbRemoveCommand.java
new file mode 100644
index 0000000..89c4bce
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/L2LbRemoveCommand.java
@@ -0,0 +1,49 @@
+/*
+ * 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.l2lb.cli;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.l2lb.api.L2LbAdminService;
+import org.onosproject.net.DeviceId;
+
+/**
+ * Command to remove a L2 load balancer.
+ */
+@Service
+@Command(scope = "onos", name = "l2lb-remove", description = "Remove L2 load balancers ")
+public class L2LbRemoveCommand extends AbstractShellCommand {
+    @Argument(index = 0, name = "deviceId",
+            description = "Device ID",
+            required = true, multiValued = false)
+    private String deviceIdStr;
+
+    @Argument(index = 1, name = "key",
+            description = "L2 load balancer key",
+            required = true, multiValued = false)
+    private String keyStr;
+
+    @Override
+    protected void doExecute() {
+        DeviceId deviceId = DeviceId.deviceId(deviceIdStr);
+        int l2LbPort = Integer.parseInt(keyStr);
+
+        L2LbAdminService l2LbAdminService = get(L2LbAdminService.class);
+        l2LbAdminService.remove(deviceId, l2LbPort);
+    }
+}
diff --git a/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/package-info.java b/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/package-info.java
new file mode 100644
index 0000000..4e157c1
--- /dev/null
+++ b/apps/l2lb/src/main/java/org/onosproject/l2lb/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * L2 load balancer CLI.
+ */
+package org.onosproject.l2lb.cli;
\ No newline at end of file
diff --git a/tools/build/bazel/modules.bzl b/tools/build/bazel/modules.bzl
index 9562206..aa426cf 100644
--- a/tools/build/bazel/modules.bzl
+++ b/tools/build/bazel/modules.bzl
@@ -159,6 +159,7 @@
     "//apps/flowanalyzer:onos-apps-flowanalyzer-oar",
     "//apps/intentsync:onos-apps-intentsync-oar",
     "//apps/influxdbmetrics:onos-apps-influxdbmetrics-oar",
+    "//apps/l2lb:onos-apps-l2lb-oar",
     "//apps/metrics:onos-apps-metrics-oar",
     "//apps/mfwd:onos-apps-mfwd-oar",
     "//apps/mlb:onos-apps-mlb-oar",