1cd5baa741d9381862b97cc2f3177e8d6087cf2c
[netconf.git] / apps / netconf-topology / src / main / java / org / opendaylight / netconf / topology / spi / AbstractNetconfTopology.java
1 /*
2  * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.topology.spi;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.VisibleForTesting;
13 import com.google.common.collect.Lists;
14 import com.google.common.util.concurrent.FutureCallback;
15 import com.google.common.util.concurrent.Futures;
16 import com.google.common.util.concurrent.ListenableFuture;
17 import com.google.common.util.concurrent.ListeningExecutorService;
18 import com.google.common.util.concurrent.MoreExecutors;
19 import io.netty.util.concurrent.EventExecutor;
20 import java.net.URL;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.concurrent.ExecutionException;
26 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
27 import org.opendaylight.controller.config.threadpool.ScheduledThreadPool;
28 import org.opendaylight.controller.config.threadpool.ThreadPool;
29 import org.opendaylight.mdsal.binding.api.DataBroker;
30 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
31 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
32 import org.opendaylight.netconf.client.NetconfClientDispatcher;
33 import org.opendaylight.netconf.client.NetconfClientSessionListener;
34 import org.opendaylight.netconf.client.conf.NetconfClientConfiguration;
35 import org.opendaylight.netconf.client.conf.NetconfReconnectingClientConfiguration;
36 import org.opendaylight.netconf.client.conf.NetconfReconnectingClientConfigurationBuilder;
37 import org.opendaylight.netconf.client.mdsal.api.DeviceActionFactory;
38 import org.opendaylight.netconf.client.mdsal.api.RemoteDevice;
39 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceHandler;
40 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
41 import org.opendaylight.netconf.client.mdsal.api.SchemaResourceManager;
42 import org.opendaylight.netconf.nettyutil.ReconnectStrategyFactory;
43 import org.opendaylight.netconf.nettyutil.TimedReconnectStrategyFactory;
44 import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.AuthenticationHandler;
45 import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.LoginPasswordHandler;
46 import org.opendaylight.netconf.sal.connect.netconf.LibraryModulesSchemas;
47 import org.opendaylight.netconf.sal.connect.netconf.NetconfDevice.SchemaResourcesDTO;
48 import org.opendaylight.netconf.sal.connect.netconf.NetconfDeviceBuilder;
49 import org.opendaylight.netconf.sal.connect.netconf.SchemalessNetconfDevice;
50 import org.opendaylight.netconf.sal.connect.netconf.auth.DatastoreBackedPublicKeyAuth;
51 import org.opendaylight.netconf.sal.connect.netconf.listener.NetconfDeviceCommunicator;
52 import org.opendaylight.netconf.sal.connect.netconf.sal.KeepaliveSalFacade;
53 import org.opendaylight.netconf.sal.connect.netconf.sal.NetconfKeystoreAdapter;
54 import org.opendaylight.netconf.sal.connect.netconf.schema.YangLibrarySchemaYangSourceProvider;
55 import org.opendaylight.netconf.sal.connect.netconf.schema.mapping.BaseNetconfSchemas;
56 import org.opendaylight.netconf.sal.connect.util.SslHandlerFactoryImpl;
57 import org.opendaylight.netconf.topology.api.NetconfTopology;
58 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Uri;
59 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430.connection.parameters.Protocol.Name;
60 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430.credentials.Credentials;
61 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430.credentials.credentials.KeyAuth;
62 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430.credentials.credentials.LoginPw;
63 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430.credentials.credentials.LoginPwUnencrypted;
64 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.optional.rev221225.NetconfNodeAugmentedOptional;
65 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev221225.NetconfNode;
66 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology;
67 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
68 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.TopologyId;
69 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.Topology;
70 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.TopologyBuilder;
71 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.TopologyKey;
72 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node;
73 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
74 import org.opendaylight.yangtools.yang.common.Empty;
75 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
76 import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
77 import org.opendaylight.yangtools.yang.model.repo.spi.PotentialSchemaSource;
78 import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceRegistration;
79 import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceRegistry;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
82
83 public abstract class AbstractNetconfTopology implements NetconfTopology {
84     private static final Logger LOG = LoggerFactory.getLogger(AbstractNetconfTopology.class);
85
86     private final NetconfClientDispatcher clientDispatcher;
87     private final EventExecutor eventExecutor;
88     private final DeviceActionFactory deviceActionFactory;
89     private final NetconfKeystoreAdapter keystoreAdapter;
90     private final SchemaResourceManager schemaManager;
91     private final BaseNetconfSchemas baseSchemas;
92
93     protected final ScheduledThreadPool keepaliveExecutor;
94     protected final ListeningExecutorService processingExecutor;
95     protected final DataBroker dataBroker;
96     protected final DOMMountPointService mountPointService;
97     protected final String topologyId;
98     protected final AAAEncryptionService encryptionService;
99     protected final HashMap<NodeId, NetconfConnectorDTO> activeConnectors = new HashMap<>();
100
101     protected AbstractNetconfTopology(final String topologyId, final NetconfClientDispatcher clientDispatcher,
102                                       final EventExecutor eventExecutor, final ScheduledThreadPool keepaliveExecutor,
103                                       final ThreadPool processingExecutor, final SchemaResourceManager schemaManager,
104                                       final DataBroker dataBroker, final DOMMountPointService mountPointService,
105                                       final AAAEncryptionService encryptionService,
106                                       final DeviceActionFactory deviceActionFactory,
107                                       final BaseNetconfSchemas baseSchemas) {
108         this.topologyId = requireNonNull(topologyId);
109         this.clientDispatcher = clientDispatcher;
110         this.eventExecutor = eventExecutor;
111         this.keepaliveExecutor = keepaliveExecutor;
112         this.processingExecutor = MoreExecutors.listeningDecorator(processingExecutor.getExecutor());
113         this.schemaManager = requireNonNull(schemaManager);
114         this.deviceActionFactory = deviceActionFactory;
115         this.dataBroker = requireNonNull(dataBroker);
116         this.mountPointService = mountPointService;
117         this.encryptionService = encryptionService;
118         this.baseSchemas = requireNonNull(baseSchemas);
119
120         keystoreAdapter = new NetconfKeystoreAdapter(dataBroker);
121
122         // FIXME: this should be a put(), as we are initializing and will be re-populating the datastore with all the
123         //        devices. Whatever has been there before should be nuked to properly re-align lifecycle.
124         final var wtx = dataBroker.newWriteOnlyTransaction();
125         wtx.merge(LogicalDatastoreType.OPERATIONAL, InstanceIdentifier.builder(NetworkTopology.class)
126             .child(Topology.class, new TopologyKey(new TopologyId(topologyId)))
127             .build(), new TopologyBuilder().setTopologyId(new TopologyId(topologyId)).build());
128         final var future = wtx.commit();
129         try {
130             future.get();
131         } catch (InterruptedException | ExecutionException e) {
132             LOG.error("Unable to initialize topology {}", topologyId, e);
133             throw new IllegalStateException(e);
134         }
135
136         LOG.debug("Topology {} initialized", topologyId);
137     }
138
139     @Override
140     public ListenableFuture<Empty> connectNode(final NodeId nodeId, final Node configNode) {
141         LOG.info("Connecting RemoteDevice{{}} , with config {}", nodeId, hideCredentials(configNode));
142         return setupConnection(nodeId, configNode);
143     }
144
145     /**
146      * Hiding of private credentials from node configuration (credentials data is replaced by asterisks).
147      *
148      * @param nodeConfiguration Node configuration container.
149      * @return String representation of node configuration with credentials replaced by asterisks.
150      */
151     @VisibleForTesting
152     public static String hideCredentials(final Node nodeConfiguration) {
153         final NetconfNode netconfNodeAugmentation = nodeConfiguration.augmentation(NetconfNode.class);
154         final String nodeCredentials = netconfNodeAugmentation.getCredentials().toString();
155         final String nodeConfigurationString = nodeConfiguration.toString();
156         return nodeConfigurationString.replace(nodeCredentials, "***");
157     }
158
159     @Override
160     public ListenableFuture<Empty> disconnectNode(final NodeId nodeId) {
161         final var nodeName = nodeId.getValue();
162         LOG.debug("Disconnecting RemoteDevice{{}}", nodeName);
163
164         final NetconfConnectorDTO connectorDTO = activeConnectors.remove(nodeId);
165         if (connectorDTO == null) {
166             return Futures.immediateFailedFuture(
167                 new IllegalStateException("Cannot disconnect " + nodeName + " as it is not connected"));
168         }
169
170         connectorDTO.close();
171         return Futures.immediateFuture(Empty.value());
172     }
173
174     protected ListenableFuture<Empty> setupConnection(final NodeId nodeId, final Node configNode) {
175         final NetconfNode netconfNode = configNode.augmentation(NetconfNode.class);
176         final NetconfNodeAugmentedOptional nodeOptional = configNode.augmentation(NetconfNodeAugmentedOptional.class);
177
178         requireNonNull(netconfNode.getHost());
179         requireNonNull(netconfNode.getPort());
180
181         final NetconfConnectorDTO deviceCommunicatorDTO = createDeviceCommunicator(nodeId, netconfNode, nodeOptional);
182         final NetconfDeviceCommunicator deviceCommunicator = deviceCommunicatorDTO.getCommunicator();
183         final NetconfClientSessionListener netconfClientSessionListener = deviceCommunicatorDTO.getSessionListener();
184         final NetconfReconnectingClientConfiguration clientConfig =
185                 getClientConfig(netconfClientSessionListener, netconfNode, nodeId);
186         final ListenableFuture<Empty> future =
187                 deviceCommunicator.initializeRemoteConnection(clientDispatcher, clientConfig);
188
189         activeConnectors.put(nodeId, deviceCommunicatorDTO);
190
191         Futures.addCallback(future, new FutureCallback<>() {
192             @Override
193             public void onSuccess(final Empty result) {
194                 LOG.debug("Connector for {} started succesfully", nodeId.getValue());
195             }
196
197             @Override
198             public void onFailure(final Throwable throwable) {
199                 LOG.error("Connector for {} failed", nodeId.getValue(), throwable);
200                 // remove this node from active connectors?
201             }
202         }, MoreExecutors.directExecutor());
203
204         return future;
205     }
206
207     protected NetconfConnectorDTO createDeviceCommunicator(final NodeId nodeId, final NetconfNode node,
208             final NetconfNodeAugmentedOptional nodeOptional) {
209         final var deviceId = NetconfNodeUtils.toRemoteDeviceId(nodeId, node);
210         final long keepaliveDelay = node.requireKeepaliveDelay().toJava();
211
212         final var deviceSalFacade = new NetconfTopologyDeviceSalFacade(deviceId, mountPointService,
213             node.requireLockDatastore(), dataBroker);
214         // The facade we are going it present to NetconfDevice
215         RemoteDeviceHandler salFacade;
216         final KeepaliveSalFacade keepAliveFacade;
217         if (keepaliveDelay > 0) {
218             LOG.info("Adding keepalive facade, for device {}", nodeId);
219             salFacade = keepAliveFacade = new KeepaliveSalFacade(deviceId, deviceSalFacade,
220                 keepaliveExecutor.getExecutor(), keepaliveDelay, node.requireDefaultRequestTimeoutMillis().toJava());
221         } else {
222             salFacade = deviceSalFacade;
223             keepAliveFacade = null;
224         }
225
226         // Setup reconnection on empty context, if so configured
227         if (nodeOptional != null && nodeOptional.getIgnoreMissingSchemaSources().getAllowed()) {
228             LOG.warn("Ignoring missing schema sources is not currently implemented for {}", deviceId);
229         }
230
231         final RemoteDevice<NetconfDeviceCommunicator> device;
232         final List<SchemaSourceRegistration<?>> yanglibRegistrations;
233         if (node.requireSchemaless()) {
234             device = new SchemalessNetconfDevice(baseSchemas, deviceId, salFacade);
235             yanglibRegistrations = List.of();
236         } else {
237             final SchemaResourcesDTO resources = schemaManager.getSchemaResources(node.getSchemaCacheDirectory(),
238                 nodeId.getValue());
239             device = new NetconfDeviceBuilder()
240                 .setReconnectOnSchemasChange(node.requireReconnectOnChangedSchema())
241                 .setSchemaResourcesDTO(resources)
242                 .setGlobalProcessingExecutor(processingExecutor)
243                 .setId(deviceId)
244                 .setSalFacade(salFacade)
245                 .setDeviceActionFactory(deviceActionFactory)
246                 .setBaseSchemas(baseSchemas)
247                 .build();
248             yanglibRegistrations = registerDeviceSchemaSources(deviceId, node, resources);
249         }
250
251         final int rpcMessageLimit = node.requireConcurrentRpcLimit().toJava();
252         if (rpcMessageLimit < 1) {
253             LOG.info("Concurrent rpc limit is smaller than 1, no limit will be enforced for device {}", deviceId);
254         }
255
256         final var netconfDeviceCommunicator = new NetconfDeviceCommunicator(deviceId, device, rpcMessageLimit,
257             NetconfNodeUtils.extractUserCapabilities(node));
258
259         if (keepAliveFacade != null) {
260             keepAliveFacade.setListener(netconfDeviceCommunicator);
261         }
262
263         return new NetconfConnectorDTO(netconfDeviceCommunicator, salFacade, yanglibRegistrations);
264     }
265
266     private static List<SchemaSourceRegistration<?>> registerDeviceSchemaSources(final RemoteDeviceId remoteDeviceId,
267             final NetconfNode node, final SchemaResourcesDTO resources) {
268         final var yangLibrary = node.getYangLibrary();
269         if (yangLibrary != null) {
270             final Uri uri = yangLibrary.getYangLibraryUrl();
271             if (uri != null) {
272                 final List<SchemaSourceRegistration<?>> registrations = new ArrayList<>();
273                 final String yangLibURL = uri.getValue();
274                 final SchemaSourceRegistry schemaRegistry = resources.getSchemaRegistry();
275
276                 // pre register yang library sources as fallback schemas to schema registry
277                 final LibraryModulesSchemas schemas;
278                 final String yangLibUsername = yangLibrary.getUsername();
279                 final String yangLigPassword = yangLibrary.getPassword();
280                 if (yangLibUsername != null && yangLigPassword != null) {
281                     schemas = LibraryModulesSchemas.create(yangLibURL, yangLibUsername, yangLigPassword);
282                 } else {
283                     schemas = LibraryModulesSchemas.create(yangLibURL);
284                 }
285
286                 for (final Map.Entry<SourceIdentifier, URL> entry : schemas.getAvailableModels().entrySet()) {
287                     registrations.add(schemaRegistry.registerSchemaSource(new YangLibrarySchemaYangSourceProvider(
288                         remoteDeviceId, schemas.getAvailableModels()),
289                         PotentialSchemaSource.create(entry.getKey(), YangTextSchemaSource.class,
290                             PotentialSchemaSource.Costs.REMOTE_IO.getValue())));
291                 }
292                 return List.copyOf(registrations);
293             }
294         }
295
296         return List.of();
297     }
298
299     public NetconfReconnectingClientConfiguration getClientConfig(final NetconfClientSessionListener listener,
300                                                                   final NetconfNode node, final NodeId nodeId) {
301         final ReconnectStrategyFactory sf = new TimedReconnectStrategyFactory(eventExecutor,
302                 node.requireMaxConnectionAttempts().toJava(), node.requireBetweenAttemptsTimeoutMillis().toJava(),
303                 node.requireSleepFactor().decimalValue());
304         final NetconfReconnectingClientConfigurationBuilder reconnectingClientConfigurationBuilder;
305         final var protocol = node.getProtocol();
306         if (node.requireTcpOnly()) {
307             reconnectingClientConfigurationBuilder = NetconfReconnectingClientConfigurationBuilder.create()
308                     .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TCP)
309                     .withAuthHandler(getHandlerFromCredentials(node.getCredentials()));
310         } else if (protocol == null || protocol.getName() == Name.SSH) {
311             reconnectingClientConfigurationBuilder = NetconfReconnectingClientConfigurationBuilder.create()
312                     .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.SSH)
313                     .withAuthHandler(getHandlerFromCredentials(node.getCredentials()));
314         } else if (protocol.getName() == Name.TLS) {
315             reconnectingClientConfigurationBuilder = NetconfReconnectingClientConfigurationBuilder.create()
316                 .withSslHandlerFactory(new SslHandlerFactoryImpl(keystoreAdapter, protocol.getSpecification()))
317                 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TLS);
318         } else {
319             throw new IllegalStateException("Unsupported protocol type: " + protocol.getName());
320         }
321
322         if (node.getOdlHelloMessageCapabilities() != null) {
323             reconnectingClientConfigurationBuilder.withOdlHelloCapabilities(
324                     Lists.newArrayList(node.getOdlHelloMessageCapabilities().getCapability()));
325         }
326
327         return reconnectingClientConfigurationBuilder
328                 .withName(nodeId.getValue())
329                 .withAddress(NetconfNodeUtils.toInetSocketAddress(node))
330                 .withConnectionTimeoutMillis(node.requireConnectionTimeoutMillis().toJava())
331                 .withReconnectStrategy(sf.createReconnectStrategy())
332                 .withConnectStrategyFactory(sf)
333                 .withSessionListener(listener)
334                 .build();
335     }
336
337     private AuthenticationHandler getHandlerFromCredentials(final Credentials credentials) {
338         if (credentials
339                 instanceof org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev230430
340                     .credentials.credentials.LoginPassword loginPassword) {
341             return new LoginPasswordHandler(loginPassword.getUsername(), loginPassword.getPassword());
342         }
343         if (credentials instanceof LoginPwUnencrypted unencrypted) {
344             final var loginPassword = unencrypted.getLoginPasswordUnencrypted();
345             return new LoginPasswordHandler(loginPassword.getUsername(), loginPassword.getPassword());
346         }
347         if (credentials instanceof LoginPw loginPw) {
348             final var loginPassword = loginPw.getLoginPassword();
349             return new LoginPasswordHandler(loginPassword.getUsername(),
350                     encryptionService.decrypt(loginPassword.getPassword()));
351         }
352         if (credentials instanceof KeyAuth keyAuth) {
353             final var keyPair = keyAuth.getKeyBased();
354             return new DatastoreBackedPublicKeyAuth(keyPair.getUsername(), keyPair.getKeyId(),
355                     keystoreAdapter, encryptionService);
356         }
357         throw new IllegalStateException("Unsupported credential type: " + credentials.getClass());
358     }
359 }