9086c978e7263bcfe934241d4ff825e7e6783932
[netconf.git] / apps / callhome-provider / src / main / java / org / opendaylight / netconf / callhome / mount / CallHomeMountService.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech s.r.o. 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.callhome.mount;
9
10 import com.google.common.annotations.VisibleForTesting;
11 import com.google.common.util.concurrent.ListenableFuture;
12 import com.google.common.util.concurrent.SettableFuture;
13 import io.netty.channel.Channel;
14 import java.net.InetSocketAddress;
15 import java.net.SocketAddress;
16 import java.util.Map;
17 import java.util.concurrent.ConcurrentHashMap;
18 import javax.annotation.PreDestroy;
19 import javax.inject.Inject;
20 import javax.inject.Singleton;
21 import org.opendaylight.mdsal.binding.api.DataBroker;
22 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
23 import org.opendaylight.netconf.callhome.server.CallHomeStatusRecorder;
24 import org.opendaylight.netconf.callhome.server.ssh.CallHomeSshSessionContext;
25 import org.opendaylight.netconf.callhome.server.ssh.CallHomeSshSessionContextManager;
26 import org.opendaylight.netconf.callhome.server.tls.CallHomeTlsAuthProvider;
27 import org.opendaylight.netconf.callhome.server.tls.CallHomeTlsSessionContext;
28 import org.opendaylight.netconf.callhome.server.tls.CallHomeTlsSessionContextManager;
29 import org.opendaylight.netconf.client.NetconfClientFactory;
30 import org.opendaylight.netconf.client.NetconfClientSession;
31 import org.opendaylight.netconf.client.NetconfClientSessionListener;
32 import org.opendaylight.netconf.client.conf.NetconfClientConfiguration;
33 import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder;
34 import org.opendaylight.netconf.client.mdsal.api.BaseNetconfSchemas;
35 import org.opendaylight.netconf.client.mdsal.api.DeviceActionFactory;
36 import org.opendaylight.netconf.client.mdsal.api.SchemaResourceManager;
37 import org.opendaylight.netconf.common.NetconfTimer;
38 import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession;
39 import org.opendaylight.netconf.topology.spi.NetconfClientConfigurationBuilderFactory;
40 import org.opendaylight.netconf.topology.spi.NetconfNodeHandler;
41 import org.opendaylight.netconf.topology.spi.NetconfNodeUtils;
42 import org.opendaylight.netconf.topology.spi.NetconfTopologySchemaAssembler;
43 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
44 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
45 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
46 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
47 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev231228.netconf.client.initiate.stack.grouping.transport.ssh.ssh.TcpClientParametersBuilder;
48 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.Protocol;
49 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.ProtocolBuilder;
50 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev231121.NetconfNodeBuilder;
51 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
52 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node;
53 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.NodeBuilder;
54 import org.opendaylight.yangtools.yang.common.Decimal64;
55 import org.opendaylight.yangtools.yang.common.Uint16;
56 import org.opendaylight.yangtools.yang.common.Uint32;
57 import org.osgi.service.component.annotations.Activate;
58 import org.osgi.service.component.annotations.Component;
59 import org.osgi.service.component.annotations.Deactivate;
60 import org.osgi.service.component.annotations.Reference;
61
62 /**
63  * Service is responsible for call-home to topology integration.
64  *
65  * <p>
66  * To manage remote device as a topology node the topology component (based on
67  * {@link org.opendaylight.netconf.topology.spi.AbstractNetconfTopology AbstractNetconfTopology}) creates an instance
68  * of {@link org.opendaylight.netconf.topology.spi.NetconfNodeHandler NetconfNodeHandler} based on provided
69  * {@link Node}.
70  *
71  * <p>
72  * The mentioned NetconfNodeHandler initializes connection to remote device via sequence of following actions (see
73  * {@link org.opendaylight.netconf.topology.spi.AbstractNetconfTopology#ensureNode(Node) ensureNode(Node)} and
74  * {@link NetconfNodeHandler#lockedConnect() connect()}):
75  *
76  * <ul>
77  *     <li>Builds an instance of {@link org.opendaylight.netconf.client.mdsal.NetconfDeviceCommunicator
78  *     NetconfDeviceCommunicator} implementation of {@link NetconfClientSessionListener} which is used to check the
79  *     NETCONF session state and communicate with device using NETCONF protocol </li>
80  *     <li>Builds Netconf client configuration using provided {@link NetconfClientConfigurationBuilderFactory}</li>
81  *     <li>Builds Netconf client using configuration composed and triggers connection</li>
82  * </ul>
83  *
84  * <p>
85  * This service uses custom implementations of {@link NetconfClientConfigurationBuilderFactory} and
86  * {@link NetconfClientFactory} in order to capture the instance of {@link NetconfClientSessionListener} from topology
87  * component which is required to establish NETCONF layer. See {@link #createClientConfigurationBuilderFactory()}
88  * and {@link #createClientFactory()}.
89  *
90  * <p>
91  * Following sequence of actions is performed when incoming connection is mapped to topology node:
92  *
93  * <ul>
94  *     <li>When incoming connection is identified the {@link CallHomeSshSessionContext} instance expected to be created.
95  *     The createContext() method is invoked within protocol associated
96  *     {@link org.opendaylight.netconf.callhome.server.CallHomeSessionContextManager CallHomeSessionContextManager} --
97  *     see {@link #createSshSessionContextManager()} and
98  *     {@link #createTlsSessionContextManager(CallHomeTlsAuthProvider, CallHomeStatusRecorder)}</li>
99  *     <li>Due to both {@link NetconfClientSessionListener} and {@link SettableFuture} are required to build session
100  *     context the {@link CallHomeTopology#enableNode(Node)} (Node)} is called using synthetic {@link Node} instance
101  *     composed via {@link #asNode(String, SocketAddress, Protocol)}. This triggers Netconf client construct/connect
102  *     logic (as explained above) resulting captured object placed into {@link #netconfLayerMapping}.</li>
103  *     <li>Accepted instance of {@link NetconfClientSessionListener} is used to establish Netconf layer --
104  *     see {@link org.opendaylight.netconf.callhome.server.CallHomeTransportChannelListener
105  *     CallHomeTransportChannelListener} </li>
106  *     <li>Accepted instance of {@link SettableFuture} (representing connection to remote device) is used to
107  *     signal connection state to topology component</li>
108  * </ul>
109  */
110 @Component(service = CallHomeMountService.class, immediate = true)
111 @Singleton
112 public final class CallHomeMountService implements AutoCloseable {
113     private static final Protocol SSH_PROTOCOL = new ProtocolBuilder().setName(Protocol.Name.SSH).build();
114     private static final Protocol TLS_PROTOCOL = new ProtocolBuilder().setName(Protocol.Name.TLS).build();
115
116     private final Map<String, NetconfLayer> netconfLayerMapping = new ConcurrentHashMap<>();
117     private final CallHomeTopology topology;
118
119     @Activate
120     @Inject
121     public CallHomeMountService(
122             final @Reference NetconfTimer timer,
123             final @Reference NetconfTopologySchemaAssembler schemaAssembler,
124             final @Reference SchemaResourceManager schemaRepositoryProvider,
125             final @Reference BaseNetconfSchemas baseSchemas,
126             final @Reference DataBroker dataBroker,
127             final @Reference DOMMountPointService mountService,
128             final @Reference DeviceActionFactory deviceActionFactory) {
129         this(NetconfNodeUtils.DEFAULT_TOPOLOGY_NAME, timer, schemaAssembler, schemaRepositoryProvider, baseSchemas,
130             dataBroker, mountService, deviceActionFactory);
131     }
132
133     public CallHomeMountService(final String topologyId, final NetconfTimer timer,
134             final NetconfTopologySchemaAssembler schemaAssembler, final SchemaResourceManager schemaRepositoryProvider,
135             final BaseNetconfSchemas baseSchemas, final DataBroker dataBroker, final DOMMountPointService mountService,
136             final DeviceActionFactory deviceActionFactory) {
137
138         final var clientConfBuilderFactory = createClientConfigurationBuilderFactory();
139         final var clientFactory = createClientFactory();
140         topology = new CallHomeTopology(topologyId, clientFactory, timer, schemaAssembler,
141             schemaRepositoryProvider, dataBroker, mountService, clientConfBuilderFactory,
142             baseSchemas, deviceActionFactory);
143     }
144
145     @VisibleForTesting
146     CallHomeMountService(final CallHomeTopology topology) {
147         this.topology = topology;
148     }
149
150     @VisibleForTesting
151     static NetconfClientConfigurationBuilderFactory createClientConfigurationBuilderFactory() {
152         // use minimal configuration, only id and session listener are used
153         return (nodeId, node) -> NetconfClientConfigurationBuilder.create()
154             .withName(nodeId.getValue())
155             // below parameters are only required to pass configuration validation
156             // actual values play no role
157             .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.TCP)
158             .withTcpParameters(new TcpClientParametersBuilder().build());
159     }
160
161     @VisibleForTesting
162     NetconfClientFactory createClientFactory() {
163         return new NetconfClientFactory() {
164             @Override
165             public ListenableFuture<NetconfClientSession> createClient(
166                     final NetconfClientConfiguration clientConfiguration) throws UnsupportedConfigurationException {
167                 final var future = SettableFuture.<NetconfClientSession>create();
168                 final var pending = new NetconfLayer(clientConfiguration.getName(),
169                     clientConfiguration.getSessionListener(), future);
170                 netconfLayerMapping.put(pending.id, pending);
171                 return future;
172             }
173
174             @Override
175             public void close() throws Exception {
176                 // do nothing
177             }
178         };
179     }
180
181     private static Node asNode(final String id, final SocketAddress socketAddress, final Protocol protocol) {
182         final var nodeAddress = socketAddress instanceof InetSocketAddress inetSocketAddress
183             ? inetSocketAddress : new InetSocketAddress("0.0.0.0", 0);
184         // construct synthetic Node object with minimal required parameters
185         return new NodeBuilder()
186             .setNodeId(new NodeId(id))
187             .addAugmentation(new NetconfNodeBuilder()
188                 .setHost(new Host(IetfInetUtil.ipAddressFor(nodeAddress.getAddress())))
189                 .setPort(new PortNumber(Uint16.valueOf(nodeAddress.getPort())))
190                 .setTcpOnly(false)
191                 .setProtocol(protocol)
192                 // below parameters are required for NetconfNodeHandler
193                 .setSchemaless(false)
194                 .setReconnectOnChangedSchema(false)
195                 .setConnectionTimeoutMillis(Uint32.valueOf(20000))
196                 .setDefaultRequestTimeoutMillis(Uint32.valueOf(60000))
197                 .setMaxConnectionAttempts(Uint32.ZERO)
198                 .setMinBackoffMillis(Uint16.valueOf(2000))
199                 .setMaxBackoffMillis(Uint32.valueOf(1800000))
200                 .setBackoffMultiplier(Decimal64.valueOf("1.5"))
201                 .setBackoffJitter(Decimal64.valueOf("0.1"))
202                 .setKeepaliveDelay(Uint32.valueOf(120))
203                 .setConcurrentRpcLimit(Uint16.ZERO)
204                 .setActorResponseWaitTime(Uint16.valueOf(5))
205                 .setLockDatastore(true)
206                 .build())
207             .build();
208     }
209
210     public CallHomeSshSessionContextManager createSshSessionContextManager() {
211         return new CallHomeSshSessionContextManager() {
212             @Override
213             public CallHomeSshSessionContext createContext(final String id, final ClientSession clientSession) {
214                 final var remoteAddr = clientSession.getRemoteAddress();
215                 topology.enableNode(asNode(id, remoteAddr, SSH_PROTOCOL));
216                 final var netconfLayer = netconfLayerMapping.remove(id);
217                 return netconfLayer == null ? null : new CallHomeSshSessionContext(id, remoteAddr, clientSession,
218                     netconfLayer.sessionListener, netconfLayer.netconfSessionFuture);
219             }
220
221             @Override
222             public void remove(final String id) {
223                 super.remove(id);
224                 topology.disableNode(new NodeId(id));
225             }
226         };
227     }
228
229     public CallHomeTlsSessionContextManager createTlsSessionContextManager(final CallHomeTlsAuthProvider authProvider,
230             final CallHomeStatusRecorder statusRecorder) {
231         return new CallHomeTlsSessionContextManager(authProvider, statusRecorder) {
232             @Override
233             public CallHomeTlsSessionContext createContext(final String id, final Channel channel) {
234                 topology.enableNode(asNode(id, channel.remoteAddress(), TLS_PROTOCOL));
235                 final var netconfLayer = netconfLayerMapping.remove(id);
236                 return netconfLayer == null ? null : new CallHomeTlsSessionContext(id, channel,
237                     netconfLayer.sessionListener, netconfLayer.netconfSessionFuture());
238             }
239
240             @Override
241             public void remove(final String id) {
242                 super.remove(id);
243                 topology.disableNode(new NodeId(id));
244             }
245         };
246     }
247
248     @PreDestroy
249     @Deactivate
250     @Override
251     public void close() {
252         netconfLayerMapping.forEach((key, value) -> value.netconfSessionFuture.cancel(true));
253         netconfLayerMapping.clear();
254     }
255
256     private record NetconfLayer(String id, NetconfClientSessionListener sessionListener,
257         SettableFuture<NetconfClientSession> netconfSessionFuture) {
258     }
259 }