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.junit.Assert.assertTrue;
14 import static org.mockito.ArgumentMatchers.any;
15 import static org.mockito.ArgumentMatchers.anyLong;
16 import static org.mockito.ArgumentMatchers.eq;
17 import static org.mockito.Mockito.doNothing;
18 import static org.mockito.Mockito.doReturn;
19 import static org.mockito.Mockito.mock;
20 import static org.mockito.Mockito.times;
21 import static org.mockito.Mockito.verify;
22 import static org.mockito.Mockito.verifyNoInteractions;
24 import com.google.common.net.InetAddresses;
25 import com.google.common.util.concurrent.Futures;
26 import com.google.common.util.concurrent.SettableFuture;
27 import io.netty.util.Timeout;
28 import io.netty.util.TimerTask;
29 import java.net.InetSocketAddress;
30 import java.util.List;
31 import java.util.concurrent.TimeUnit;
32 import org.junit.After;
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.NetconfClientFactoryImpl;
45 import org.opendaylight.netconf.client.NetconfClientSession;
46 import org.opendaylight.netconf.client.mdsal.NetconfDeviceCapabilities;
47 import org.opendaylight.netconf.client.mdsal.NetconfDeviceSchema;
48 import org.opendaylight.netconf.client.mdsal.api.BaseNetconfSchemaProvider;
49 import org.opendaylight.netconf.client.mdsal.api.CredentialProvider;
50 import org.opendaylight.netconf.client.mdsal.api.DeviceActionFactory;
51 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
52 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceHandler;
53 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
54 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceServices;
55 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceServices.Rpcs;
56 import org.opendaylight.netconf.client.mdsal.api.SchemaResourceManager;
57 import org.opendaylight.netconf.client.mdsal.api.SslContextFactoryProvider;
58 import org.opendaylight.netconf.client.mdsal.impl.DefaultBaseNetconfSchemaProvider;
59 import org.opendaylight.netconf.common.NetconfTimer;
60 import org.opendaylight.netconf.common.impl.DefaultNetconfTimer;
61 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
62 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress;
63 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Ipv4Address;
64 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
65 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.KeyAuthBuilder;
66 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.LoginPwUnencryptedBuilder;
67 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.key.auth.KeyBasedBuilder;
68 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.login.pw.unencrypted.LoginPasswordUnencryptedBuilder;
69 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev231121.NetconfNodeBuilder;
70 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
71 import org.opendaylight.yangtools.yang.common.Decimal64;
72 import org.opendaylight.yangtools.yang.common.Uint16;
73 import org.opendaylight.yangtools.yang.common.Uint32;
74 import org.opendaylight.yangtools.yang.data.api.schema.MountPointContext;
75 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
76 import org.opendaylight.yangtools.yang.parser.impl.DefaultYangParserFactory;
78 @RunWith(MockitoJUnitRunner.StrictStubs.class)
79 public class NetconfNodeHandlerTest {
80 private static final RemoteDeviceId DEVICE_ID = new RemoteDeviceId("netconf-topology",
81 new InetSocketAddress(InetAddresses.forString("127.0.0.1"), 9999));
82 private static final NodeId NODE_ID = new NodeId("testing-node");
84 private static BaseNetconfSchemaProvider BASE_SCHEMAS;
88 private NetconfTimer timer;
90 private SchemaResourceManager schemaManager;
92 private DeviceActionFactory deviceActionFactory;
94 private RemoteDeviceHandler delegate;
96 // DefaultNetconfClientConfigurationBuilderFactory setup
98 private SslContextFactoryProvider sslContextFactoryProvider;
100 private AAAEncryptionService encryptionService;
102 private CredentialProvider credentialProvider;
104 // Mock client dispatcher-related things
106 private NetconfClientFactory clientFactory;
108 private NetconfClientSession clientSession;
110 private ArgumentCaptor<NetconfDeviceSchema> schemaCaptor;
112 private ArgumentCaptor<NetconfSessionPreferences> prefsCaptor;
114 private ArgumentCaptor<RemoteDeviceServices> servicesCaptor;
116 // Mock Timer-related things
118 private Timeout timeout;
120 private ArgumentCaptor<TimerTask> timerCaptor;
122 private EffectiveModelContext schemaContext;
124 private NetconfTopologySchemaAssembler schemaAssembler;
125 private NetconfNodeHandler handler;
128 public static void beforeClass() throws Exception {
129 BASE_SCHEMAS = new DefaultBaseNetconfSchemaProvider(new DefaultYangParserFactory());
133 public static void afterClass() throws Exception {
138 public void before() {
139 schemaAssembler = new NetconfTopologySchemaAssembler(1, 1, 0, TimeUnit.SECONDS);
141 // Instantiate the handler
142 handler = new NetconfNodeHandler(clientFactory, timer, BASE_SCHEMAS, schemaManager, schemaAssembler,
143 new NetconfClientConfigurationBuilderFactoryImpl(encryptionService, credentialProvider,
144 sslContextFactoryProvider),
145 deviceActionFactory, delegate, DEVICE_ID, NODE_ID, new NetconfNodeBuilder()
146 .setHost(new Host(new IpAddress(new Ipv4Address("127.0.0.1"))))
147 .setPort(new PortNumber(Uint16.valueOf(9999)))
148 .setReconnectOnChangedSchema(true)
151 .setBackoffMultiplier(Decimal64.valueOf("1.5"))
152 .setConcurrentRpcLimit(Uint16.ONE)
153 // One reconnection attempt
154 .setMaxConnectionAttempts(Uint32.TWO)
155 .setDefaultRequestTimeoutMillis(Uint32.valueOf(1000))
156 .setMinBackoffMillis(Uint16.valueOf(100))
157 .setKeepaliveDelay(Uint32.valueOf(1000))
158 .setConnectionTimeoutMillis(Uint32.valueOf(1000))
159 .setMaxBackoffMillis(Uint32.valueOf(1000))
160 .setBackoffJitter(Decimal64.valueOf("0.0"))
161 .setCredentials(new LoginPwUnencryptedBuilder()
162 .setLoginPasswordUnencrypted(new LoginPasswordUnencryptedBuilder()
163 .setUsername("testuser")
164 .setPassword("testpassword")
171 public void after() {
172 schemaAssembler.close();
176 public void successfulOnDeviceConnectedPropagates() throws Exception {
177 assertSuccessfulConnect();
178 assertEquals(1, handler.attempts());
180 final var schema = new NetconfDeviceSchema(NetconfDeviceCapabilities.empty(),
181 MountPointContext.of(schemaContext));
182 final var netconfSessionPreferences = NetconfSessionPreferences.fromStrings(List.of(CapabilityURN.CANDIDATE));
183 final var deviceServices = new RemoteDeviceServices(mock(Rpcs.Normalized.class), null);
185 // when the device is connected, we propagate the information
186 doNothing().when(delegate).onDeviceConnected(schemaCaptor.capture(), prefsCaptor.capture(),
187 servicesCaptor.capture());
188 handler.onDeviceConnected(schema, netconfSessionPreferences, deviceServices);
190 assertEquals(schema, schemaCaptor.getValue());
191 assertEquals(netconfSessionPreferences, prefsCaptor.getValue());
192 assertEquals(deviceServices, servicesCaptor.getValue());
193 assertEquals(0, handler.attempts());
197 public void failedSchemaCausesReconnect() throws Exception {
198 assertSuccessfulConnect();
199 assertEquals(1, handler.attempts());
201 // Note: this will count as a second attempt
202 doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), anyLong(), any());
204 handler.onDeviceFailed(new AssertionError("schema failure"));
206 assertEquals(2, handler.attempts());
208 // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
209 timerCaptor.getValue().run(timeout);
210 verify(clientFactory, times(2)).createClient(any());
211 assertEquals(2, handler.attempts());
215 public void downAfterUpCausesReconnect() throws Exception {
216 // Let's borrow common bits
217 successfulOnDeviceConnectedPropagates();
219 // when the device is connected, we propagate the information and initiate reconnect
220 doNothing().when(delegate).onDeviceDisconnected();
221 doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), eq(100L), eq(TimeUnit.MILLISECONDS));
222 handler.onDeviceDisconnected();
224 assertEquals(1, handler.attempts());
226 // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
227 timerCaptor.getValue().run(timeout);
228 verify(clientFactory, times(2)).createClient(any());
229 assertEquals(1, handler.attempts());
233 public void socketFailuresAreRetried() throws Exception {
234 final var firstFuture = SettableFuture.create();
235 final var secondFuture = SettableFuture.create();
236 doReturn(firstFuture, secondFuture).when(clientFactory).createClient(any());
238 assertEquals(1, handler.attempts());
240 doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), eq(150L), eq(TimeUnit.MILLISECONDS));
241 firstFuture.setException(new AssertionError("first"));
243 assertEquals(2, handler.attempts());
245 // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
246 timerCaptor.getValue().run(timeout);
247 verify(clientFactory, times(2)).createClient(any());
248 assertEquals(2, handler.attempts());
250 // now report the second failure
251 final var throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
252 doNothing().when(delegate).onDeviceFailed(throwableCaptor.capture());
253 secondFuture.setException(new AssertionError("second"));
254 assertThat(throwableCaptor.getValue(), instanceOf(ConnectGivenUpException.class));
256 // but nothing else happens
257 assertEquals(2, handler.attempts());
260 // Initiate connect() which results in immediate clientDispatcher report. No interactions with delegate may occur,
261 // as this is just a prelude to a follow-up callback
262 private void assertSuccessfulConnect() throws Exception {
263 doReturn(Futures.immediateFuture(clientSession)).when(clientFactory).createClient(any());
265 verify(clientFactory).createClient(any());
266 verifyNoInteractions(delegate);
270 public void failToConnectOnUnsupportedConfiguration() {
271 final var defaultTimer = new DefaultNetconfTimer();
272 final var factory = new NetconfClientFactoryImpl(defaultTimer);
274 final var keyId = "keyId";
275 final var keyAuthHandler = new NetconfNodeHandler(factory, defaultTimer, BASE_SCHEMAS, schemaManager,
276 schemaAssembler, new NetconfClientConfigurationBuilderFactoryImpl(encryptionService, credentialProvider,
277 sslContextFactoryProvider),
278 deviceActionFactory, delegate, DEVICE_ID, NODE_ID, new NetconfNodeBuilder()
279 .setHost(new Host(new IpAddress(new Ipv4Address("127.0.0.1"))))
280 .setPort(new PortNumber(Uint16.valueOf(9999)))
281 .setReconnectOnChangedSchema(true)
285 .setBackoffMultiplier(Decimal64.valueOf("1.5"))
286 .setConcurrentRpcLimit(Uint16.ONE)
287 // One reconnection attempt
288 .setMaxConnectionAttempts(Uint32.ONE)
289 .setDefaultRequestTimeoutMillis(Uint32.valueOf(1000))
290 .setMinBackoffMillis(Uint16.valueOf(100))
291 .setKeepaliveDelay(Uint32.valueOf(1000))
292 .setConnectionTimeoutMillis(Uint32.valueOf(1000))
293 .setMaxBackoffMillis(Uint32.valueOf(1000))
294 .setBackoffJitter(Decimal64.valueOf("0.0"))
295 .setCredentials(new KeyAuthBuilder()
296 .setKeyBased(new KeyBasedBuilder()
297 .setUsername("testuser")
303 // return null when attempt to load credentials fot key id
304 doReturn(null).when(credentialProvider).credentialForId(any());
305 doNothing().when(delegate).onDeviceFailed(any());
306 keyAuthHandler.connect();
307 verify(credentialProvider).credentialForId(eq(keyId));
308 // attempt to connect fails due to unsupported configuration, and there is attempt to reconnect
309 final var captor = ArgumentCaptor.forClass(Throwable.class);
310 verify(delegate).onDeviceFailed(captor.capture());
311 assertTrue(captor.getValue() instanceof ConnectGivenUpException);
312 assertEquals(1, keyAuthHandler.attempts());