8fd3bae56f0a2f092c462f23347cda0d3a7ca617
[netconf.git] / apps / netconf-topology / src / test / java / org / opendaylight / netconf / topology / spi / NetconfNodeHandlerTest.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.topology.spi;
9
10 import static org.junit.jupiter.api.Assertions.assertEquals;
11 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
12 import static org.mockito.ArgumentMatchers.any;
13 import static org.mockito.ArgumentMatchers.anyLong;
14 import static org.mockito.ArgumentMatchers.eq;
15 import static org.mockito.Mockito.doNothing;
16 import static org.mockito.Mockito.doReturn;
17 import static org.mockito.Mockito.mock;
18 import static org.mockito.Mockito.times;
19 import static org.mockito.Mockito.verify;
20 import static org.mockito.Mockito.verifyNoInteractions;
21
22 import com.google.common.net.InetAddresses;
23 import com.google.common.util.concurrent.Futures;
24 import com.google.common.util.concurrent.SettableFuture;
25 import io.netty.util.Timeout;
26 import io.netty.util.TimerTask;
27 import java.net.InetSocketAddress;
28 import java.util.List;
29 import java.util.concurrent.TimeUnit;
30 import org.junit.jupiter.api.AfterAll;
31 import org.junit.jupiter.api.AfterEach;
32 import org.junit.jupiter.api.BeforeAll;
33 import org.junit.jupiter.api.BeforeEach;
34 import org.junit.jupiter.api.Test;
35 import org.junit.jupiter.api.extension.ExtendWith;
36 import org.mockito.ArgumentCaptor;
37 import org.mockito.Captor;
38 import org.mockito.Mock;
39 import org.mockito.junit.jupiter.MockitoExtension;
40 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
41 import org.opendaylight.netconf.api.CapabilityURN;
42 import org.opendaylight.netconf.client.NetconfClientFactory;
43 import org.opendaylight.netconf.client.NetconfClientFactoryImpl;
44 import org.opendaylight.netconf.client.NetconfClientSession;
45 import org.opendaylight.netconf.client.mdsal.NetconfDeviceCapabilities;
46 import org.opendaylight.netconf.client.mdsal.NetconfDeviceSchema;
47 import org.opendaylight.netconf.client.mdsal.api.BaseNetconfSchemaProvider;
48 import org.opendaylight.netconf.client.mdsal.api.CredentialProvider;
49 import org.opendaylight.netconf.client.mdsal.api.DeviceActionFactory;
50 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
51 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceHandler;
52 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
53 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceServices;
54 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceServices.Rpcs;
55 import org.opendaylight.netconf.client.mdsal.api.SchemaResourceManager;
56 import org.opendaylight.netconf.client.mdsal.api.SslContextFactoryProvider;
57 import org.opendaylight.netconf.client.mdsal.impl.DefaultBaseNetconfSchemaProvider;
58 import org.opendaylight.netconf.common.NetconfTimer;
59 import org.opendaylight.netconf.common.impl.DefaultNetconfTimer;
60 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
61 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress;
62 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Ipv4Address;
63 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
64 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.KeyAuthBuilder;
65 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.LoginPwUnencryptedBuilder;
66 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.key.auth.KeyBasedBuilder;
67 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.login.pw.unencrypted.LoginPasswordUnencryptedBuilder;
68 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev231121.NetconfNodeBuilder;
69 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
70 import org.opendaylight.yangtools.yang.common.Decimal64;
71 import org.opendaylight.yangtools.yang.common.Uint16;
72 import org.opendaylight.yangtools.yang.common.Uint32;
73 import org.opendaylight.yangtools.yang.data.api.schema.MountPointContext;
74 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
75 import org.opendaylight.yangtools.yang.parser.impl.DefaultYangParserFactory;
76
77 @ExtendWith(MockitoExtension.class)
78 class NetconfNodeHandlerTest {
79     private static final RemoteDeviceId DEVICE_ID = new RemoteDeviceId("netconf-topology",
80         new InetSocketAddress(InetAddresses.forString("127.0.0.1"), 9999));
81     private static final NodeId NODE_ID = new NodeId("testing-node");
82
83     private static BaseNetconfSchemaProvider BASE_SCHEMAS;
84
85     // Core setup
86     @Mock
87     private NetconfTimer timer;
88     @Mock
89     private SchemaResourceManager schemaManager;
90     @Mock
91     private DeviceActionFactory deviceActionFactory;
92     @Mock
93     private RemoteDeviceHandler delegate;
94
95     // DefaultNetconfClientConfigurationBuilderFactory setup
96     @Mock
97     private SslContextFactoryProvider sslContextFactoryProvider;
98     @Mock
99     private AAAEncryptionService encryptionService;
100     @Mock
101     private CredentialProvider credentialProvider;
102
103     // Mock client dispatcher-related things
104     @Mock
105     private NetconfClientFactory clientFactory;
106     @Mock
107     private NetconfClientSession clientSession;
108     @Captor
109     private ArgumentCaptor<NetconfDeviceSchema> schemaCaptor;
110     @Captor
111     private ArgumentCaptor<NetconfSessionPreferences> prefsCaptor;
112     @Captor
113     private ArgumentCaptor<RemoteDeviceServices> servicesCaptor;
114
115     // Mock Timer-related things
116     @Mock
117     private Timeout timeout;
118     @Captor
119     private ArgumentCaptor<TimerTask> timerCaptor;
120     @Mock
121     private EffectiveModelContext schemaContext;
122
123     private NetconfTopologySchemaAssembler schemaAssembler;
124     private NetconfNodeHandler handler;
125
126     @BeforeAll
127     static void beforeClass() throws Exception {
128         BASE_SCHEMAS = new DefaultBaseNetconfSchemaProvider(new DefaultYangParserFactory());
129     }
130
131     @AfterAll
132     static void afterClass() throws Exception {
133         BASE_SCHEMAS = null;
134     }
135
136     @BeforeEach
137     void before() {
138         schemaAssembler = new NetconfTopologySchemaAssembler(1, 1, 0, TimeUnit.SECONDS);
139
140         // Instantiate the handler
141         handler = new NetconfNodeHandler(clientFactory, timer, BASE_SCHEMAS, schemaManager, schemaAssembler,
142             new NetconfClientConfigurationBuilderFactoryImpl(encryptionService, credentialProvider,
143                 sslContextFactoryProvider),
144             deviceActionFactory, delegate, DEVICE_ID, NODE_ID, new NetconfNodeBuilder()
145                 .setHost(new Host(new IpAddress(new Ipv4Address("127.0.0.1"))))
146                 .setPort(new PortNumber(Uint16.valueOf(9999)))
147                 .setReconnectOnChangedSchema(true)
148                 .setSchemaless(true)
149                 .setTcpOnly(true)
150                 .setBackoffMultiplier(Decimal64.valueOf("1.5"))
151                 .setConcurrentRpcLimit(Uint16.ONE)
152                 // One reconnection attempt
153                 .setMaxConnectionAttempts(Uint32.TWO)
154                 .setDefaultRequestTimeoutMillis(Uint32.valueOf(1000))
155                 .setMinBackoffMillis(Uint16.valueOf(100))
156                 .setKeepaliveDelay(Uint32.valueOf(1000))
157                 .setConnectionTimeoutMillis(Uint32.valueOf(1000))
158                 .setMaxBackoffMillis(Uint32.valueOf(1000))
159                 .setBackoffJitter(Decimal64.valueOf("0.0"))
160                 .setCredentials(new LoginPwUnencryptedBuilder()
161                     .setLoginPasswordUnencrypted(new LoginPasswordUnencryptedBuilder()
162                         .setUsername("testuser")
163                         .setPassword("testpassword")
164                         .build())
165                     .build())
166                 .build(), null);
167     }
168
169     @AfterEach
170     void after() {
171         schemaAssembler.close();
172     }
173
174     @Test
175     void successfulOnDeviceConnectedPropagates() throws Exception {
176         assertSuccessfulConnect();
177         assertEquals(1, handler.attempts());
178
179         final var schema = new NetconfDeviceSchema(NetconfDeviceCapabilities.empty(),
180             MountPointContext.of(schemaContext));
181         final var netconfSessionPreferences = NetconfSessionPreferences.fromStrings(List.of(CapabilityURN.CANDIDATE));
182         final var deviceServices = new RemoteDeviceServices(mock(Rpcs.Normalized.class), null);
183
184         // when the device is connected, we propagate the information
185         doNothing().when(delegate).onDeviceConnected(schemaCaptor.capture(), prefsCaptor.capture(),
186             servicesCaptor.capture());
187         handler.onDeviceConnected(schema, netconfSessionPreferences, deviceServices);
188
189         assertEquals(schema, schemaCaptor.getValue());
190         assertEquals(netconfSessionPreferences, prefsCaptor.getValue());
191         assertEquals(deviceServices, servicesCaptor.getValue());
192         assertEquals(0, handler.attempts());
193     }
194
195     @Test
196     void failedSchemaCausesReconnect() throws Exception {
197         assertSuccessfulConnect();
198         assertEquals(1, handler.attempts());
199
200         // Note: this will count as a second attempt
201         doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), anyLong(), any());
202
203         handler.onDeviceFailed(new AssertionError("schema failure"));
204
205         assertEquals(2, handler.attempts());
206
207         // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
208         timerCaptor.getValue().run(timeout);
209         verify(clientFactory, times(2)).createClient(any());
210         assertEquals(2, handler.attempts());
211     }
212
213     @Test
214     void downAfterUpCausesReconnect() throws Exception {
215         // Let's borrow common bits
216         successfulOnDeviceConnectedPropagates();
217
218         // when the device is connected, we propagate the information and initiate reconnect
219         doNothing().when(delegate).onDeviceDisconnected();
220         doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), eq(100L), eq(TimeUnit.MILLISECONDS));
221         handler.onDeviceDisconnected();
222
223         assertEquals(1, handler.attempts());
224
225         // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
226         timerCaptor.getValue().run(timeout);
227         verify(clientFactory, times(2)).createClient(any());
228         assertEquals(1, handler.attempts());
229     }
230
231     @Test
232     void socketFailuresAreRetried() throws Exception {
233         final var firstFuture = SettableFuture.create();
234         final var secondFuture = SettableFuture.create();
235         doReturn(firstFuture, secondFuture).when(clientFactory).createClient(any());
236         handler.connect();
237         assertEquals(1, handler.attempts());
238
239         doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), eq(150L), eq(TimeUnit.MILLISECONDS));
240         firstFuture.setException(new AssertionError("first"));
241
242         assertEquals(2, handler.attempts());
243
244         // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
245         timerCaptor.getValue().run(timeout);
246         verify(clientFactory, times(2)).createClient(any());
247         assertEquals(2, handler.attempts());
248
249         // now report the second failure
250         final var throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
251         doNothing().when(delegate).onDeviceFailed(throwableCaptor.capture());
252         secondFuture.setException(new AssertionError("second"));
253         assertInstanceOf(ConnectGivenUpException.class, throwableCaptor.getValue());
254
255         // but nothing else happens
256         assertEquals(2, handler.attempts());
257     }
258
259     // Initiate connect() which results in immediate clientDispatcher report. No interactions with delegate may occur,
260     // as this is just a prelude to a follow-up callback
261     private void assertSuccessfulConnect() throws Exception {
262         doReturn(Futures.immediateFuture(clientSession)).when(clientFactory).createClient(any());
263         handler.connect();
264         verify(clientFactory).createClient(any());
265         verifyNoInteractions(delegate);
266     }
267
268     @Test
269     void failToConnectOnUnsupportedConfiguration() {
270         final var defaultTimer = new DefaultNetconfTimer();
271         final var factory = new NetconfClientFactoryImpl(defaultTimer);
272
273         final var keyId = "keyId";
274         final var keyAuthHandler = new NetconfNodeHandler(factory, defaultTimer, BASE_SCHEMAS, schemaManager,
275             schemaAssembler, new NetconfClientConfigurationBuilderFactoryImpl(encryptionService, credentialProvider,
276                 sslContextFactoryProvider),
277             deviceActionFactory, delegate, DEVICE_ID, NODE_ID, new NetconfNodeBuilder()
278                 .setHost(new Host(new IpAddress(new Ipv4Address("127.0.0.1"))))
279                 .setPort(new PortNumber(Uint16.valueOf(9999)))
280                 .setReconnectOnChangedSchema(true)
281                 .setSchemaless(true)
282                 .setTcpOnly(false)
283                 .setProtocol(null)
284                 .setBackoffMultiplier(Decimal64.valueOf("1.5"))
285                 .setConcurrentRpcLimit(Uint16.ONE)
286                 // One reconnection attempt
287                 .setMaxConnectionAttempts(Uint32.ONE)
288                 .setDefaultRequestTimeoutMillis(Uint32.valueOf(1000))
289                 .setMinBackoffMillis(Uint16.valueOf(100))
290                 .setKeepaliveDelay(Uint32.valueOf(1000))
291                 .setConnectionTimeoutMillis(Uint32.valueOf(1000))
292                 .setMaxBackoffMillis(Uint32.valueOf(1000))
293                 .setBackoffJitter(Decimal64.valueOf("0.0"))
294                 .setCredentials(new KeyAuthBuilder()
295                     .setKeyBased(new KeyBasedBuilder()
296                         .setUsername("testuser")
297                         .setKeyId(keyId)
298                         .build())
299                     .build())
300                 .build(), null);
301
302         // return null when attempt to load credentials fot key id
303         doReturn(null).when(credentialProvider).credentialForId(any());
304         doNothing().when(delegate).onDeviceFailed(any());
305         keyAuthHandler.connect();
306         verify(credentialProvider).credentialForId(eq(keyId));
307         // attempt to connect fails due to unsupported configuration, and there is attempt to reconnect
308         final var captor = ArgumentCaptor.forClass(Throwable.class);
309         verify(delegate).onDeviceFailed(captor.capture());
310         assertInstanceOf(ConnectGivenUpException.class, captor.getValue());
311         assertEquals(1, keyAuthHandler.attempts());
312     }
313 }