2 * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.netconf.topology.spi;
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;
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.Timer;
28 import io.netty.util.TimerTask;
29 import java.net.InetSocketAddress;
30 import java.util.List;
31 import java.util.concurrent.Executor;
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.NetconfClientFactory;
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.rev231025.credentials.credentials.LoginPwUnencryptedBuilder;
63 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev231025.credentials.credentials.login.pw.unencrypted.LoginPasswordUnencryptedBuilder;
64 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev221225.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;
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");
79 private static BaseNetconfSchemas BASE_SCHEMAS;
85 private SchemaResourceManager schemaManager;
87 private Executor processingExecutor;
89 private DeviceActionFactory deviceActionFactory;
91 private RemoteDeviceHandler delegate;
93 // DefaultNetconfClientConfigurationBuilderFactory setup
95 private SslHandlerFactoryProvider sslHandlerFactoryProvider;
97 private AAAEncryptionService encryptionService;
99 private CredentialProvider credentialProvider;
101 // Mock client dispatcher-related things
103 private NetconfClientFactory clientFactory;
105 private NetconfClientSession clientSession;
107 private ArgumentCaptor<NetconfDeviceSchema> schemaCaptor;
109 private ArgumentCaptor<NetconfSessionPreferences> prefsCaptor;
111 private ArgumentCaptor<RemoteDeviceServices> servicesCaptor;
113 // Mock Timer-related things
115 private Timeout timeout;
117 private ArgumentCaptor<TimerTask> timerCaptor;
119 private EffectiveModelContext schemaContext;
121 private NetconfNodeHandler handler;
124 public static void beforeClass() throws Exception {
125 BASE_SCHEMAS = new DefaultBaseNetconfSchemas(new DefaultYangParserFactory());
129 public static void afterClass() throws Exception {
134 public void before() {
135 // Instantiate the handler
136 handler = new NetconfNodeHandler(clientFactory, timer, BASE_SCHEMAS, schemaManager, processingExecutor,
137 new NetconfClientConfigurationBuilderFactoryImpl(encryptionService, credentialProvider,
138 sslHandlerFactoryProvider),
139 deviceActionFactory, delegate, DEVICE_ID, NODE_ID, new NetconfNodeBuilder()
140 .setHost(new Host(new IpAddress(new Ipv4Address("127.0.0.1"))))
141 .setPort(new PortNumber(Uint16.valueOf(9999)))
142 .setReconnectOnChangedSchema(true)
145 .setSleepFactor(Decimal64.valueOf("1.5"))
146 .setConcurrentRpcLimit(Uint16.ONE)
147 // One reconnection attempt
148 .setMaxConnectionAttempts(Uint32.TWO)
149 .setDefaultRequestTimeoutMillis(Uint32.valueOf(1000))
150 .setBetweenAttemptsTimeoutMillis(Uint16.valueOf(100))
151 .setKeepaliveDelay(Uint32.valueOf(1000))
152 .setConnectionTimeoutMillis(Uint32.valueOf(1000))
153 .setCredentials(new LoginPwUnencryptedBuilder()
154 .setLoginPasswordUnencrypted(new LoginPasswordUnencryptedBuilder()
155 .setUsername("testuser")
156 .setPassword("testpassword")
163 public void successfulOnDeviceConnectedPropagates() throws Exception {
164 assertSuccessfulConnect();
165 assertEquals(1, handler.attempts());
167 final var schema = new NetconfDeviceSchema(NetconfDeviceCapabilities.empty(),
168 MountPointContext.of(schemaContext));
169 final var netconfSessionPreferences = NetconfSessionPreferences.fromStrings(List.of(CapabilityURN.CANDIDATE));
170 final var deviceServices = new RemoteDeviceServices(mock(Rpcs.Normalized.class), null);
172 // when the device is connected, we propagate the information
173 doNothing().when(delegate).onDeviceConnected(schemaCaptor.capture(), prefsCaptor.capture(),
174 servicesCaptor.capture());
175 handler.onDeviceConnected(schema, netconfSessionPreferences, deviceServices);
177 assertEquals(schema, schemaCaptor.getValue());
178 assertEquals(netconfSessionPreferences, prefsCaptor.getValue());
179 assertEquals(deviceServices, servicesCaptor.getValue());
180 assertEquals(0, handler.attempts());
184 public void failedSchemaCausesReconnect() throws Exception {
185 assertSuccessfulConnect();
186 assertEquals(1, handler.attempts());
188 // Note: this will count as a second attempt
189 doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), anyLong(), any());
191 handler.onDeviceFailed(new AssertionError("schema failure"));
193 assertEquals(2, handler.attempts());
195 // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
196 timerCaptor.getValue().run(timeout);
197 verify(clientFactory, times(2)).createClient(any());
198 assertEquals(2, handler.attempts());
202 public void downAfterUpCausesReconnect() throws Exception {
203 // Let's borrow common bits
204 successfulOnDeviceConnectedPropagates();
206 // when the device is connected, we propagate the information and initiate reconnect
207 doNothing().when(delegate).onDeviceDisconnected();
208 doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), eq(100L), eq(TimeUnit.MILLISECONDS));
209 handler.onDeviceDisconnected();
211 assertEquals(1, handler.attempts());
213 // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
214 timerCaptor.getValue().run(timeout);
215 verify(clientFactory, times(2)).createClient(any());
216 assertEquals(1, handler.attempts());
220 public void socketFailuresAreRetried() throws Exception {
221 final var firstFuture = SettableFuture.create();
222 final var secondFuture = SettableFuture.create();
223 doReturn(firstFuture, secondFuture).when(clientFactory).createClient(any());
225 assertEquals(1, handler.attempts());
227 doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), eq(150L), eq(TimeUnit.MILLISECONDS));
228 firstFuture.setException(new AssertionError("first"));
230 assertEquals(2, handler.attempts());
232 // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
233 timerCaptor.getValue().run(timeout);
234 verify(clientFactory, times(2)).createClient(any());
235 assertEquals(2, handler.attempts());
237 // now report the second failure
238 final var throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
239 doNothing().when(delegate).onDeviceFailed(throwableCaptor.capture());
240 secondFuture.setException(new AssertionError("second"));
241 assertThat(throwableCaptor.getValue(), instanceOf(ConnectGivenUpException.class));
243 // but nothing else happens
244 assertEquals(2, handler.attempts());
247 // Initiate connect() which results in immediate clientDispatcher report. No interactions with delegate may occur,
248 // as this is just a prelude to a follow-up callback
249 private void assertSuccessfulConnect() throws Exception {
250 doReturn(Futures.immediateFuture(clientSession)).when(clientFactory).createClient(any());
252 verify(clientFactory).createClient(any());
253 verifyNoInteractions(delegate);