/* * Copyright (c) 2016 Pantheon Technologies s.r.o. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.openflowplugin.impl.lifecycle; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Verify; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import io.netty.util.HashedWheelTimer; import io.netty.util.Timeout; import io.netty.util.TimerTask; import io.netty.util.internal.ConcurrentSet; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.opendaylight.controller.md.sal.common.api.clustering.EntityOwnershipChange; import org.opendaylight.controller.md.sal.common.api.clustering.EntityOwnershipListenerRegistration; import org.opendaylight.controller.md.sal.common.api.clustering.EntityOwnershipService; import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException; import org.opendaylight.mdsal.singleton.common.api.ClusterSingletonServiceProvider; import org.opendaylight.openflowplugin.api.openflow.OFPManager; import org.opendaylight.openflowplugin.api.openflow.connection.ConnectionContext; import org.opendaylight.openflowplugin.api.openflow.connection.ConnectionStatus; import org.opendaylight.openflowplugin.api.openflow.device.DeviceContext; import org.opendaylight.openflowplugin.api.openflow.device.DeviceInfo; import org.opendaylight.openflowplugin.api.openflow.device.DeviceManager; import org.opendaylight.openflowplugin.api.openflow.lifecycle.ContextChain; import org.opendaylight.openflowplugin.api.openflow.lifecycle.ContextChainHolder; import org.opendaylight.openflowplugin.api.openflow.lifecycle.ContextChainMastershipState; import org.opendaylight.openflowplugin.api.openflow.lifecycle.LifecycleService; import org.opendaylight.openflowplugin.api.openflow.rpc.RpcContext; import org.opendaylight.openflowplugin.api.openflow.rpc.RpcManager; import org.opendaylight.openflowplugin.api.openflow.statistics.StatisticsContext; import org.opendaylight.openflowplugin.api.openflow.statistics.StatisticsManager; import org.opendaylight.openflowplugin.impl.util.DeviceStateUtil; import org.opendaylight.yang.gen.v1.urn.opendaylight.inventory.rev130819.NodeId; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ContextChainHolderImpl implements ContextChainHolder { private static final Logger LOG = LoggerFactory.getLogger(ContextChainHolderImpl.class); private static final String CONTEXT_CREATED_FOR_CONNECTION = " context created for connection: {}"; private static final long DEFAULT_CHECK_ROLE_MASTER = 10000L; private static final String SERVICE_ENTITY_TYPE = "org.opendaylight.mdsal.ServiceEntityType"; private static final String ASYNC_SERVICE_ENTITY_TYPE = "org.opendaylight.mdsal.AsyncServiceCloseEntityType"; private final ConcurrentHashMap<DeviceInfo, ContextChain> contextChainMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap<DeviceInfo, ContextChain> withoutRoleChains = new ConcurrentHashMap<>(); private final List<DeviceInfo> markToBeRemoved = new ArrayList<>(); private final HashedWheelTimer timer; private final Long checkRoleMaster; private DeviceManager deviceManager; private RpcManager rpcManager; private StatisticsManager statisticsManager; private EntityOwnershipListenerRegistration eosListenerRegistration; private ClusterSingletonServiceProvider singletonServicesProvider; private boolean timerIsRunningRole; public ContextChainHolderImpl(final HashedWheelTimer timer) { this.timerIsRunningRole = false; this.timer = timer; this.checkRoleMaster = DEFAULT_CHECK_ROLE_MASTER; } @Override public <T extends OFPManager> void addManager(final T manager) { if (Objects.isNull(deviceManager) && manager instanceof DeviceManager) { LOG.trace("Context chain holder: Device manager OK."); deviceManager = (DeviceManager) manager; } else if (Objects.isNull(rpcManager) && manager instanceof RpcManager) { LOG.trace("Context chain holder: RPC manager OK."); rpcManager = (RpcManager) manager; } else if (Objects.isNull(statisticsManager) && manager instanceof StatisticsManager) { LOG.trace("Context chain holder: Statistics manager OK."); statisticsManager = (StatisticsManager) manager; } } @Override public ContextChain createContextChain(final ConnectionContext connectionContext) { final DeviceInfo deviceInfo = connectionContext.getDeviceInfo(); final String deviceInfoLOGValue = deviceInfo.getLOGValue(); if (LOG.isDebugEnabled()) { LOG.debug("Creating a new chain" + CONTEXT_CREATED_FOR_CONNECTION, deviceInfoLOGValue); } final ContextChain contextChain = new ContextChainImpl(connectionContext); final LifecycleService lifecycleService = new LifecycleServiceImpl(this); lifecycleService.registerDeviceRemovedHandler(deviceManager); lifecycleService.registerDeviceRemovedHandler(rpcManager); lifecycleService.registerDeviceRemovedHandler(statisticsManager); if (LOG.isDebugEnabled()) { LOG.debug("Lifecycle services" + CONTEXT_CREATED_FOR_CONNECTION, deviceInfoLOGValue); } final DeviceContext deviceContext = deviceManager.createContext(connectionContext); if (LOG.isDebugEnabled()) { LOG.debug("Device" + CONTEXT_CREATED_FOR_CONNECTION, deviceInfoLOGValue); } final RpcContext rpcContext = rpcManager.createContext(connectionContext.getDeviceInfo(), deviceContext); if (LOG.isDebugEnabled()) { LOG.debug("RPC" + CONTEXT_CREATED_FOR_CONNECTION, deviceInfoLOGValue); } final StatisticsContext statisticsContext = statisticsManager.createContext(deviceContext); if (LOG.isDebugEnabled()) { LOG.debug("Statistics" + CONTEXT_CREATED_FOR_CONNECTION, deviceInfoLOGValue); } deviceContext.setLifecycleInitializationPhaseHandler(statisticsContext); statisticsContext.setLifecycleInitializationPhaseHandler(rpcContext); statisticsContext.setInitialSubmitHandler(deviceContext); contextChain.addLifecycleService(lifecycleService); contextChain.addContext(deviceContext); contextChain.addContext(rpcContext); contextChain.addContext(statisticsContext); this.withoutRoleChains.put(deviceInfo, contextChain); if (!this.timerIsRunningRole) { this.startTimerRole(); } deviceContext.onPublished(); contextChain.registerServices(this.singletonServicesProvider); return contextChain; } @Override public ListenableFuture<Void> destroyContextChain(final DeviceInfo deviceInfo) { ContextChain chain = contextChainMap.remove(deviceInfo); if (chain != null) { chain.close(); } if (markToBeRemoved.contains(deviceInfo)) { markToBeRemoved.remove(deviceInfo); LOG.info("Removing device: {} from DS", deviceInfo.getLOGValue()); return deviceManager.removeDeviceFromOperationalDS(deviceInfo); } else { return Futures.immediateFuture(null); } } @Override public ConnectionStatus deviceConnected(final ConnectionContext connectionContext) throws Exception { DeviceInfo deviceInfo = connectionContext.getDeviceInfo(); LOG.info("Device {} connected.", deviceInfo.getLOGValue()); ContextChain contextChain = contextChainMap.get(deviceInfo); if (contextChain != null) { if (contextChain.addAuxiliaryConnection(connectionContext)) { LOG.info("An auxiliary connection was added to device: {}", deviceInfo.getLOGValue()); return ConnectionStatus.MAY_CONTINUE; } else { LOG.warn("Device {} already connected. Closing all connection to the device.", deviceInfo.getLOGValue()); destroyContextChain(deviceInfo); return ConnectionStatus.ALREADY_CONNECTED; } } else { if (LOG.isDebugEnabled()) { LOG.debug("No context chain found for device: {}, creating new.", deviceInfo.getLOGValue()); } contextChainMap.put(deviceInfo, createContextChain(connectionContext)); } return ConnectionStatus.MAY_CONTINUE; } @Override public void addSingletonServicesProvider(final ClusterSingletonServiceProvider singletonServicesProvider) { this.singletonServicesProvider = singletonServicesProvider; } @Override public void onNotAbleToStartMastership(final DeviceInfo deviceInfo, @Nonnull final String reason, final boolean mandatory) { this.withoutRoleChains.remove(deviceInfo); LOG.warn("Not able to set MASTER role on device {}, reason: {}", deviceInfo.getLOGValue(), reason); if (mandatory && contextChainMap.containsKey(deviceInfo)) { LOG.warn("This mastering is mandatory, destroying context chain and closing connection."); Futures.transform(contextChainMap.get(deviceInfo).stopChain(), new Function<Void, Object>() { @Nullable @Override public Object apply(@Nullable Void aVoid) { destroyContextChain(deviceInfo); return null; } }); } } @Override public void onMasterRoleAcquired(final DeviceInfo deviceInfo, @Nonnull final ContextChainMastershipState mastershipState) { this.withoutRoleChains.remove(deviceInfo); ContextChain contextChain = contextChainMap.get(deviceInfo); if (contextChain != null) { if (contextChain.isMastered(mastershipState)) { LOG.info("Role MASTER was granted to device {}", deviceInfo.getLOGValue()); this.sendNotificationNodeAdded(deviceInfo); } } } @Override public void onSlaveRoleAcquired(final DeviceInfo deviceInfo) { this.withoutRoleChains.remove(deviceInfo); ContextChain contextChain = contextChainMap.get(deviceInfo); if (contextChain != null) { contextChain.makeContextChainStateSlave(); } } @Override public void onSlaveRoleNotAcquired(final DeviceInfo deviceInfo) { this.withoutRoleChains.remove(deviceInfo); ContextChain contextChain = contextChainMap.get(deviceInfo); if (contextChain != null) { destroyContextChain(deviceInfo); } } @Override public void onDeviceDisconnected(final ConnectionContext connectionContext) { final DeviceInfo deviceInfo = connectionContext.getDeviceInfo(); if (deviceInfo != null) { ContextChain chain = contextChainMap.get(deviceInfo); if (chain != null) { if (chain.auxiliaryConnectionDropped(connectionContext)) { LOG.info("Auxiliary connection from device {} disconnected.", deviceInfo.getLOGValue()); } else { LOG.info("Device {} disconnected.", deviceInfo.getLOGValue()); Futures.transform(chain.connectionDropped(), new Function<Void, Object>() { @Nullable @Override public Object apply(@Nullable Void aVoid) { destroyContextChain(deviceInfo); return null; } }); } } } } @Override public void changeEntityOwnershipService(final EntityOwnershipService entityOwnershipService) { if (Objects.nonNull(this.eosListenerRegistration)) { LOG.warn("EOS Listener already registered."); } else { this.eosListenerRegistration = Verify.verifyNotNull(entityOwnershipService.registerListener (ASYNC_SERVICE_ENTITY_TYPE, this)); } } private void startTimerRole() { this.timerIsRunningRole = true; if (LOG.isDebugEnabled()) { LOG.debug("There is a context chain without role, starting timer."); } timer.newTimeout(new RoleTimerTask(), this.checkRoleMaster, TimeUnit.MILLISECONDS); } private void stopTimerRole() { this.timerIsRunningRole = false; if (LOG.isDebugEnabled()) { LOG.debug("There are no context chains, stopping timer."); } } private void timerTickRole() { if (!withoutRoleChains.isEmpty()) { this.withoutRoleChains.forEach((deviceInfo, contextChain) -> contextChain.makeDeviceSlave()); timer.newTimeout(new RoleTimerTask(), this.checkRoleMaster, TimeUnit.MILLISECONDS); } else { this.stopTimerRole(); } } @VisibleForTesting boolean checkAllManagers() { return Objects.nonNull(deviceManager) && Objects.nonNull(rpcManager) && Objects.nonNull(statisticsManager); } @Override public void close() throws Exception { this.contextChainMap.forEach((deviceInfo, contextChain) -> { if (contextChain.isMastered(ContextChainMastershipState.CHECK)) { contextChain.stopChain(); } contextChain.close(); }); if (Objects.nonNull(eosListenerRegistration)) { eosListenerRegistration.close(); } } @Override public void ownershipChanged(EntityOwnershipChange entityOwnershipChange) { if (!entityOwnershipChange.hasOwner()) { final YangInstanceIdentifier yii = entityOwnershipChange.getEntity().getId(); final YangInstanceIdentifier.NodeIdentifierWithPredicates niiwp = (YangInstanceIdentifier.NodeIdentifierWithPredicates) yii.getLastPathArgument(); String entityName = niiwp.getKeyValues().values().iterator().next().toString(); if (LOG.isDebugEnabled()) { LOG.debug("Entity {} has no owner", entityName); } if (entityName != null ){ final NodeId nodeId = new NodeId(entityName); DeviceInfo inMap = null; for (Map.Entry<DeviceInfo, ContextChain> entry : contextChainMap.entrySet()) { if (entry.getKey().getNodeId().equals(nodeId)) { inMap = entry.getKey(); break; } } if (Objects.nonNull(inMap)) { markToBeRemoved.add(inMap); } else { try { LOG.info("Removing device: {} from DS", nodeId); deviceManager .removeDeviceFromOperationalDS(DeviceStateUtil.createNodeInstanceIdentifier(nodeId)) .checkedGet(5L, TimeUnit.SECONDS); } catch (TimeoutException | TransactionCommitFailedException e) { LOG.info("Not able to remove device {} from DS. Probably removed by another cluster node.", nodeId); } } } } } private void sendNotificationNodeAdded(final DeviceInfo deviceInfo) { this.deviceManager.sendNodeAddedNotification(deviceInfo); } private class RoleTimerTask implements TimerTask { @Override public void run(Timeout timeout) throws Exception { timerTickRole(); } } }