Fix incorrect operational state of device configuration
[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.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 io.netty.util.concurrent.DefaultPromise;
24 import io.netty.util.concurrent.EventExecutor;
25 import io.netty.util.concurrent.ImmediateEventExecutor;
26 import io.netty.util.concurrent.ScheduledFuture;
27 import io.netty.util.concurrent.SucceededFuture;
28 import java.net.InetSocketAddress;
29 import java.util.List;
30 import java.util.concurrent.Executor;
31 import java.util.concurrent.ScheduledExecutorService;
32 import java.util.concurrent.TimeUnit;
33 import org.junit.Before;
34 import org.junit.BeforeClass;
35 import org.junit.Test;
36 import org.junit.runner.RunWith;
37 import org.mockito.ArgumentCaptor;
38 import org.mockito.Captor;
39 import org.mockito.Mock;
40 import org.mockito.junit.MockitoJUnitRunner;
41 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
42 import org.opendaylight.netconf.api.CapabilityURN;
43 import org.opendaylight.netconf.client.NetconfClientDispatcher;
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.BaseNetconfSchemas;
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.SslHandlerFactoryProvider;
57 import org.opendaylight.netconf.client.mdsal.impl.DefaultBaseNetconfSchemas;
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.rev230430.credentials.credentials.LoginPasswordBuilder;
63 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev221225.NetconfNodeBuilder;
64 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
65 import org.opendaylight.yangtools.yang.common.Decimal64;
66 import org.opendaylight.yangtools.yang.common.Uint16;
67 import org.opendaylight.yangtools.yang.common.Uint32;
68 import org.opendaylight.yangtools.yang.data.api.schema.MountPointContext;
69 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
70 import org.opendaylight.yangtools.yang.parser.impl.DefaultYangParserFactory;
71
72 @RunWith(MockitoJUnitRunner.StrictStubs.class)
73 public class NetconfNodeHandlerTest {
74     private static final RemoteDeviceId DEVICE_ID = new RemoteDeviceId("netconf-topology",
75         new InetSocketAddress(InetAddresses.forString("127.0.0.1"), 9999));
76     private static final NodeId NODE_ID = new NodeId("testing-node");
77
78     private static BaseNetconfSchemas BASE_SCHEMAS;
79
80     // Core setup
81     @Mock
82     private ScheduledExecutorService keepaliveExecutor;
83     @Mock
84     private SchemaResourceManager schemaManager;
85     @Mock
86     private Executor processingExecutor;
87     @Mock
88     private DeviceActionFactory deviceActionFactory;
89     @Mock
90     private RemoteDeviceHandler delegate;
91
92     // DefaultNetconfClientConfigurationBuilderFactory setup
93     @Mock
94     private SslHandlerFactoryProvider sslHandlerFactoryProvider;
95     @Mock
96     private AAAEncryptionService encryptionService;
97     @Mock
98     private CredentialProvider credentialProvider;
99
100     // Mock client dispatcher-related things
101     @Mock
102     private NetconfClientDispatcher clientDispatcher;
103     @Mock
104     private NetconfClientSession clientSession;
105     @Captor
106     private ArgumentCaptor<NetconfDeviceSchema> schemaCaptor;
107     @Captor
108     private ArgumentCaptor<NetconfSessionPreferences> prefsCaptor;
109     @Captor
110     private ArgumentCaptor<RemoteDeviceServices> servicesCaptor;
111
112     // Mock eventExecutor-related things
113     @Mock
114     private EventExecutor eventExecutor;
115     @Mock
116     private ScheduledFuture<?> scheduleFuture;
117     @Captor
118     private ArgumentCaptor<Runnable> scheduleCaptor;
119     @Mock
120     private EffectiveModelContext schemaContext;
121
122     private NetconfNodeHandler handler;
123
124     @BeforeClass
125     public static void beforeClass() throws Exception {
126         BASE_SCHEMAS = new DefaultBaseNetconfSchemas(new DefaultYangParserFactory());
127     }
128
129     @BeforeClass
130     public static void afterClass() throws Exception {
131         BASE_SCHEMAS = null;
132     }
133
134     @Before
135     public void before() {
136         // Instantiate the handler
137         handler = new NetconfNodeHandler(clientDispatcher, eventExecutor, keepaliveExecutor, BASE_SCHEMAS,
138             schemaManager, processingExecutor,
139             new DefaultNetconfClientConfigurationBuilderFactory(encryptionService, credentialProvider,
140                 sslHandlerFactoryProvider),
141             deviceActionFactory, delegate, DEVICE_ID, NODE_ID, new NetconfNodeBuilder()
142                 .setHost(new Host(new IpAddress(new Ipv4Address("127.0.0.1"))))
143                 .setPort(new PortNumber(Uint16.valueOf(9999)))
144                 .setReconnectOnChangedSchema(true)
145                 .setSchemaless(true)
146                 .setTcpOnly(true)
147                 .setSleepFactor(Decimal64.valueOf("1.5"))
148                 .setConcurrentRpcLimit(Uint16.ONE)
149                 // One reconnection attempt
150                 .setMaxConnectionAttempts(Uint32.TWO)
151                 .setDefaultRequestTimeoutMillis(Uint32.valueOf(1000))
152                 .setBetweenAttemptsTimeoutMillis(Uint16.valueOf(100))
153                 .setKeepaliveDelay(Uint32.valueOf(1000))
154                 .setConnectionTimeoutMillis(Uint32.valueOf(1000))
155                 .setCredentials(new LoginPasswordBuilder().setUsername("testuser").setPassword("testpassword").build())
156                 .build(), null);
157     }
158
159     @Test
160     public void successfullOnDeviceConnectedPropagates() {
161         assertSuccessfulConnect();
162         assertEquals(1, handler.attempts());
163
164         final var schema = new NetconfDeviceSchema(NetconfDeviceCapabilities.empty(),
165             MountPointContext.of(schemaContext));
166         final var netconfSessionPreferences = NetconfSessionPreferences.fromStrings(List.of(CapabilityURN.CANDIDATE));
167         final var deviceServices = new RemoteDeviceServices(mock(Rpcs.Normalized.class), null);
168
169         // when the device is connected, we propagate the information
170         doNothing().when(delegate).onDeviceConnected(schemaCaptor.capture(), prefsCaptor.capture(),
171             servicesCaptor.capture());
172         handler.onDeviceConnected(schema, netconfSessionPreferences, deviceServices);
173
174         assertEquals(schema, schemaCaptor.getValue());
175         assertEquals(netconfSessionPreferences, prefsCaptor.getValue());
176         assertEquals(deviceServices, servicesCaptor.getValue());
177         assertEquals(0, handler.attempts());
178     }
179
180     @Test
181     public void failedSchemaCausesReconnect() {
182         assertSuccessfulConnect();
183         assertEquals(1, handler.attempts());
184
185         // Note: this will count as a second attempt
186         doReturn(scheduleFuture).when(eventExecutor).schedule(scheduleCaptor.capture(), eq(150L),
187             eq(TimeUnit.MILLISECONDS));
188         handler.onDeviceFailed(new AssertionError("schema failure"));
189
190         assertEquals(2, handler.attempts());
191
192         // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
193         scheduleCaptor.getValue().run();
194         verify(clientDispatcher, times(2)).createClient(any());
195         assertEquals(2, handler.attempts());
196     }
197
198     /**
199      * Tests the initialization of the NetconfNodeHandler's client configuration.
200      *
201      * <p>
202      * Ensures that no connection attempts are made by the handler after calling connect().
203      */
204     @Test
205     public void testClientConfigInitializationFailure() {
206         doNothing().when(delegate).onDeviceFailed(any(Exception.class));
207         // Creates a netconfNode without specifying a Port and Host.
208         final var netconfNode = new NetconfNodeBuilder()
209             .setReconnectOnChangedSchema(true)
210             .setSchemaless(true)
211             .setTcpOnly(true)
212             .setSleepFactor(Decimal64.valueOf("1.5"))
213             .setConcurrentRpcLimit(Uint16.ONE)
214             // One reconnection attempt
215             .setMaxConnectionAttempts(Uint32.TWO)
216             .setDefaultRequestTimeoutMillis(Uint32.valueOf(1000))
217             .setBetweenAttemptsTimeoutMillis(Uint16.valueOf(100))
218             .setKeepaliveDelay(Uint32.valueOf(1000))
219             .setConnectionTimeoutMillis(Uint32.valueOf(1000))
220             .setCredentials(new LoginPasswordBuilder().setUsername("testuser").setPassword("testpassword").build())
221             .build();
222         handler = new NetconfNodeHandler(clientDispatcher, eventExecutor, keepaliveExecutor, BASE_SCHEMAS,
223             schemaManager, processingExecutor, new DefaultNetconfClientConfigurationBuilderFactory(encryptionService,
224             credentialProvider,
225             sslHandlerFactoryProvider), deviceActionFactory, delegate, DEVICE_ID, NODE_ID,
226             netconfNode, null);
227         handler.connect();
228         // Expect 0 connection attempts since the clientConfig has not been initialized.
229         assertEquals(0, handler.attempts());
230     }
231
232     @Test
233     public void downAfterUpCausesReconnect() {
234         // Let's borrow common bits
235         successfullOnDeviceConnectedPropagates();
236
237         // when the device is connected, we propagate the information and initiate reconnect
238         doNothing().when(delegate).onDeviceDisconnected();
239         doReturn(scheduleFuture).when(eventExecutor).schedule(scheduleCaptor.capture(), eq(100L),
240             eq(TimeUnit.MILLISECONDS));
241         handler.onDeviceDisconnected();
242
243         assertEquals(1, handler.attempts());
244
245         // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
246         scheduleCaptor.getValue().run();
247         verify(clientDispatcher, times(2)).createClient(any());
248         assertEquals(1, handler.attempts());
249     }
250
251     @Test
252     public void socketFailuresAreRetried() {
253         final var firstPromise = new DefaultPromise<NetconfClientSession>(ImmediateEventExecutor.INSTANCE);
254         final var secondPromise = new DefaultPromise<NetconfClientSession>(ImmediateEventExecutor.INSTANCE);
255         doReturn(firstPromise, secondPromise).when(clientDispatcher).createClient(any());
256         handler.connect();
257         assertEquals(1, handler.attempts());
258
259         doReturn(scheduleFuture).when(eventExecutor).schedule(scheduleCaptor.capture(), eq(150L),
260             eq(TimeUnit.MILLISECONDS));
261         firstPromise.setFailure(new AssertionError("first"));
262
263         assertEquals(2, handler.attempts());
264
265         // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
266         scheduleCaptor.getValue().run();
267         verify(clientDispatcher, times(2)).createClient(any());
268         assertEquals(2, handler.attempts());
269
270         // now report the second failure
271         final var throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
272         doNothing().when(delegate).onDeviceFailed(throwableCaptor.capture());
273         secondPromise.setFailure(new AssertionError("second"));
274         assertThat(throwableCaptor.getValue(), instanceOf(ConnectGivenUpException.class));
275
276         // but nothing else happens
277         assertEquals(2, handler.attempts());
278     }
279
280     // Initiate a connect() which results in immediate clientDispatcher report. No interactions with delegate may occur,
281     // as this is just a prelude to a follow-up callback
282     private void assertSuccessfulConnect() {
283         doReturn(new SucceededFuture<>(ImmediateEventExecutor.INSTANCE, clientSession))
284             .when(clientDispatcher).createClient(any());
285         handler.connect();
286         verify(clientDispatcher).createClient(any());
287         verifyNoInteractions(delegate);
288     }
289 }