Update to pom files and package organization of bng-app

Also, update maven repo to HTTPS.

Change-Id: I664f819b2a671a5e85dcfd3e87e333f1163366d5
diff --git a/app/src/main/java/org/opencord/bng/impl/BngManager.java b/app/src/main/java/org/opencord/bng/impl/BngManager.java
new file mode 100644
index 0000000..1f399e1
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/impl/BngManager.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright 2019-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.opencord.bng.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.packet.EthType;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.behaviour.BngProgrammable;
+import org.onosproject.net.behaviour.BngProgrammable.BngProgrammableException;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.host.DefaultHostDescription;
+import org.onosproject.net.host.HostDescription;
+import org.onosproject.net.host.HostProvider;
+import org.onosproject.net.host.HostProviderRegistry;
+import org.onosproject.net.host.HostProviderService;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.provider.ProviderId;
+import org.opencord.bng.BngAttachment;
+import org.opencord.bng.BngService;
+import org.opencord.bng.PppoeBngAttachment;
+import org.opencord.bng.config.BngConfig;
+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 org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
+
+/**
+ * Implements the network level BNG service API to manage attachments.
+ */
+@Component(immediate = true)
+public class BngManager implements HostProvider, BngService {
+    public static final String BNG_APP = "org.opencord.bng";
+
+    private static final ProviderId PROVIDER_ID = new ProviderId("bngapp", BngManager.BNG_APP);
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final AtomicBoolean bnguInitialized = new AtomicBoolean(false);
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected LinkService linkService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected NetworkConfigRegistry cfgService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected HostProviderRegistry providerRegistry;
+
+    private ConfigFactory<ApplicationId, BngConfig> cfgFactory = new ConfigFactory<>(
+            APP_SUBJECT_FACTORY,
+            BngConfig.class,
+            BngConfig.KEY) {
+        @Override
+        public BngConfig createConfig() {
+            return new BngConfig();
+        }
+    };
+    private BngProgrammable bngProgrammable;
+    private DeviceId bngDeviceId;
+    private InternalDeviceListener deviceListener;
+    private InternalConfigListener cfgListener;
+    private HostProviderService hostProviderService;
+    // TODO: add support for other attachment type
+    private Map<String, Pair<BngAttachment, HostId>> registeredAttachment;
+    private ApplicationId appId;
+
+    @Activate
+    protected void activate() {
+        appId = coreService.registerApplication(BNG_APP);
+        hostProviderService = providerRegistry.register(this);
+        registeredAttachment = Maps.newHashMap();
+        bngProgrammable = null;
+        bngDeviceId = null;
+        deviceListener = new InternalDeviceListener();
+        cfgListener = new InternalConfigListener();
+        cfgService.addListener(cfgListener);
+        cfgService.registerConfigFactory(cfgFactory);
+
+        // Update the BNG relay configuration
+        updateConfig();
+
+        deviceService.addListener(deviceListener);
+
+        log.info("BNG app activated");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        providerRegistry.unregister(this);
+        if (bngProgrammableAvailable()) {
+            try {
+                bngProgrammable.cleanUp(appId);
+            } catch (BngProgrammableException e) {
+                log.error("Error cleaning-up the BNG pipeline, {}", e.getMessage());
+            }
+        }
+        deviceService.removeListener(deviceListener);
+        cfgService.removeListener(cfgListener);
+        cfgService.unregisterConfigFactory(cfgFactory);
+        registeredAttachment = null;
+        bnguInitialized.set(false);
+        log.info("BNG app deactivated");
+    }
+
+    @Override
+    public void setupAttachment(String attachmentKey, BngAttachment attachment) {
+        // FIXME: the update case is not completely clear. Should the programAttachment method clean up the counters?
+        assert attachment.type().equals(BngProgrammable.Attachment.AttachmentType.PPPoE);
+        boolean updating = false;
+        var alreadyRegAttachment = registeredAttachment.get(attachmentKey);
+        if (alreadyRegAttachment == null) {
+            log.info("Registering a new attachment: {}", attachment.toString());
+        } else if (!attachment.equals(alreadyRegAttachment.getLeft())) {
+            log.info("Updating the attachment: {}", attachment.toString());
+            updating = true;
+        } else {
+            log.info("Attachment already registered: {}", attachment.toString());
+            return;
+        }
+        // FIXME: it could register anyway the attachment but do not program it on the BNG data-plane device.
+        if (attachment.type() != BngProgrammable.Attachment.AttachmentType.PPPoE) {
+            log.warn("Attachment type not supported, rejecting attachment: {}", attachmentKey);
+            return;
+        }
+        var pppoeAttachment = (PppoeBngAttachment) attachment;
+        // Retrieve the connect point on the ASG device
+        var asgConnectPoint = getAsgConnectPoint(pppoeAttachment.oltConnectPoint()).orElseThrow();
+        final HostId hostId = HostId.hostId(attachment.macAddress(), attachment.sTag());
+        final HostDescription hostDescription = createHostDescription(
+                attachment.cTag(), attachment.sTag(),
+                attachment.macAddress(), attachment.ipAddress(),
+                asgConnectPoint, pppoeAttachment.oltConnectPoint(), pppoeAttachment.onuSerial());
+
+        // Make sure that bngProgrammable is available and if so that the attachment is connected to the bngProgrammable
+        if (bngProgrammableAvailable() && isCorrectlyConnected(asgConnectPoint)) {
+            try {
+                programAttachment(attachment, hostId, hostDescription, updating);
+            } catch (BngProgrammableException ex) {
+                log.error("Attachment not created: " + ex.getMessage());
+            }
+        } else {
+            // If the BNG user plane is not available, or the attachment is not connected to
+            // the correct BNG user planee, accept anyway the attachment.
+            // Check if the attachment is correctly connected to the BNG device when that device will show up.
+            log.info("BNG user plane not available, attachment accepted but not programmed");
+        }
+        log.info("PPPoE Attachment created/updated: {}", pppoeAttachment);
+        registeredAttachment.put(attachmentKey, Pair.of(pppoeAttachment, hostId));
+    }
+
+    private Optional<ConnectPoint> getAsgConnectPoint(ConnectPoint oltConnectPoint) {
+        try {
+            // Here I suppose that each OLT can be connected to a SINGLE ASG that is BNG U capable
+            return Optional.of(linkService.getDeviceEgressLinks(oltConnectPoint.deviceId()).stream()
+                                       .filter(link -> isBngProgrammable(link.dst().deviceId()))
+                                       .map(link -> link.dst())
+                                       .collect(Collectors.toList())
+                                       .get(0));
+
+        } catch (IndexOutOfBoundsException ex) {
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * Setup of an attachment. Before calling this method, make sure that BNG
+     * programmable is available.
+     *
+     * @param attachment
+     * @param hostId
+     * @param hostDescription
+     * @param update
+     * @throws BngProgrammableException
+     */
+    private void programAttachment(BngAttachment attachment, HostId hostId,
+                                   HostDescription hostDescription, boolean update)
+            throws BngProgrammableException {
+        assert bngProgrammableAvailable();
+        bngProgrammable.setupAttachment(attachment);
+        if (!update) {
+            bngProgrammable.resetCounters(attachment);
+        }
+        // Trigger host creation in ONOS
+        hostProviderService.hostDetected(hostId, hostDescription, true);
+    }
+
+    /**
+     * Create an host description from the attachment information.
+     *
+     * @param cTag            Vlan C-TAG.
+     * @param sTag            Vlan S-TAG.
+     * @param hostMac         MAC address of the attachment.
+     * @param hostIp          IP address of the attachment.
+     * @param asgConnectPoint Attachment connect point from the ASG switch
+     *                        perspective.
+     * @param oltConnectPoint Attachment connect point from the OLT
+     *                        perspective.
+     * @param onuSerialNumber ONU Serial Number.
+     * @return Host description of the attachment
+     */
+    private HostDescription createHostDescription(VlanId cTag, VlanId sTag,
+                                                  MacAddress hostMac,
+                                                  IpAddress hostIp,
+                                                  ConnectPoint asgConnectPoint,
+                                                  ConnectPoint oltConnectPoint,
+                                                  String onuSerialNumber) {
+//        Set<HostLocation> hostLocation = Set.of(new HostLocation(oltConnectPoint.deviceId(),
+//                oltConnectPoint.port(),
+//                System.currentTimeMillis()));
+//        var auxLocation = String.join(",", asgConnectPoint.toString());
+        // FIXME: remove this hostLocation and decomment the above rows when aux-location patch will be merged
+        Set<HostLocation> hostLocation = Set.of(new HostLocation(asgConnectPoint.deviceId(),
+                                                                 asgConnectPoint.port(),
+                                                                 System.currentTimeMillis()));
+        var annotations = DefaultAnnotations.builder()
+//                .set(AUX_LOCATIONS_ANNOTATION, auxLocation)
+                .set(ONU_ANNOTATION, onuSerialNumber)
+                .build();
+        Set<IpAddress> ips = hostIp != null
+                ? ImmutableSet.of(hostIp) : ImmutableSet.of();
+        return new DefaultHostDescription(hostMac, sTag,
+                                          hostLocation,
+                                          ips, cTag, EthType.EtherType.QINQ.ethType(),
+                                          false, annotations);
+    }
+
+    @Override
+    public void removeAttachment(String attachmentKey) {
+        assert attachmentKey != null;
+        if (!registeredAttachment.containsKey(attachmentKey)) {
+            log.info("Attachment cannot be removed if it wasn't registered");
+            return;
+        }
+        var regAttachment = registeredAttachment.get(attachmentKey).getLeft();
+
+        final HostId hostToBeRemoved = HostId.hostId(regAttachment.macAddress(), regAttachment.sTag());
+        registeredAttachment.remove(attachmentKey);
+        // Try to remove host even if the BNG user plane device is not available
+        hostProviderService.hostVanished(hostToBeRemoved);
+        if (bngProgrammableAvailable()) {
+            bngProgrammable.removeAttachment(regAttachment);
+        } else {
+            log.info("BNG user plane not available!");
+        }
+        log.info("Attachment {} removed successfully!", regAttachment);
+    }
+
+
+    @Override
+    public Map<String, BngAttachment> getAttachments() {
+        return Maps.asMap(registeredAttachment.keySet(),
+                          key -> registeredAttachment.get(key).getLeft());
+    }
+
+    @Override
+    public BngAttachment getAttachment(String attachmentKey) {
+        return registeredAttachment.getOrDefault(attachmentKey, Pair.of(null, null))
+                .getLeft();
+    }
+
+    /**
+     * Check if the given connect point is part of the BNG user plane device.
+     * Before calling this method, make sure that bngProgrammable is available.
+     *
+     * @param asgConnectPoint The connect point to check
+     * @return
+     */
+    private boolean isCorrectlyConnected(ConnectPoint asgConnectPoint) {
+        assert bngProgrammableAvailable();
+        return asgConnectPoint.deviceId().equals(bngProgrammable.data().deviceId());
+    }
+
+    /**
+     * Setup of the BNG user plane device. This method will cleanup the BNG
+     * pipeline, initialize it and then submit all the attachment already
+     * registered.
+     *
+     * @param deviceId BNG user plane device ID
+     */
+    private void setBngDevice(DeviceId deviceId) {
+        synchronized (bnguInitialized) {
+            if (bnguInitialized.get()) {
+                log.debug("BNG device {} already initialized", deviceId);
+                return;
+            }
+            if (!isBngProgrammable(deviceId)) {
+                log.warn("{} is not BNG-U", deviceId);
+                return;
+            }
+            if (bngProgrammable != null && !bngProgrammable.data().deviceId().equals(deviceId)) {
+                log.error("Change of the BNG-U while BNG-U device is available is not supported!");
+                return;
+            }
+
+            bngProgrammable = deviceService.getDevice(deviceId).as(BngProgrammable.class);
+            log.info("Program BNG-U device {}", deviceId);
+
+            // Initialize behavior
+            try {
+                bngProgrammable.cleanUp(appId);
+                bngProgrammable.init(appId);
+                // FIXME: we can improve this re-registration, keeping track of which attachment
+                //  already has the flow rules submitted in the flow rule subsystem.
+                //  In this way we do not need to cleanUp the bngProgrammable every time it come back online.
+                //  If there is any already registered attachment, try to re-setup their attachment.
+                resubmitRegisteredAttachment();
+
+                bnguInitialized.set(true);
+            } catch (BngProgrammableException e) {
+                log.error("Error in BNG user plane, {}", e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Resubmit all the attachments to the BNG user plane device. Before calling
+     * this method, make sure that bngProgrammable is available
+     *
+     * @throws BngProgrammableException when error in BNG user plane device.
+     */
+    private void resubmitRegisteredAttachment() throws BngProgrammableException {
+        assert bngProgrammableAvailable();
+        for (var registeredAttachemnt : registeredAttachment.entrySet()) {
+            var attachment = registeredAttachemnt.getValue().getLeft();
+            var host = registeredAttachemnt.getValue().getRight();
+            var attachentKey = registeredAttachemnt.getKey();
+            var asgConnectPoint = getAsgConnectPoint(attachment.oltConnectPoint());
+            if (attachment.type() != BngProgrammable.Attachment.AttachmentType.PPPoE) {
+                log.info("Unsupported attachment: {}", attachentKey);
+                continue;
+            }
+            if (asgConnectPoint.isPresent() && isCorrectlyConnected(asgConnectPoint.orElseThrow())) {
+                HostDescription hostDescription = createHostDescription(
+                        attachment.cTag(), attachment.sTag(),
+                        attachment.macAddress(), attachment.ipAddress(),
+                        asgConnectPoint.orElseThrow(), attachment.oltConnectPoint(),
+                        attachment.onuSerial());
+                // When resubmitting registered attachment act as the attachment is being setting up.
+                programAttachment(attachment, host, hostDescription, false);
+            } else {
+                log.info("Attachment is not connected to a valid BNG user plane: {}", attachment);
+            }
+        }
+    }
+
+    /**
+     * Unset the BNG user plane device. If available it will be cleaned-up.
+     */
+    private void unsetBngDevice() {
+        synchronized (bnguInitialized) {
+            if (bngProgrammable != null) {
+                try {
+                    bngProgrammable.cleanUp(appId);
+                } catch (BngProgrammableException e) {
+                    log.error("Error in BNG user plane, {}", e.getMessage());
+                }
+                bngProgrammable = null;
+                bnguInitialized.set(false);
+            }
+        }
+    }
+
+    /**
+     * Check if the device is registered and is BNG user plane.
+     *
+     * @param deviceId
+     * @return
+     */
+    private boolean isBngProgrammable(DeviceId deviceId) {
+        final Device device = deviceService.getDevice(deviceId);
+        return device != null && device.is(BngProgrammable.class);
+    }
+
+    /**
+     * Check if the BNG user plane is available.
+     *
+     * @return
+     * @throws BngProgrammableException
+     */
+    private boolean bngProgrammableAvailable() {
+        return bngProgrammable != null;
+    }
+
+    private void bngUpdateConfig(BngConfig config) {
+        if (config.isValid()) {
+            bngDeviceId = config.getBnguDeviceId();
+            setBngDevice(bngDeviceId);
+        }
+    }
+
+    @Override
+    public DeviceId getBngDeviceId() {
+        return bngDeviceId;
+    }
+
+    /**
+     * Updates BNG app configuration.
+     */
+    private void updateConfig() {
+        BngConfig bngConfig = cfgService.getConfig(appId, BngConfig.class);
+        if (bngConfig != null) {
+            bngUpdateConfig(bngConfig);
+        }
+    }
+
+    @Override
+    public void triggerProbe(Host host) {
+        // Do nothing here
+    }
+
+    @Override
+    public ProviderId id() {
+        return PROVIDER_ID;
+    }
+
+    /**
+     * React to new devices. The first device recognized to have BNG-U
+     * functionality is taken as BNG-U device.
+     */
+    private class InternalDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            DeviceId deviceId = event.subject().id();
+            if (deviceId.equals(bngDeviceId)) {
+                switch (event.type()) {
+                    case DEVICE_ADDED:
+                    case DEVICE_UPDATED:
+                    case DEVICE_AVAILABILITY_CHANGED:
+                        // FIXME: do I need the IF?
+                        //if (deviceService.isAvailable(deviceId)) {
+                        log.warn("Event: {}, SETTING BNG device", event.type());
+                        setBngDevice(deviceId);
+                        //}
+                        break;
+                    case DEVICE_REMOVED:
+                    case DEVICE_SUSPENDED:
+                        unsetBngDevice();
+                        break;
+                    case PORT_ADDED:
+                    case PORT_UPDATED:
+                    case PORT_REMOVED:
+                    case PORT_STATS_UPDATED:
+                        break;
+                    default:
+                        log.warn("Unknown device event type {}", event.type());
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Listener for network config events.
+     */
+    private class InternalConfigListener implements NetworkConfigListener {
+        @Override
+        public void event(NetworkConfigEvent event) {
+            switch (event.type()) {
+                case CONFIG_UPDATED:
+                case CONFIG_ADDED:
+                    event.config().ifPresent(config -> {
+                        bngUpdateConfig((BngConfig) config);
+                        log.info("{} updated", config.getClass().getSimpleName());
+                    });
+                    break;
+                case CONFIG_REMOVED:
+                    event.prevConfig().ifPresent(config -> {
+                        unsetBngDevice();
+                        log.info("{} removed", config.getClass().getSimpleName());
+                    });
+                    break;
+                case CONFIG_REGISTERED:
+                case CONFIG_UNREGISTERED:
+                    break;
+                default:
+                    log.warn("Unsupported event type {}", event.type());
+                    break;
+            }
+        }
+
+        @Override
+        public boolean isRelevant(NetworkConfigEvent event) {
+            if (event.configClass().equals(BngConfig.class)) {
+                return true;
+            }
+            log.debug("Ignore irrelevant event class {}", event.configClass().getName());
+            return false;
+        }
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/impl/BngStatsManager.java b/app/src/main/java/org/opencord/bng/impl/BngStatsManager.java
new file mode 100644
index 0000000..6a2a716
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/impl/BngStatsManager.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2019-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.opencord.bng.impl;
+
+import com.google.common.collect.Maps;
+import org.onlab.util.SharedScheduledExecutors;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.event.AbstractListenerManager;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.behaviour.BngProgrammable;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+import org.opencord.bng.BngAttachment;
+import org.opencord.bng.BngService;
+import org.opencord.bng.BngStatsEvent;
+import org.opencord.bng.BngStatsEventListener;
+import org.opencord.bng.BngStatsEventSubject;
+import org.opencord.bng.BngStatsService;
+import org.osgi.service.component.ComponentContext;
+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.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Dictionary;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import static org.opencord.bng.impl.OsgiPropertyConstants.BNG_STATISTICS_PROBE_RATE;
+import static org.opencord.bng.impl.OsgiPropertyConstants.BNG_STATISTICS_PROBE_RATE_DEFAULT;
+
+@Component(immediate = true,
+        property = {
+                BNG_STATISTICS_PROBE_RATE + ":Long=" + BNG_STATISTICS_PROBE_RATE_DEFAULT,
+        }
+)
+public class BngStatsManager
+        extends AbstractListenerManager<BngStatsEvent, BngStatsEventListener> implements BngStatsService {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final BngStatisticsMonitor bngStatsMonitor = new BngStatisticsMonitor();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ComponentConfigService componentConfigService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected BngService bngService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    private ApplicationId appId;
+    /**
+     * The BNG statistics probe rate.
+     */
+    private long bngStatisticsProbeRate = BNG_STATISTICS_PROBE_RATE_DEFAULT;
+    private ScheduledFuture<?> timeout;
+
+    @Activate
+    protected void activate() {
+        eventDispatcher.addSink(BngStatsEvent.class, listenerRegistry);
+        componentConfigService.registerProperties(getClass());
+        appId = coreService.getAppId(BngManager.BNG_APP);
+        start();
+        log.info("BNG Statistics manager activated");
+    }
+
+    @Modified
+    protected void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context != null ? context.getProperties() : new Properties();
+        Long probeRate = Tools.getLongProperty(properties, BNG_STATISTICS_PROBE_RATE);
+        if (probeRate != null) {
+            bngStatisticsProbeRate = probeRate;
+        }
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        shutdown();
+        componentConfigService.unregisterProperties(getClass(), false);
+        eventDispatcher.removeSink(BngStatsEvent.class);
+        log.info("BNG Statistics manager deactivated");
+
+    }
+
+    /**
+     * Starts the BNG statistics monitor. Does nothing if the monitor is already
+     * running.
+     */
+    private void start() {
+        synchronized (bngStatsMonitor) {
+            if (timeout == null) {
+                timeout = SharedScheduledExecutors.newTimeout(bngStatsMonitor, 0, TimeUnit.MILLISECONDS);
+            }
+        }
+    }
+
+    /**
+     * Stops the BNG statistics monitor.
+     */
+    private void shutdown() {
+        synchronized (bngStatsMonitor) {
+            if (timeout != null) {
+                timeout.cancel(true);
+                timeout = null;
+            }
+        }
+    }
+
+    private Map<String, Map<BngProgrammable.BngCounterType, PiCounterCellData>> getStats(
+            Map<String, BngAttachment> attachments) {
+        Map<String, Map<BngProgrammable.BngCounterType, PiCounterCellData>>
+                stats = Maps.newHashMap();
+        attachments.forEach((key, value) -> stats.put(key, getStats(key)));
+        return stats;
+    }
+
+    @Override
+    public Map<BngProgrammable.BngCounterType, PiCounterCellData> getStats(
+            String bngAttachmentKey) {
+        BngProgrammable bngProgrammable = getBngProgrammable(bngService.getBngDeviceId());
+        BngAttachment attachment = bngService.getAttachment(bngAttachmentKey);
+        if (bngProgrammable != null && attachment != null) {
+            try {
+                return bngProgrammable.readCounters(attachment);
+            } catch (BngProgrammable.BngProgrammableException e) {
+                log.error("Error getting statistics of {}", bngAttachmentKey);
+            }
+        }
+        return Maps.newHashMap();
+    }
+
+    @Override
+    public PiCounterCellData getControlStats() {
+        BngProgrammable bngProgrammable = getBngProgrammable(bngService.getBngDeviceId());
+        if (bngProgrammable != null) {
+            try {
+                return bngProgrammable.readControlTrafficCounter();
+            } catch (BngProgrammable.BngProgrammableException e) {
+                log.error("Error control plane packets statistics");
+            }
+        }
+        return null;
+    }
+
+    private BngProgrammable getBngProgrammable(DeviceId deviceId) {
+        if (deviceId != null && deviceService.isAvailable(deviceId)) {
+            return deviceService.getDevice(deviceId).as(BngProgrammable.class);
+        }
+        return null;
+    }
+
+    private class BngStatisticsMonitor implements Runnable {
+        @Override
+        public void run() {
+            BngProgrammable bngProgrammable = getBngProgrammable(bngService.getBngDeviceId());
+            if (bngProgrammable != null) {
+                var attachments = bngService.getAttachments();
+                Map<String, Map<BngProgrammable.BngCounterType, PiCounterCellData>>
+                        attachmentsStats = getStats(attachments);
+                // Create an event for each attachment statistics
+                attachmentsStats.forEach((attachmentKey, stats) -> {
+                    BngStatsEventSubject evInfo =
+                            new BngStatsEventSubject(attachmentKey,
+                                                     attachments.get(attachmentKey),
+                                                     stats);
+                    post(new BngStatsEvent(BngStatsEvent.EventType.STATS_UPDATED, evInfo));
+                });
+            } else {
+                log.debug("BngProgrammable not available");
+            }
+            synchronized (this) {
+                if (timeout != null) {
+                    timeout = SharedScheduledExecutors.newTimeout(this, bngStatisticsProbeRate, TimeUnit.MILLISECONDS);
+                }
+            }
+        }
+    }
+}
+
+
diff --git a/app/src/main/java/org/opencord/bng/impl/BngUtils.java b/app/src/main/java/org/opencord/bng/impl/BngUtils.java
new file mode 100644
index 0000000..17c04dc
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/impl/BngUtils.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2019-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.opencord.bng.impl;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.opencord.bng.PppoeEventSubject;
+
+public final class BngUtils {
+
+    private BngUtils() {
+
+    }
+
+    /**
+     * Extract the BNG attachment key given an event subject.
+     *
+     * @param eventInfo The event subsject
+     * @return BNG attachment ID
+     */
+    public static String calculateBngAttachmentKey(PppoeEventSubject eventInfo) {
+        return calculateBngAttachmentKey(eventInfo.getOnuSerialNumber(),
+                                         eventInfo.getcTag(), eventInfo.getsTag(),
+                                         eventInfo.getOltConnectPoint(), eventInfo.getIpAddress(),
+                                         eventInfo.getMacAddress());
+    }
+
+    /**
+     * Extract the BNG attachment key given some of the attachment fields.
+     *
+     * @param onuSerialNumber Serial number of the ONU
+     * @param cTag            VLAN C-Tag
+     * @param sTag            VLAN S-Tag
+     * @param oltConnectPoint The OLT-level connect point
+     * @param ipAddress       The attachment IP address
+     * @param macAddress      The attachment MAC address
+     * @return The built attachment ID
+     */
+    public static String calculateBngAttachmentKey(String onuSerialNumber,
+                                                   VlanId cTag, VlanId sTag,
+                                                   ConnectPoint oltConnectPoint,
+                                                   IpAddress ipAddress,
+                                                   MacAddress macAddress) {
+        return String.join("/", onuSerialNumber, cTag.toString(),
+                           sTag.toString(), oltConnectPoint.toString(),
+                           macAddress.toString()
+        );
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/impl/OsgiPropertyConstants.java b/app/src/main/java/org/opencord/bng/impl/OsgiPropertyConstants.java
new file mode 100644
index 0000000..1bd86a0
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/impl/OsgiPropertyConstants.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019-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.opencord.bng.impl;
+
+public final class OsgiPropertyConstants {
+
+    public static final String ENABLE_LOCAL_EVENT_HANDLER = "enableLocalEventHandler";
+    public static final boolean ENABLE_LOCAL_EVENT_HANDLER_DEFAULT = true;
+    public static final String BNG_STATISTICS_PROBE_RATE = "bngStatisticsProbeRate";
+    public static final long BNG_STATISTICS_PROBE_RATE_DEFAULT = 5000;
+
+    private OsgiPropertyConstants() {
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/bng/impl/PppoeHandlerRelay.java b/app/src/main/java/org/opencord/bng/impl/PppoeHandlerRelay.java
new file mode 100644
index 0000000..56566c3
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/impl/PppoeHandlerRelay.java
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2019-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.opencord.bng.impl;
+
+import com.google.common.collect.Maps;
+import org.onlab.packet.Data;
+import org.onlab.packet.DeserializationException;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.util.ItemNotFoundException;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.event.AbstractListenerManager;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.driver.DriverService;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketService;
+import org.opencord.bng.BngAttachment;
+import org.opencord.bng.PppoeBngAttachment;
+import org.opencord.bng.PppoeBngControlHandler;
+import org.opencord.bng.PppoeEvent;
+import org.opencord.bng.PppoeEventListener;
+import org.opencord.bng.PppoeEventSubject;
+import org.opencord.bng.config.PppoeRelayConfig;
+import org.opencord.bng.packets.GenericPpp;
+import org.opencord.bng.packets.Ipcp;
+import org.opencord.bng.packets.PppProtocolType;
+import org.opencord.bng.packets.Pppoe;
+import org.opencord.sadis.SadisService;
+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 org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
+
+@Component(immediate = true)
+public class PppoeHandlerRelay
+        extends AbstractListenerManager<PppoeEvent, PppoeEventListener>
+        implements PppoeBngControlHandler {
+
+    private static final IpAddress IP_ADDRESS_ZERO = IpAddress.valueOf(0);
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final InternalConfigListener cfgListener = new InternalConfigListener();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected NetworkConfigRegistry cfgService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected InterfaceService interfaceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected PacketService packetService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected SadisService sadisService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected LinkService linkService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DriverService driverService;
+
+    private ConfigFactory<ApplicationId, PppoeRelayConfig> cfgFactory = new ConfigFactory<>(
+            APP_SUBJECT_FACTORY,
+            PppoeRelayConfig.class,
+            PppoeRelayConfig.KEY) {
+        @Override
+        public PppoeRelayConfig createConfig() {
+            return new PppoeRelayConfig();
+        }
+    };
+    private ApplicationId appId;
+    private InternalPacketProcessor internalPacketProcessor;
+    private PppoeRelayConfig pppoeRelayConfig;
+    private MacAddress macPppoeServer;
+
+    /**
+     * Ephemeral internal map to trace the attachment information. This map is
+     * mainly used to modify the packet towards the PPPoE server or towards the
+     * attachment. FIXME: this map should be cleaned after some time.
+     */
+    private Map<MacAddress, BngAttachment> mapSrcMacToAttInfo;
+
+    @Activate
+    protected void activate() {
+        mapSrcMacToAttInfo = Maps.newHashMap();
+        appId = coreService.getAppId(BngManager.BNG_APP);
+        cfgService.addListener(cfgListener);
+        cfgService.registerConfigFactory(cfgFactory);
+
+        eventDispatcher.addSink(PppoeEvent.class, listenerRegistry);
+
+        updateConfig();
+
+        internalPacketProcessor = new InternalPacketProcessor();
+        packetService.addProcessor(internalPacketProcessor, PacketProcessor.director(0));
+
+        log.info("PPPoE Handler Relay activated");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        eventDispatcher.removeSink(PppoeEvent.class);
+        packetService.removeProcessor(internalPacketProcessor);
+        cfgService.unregisterConfigFactory(cfgFactory);
+        pppoeRelayConfig = null;
+        mapSrcMacToAttInfo = null;
+        internalPacketProcessor = null;
+        macPppoeServer = null;
+
+        log.info("PPPoE Handler Relay deactivated");
+    }
+
+
+    private void updateConfig() {
+        PppoeRelayConfig newPppoeRelayConfig = cfgService.getConfig(appId, PppoeRelayConfig.class);
+        log.info("{}", newPppoeRelayConfig);
+        if (this.pppoeRelayConfig == null &&
+                newPppoeRelayConfig != null &&
+                newPppoeRelayConfig.isValid()) {
+            // TODO: what happens if this is triggered in the middle of a session auth/packet relay?
+            this.pppoeRelayConfig = newPppoeRelayConfig;
+        }
+    }
+
+    private void processPppoePacket(PacketContext context) {
+        if (!isConfigured()) {
+            log.warn("Missing BNG PPPoE handler relay config. Abort packet processing");
+            return;
+        }
+        Ethernet eth = context.inPacket().parsed();
+        log.debug("Parsing the PPPoE header");
+        //FIXME: PPPoE and above headers are extracted from the ethernet
+        // payload. In case we want to modify the PPPoE/upper-layer header, remember to
+        // serialize it back on the Ethernet payload.
+        Pppoe pppoe = parsePppoeHeader(eth);
+        if (pppoe == null) {
+            return;
+        }
+
+        log.debug("Processing PPPoE header");
+
+        // Check from where the packet is received and if the interface is configured
+        ConnectPoint heardOn = context.inPacket().receivedFrom();
+        if (!heardOn.equals(pppoeRelayConfig.getPppoeServerConnectPoint()) &&
+                !heardOn.equals(pppoeRelayConfig.getAsgToOltConnectPoint()) &&
+                !interfaceService.getInterfacesByPort(heardOn).isEmpty()) {
+            log.info("PPPoE packet from unregistered port {}", heardOn);
+            return;
+        }
+
+        // Retrieve the MAC address of the device that intercepted the packet.
+        // This MAC address is the actual PPPoE server MAC address seen by the attachment
+        MacAddress bnguMac = interfaceService.getInterfacesByPort(heardOn).iterator().next().mac();
+
+        VlanId cTag = VlanId.vlanId(eth.getVlanID());
+        VlanId sTag = VlanId.vlanId(eth.getQinQVID());
+
+        // --------------------------------------- DEBUG ----------------------------------------------
+        if (Pppoe.isPPPoED(eth)) {
+            log.info("Received {} packet from {}",
+                     pppoe.getPacketType(),
+                     heardOn);
+        }
+
+        StringBuilder logPacketPppoes = new StringBuilder();
+        if (Pppoe.isPPPoES(eth)) {
+            logPacketPppoes.append("Received PPPoES ")
+                    .append(PppProtocolType.lookup(pppoe.getPppProtocol()).type())
+                    .append(" packet from ").append(heardOn).append(".");
+        }
+        if (logPacketPppoes.length() > 0) {
+            log.debug(logPacketPppoes.toString());
+        }
+        log.debug(eth.toString());
+        // --------------------------------------------------------------------------------------------
+
+        if (heardOn.equals(pppoeRelayConfig.getPppoeServerConnectPoint())) {
+            // DOWNSTREAM PACKET: from the PPPoE server to the attachment.
+
+            // Learn the MAC address of the PPPoE server
+            if (macPppoeServer == null) {
+                macPppoeServer = eth.getSourceMAC();
+            }
+
+            MacAddress dstMac = eth.getDestinationMAC();
+            log.debug("Packet to the attachment: {}", eth);
+            if (!mapSrcMacToAttInfo.containsKey(dstMac)) {
+                BngAttachment newAttInfo = PppoeBngAttachment.builder()
+                        .withMacAddress(dstMac)
+                        .withSTag(sTag)
+                        .withCTag(cTag)
+                        .withQinqTpid(eth.getQinQTPID())
+                        .build();
+                mapSrcMacToAttInfo.put(dstMac, newAttInfo);
+            }
+            // Retrieve the information about the attachment from the internal MAP
+            BngAttachment attInfo = mapSrcMacToAttInfo.get(dstMac);
+
+            // Generate the events for this attachment
+            manageAttachmentStateDownstream(eth, pppoe, attInfo);
+            modPacketForAttachment(eth, attInfo, bnguMac);
+
+            log.debug("Packet modified as: {}", eth);
+            // Send out the packet towards the OLT
+            forwardPacket(pppoeRelayConfig.getAsgToOltConnectPoint(), eth);
+        } else {
+            // UPSTREAM DIRECTION: from the attachment to the PPPoE server
+            MacAddress srcMac = eth.getSourceMAC();
+            if (!mapSrcMacToAttInfo.containsKey(srcMac)) {
+                BngAttachment newAttInfo = PppoeBngAttachment.builder()
+                        .withMacAddress(srcMac)
+                        .withSTag(sTag)
+                        .withCTag(cTag)
+                        .withQinqTpid(eth.getQinQTPID())
+                        .build();
+                mapSrcMacToAttInfo.put(srcMac, newAttInfo);
+            }
+
+            manageAttachmentStateUpstream(eth, pppoe);
+
+            modPacketForPPPoEServer(eth);
+            log.debug("Packet modified as: {}", eth);
+            // Forward packet to the PPPoE server connect point
+            forwardPacket(pppoeRelayConfig.getPppoeServerConnectPoint(), eth);
+        }
+    }
+
+    /**
+     * Generate an event related to PPPoE or IPCP state change.
+     *
+     * @param bngAppEventType Event type
+     * @param ip              IP Address if it has been assigned, otherwise
+     *                        0.0.0.0
+     * @param attInfo         Local attachment information
+     */
+    private void generateEventPppoe(PppoeEvent.EventType bngAppEventType,
+                                    BngAttachment attInfo, short pppoeSessionId,
+                                    IpAddress ip) {
+        // Retrive the NNI connect point
+        var oltConnectPoint = getOltConnectPoint(attInfo.sTag(), attInfo.cTag(),
+                                                 pppoeRelayConfig.getAsgToOltConnectPoint());
+        assert oltConnectPoint.orElse(null) != null;
+        log.info("Generating event of type {}", bngAppEventType);
+        post(new PppoeEvent(
+                     bngAppEventType,
+                     new PppoeEventSubject(
+                             oltConnectPoint.orElseThrow(),
+                             ip,
+                             attInfo.macAddress(),
+                             getPortNameAnnotation(oltConnectPoint.orElse(null)),
+                             pppoeSessionId,
+                             attInfo.sTag(),
+                             attInfo.cTag())
+             )
+        );
+    }
+
+    /**
+     * Generate attachment related state for the upstream direction.
+     *
+     * @param eth   The ethernet packet
+     * @param pppoe PPPoE header
+     */
+    private void manageAttachmentStateUpstream(Ethernet eth, Pppoe pppoe) {
+        PppoeEvent.EventType eventType = null;
+        MacAddress srcMac = eth.getSourceMAC();
+        VlanId cTag = VlanId.vlanId(eth.getVlanID());
+        VlanId sTag = VlanId.vlanId(eth.getQinQVID());
+        BngAttachment attInfo = mapSrcMacToAttInfo.get(srcMac);
+        switch (PppProtocolType.lookup(pppoe.getPppProtocol())) {
+            case IPCP:
+                // Attachment information should be already present
+                Ipcp ipcp = (Ipcp) pppoe.getPayload();
+                if (ipcp.getCode() == Ipcp.CONF_REQ) {
+                    log.debug("IPCP configuration request from attachment");
+                    eventType = PppoeEvent.EventType.IPCP_CONF_REQUEST;
+                }
+                break;
+            case NO_PROTOCOL:
+                if (Pppoe.isPPPoED(eth) &&
+                        pppoe.getPacketType() == Pppoe.PppoeType.PADI) {
+                    log.info("PADI received from attachment {}/{}. Saved in internal store",
+                             srcMac, sTag);
+                    eventType = PppoeEvent.EventType.SESSION_INIT;
+                }
+                break;
+            default:
+        }
+        if (eventType != null) {
+            generateEventPppoe(eventType, attInfo, pppoe.getSessionId(), IP_ADDRESS_ZERO);
+        }
+    }
+
+    private String getPortNameAnnotation(ConnectPoint oltConnectPoint) {
+        return deviceService.getPort(oltConnectPoint.deviceId(),
+                                     oltConnectPoint.port()).annotations().value("portName");
+    }
+
+    /**
+     * Generate attachment related state for the downstream direction.
+     *
+     * @param eth     The ethernet packet
+     * @param pppoe   PPPoE header
+     * @param attInfo Attachment info stored in the internal store
+     */
+    private void manageAttachmentStateDownstream(Ethernet eth, Pppoe pppoe,
+                                                 BngAttachment attInfo) {
+        PppoeEvent.EventType eventType = null;
+        IpAddress assignedIpAddress = IP_ADDRESS_ZERO;
+        switch (PppProtocolType.lookup(pppoe.getPppProtocol())) {
+            case IPCP:
+                Ipcp ipcp = (Ipcp) pppoe.getPayload();
+                if (ipcp.getCode() == Ipcp.ACK) {
+                    log.info("Received a IPCP ACK from Server. Assigned IP Address {}",
+                             ipcp.getIpAddress());
+                    assignedIpAddress = ipcp.getIpAddress();
+                    eventType = PppoeEvent.EventType.IPCP_CONF_ACK;
+                }
+                break;
+
+            case CHAP:
+                // Check if server has correctly authenticated the attachment
+                GenericPpp chap = (GenericPpp) pppoe.getPayload();
+                if (chap.getCode() == GenericPpp.CHAP_CODE_SUCCESS) {
+                    log.info("CHAP authentication success: {}", attInfo.macAddress());
+                    eventType = PppoeEvent.EventType.AUTH_SUCCESS;
+                }
+                if (chap.getCode() == GenericPpp.CHAP_CODE_FAILURE) {
+                    log.info("CHAP authentication failed: {}", attInfo.macAddress());
+                    eventType = PppoeEvent.EventType.AUTH_FAILURE;
+                }
+                break;
+
+            case PAP:
+                // Check if server has correctly authenticated the attachment
+                GenericPpp pap = (GenericPpp) pppoe.getPayload();
+                if (pap.getCode() == GenericPpp.PAP_AUTH_ACK) {
+                    log.info("PAP authentication success: {}", attInfo.macAddress());
+                    eventType = PppoeEvent.EventType.AUTH_SUCCESS;
+                }
+                if (pap.getCode() == GenericPpp.PAP_AUTH_NACK) {
+                    log.info("PAP authentication failed: {}", attInfo.macAddress());
+                    eventType = PppoeEvent.EventType.AUTH_FAILURE;
+                }
+                break;
+
+            case LCP:
+                GenericPpp lcp = (GenericPpp) pppoe.getPayload();
+                if (lcp.getCode() == GenericPpp.CODE_TERM_REQ) {
+                    log.info("LCP Termination request from PPPoE server");
+                    eventType = PppoeEvent.EventType.SESSION_TERMINATION;
+                }
+                break;
+
+            case NO_PROTOCOL:
+                if (Pppoe.isPPPoED(eth)) {
+                    switch (pppoe.getPacketType()) {
+                        case PADS:
+                            // Set the current PPPoE session ID
+                            eventType = PppoeEvent.EventType.SESSION_CONFIRMATION;
+                            break;
+                        case PADT:
+                            log.info("PADT received from PPPoE server");
+                            eventType = PppoeEvent.EventType.SESSION_TERMINATION;
+                            break;
+                        default:
+                    }
+                }
+                break;
+            default:
+        }
+        // Generate and event if needed
+        if (eventType != null) {
+            generateEventPppoe(eventType, attInfo, pppoe.getSessionId(), assignedIpAddress);
+        }
+    }
+
+    private Pppoe parsePppoeHeader(Ethernet eth) {
+        try {
+            return Pppoe.deserializer().deserialize(((Data) eth.getPayload()).getData(),
+                                                    0,
+                                                    ((Data) eth.getPayload()).getData().length);
+        } catch (DeserializationException e) {
+            log.error("Error parsing the PPPoE Headers, packet skipped. \n" + e.getMessage());
+            return null;
+        }
+    }
+
+
+    /**
+     * Apply the modification to the packet to send it to the attachment.
+     *
+     * @param eth     Packet to be modified
+     * @param attInfo Attachment information store in the internal map
+     */
+    private void modPacketForAttachment(Ethernet eth,
+                                        BngAttachment attInfo,
+                                        MacAddress newSourceMac) {
+        eth.setVlanID(attInfo.cTag().toShort());
+        eth.setQinQVID(attInfo.sTag().toShort());
+        eth.setQinQTPID(attInfo.qinqTpid());
+        eth.setSourceMACAddress(newSourceMac);
+    }
+
+    /**
+     * Apply the modification to the packet to send it to the PPPoE Server.
+     *
+     * @param eth Packet to be modified
+     */
+    private void modPacketForPPPoEServer(Ethernet eth) {
+        // TODO: rewrite it. Retrieve information about the interface where
+        //  PPPoE Server is connected and apply them to the packet
+        Set<Interface> interfaces = interfaceService.getInterfacesByPort(pppoeRelayConfig.getPppoeServerConnectPoint());
+        if (interfaces != null &&
+                interfaces.iterator().hasNext() &&
+                interfaces.iterator().next().vlanTagged() != null &&
+                interfaces.iterator().next().vlanTagged().iterator().hasNext()) {
+            VlanId vlanId = interfaces.iterator().next().vlanTagged().iterator().next();
+            if (vlanId != null && vlanId != VlanId.NONE) {
+                eth.setVlanID(vlanId.toShort());
+                eth.setQinQVID(Ethernet.VLAN_UNTAGGED);
+            } else {
+                eth.setVlanID(Ethernet.VLAN_UNTAGGED);
+                eth.setQinQVID(Ethernet.VLAN_UNTAGGED);
+            }
+        } else {
+            eth.setVlanID(Ethernet.VLAN_UNTAGGED);
+            eth.setQinQVID(Ethernet.VLAN_UNTAGGED);
+        }
+        // Modify DST Mac Address with the one of the PPPoE Server
+        if (!eth.getDestinationMAC().isBroadcast()) {
+            if (macPppoeServer == null) {
+                log.warn("NO Mac address for PPPoE server available! Dropping packet");
+                return;
+            }
+            eth.setDestinationMACAddress(macPppoeServer);
+        }
+    }
+
+    /**
+     * Retrieve the NNI Connect Point given the S-Tag, C-Tag and the OLT facing
+     * ASG connect point.
+     *
+     * @param sTag                 The S-Tag VLAN tag
+     * @param cTag                 The C-Tag VLAN tag
+     * @param asgToOltConnectPoint Connect point from ASG to OLT.
+     * @return
+     */
+    private Optional<ConnectPoint> getOltConnectPoint(
+            VlanId sTag, VlanId cTag, ConnectPoint asgToOltConnectPoint) {
+        // Retrieve the UNI port where this attachment is attached to. We assume
+        // an attachment is uniquely identified by its c-tag and s-tag in the
+        // scope of an OLT. In lack of a better API in SADIS, we retrieve info
+        // for all OLT ports and match those that have same c-tag and s-tag as
+        // the given attachemnt info.
+
+        var oltDeviceIds = linkService.getIngressLinks(asgToOltConnectPoint)
+                .stream()
+                .map(link -> link.src().deviceId())
+                .filter(deviceId -> {
+                    try {
+                        return driverService.getDriver(deviceId)
+                                .name().contains("voltha");
+                    } catch (ItemNotFoundException e) {
+                        log.warn("Unable to find driver for {}", deviceId);
+                        return false;
+                    }
+                })
+                .collect(Collectors.toSet());
+
+        var oltConnectPoints = oltDeviceIds.stream()
+                .flatMap(deviceId -> deviceService.getPorts(deviceId).stream())
+                .filter(port -> {
+                    var portName = port.annotations().value("portName");
+                    if (portName == null) {
+                        return false;
+                    }
+                    var info = sadisService.getSubscriberInfoService()
+                            .get(portName);
+                    return info != null &&
+                            Objects.equals(cTag, info.cTag()) &&
+                            Objects.equals(sTag, info.sTag());
+                })
+                .map(port -> new ConnectPoint(port.element().id(), port.number()))
+                .collect(Collectors.toSet());
+
+        if (oltConnectPoints.isEmpty()) {
+            log.error("Unable to find a connect point for attachment with S-Tag {} C-Tag {} on OLTs {}",
+                      sTag, cTag, oltDeviceIds);
+            return Optional.empty();
+        } else if (oltConnectPoints.size() > 1) {
+            log.error("Multiple OLT connect points found for attachment S-Tag {} C-Tag {}," +
+                              "aborting discovery as this is NOT supported (yet)..." +
+                              "oltConnectPoints={}",
+                      sTag, cTag, oltConnectPoints);
+            return Optional.empty();
+        }
+
+        return Optional.of(oltConnectPoints.iterator().next());
+    }
+
+    /**
+     * Send the specified packet, out to the specified connect point.
+     *
+     * @param toPort Output port to send the packet
+     * @param packet Packet to be sent
+     */
+    private void forwardPacket(ConnectPoint toPort, Ethernet packet) {
+        TrafficTreatment toPortTreatment = DefaultTrafficTreatment.builder()
+                .setOutput(toPort.port()).build();
+        OutboundPacket outboundPacket = new DefaultOutboundPacket(
+                toPort.deviceId(), toPortTreatment, ByteBuffer.wrap(packet.serialize()));
+        packetService.emit(outboundPacket);
+    }
+
+    /**
+     * Check if the handler is correctly configured.
+     *
+     * @return True if it is correctly configure, False otherwise
+     */
+    private boolean isConfigured() {
+        return pppoeRelayConfig != null;
+    }
+
+    /**
+     * The internal packet processor for PPPoE packets.
+     */
+    private class InternalPacketProcessor implements PacketProcessor {
+
+        @Override
+        public void process(PacketContext context) {
+            processPacketInternal(context);
+        }
+
+        private void processPacketInternal(PacketContext context) {
+            if (context == null || context.isHandled()) {
+                return;
+            }
+            Ethernet eth = context.inPacket().parsed();
+            if (eth == null) {
+                return;
+            }
+            if (!Pppoe.isPPPoES(eth) && !Pppoe.isPPPoED(eth)) {
+                return;
+            }
+            processPppoePacket(context);
+        }
+    }
+
+    /**
+     * Listener for network config events.
+     */
+    private class InternalConfigListener implements NetworkConfigListener {
+        @Override
+        public void event(NetworkConfigEvent event) {
+            switch (event.type()) {
+                case CONFIG_ADDED:
+                    log.info("CONFIG_ADDED");
+                    event.config().ifPresent(config -> {
+                        pppoeRelayConfig = ((PppoeRelayConfig) config);
+                        log.info("{} added", config.getClass().getSimpleName());
+                    });
+                    break;
+                // TODO: support at least updated and removed events
+                case CONFIG_UPDATED:
+                case CONFIG_REGISTERED:
+                case CONFIG_UNREGISTERED:
+                case CONFIG_REMOVED:
+                default:
+                    log.warn("Unsupported event type {}", event.type());
+                    break;
+            }
+        }
+
+        @Override
+        public boolean isRelevant(NetworkConfigEvent event) {
+            if (event.configClass().equals(PppoeRelayConfig.class)) {
+                return true;
+            }
+            log.debug("Ignore irrelevant event class {}", event.configClass().getName());
+            return false;
+        }
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/impl/SimpleAttachmentEventHandler.java b/app/src/main/java/org/opencord/bng/impl/SimpleAttachmentEventHandler.java
new file mode 100644
index 0000000..eb14ecb
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/impl/SimpleAttachmentEventHandler.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2019-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.opencord.bng.impl;
+
+import org.glassfish.jersey.internal.guava.Sets;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.store.service.StorageService;
+import org.opencord.bng.BngAttachment;
+import org.opencord.bng.BngService;
+import org.opencord.bng.PppoeBngAttachment;
+import org.opencord.bng.PppoeBngControlHandler;
+import org.opencord.bng.PppoeEvent;
+import org.opencord.bng.PppoeEventListener;
+import org.opencord.bng.PppoeEventSubject;
+import org.osgi.service.component.ComponentContext;
+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.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Dictionary;
+import java.util.Properties;
+import java.util.Set;
+
+import static org.opencord.bng.impl.OsgiPropertyConstants.ENABLE_LOCAL_EVENT_HANDLER;
+import static org.opencord.bng.impl.OsgiPropertyConstants.ENABLE_LOCAL_EVENT_HANDLER_DEFAULT;
+
+/**
+ * Service to intercept the PPPoE Handler events and trigger the creation of a
+ * new attachment in BNG service.
+ */
+@Component(immediate = true,
+        property = {
+                ENABLE_LOCAL_EVENT_HANDLER + ":Boolean=" + ENABLE_LOCAL_EVENT_HANDLER_DEFAULT,
+        }
+)
+public class SimpleAttachmentEventHandler {
+
+    private static final String ATTACHMENT_ID_GENERATOR_NAME = "SIMPLE_ATTACHMENT_EVENT_HANDLER_ATTACHMENT_ID";
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ComponentConfigService componentConfigService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected PppoeBngControlHandler pppoEHandlerRelay;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected BngService bngService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected StorageService storageService;
+
+    /**
+     * Whether to enable of not the local attachment event handler, for debugging/development.
+     */
+    private boolean enableLocalEventHandler = ENABLE_LOCAL_EVENT_HANDLER_DEFAULT;
+    private InternalPppoeEvent pppoeEventListener = new InternalPppoeEvent();
+
+    // Map to store the attachment that this component has submitted through the BNG Service
+    private Set<String> addedAttachmentKeys;
+
+    private ApplicationId appId;
+
+    @Activate
+    protected void activate() {
+        appId = coreService.getAppId(BngManager.BNG_APP);
+        addedAttachmentKeys = Sets.newHashSet();
+        componentConfigService.registerProperties(getClass());
+        pppoEHandlerRelay.addListener(pppoeEventListener);
+        log.info("Simple Attachment Event Handler STARTED");
+    }
+
+    @Modified
+    public void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context != null ? context.getProperties() : new Properties();
+
+        Boolean localEvent = Tools.isPropertyEnabled(properties, ENABLE_LOCAL_EVENT_HANDLER);
+        if (localEvent != null) {
+            enableLocalEventHandler = localEvent;
+        }
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        pppoEHandlerRelay.removeListener(pppoeEventListener);
+        addedAttachmentKeys = null;
+        componentConfigService.unregisterProperties(getClass(), false);
+        log.info("Simple Attachment Event Handler STOPPED");
+    }
+
+    /**
+     * Listener for BNG Attachment event for PPPoE attachments.
+     */
+    class InternalPppoeEvent implements PppoeEventListener {
+        @Override
+        public void event(PppoeEvent event) {
+            PppoeEventSubject eventInfo = event.subject();
+            String attachmentKey = BngUtils.calculateBngAttachmentKey(eventInfo);
+            switch (event.type()) {
+                case IPCP_CONF_ACK:
+                    log.debug("Received IPCP_CONF_ACK event, submit a new attachment");
+                    log.debug(eventInfo.toString());
+                    BngAttachment newAttachment = PppoeBngAttachment.builder()
+                            .withPppoeSessionId(eventInfo.getSessionId())
+                            .withApplicationId(appId)
+                            .withCTag(eventInfo.getcTag())
+                            .withSTag(eventInfo.getsTag())
+                            .withIpAddress(eventInfo.getIpAddress())
+                            .withMacAddress(eventInfo.getMacAddress())
+                            .withOnuSerial(eventInfo.getOnuSerialNumber())
+                            .withOltConnectPoint(eventInfo.getOltConnectPoint())
+                            .lineActivated(true)
+                            .build();
+                    if (!addedAttachmentKeys.add(attachmentKey)) {
+                        log.warn("Attachment ID already present. Re-submit the attachment");
+                    }
+                    bngService.setupAttachment(attachmentKey, newAttachment);
+                    break;
+
+                case SESSION_TERMINATION:
+                    attachmentKey =  BngUtils.calculateBngAttachmentKey(eventInfo);
+                    log.debug("Received SESSION_TERMINATION event, remove the attachment {}",
+                              attachmentKey);
+                    if (!addedAttachmentKeys.remove(attachmentKey)) {
+                        log.debug("Received SESSION_TERMINATION event, for attachment {} " +
+                                          "but attachment not present in local store", attachmentKey);
+                    } else {
+                        log.debug("Received SESSION_TERMINATION event, remove the attachment {}",
+                                  attachmentKey);
+                        bngService.removeAttachment(attachmentKey);
+                    }
+                    break;
+                case AUTH_FAILURE:
+                case AUTH_REQUEST:
+                case AUTH_SUCCESS:
+                case SESSION_INIT:
+                case IPCP_CONF_REQUEST:
+                case SESSION_CONFIRMATION:
+                    log.debug("Received event {}, nothing to do here.", event.type().toString());
+                    break;
+                default:
+                    throw new IllegalStateException("Unexpected value: " + event.type() +
+                                                            ", for attachment: " + attachmentKey);
+            }
+        }
+
+        @Override
+        public boolean isRelevant(PppoeEvent event) {
+            return enableLocalEventHandler &&
+                    event.subject().getClass().equals(PppoeEventSubject.class);
+        }
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/impl/package-info.java b/app/src/main/java/org/opencord/bng/impl/package-info.java
new file mode 100644
index 0000000..c8954f0
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-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.
+ */
+
+/**
+ * BNG application.
+ */
+package org.opencord.bng.impl;