blob: 3a76aaa0c2ce3c887eb4329076a796573620c04e [file] [log] [blame]
/*
* 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-U, {}", 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-U.
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 plane, accept anyway the attachment.
// Check if the attachment is correctly connected to the BNG user plane 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 user plane 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) {
// FIXME: use auxLocation when ONOS bug is fixed.
// Set<HostLocation> auxLocation = Set.of(new HostLocation(asgConnectPoint.deviceId(),
// asgConnectPoint.port(),
// System.currentTimeMillis()));
// Set<HostLocation> hostLocation = Set.of(new HostLocation(oltConnectPoint.deviceId(),
// oltConnectPoint.port(),
// System.currentTimeMillis()));
Set<HostLocation> hostLocation = Set.of(new HostLocation(asgConnectPoint.deviceId(),
asgConnectPoint.port(),
System.currentTimeMillis()));
var annotations = DefaultAnnotations.builder()
.set(ONU_ANNOTATION, onuSerialNumber)
.build();
Set<IpAddress> ips = hostIp != null
? ImmutableSet.of(hostIp) : ImmutableSet.of();
return new DefaultHostDescription(hostMac, sTag,
hostLocation,
// auxLocation,
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 is not available
hostProviderService.hostVanished(hostToBeRemoved);
if (bngProgrammableAvailable()) {
try {
bngProgrammable.removeAttachment(regAttachment);
} catch (BngProgrammableException ex) {
log.error("Exception when removing the attachment: " + ex.getMessage());
}
} else {
log.info("BNG-U 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-U {} already initialized", deviceId);
return;
}
if (!deviceService.isAvailable(deviceId)) {
log.info("BNG-U is currently unavailable, skip setup");
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("Setup BNG-U: {}", 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);
log.info("BNG-U setup successful!");
} catch (BngProgrammableException e) {
log.error("Error setup BNG-U, {}", 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) {
log.info("BNG-U cleanup");
try {
bngProgrammable.cleanUp(appId);
} catch (BngProgrammableException e) {
log.error("Error in BNG-U, {}", 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:
if (deviceService.isAvailable(deviceId)) {
log.debug("Event: {}, setting BNG-U", 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;
}
}
}