2 * Copyright (c) 2014 Brocade Communications Systems, Inc. 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
9 package org.opendaylight.netconf.sal.connect.netconf.listener;
11 import static org.junit.Assert.assertEquals;
12 import static org.junit.Assert.assertFalse;
13 import static org.junit.Assert.assertNotNull;
14 import static org.mockito.Matchers.any;
15 import static org.mockito.Matchers.eq;
16 import static org.mockito.Matchers.same;
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.never;
21 import static org.mockito.Mockito.reset;
22 import static org.mockito.Mockito.spy;
23 import static org.mockito.Mockito.timeout;
24 import static org.mockito.Mockito.verify;
25 import static org.opendaylight.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0;
26 import com.google.common.base.CharMatcher;
27 import com.google.common.base.Strings;
28 import com.google.common.collect.Sets;
29 import com.google.common.util.concurrent.ListenableFuture;
30 import io.netty.channel.ChannelFuture;
31 import io.netty.channel.EventLoopGroup;
32 import io.netty.channel.nio.NioEventLoopGroup;
33 import io.netty.util.HashedWheelTimer;
34 import io.netty.util.Timer;
35 import io.netty.util.concurrent.Future;
36 import io.netty.util.concurrent.GenericFutureListener;
37 import io.netty.util.concurrent.GlobalEventExecutor;
38 import java.io.ByteArrayInputStream;
39 import java.net.InetSocketAddress;
40 import java.util.Collection;
41 import java.util.Collections;
42 import java.util.UUID;
43 import java.util.concurrent.TimeUnit;
44 import java.util.concurrent.TimeoutException;
45 import javax.xml.parsers.DocumentBuilderFactory;
46 import javax.xml.parsers.ParserConfigurationException;
47 import org.junit.Before;
48 import org.junit.Test;
49 import org.mockito.ArgumentCaptor;
50 import org.mockito.Mock;
51 import org.mockito.MockitoAnnotations;
52 import org.opendaylight.controller.config.util.xml.XmlMappingConstants;
53 import org.opendaylight.netconf.api.NetconfMessage;
54 import org.opendaylight.netconf.api.NetconfTerminationReason;
55 import org.opendaylight.netconf.client.NetconfClientDispatcherImpl;
56 import org.opendaylight.netconf.client.NetconfClientSession;
57 import org.opendaylight.netconf.client.conf.NetconfClientConfiguration;
58 import org.opendaylight.netconf.client.conf.NetconfReconnectingClientConfiguration;
59 import org.opendaylight.netconf.client.conf.NetconfReconnectingClientConfigurationBuilder;
60 import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.LoginPassword;
61 import org.opendaylight.netconf.sal.connect.api.RemoteDevice;
62 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil;
63 import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId;
64 import org.opendaylight.protocol.framework.ReconnectStrategy;
65 import org.opendaylight.protocol.framework.ReconnectStrategyFactory;
66 import org.opendaylight.protocol.framework.TimedReconnectStrategy;
67 import org.opendaylight.yangtools.yang.common.QName;
68 import org.opendaylight.yangtools.yang.common.RpcError;
69 import org.opendaylight.yangtools.yang.common.RpcResult;
70 import org.w3c.dom.Document;
71 import org.w3c.dom.Element;
73 public class NetconfDeviceCommunicatorTest {
76 NetconfClientSession mockSession;
79 RemoteDevice<NetconfSessionPreferences, NetconfMessage, NetconfDeviceCommunicator> mockDevice;
81 NetconfDeviceCommunicator communicator;
84 public void setUp() throws Exception {
85 MockitoAnnotations.initMocks( this );
87 communicator = new NetconfDeviceCommunicator( new RemoteDeviceId( "test", InetSocketAddress.createUnresolved("localhost", 22)), mockDevice);
91 doReturn(Collections.<String>emptySet()).when(mockSession).getServerCapabilities();
92 doNothing().when(mockDevice).onRemoteSessionUp(any(NetconfSessionPreferences.class),
93 any(NetconfDeviceCommunicator.class));
94 communicator.onSessionUp(mockSession);
97 private ListenableFuture<RpcResult<NetconfMessage>> sendRequest() throws Exception {
98 return sendRequest( UUID.randomUUID().toString() );
101 @SuppressWarnings("unchecked")
102 private ListenableFuture<RpcResult<NetconfMessage>> sendRequest( final String messageID ) throws Exception {
103 Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
104 Element element = doc.createElement( "request" );
105 element.setAttribute( "message-id", messageID );
106 doc.appendChild( element );
107 NetconfMessage message = new NetconfMessage( doc );
109 ChannelFuture mockChannelFuture = mock( ChannelFuture.class );
110 doReturn( mockChannelFuture ).when( mockChannelFuture )
111 .addListener( any( (GenericFutureListener.class ) ) );
112 doReturn( mockChannelFuture ).when( mockSession ).sendMessage( same( message ) );
114 ListenableFuture<RpcResult<NetconfMessage>> resultFuture =
115 communicator.sendRequest( message, QName.create( "mock rpc" ) );
117 assertNotNull( "ListenableFuture is null", resultFuture );
122 public void testOnSessionUp() {
123 String testCapability = "urn:opendaylight:params:xml:ns:test?module=test-module&revision=2014-06-02";
124 Collection<String> serverCapabilities =
125 Sets.newHashSet( NetconfMessageTransformUtil.NETCONF_ROLLBACK_ON_ERROR_URI.toString(),
126 NetconfMessageTransformUtil.IETF_NETCONF_MONITORING.getNamespace().toString(),
128 doReturn( serverCapabilities ).when( mockSession ).getServerCapabilities();
130 ArgumentCaptor<NetconfSessionPreferences> NetconfSessionPreferences =
131 ArgumentCaptor.forClass( NetconfSessionPreferences.class );
132 doNothing().when( mockDevice ).onRemoteSessionUp( NetconfSessionPreferences.capture(), eq( communicator ) );
134 communicator.onSessionUp( mockSession );
136 verify( mockSession ).getServerCapabilities();
137 verify( mockDevice ).onRemoteSessionUp( NetconfSessionPreferences.capture(), eq( communicator ) );
139 NetconfSessionPreferences actualCapabilites = NetconfSessionPreferences.getValue();
140 assertEquals( "containsModuleCapability", true, actualCapabilites.containsNonModuleCapability(
141 NetconfMessageTransformUtil.NETCONF_ROLLBACK_ON_ERROR_URI.toString()) );
142 assertEquals( "containsModuleCapability", false, actualCapabilites.containsNonModuleCapability(testCapability) );
143 assertEquals( "getModuleBasedCaps", Sets.newHashSet(
144 QName.create( "urn:opendaylight:params:xml:ns:test", "2014-06-02", "test-module" )),
145 actualCapabilites.getModuleBasedCaps() );
146 assertEquals( "isRollbackSupported", true, actualCapabilites.isRollbackSupported() );
147 assertEquals( "isMonitoringSupported", true, actualCapabilites.isMonitoringSupported() );
150 @SuppressWarnings("unchecked")
152 public void testOnSessionDown() throws Exception {
155 ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest();
156 ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest();
158 doNothing().when( mockDevice ).onRemoteSessionDown();
160 communicator.onSessionDown( mockSession, new Exception( "mock ex" ) );
162 verifyErrorRpcResult( resultFuture1.get(), RpcError.ErrorType.TRANSPORT, "operation-failed" );
163 verifyErrorRpcResult( resultFuture2.get(), RpcError.ErrorType.TRANSPORT, "operation-failed" );
165 verify( mockDevice ).onRemoteSessionDown();
169 communicator.onSessionDown( mockSession, new Exception( "mock ex" ) );
171 verify( mockDevice, never() ).onRemoteSessionDown();
175 public void testOnSessionTerminated() throws Exception {
178 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest();
180 doNothing().when( mockDevice ).onRemoteSessionDown();
182 String reasonText = "testing terminate";
183 NetconfTerminationReason reason = new NetconfTerminationReason( reasonText );
184 communicator.onSessionTerminated( mockSession, reason );
186 RpcError rpcError = verifyErrorRpcResult( resultFuture.get(), RpcError.ErrorType.TRANSPORT,
187 "operation-failed" );
188 assertEquals( "RpcError message", reasonText, rpcError.getMessage() );
190 verify( mockDevice ).onRemoteSessionDown();
194 public void testClose() throws Exception {
195 communicator.close();
196 verify( mockDevice, never() ).onRemoteSessionDown();
199 @SuppressWarnings({ "rawtypes", "unchecked" })
201 public void testSendRequest() throws Exception {
204 NetconfMessage message = new NetconfMessage(
205 DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() );
206 QName rpc = QName.create( "mock rpc" );
208 ArgumentCaptor<GenericFutureListener> futureListener =
209 ArgumentCaptor.forClass( GenericFutureListener.class );
211 ChannelFuture mockChannelFuture = mock( ChannelFuture.class );
212 doReturn( mockChannelFuture ).when( mockChannelFuture ).addListener( futureListener.capture() );
213 doReturn( mockChannelFuture ).when( mockSession ).sendMessage( same( message ) );
215 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest( message, rpc );
217 verify( mockSession ).sendMessage( same( message ) );
219 assertNotNull( "ListenableFuture is null", resultFuture );
221 verify( mockChannelFuture ).addListener( futureListener.capture() );
222 Future<Void> operationFuture = mock( Future.class );
223 doReturn( true ).when( operationFuture ).isSuccess();
224 doReturn( true ).when( operationFuture ).isDone();
225 futureListener.getValue().operationComplete( operationFuture );
228 resultFuture.get( 1, TimeUnit.MILLISECONDS ); // verify it's not cancelled or has an error set
230 catch( TimeoutException e ) {} // expected
234 public void testSendRequestWithNoSession() throws Exception {
235 NetconfMessage message = new NetconfMessage(
236 DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() );
237 QName rpc = QName.create( "mock rpc" );
239 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest( message, rpc );
241 assertNotNull( "ListenableFuture is null", resultFuture );
243 // Should have an immediate result
244 RpcResult<NetconfMessage> rpcResult = resultFuture.get( 3, TimeUnit.MILLISECONDS );
246 verifyErrorRpcResult( rpcResult, RpcError.ErrorType.TRANSPORT, "operation-failed" );
249 @SuppressWarnings({ "rawtypes", "unchecked" })
251 public void testSendRequestWithWithSendFailure() throws Exception {
254 NetconfMessage message = new NetconfMessage(
255 DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() );
256 QName rpc = QName.create( "mock rpc" );
258 ArgumentCaptor<GenericFutureListener> futureListener =
259 ArgumentCaptor.forClass( GenericFutureListener.class );
261 ChannelFuture mockChannelFuture = mock( ChannelFuture.class );
262 doReturn( mockChannelFuture ).when( mockChannelFuture ).addListener( futureListener.capture() );
263 doReturn( mockChannelFuture ).when( mockSession ).sendMessage( same( message ) );
265 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest( message, rpc );
267 assertNotNull( "ListenableFuture is null", resultFuture );
269 verify( mockChannelFuture ).addListener( futureListener.capture() );
271 Future<Void> operationFuture = mock( Future.class );
272 doReturn( false ).when( operationFuture ).isSuccess();
273 doReturn( true ).when( operationFuture ).isDone();
274 doReturn( new Exception( "mock error" ) ).when( operationFuture ).cause();
275 futureListener.getValue().operationComplete( operationFuture );
277 // Should have an immediate result
278 RpcResult<NetconfMessage> rpcResult = resultFuture.get( 3, TimeUnit.MILLISECONDS );
280 RpcError rpcError = verifyErrorRpcResult( rpcResult, RpcError.ErrorType.TRANSPORT, "operation-failed" );
281 assertEquals( "RpcError message contains \"mock error\"", true,
282 rpcError.getMessage().contains( "mock error" ) );
285 private static NetconfMessage createSuccessResponseMessage( final String messageID ) throws ParserConfigurationException {
286 Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
287 Element rpcReply = doc.createElementNS( URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0, XmlMappingConstants.RPC_REPLY_KEY);
288 rpcReply.setAttribute( "message-id", messageID );
289 Element element = doc.createElementNS( "ns", "data" );
290 element.setTextContent( messageID );
291 rpcReply.appendChild( element );
292 doc.appendChild( rpcReply );
294 return new NetconfMessage( doc );
297 //Test scenario verifying whether missing message is handled
299 public void testOnMissingResponseMessage() throws Exception {
303 String messageID1 = UUID.randomUUID().toString();
304 ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest( messageID1 );
306 String messageID2 = UUID.randomUUID().toString();
307 ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest( messageID2 );
309 String messageID3 = UUID.randomUUID().toString();
310 ListenableFuture<RpcResult<NetconfMessage>> resultFuture3 = sendRequest( messageID3 );
312 //response messages 1,2 are omitted
313 communicator.onMessage( mockSession, createSuccessResponseMessage( messageID3 ) );
315 verifyResponseMessage( resultFuture3.get(), messageID3 );
319 public void testOnSuccessfulResponseMessage() throws Exception {
322 String messageID1 = UUID.randomUUID().toString();
323 ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest( messageID1 );
325 String messageID2 = UUID.randomUUID().toString();
326 ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest( messageID2 );
328 communicator.onMessage( mockSession, createSuccessResponseMessage( messageID1 ) );
329 communicator.onMessage( mockSession, createSuccessResponseMessage( messageID2 ) );
331 verifyResponseMessage( resultFuture1.get(), messageID1 );
332 verifyResponseMessage( resultFuture2.get(), messageID2 );
336 public void testOnResponseMessageWithError() throws Exception {
339 String messageID = UUID.randomUUID().toString();
340 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest( messageID );
342 communicator.onMessage( mockSession, createErrorResponseMessage( messageID ) );
344 RpcError rpcError = verifyErrorRpcResult( resultFuture.get(), RpcError.ErrorType.RPC,
345 "missing-attribute" );
346 assertEquals( "RpcError message", "Missing attribute", rpcError.getMessage() );
348 String errorInfo = rpcError.getInfo();
349 assertNotNull( "RpcError info is null", errorInfo );
350 assertEquals( "Error info contains \"foo\"", true,
351 errorInfo.contains( "<bad-attribute>foo</bad-attribute>" ) );
352 assertEquals( "Error info contains \"bar\"", true,
353 errorInfo.contains( "<bad-element>bar</bad-element>" ) );
357 * Test whether reconnect is scheduled properly
360 public void testNetconfDeviceReconnectInCommunicator() throws Exception {
361 final RemoteDevice<NetconfSessionPreferences, NetconfMessage, NetconfDeviceCommunicator> device = mock(RemoteDevice.class);
363 final TimedReconnectStrategy timedReconnectStrategy = new TimedReconnectStrategy(GlobalEventExecutor.INSTANCE, 10000, 0, 1.0, null, 100L, null);
364 final ReconnectStrategy reconnectStrategy = spy(new ReconnectStrategy() {
366 public int getConnectTimeout() throws Exception {
367 return timedReconnectStrategy.getConnectTimeout();
371 public Future<Void> scheduleReconnect(final Throwable cause) {
372 return timedReconnectStrategy.scheduleReconnect(cause);
376 public void reconnectSuccessful() {
377 timedReconnectStrategy.reconnectSuccessful();
381 final EventLoopGroup group = new NioEventLoopGroup();
382 final Timer time = new HashedWheelTimer();
384 final NetconfDeviceCommunicator listener = new NetconfDeviceCommunicator(new RemoteDeviceId("test", InetSocketAddress.createUnresolved("localhost", 22)), device);
385 final NetconfReconnectingClientConfiguration cfg = NetconfReconnectingClientConfigurationBuilder.create()
386 .withAddress(new InetSocketAddress("localhost", 65000))
387 .withReconnectStrategy(reconnectStrategy)
388 .withConnectStrategyFactory(new ReconnectStrategyFactory() {
390 public ReconnectStrategy createReconnectStrategy() {
391 return reconnectStrategy;
394 .withAuthHandler(new LoginPassword("admin", "admin"))
395 .withConnectionTimeoutMillis(10000)
396 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.SSH)
397 .withSessionListener(listener)
400 listener.initializeRemoteConnection(new NetconfClientDispatcherImpl(group, group, time), cfg);
402 verify(reconnectStrategy, timeout((int) TimeUnit.MINUTES.toMillis(3)).times(101)).scheduleReconnect(any(Throwable.class));
405 group.shutdownGracefully();
410 public void testOnResponseMessageWithWrongMessageID() throws Exception {
413 String messageID = UUID.randomUUID().toString();
414 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest( messageID );
416 communicator.onMessage( mockSession, createSuccessResponseMessage( UUID.randomUUID().toString() ) );
418 RpcError rpcError = verifyErrorRpcResult( resultFuture.get(), RpcError.ErrorType.PROTOCOL,
420 assertEquals( "RpcError message non-empty", true,
421 !Strings.isNullOrEmpty( rpcError.getMessage() ) );
423 String errorInfo = rpcError.getInfo();
424 assertNotNull( "RpcError info is null", errorInfo );
425 assertEquals( "Error info contains \"actual-message-id\"", true,
426 errorInfo.contains( "actual-message-id" ) );
427 assertEquals( "Error info contains \"expected-message-id\"", true,
428 errorInfo.contains( "expected-message-id" ) );
431 private static NetconfMessage createErrorResponseMessage( final String messageID ) throws Exception {
433 "<rpc-reply xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"" +
434 " message-id=\"" + messageID + "\">" +
436 " <error-type>rpc</error-type>" +
437 " <error-tag>missing-attribute</error-tag>" +
438 " <error-severity>error</error-severity>" +
439 " <error-message>Missing attribute</error-message>" +
441 " <bad-attribute>foo</bad-attribute>" +
442 " <bad-element>bar</bad-element>" +
447 ByteArrayInputStream bis = new ByteArrayInputStream( xmlStr.getBytes() );
448 Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( bis );
449 return new NetconfMessage( doc );
452 private static void verifyResponseMessage( final RpcResult<NetconfMessage> rpcResult, final String dataText ) {
453 assertNotNull( "RpcResult is null", rpcResult );
454 assertEquals( "isSuccessful", true, rpcResult.isSuccessful() );
455 NetconfMessage messageResult = rpcResult.getResult();
456 assertNotNull( "getResult", messageResult );
457 // List<SimpleNode<?>> nodes = messageResult.getSimpleNodesByName(
458 // QName.create( URI.create( "ns" ), null, "data" ) );
459 // assertNotNull( "getSimpleNodesByName", nodes );
460 // assertEquals( "List<SimpleNode<?>> size", 1, nodes.size() );
461 // assertEquals( "SimpleNode value", dataText, nodes.iterator().next().getValue() );
464 private static RpcError verifyErrorRpcResult( final RpcResult<NetconfMessage> rpcResult,
465 final RpcError.ErrorType expErrorType, final String expErrorTag ) {
466 assertNotNull( "RpcResult is null", rpcResult );
467 assertEquals( "isSuccessful", false, rpcResult.isSuccessful() );
468 assertNotNull( "RpcResult errors is null", rpcResult.getErrors() );
469 assertEquals( "Errors size", 1, rpcResult.getErrors().size() );
470 RpcError rpcError = rpcResult.getErrors().iterator().next();
471 assertEquals( "getErrorSeverity", RpcError.ErrorSeverity.ERROR, rpcError.getSeverity() );
472 assertEquals( "getErrorType", expErrorType, rpcError.getErrorType() );
473 assertEquals( "getErrorTag", expErrorTag, rpcError.getTag() );
475 final String msg = rpcError.getMessage();
476 assertNotNull("getMessage is null", msg);
477 assertFalse("getMessage is empty", msg.isEmpty());
478 assertFalse("getMessage is blank", CharMatcher.WHITESPACE.matchesAllOf(msg));