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.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;
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;
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");
78 private static BaseNetconfSchemas BASE_SCHEMAS;
82 private ScheduledExecutorService keepaliveExecutor;
84 private SchemaResourceManager schemaManager;
86 private Executor processingExecutor;
88 private DeviceActionFactory deviceActionFactory;
90 private RemoteDeviceHandler delegate;
92 // DefaultNetconfClientConfigurationBuilderFactory setup
94 private SslHandlerFactoryProvider sslHandlerFactoryProvider;
96 private AAAEncryptionService encryptionService;
98 private CredentialProvider credentialProvider;
100 // Mock client dispatcher-related things
102 private NetconfClientDispatcher clientDispatcher;
104 private NetconfClientSession clientSession;
106 private ArgumentCaptor<NetconfDeviceSchema> schemaCaptor;
108 private ArgumentCaptor<NetconfSessionPreferences> prefsCaptor;
110 private ArgumentCaptor<RemoteDeviceServices> servicesCaptor;
112 // Mock eventExecutor-related things
114 private EventExecutor eventExecutor;
116 private ScheduledFuture<?> scheduleFuture;
118 private ArgumentCaptor<Runnable> scheduleCaptor;
120 private EffectiveModelContext schemaContext;
122 private NetconfNodeHandler handler;
125 public static void beforeClass() throws Exception {
126 BASE_SCHEMAS = new DefaultBaseNetconfSchemas(new DefaultYangParserFactory());
130 public static void afterClass() throws Exception {
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)
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())
160 public void successfullOnDeviceConnectedPropagates() {
161 assertSuccessfulConnect();
162 assertEquals(1, handler.attempts());
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);
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);
174 assertEquals(schema, schemaCaptor.getValue());
175 assertEquals(netconfSessionPreferences, prefsCaptor.getValue());
176 assertEquals(deviceServices, servicesCaptor.getValue());
177 assertEquals(0, handler.attempts());
181 public void failedSchemaCausesReconnect() {
182 assertSuccessfulConnect();
183 assertEquals(1, handler.attempts());
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"));
190 assertEquals(2, handler.attempts());
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());
199 * Tests the initialization of the NetconfNodeHandler's client configuration.
202 * Ensures that no connection attempts are made by the handler after calling connect().
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)
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())
222 handler = new NetconfNodeHandler(clientDispatcher, eventExecutor, keepaliveExecutor, BASE_SCHEMAS,
223 schemaManager, processingExecutor, new DefaultNetconfClientConfigurationBuilderFactory(encryptionService,
225 sslHandlerFactoryProvider), deviceActionFactory, delegate, DEVICE_ID, NODE_ID,
228 // Expect 0 connection attempts since the clientConfig has not been initialized.
229 assertEquals(0, handler.attempts());
233 public void downAfterUpCausesReconnect() {
234 // Let's borrow common bits
235 successfullOnDeviceConnectedPropagates();
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();
243 assertEquals(1, handler.attempts());
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());
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());
257 assertEquals(1, handler.attempts());
259 doReturn(scheduleFuture).when(eventExecutor).schedule(scheduleCaptor.capture(), eq(150L),
260 eq(TimeUnit.MILLISECONDS));
261 firstPromise.setFailure(new AssertionError("first"));
263 assertEquals(2, handler.attempts());
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());
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));
276 // but nothing else happens
277 assertEquals(2, handler.attempts());
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());
286 verify(clientDispatcher).createClient(any());
287 verifyNoInteractions(delegate);