import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
import io.netty.util.concurrent.EventExecutor;
-import java.net.URL;
-import java.util.ArrayList;
import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.concurrent.ExecutionException;
-import org.opendaylight.aaa.encrypt.AAAEncryptionService;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
import org.opendaylight.controller.config.threadpool.ScheduledThreadPool;
import org.opendaylight.controller.config.threadpool.ThreadPool;
import org.opendaylight.mdsal.binding.api.DataBroker;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
import org.opendaylight.netconf.client.NetconfClientDispatcher;
-import org.opendaylight.netconf.client.NetconfClientSessionListener;
-import org.opendaylight.netconf.client.conf.NetconfClientConfiguration;
-import org.opendaylight.netconf.client.conf.NetconfReconnectingClientConfiguration;
-import org.opendaylight.netconf.client.conf.NetconfReconnectingClientConfigurationBuilder;
-import org.opendaylight.netconf.client.mdsal.DatastoreBackedPublicKeyAuth;
-import org.opendaylight.netconf.client.mdsal.LibraryModulesSchemas;
-import org.opendaylight.netconf.client.mdsal.LibrarySchemaSourceProvider;
-import org.opendaylight.netconf.client.mdsal.NetconfDevice.SchemaResourcesDTO;
-import org.opendaylight.netconf.client.mdsal.NetconfDeviceBuilder;
-import org.opendaylight.netconf.client.mdsal.NetconfDeviceCommunicator;
-import org.opendaylight.netconf.client.mdsal.SchemalessNetconfDevice;
import org.opendaylight.netconf.client.mdsal.api.BaseNetconfSchemas;
-import org.opendaylight.netconf.client.mdsal.api.CredentialProvider;
import org.opendaylight.netconf.client.mdsal.api.DeviceActionFactory;
-import org.opendaylight.netconf.client.mdsal.api.RemoteDevice;
import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceHandler;
import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
import org.opendaylight.netconf.client.mdsal.api.SchemaResourceManager;
-import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider;
-import org.opendaylight.netconf.client.mdsal.spi.KeepaliveSalFacade;
-import org.opendaylight.netconf.nettyutil.ReconnectStrategyFactory;
-import org.opendaylight.netconf.nettyutil.TimedReconnectStrategyFactory;
-import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.AuthenticationHandler;
-import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.LoginPasswordHandler;
-import org.opendaylight.netconf.topology.api.NetconfTopology;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Uri;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430.connection.parameters.Protocol.Name;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430.credentials.Credentials;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430.credentials.credentials.KeyAuth;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430.credentials.credentials.LoginPw;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430.credentials.credentials.LoginPwUnencrypted;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.optional.rev221225.NetconfNodeAugmentedOptional;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev221225.NetconfNode;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.TopologyKey;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node;
import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
-import org.opendaylight.yangtools.yang.common.Empty;
-import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
-import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
-import org.opendaylight.yangtools.yang.model.repo.spi.PotentialSchemaSource;
-import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceRegistration;
-import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public abstract class AbstractNetconfTopology implements NetconfTopology {
+public abstract class AbstractNetconfTopology {
private static final Logger LOG = LoggerFactory.getLogger(AbstractNetconfTopology.class);
+ private final HashMap<NodeId, NetconfNodeHandler> activeConnectors = new HashMap<>();
private final NetconfClientDispatcher clientDispatcher;
private final EventExecutor eventExecutor;
private final DeviceActionFactory deviceActionFactory;
- private final CredentialProvider credentialProvider;
- private final SslHandlerFactoryProvider sslHandlerFactoryProvider;
private final SchemaResourceManager schemaManager;
private final BaseNetconfSchemas baseSchemas;
+ private final NetconfClientConfigurationBuilderFactory builderFactory;
- protected final ScheduledThreadPool keepaliveExecutor;
- protected final ListeningExecutorService processingExecutor;
+ protected final ScheduledExecutorService keepaliveExecutor;
+ protected final Executor processingExecutor;
protected final DataBroker dataBroker;
protected final DOMMountPointService mountPointService;
protected final String topologyId;
- protected final AAAEncryptionService encryptionService;
- protected final HashMap<NodeId, NetconfConnectorDTO> activeConnectors = new HashMap<>();
protected AbstractNetconfTopology(final String topologyId, final NetconfClientDispatcher clientDispatcher,
- final EventExecutor eventExecutor, final ScheduledThreadPool keepaliveExecutor,
- final ThreadPool processingExecutor, final SchemaResourceManager schemaManager,
- final DataBroker dataBroker, final DOMMountPointService mountPointService,
- final AAAEncryptionService encryptionService,
- final DeviceActionFactory deviceActionFactory,
- final BaseNetconfSchemas baseSchemas,
- final CredentialProvider credentialProvider,
- final SslHandlerFactoryProvider sslHandlerFactoryProvider) {
+ final EventExecutor eventExecutor, final ScheduledThreadPool keepaliveExecutor,
+ final ThreadPool processingExecutor, final SchemaResourceManager schemaManager, final DataBroker dataBroker,
+ final DOMMountPointService mountPointService, final NetconfClientConfigurationBuilderFactory builderFactory,
+ final DeviceActionFactory deviceActionFactory, final BaseNetconfSchemas baseSchemas) {
this.topologyId = requireNonNull(topologyId);
this.clientDispatcher = clientDispatcher;
this.eventExecutor = eventExecutor;
- this.keepaliveExecutor = keepaliveExecutor;
- this.processingExecutor = MoreExecutors.listeningDecorator(processingExecutor.getExecutor());
+ this.keepaliveExecutor = keepaliveExecutor.getExecutor();
+ this.processingExecutor = processingExecutor.getExecutor();
this.schemaManager = requireNonNull(schemaManager);
this.deviceActionFactory = deviceActionFactory;
this.dataBroker = requireNonNull(dataBroker);
this.mountPointService = mountPointService;
- this.encryptionService = encryptionService;
+ this.builderFactory = requireNonNull(builderFactory);
this.baseSchemas = requireNonNull(baseSchemas);
- this.credentialProvider = requireNonNull(credentialProvider);
- this.sslHandlerFactoryProvider = requireNonNull(sslHandlerFactoryProvider);
// FIXME: this should be a put(), as we are initializing and will be re-populating the datastore with all the
// devices. Whatever has been there before should be nuked to properly re-align lifecycle.
LOG.debug("Topology {} initialized", topologyId);
}
- @Override
- public ListenableFuture<Empty> connectNode(final NodeId nodeId, final Node configNode) {
- LOG.info("Connecting RemoteDevice{{}} , with config {}", nodeId, hideCredentials(configNode));
- return setupConnection(nodeId, configNode);
+ // Non-final for testing
+ protected void ensureNode(final Node node) {
+ lockedEnsureNode(node);
}
- /**
- * Hiding of private credentials from node configuration (credentials data is replaced by asterisks).
- *
- * @param nodeConfiguration Node configuration container.
- * @return String representation of node configuration with credentials replaced by asterisks.
- */
- @VisibleForTesting
- public static String hideCredentials(final Node nodeConfiguration) {
- final NetconfNode netconfNodeAugmentation = nodeConfiguration.augmentation(NetconfNode.class);
- final String nodeCredentials = netconfNodeAugmentation.getCredentials().toString();
- final String nodeConfigurationString = nodeConfiguration.toString();
- return nodeConfigurationString.replace(nodeCredentials, "***");
- }
-
- @Override
- public ListenableFuture<Empty> disconnectNode(final NodeId nodeId) {
- final var nodeName = nodeId.getValue();
- LOG.debug("Disconnecting RemoteDevice{{}}", nodeName);
-
- final NetconfConnectorDTO connectorDTO = activeConnectors.remove(nodeId);
- if (connectorDTO == null) {
- return Futures.immediateFailedFuture(
- new IllegalStateException("Cannot disconnect " + nodeName + " as it is not connected"));
- }
-
- connectorDTO.close();
- return Futures.immediateFuture(Empty.value());
- }
-
- protected ListenableFuture<Empty> setupConnection(final NodeId nodeId, final Node configNode) {
- final NetconfNode netconfNode = configNode.augmentation(NetconfNode.class);
- final NetconfNodeAugmentedOptional nodeOptional = configNode.augmentation(NetconfNodeAugmentedOptional.class);
-
- requireNonNull(netconfNode.getHost());
- requireNonNull(netconfNode.getPort());
-
- final NetconfConnectorDTO deviceCommunicatorDTO = createDeviceCommunicator(nodeId, netconfNode, nodeOptional);
- final NetconfDeviceCommunicator deviceCommunicator = deviceCommunicatorDTO.getCommunicator();
- final NetconfClientSessionListener netconfClientSessionListener = deviceCommunicatorDTO.getSessionListener();
- final NetconfReconnectingClientConfiguration clientConfig =
- getClientConfig(netconfClientSessionListener, netconfNode, nodeId);
- final ListenableFuture<Empty> future =
- deviceCommunicator.initializeRemoteConnection(clientDispatcher, clientConfig);
-
- activeConnectors.put(nodeId, deviceCommunicatorDTO);
-
- Futures.addCallback(future, new FutureCallback<>() {
- @Override
- public void onSuccess(final Empty result) {
- LOG.debug("Connector for {} started succesfully", nodeId.getValue());
- }
-
- @Override
- public void onFailure(final Throwable throwable) {
- LOG.error("Connector for {} failed", nodeId.getValue(), throwable);
- // remove this node from active connectors?
- }
- }, MoreExecutors.directExecutor());
-
- return future;
- }
-
- protected NetconfConnectorDTO createDeviceCommunicator(final NodeId nodeId, final NetconfNode node,
- final NetconfNodeAugmentedOptional nodeOptional) {
- final var deviceId = NetconfNodeUtils.toRemoteDeviceId(nodeId, node);
- final long keepaliveDelay = node.requireKeepaliveDelay().toJava();
-
- final var deviceSalFacade = new NetconfTopologyDeviceSalFacade(deviceId, mountPointService,
- node.requireLockDatastore(), dataBroker);
- // The facade we are going it present to NetconfDevice
- RemoteDeviceHandler salFacade;
- final KeepaliveSalFacade keepAliveFacade;
- if (keepaliveDelay > 0) {
- LOG.info("Adding keepalive facade, for device {}", nodeId);
- salFacade = keepAliveFacade = new KeepaliveSalFacade(deviceId, deviceSalFacade,
- keepaliveExecutor.getExecutor(), keepaliveDelay, node.requireDefaultRequestTimeoutMillis().toJava());
- } else {
- salFacade = deviceSalFacade;
- keepAliveFacade = null;
+ private synchronized void lockedEnsureNode(final Node node) {
+ final var nodeId = node.requireNodeId();
+ final var prev = activeConnectors.remove(nodeId);
+ if (prev != null) {
+ LOG.info("RemoteDevice{{}} was already configured, disconnecting", nodeId);
+ prev.close();
}
-
- // Setup reconnection on empty context, if so configured
- if (nodeOptional != null && nodeOptional.getIgnoreMissingSchemaSources().getAllowed()) {
- LOG.warn("Ignoring missing schema sources is not currently implemented for {}", deviceId);
+ final var netconfNode = node.augmentation(NetconfNode.class);
+ if (netconfNode == null) {
+ LOG.warn("RemoteDevice{{}} is missing NETCONF node configuration, not connecting it", nodeId);
+ return;
}
-
- final RemoteDevice<NetconfDeviceCommunicator> device;
- final List<SchemaSourceRegistration<?>> yanglibRegistrations;
- if (node.requireSchemaless()) {
- device = new SchemalessNetconfDevice(baseSchemas, deviceId, salFacade);
- yanglibRegistrations = List.of();
- } else {
- final SchemaResourcesDTO resources = schemaManager.getSchemaResources(node.getSchemaCacheDirectory(),
- nodeId.getValue());
- device = new NetconfDeviceBuilder()
- .setReconnectOnSchemasChange(node.requireReconnectOnChangedSchema())
- .setSchemaResourcesDTO(resources)
- .setGlobalProcessingExecutor(processingExecutor)
- .setId(deviceId)
- .setSalFacade(salFacade)
- .setDeviceActionFactory(deviceActionFactory)
- .setBaseSchemas(baseSchemas)
- .build();
- yanglibRegistrations = registerDeviceSchemaSources(deviceId, node, resources);
+ final RemoteDeviceId deviceId;
+ try {
+ deviceId = NetconfNodeUtils.toRemoteDeviceId(nodeId, netconfNode);
+ } catch (NoSuchElementException e) {
+ LOG.warn("RemoteDevice{{}} has invalid configuration, not connecting it", nodeId, e);
+ return;
}
- final int rpcMessageLimit = node.requireConcurrentRpcLimit().toJava();
- if (rpcMessageLimit < 1) {
- LOG.info("Concurrent rpc limit is smaller than 1, no limit will be enforced for device {}", deviceId);
- }
+ LOG.info("Connecting RemoteDevice{{}}, with config {}", nodeId, hideCredentials(node));
- final var netconfDeviceCommunicator = new NetconfDeviceCommunicator(deviceId, device, rpcMessageLimit,
- NetconfNodeUtils.extractUserCapabilities(node));
+ // Instantiate the handler ...
+ final var nodeOptional = node.augmentation(NetconfNodeAugmentedOptional.class);
+ final var deviceSalFacade = createSalFacade(deviceId, netconfNode.requireLockDatastore());
- if (keepAliveFacade != null) {
- keepAliveFacade.setListener(netconfDeviceCommunicator);
+ final NetconfNodeHandler nodeHandler;
+ try {
+ nodeHandler = new NetconfNodeHandler(clientDispatcher, eventExecutor, keepaliveExecutor, baseSchemas,
+ schemaManager, processingExecutor, builderFactory, deviceActionFactory, deviceSalFacade, deviceId,
+ nodeId, netconfNode, nodeOptional);
+ } catch (IllegalArgumentException e) {
+ // This is a workaround for NETCONF-1114 where the encrypted password's lexical structure is not enforced
+ // in the datastore and it ends up surfacing when we decrypt the password.
+ LOG.warn("RemoteDevice{{}} failed to connect, removing from operational datastore", nodeId, e);
+ deviceSalFacade.close();
+ return;
}
- return new NetconfConnectorDTO(netconfDeviceCommunicator, salFacade, yanglibRegistrations);
+ // ... record it ...
+ activeConnectors.put(nodeId, nodeHandler);
+
+ // ... and start it
+ nodeHandler.connect();
}
- private static List<SchemaSourceRegistration<?>> registerDeviceSchemaSources(final RemoteDeviceId remoteDeviceId,
- final NetconfNode node, final SchemaResourcesDTO resources) {
- final var yangLibrary = node.getYangLibrary();
- if (yangLibrary != null) {
- final Uri uri = yangLibrary.getYangLibraryUrl();
- if (uri != null) {
- final List<SchemaSourceRegistration<?>> registrations = new ArrayList<>();
- final String yangLibURL = uri.getValue();
- final SchemaSourceRegistry schemaRegistry = resources.getSchemaRegistry();
+ // Non-final for testing
+ protected void deleteNode(final NodeId nodeId) {
+ lockedDeleteNode(nodeId);
+ }
- // pre register yang library sources as fallback schemas to schema registry
- final LibraryModulesSchemas schemas;
- final String yangLibUsername = yangLibrary.getUsername();
- final String yangLigPassword = yangLibrary.getPassword();
- if (yangLibUsername != null && yangLigPassword != null) {
- schemas = LibraryModulesSchemas.create(yangLibURL, yangLibUsername, yangLigPassword);
- } else {
- schemas = LibraryModulesSchemas.create(yangLibURL);
- }
+ private synchronized void lockedDeleteNode(final NodeId nodeId) {
+ final var nodeName = nodeId.getValue();
+ LOG.debug("Disconnecting RemoteDevice{{}}", nodeName);
- for (final Map.Entry<SourceIdentifier, URL> entry : schemas.getAvailableModels().entrySet()) {
- registrations.add(schemaRegistry.registerSchemaSource(new LibrarySchemaSourceProvider(
- remoteDeviceId, schemas.getAvailableModels()),
- PotentialSchemaSource.create(entry.getKey(), YangTextSchemaSource.class,
- PotentialSchemaSource.Costs.REMOTE_IO.getValue())));
- }
- return List.copyOf(registrations);
- }
+ final var connectorDTO = activeConnectors.remove(nodeId);
+ if (connectorDTO != null) {
+ connectorDTO.close();
}
-
- return List.of();
}
- public NetconfReconnectingClientConfiguration getClientConfig(final NetconfClientSessionListener listener,
- final NetconfNode node, final NodeId nodeId) {
- final ReconnectStrategyFactory sf = new TimedReconnectStrategyFactory(eventExecutor,
- node.requireMaxConnectionAttempts().toJava(), node.requireBetweenAttemptsTimeoutMillis().toJava(),
- node.requireSleepFactor().decimalValue());
- final NetconfReconnectingClientConfigurationBuilder reconnectingClientConfigurationBuilder;
- final var protocol = node.getProtocol();
- if (node.requireTcpOnly()) {
- reconnectingClientConfigurationBuilder = NetconfReconnectingClientConfigurationBuilder.create()
- .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TCP)
- .withAuthHandler(getHandlerFromCredentials(node.getCredentials()));
- } else if (protocol == null || protocol.getName() == Name.SSH) {
- reconnectingClientConfigurationBuilder = NetconfReconnectingClientConfigurationBuilder.create()
- .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.SSH)
- .withAuthHandler(getHandlerFromCredentials(node.getCredentials()));
- } else if (protocol.getName() == Name.TLS) {
- reconnectingClientConfigurationBuilder = NetconfReconnectingClientConfigurationBuilder.create()
- .withSslHandlerFactory(sslHandlerFactoryProvider.getSslHandlerFactory(protocol.getSpecification()))
- .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TLS);
- } else {
- throw new IllegalStateException("Unsupported protocol type: " + protocol.getName());
- }
-
- if (node.getOdlHelloMessageCapabilities() != null) {
- reconnectingClientConfigurationBuilder.withOdlHelloCapabilities(
- Lists.newArrayList(node.getOdlHelloMessageCapabilities().getCapability()));
- }
+ protected final synchronized void deleteAllNodes() {
+ activeConnectors.values().forEach(NetconfNodeHandler::close);
+ activeConnectors.clear();
+ }
- return reconnectingClientConfigurationBuilder
- .withName(nodeId.getValue())
- .withAddress(NetconfNodeUtils.toInetSocketAddress(node))
- .withConnectionTimeoutMillis(node.requireConnectionTimeoutMillis().toJava())
- .withReconnectStrategy(sf.createReconnectStrategy())
- .withConnectStrategyFactory(sf)
- .withSessionListener(listener)
- .build();
+ protected RemoteDeviceHandler createSalFacade(final RemoteDeviceId deviceId, final boolean lockDatastore) {
+ return new NetconfTopologyDeviceSalFacade(deviceId, mountPointService, lockDatastore, dataBroker);
}
- private AuthenticationHandler getHandlerFromCredentials(final Credentials credentials) {
- if (credentials
- instanceof org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430
- .credentials.credentials.LoginPassword loginPassword) {
- return new LoginPasswordHandler(loginPassword.getUsername(), loginPassword.getPassword());
- }
- if (credentials instanceof LoginPwUnencrypted unencrypted) {
- final var loginPassword = unencrypted.getLoginPasswordUnencrypted();
- return new LoginPasswordHandler(loginPassword.getUsername(), loginPassword.getPassword());
- }
- if (credentials instanceof LoginPw loginPw) {
- final var loginPassword = loginPw.getLoginPassword();
- return new LoginPasswordHandler(loginPassword.getUsername(),
- encryptionService.decrypt(loginPassword.getPassword()));
- }
- if (credentials instanceof KeyAuth keyAuth) {
- final var keyPair = keyAuth.getKeyBased();
- return new DatastoreBackedPublicKeyAuth(keyPair.getUsername(), keyPair.getKeyId(), credentialProvider,
- encryptionService);
- }
- throw new IllegalStateException("Unsupported credential type: " + credentials.getClass());
+ /**
+ * Hiding of private credentials from node configuration (credentials data is replaced by asterisks).
+ *
+ * @param nodeConfiguration Node configuration container.
+ * @return String representation of node configuration with credentials replaced by asterisks.
+ */
+ @VisibleForTesting
+ static final String hideCredentials(final Node nodeConfiguration) {
+ final var netconfNodeAugmentation = nodeConfiguration.augmentation(NetconfNode.class);
+ final var nodeCredentials = netconfNodeAugmentation.getCredentials().toString();
+ final var nodeConfigurationString = nodeConfiguration.toString();
+ return nodeConfigurationString.replace(nodeCredentials, "***");
}
}