blob: f20a085db139e1ca6961080f70ab58929cde441f [file] [log] [blame]
/*
* Copyright 2014-2015 Open Networking Laboratory
*
* 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.cordvtn;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.Service;
import org.onlab.packet.Ethernet;
import org.onlab.packet.Ip4Address;
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.dhcp.DhcpService;
import org.onosproject.mastership.MastershipService;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultAnnotations;
import org.onosproject.net.Host;
import org.onosproject.net.HostId;
import org.onosproject.net.HostLocation;
import org.onosproject.net.Port;
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.config.NetworkConfigService;
import org.onosproject.net.config.basics.SubjectFactories;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.driver.DriverService;
import org.onosproject.net.flow.FlowRuleService;
import org.onosproject.net.group.GroupService;
import org.onosproject.net.host.DefaultHostDescription;
import org.onosproject.net.host.HostDescription;
import org.onosproject.net.host.HostEvent;
import org.onosproject.net.host.HostListener;
import org.onosproject.net.host.HostProvider;
import org.onosproject.net.host.HostProviderRegistry;
import org.onosproject.net.host.HostProviderService;
import org.onosproject.net.host.HostService;
import org.onosproject.net.packet.PacketContext;
import org.onosproject.net.packet.PacketProcessor;
import org.onosproject.net.packet.PacketService;
import org.onosproject.net.provider.AbstractProvider;
import org.onosproject.net.provider.ProviderId;
import org.onosproject.openstackinterface.OpenstackInterfaceService;
import org.onosproject.openstackinterface.OpenstackNetwork;
import org.onosproject.openstackinterface.OpenstackPort;
import org.onosproject.openstackinterface.OpenstackSubnet;
import org.slf4j.Logger;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
import static org.onlab.util.Tools.groupedThreads;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Provisions virtual tenant networks with service chaining capability
* in OpenStack environment.
*/
@Component(immediate = true)
@Service
public class CordVtn extends AbstractProvider implements CordVtnService, HostProvider {
protected final Logger log = getLogger(getClass());
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected CoreService coreService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected NetworkConfigRegistry configRegistry;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected NetworkConfigService configService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected HostProviderRegistry hostProviderRegistry;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected DeviceService deviceService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected HostService hostService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected DriverService driverService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected FlowRuleService flowRuleService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected PacketService packetService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected MastershipService mastershipService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected GroupService groupService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected OpenstackInterfaceService openstackService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected DhcpService dhcpService;
private final ConfigFactory configFactory =
new ConfigFactory(SubjectFactories.APP_SUBJECT_FACTORY, CordVtnConfig.class, "cordvtn") {
@Override
public CordVtnConfig createConfig() {
return new CordVtnConfig();
}
};
private static final String DEFAULT_TUNNEL = "vxlan";
private static final String SERVICE_ID = "serviceId";
private static final String OPENSTACK_PORT_ID = "openstackPortId";
private static final String DATA_PLANE_IP = "dataPlaneIp";
private static final String DATA_PLANE_INTF = "dataPlaneIntf";
private static final String S_TAG = "stag";
private static final String VSG_HOST_ID = "vsgHostId";
private static final String CREATED_TIME = "createdTime";
private static final Ip4Address DEFAULT_DNS = Ip4Address.valueOf("8.8.8.8");
private final ExecutorService eventExecutor =
newSingleThreadScheduledExecutor(groupedThreads("onos/cordvtn", "event-handler"));
private final PacketProcessor packetProcessor = new InternalPacketProcessor();
private final HostListener hostListener = new InternalHostListener();
private final NetworkConfigListener configListener = new InternalConfigListener();
private ApplicationId appId;
private HostProviderService hostProvider;
private CordVtnRuleInstaller ruleInstaller;
private CordVtnArpProxy arpProxy;
private volatile MacAddress privateGatewayMac = MacAddress.NONE;
/**
* Creates an cordvtn host location provider.
*/
public CordVtn() {
super(new ProviderId("host", CORDVTN_APP_ID));
}
@Activate
protected void activate() {
appId = coreService.registerApplication("org.onosproject.cordvtn");
ruleInstaller = new CordVtnRuleInstaller(appId, flowRuleService,
deviceService,
driverService,
groupService,
configRegistry,
DEFAULT_TUNNEL);
arpProxy = new CordVtnArpProxy(appId, packetService, hostService);
packetService.addProcessor(packetProcessor, PacketProcessor.director(0));
arpProxy.requestPacket();
hostService.addListener(hostListener);
hostProvider = hostProviderRegistry.register(this);
configRegistry.registerConfigFactory(configFactory);
configService.addListener(configListener);
log.info("Started");
}
@Deactivate
protected void deactivate() {
hostProviderRegistry.unregister(this);
hostService.removeListener(hostListener);
packetService.removeProcessor(packetProcessor);
configRegistry.unregisterConfigFactory(configFactory);
configService.removeListener(configListener);
eventExecutor.shutdown();
log.info("Stopped");
}
@Override
public void triggerProbe(Host host) {
/*
* Note: In CORD deployment, we assume that all hosts are configured.
* Therefore no probe is required.
*/
}
@Override
public void createServiceDependency(CordServiceId tServiceId, CordServiceId pServiceId,
boolean isBidirectional) {
CordService tService = getCordService(tServiceId);
CordService pService = getCordService(pServiceId);
if (tService == null || pService == null) {
log.error("Failed to create CordService for {}", tServiceId.id());
return;
}
log.info("Service dependency from {} to {} created.", tService.id().id(), pService.id().id());
ruleInstaller.populateServiceDependencyRules(tService, pService, isBidirectional);
}
@Override
public void removeServiceDependency(CordServiceId tServiceId, CordServiceId pServiceId) {
CordService tService = getCordService(tServiceId);
CordService pService = getCordService(pServiceId);
if (tService == null || pService == null) {
log.error("Failed to create CordService for {}", tServiceId.id());
return;
}
log.info("Service dependency from {} to {} removed.", tService.id().id(), pService.id().id());
ruleInstaller.removeServiceDependencyRules(tService, pService);
}
@Override
public void addServiceVm(CordVtnNode node, ConnectPoint connectPoint) {
Port port = deviceService.getPort(connectPoint.deviceId(), connectPoint.port());
OpenstackPort vPort = openstackService.port(port);
if (vPort == null) {
log.warn("Failed to get OpenstackPort for {}", getPortName(port));
return;
}
MacAddress mac = vPort.macAddress();
HostId hostId = HostId.hostId(mac);
Host existingHost = hostService.getHost(hostId);
if (existingHost != null) {
String serviceId = existingHost.annotations().value(SERVICE_ID);
if (serviceId == null || !serviceId.equals(vPort.networkId())) {
// this host is not injected by cordvtn or a stale host, remove it
hostProvider.hostVanished(existingHost.id());
}
}
// Included CREATED_TIME to annotation intentionally to trigger HOST_UPDATED
// event so that the flow rule population for this host can happen.
// This ensures refreshing data plane by pushing network config always make
// the data plane synced.
Set<IpAddress> fixedIp = Sets.newHashSet(vPort.fixedIps().values());
DefaultAnnotations.Builder annotations = DefaultAnnotations.builder()
.set(SERVICE_ID, vPort.networkId())
.set(OPENSTACK_PORT_ID, vPort.id())
.set(DATA_PLANE_IP, node.dpIp().ip().toString())
.set(DATA_PLANE_INTF, node.dpIntf())
.set(CREATED_TIME, String.valueOf(System.currentTimeMillis()));
String serviceVlan = getServiceVlan(vPort);
if (serviceVlan != null) {
annotations.set(S_TAG, serviceVlan);
}
HostDescription hostDesc = new DefaultHostDescription(
mac,
VlanId.NONE,
new HostLocation(connectPoint, System.currentTimeMillis()),
fixedIp,
annotations.build());
hostProvider.hostDetected(hostId, hostDesc, false);
}
@Override
public void removeServiceVm(ConnectPoint connectPoint) {
hostService.getConnectedHosts(connectPoint)
.stream()
.forEach(host -> hostProvider.hostVanished(host.id()));
}
@Override
public void updateVirtualSubscriberGateways(HostId vSgHostId, String serviceVlan,
Map<IpAddress, MacAddress> vSgs) {
Host vSgHost = hostService.getHost(vSgHostId);
if (vSgHost == null || !vSgHost.annotations().value(S_TAG).equals(serviceVlan)) {
log.debug("Invalid vSG updates for {}", serviceVlan);
return;
}
log.info("Updates vSGs in {} with {}", vSgHost.id(), vSgs.toString());
vSgs.entrySet().stream()
.filter(entry -> hostService.getHostsByMac(entry.getValue()).isEmpty())
.forEach(entry -> addVirtualSubscriberGateway(
vSgHost,
entry.getKey(),
entry.getValue(),
serviceVlan));
hostService.getConnectedHosts(vSgHost.location()).stream()
.filter(host -> !host.mac().equals(vSgHost.mac()))
.filter(host -> !vSgs.values().contains(host.mac()))
.forEach(host -> {
log.info("Removed vSG {}", host.toString());
hostProvider.hostVanished(host.id());
});
}
/**
* Adds virtual subscriber gateway to the system.
*
* @param vSgHost host virtual machine of this vSG
* @param vSgIp vSG ip address
* @param vSgMac vSG mac address
* @param serviceVlan service vlan
*/
private void addVirtualSubscriberGateway(Host vSgHost, IpAddress vSgIp, MacAddress vSgMac,
String serviceVlan) {
log.info("vSG with IP({}) MAC({}) added", vSgIp.toString(), vSgMac.toString());
HostId hostId = HostId.hostId(vSgMac);
DefaultAnnotations.Builder annotations = DefaultAnnotations.builder()
.set(S_TAG, serviceVlan)
.set(VSG_HOST_ID, vSgHost.id().toString())
.set(CREATED_TIME, String.valueOf(System.currentTimeMillis()));
HostDescription hostDesc = new DefaultHostDescription(
vSgMac,
VlanId.NONE,
vSgHost.location(),
Sets.newHashSet(vSgIp),
annotations.build());
hostProvider.hostDetected(hostId, hostDesc, false);
}
/**
* Returns public ip addresses of vSGs running inside a give vSG host.
*
* @param vSgHost vSG host
* @return map of ip and mac address, or empty map
*/
private Map<IpAddress, MacAddress> getSubscriberGateways(Host vSgHost) {
String vPortId = vSgHost.annotations().value(OPENSTACK_PORT_ID);
String serviceVlan = vSgHost.annotations().value(S_TAG);
OpenstackPort vPort = openstackService.port(vPortId);
if (vPort == null) {
log.warn("Failed to get OpenStack port {} for VM {}", vPortId, vSgHost.id());
return Maps.newHashMap();
}
if (!serviceVlan.equals(getServiceVlan(vPort))) {
log.error("Host({}) s-tag does not match with vPort s-tag", vSgHost.id());
return Maps.newHashMap();
}
return vPort.allowedAddressPairs();
}
/**
* Returns CordService by service ID.
*
* @param serviceId service id
* @return cord service, or null if it fails to get network from OpenStack
*/
private CordService getCordService(CordServiceId serviceId) {
OpenstackNetwork vNet = openstackService.network(serviceId.id());
if (vNet == null) {
log.warn("Couldn't find OpenStack network for service {}", serviceId.id());
return null;
}
OpenstackSubnet subnet = vNet.subnets().stream()
.findFirst()
.orElse(null);
if (subnet == null) {
log.warn("Couldn't find OpenStack subnet for service {}", serviceId.id());
return null;
}
Set<CordServiceId> tServices = Sets.newHashSet();
// TODO get tenant services from XOS
Map<Host, IpAddress> hosts = getHostsWithOpenstackNetwork(vNet)
.stream()
.collect(Collectors.toMap(host -> host, this::getTunnelIp));
return new CordService(vNet, subnet, hosts, tServices);
}
/**
* Returns CordService by OpenStack network.
*
* @param vNet OpenStack network
* @return cord service
*/
private CordService getCordService(OpenstackNetwork vNet) {
checkNotNull(vNet);
CordServiceId serviceId = CordServiceId.of(vNet.id());
OpenstackSubnet subnet = vNet.subnets().stream()
.findFirst()
.orElse(null);
if (subnet == null) {
log.warn("Couldn't find OpenStack subnet for service {}", serviceId);
return null;
}
Set<CordServiceId> tServices = Sets.newHashSet();
// TODO get tenant services from XOS
Map<Host, IpAddress> hosts = getHostsWithOpenstackNetwork(vNet)
.stream()
.collect(Collectors.toMap(host -> host, this::getTunnelIp));
return new CordService(vNet, subnet, hosts, tServices);
}
/**
* Returns IP address for tunneling for a given host.
*
* @param host host
* @return ip address, or null
*/
private IpAddress getTunnelIp(Host host) {
String ip = host.annotations().value(DATA_PLANE_IP);
return ip == null ? null : IpAddress.valueOf(ip);
}
/**
* Returns port name.
*
* @param port port
* @return port name
*/
private String getPortName(Port port) {
return port.annotations().value("portName");
}
/**
* Returns s-tag from a given OpenStack port.
*
* @param vPort openstack port
* @return s-tag string
*/
private String getServiceVlan(OpenstackPort vPort) {
checkNotNull(vPort);
if (vPort.name() != null && vPort.name().startsWith(S_TAG)) {
return vPort.name().split("-")[1];
} else {
return null;
}
}
/**
* Returns service ID of this host.
*
* @param host host
* @return service id, or null if not found
*/
private String getServiceId(Host host) {
return host.annotations().value(SERVICE_ID);
}
/**
* Returns hosts associated with a given OpenStack network.
*
* @param vNet openstack network
* @return set of hosts
*/
private Set<Host> getHostsWithOpenstackNetwork(OpenstackNetwork vNet) {
checkNotNull(vNet);
String vNetId = vNet.id();
return StreamSupport.stream(hostService.getHosts().spliterator(), false)
.filter(host -> Objects.equals(vNetId, getServiceId(host)))
.collect(Collectors.toSet());
}
/**
* Registers static DHCP lease for a given host.
*
* @param host host
* @param service cord service
*/
private void registerDhcpLease(Host host, CordService service) {
List<Ip4Address> options = Lists.newArrayList();
options.add(Ip4Address.makeMaskPrefix(service.serviceIpRange().prefixLength()));
options.add(service.serviceIp().getIp4Address());
options.add(service.serviceIp().getIp4Address());
options.add(DEFAULT_DNS);
log.debug("Set static DHCP mapping for {}", host.mac());
dhcpService.setStaticMapping(host.mac(),
host.ipAddresses().stream().findFirst().get().getIp4Address(),
true,
options);
}
/**
* Handles VM detected situation.
*
* @param host host
*/
private void serviceVmAdded(Host host) {
String serviceVlan = host.annotations().value(S_TAG);
if (serviceVlan != null) {
virtualSubscriberGatewayAdded(host, serviceVlan);
}
String vNetId = host.annotations().value(SERVICE_ID);
if (vNetId == null) {
// ignore this host, it is not the service VM, or it's a vSG
return;
}
OpenstackNetwork vNet = openstackService.network(vNetId);
if (vNet == null) {
log.warn("Failed to get OpenStack network {} for VM {}.",
vNetId, host.id());
return;
}
log.info("VM is detected, MAC: {} IP: {}",
host.mac(),
host.ipAddresses().stream().findFirst().get());
CordService service = getCordService(vNet);
if (service == null) {
return;
}
switch (service.serviceType()) {
case MANAGEMENT:
ruleInstaller.populateManagementNetworkRules(host, service);
break;
case PRIVATE:
arpProxy.addGateway(service.serviceIp(), privateGatewayMac);
case PUBLIC:
default:
// TODO check if the service needs an update on its group buckets after done CORD-433
ruleInstaller.updateServiceGroup(service);
// sends gratuitous ARP here for the case of adding existing VMs
// when ONOS or cordvtn app is restarted
arpProxy.sendGratuitousArpForGateway(service.serviceIp(), Sets.newHashSet(host));
break;
}
registerDhcpLease(host, service);
ruleInstaller.populateBasicConnectionRules(host, getTunnelIp(host), vNet);
}
/**
* Handles VM removed situation.
*
* @param host host
*/
private void serviceVmRemoved(Host host) {
String serviceVlan = host.annotations().value(S_TAG);
if (serviceVlan != null) {
virtualSubscriberGatewayRemoved(host);
}
String vNetId = host.annotations().value(SERVICE_ID);
if (vNetId == null) {
// ignore it, it's not the service VM or it's a vSG
return;
}
OpenstackNetwork vNet = openstackService.network(vNetId);
if (vNet == null) {
log.warn("Failed to get OpenStack network {} for VM {}",
vNetId, host.id());
return;
}
log.info("VM is vanished, MAC: {} IP: {}",
host.mac(),
host.ipAddresses().stream().findFirst().get());
ruleInstaller.removeBasicConnectionRules(host);
dhcpService.removeStaticMapping(host.mac());
CordService service = getCordService(vNet);
if (service == null) {
return;
}
switch (service.serviceType()) {
case MANAGEMENT:
ruleInstaller.removeManagementNetworkRules(host, service);
break;
case PRIVATE:
if (getHostsWithOpenstackNetwork(vNet).isEmpty()) {
arpProxy.removeGateway(service.serviceIp());
}
case PUBLIC:
default:
// TODO check if the service needs an update on its group buckets after done CORD-433
ruleInstaller.updateServiceGroup(service);
break;
}
}
/**
* Handles virtual subscriber gateway VM or container.
*
* @param host new host with stag, it can be vsg VM or vsg
* @param serviceVlan service vlan
*/
private void virtualSubscriberGatewayAdded(Host host, String serviceVlan) {
Map<IpAddress, MacAddress> vSgs;
Host vSgHost;
String vSgHostId = host.annotations().value(VSG_HOST_ID);
if (vSgHostId == null) {
log.debug("vSG VM detected {}", host.id());
vSgHost = host;
vSgs = getSubscriberGateways(vSgHost);
vSgs.entrySet().stream().forEach(entry -> addVirtualSubscriberGateway(
vSgHost,
entry.getKey(),
entry.getValue(),
serviceVlan));
} else {
vSgHost = hostService.getHost(HostId.hostId(vSgHostId));
if (vSgHost == null) {
return;
}
log.debug("vSG detected {}", host.id());
vSgs = getSubscriberGateways(vSgHost);
}
ruleInstaller.populateSubscriberGatewayRules(vSgHost, vSgs.keySet());
}
/**
* Handles virtual subscriber gateway removed.
*
* @param vSg vsg host to remove
*/
private void virtualSubscriberGatewayRemoved(Host vSg) {
String vSgHostId = vSg.annotations().value(VSG_HOST_ID);
if (vSgHostId == null) {
return;
}
Host vSgHost = hostService.getHost(HostId.hostId(vSgHostId));
if (vSgHost == null) {
return;
}
log.info("vSG removed {}", vSg.id());
Map<IpAddress, MacAddress> vSgs = getSubscriberGateways(vSgHost);
ruleInstaller.populateSubscriberGatewayRules(vSgHost, vSgs.keySet());
}
/**
* Sets service network gateway MAC address and sends out gratuitous ARP to all
* VMs to update the gateway MAC address.
*
* @param newMac mac address to update
*/
private void setPrivateGatewayMac(MacAddress newMac) {
if (newMac == null || newMac.equals(privateGatewayMac)) {
// no updates, do nothing
return;
}
privateGatewayMac = newMac;
log.debug("Set service gateway MAC address to {}", privateGatewayMac.toString());
// TODO get existing service list from XOS and replace the loop below
Set<String> vNets = Sets.newHashSet();
hostService.getHosts().forEach(host -> vNets.add(host.annotations().value(SERVICE_ID)));
vNets.remove(null);
vNets.stream().forEach(vNet -> {
CordService service = getCordService(CordServiceId.of(vNet));
if (service != null) {
arpProxy.addGateway(service.serviceIp(), privateGatewayMac);
arpProxy.sendGratuitousArpForGateway(service.serviceIp(), service.hosts().keySet());
}
});
}
/**
* Sets public gateway MAC address.
*
* @param publicGateways gateway ip and mac address pairs
*/
private void setPublicGatewayMac(Map<IpAddress, MacAddress> publicGateways) {
publicGateways.entrySet()
.stream()
.forEach(entry -> {
arpProxy.addGateway(entry.getKey(), entry.getValue());
log.info("Added public gateway IP {}, MAC {}",
entry.getKey().toString(), entry.getValue().toString());
});
// TODO notice gateway MAC change to VMs holds this gateway IP
}
/**
* Updates configurations.
*/
private void readConfiguration() {
CordVtnConfig config = configRegistry.getConfig(appId, CordVtnConfig.class);
if (config == null) {
log.debug("No configuration found");
return;
}
setPrivateGatewayMac(config.privateGatewayMac());
setPublicGatewayMac(config.publicGateways());
}
private class InternalHostListener implements HostListener {
@Override
public void event(HostEvent event) {
Host host = event.subject();
if (!mastershipService.isLocalMaster(host.location().deviceId())) {
// do not allow to proceed without mastership
return;
}
switch (event.type()) {
case HOST_UPDATED:
case HOST_ADDED:
eventExecutor.submit(() -> serviceVmAdded(host));
break;
case HOST_REMOVED:
eventExecutor.submit(() -> serviceVmRemoved(host));
break;
default:
break;
}
}
}
private class InternalPacketProcessor implements PacketProcessor {
@Override
public void process(PacketContext context) {
if (context.isHandled()) {
return;
}
Ethernet ethPacket = context.inPacket().parsed();
if (ethPacket == null || ethPacket.getEtherType() != Ethernet.TYPE_ARP) {
return;
}
arpProxy.processArpPacket(context, ethPacket);
}
}
private class InternalConfigListener implements NetworkConfigListener {
@Override
public void event(NetworkConfigEvent event) {
if (!event.configClass().equals(CordVtnConfig.class)) {
return;
}
switch (event.type()) {
case CONFIG_ADDED:
case CONFIG_UPDATED:
log.info("Network configuration changed");
eventExecutor.execute(CordVtn.this::readConfiguration);
break;
default:
break;
}
}
}
}