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