6f69c84e32530a64ef8e0f180229c151a6cfd14f
[netconf.git] / netconf / sal-netconf-connector / src / test / java / org / opendaylight / netconf / sal / connect / netconf / listener / NetconfDeviceCommunicatorTest.java
1 /*
2  * Copyright (c) 2014 Brocade Communications Systems, Inc. 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
9 package org.opendaylight.netconf.sal.connect.netconf.listener;
10
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;
72
73 public class NetconfDeviceCommunicatorTest {
74
75     @Mock
76     NetconfClientSession mockSession;
77
78     @Mock
79     RemoteDevice<NetconfSessionPreferences, NetconfMessage, NetconfDeviceCommunicator> mockDevice;
80
81     NetconfDeviceCommunicator communicator;
82
83     @Before
84     public void setUp() throws Exception {
85         MockitoAnnotations.initMocks( this );
86
87         communicator = new NetconfDeviceCommunicator( new RemoteDeviceId( "test", InetSocketAddress.createUnresolved("localhost", 22)), mockDevice);
88     }
89
90     void setupSession() {
91         doReturn(Collections.<String>emptySet()).when(mockSession).getServerCapabilities();
92         doNothing().when(mockDevice).onRemoteSessionUp(any(NetconfSessionPreferences.class),
93                 any(NetconfDeviceCommunicator.class));
94         communicator.onSessionUp(mockSession);
95     }
96
97     private ListenableFuture<RpcResult<NetconfMessage>> sendRequest() throws Exception {
98         return sendRequest( UUID.randomUUID().toString() );
99     }
100
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 );
108
109         ChannelFuture mockChannelFuture = mock( ChannelFuture.class );
110         doReturn( mockChannelFuture ).when( mockChannelFuture )
111             .addListener( any( (GenericFutureListener.class ) ) );
112         doReturn( mockChannelFuture ).when( mockSession ).sendMessage( same( message ) );
113
114         ListenableFuture<RpcResult<NetconfMessage>> resultFuture =
115                                       communicator.sendRequest( message, QName.create( "mock rpc" ) );
116
117         assertNotNull( "ListenableFuture is null", resultFuture );
118         return resultFuture;
119     }
120
121     @Test
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(),
127                                  testCapability );
128         doReturn( serverCapabilities ).when( mockSession ).getServerCapabilities();
129
130         ArgumentCaptor<NetconfSessionPreferences> NetconfSessionPreferences =
131                                               ArgumentCaptor.forClass( NetconfSessionPreferences.class );
132         doNothing().when( mockDevice ).onRemoteSessionUp( NetconfSessionPreferences.capture(), eq( communicator ) );
133
134         communicator.onSessionUp( mockSession );
135
136         verify( mockSession ).getServerCapabilities();
137         verify( mockDevice ).onRemoteSessionUp( NetconfSessionPreferences.capture(), eq( communicator ) );
138
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() );
148     }
149
150     @SuppressWarnings("unchecked")
151     @Test(timeout=5000)
152     public void testOnSessionDown() throws Exception {
153         setupSession();
154
155         ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest();
156         ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest();
157
158         doNothing().when( mockDevice ).onRemoteSessionDown();
159
160         communicator.onSessionDown( mockSession, new Exception( "mock ex" ) );
161
162         verifyErrorRpcResult( resultFuture1.get(), RpcError.ErrorType.TRANSPORT, "operation-failed" );
163         verifyErrorRpcResult( resultFuture2.get(), RpcError.ErrorType.TRANSPORT, "operation-failed" );
164
165         verify( mockDevice ).onRemoteSessionDown();
166
167         reset( mockDevice );
168
169         communicator.onSessionDown( mockSession, new Exception( "mock ex" ) );
170
171         verify( mockDevice, never() ).onRemoteSessionDown();
172     }
173
174     @Test
175     public void testOnSessionTerminated() throws Exception {
176         setupSession();
177
178         ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest();
179
180         doNothing().when( mockDevice ).onRemoteSessionDown();
181
182         String reasonText = "testing terminate";
183         NetconfTerminationReason reason = new NetconfTerminationReason( reasonText );
184         communicator.onSessionTerminated( mockSession, reason );
185
186         RpcError rpcError = verifyErrorRpcResult( resultFuture.get(), RpcError.ErrorType.TRANSPORT,
187                                                   "operation-failed" );
188         assertEquals( "RpcError message", reasonText, rpcError.getMessage() );
189
190         verify( mockDevice ).onRemoteSessionDown();
191     }
192
193     @Test
194     public void testClose() throws Exception {
195         communicator.close();
196         verify( mockDevice, never() ).onRemoteSessionDown();
197     }
198
199     @SuppressWarnings({ "rawtypes", "unchecked" })
200     @Test
201     public void testSendRequest() throws Exception {
202         setupSession();
203
204         NetconfMessage message = new NetconfMessage(
205                               DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() );
206         QName rpc = QName.create( "mock rpc" );
207
208         ArgumentCaptor<GenericFutureListener> futureListener =
209                                             ArgumentCaptor.forClass( GenericFutureListener.class );
210
211         ChannelFuture mockChannelFuture = mock( ChannelFuture.class );
212         doReturn( mockChannelFuture ).when( mockChannelFuture ).addListener( futureListener.capture() );
213         doReturn( mockChannelFuture ).when( mockSession ).sendMessage( same( message ) );
214
215         ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest( message, rpc );
216
217         verify( mockSession ).sendMessage( same( message ) );
218
219         assertNotNull( "ListenableFuture is null", resultFuture );
220
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 );
226
227         try {
228             resultFuture.get( 1, TimeUnit.MILLISECONDS ); // verify it's not cancelled or has an error set
229         }
230         catch( TimeoutException e ) {} // expected
231     }
232
233     @Test
234     public void testSendRequestWithNoSession() throws Exception {
235         NetconfMessage message = new NetconfMessage(
236                               DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() );
237         QName rpc = QName.create( "mock rpc" );
238
239         ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest( message, rpc );
240
241         assertNotNull( "ListenableFuture is null", resultFuture );
242
243         // Should have an immediate result
244         RpcResult<NetconfMessage> rpcResult = resultFuture.get( 3, TimeUnit.MILLISECONDS );
245
246         verifyErrorRpcResult( rpcResult, RpcError.ErrorType.TRANSPORT, "operation-failed" );
247     }
248
249     @SuppressWarnings({ "rawtypes", "unchecked" })
250     @Test
251     public void testSendRequestWithWithSendFailure() throws Exception {
252         setupSession();
253
254         NetconfMessage message = new NetconfMessage(
255                               DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() );
256         QName rpc = QName.create( "mock rpc" );
257
258         ArgumentCaptor<GenericFutureListener> futureListener =
259                                             ArgumentCaptor.forClass( GenericFutureListener.class );
260
261         ChannelFuture mockChannelFuture = mock( ChannelFuture.class );
262         doReturn( mockChannelFuture ).when( mockChannelFuture ).addListener( futureListener.capture() );
263         doReturn( mockChannelFuture ).when( mockSession ).sendMessage( same( message ) );
264
265         ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest( message, rpc );
266
267         assertNotNull( "ListenableFuture is null", resultFuture );
268
269         verify( mockChannelFuture ).addListener( futureListener.capture() );
270
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 );
276
277         // Should have an immediate result
278         RpcResult<NetconfMessage> rpcResult = resultFuture.get( 3, TimeUnit.MILLISECONDS );
279
280         RpcError rpcError = verifyErrorRpcResult( rpcResult, RpcError.ErrorType.TRANSPORT, "operation-failed" );
281         assertEquals( "RpcError message contains \"mock error\"", true,
282                     rpcError.getMessage().contains( "mock error" ) );
283     }
284
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 );
293
294         return new NetconfMessage( doc );
295     }
296
297     //Test scenario verifying whether missing message is handled
298     @Test
299     public void testOnMissingResponseMessage() throws Exception {
300
301         setupSession();
302
303         String messageID1 = UUID.randomUUID().toString();
304         ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest( messageID1 );
305
306         String messageID2 = UUID.randomUUID().toString();
307         ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest( messageID2 );
308
309         String messageID3 = UUID.randomUUID().toString();
310         ListenableFuture<RpcResult<NetconfMessage>> resultFuture3 = sendRequest( messageID3 );
311
312         //response messages 1,2 are omitted
313         communicator.onMessage( mockSession, createSuccessResponseMessage( messageID3 ) );
314
315         verifyResponseMessage( resultFuture3.get(), messageID3 );
316     }
317
318     @Test
319     public void testOnSuccessfulResponseMessage() throws Exception {
320         setupSession();
321
322         String messageID1 = UUID.randomUUID().toString();
323         ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest( messageID1 );
324
325         String messageID2 = UUID.randomUUID().toString();
326         ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest( messageID2 );
327
328         communicator.onMessage( mockSession, createSuccessResponseMessage( messageID1 ) );
329         communicator.onMessage( mockSession, createSuccessResponseMessage( messageID2 ) );
330
331         verifyResponseMessage( resultFuture1.get(), messageID1 );
332         verifyResponseMessage( resultFuture2.get(), messageID2 );
333     }
334
335     @Test
336     public void testOnResponseMessageWithError() throws Exception {
337         setupSession();
338
339         String messageID = UUID.randomUUID().toString();
340         ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest( messageID );
341
342         communicator.onMessage( mockSession, createErrorResponseMessage( messageID ) );
343
344         RpcError rpcError = verifyErrorRpcResult( resultFuture.get(), RpcError.ErrorType.RPC,
345                                                   "missing-attribute" );
346         assertEquals( "RpcError message", "Missing attribute", rpcError.getMessage() );
347
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>" ) );
354     }
355
356     /**
357      * Test whether reconnect is scheduled properly
358      */
359     @Test
360     public void testNetconfDeviceReconnectInCommunicator() throws Exception {
361         final RemoteDevice<NetconfSessionPreferences, NetconfMessage, NetconfDeviceCommunicator> device = mock(RemoteDevice.class);
362
363         final TimedReconnectStrategy timedReconnectStrategy = new TimedReconnectStrategy(GlobalEventExecutor.INSTANCE, 10000, 0, 1.0, null, 100L, null);
364         final ReconnectStrategy reconnectStrategy = spy(new ReconnectStrategy() {
365             @Override
366             public int getConnectTimeout() throws Exception {
367                 return timedReconnectStrategy.getConnectTimeout();
368             }
369
370             @Override
371             public Future<Void> scheduleReconnect(final Throwable cause) {
372                 return timedReconnectStrategy.scheduleReconnect(cause);
373             }
374
375             @Override
376             public void reconnectSuccessful() {
377                 timedReconnectStrategy.reconnectSuccessful();
378             }
379         });
380
381         final EventLoopGroup group = new NioEventLoopGroup();
382         final Timer time = new HashedWheelTimer();
383         try {
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() {
389                         @Override
390                         public ReconnectStrategy createReconnectStrategy() {
391                             return reconnectStrategy;
392                         }
393                     })
394                     .withAuthHandler(new LoginPassword("admin", "admin"))
395                     .withConnectionTimeoutMillis(10000)
396                     .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.SSH)
397                     .withSessionListener(listener)
398                     .build();
399
400             listener.initializeRemoteConnection(new NetconfClientDispatcherImpl(group, group, time), cfg);
401
402             verify(reconnectStrategy, timeout((int) TimeUnit.MINUTES.toMillis(3)).times(101)).scheduleReconnect(any(Throwable.class));
403         } finally {
404             time.stop();
405             group.shutdownGracefully();
406         }
407     }
408
409     @Test
410     public void testOnResponseMessageWithWrongMessageID() throws Exception {
411         setupSession();
412
413         String messageID = UUID.randomUUID().toString();
414         ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest( messageID );
415
416         communicator.onMessage( mockSession, createSuccessResponseMessage( UUID.randomUUID().toString() ) );
417
418         RpcError rpcError = verifyErrorRpcResult( resultFuture.get(), RpcError.ErrorType.PROTOCOL,
419                                                   "bad-attribute" );
420         assertEquals( "RpcError message non-empty", true,
421                       !Strings.isNullOrEmpty( rpcError.getMessage() ) );
422
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" ) );
429     }
430
431     private static NetconfMessage createErrorResponseMessage( final String messageID ) throws Exception {
432         String xmlStr =
433             "<rpc-reply xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"" +
434             "           message-id=\"" + messageID + "\">" +
435             "  <rpc-error>" +
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>" +
440             "    <error-info>" +
441             "      <bad-attribute>foo</bad-attribute>" +
442             "      <bad-element>bar</bad-element>" +
443             "    </error-info>" +
444             "  </rpc-error>" +
445             "</rpc-reply>";
446
447         ByteArrayInputStream bis = new ByteArrayInputStream( xmlStr.getBytes() );
448         Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( bis );
449         return new NetconfMessage( doc );
450     }
451
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() );
462     }
463
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() );
474
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));
479         return rpcError;
480     }
481 }